首页
社区
课程
招聘
[原创]IDA Python 去混淆
2023-8-29 10:57 15182

[原创]IDA Python 去混淆

2023-8-29 10:57
15182

基础理论

程序的结构

我们可以认为一个程序的代码结构如下图所示:

一个程序由多个函数(function)组成,而每个函数由多个分支(branch)组成,对于函数和分支我们做如下定义:

  • 函数:从 CALL 指令跳转到的代码开始,在不通过 CALL 指令跳转的前提下能访问到的所有代码。
  • 分支:通过 JCC 跳转到的代码开始,直到以 RET 结尾或者跳转到已分析过的分支的代码块。

因此去混淆的时候我们可以有如下代码框架,即先 bfs 函数,然后在每个函数内部再 bfs 所有分支。在 bfs 的过程中将已去混淆的代码拼接起来。这样做的好处是同一个函数的代码尽可能放在一起,ida 在反编译的时候容易识别。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func_queue = Queue()
func_queue.put(entry_point)
 
while not func_queue.empty():
    func_address = func_queue.get()
   
    branch_queue = Queue()
    branch_queue.put(func_address)
    while not branch_queue.empty():
        branch_address = branch_queue.get()
        ... # 去混淆代码
            if idc.print_insn_mnem(ea) == 'call': # CALL function
                func_queue.put(call_target)
            elif idc.print_insn_mnem(ea)[0] == 'j' # JCC branch
                branch_queue.put(jcc_target)
... # 重定位代码

代码重定位

代码的位置移动时,原本的 CALL 和 JCC 等跳转指令要想跳转到原来的地方需要进行指令修正,这个可以借助 keystone-engine 和 capstone 来完成。

1
2
def mov_code(ea, new_code_ea):
    return asm(disasm(idc.get_bytes(ea, idc.get_item_size(ea)), ea), new_code_ea)

然而在完成去混淆后程序中的绝大多数代码都移动了位置,因此程序中所有的 CALL 和 JCC 等跳转指令跳转的地址需要进行修正,也就是重定位。

对于指令修正我们可以通过并查集来维护。

一个程序的跳转指令可以看做是上图左边的结构。即存在一个跳转指令跳转到另一个跳转指令的情况。通过并查集我们可以将指令 A,B,C,D,E 的真实地址都修正为指令 E 的真实地址

在使用并查集维护重定位的时候需要注意以下几点:

  • 上图中的指令 E 需要确保不存在指令复用的情况。因为有的代码混淆会将程序拆分成指令后放到一个巨大的 switch 中,然后通过在 switch 中查找依次执行指令。这种情况会造成一条指令在不同的分支中都会使用,如果此时我们用并查集维护就会把该指令重定位到其中一个使用该指令的地址,但实际上该指令还会在其他地址出现,这就造成了程序可能会跳转到错误的分支上。对于这种情况我们需要重定位查找 switch 的代码到去混淆的代码上,而不是重定位 switch 中的具体指令,这样就保证一一对应了。
  • 在上图的结构中我们可以发现,只有连接根节点的边是重定位的边,其余的边都是跳转的边。因为在跳转的时候我们不需要关心中间的跳转指令在哪里,而是需要关心最终跳转到的位置的真实地址。因此在并查集合并的时候如果是一条 JMP 指令就需要将该指令的重定位后的实际地址合并到指令的原本地址,然后将指令的原本地址合并到指令的跳转地址,否则将该指令的原本地址合并到指令的重定位后的实际地址。这样在并查集路径压缩之后每一个跳转指令跳转地址都被重定位到非 JMP 指令的实际地址

例题:强网杯2022 find_basic

附件下载链接

观察发现程序由下面的代码块构成:

1
2
3
4
5
6
.text:000048F4 pushf
.text:000048F5 pusha
.text:000048F6 mov     cl, 3Fh ; '?'
.text:000048F8 call    sub_44FA
.text:000048F8
.text:000048FD pop     eax

分析该代码块的执行过程,发现本质是在一个 switch 中查找实际指令。该代码块可由 lea ecx, [esp+4] 指令代替。

首先,我们需要将程序中的代码块提取出来,然后记录几个有用的信息:

  • start_ea:代码块的起始地址
  • end_ea:代码块的结束地址
  • imm:在 switch 中查找指令用的立即数
  • reg:存放立即数用的寄存器
  • call_target:调用的 switch 函数

在提取代码块的有效信息的同时也可以检测该代码块是否有效,因此分析发现程序中会在代码块直接插入一些有实际功能的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Block:
    def __init__(self, start_ea, end_ea, imm, reg, call_target):
        self.start_ea = start_ea
        self.end_ea = end_ea
        self.imm = imm
        self.reg = reg
        self.call_target = call_target
         
def get_block(start_ea):
    global imm, reg, call_target
    mnem_list = ['pushf', 'pusha', 'mov', 'call', 'pop']
    ea = start_ea
    for i in range(5):
        mnem = idc.print_insn_mnem(ea)
        assert mnem == mnem_list[i]
        if mnem == 'mov':
            imm = idc.get_operand_value(ea, 1)
            reg = idc.print_operand(ea, 0)
        elif mnem == 'call':
            call_target = idc.get_operand_value(ea, 0)
        ea += idc.get_item_size(ea)
    return Block(start_ea, ea, imm, reg, call_target)

