SUSCTF_tttree(使用idapython去混淆的学习)

这道题里面有大量的相同结构的混淆,所以采用了idapython的方式解混淆,这道题需要就是学习用idapython解混淆的过程。后面的平衡树的算法太难了,就没有继续往下解了。

混淆

这里面有两个混淆,出题人声称采用了 这里参考了古月浪子大佬的一个混淆思路。

混淆一

重构序列

花指令形式特征

1
2
3
4
5
6
7
8
9
10
push rax 
push rax
pushfq
call $+5
pop rax
add rax,xxxx
push rax
mov [rsp+10h], rax
popfq
pop rax

作用

当该花指令的后面接着的是retn这个指令的时候,就是跳转到这个计算得到的地址

当该花指令的后面接着的是jmp addr2这个指令时,相当于call addr2(addr1就是这个指令之后的返回地址)

解混淆的方法

如果是retn指令时:通过“pop rax”和”add rax,xxxx”这两个指令计算得到需要jmp的偏移地址,将jmp+偏移地址的指令写入花指令这个地方的地址,其余空间地址“nop”

如果时jmp addr2指令时,先将这个“pop rax”和”add rax,xxxx”这两个指令计算得到的目标地址(注意这个地方不是相对地址)push,将push+目标地址的指令写入花指令的地址,其余空间地址“nop”。然后先jmp “jmp addr2”的地址,再jmp “push”指令里面的地址

该程序之中的示例

image-20220306092253733

image-20220306092311317

混淆二

无效push 和 pop

花指令形式特征

1
2
push [rax|...] 
pop [rax|...]

解释:push之后马上pop出来,显然这个指令时无效的

解混淆的方法:直接pop掉这两句就可以了

该程序之中的示例:

image-20220306093739567

idapython脚本

idapython的教程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
import struct
start = 0x140001000 #起始位置
end = 0x14001C694 #结束位置
address_m = [0 for x in range(11)]
address_target = ['push rax','push rax','pushfq','call $+5','pop rax','add rax,','mov ','popfq','pop rax','retn']
# 第一种花指令的特征 注意这个里面的每一条指令中间的空格数都要和ida之中展示的汇编代码相同,所以最好是进行复制粘贴

def check1():
cnt = 0
for i in range(9):
if i == 5 or i == 6:
cnt += GetDisasm(address_m[i]).find(address_target[i]) != -1 # GetDisasm:获得对应位置的汇编地址 因为'add rax,','mov '这两个指令中有数据,每个部分的代码就会不一样,所以使用find函数来判断是否存在
else:
cnt += GetDisasm(address_m[i]) == address_target[i] # 除了那两个语句以外就需要判断剩下的这些语句是否相等就可以了
return cnt == 9 # 如果9条语句都和目标类型已知 就认为是第一种花指令


def check2(x,y):
cnt = 0
cnt += print_insn_mnem(x) == "push" # idc.print_insn_mnem()获取操作符 判断操作符是否是push指令
cnt += print_insn_mnem(y) == "pop" # idc.print_insn_mnem(y)获取操作符 判断操作符是否是 pop 指令
cnt += print_operand(x,0) == print_operand(y,0) # idc.print_operand(x, 0) 获取第一个操作数 判断这两个指令的操作数是否相等
return cnt == 3 # 当以上的三个条件都满足的时候就是第二种花指令


def check3():
cnt = 0
cnt += print_insn_mnem(address_m[0]) == "push" # print_insn_mnem获取操作符 判断操作符是否是push
cnt += get_operand_type(address_m[0], 0) == o_imm # 如果操作数是一个确定的数值的话,那么返回类型,值为 5
return cnt == 2


def nop(u,v): # u是指令的起始地址 v是结束的地址,但是v地址的指令不会被nop
patch_add = u
while(patch_add < v):
patch_byte(patch_add,0x90)
patch_add += 1


p = start
while p <= end:
address_m[0] = p
p = next_head(p) # 利用了 idc.next_head()使当前地址不断增长

