REVERSE (NSSCTF)

TIPS

DLL文件用ilspy或者dnspy

1.1求乘法逆元

求乘法逆元用invert函数,代码实现:

from gmpy2 import invert
x = 0xccffbbbb
p = 0xffffffff+1
print(hex(invert(x, p)))
#include<stdio.h>
int main()
{
int s[]={0xb5073388 , 0xf58ea46f , 0x8cd2d760 , 0x7fc56cda , 0x52bc07da , 0x29054b48 ,
0x42d74750 , 0x11297e95 , 0x5cf2821b , 0x747970da , 0x64793c81};
for(int i=0;i<11;i++)
{
s[i]=((s[i]^(0xd3906+0xdeadbeef))-0xdeadc0de)*0x8d61d173;
//data[i] = ((data[i] ^ (0xdeadbeef + 0xd3906)) - 0xdeadc0de) * 0x8d61d173;
}
unsigned char *k=(unsigned char *)s;
for(int i=0;i<44;i++)
{
printf("%c",*(k+i));
}
}

当数过大就用c代码转换

1.2fromhex函数

bytes对象的hex函数,用来将bytes对象的值转换成hexstr;
而fromhex函数,用来将hexstr导入bytes对象,相当于用hexstr来创建bytes对象。
bytes对象的这两个函数,有的时候非常好用!

bytes([0,1,2,3,4,5]).hex()
‘000102030405’
bytes.fromhex(‘000102030405’)
b’\x00\x01\x02\x03\x04\x05’
b’abcde’.hex()
‘6162636465’
a = bytes.fromhex(‘6162636465’)
a
b’abcde’

参数的顺序要对齐声明的顺序

1.3 注意exe对应win

elf对应Ubuntu

在查看程序位数的时候就要注意

1.4 Anaconda Prompt 改python环境

搜索Anaconda Prompt

1.conda info 查看当前环境的信息

2.conda info -e 查看已经创建的所有虚拟环境

3.conda activate xx 切换到xx虚拟环境

4.set CONDA_FORCE_32BIT=1 # 切换到32位
set CONDA_FORCE_32BIT=0 # 切换到64位

5.conda create -n xxx python=2.7 创建一个python2.7 名为xxx的虚拟环境,如要创建32位的python环境,先设置为32位在创建环境,这样创建好的环境即为32位的Python环境,先切换到创建好的环境中
然后输入python 检查下是否为32位的python2.7版本,这样即创好了

6.conda remove -n env_name –all 移除环境,也可在Anaconda Navigator中移除

Anaconda Navigator为可视化管理软件

1.5 md5加密

import hashlib
str='ssssddssaassddddwwwwddwwddddddwwddddddssddwwddddddddssssaawwaassaassaassddssaassaawwwwwwaaaaaaaassaassddddwwddssddssssaassddssssaaaaaawwddwwaawwwwaassssssssssssddddssddssddddddddwwaaaaaawwwwddssddwwwwwwwwddssddssssssssddddss'

md5 = hashlib.md5() # 创建md5加密对象
md5.update(str.encode('utf-8')) # 指定需要加密的字符串
str_md5 = md5.hexdigest() # 加密后的字符串

print(str_md5) # 结果:e10adc3949ba59abbe56e057f20f883e

1.BASE64 换编码表

首先 F5 观察

发现401570函数

image-20240429214556149

可能是base64编码

Base64 是一种基于 64 个可打印字符来表示二进制数据的表示方法。由于 2⁶ = 64 ,所以每 6 个比特为一个单元,对应某个可打印字符。3 个字节有 24 个比特,对应于 4 个 base64 单元,即 3 个字节可由 4 个可打印字符来表示。如果要编码的字节数不能被 3 整除,最后会多出 1 个或 2 个字节,那么可以使用下面的方法进行处理:先使用 0 字节值在末尾补足,使其能够被 3 整除,然后再进行 base64 的编码。

image-20240429215250152

自定义字符映射

import base64
import string

str1 = "5Mc58bPHLiAx7J8ocJIlaVUxaJvMcoYMaoPMaOfg15c475tscHfM/8=="
string1 = "qvEJAfHmUYjBac+u8Ph5n9Od17FrICL/X0gVtM4Qk6T2z3wNSsyoebilxWKGZpRD" #自定义base加密表
string2 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"

print(base64.b64decode(str1.translate(str.maketrans(string1,string2))))

shift+F12 查找字符串

  1. 将待转换的字符串,每 3 个字节分为一组,每个字节占 8 bit,共 24 个二进制位
  2. 将上面的 24 个二进制位,每 6 个字节做为一组,共分为 4 组 (若最后一组字符数不足三个,用 ‘=’ 补充)
  3. 在每组前面添加两个 0,每组由 6 个变为 8 个二进制位,总共 32 个二进制位,即 4 个字节
  4. 根据 Base64 编码对照表获得对应的值

Base64 算法解码过程
去掉所有的等号,查表将字符转为二进制的索引值,最后每 8 位一组计算 ASCii 码还原字符,不足 8 位则丢弃

原始 Base64 码表ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/

Base64 & Base32 & Base16
Base64 就是用每 6 位二进制(2 的 6 次幂就是 64) 来表示一个字符
Base32 就是用每 5 位二进制(2 的 5 次幂就是 32) 来表示一个字符
Base16 就是用每 4 位二进制(2 的 4 次幂就是 16) 来表示一个字符

问:Base64 为什么使用 3 个字节作为一组呢?
因为 6 和 8 的最小公倍数为 24,三个字节正好 24 个二进制位,每 6 bit 为一组,恰好能够分为 4 组

特点

  1. Base64 要用到 Base64 码表,可以在程序中找到连续的字符串:ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/
  2. 根据 Base64 加密原理,代码中必然存在根据余数个数判断是否添加等号的代码
    字符 '=' 的 ASCii 码:61(0x3D),也有可能直接索引码表里面的 '='
  3. 识别代码中对数据的左移右移操作
    ((a[0] & 3) << 4) | (a[1] >> 4 )(16 * (a[0] & 3)) | (a[1] / 16) 是等价操作,都表示取 a[0] 后 2 位与 a[1] 前 4 位拼接,是 Base64 中的常见操作
  4. 最主要的是理解编码解码原理,比如编码时通常都会用 3 个字节一组来处理比特位数据

原理

以下图的表格为示例

image-20240507175406955