在提取出代码块之后利用提取到的有效信息可以在 call_target 中查找代码块对应的实际代码。这里有几个特殊情况:

  • 一般情况在 cmp 判断找到对应位置后会依次执行 jnz,popa,popf 三条指令,然后后面紧跟着代码块对应的实际代码。然而想下面这种情况,在执行完 popf 后面紧跟着 pusha 而不是代码块对应的实际代码,简单分析一下发现这种情况代码块对应的实际代码为 retn 。这种情况需要返回 True 表示一个 branch 的结束。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    .text:000045CC popa
    .text:000045CD popf
    .text:000045CE pushf
    .text:000045CF pusha
    .text:000045D0 call    dec_index
    .text:000045D0
    .text:000045D5 popa
    .text:000045D6 popf
    .text:000045D7 retn
  • 通常认为代码块对应的实际代码的结束标志为一个 jmp 指令,但是有的地方在 jmp 之后还会执行几条有效指令,因此判断实际代码的结束标志应当是 pushf 。
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
def get_real_code(block, new_code_ea):
    ea = block.call_target
    while True:
        if idc.print_insn_mnem(ea) == 'cmp':
            reg = idc.print_operand(ea, 0)
            imm = idc.get_operand_value(ea, 1)
            if reg == block.reg and imm == block.imm:
                ea += idc.get_item_size(ea)
                break
        ea += idc.get_item_size(ea)
 
    # 在 cmp 判断找到对应位置后会依次执行 jnz,popa,popf 三条指令
    assert idc.print_insn_mnem(ea) == 'jnz'
    ea += idc.get_item_size(ea)
 
    assert idc.print_insn_mnem(ea) == 'popa'
    ea += idc.get_item_size(ea)
 
    assert idc.print_insn_mnem(ea) == 'popf'
    ea += idc.get_item_size(ea)
 
    if idc.print_insn_mnem(ea) == 'pushf'# 第一种特殊情况,实际是 ret 指令。
        return True, asm('ret')
 
    new_code = b''
    while True:
        if idc.print_insn_mnem(ea) == 'jmp'# 第二种特殊情况,跳转过去可能还会有几条实际功能指令。
            jmp_ea = idc.get_operand_value(ea, 0)
            if idc.print_insn_mnem(jmp_ea) == 'pushf':
                break
            ea = jmp_ea
        else:
            code = mov_code(ea, new_code_ea)
            new_code += code
            new_code_ea += len(code)
            ea += get_item_size(ea)
    return False, new_code

这里涉及到了维护重定位的并查集 RelocDSU ,对应代码如下。在 get 函数中如果遇到了 jmp 指令且操作数是立即数就路径压缩到跳转的地址,直到地址在 .got.plt 或者指令不是 jmp 指令。另外判断是否是已处理代码是根据地址对应的最终地址是否不在 .text 段。

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
class RelocDSU:
 
    def __init__(self):
        self.reloc = {}
 
    def get(self, ea):
        if ea not in self.reloc:
            if idc.print_insn_mnem(ea) == 'jmp' and idc.get_operand_type(ea, 0) != idc.o_reg:
                jmp_ea = idc.get_operand_value(ea, 0)
 
                if idc.get_segm_name(jmp_ea) == '.got.plt':
                    self.reloc[ea] = ea
                    return self.reloc[ea], False
 
                self.reloc[ea], need_handle = self.get(idc.get_operand_value(ea, 0))
                return self.reloc[ea], need_handle
            else:
                self.reloc[ea] = ea
        if self.reloc[ea] != ea: self.reloc[ea] = self.get(self.reloc[ea])[0]
        return self.reloc[ea], idc.get_segm_name(self.reloc[ea]) == '.text'
 
    def merge(self, ea, reloc_ea):
        self.reloc[self.get(ea)[0]] = self.get(reloc_ea)[0]
 
 
reloc = RelocDSU()

接下来就是考虑如何提取出一个 branch 的代码了。前面提到过程序中会在代码块直接插入一些有实际功能的代码,因此需要借助 try:...except:...assert 来处理。除此之外这里还有几个特殊情况:

  • 程序中的 0x900 和 0x435c 处分别有一个获取返回地址 eip 到 ebx 和 eax 的函数,程序借助这两个函数来访问全局变量实现地址无关代码,然而重定位后代码地址改变,因此这里需要将其修正为 mov reg, xxx
  • 需要根据程序中的 jmp 指令来决定下一步需要去混淆的代码位置,这里需要判断 jmp 后面跟的是否是立即数,另外需要判断 jmp 到的代码是否是已经处理过的代码。
  • 并查集合并的时候如果是代码块,需要将代码块的地址合并到代码块对应指令的实际重定位后的地址;如果不是代码块如果是 jmp 指令且操作数是立即数,需要将 jmp 指令和该指令的重定位后的实际地址合并到指令的原本地址,然后将指令的原本地址合并到指令的跳转地址,否则将该指令的地址合并到重定位后的地址。
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
def handle_one_branch(branch_address, new_code_ea):
    new_code = b''
    ea = branch_address
    while True:
        try:
            block = get_block(ea)
            is_ret, real_code = get_real_code(block, new_code_ea)
            reloc.merge(ea, new_code_ea)
            ea = block.end_ea
            new_code_ea += len(real_code)
            new_code += real_code
            if is_ret: break
        except:
            get_eip_func = {0x900: 'ebx', 0x435c: 'eax'}
            if idc.print_insn_mnem(ea) == 'call' and get_operand_value(ea, 0) in get_eip_func:
                reloc.merge(ea, new_code_ea)
                real_code = asm('mov %s, 0x%x' % (get_eip_func[get_operand_value(ea, 0)], ea + 5), new_code_ea)
            else:
                if idc.print_insn_mnem(ea) == 'jmp' and idc.get_operand_type(ea, 0) != idc.o_reg:
                    reloc.merge(new_code_ea, ea)
                else:
                    reloc.merge(ea, new_code_ea)
                real_code = mov_code(ea, new_code_ea)
 
            new_code += real_code
            if real_code == asm('ret'): break
            new_code_ea += len(real_code)
            if idc.print_insn_mnem(ea) == 'jmp' and idc.get_operand_type(ea, 0) != idc.o_reg:  # jmp reg is a swtich
                jmp_ea = idc.get_operand_value(ea, 0)
                if reloc.get(jmp_ea)[1] == False: break  # 跳回之前的代码说明是个循环
                ea = reloc.get(jmp_ea)[0]
            else:
                ea += get_item_size(ea)
    return new_code

能够处理 branch 后,我们就可以 bfs 依次处理所有的 function 和 branch 了,这里还有几个特殊情况:

  • 0x4148 地址处的函数中有一个 switch ,由于是通过跳转表跳转,去混淆脚本分析不到跳转的分支,因此需要读取跳转表找到跳转的 branch 然后添加到 branch_queue 中。
  • 寻找新的 branch 时需要判断 jcc 的操作数类型是否是立即数。
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
func_queue = Queue()
func_queue.put(entry_point)
 