# 将第二种花指令进行nop
while print_insn_mnem(p) == "nop": # 如果是nop语句就跳过
p = next_head(p)
if check2(address_m[0], p) == 1: # 判断这两个连续的语句是否是满足第二种花指令的要求
p = next_head(p) # p定位到第二种花指令结束之后那个地址
nop(address_m[0], p) # address_m[0]是nop的起始位置 p是nop的结束位置
else:
p = address_m[0] # 如果不是花指令起点又回到最初的那个地方

# 将第一种花指令nop
address_m[0] = p # 将起始的地址放入到数组的第一项之中
for i in range(1, 11): # 判断这个地方连续的10条指令是否满足第一种花指令的要求,取出这10条指令的地址放入到数组之中
address_m[i] = next_head(address_m[i - 1])

if check1() == 1: # 判断是否是第一种花指令
addri = get_operand_value(address_m[5], 1) # get_operand_value(address_m[5], 1) 获取操作数的数值 即该条 'add rax,' 命令的第二个操作数的数值
addri += address_m[4] # rax加上操作数,得到目标地址 x+a4-a0-5
if address_target[9] == GetDisasm(address_m[9]): # 当最后一条指令是ret时
addri -= (address_m[0] + 5)
patch_byte(address_m[0], 0xE9) # 将这个地方的值修改为 0xE9 就是jmp指令 因为 0xE9 JMP 后面的四个字节是偏移 所以这里addri需要计算成偏移地址
patch_dword(address_m[0] + 1, addri & 0xffffffff) # dw的数值来存放目标的地址
nop(address_m[0] + 5, address_m[10]) # 将剩下的nop掉
p = address_m[10]
else:
patch_byte(address_m[0], 0x68) # 0x68是push 入栈的操作
patch_dword(address_m[0] + 1, addri & 0xffffffff) # 将对应的地址push
nop(address_m[0] + 5, address_m[9])
p = address_m[9]
else:
p = address_m[1] # 指向下一个指令



# 对第一种花指令中结尾是push的操作
p = start # 从程序的开始进行遍历
while p <= end:
address_m[0] = p
address_m[1] = next_head(p)
if check3() == 1: # 当指令满足check3()中指令的要求时进行的操作
addri = get_operand_value(address_m[0], 0) + 2 ** 32 # get_operand_value 获取push的地址的值 2 ** 32代表2的32次方
p = address_m[1] # push指令后的下一条指令
while print_insn_mnem(p) == "nop": # 如果遇到nop指令就跳过
p += 1
if print_insn_mnem(p) == "jmp": # 如果是jmp指令
addrj = struct.unpack('<I', get_bytes(p + 1, 4))[0] + p - address_m[0] # struct.unpack('<I', get_bytes(p + 1, 4)) 利用小序端取得地址 这个得到的是一个元组 这条jmp指令的起始位置 减去上一条指令的位置
addri -= p + 5 # push的地址 减去jmp地址结束的那个地址
if addri < 0:
addri += 2 ** 32 # 保持addri这个地址始终大于0
patch_byte(address_m[0], 0xe8) # 0xE8 CALL 后面的四个字节是地址
patch_dword(address_m[0] + 1, addrj & 0xffffffff)
patch_byte(p, 0xe9) # 0xE9 JMP 后面的四个字节是偏移
p += 1
patch_dword(p, addri)# 需要跳转到的偏移地址
p += 4
else:
p = address_m[1] # 下一个指令
print("Finish")

idapython脚本学习

idapython的指令查询

跳转指令和机器码

注意相同的指令,但是不同的机器码后面所跟着的地址的要求是不一样的

1
2
3
4
5
6
7
8
9
0xE8 CALL 后面的四个字节是地址
0xE9 JMP 后面的四个字节是偏移
0xEB JMP 后面的二个字节是偏移
0xFF15 CALL 后面的四个字节是存放地址的地址
0xFF25 JMP 后面的四个字节是存放地址的地址