具体分析一下整个过程:

  1. 第 1 步,根据 'M''a''n' 对应的 ASCii 码值分别为 77,97,110,对应的二进制值是:01001101、01100001、01101110,由此组成一个 24 位的二进制字符串
  2. 第 2 步,如图红色框,将 24 位每 6 位二进制位一组分成 4 组
  3. 第 3 步,在上面每一组前面补两个 0,扩展成 32 个二进制位:00010011、00010110、00000101、00101110
  4. 第 4 步,四组 8bit 分别对应的值 (Base64 编码索引) 为:19、22、5、46,在 Base64 编码表中进行查找,分别对应:'T''W''F''u',因此 “Man” 经过 Base64 编码之后就变为:"TWFu"

如果遇到位数不足的情况,位数不足用 ‘=’ 补充,总共有两种情况:

  1. 最后一组只有一个字符
  2. 最后一组有两个字符

image-20240507175423170

解码代码

# coding:utf-8
s = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" # 原始码表


def my_base64_encode(inputs):
try:
# 将字符串转化为2进制
bin_str = []
for i in inputs:
x = str(bin(ord(i))).replace('0b', '')
bin_str.append('{:0>8}'.format(x))
# : 开始一个格式说明符。
# 0 表示用0来填充字符串。
# > 表示右对齐。
# 8 表示总宽度为8个字符
# .format(x)则是用x的值来替换格式说明符中的占位符。

# print(bin_str)
# 输出的字符串
outputs = ""
# 不够三倍数,需补齐的次数
nums = 0
while bin_str:
# 每次取三个字符的二进制
temp_list = bin_str[:3]
if len(temp_list) != 3:
nums = 3 - len(temp_list)
while len(temp_list) < 3:
temp_list += ['0' * 8]
temp_str = "".join(temp_list)
# print(temp_str)
# 将三个8字节的二进制转换为4个十进制
temp_str_list = []
for i in range(0, 4):
temp_str_list.append(int(temp_str[i * 6:(i + 1) * 6], 2))
# print(temp_str_list)
if nums:
temp_str_list = temp_str_list[0:4 - nums]

for i in temp_str_list:
outputs += s[i]
bin_str = bin_str[3:]
outputs += nums * '='
print("加密完成:\n%s " % outputs)
except Exception as e:
print(f"加密错误: \n{e}")


def my_base64_decode(inputs):
try:
# 将字符串转化为2进制
bin_str = []
for i in inputs:
if i != '=':
x = str(bin(s.index(i))).replace('0b', '')
bin_str.append('{:0>6}'.format(x))
# print(bin_str)
# 输出的字符串
outputs = ""
nums = inputs.count('=')
while bin_str:
temp_list = bin_str[:4]
temp_str = "".join(temp_list)
# print(temp_str)
# 补足8位字节
if (len(temp_str) % 8 != 0):
temp_str = temp_str[0:-1 * nums * 2]
# 将四个6字节的二进制转换为三个字符
for i in range(0, int(len(temp_str) / 8)):
outputs += chr(int(temp_str[i * 8:(i + 1) * 8], 2))
bin_str = bin_str[4:]
print("解密完成:\n%s " % outputs)
except Exception as e:
print(f"解密错误: \n{e}")


print(" 可更换码表的 Base64 加解密系统 ")
print("*************************************")
select = input("是否更换加密的码表? (y or n) 你的选择: ")
if select == "y" or select == "yes":
s = input("在这里输入码表: ")
print(".....done, 已更改!")
else:
print("码表未做更改!")

input_str = input("输入数据: ")
print("*************************************")
my_base64_encode(input_str)
print("*************************************")
my_base64_decode(input_str)
print("*************************************", end='')

2.简单的逻辑

# flag = 'xxxxxxxxxxxxxxxxxxxxx'
s = 'wesyvbniazxchjko1973652048@$+-&*<>'
# result = ''
# for i in range(len(flag)):
# s1 = ord(flag[i])//17 //flag[i]的商 ascll码最多使用126 126//17=7
# s2 = ord(flag[i])%17 //flag[i]的余数
#则可知 flag【i】=s1*17+s2
# result += s[(s1+i)%34]+s[-(s2+i+1)%34]

# s1+i 最大只有7+21=28 %34 无所谓 s1=list[i]-i
# s2+i+1 最大为38 最小为22 s2=34-list[i+1]-i-1