while not func_queue.empty():
    func_address = func_queue.get()
    if reloc.get(func_address)[1] == False: continue
    reloc.merge(func_address, new_code_ea)
    branch_queue = Queue()
    branch_queue.put(func_address)
    if func_address == 0x4148# 特判 0x4148 地址处的函数,读取跳转表。
        assert new_code_ea == 0x963d0
        for eax in range(0x20):
            jmp_target = (ida_bytes.get_dword(jmp_table[0] + eax * 4) + jmp_table[1]) & 0xFFFFFFFF
            new_jmp_target, need_handle = reloc.get(jmp_target)
            if need_handle: branch_queue.put(jmp_target)
 
    while not branch_queue.empty():
        branch_address = branch_queue.get()
        new_code = handle_one_branch(branch_address, new_code_ea)
        ida_bytes.patch_bytes(new_code_ea, new_code)
 
        # 当前 branch 去完混淆之后需要遍历代码找到 call 和 jmp 指令从而找到其他的 function 和 branch 。
        ea = new_code_ea
        while ea < new_code_ea + len(new_code):
            idc.create_insn(ea)
            if idc.print_insn_mnem(ea) == 'call':
                call_target, need_handle = reloc.get(get_operand_value(ea, 0))
                if need_handle: func_queue.put(call_target)
            elif idc.print_insn_mnem(ea)[0] == 'j' and idc.get_operand_type(ea, 0) != idc.o_reg:
                jcc_target, need_handle = reloc.get(get_operand_value(ea, 0))
                if need_handle == True:
                    branch_queue.put(jcc_target)
            ea += get_item_size(ea)
        new_code_ea += len(new_code)

在完成代码去混淆之后需要对代码进行重定位,重定位的时候需要注意 jmp 指令长度的变化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ea = new_code_start
while ea < new_code_ea:
    idc.create_insn(ea)
    mnem = idc.print_insn_mnem(ea)
 
    if mnem == 'call':
        call_target, need_handle = reloc.get(get_operand_value(ea, 0))
        assert need_handle == False
        ida_bytes.patch_bytes(ea, asm('call 0x%x' % (call_target), ea))
    elif mnem[0] == 'j' and idc.get_operand_type(ea, 0) != idc.o_reg:
        jcc_target, need_handle = reloc.get(get_operand_value(ea, 0))
        assert need_handle == False
        ida_bytes.patch_bytes(ea, asm('%s 0x%x' % (mnem, jcc_target), ea).ljust(idc.get_item_size(ea), b'\x90'))
    elif mnem == 'pushf':
        ida_bytes.patch_bytes(ea, b'\x90' * 9)
        ea += 9
        continue
    ea += get_item_size(ea)

最后去混淆后的 switch 不能被 ida 正常识别出来,具体原因是前面获取返回地址 eip 的函数被 patch 成了 mov reg, xxx 指令,导致其与编译器默认编译出的汇编不同(程序开启了 PIE,直接访问跳转表的地址 ida 不能正确识别),因此需要将这里的代码重新 patch 回去。

同时为了不影响原本程序中的数据,这里我将修复的跳转表放到了其他位置。另外还有两个字符串全局变量也移动到了正确位置。

1
2
3
4
5
6
7
8
9
10
11
12
new_jmp_table = (0xA6000 - 0x2D54, 0xA6000)
 
# 移动并修复跳转表
for eax in range(0x20):
    jmp_target = (ida_bytes.get_dword(jmp_table[0] + eax * 4) + jmp_table[1]) & 0xFFFFFFFF
    new_jmp_target, need_handle = reloc.get(jmp_target)
    assert need_handle == False
    ida_bytes.patch_dword(new_jmp_table[0] + eax * 4, (new_jmp_target - new_jmp_table[1]) & 0xFFFFFFFF)
 
need_patch_addr = 0x963D7
ida_bytes.patch_bytes(need_patch_addr, asm('call 0x900;add ebx, 0x%x' % (new_jmp_table[1] - (need_patch_addr + 5)), need_patch_addr))  # 修复指令
ida_bytes.patch_bytes(new_jmp_table[1] - 0x2d7a, ida_bytes.get_bytes(jmp_table[1] - 0x2d7a, 0x26))  # 复制字符串到正确位置

最终去混淆脚本如下:

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
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
from queue import *
import ida_bytes
from idc import *
import idc
from keystone import *
from capstone import *
 
asmer = Ks(KS_ARCH_X86, KS_MODE_32)
disasmer = Cs(CS_ARCH_X86, CS_MODE_32)
 
 
def disasm(machine_code, addr=0):
    l = ""
    for i in disasmer.disasm(machine_code, addr):
        l += "{:8s} {};\n".format(i.mnemonic, i.op_str)
    return l.strip('\n')
 
 
def asm(asm_code, addr=0):
    l = b''
    for i in asmer.asm(asm_code, addr)[0]:
        l += bytes([i])
    return l
 
 
def print_asm(ea):
    print(disasm(idc.get_bytes(ea, idc.get_item_size(ea)), ea))
 
 
class RelocDSU:
 
    def __init__(self):
        self.reloc = {}
 
    def get(self, ea):
        if ea not in self.reloc:
            if idc.print_insn_mnem(ea) == 'jmp' and idc.get_operand_type(ea, 0) != idc.o_reg:
                jmp_ea = idc.get_operand_value(ea, 0)
 
                if idc.get_segm_name(jmp_ea) == '.got.plt':
                    self.reloc[ea] = ea
                    return self.reloc[ea], False
 
                self.reloc[ea], need_handle = self.get(idc.get_operand_value(ea, 0))
                return self.reloc[ea], need_handle
            else:
                self.reloc[ea] = ea
        if self.reloc[ea] != ea: self.reloc[ea] = self.get(self.reloc[ea])[0]
        return self.reloc[ea], idc.get_segm_name(self.reloc[ea]) == '.text'
 
    def merge(self, ea, reloc_ea):
        self.reloc[self.get(ea)[0]] = self.get(reloc_ea)[0]
 
 