0x68 PUSH 后面的四个字节入栈
0x6A PUSH 后面的一个字节入栈
对于每条指令我们检查他的操作数类型是否为 o_imm(值为 5),o_imm 类型的操作数就是一个确定的数值或者偏移,一旦这个发现这种类型的操作数

常用到的idapython的函数

对指令(操作符 操作数等)进行操作的函数:

  • **GetDisasm(ea)**:获得ea这个地址的汇编代码
  • print_insn_mnem(ea):获取ea这个地址的操作符
  • print_operand(ea, 0):获取当前地址操作数。例如获取“MOV AH,06H“指令的AH这个操作数
  • get_operand_value(ea, n):获取操作数的数值
  • next_head(ea):获取下一个指令地址
  • prev_head(ea):获取上一个指令地址
  • next_addr(ea):获取下一个地址
  • prev_addr(ea):获取上一个地址
  • **get_operand_type(ea,n)**:获取操作数类型。其返回类型有八种 + 不同的处理器 6 种,这道题之中的o_imm代表是一个确定的数值。还有其它的类型,可以查看上面的那个网址

补丁:

  • patch_byte/patch_word/patch_dword/patch_qword(ea, value):打补丁,反调试 混淆等等都用得到,修改相应的机器码等,比如这道题之中的指令 patch_byte(address_m[0], 0xe8) 就是修改这个地址的机器码为0xe8 这个jmp指令

机器码式搜索:

  • **FindBinary(ea,flag, searchstr, radix=16)**:实行字节或者二进制的搜索。ea代表地址,flag 代表搜索方向或者条件(有具体的参数可以选择,根据需要查表填入)比如说SEARCH_UP和SEARCH_DOWN 用来指明搜索的方向。searchstr代表的是字节字符串,形如’55 48 89 E5’这个样子的

对汇编之中函数操作的函数:

  • **idautils.Functions()**:获取函数列表,使用for循环的方式遍历所有的这些地址,然后对每个函数对象进行相应的操作。例如 get_func_name(func) func就是这个函数列表之中的一个对象,这个函数获取这个函数对象的名称。
  • **get_func_attr(func, FUNCATTR_FLAGS)**:信息收集函数,可以用来检索关于函数的信息,例如它是否是库中代码,或者函数是否有返回值等。对于一个函数来说有九个可能的标志。

python函数说明

struct.pack()和struct.unpack()

使用这两个函数需要使用struct包

**struct.pack(fmt,v1,v2,…..)**:将v1,v2等参数的值进行一层包装,包装的方法由fmt指定。被包装的参数必须严格符合fmt。最后返回一个包装后的字符串。 将数据转换成c语言之中相应的存储的形式。

struct.unpack(fmt,string):顾名思义,解包。返回一个由解包数据(string)得到的一个元组(tuple), 即使仅有一个数据也会被解包成元组。其中len(string) 必须等于 calcsize(fmt),这里面涉及到了一个calcsize函数。struct.calcsize(fmt):这个就是用来计算fmt格式所描述的结构的大小。

fmt的构造

格式字符串(format string)由一个或多个格式字符(format characters)组成

image-20220306114015713

在Format string 的首位,有一个可选字符来决定大端和小端:

image-20220306114148873

例子:

image-20220306152233838

find()函数

描述:Python find() 方法检测字符串中是否包含子字符串 str 。如果指定 beg(开始) 和 end(结束) 范围,则检查是否包含在指定范围内,如果包含子字符串返回开始的索引值,否则返回-1

语法:str.find(str, beg=0, end=len(string))

  • str – 指定检索的字符串
  • beg – 开始索引,默认为0。
  • end – 结束索引,默认为字符串的长度。

返回值:如果包含子字符串返回开始的索引值,否则返回-1。

注意:寻找目的是搜索的对象(子字符串)是不是在这串字符串里面,不在于它(子字符串)的位置在哪里。子字符串之中的空格是不能忽视的,并且搜索对象(子字符串)在字符串之中必须是连续完整存在的,这样才能说明字符串中包含了子字符串 str。

参考文章:https://psyduck0409.github.io/2021/03/01/2021/idapython%E7%AC%94%E8%AE%B0/