# result += s[s1+i]+s[-(s2+i+1)%34]
# list[i]=s[s1+i]
#result被扩展两位
# print(result)
result = 'v0b9n1nkajz@j0c4jjo3oi1h1i937b395i5y5e0e$i'
list=[]
for i in range(0,len(result)):
list.append(0)
flag=''
for i in range(0,len(result)):
for j in range(0,len(s)):
if s[j] == result[i]:
list[i]=j
break
print(list)
#此时找到了result中对应s的位置
for i in range(len(result)//2):
x = (list[2*i]-i)*17+(34-list[2*i+1]-i-1)
flag += chr(x)
print(flag)

知识点:

ascll码最多用到了126

127是删除

image-20240504171645744

3.花指令【未被解决】

遇到一些个爆红的地址

image-20240922212444967

E9改为90

4.加壳

将程序加壳后任然可以直接执行

另一种方式是在二进制程序中植入一段代码,在运行的时候优先取得程序的控制权,之后在把控制权交还给原始代码,这样做得目的是隐藏程序真正的OEP(入口点,防止被破解,查壳就是为了找它

  • 压缩壳:特点是减小软件体积大小,加密保护不是重点。
  • 加密壳:此种类型比较多,不同壳侧重点不同,如单纯保护程序、提供注册机制、使用次数、使用限制等。

5.IDA常用快捷键

作用 快捷键
查看字符串 Shift + F12
反汇编 F5
快速查看 16 进制数的 ASCii 码对应的字符 r
在反汇编后的界面中写下注释
在反编译后伪代码的界面中写下注释 /
查看、隐藏变量的类型 ****
对着某个函数、变量按该快捷键,查看它的交叉引用 x
直接跳转到某个地址 g
更改变量的名称 n
拍摄 IDA 快照 ctrl + shift + w
嵌入脚本 shift + F2
文本搜索字符串 alt + t
将数据转换为 16 进制 h
获取数组的数据 shift + e
对数据 db/dw/dd 之间进行切换 d
转化为函数 p
将数据转化为代码 c
将数据转化为字符串 a
将代码转换为数据 u

6.IDA命名前缀

前缀 意义
sub_ 指令和子函数起点
locret_ 返回指令
loc_ 指令
off_ 数据,包含偏移量
seg_ 数据,包含段地址值
asc_ 数据,ASCII 字符串
byte_ 数据,字节(或字节数组)
word_ 数据,16 位数据(或字数组)
dword_ 数据,32 位数据(或双字数组)
qword_ 数据,64 位数据(或 4 字数组)
flt_ 浮点数据,32 位(或浮点数组)
dbl_ 浮点数,64 位(或双精度数组)
tbyte_ 浮点数,80 位(或扩展精度浮点数)
stru_ 结构体 (或结构体数组)
algn_ 对齐指示
unk_ 未处理字节

7.遇到的一些小问题

【1】在linux里运行代码

gcc 代码名 -o 可执行文件名

./可执行文件名

【2】下载下来的文件或许要在相应的环境下写出来的攻击代码才有效

比如在Linux环境下运行攻击脚本

【3】

Destination =sourse[:]

如果仅仅只是等于符号 那么就相当于是一个起别名操作 Destination和source指向了同一块地址

一个修改另一个也随之修改 加[:]就相当于是把source的副本复制给了Destination

8.python反编译

首先将.exe放入exeinfo,查壳
![](C:\Users\20515\Pictures\Screenshots\屏幕截图 2024-05-08 205359.png)
发现有Pyinstaller字样,该库可以用来打包python应用程序,生成可执行性文件。

根据其他大佬的wp 采取了下面方法来解决
**1.**工具的使用
pyinstxtractor.py、HEX编辑器、uncompyle 库

github

https://github.com/extremecoders-re/pyinstxtractor

还得是github 下载别人的老是有问题

**2.**下载完上述工具后
首先先将.exe 与pyinstxtractor.py放于同一文件夹下
在该目录打开终端,输入 python pyinstxtractor.py ez_python.exe

**3.**src文件添加.pyc的后缀,并用HEX编辑器打开src和struct文件
由于某些我也不懂的原因,直接将src添加.pyc后缀后所编译的py文件是缺少Magic Number的,所以我们要用struct文件的文件头来修复src文件的文件头

**4.**将strcut文件E3前面的11字节复制到src文件的文件头就可以了
如果不是直接以E3开头,就比较两个文件E3前面的字节,将src文件E3前面的字节改为struct文件E3前面的字节即可

**5.**回到目录下,打开控制台,输入命令 uncompyle6 src.pyc > src.py回车执行,就可以看到目录下生成了 .py 文件了
好像也可以不进行3、4步,直接执行第5步。

如果出现Unknown magic number 227 in login.pyc

当题目的python版本高于3.9 umcompyle6就不能使用了

于是有以下两种方法解决

1、在线版

https://tool.lu/pyc/

Python在线工具 可以直接将pyc文件反编译为py文件

2、离线版

进行反编译**

!!!!!!在Linux环境下使用!!!!!!

进入存放pyc的文件夹

输入命令

./pycdc 【文件名】.pyc

python2的pyc文件的前4个字节是一个固定的魔数(03 F3 0D 0A),而紧接着的后 4 个字节表示编译这个 .pyc 文件的 Python 解释器的版本号

python3的pyc文件前4个字节是固定的魔数 (33 0D 0D 0A),然后是两个字节的时间戳,标识了 .py 文件的最后修改时间, 接着是 4 个字节的源文件大小,最后是源文件名的字符串,以 null 字节结尾

注意:Python3的pyc文件头部并非固定的 16 个字节,而是一个不确定的长度,至少是 12 个字节,加上源文件名字符串的长度

获得pyc字节码 也可用010

import dis,marshal

#导入 Python 的两个标准库模块 dis 和 marshal。dis 用于反汇编字节码,marshal 用于序列化和反序列化对象

f=open("Pz.pyc","rb").read()

#以二进制只读模式打开文件 Pz.pyc

f

#将读取的字节流内容存储在变量 f 中

code=marshal.loads(f[8:])

#跳过文件头部分将字节码加载到code中

code

#查看

dis.dis(code)

不同版本的python的魔数头

PyObject_HEAD

不同的 Python 版本会有不同的 PyObject_HEAD:

Python 版本 十六进制文件头
Python 2.7 03f30d0a00000000
Python 3.0 3b0c0d0a00000000
Python 3.1 4f0c0d0a00000000
Python 3.2 6c0c0d0a00000000
Python 3.3 9e0c0d0a0000000000000000
Python 3.4 ee0c0d0a0000000000000000
Python 3.5 170d0d0a0000000000000000
Python 3.6 330d0d0a0000000000000000
Python 3.7 420d0d0a000000000000000000000000
Python 3.8 55 0d 0d 0a 00 00 00 00 00 00 00 00 00 00 00 00
Python 3.9 610d0d0a000000000000000000000000
Python 3.10 6f0d0d0a000000000000000000000000
Python 3.11 a70d0d0a000000000000000000000000

9.Z3约束求解器

能够检查逻辑表达式的可满足性,可以用来软件/硬件验证和测试。Z3 在工业应用中实际上常见于软件验证、程序分析等。然而由于功能实在强大,也被用于很多其他领域。CTF 领域来说,能够用约束求解器搞定的问题常见于密码题、二进制逆向、符号执行、Fuzzing 模糊测试等。

例如最简单的:我们知道了未知量x,y,也知道了约束条件x+y=5,那么此时我们就可以使用Z3来求解x和y的值,因为x和y的值肯定有多个解,而我们最后的flag肯定只有一个,那么我们就可以继续添加约束条件来减少解的数量,最后得出正确的flag。

Z3语法简介:

Z3在python中主要有以下数据类型:

Int –> 整型 (注意逐个列出时使用Int(),一次全部列出使用Ints())

v = [Int(f’v{i}’) for i in range(0, 16)]

Bool –> 布尔型

Array –> 数组

BitVec(‘a’,8) –> char型(其中的数字不一定是8,例如C语言中的int型可以用BitVec(‘a’,32)表示)

几个常用API

Solver():创建一个通用求解器,创建后我们可以添加我们的约束条件,进行下一步的求解。

add():添加约束条件,通常在solver()命令之后,添加的约束条件通常是一个逻辑等式。

check():通常用来判断在添加完约束条件后,来检测解的情况,有解的时候会回显sat,无解的时候会回显unsat

model():在存在解的时候,该函数会将每个限制条件所对应的解集取交集,进而得出正解。

问题:假设有两个未知数x和y,已知x+y=5且2x+3y=14,让我们求x和y分别是多少?(可能有的小伙伴会问了,你不会是个傻子吧,这还用编程计算?我口算都能算得出来,这里我只是给大家随便举了个简单的例子,大家别往心里去,主要目的是演示Z3用法 -。-)下面我们按照我们刚才的思路使用Z3进行编写:

1.设未知数:

from z3 import *
x = Int('x')
y = Int('y')
x,y = Ints('x y')

2.列方程:

s = Solver()
s.add(x+y==5)
s.add(2*x+3*y==14)

3.解方程判断是否有解

if s.check() == sat:
result = s.model()

4.输出方程的解,没有解则输出无解

	print (result)
else:
print ('无解')

10.常见的几类题型

算法逆向特征识别

  • 基于yara规则的IDA插件findcrypt3
  • AES、DES、BlowFish
  • RC4
    • 本质也是异或,特征… init,256 or 其他轮循环对 key填充,最后加密就是s盒变换对明文进行异或
    • 对称加密,写解密脚本 or 输出变输入,直接debug到cmp获取明文
  • base家族
    • 原表、变表、加密过程是否修改
  • RSA
    • 一般会选择递归快速幂实现pow函数
  • Tea、xtea、xxtea
    • Tea的本质是异或,识别一般一眼观察左右移2345,连续异或,delta

需要注意的点

  • 加密是否魔改
  • 数据格式
  • 处理过程中是否有数据丢失

迷宫题目

抓住几个要素去分析即可
  • 迷宫的样子【维度、长什么样】
  • 迷宫的约束【起点、终点、什么可走什么不可走】
  • 用哪些操作代表在迷宫中行走【怎么走】
对于解题来说
  • 小迷宫 手解
  • 中等迷宫 暴力
  • 大、高维迷宫 写算法,比如DFS、BFS

11.迷宫脚本

from collections import deque
# str为ida中使用快捷键[shift+e]提取到的数据, 如果提取的是string literal则加上引号视作字符串,如果是C array(decimal)则加上中括号视作列表
str = 'S ####### ## #### # ##### ### # ####### ## #######E#' # "字符串" / [一维列表]
s = 0 # s用作索引访问str, 供下面tmp列表取值

# 分析题目后设置迷宫的行列
row =int(input("输入二维迷宫行数:")) #
col =int(input('输入二维迷宫列数:') ) #

maze = []
for i in range(row):
tmp = []
for j in range(col):
tmp.append(str[s])
s += 1
maze.append(tmp) # 凑一行添加一行到迷宫中
print(maze)


# # 设置二维四向迷宫, 如果题目是多个小迷宫问题, 拆分多次调用脚本获取路径即可
# # maze = 二维列表迷宫
# path_len = 0x7fffffff # 如果题目未给出终点坐标,则一定会指定路径的长度,在此处修改路径长度,否则请保留path_len的极大值
#
#
# # 进行BFS寻找路径
# def bfs(start, end, barrier):
# directions = [(0, 1), (1, 0), (0, -1), (-1, 0)] # 定义四个方向的移动
# for i in range(len(maze)): # 获取起点和终点在列表中的索引
# for j in range(len(maze[i])):
# if (maze[i][j] == start):
# start = (i, j)
# if (maze[i][j] == end):
# end = (i, j)
# # 以下均是bfs算法套路
# queue = deque()
# queue.append((start, [start])) # (当前位置, 路径)
# visited = set()
# visited.add(start)
# while queue:
# position, path = queue.popleft()
# if position == end:
# return path
# elif len(path) == path_len:
# return path
# for d in directions:
# next_position = (position[0] + d[0], position[1] + d[1])
# if 0 <= next_position[0] < len(maze) and 0 <= next_position[1] < len(maze[0]) and \
# maze[next_position[0]][next_position[1]] != barrier and next_position not in visited:
# queue.append((next_position, path + [next_position]))
# visited.add(next_position)
# return None
#
#
# # 执行BFS搜索并打印结果
# if __name__ == '__main__':
# # maze[起点x坐标][起点y坐标] = 'S' #如果题目给了起点终点的坐标,在这里直接给起点和终点添加特征
# # maze[终点x坐标][终点y坐标] = 'E'
#
# path = bfs('S', 'E', 1) # bfs函数传入参数代表起点、终点、障碍的特征(若题目给出的数据无特征, 手动添加特征即可, 通常障碍是1也有可能是0或其它字符如'#')
# print("移动路径坐标:", path)
# print("移动路径方位:", end='')
# for i in range(1, len(path)):
# x1, y1, x2, y2 = path[i - 1][0], path[i - 1][1], path[i][0], path[i][1]
# if (x1 > x2): # 上
# print("w", end='')
# elif (x1 < x2): # 下
# print("s", end='')
# elif (y1 > y2): # 左
# print("a", end='')
# elif (y1 < y2): # 右
# print("d", end='')
#
from collections import deque

# 示例迷宫,使用 '#' 表示障碍,'S' 表示起点,'E' 表示终点
# maze = [
# ['#', '#', '#', '#', '#'],
# ['#', 'S', '.', '.', '#'],
# ['#', '.', '#', '.', '#'],
# ['#', '.', '.', '.', 'E'],
# ['#', '#', '#', '#', '#']
# ]

# 起点和终点的坐标
start_x, start_y = None, None
end_x, end_y = None, None

# 找到起点和终点的坐标
for i, row in enumerate(maze):
for j, cell in enumerate(row):
if cell == 'S':
start_x, start_y = i, j
elif cell == 'E':
end_x, end_y = i, j

# 如果未找到起点或终点,则退出程序
if start_x is None or end_y is None:
print("起点或终点未找到!")
exit()

# 路径长度(如果题目未给出,则使用极大值)

path_len = int(input("请输入最短路径长度(如果题目未给出,则输入0):"))
if path_len==0:
path_len = 0x7fffffff


# 进行BFS寻找路径
def bfs(start, end, maze, barrier):
directions = [(0, 1), (1, 0), (0, -1), (-1, 0)] # 定义四个方向的移动
for i in range(len(maze)): # 获取起点和终点在列表中的索引
for j in range(len(maze[i])):
if (maze[i][j] == start):
start = (i, j)
if (maze[i][j] == end):
end = (i, j)
# 以下均是bfs算法套路
queue = deque()
queue.append((start, [start])) # (当前位置, 路径)
visited = set()
visited.add(start)
while queue:
position, path = queue.popleft()
if position == end:
return path
elif len(path) == path_len:
return path
for d in directions:
next_position = (position[0] + d[0], position[1] + d[1])
if 0 <= next_position[0] < len(maze) and 0 <= next_position[1] < len(maze[0]) and \
maze[next_position[0]][next_position[1]] != barrier and next_position not in visited:
queue.append((next_position, path + [next_position]))
visited.add(next_position)
return None

# ... (其余代码与您的 bfs 函数相同)

# 执行BFS搜索并打印结果
if __name__ == '__main__':
start = (start_x, start_y)
end = (end_x, end_y)
barrier = '#' # 假设 '#' 是障碍
path = bfs(start, end, maze, barrier)
if path:
print("移动路径坐标:", path)
print("移动路径方位:", end='')
for i in range(1, len(path)):
x1, y1, x2, y2 = path[i - 1][0], path[i - 1][1], path[i][0], path[i][1]
if x1 > x2: # 上
print("w", end='')
elif x1 < x2: # 下
print("s", end='')
elif y1 > y2: # 左
print("a", end='')
elif y1 < y2: # 右
print("d", end='')
print() # 换行
else:
print("没有找到路径!")


# 下面是可能会用到的一些个字符
print()
maze_m=[]
for z in range(len(maze)):
for y in range(len(maze[0])):
maze_m.append(maze[z][y])
print(maze_m)
print()
# 画出该迷宫

for j in range(len(maze)):
print(maze[0])
maze.pop(0)

12.壳与混淆

加壳

什么是壳?

简而言之,就是在不影响程序运行的前提下,给程序套上一层能够防止别人逆向、破解、篡改的东西

  • 壳的分类方式有很多,最常见的莫过于如下这样
压缩壳
  • 从某种角度上你可以理解为他是为了减少程序体积
  • UPX、ASPack、PECompact
  • EP、OEP?
加密壳
  • 使用加密算法或其他手段对程序进行保护,同时可能会提供类似注册机制的一些额外功能
  • Themida、ASProtect
现代壳
  • VMP,三个字母诠释一切

当然,对于比赛来讲,我们更需要关心的是怎么脱壳、怎么得到flag

一般来讲CTF比赛中涉及壳的会有这么几种场景
  1. 原封不动的常见壳,基本也就upx
    • 集成脱壳工具
    • 官方工具指令脱壳
  2. 修改节区部分字节的常见壳,像UPX1变为1PX1,无法直接工具脱壳
    • 恢复字节后工具脱壳
    • 手动脱壳
  3. 写一套自己的解释器或者指令集加固,类VMP这种虚拟机壳
    • 无它,梳理对应代码指令,借助脚本进行半自动化分析

还是那句话:逆向手搓破万法

混淆

代码混淆常见的是花指令和SMC。

花指令就是在代码中插入一些垃圾数据,或是构造一些跳转语句来干扰静态调试。SMC是自修改代码,通过修改内存地址,使程序无法静态分析,但是在程序运行时会进行自解密,也就是我们调试的方法就是通过动态调试 调试到修改后的代码处,然后再dump’出来就好。

反调试
什么是反调试
  • 顾名思义,阻止他人调试程序
  • 一般会设置虚假的执行流,在通过debug检测技术检测到程序被调试后,让程序走入错误的执行流,得到错误的运行结果
  • 类似下面这样
int	CheckDebug(){						
return IsDebuggerPresent();
}

int main()
{
if (CheckDebug()){
return 0;
}
}
  • 还有像TLS回调、.init、.fina这些都经常会出反调试的一些东西,需要特别留意
常规的处理方式
  • 直接把反调试函数或者相关代码nop
  • 通过修改标志位绕过
  • 修改字节码
  • 仿写正确执行的代码逻辑,从而获取正确结果
花指令
  • 未定义–>修改字节–>重分析
LLVM
  • 现有的一些去混淆工具,像d810
  • 符号执行去混淆,比如angr deflat
SMC(Self Modifying Code)

能够在运行时修改自身的计算机程序代码。允许程序在执行过程中修改其指令或数据,以适应不同的条件或实现特定的功能,当然,对于CTF来说,就是为了隐藏真实的代码逻辑

处理思路

  • 仿写SMC的执行代码,直接patch对应位置字节码,重新静态识别
  • 下断让程序运行到SMC函数执行结束
  • 当然,如果SMC前面有反调试又不想patch,可以考虑在更后面下断,直接运行程序附加

13.UPX脱壳

一、工具脱壳

使用到了upx -3.96

先打开命令提示符

d:

cd D:\HuaweiMoveData\Users\20515\Desktop\reverse\upx-3.96-win64

upx.exe -h

然后就可以使用啦

image-20240511212327205

之后脱壳就先到D:\HuaweiMoveData\Users\20515\Desktop\reverse\upx-3.96-win64文件夹目录下

upx -d [需要脱壳的程序的地址]

二、ESP脱壳定律

ESP 定律的原理在于利用程序中堆栈平衡来快速找到 OEP.

由于在程序自解密或者自解压过程中, 不少壳会先将当前寄存器状态压栈, 如使用pushad, 在解压结束后, 会将之前的寄存器值出栈, 如使用popad. 因此在寄存器出栈时, 往往程序代码被恢复, 此时硬件断点触发. 然后在程序当前位置, 只需要少许单步操作, 就很容易到达正确的 OEP 位置.

  1. 程序刚载入开始 pushad/pushfd
  2. 将全部寄存器压栈后就设对 ESP 寄存器设硬件断点
  3. 运行程序, 触发断点
  4. 删除硬件断点开始分析

14.SMC

SMC,即Self Modifying Code,动态代码加密技术,指通过修改代码或数据,阻止别人直接静态分析,然后在动态运行程序时对代码进行解密,达到程序正常运行的效果。

在CTF中,一般适用异或的加密形式对保护的函数(或者代码片段) 进行加密和解密。

SMC的实现是需要对目标内存进行修改的,.text一般是没有写权限的。那么就需要拥有修改目标内存的权限:

在linux系统中,可以通过mprotect函数修改目标内存的权限
在Windows系统中,VirtualProtect函数实现内存权限的修改

因此也可以观察是否有这俩个函数来判断是否进行了SMC。

mprotect函数

#include <sys/mman.h>
int mprotect(void *addr, size_t len, int prot);

mprotect()系统调用修改起始位置为addr,长度为length字节的虚拟内存区域中分页上的保护。addr取值必须为分页大小的整数倍,length会被向上舍入到系统分页大小的下一个整数倍。prot参数是一个位掩码。

VirtualProtect()

#include <Memoryapi.h>
 
BOOL VirtualProtect(
  LPVOID lpAddress,
  SIZE_T dwSize,
  DWORD  flNewProtect,
  PDWORD lpflOldProtect
);

VirtualProtect()函数有4个参数,lpAddress是要改变属性的内存起始地址,dwSize是要改变属性的内存区域大小,flAllocationType是内存新的属性类型,lpflOldProtect内存原始属性类型保存地址。而flAllocationType部分值如下表。在 SMC 中常用的是 0x40。

方法一:IDA-python解密

这种适合于程序我们无法进行运行,只能静态分析,当然动态分析的时候也可以使用。

我们使用脚本来进行自动解密

作者使用的环境:IDA7.7、python3.8

给大家整理了一个解密的模板,根据题目的地址进行替换即可

import idc
addr=0x004014D0 #替换为 实际的地址
size=122 #替换为 实际加密片段的大小
for i in range(addr, addr+size):
idc.patch_byte(i, get_wide_byte(i)^0x66) #秘钥 0x66 根据实际情况替换
print("ok")
方法二:动态调试解密

如果题目可以运行,在关键代码处先下个断点,然后运行程序。再回去查看刚才的乱码有没有被修复,再进行下一步的分析。

这里需要掌握IDA的基本快捷键:U、C、P

  1. U: 在IDA Pro中,按“U”重新定义汇编,转换为字节码格式
  2. C: 在IDA Pro中按“C”,转换字节码为汇编形式
  3. P: 在IDA Pro中,按“P” 转换汇编语言为高级语言函数视图

15.RC4

RC4是对称加密算法,通过密钥key和S盒生成密钥流,明文逐字节异或S盒,同时S盒也会发生改变。所以加密与解密使用了相同的函数和密钥K。RC4加密的强度主要来源于密钥的安全性,如果密钥泄露,则能直接解密出明文。

可以容易的发现两个256循环,第一个循环给s盒赋值,第二个循环根据密钥key对S盒进行swap。

加解密代码

#include<stdio.h>
#include<string.h>

struct rc4_state
{
int x, y, m[256];
}rc4_state;

void rc4_setup( struct rc4_state *s, unsigned char *key, int length );
void rc4_crypt( struct rc4_state *s, unsigned char *data, int length );

void rc4_setup( struct rc4_state *s, unsigned char *key, int length )
{
int i, j, k, *m, a;

s->x = 0;
s->y = 0;
m = s->m;

for( i = 0; i < 256; i++ )
{
m[i] = i;
}

j = k = 0;

for( i = 0; i < 256; i++ )
{
a = m[i];
j = (unsigned char) ( j + a + key[k] );
m[i] = m[j]; m[j] = a;
if( ++k >= length ) k = 0;
}
}

void rc4_crypt( struct rc4_state *s, unsigned char *data, int length )
{
int i, x, y, *m, a, b;

x = s->x;
y = s->y;
m = s->m;

for( i = 0; i < length; i++ )
{
x = (unsigned char) ( x + 1 );
a = m[x];
y = (unsigned char) ( y + a );
m[x] = b = m[y];
m[y] = a;
data[i] ^= m[(unsigned char) ( a + b )];
}

s->x = x;
s->y = y;
}


int main()
{
struct rc4_state rc4_ctx;
char* key = "abelxuabelxu";
unsigned char content[256] = "0123456789abcdef";
//encrypt为RC4(content,key)得到的密文
unsigned char encrpyt[256] = {0x7d,0x71,0x12,0xe2,0x97,0xb1,0x24,0xef,0xc4,0xa9,0xe2,0xe3,0xab,0xf4,0x74,0xd7};
memset(&rc4_ctx,0,sizeof(rc4_state));
rc4_setup(&rc4_ctx,key,strlen(key));
rc4_crypt(&rc4_ctx,content,strlen(content));
for (int i = 0; i < strlen(content); i++)
printf("%2.2x", content[i]);
printf("\n");
rc4_setup(&rc4_ctx,key,strlen(key));
rc4_crypt(&rc4_ctx,encrpyt,strlen(encrpyt));
printf("%s\n");
printf("\n");
}

#RC4加密
def rc4(key, ciphertext):
# 初始化S盒
sbox = list(range(256))
j = 0
for i in range(256):
j = (j + sbox[i] + key[i % len(key)]) % 256
sbox[i], sbox[j] = sbox[j], sbox[i]

# 生成密钥流
i = 0
j = 0
keystream = []
for _ in range(len(ciphertext)):
i = (i + 1) % 256
j = (j + sbox[i]) % 256
sbox[i], sbox[j] = sbox[j], sbox[i]
k = sbox[(sbox[i] + sbox[j]) % 256]
keystream.append(k)
# print(keystream)

# 解密密文
plaintext = []
for i in range(len(ciphertext)):
m = ciphertext[i] ^ keystream[i]
plaintext.append(m)
print(plaintext)

# 将明文转换为字符串
return ''.join([chr(p) for p in plaintext])

# 测试
key = b"gamelab@"
ciphertext =[0xB6,0x42,0xB7,0xFC,0xF0,0xA2,0x5E,0xA9,0x3D,0x29,0x36,0x1F,0x54,0x29,
0x72,0xA8,0x63,0x32,0xF2,0x44,0x8B,0x85,0xEC,0xD,0xAD,0x3F,0x93,0xA3,0x92,
0x74,0x81,0x65,0x69,0xEC,0xE4,0x39,0x85,0xA9,0xCA,0xAF,0xB2,0xC6]
# for i in ciphertext:
# print(chr(i),end="")
plaintext = rc4(key, ciphertext)
print(plaintext)
#flag{12601b2b-2f1e-468a-ae43-92391ff76ef3}

16.TEA系列 加密算法

TEA 系列算法中均使用了一个 DELTA 常数,但 DELTA 的值对算法并无什么影响,只是为了避免不良的取值,推荐DELTA 的值取为黄金分割数 (5√-2)/2 与 232 的乘积,取整后的十六进制值为 0x9e3779B9,用于保证每一轮加密都不相同。

C语言

#include <stdio.h>

int main()
{
int key[] = {2233, 4455, 6677, 8899};
//密钥
unsigned int value[10];
value[0] = 0x1A800BDA;
value[1] = 0xF7A6219B;
value[2] = 0x491811D8;
value[3] = 0xF2013328;
value[4] = 0x156C365B;
value[5] = 0x3C6EAAD8;
value[6] = 0x84D4BF28;
value[7] = 0xF11A7EE7;
value[8] = 0x3313B252;
value[9] = 0xDD9FE279;
//明文
int dalte = 0xF462900;
//
int i = 0;
int wheel;
//
int sum = 0;

// 逆算法
for(i=8; i>=0; i--){
// 轮数
wheel = 33;

sum = dalte * (i+wheel);
while(wheel--){
sum -= dalte;
value[i+1] -= (sum + key[(sum >> 11) & 3]) ^ (value[i] + ((value[i] >> 5) ^ (16 * value[i])));
value[i] -= sum ^ (value[i+1] + ((value[i+1] >> 5) ^ (16 * value[i+1]))) ^ (sum + key[sum&3]);
}
}
for(i=0;i<=9;i++){
printf("%x", value[i]);
}

return 0;
}

Python

from ctypes import *


def encrypt(v, k):
v0 = c_uint32(v[0])
v1 = c_uint32(v[1])
sum1 = c_uint32(0)
delta = 0x61C88647
for i in range(32):
sum1.value -= delta
v0.value += ((v1.value << 4) + k[0]) ^ (v1.value + sum1.value) ^ ((v1.value >> 5) + k[1])
v1.value += ((v0.value << 4) + k[2]) ^ (v0.value + sum1.value) ^ ((v0.value >> 5) + k[3])
return v0.value, v1.value


def decrypt(v, k): 绝对正确

v0 = c_uint32(v[0])
v1 = c_uint32(v[1])
delta = 0x61C88647

for i in range(32):
sum1= 0
for j in range(0,32-i):
sum1 -=delta

v1.value -= (v0.value + sum1) ^ (16 * v0.value+ k[2]) ^ ((v0.value >> 5) + k[3])
v0.value -= (v1.value + sum1) ^ (16 * v1.value + k[0]) ^ ((v1.value >> 5) + k[1])

return v0.value, v1.value


if __name__ == '__main__':
a = [0x284C2234,0x3910C558]
k = [1702060386, 1870148662, 1634038898,1634038904]
print("加密前数据:", a)
# res = encrypt(a, k)
# print("加密后的数据:", res)
res = decrypt(a, k)
print("解密后数据:", res)
print(hex(res[0]))
print(hex(res[1]))

XTEA

C

#include<stdio.h>
#include<stdint.h>

void encipher(unsigned int num_rounds, uint32_t v[2], uint32_t const key[4]){
unsigned int i;
uint32_t v0=v[0],v1=v[1],sum=0,delta=0x9E3779B9;
for(i=0;i<num_rounds;i++){
v0+=(((v1<<4)^(v1>>5))+v1)^(sum+key[sum&3]);
sum+=delta;
v1+=(((v0<<4)^(v0>>5))+v0)^(sum+key[(sum>>11)&3]);
}
v[0]=v0;v[1]=v1;
}

void decipher(unsigned int num_rounds,uint32_t v[2],uint32_t const key[4]){
unsigned int i;
uint32_t v0=v[0],v1=v[1],delta=0x9E3779B9,sum=delta*num_rounds;
for(i=0;i<num_rounds;i++){
v1-=(((v0<<4)^(v0>>5))+v0)^(sum+key[(sum>>11)&3]);
sum-=delta;
v0-=(((v1<<4)^(v1>>5))+v1)^(sum+key[sum&3]);
}
v[0]=v0;v[1]=v1;
}

int main(){
uint32_t v[2]={1,2};
uint32_t const k[4]={2,2,3,4};
unsigned int r=32; //这里是加密轮数,自己设置
printf("加密前原始数据:%u %u\n",v[0],v[1]);
encipher(r,v,k);
printf("加密后原始数据:%u %u\n",v[0],v[1]);
decipher(r,v,k);
printf("解密后原始数据:%u %u\n",v[0],v[1]);
return 0;
}

PYTHON

from ctypes import * 
def encrypt(v,k):
v0=c_uint32(v[0])
v1=c_uint32(v[1])
sum1=c_uint32(0)
delta=0x9e3779b9
for i in range(32):
v0.value+=(((v1.value<<4)^(v1.value>>5))+v1.value)^(sum1.value+k[sum1.value&3])
sum1.value+=delta
v1.value+=(((v0.value<<4)^(v0.value>>5))+v0.value)^(sum1.value+k[(sum1.value>>11)&3])
return v0.value,v1.value

def decrypt(v,k):
v0=c_uint32(v[0])
v1=c_uint32(v[1])
delta=0x9e3779b9
sum1=c_uint32(delta*32)
for i in range(32):
v1.value-=(((v0.value<<4)^(v0.value>>5))+v0.value)^(sum1.value+k[(sum1.value>>11)&3])
sum1.value-=delta
v0.value-=(((v1.value<<4)^(v1.value>>5))+v1.value)^(sum1.value+k[sum1.value&3])
return v0.value,v1.value

if __name__=='__main__':
a=[1,2]
k=[2,2,3,4]
print("加密前数据:",a)
res=encrypt(a,k)
print("加密后的数据:",res)
res=decrypt(res,k)
print("解密后数据:",res)

XXTEA

C

#include<stdio.h>
#include<stdint.h>
#define DELTA 0x933779b9
#define MX (((z>>5^y<<2)+(y>>3^z<<4))^((sum^y)+(key[(p&3)^e]^z)))
void btea(uint32_t *v,int n,uint32_t const key[4])
{
uint32_t y,z,sum;
unsigned p,rounds,e;
if(n>1)
{
rounds=6+52/n; //这里可以说是预定义值,n=2是rounds=32
sum=0;
z=v[n-1];
do
{
sum+=DELTA;
e=(sum>>2)&3;
for(p=0;p<n-1;p++) //注意这里的p是从0~n-1
{
y=v[p+1];
z=v[p]+=MX;
}
y=v[0];
z=v[n-1]+=MX; //这里的MX中传入的p=n-1
}
while(--rounds);
}
else if(n<-1)
{
n=-n;
rounds=6+52/n;
sum=rounds*DELTA;
y=v[0];
do
{
e=(sum>>2)&3;
for(p=n-1;p>0;p--) //注意这里的p是从n-1~0,和上面是反过来的
{
z=v[p-1];
y=v[p]-=MX;
}
z=v[n-1];
y=v[0]-=MX; //这里的MX中传入的 p=0
sum-=DELTA;
}
while(--rounds);
}
}

int main()
{
uint32_t v[2]={1,2};
uint32_t const k[4]={2,2,3,4};
int n=2;
printf("加密前原始数据:%u %u\n",v[0],v[1]);
btea(v,n,k);
printf("加密后数据:%u %u\n",v[0],v[1]);
btea(v,-n,k);
printf("解密后数据:%u %u\n",v[0],v[1]);
return 0;

}

PYTHON

from ctypes import * 

def MX(z, y, sum1, k, p, e):
return c_uint32(((z.value>>5^y.value<<2)+(y.value>>3^z.value<<4))^((sum1.value^y.value)+(k[(p&3)^e.value]^z.value)))
def btea(v,k,n,delta):

if n>1:
sum1=c_uint32(0)
z=c_uint32(v[n-1])
rounds=6+52//n
e=c_uint32(0)

while rounds>0:
sum1.value+=delta
e.value=((sum1.value>>2)&3) #e都要32位哦
for p in range(n-1):
y=c_uint32(v[p+1])
#v[p]=c_uint32(v[p]+c_uint32((((z.value>>5^y.value<<2)+(y.value>>3^z.value<<4))^((sum1.value^y.value)+(k[(p&3)^e.value]^z.value)))).value).value
v[p] = c_uint32(v[p] + MX(z,y,sum1,k,p,e).value).value
z.value=v[p]

y=c_uint32(v[0])
#v[n-1]=c_uint32(v[n-1]+c_uint32((((z.value>>5^y.value<<2)+(y.value>>3^z.value<<4))^((sum1.value^y.value)+(k[((n-1)&3)^e.value]^z.value)))).value).value #这里tmd传入的是k[((n-1)&3)啊我草,找了半天!!!
v[n-1] = c_uint32(v[n-1] + MX(z,y,sum1,k,n-1,e).value).value
z.value=v[n-1]
rounds-=1

else:
sum1=c_uint32(0)
n=-n
rounds=6+52//n
sum1.value=rounds*delta
y=c_uint32(v[0])
e=c_uint32(0)

while rounds>0:
e.value=((sum1.value>>2)&3) #e都要32位哦
for p in range(n-1, 0, -1):
z=c_uint32(v[p-1])
#y[p]=c_uint32(v[p]-c_uint32((((z.value>>5^y.value<<2)+(y.value>>3^z.value<<4))^((sum1.value^y.value)+(k[(p&3)^e.value]^z.value)))).value).value
v[p] = c_uint32(v[p] - MX(z,y,sum1,k,p,e).value).value
y.value=v[p]

z=c_uint32(v[n-1])
#v[n-1]=c_uint32(v[n-1]-c_uint32((((z.value>>5^y.value<<2)+(y.value>>3^z.value<<4))^((sum1.value^y.value)+(k[((n-1)&3)^e.value]^z.value)))).value).value #这里tmd传入的是k[((n-1)&3)啊我草,找了半天!!!
v[0] = c_uint32(v[0] - MX(z,y,sum1,k,0,e).value).value
y.value=v[0]
sum1.value-=delta
rounds-=1

return v




if __name__=='__main__':
a=[1,2]
k=[2,2,3,4]
delta=0x9e3779b9
n=2
print("加密前数据:",a)
res=btea(a,k,n,delta)
print("加密后数据:",res)
res=btea(a,k,-n,delta)
print("解密后数据:",res)

17.Android

开发基础

具体的操作如下

  1. 使用 aapt(The Android Asset Packing Tool) 对资源文件进行打包,生成 R.java 文件。
  2. 如果项目中使用到了 AIDL(Android Interface Definition Language)提供的服务,则需要使用 AIDL 工具解析 AIDL 接口文件生成相应的 Java 代码。
  3. 使用 javac 将 R.java 和 AIDL 文件编译为 .class 文件。
  4. 使用 dx 工具将 class 和第三方的 library 转换为 dex 文件。
  5. 利用 apkbuilder 将第一步编译后的资源、第四步生成的 .dex 文件,以及一些其它资源打包到 APK 文件中。
  6. 这一部主要是对 APK 进行签名。可以分为两种情况,如果我们是要发布 App,那就采用 RealeaseKeystore 签名;反之,我们如果只是想要对 App 进行调试,那就使用 debug.keystore 签名。
  7. 在发布正式版之前,我们需要将 APK 包中资源文件距离文件的起始偏移修改为 4 字节的整数倍数,这样,在之后运行 App 的时候,速度会比较快。

Apk 文件结构

  • AndroidManifest.xml
    • 该文件主要用于声明应用程序的名称,组件,权限等基本信息。
  • class.dex
    • 该文件是 dalvik 虚拟机对应的可执行文件,包含应用程序的可执行代码。
  • resource.arsc
    • 该文件主要是应用程序编译后的二进制资源以及资源位置与资源 id 之间的映射关系,如字符串。
  • assets
    • 该文件夹一般用于包含应用程序的原始资源文件,例如字体和音乐文件。程序在运行的时候,可以通过 API 获取这些信息。
  • lib/
    • lib 目录下主要用于存储通过 JNI(Java Native Interface)机制使用的本地库文件,并且会按照其支持的架构,分别创建对应的子目录。
  • res/
    • 该目录主要包含了 Android 应用引用的资源,并且会按照资源类型进行存储,如图片,动画,菜单等。主要还有一个 value 文件夹,包含了各类属性资源
  • colors.xml→颜色资源
  • dimens.xml—> 尺寸资源
  • strings—> 字符串资源
  • styles.xml→样式资源
  • META-INF/
  • 类似于 JAR 文件,APK 文件中也包含了 META-INF 目录,用于存放代码签名等文件,以便于用来确保 APK 文件不会被人随意修改。

18.ida远程调试

(1)先运行对应的程序的在kali的server文件

(2)找到目前kali的ip

image-20240922220432800

(3)特别注意这些文件地址都是kali里的