reloc = RelocDSU()
 
 
class Block:
    def __init__(self, start_ea, end_ea, imm, reg, call_target):
        self.start_ea = start_ea
        self.end_ea = end_ea
        self.imm = imm
        self.reg = reg
        self.call_target = call_target
 
 
def mov_code(ea, new_code_ea):
    return asm(disasm(idc.get_bytes(ea, idc.get_item_size(ea)), ea), new_code_ea)
 
 
def get_real_code(block, new_code_ea):
    ea = block.call_target
    while True:
        if idc.print_insn_mnem(ea) == 'cmp':
            reg = idc.print_operand(ea, 0)
            imm = idc.get_operand_value(ea, 1)
            if reg == block.reg and imm == block.imm:
                ea += idc.get_item_size(ea)
                break
        ea += idc.get_item_size(ea)
 
    # 在 cmp 判断找到对应位置后会依次执行 jnz,popa,popf 三条指令
    assert idc.print_insn_mnem(ea) == 'jnz'
    ea += idc.get_item_size(ea)
 
    assert idc.print_insn_mnem(ea) == 'popa'
    ea += idc.get_item_size(ea)
 
    assert idc.print_insn_mnem(ea) == 'popf'
    ea += idc.get_item_size(ea)
 
    if idc.print_insn_mnem(ea) == 'pushf'# 第一种特殊情况,实际是 ret 指令。
        return True, asm('ret')
 
    new_code = b''
    while True:
        if idc.print_insn_mnem(ea) == 'jmp'# 第二种特殊情况,跳转过去可能还会有几条实际功能指令。
            jmp_ea = idc.get_operand_value(ea, 0)
            if idc.print_insn_mnem(jmp_ea) == 'pushf':
                break
            ea = jmp_ea
        else:
            code = mov_code(ea, new_code_ea)
            new_code += code
            new_code_ea += len(code)
            ea += get_item_size(ea)
    return False, new_code
 
 
def get_block(start_ea):
    global imm, reg, call_target
    mnem_list = ['pushf', 'pusha', 'mov', 'call', 'pop']
    ea = start_ea
    for i in range(5):
        mnem = idc.print_insn_mnem(ea)
        assert mnem == mnem_list[i]
        if mnem == 'mov':
            imm = idc.get_operand_value(ea, 1)
            reg = idc.print_operand(ea, 0)
        elif mnem == 'call':
            call_target = idc.get_operand_value(ea, 0)
        ea += idc.get_item_size(ea)
    return Block(start_ea, ea, imm, reg, call_target)
 
 
def handle_one_branch(branch_address, new_code_ea):
    new_code = b''
    ea = branch_address
    while True:
        try:
            block = get_block(ea)
            is_ret, real_code = get_real_code(block, new_code_ea)
            reloc.merge(ea, new_code_ea)
            ea = block.end_ea
            new_code_ea += len(real_code)
            new_code += real_code
            if is_ret: break
        except:
            get_eip_func = {0x900: 'ebx', 0x435c: 'eax'}
            if idc.print_insn_mnem(ea) == 'call' and get_operand_value(ea, 0) in get_eip_func:
                reloc.merge(ea, new_code_ea)
                real_code = asm('mov %s, 0x%x' % (get_eip_func[get_operand_value(ea, 0)], ea + 5), new_code_ea)
            else:
                if idc.print_insn_mnem(ea) == 'jmp' and idc.get_operand_type(ea, 0) != idc.o_reg:
                    reloc.merge(new_code_ea, ea)
                else:
                    reloc.merge(ea, new_code_ea)
                real_code = mov_code(ea, new_code_ea)
 
            new_code += real_code
            if real_code == asm('ret'): break
            new_code_ea += len(real_code)
            if idc.print_insn_mnem(ea) == 'jmp' and idc.get_operand_type(ea, 0) != idc.o_reg:  # jmp reg is a swtich
                jmp_ea = idc.get_operand_value(ea, 0)
                if reloc.get(jmp_ea)[1] == False: break  # 跳回之前的代码说明是个循环
                ea = reloc.get(jmp_ea)[0]
            else:
                ea += get_item_size(ea)
    return new_code
 
 
def solve():
    entry_point = 0x48F4
    new_code_start = 0x96150
    new_code_ea = new_code_start
 
    jmp_table = (0x892ac, 0x8c000# [0x8c000 + (eax>>2) - 0x2d54] + 0x8c000
 
    for _ in range(0x10000): idc.del_items(new_code_ea + _)
    ida_bytes.patch_bytes(new_code_ea, 0x10000 * b'\x90')
 
    func_queue = Queue()
    func_queue.put(entry_point)
 
    while not func_queue.empty():
        func_address = func_queue.get()
        if reloc.get(func_address)[1] == False: continue
        reloc.merge(func_address, new_code_ea)
        branch_queue = Queue()
        branch_queue.put(func_address)
        if func_address == 0x4148# 特判 0x4148 地址处的函数,读取跳转表。
            assert new_code_ea == 0x963d0
            for eax in range(0x20):
                jmp_target = (ida_bytes.get_dword(jmp_table[0] + eax * 4) + jmp_table[1]) & 0xFFFFFFFF
                new_jmp_target, need_handle = reloc.get(jmp_target)
                if need_handle: branch_queue.put(jmp_target)
 
        while not branch_queue.empty():
            branch_address = branch_queue.get()
            new_code = handle_one_branch(branch_address, new_code_ea)
            ida_bytes.patch_bytes(new_code_ea, new_code)
 
            # 当前 branch 去完混淆之后需要遍历代码找到 call 和 jmp 指令从而找到其他的 function 和 branch 。
            ea = new_code_ea
            while ea < new_code_ea + len(new_code):
                idc.create_insn(ea)
                if idc.print_insn_mnem(ea) == 'call':
                    call_target, need_handle = reloc.get(get_operand_value(ea, 0))
                    if need_handle: func_queue.put(call_target)
                elif idc.print_insn_mnem(ea)[0] == 'j' and idc.get_operand_type(ea, 0) != idc.o_reg:
                    jcc_target, need_handle = reloc.get(get_operand_value(ea, 0))
                    if need_handle == True:
                        branch_queue.put(jcc_target)
                ea += get_item_size(ea)
            new_code_ea += len(new_code)
 
    ea = new_code_start
    while ea < new_code_ea:
        idc.create_insn(ea)
        mnem = idc.print_insn_mnem(ea)
 
        if mnem == 'call':
            call_target, need_handle = reloc.get(get_operand_value(ea, 0))
            assert need_handle == False
            ida_bytes.patch_bytes(ea, asm('call 0x%x' % (call_target), ea))
        elif mnem[0] == 'j' and idc.get_operand_type(ea, 0) != idc.o_reg:
            jcc_target, need_handle = reloc.get(get_operand_value(ea, 0))
            assert need_handle == False
            ida_bytes.patch_bytes(ea, asm('%s 0x%x' % (mnem, jcc_target), ea).ljust(idc.get_item_size(ea), b'\x90'))
        elif mnem == 'pushf':
            ida_bytes.patch_bytes(ea, b'\x90' * 9)
            ea += 9
            continue
        ea += get_item_size(ea)
 
    new_jmp_table = (0xA6000 - 0x2D54, 0xA6000)
 
    # 移动并修复跳转表
    for eax in range(0x20):
        jmp_target = (ida_bytes.get_dword(jmp_table[0] + eax * 4) + jmp_table[1]) & 0xFFFFFFFF
        new_jmp_target, need_handle = reloc.get(jmp_target)
        assert need_handle == False
        ida_bytes.patch_dword(new_jmp_table[0] + eax * 4, (new_jmp_target - new_jmp_table[1]) & 0xFFFFFFFF)
 
    need_patch_addr = 0x963D7
    ida_bytes.patch_bytes(need_patch_addr, asm('call 0x900;add ebx, 0x%x' % (new_jmp_table[1] - (need_patch_addr + 5)), need_patch_addr))  # 修复指令
    ida_bytes.patch_bytes(new_jmp_table[1] - 0x2d7a, ida_bytes.get_bytes(jmp_table[1] - 0x2d7a, 0x26))  # 复制字符串到正确位置
 
    for _ in range(0x10000): idc.del_items(new_code_ea + _)
    idc.jumpto(new_code_start)
    ida_funcs.add_func(new_code_start)
 
    print("finish")
 
 
solve()

例题:SUSCTF2022 tttree

附件下载链接

首先将 0x1400100740x140017EFA140018C67 起始处的数据转换为汇编。

观察汇编,发现很多代码块之间相互跳转,因此先按照 retn 划分代码块。通过对代码块的观察,发现这些代码块按照 call $+5;pop rax(即 E8 00 00 00 00 58 ) 的出现次数可以分为三种:

  • 出现 0 次:

    本质上是 其它操作 + retn

  • 出现 1 次:

    这种代码块本质为 其它操作 + jmp target ,注意 其它操作 中可能包含 branch 。

  • 出现 2 次:

    这个可以看做 2 个出现 1 次的代码块两个拼在一起,其中前面一个代码块去掉 retn 。执行完前面一个代码块后由于没有 retn ,因此 target1 留在栈中。执行第 2 个代码块跳转到 target2 执行 ,在 target2 代码块返回时会返回到 target1 。因此这种代码块本质上相当于 其它操作 + call target2 且下一个要执行的代码块为 target1

我们定义代码块 Block 几个关键信息:

  • start_addr:代码块的起始地址。
  • asm_list:代码块的有效汇编,由于汇编指令可能包含 [rip + xxx] ,因此需要记录汇编指令的地址以便后续修正。
  • direct_next:执行完此代码块后接下来要执行的代码块地址。
  • branch_list:代码块中的所有条件跳转语句跳到的地址。
  • call_target:代码块调用函数地址。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Block:
    def __init__(self, start_ea, asm_list, direct_next, branch_list, call_target):
        self.start_ea = start_ea
        self.asm_list = asm_list
        self.direct_next = direct_next
        self.branch_list = branch_list
        self.call_target = call_target
 
    def __str__(self):
        return 'start_ea: 0x%x\ndirect_next: 0x%x\ncall_target: 0x%x\nbranch_list: %s\nasm_list:\n%s\n' % (
            0 if self.start_ea == None else self.start_ea,
            0 if self.direct_next == None else self.direct_next,
            0 if self.call_target == None else self.call_target,
            str([hex(x) for x in self.branch_list]),
            str('\n'.join([hex(addr) + '    ' + asm for addr, asm in self.asm_list]))
        )

get_block 函数可以获取给定地址处的代码块并提取相关信息。代码块中可能有 push xxx;pop xxx; 这样的无意义指令,可以通过栈模拟来去除。

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
def get_block(start_ea):
    ea = start_ea
    stack = []
    asm_list = []
    branch_list = []
    call_target = None
    direct_next = None
 
    while True:
        idc.create_insn(ea)
        mnem = idc.print_insn_mnem(ea)
 
        # 处理混淆中跳转的情况
        if mnem == 'pushfq':
            ea += idc.get_item_size(ea)
 
            assert idc.get_bytes(ea, idc.get_item_size(ea)) == b'\xE8\x00\x00\x00\x00'
            ea += idc.get_item_size(ea)
            jmp_base = ea
 
            assert idc.print_insn_mnem(ea) == 'pop' and idc.get_operand_type(ea, 0) == o_reg
            reg = idc.print_operand(ea, 0)
            ea += idc.get_item_size(ea)
 
            assert idc.print_insn_mnem(ea) == 'add' and idc.print_operand(ea, 0) == reg
            assert idc.get_operand_type(ea, 1) == o_imm
 
            jmp_target = (jmp_base + idc.get_operand_value(ea, 1)) & 0xFFFFFFFFFFFFFFFF
            ea += idc.get_item_size(ea)
 
            assert idc.get_bytes(ea, idc.get_item_size(ea)) == asm('mov [rsp + 0x10], %s' % reg, ea)
            ea += idc.get_item_size(ea)
 
            assert idc.print_insn_mnem(ea) == 'popfq'
            ea += idc.get_item_size(ea)
 
            assert idc.print_insn_mnem(ea) == 'pop' and idc.print_operand(ea, 0) == reg
            assert len(stack) != 0 and stack[-1][0] == 'push' and stack[-1][1] == reg
            stack.pop()
            asm_list.pop()
 
            assert len(stack) != 0 and stack[-1][0] == 'push' and stack[-1][1] == reg
            stack.pop()
            asm_list.pop()
 
            ea += idc.get_item_size(ea)
 
            if idc.print_insn_mnem(ea) == 'retn':
                if direct_next == None:
                    direct_next = jmp_target
                elif call_target == None:
                    call_target = jmp_target
                    asm_list.append((0, 'call 0x%x' % (call_target)))
                else:
                    print(" ----")
                    assert False
                break
            else:
                assert call_target == None and direct_next == None
                direct_next = jmp_target
                continue
 
        if mnem == 'push':
            stack.append((mnem, idc.print_operand(ea, 0)))
        elif mnem == 'pop':
            if len(stack) != 0 and stack[-1][0] == 'push' and stack[-1][1] == idc.print_operand(ea, 0):
                stack.pop()
                asm_list.pop()
                ea += idc.get_item_size(ea)
                continue
            else:
                stack.clear()
        else:
            stack.clear()
 
        asm_list.append((ea, disasm(idc.get_bytes(ea, idc.get_item_size(ea)), ea)))
 
        if mnem == 'retn': break
        if mnem[0] == 'j' and mnem != 'jmp' and idc.get_operand_type(ea, 0) != o_reg:
            branch_list.append(idc.get_operand_value(ea, 0))
 
        if mnem == 'jmp':
            if idc.get_segm_name(idc.get_operand_value(ea, 0)) not in ['.text', '.aaa']:
                break
            else:
                ea = idc.get_operand_value(ea, 0)
        else:
            ea += idc.get_item_size(ea)
 
    return Block(start_ea, asm_list, direct_next, branch_list, call_target)

能够获取代码块信息之后就可以 bfs 函数以及函数中的所有分支,提取出汇编代码并写入 newcode 段。这里需要注意以下几点:

  • 涉及 rip 的汇编指令不能只是简单把指令中的 rip 替换为对应的具体数值,因为有的指令立即数的长度被限制在 4 字节,直接替换成数值会溢出。一个比较好的解决方法是将 rip 替换为 rip + (指令原本地址 - 指令当前地址) 。这样借助 rip 寄存器扩大访问范围并且代码移动的距离不会超过 0x100000000 因此可以保证正确性。
  • 如果 block.direct_next 对应的代码已经被去混淆了需要加上一条 jmp 指令跳转到已经去混淆的代码。
  • 有的汇编指令 keystone 不支持汇编,比如 bnd ret ,需要特判。
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
while not func_queue.empty():
    func_address = func_queue.get()
    if reloc.get(func_address)[1] == False: continue
    branch_queue = Queue()
    branch_queue.put(func_address)
 
    while not branch_queue.empty():
        branch_address = branch_queue.get()
        ea = branch_address
 
        while True:
            block = get_block(ea)
            reloc.merge(ea, new_code_ea)
 
            for addr, insn in block.asm_list:
                insn = insn.replace('rip', 'rip - 0x%x' % (new_code_ea - addr))
                if insn == 'bnd ret  ;':
                    code = b'\xF2\xC3'
                else:
                    code = asm(insn, new_code_ea)
                ida_bytes.patch_bytes(new_code_ea, code)
                if addr != 0: reloc.merge(addr, new_code_ea)
                new_code_ea += len(code)
 
            if block.call_target != None:
                call_target, need_handle = reloc.get(block.call_target)
                if need_handle: func_queue.put(call_target)
 
            for branch_address in block.branch_list:
                jcc_target, need_handle = reloc.get(branch_address)
                if need_handle: branch_queue.put(jcc_target)
 
            if block.direct_next == None: break
 
            next_target, need_handle = reloc.get(block.direct_next)
            if need_handle == False:
                code = asm('jmp 0x%x' % (next_target), new_code_ea)
                ida_bytes.patch_bytes(new_code_ea, code)
                new_code_ea += len(code)
                break
            else:
                ea = block.direct_next

最后对代码进行重定位,需要注意的是代码块中的有效指令中也可能有 call 指令,这里 call 调用的是一个类似 plt 表的结构,会直接跳转到导入表中的函数地址表指向的函数,需要特判这种情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
ea = new_code_start
while ea < new_code_ea:
    assert idc.create_insn(ea) != 0
    mnem = idc.print_insn_mnem(ea)
 
    if mnem == 'call':
        call_target, need_handle = reloc.get(get_operand_value(ea, 0))
        if need_handle == True:
            if idc.print_insn_mnem(call_target) == 'jmp' and idc.get_segm_name(idc.get_operand_value(call_target, 0)) == '.idata':
                ea += get_item_size(ea)
                continue
            else:
                assert False
        ida_bytes.patch_bytes(ea, asm('call 0x%x' % (call_target), ea).ljust(idc.get_item_size(ea), b'\x90'))
    elif mnem[0] == 'j' and idc.get_operand_type(ea, 0) != idc.o_reg:
        jcc_target, need_handle = reloc.get(get_operand_value(ea, 0))
        assert need_handle == False
        ida_bytes.patch_bytes(ea, asm('%s 0x%x' % (mnem, jcc_target), ea).ljust(idc.get_item_size(ea), b'\x90'))
 
    ea += get_item_size(ea)

最后完整代码:

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
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
from queue import *
from idc import *
import idc
from keystone import *
from capstone import *
 
asmer = Ks(KS_ARCH_X86, KS_MODE_64)
disasmer = Cs(CS_ARCH_X86, CS_MODE_64)
 
 
def disasm(machine_code, addr=0):
    l = ""
    for i in disasmer.disasm(machine_code, addr):
        l += "{:8s} {};\n".format(i.mnemonic, i.op_str)
    return l.strip('\n')
 
 
def asm(asm_code, addr=0):
    l = b''
    for i in asmer.asm(asm_code, addr)[0]:
        l += bytes([i])
    return l
 
 
class RelocDSU:
 
    def __init__(self):
        self.reloc = {}
 
    def get(self, ea):
        if ea not in self.reloc:
            if idc.print_insn_mnem(ea) == 'jmp' and idc.get_operand_type(ea, 0) != idc.o_reg:
                jmp_ea = idc.get_operand_value(ea, 0)
 
                if idc.get_segm_name(jmp_ea) == '.idata':
                    self.reloc[ea] = ea
                    return self.reloc[ea], False
 
                self.reloc[ea], need_handle = self.get(idc.get_operand_value(ea, 0))
                return self.reloc[ea], need_handle
            else:
                self.reloc[ea] = ea
        if self.reloc[ea] != ea: self.reloc[ea] = self.get(self.reloc[ea])[0]
        return self.reloc[ea], idc.get_segm_name(self.reloc[ea]) in ['.text', '.aaa']
 
    def merge(self, ea, reloc_ea):
        # print((hex(ea), hex(reloc_ea)))
        self.reloc[self.get(ea)[0]] = self.get(reloc_ea)[0]
 
 
reloc = RelocDSU()
 
 
class Block:
    def __init__(self, start_ea, asm_list, direct_next, branch_list, call_target):
        self.start_ea = start_ea
        self.asm_list = asm_list
        self.direct_next = direct_next
        self.branch_list = branch_list
        self.call_target = call_target
 
    def __str__(self):
        return 'start_ea: 0x%x\ndirect_next: 0x%x\ncall_target: 0x%x\nbranch_list: %s\nasm_list:\n%s\n' % (
            0 if self.start_ea == None else self.start_ea,
            0 if self.direct_next == None else self.direct_next,
            0 if self.call_target == None else self.call_target,
            str([hex(x) for x in self.branch_list]),
            str('\n'.join([hex(addr) + '    ' + asm for addr, asm in self.asm_list]))
        )
 
 
def get_block(start_ea):
    ea = start_ea
    stack = []
    asm_list = []
    branch_list = []
    call_target = None
    direct_next = None
 
    while True:
        idc.create_insn(ea)
        mnem = idc.print_insn_mnem(ea)
 
        # 处理混淆中跳转的情况
        if mnem == 'pushfq':
            ea += idc.get_item_size(ea)
 
            assert idc.get_bytes(ea, idc.get_item_size(ea)) == b'\xE8\x00\x00\x00\x00'
            ea += idc.get_item_size(ea)
            jmp_base = ea
 
            assert idc.print_insn_mnem(ea) == 'pop' and idc.get_operand_type(ea, 0) == o_reg
            reg = idc.print_operand(ea, 0)
            ea += idc.get_item_size(ea)
 
            assert idc.print_insn_mnem(ea) == 'add' and idc.print_operand(ea, 0) == reg
            assert idc.get_operand_type(ea, 1) == o_imm
 
            jmp_target = (jmp_base + idc.get_operand_value(ea, 1)) & 0xFFFFFFFFFFFFFFFF
            ea += idc.get_item_size(ea)
 
            assert idc.get_bytes(ea, idc.get_item_size(ea)) == asm('mov [rsp + 0x10], %s' % reg, ea)
            ea += idc.get_item_size(ea)
 
            assert idc.print_insn_mnem(ea) == 'popfq'
            ea += idc.get_item_size(ea)
 
            assert idc.print_insn_mnem(ea) == 'pop' and idc.print_operand(ea, 0) == reg
            assert len(stack) != 0 and stack[-1][0] == 'push' and stack[-1][1] == reg
            stack.pop()
            asm_list.pop()
 
            assert len(stack) != 0 and stack[-1][0] == 'push' and stack[-1][1] == reg
            stack.pop()
            asm_list.pop()
 
            ea += idc.get_item_size(ea)
 
            if idc.print_insn_mnem(ea) == 'retn':
                if direct_next == None:
                    direct_next = jmp_target
                elif call_target == None:
                    call_target = jmp_target
                    asm_list.append((0, 'call 0x%x' % (call_target)))
                else:
                    print("××××××")
                    assert False
                break
            else:
                assert call_target == None and direct_next == None
                direct_next = jmp_target
                continue
 
        if mnem == 'push':
            stack.append((mnem, idc.print_operand(ea, 0)))
        elif mnem == 'pop':
            if len(stack) != 0 and stack[-1][0] == 'push' and stack[-1][1] == idc.print_operand(ea, 0):
                stack.pop()
                asm_list.pop()
                ea += idc.get_item_size(ea)
                continue
            else:
                stack.clear()
        else:
            stack.clear()
 
        asm_list.append((ea, disasm(idc.get_bytes(ea, idc.get_item_size(ea)), ea)))
 
        if mnem == 'retn': break
        if mnem[0] == 'j' and mnem != 'jmp' and idc.get_operand_type(ea, 0) != o_reg:
            branch_list.append(idc.get_operand_value(ea, 0))
 
        if mnem == 'jmp':
            if idc.get_segm_name(idc.get_operand_value(ea, 0)) not in ['.text', '.aaa']:
                break
            else:
                ea = idc.get_operand_value(ea, 0)
        else:
            ea += idc.get_item_size(ea)
 
    return Block(start_ea, asm_list, direct_next, branch_list, call_target)
 
 
entry_point = 0x1400133B7
new_code_start = 0x14001D000
 
 
def solve():
    for i in range(0x10000):
        idc.set_name(new_code_start + i, '')
        idc.del_items(new_code_start + i)
    ida_bytes.patch_bytes(new_code_start, b'\x90' * 0x10000)
 
    func_queue = Queue()
    func_queue.put(entry_point)
    new_code_ea = new_code_start
 
    while not func_queue.empty():
        func_address = func_queue.get()
        if reloc.get(func_address)[1] == False: continue
        branch_queue = Queue()
        branch_queue.put(func_address)
 
        while not branch_queue.empty():
            branch_address = branch_queue.get()
            ea = branch_address
 
            while True:
                block = get_block(ea)
                reloc.merge(ea, new_code_ea)
 
                for addr, insn in block.asm_list:
                    insn = insn.replace('rip', 'rip - 0x%x' % (new_code_ea - addr))
                    if insn == 'bnd ret  ;':
                        code = b'\xF2\xC3'
                    else:
                        code = asm(insn, new_code_ea)
                    ida_bytes.patch_bytes(new_code_ea, code)
                    if addr != 0: reloc.merge(addr, new_code_ea)
                    new_code_ea += len(code)
 
                if block.call_target != None:
                    call_target, need_handle = reloc.get(block.call_target)
                    if need_handle: func_queue.put(call_target)
 
                for branch_address in block.branch_list:
                    jcc_target, need_handle = reloc.get(branch_address)
                    if need_handle: branch_queue.put(jcc_target)
 
                if block.direct_next == None: break
 
                next_target, need_handle = reloc.get(block.direct_next)
                if need_handle == False:
                    code = asm('jmp 0x%x' % (next_target), new_code_ea)
                    ida_bytes.patch_bytes(new_code_ea, code)
                    new_code_ea += len(code)
                    break
                else:
                    ea = block.direct_next
 
    ea = new_code_start
    while ea < new_code_ea:
        assert idc.create_insn(ea) != 0
        mnem = idc.print_insn_mnem(ea)
 
        if mnem == 'call':
            call_target, need_handle = reloc.get(get_operand_value(ea, 0))
            if need_handle == True:
                if idc.print_insn_mnem(call_target) == 'jmp' and idc.get_segm_name(idc.get_operand_value(call_target, 0)) == '.idata':
                    ea += get_item_size(ea)
                    continue
                else:
                    assert False
            ida_bytes.patch_bytes(ea, asm('call 0x%x' % (call_target), ea).ljust(idc.get_item_size(ea), b'\x90'))
        elif mnem[0] == 'j' and idc.get_operand_type(ea, 0) != idc.o_reg:
            jcc_target, need_handle = reloc.get(get_operand_value(ea, 0))
            assert need_handle == False
            ida_bytes.patch_bytes(ea, asm('%s 0x%x' % (mnem, jcc_target), ea).ljust(idc.get_item_size(ea), b'\x90'))
 
        ea += get_item_size(ea)
 
    for i in range(0x10000): idc.del_items(new_code_start + i)
    idc.jumpto(new_code_start)
    idc.add_func(new_code_start)
 
    print("finish")
 
 
solve()

[培训]《安卓高级研修班(网课)》月薪三万计划,掌握调试、分析还原ollvm、vmp的方法,定制art虚拟机自动化脱壳的方法

最后于 2023-8-29 19:13 被sky_123编辑 ,原因:
收藏
点赞35
打赏
分享
打赏 + 2.00雪花
打赏次数 2 雪花 + 2.00
 
赞赏  jelasin   +1.00 2023/09/21 厉害!!!
赞赏  R0g   +1.00 2023/09/19
最新回复 (21)
雪    币: 169
活跃值: (392)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
ldzspace 2023-8-29 23:17
2
0
学习
雪    币: 19461
活跃值: (29125)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
秋狝 2023-8-30 09:07
3
1
感谢分享
雪    币: 964
活跃值: (1265)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
mb_fssslkzs 2023-8-30 10:21
4
0
感谢分享
雪    币: 295
活跃值: (213427)
能力值: ( LV4,RANK:40 )
在线值:
发帖
回帖
粉丝
shinratensei 1 2023-8-30 10:42
5
0
tql
雪    币: 8921
活跃值: (3364)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
chengdrgon 2023-8-30 13:37
6
0
感谢分享,强
雪    币: 8283
活跃值: (4811)
能力值: ( LV4,RANK:45 )
在线值:
发帖
回帖
粉丝
v0id_ 2023-8-30 16:20
7
0
难得的讲的明白的好文章,强
雪    币: 3377
活跃值: (3412)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
fengyunabc 1 2023-8-30 21:49
8
0
感谢分享!
雪    币: 7924
活跃值: (3102)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
ldljlzw 2023-8-31 09:49
9
0
感谢分享
雪    币: 573
活跃值: (949)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
ChengQing 2023-8-31 09:55
10
0
1024
雪    币: 1699
活跃值: (760)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
wx_Dispa1r 2023-8-31 11:17
11
0
冷饭嗯炒
雪    币: 16
活跃值: (1487)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
mb_rhynjqzk 2023-8-31 17:17
12
0
楼主能不能说下这个流程图是在哪里画的啊
雪    币: 15
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
breezes11 2023-8-31 17:25
13
0
看的我头晕
雪    币: 1040
活跃值: (601)
能力值: ( LV4,RANK:40 )
在线值:
发帖
回帖
粉丝
sky_123 1 2023-9-1 07:59
14
0
mb_rhynjqzk 楼主能不能说下这个流程图是在哪里画的啊[em_3]
draw.io
雪    币: 16
活跃值: (1487)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
mb_rhynjqzk 2023-9-1 11:22
15
0
sky_123 draw.io
灰常感谢!
雪    币: 1412
活跃值: (3226)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
方向感 2023-9-2 02:46
16
0
ida用哪个版本?7.7咋不行呢?
雪    币: 5453
活跃值: (1422)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
DemoCc 2023-9-2 10:27
17
0
先收藏下 以后学到了再来
雪    币: 1040
活跃值: (601)
能力值: ( LV4,RANK:40 )
在线值:
发帖
回帖
粉丝
sky_123 1 2023-9-2 23:11
18
0
方向感 ida用哪个版本?7.7咋不行呢?
7.7 应该可以,你是不是忘了在程序后面新创建一个段用于存放去混淆的代码?
雪    币: 1174
活跃值: (1177)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
cmputer 2023-9-4 15:25
19
0
mark
雪    币: 1517
活跃值: (3290)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
小希希 2023-9-19 12:21
20
0
感谢分享
雪    币:
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
ac_ 2024-3-4 00:02
21
0
第二题脚本新建了段,为什么会出现断言错误
雪    币: 1040
活跃值: (601)
能力值: ( LV4,RANK:40 )
在线值:
发帖
回帖
粉丝
sky_123 1 2024-3-4 12:46
22
0
ac_ 第二题脚本新建了段,为什么会出现断言错误
附件里面的idb可以正常去混淆,可以对比看看哪里设置错了。
游客
登录 | 注册 方可回帖
返回