分析1

「影」核心系统「根」需要在一些开启了特殊特性的机器上才能部署成功,逆向找到成功部署条件!成功部署「影」核心系统,即成功运行shadow_panel.exe,控制台程序成功运行进入至输入终止密码的终端。(满分0.5分

首次运行提示,这里运行后卡死,猜测是检测环境失败的措施,提前需要打快照

搜索字符串交叉引用找到

这里能看出来,哪个是0,就说明是过了,那么看来是1没过,这里借助agent分析,得到

这个嫌疑函数,并且得到要求

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
shadow_panel.exe 会检查机器是否满足这组“特殊特性”:

- 必须存在 hypervisor,CPUID(1).ECX[31] = 1
- hypervisor 厂商必须是 Microsoft Hv,也就是机器当前跑在 Hyper-V 之上,而不是普通裸机/VMware/KVM
- 还会继续检查 ACPI 固件表里是否存在
- DMAR
- 或 IVRS

- DMAR:Intel VT-d / DMA Remapping
- IVRS:AMD IOMMU / AMD-Vi

关键证据在这两个函数:

- [sub_144A07540] 用 cpuid 检查 hypervisor bit,然后读取 0x40000000 叶子,硬编码比较 "Microsoft Hv"
- [sub_144A06650] 动态取系统固件表接口,查询 ACPI 表,并扫描是否有 DMAR / IVRS

所以你现在可以把(1)的核心部署条件先写成:

“影”核心系统“根”要成功部署,目标机器需要开启并运行 Microsoft Hyper-V,且 BIOS/平台侧启用了 IOMMU/VT-d 能力;程序会通过
ACPI 表中的 DMAR(Intel)或 IVRS(AMD)来确认这一点。

更直白一点:

- Intel 机器:要有 Hyper-V + VT-d(DMAR)
- AMD 机器:要有 Hyper-V + AMD IOMMU/IVRS

补充一点,样本里还有 VHD/virtdisk、驱动服务创建启动链路,所以实际运行时还需要系统支持虚拟磁盘接口和驱动部署,但“特殊特
性机器”这一条,当前逆到的核心就是上面这组 Hyper-V + DMAR/IVRS 条件。

配好win10 1909的虚拟机后,开启vt,又报了4的错误,那么进一步去看has_acpi_dmar_or_ivrs_table函数情况,如果返回0就对了

这里还原代码自己跑的时候也是不行的,但是我的DMAR环境在RW里面看确实是好了,所以只能先patch掉这里了

第一关过了,看进度感觉还有3关,那么sub_144A04160函数就很明显是中心派发的函数,每轮断点在

(*(*v11 + 16LL))(v11, &v45);就可以

那么就得到加上第一个的四个函数,这里直接放我的结论

函数1:检测vt环境以及DMAR

函数2:初始化,同时可以检测是否管理员启动

函数3:内存提取文件并落地,经过一个混淆的函数,删除服务(这里感觉有点像题目里面的隐藏)

函数4:调用cpuid,对比模数

函数3

这里会创建一个sys文件,后面会从 +0x5cca010h 位置是这个sys文件

函数4

在函数3里面会调用一样的代码

1
2
exec_cpuid_leaf_subleaf(0xDEADBEEF, 0x114514u, 0, 0, out_regs_);
if ( *out_regs_ == 0x1919810 )

但是在3里面就有,4就没有,不过我先nop了,先得到最终提交flag界面

分析2

「根」使用特殊方法,对操作系统底层进行了攻击,并借此将关键核心代码隐藏了起来,分析其完整实现流程。(满分2.5分)

这里前文分析1中的函数3里面有一个混淆的函数,初步猜测这个就是关键攻击的核心代码

进去发现是个llvm吧,写一个idc脚本去除这个混淆,哦这里也对应着第三点,编写检测代码检测特殊方式吧,检测代码就是去混淆代码吧

阅读规则才看见要完整提供提示词聊天记录,前面的确实没了,因为是终端开的agent,后面的都加上

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
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
0000000147F188A4这里开始是一段llvm的混淆间接跳转代码,你分析一下,并尝试给我idc脚本去除

• 我先定位 0x147F188A4 所在函数,读汇编和基本块,判断这段 LLVM 间接跳转混淆的具体形态,再给出可落地的 IDC 去除脚本。

• Called ida-pro-mcp.get_bytes({"regions":[{"addr":"0x147F188A4","size":128}]})
└ [{"addr": "0x147F188A4", "data": "0x48 0x8d 0xa4 0x24 0x38 0xf4 0xff 0xff 0x48 0x89 0x6c 0x24 0x10 0x48 0x89 0xe5
0x9c 0x8f 0x45 0x0 0x4c 0x89 0x75 0x78 0x49 0xf7 0xd0 0x4c 0x87 0xc2 0x4c 0x89 0x45 0x30 0x48 0xf7 0xd2 0x4c
0x87 0xc2 0x49 0xf7 0xd0 0x4d 0x87 0xc1 0x4c 0x89 0x45 0x50 0x49 0xf7 0xd1 0x4d 0x87 0xc1 0x49 0xf7 0xd1 0x41
0x50 0x4c 0x8d 0x5 0x45 0xc7 0xcc 0xc4 0x4d 0x8d 0x80 0x10 0x6f 0x33 0x3b 0x41 0xff 0xe0 0xe8 0x90 0x4c 0x8d
0x9d 0x90 0x0 0x0 0x0 0x48 0x91 0x49 0x8b 0xb 0x48 0xf7 0xd1 0x48 0x91 0x48 0xf7 0xd0 0x49 0x8b 0x5b 0x8 0x48
0x87 0xca 0x49 0x8b ...

• Called ida-pro-mcp.disasm({"addr":"0x147F188A4","max_instructions":120,"include_total":true})
└ {"addr": "0x147F188A4", "asm": {"name": "sub_147F188A4", "start_ea": "0x147f188a4", "lines": "sub_147F188A4 (.tvm0 @
0x147f188a4):\n147f188a4 lea rsp, [rsp-0BC8h]\n147f188ac mov [rsp+var_s10], rbp\n147f188b1 mov
rbp, rsp\n147f188b4 pushfq\n147f188b5 pop [rbp+var_s0]\n147f188b8 mov [rbp+var_s78], r14\n147f188bc
not r8\n147f188bf xchg r8, rdx\n147f188c2 mov [rbp+var_s30], r8\n147f188c6 not rdx\n147f188c9
xchg r8, rdx\n147f188cc not r8\n147f188cf xchg r8, r9\n147f188d2 mov [rbp+var_s50],
r8\n147f188d6 not ...

• Called ida-pro-mcp.lookup_funcs({"queries":"0x147F1BF3D"})
└ [{"query": "0x147F1BF3D", "fn": null, "error": "Not a function"}]

r13\n147f1bf6a not r11\...

• Called ida-pro-mcp.xrefs_to({"addrs":"0x147F188A4","limit":50})
└ [{"addr": "0x147F188A4", "xrefs": [{"addr": "0x144a068b0", "type": "code", "fn": {"addr": "0x144a068b0", "name":
"sub_144A068B0", "size": "0x5"}}, {"addr": "0x148220124", "type": "data", "fn": null}], "more": false}]

• Ran [UInt64](0x10CBE502D) + [UInt64](0x3B336F10); [UInt64](0xEF657195) + [UInt64](0x588C4116)
└ 5501992765
无法将值“-278564459”转换为类型“System.UInt64”。错误:“值对于 UInt64 太大或太小。”
… +4 lines (ctrl + t to view transcript)

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

• 定位结果更清楚了:这是一串串接的“状态块”,每个块末尾都通过常量构造目标地址再 jmp,不是基于运行时表索引的 switch。也就
是说,去除方式可以做成“识别末尾跳转目标后,直接补成短的无条件跳转,并让 IDA 重新建图”。

47F1B2AB

• Called ida-pro-mcp.lookup_funcs({"queries":["0x147F1BF3D","0x147F1B2AB"]})
└ [{"query": "0x147F1BF3D", "fn": null, "error": "Not a function"}, {"query": "0x147F1B2AB", "fn": null, "error": "Not
a function"}]

• Called ida-pro-mcp.basic_blocks({"addrs":["0x147F188A4","0x147F1BF3D"],"max_blocks":20})
└ [{"addr": "0x147F188A4", "blocks": [{"start": "0x147f188a4", "end": "0x147f188f2", "size": 78, "type": 1,
"successors": [], "predecessors": []}], "count": 1, "total_blocks": 1, "cursor": {"done": true}, "error": null},
{"addr": "0x147F1BF3D", "error": "Function not found", "blocks": [], "cursor": {"done": true}}]

• Called ida-pro-mcp.xrefs_to({"addrs":["0x147F1BF3D","0x147F1B2AB"],"limit":50})
└ [{"addr": "0x147F1BF3D", "xrefs": [], "more": false}, {"addr": "0x147F1B2AB", "xrefs": [], "more": false}]

• Called ida-pro-mcp.disasm({"addr":"0x147F1B2AB","max_instructions":80,"include_total":true})
└ {"addr": "0x147F1B2AB", "asm": {"name": "<no function>", "start_ea": "0x147f1b2ab", "lines": "<no function> (.tvm0 @
0x147f1b2ab):\n147f1b2ab dw 41E9h, 4859h, 7589h\n147f1b2b0 cmp [rcx+rcx*4+65h], cl\n147f1b2b4 push
48D2F748h\n147f1b2b9 xchg edx, ecx\n147f1b2bb mov [rbp+20h], rdx\n147f1b2bf not rcx\n147f1b2c2
xchg rdx, rcx\n147f1b2c5 pushfq\n147f1b2c6 add qword ptr [rbp+40h], 0BC8h\n147f1b2ce popfq\n147f1b2cf

• Called ida-pro-mcp.get_bytes({"regions":[{"addr":"0x147F1BF3D","size":128},{"addr":"0x147F1B2AB","size":96}]})
└ [{"addr": "0x147F1BF3D", "data": "0x41 0x58 0x49 0x91 0x4c 0x89 0x4d 0x8 0x48 0xf7 0xd0 0x49 0x91 0x49 0xf7 0xd2
0x4d 0x87 0xd3 0x4c 0x89 0x55 0x60 0x49 0xf7 0xd3 0x4d 0x87 0xd3 0x48 0x89 0x5d 0x18 0x48 0x89 0x65 0x40 0x48
0x89 0x7d 0x28 0x4c 0x89 0x6d 0x70 0x49 0xf7 0xd3 0x4d 0x87 0xd8 0x4c 0x89 0x5d 0x48 0x49 0xf7 0xd0 0x4d 0x87
0xd8 0x4c 0x89 0xbd 0x80 0x0 0x0 0x0 0x48 0xf7 0xd0 0x49 0x92 0x48 0x89 0x45 0x58 0x49 0xf7 0xd2 0x49 0x92 0x41
0x51 0x49 0xb9 0x95 0x71 0x65 0xef 0x0 0x0 0x0 0x0 0x9c 0x49 0x81 0xc1 0x16 0x41 0x8c 0x58 0x9d 0x41 0xff 0xe1
0xe9 0x90 0x4c 0...

• Called ida-pro-mcp.disasm({"addr":"0x144A068B0","max_instructions":30,"include_total":true})
└ {"addr": "0x144A068B0", "asm": {"name": "sub_144A068B0", "start_ea": "0x144a068b0", "lines": "sub_144A068B0 (.text @
0x144a068b0):\n144a068b0 jmp sub_147F188A4", "return_type": "void", "arguments": [{"name": "p_lpFileName",
"type": "__int64"}, {"name": "p_lpServiceName", "type": "__int64"}, {"name": "arg2", "type": "__int64"},
{"name": "arg3", "type": "__int64"}]}, "instruction_count": 1, "total_instructions": 1, "cursor": {"done":
true}}

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

• 这套 LLVM flatten/indirect-jump 模板已经能归纳出来了:块内用 push scratch_reg 保存一个临时寄存器,构造常量地址到同一个
寄存器,再 jmp reg;目标块开头第一个指令通常就是对应的 pop scratch_reg。我现在按这个模板给你写 IDC:先解码目标地址,再
把末尾改成 E9 rel32 + NOP,并递归清理后续块。

• Called ida-pro-mcp.disasm({"addr":"0x147F1B513","max_instructions":40,"include_total":true})
└ {"addr": "0x147F1B513", "asm": {"name": "<no function>", "start_ea": "0x147f1b513", "lines": "<no function> (.tvm0 @
0x147f1b513):\n147f1b513 dw 41E9h, 0FF5Ah, 2875h\n147f1b518 pop qword ptr [r11+38h]\n147f1b51c push
qword ptr [rbp+48h]\n147f1b51f pop qword ptr [r11+40h]\n147f1b523 push qword ptr [rbp+50h]\n147f1b526
pop qword ptr [r11+48h]\n147f1b52a push qword ptr [rbp+58h]\n147f1b52d pop qword ptr
[r11+50h]\n147f1b531 push qword ptr [rbp+60h]\n147f1b534 pop qword ptr [r11+58h]\n147f1b538 push
qword ptr [rbp+68h]\n147f...

• Called ida-pro-mcp.get_bytes({"regions":[{"addr":"0x147F1B300","size":32},{"addr":"0x147F188DF","size":24}]})
└ [{"addr": "0x147F1B300", "data": "0x75 0x38 0x41 0x8f 0x43 0x30 0x41 0x52 0x4c 0x8d 0x15 0x18 0x1 0x0 0x0 0x4d 0x8d
0x92 0xec 0x0 0x0 0x0 0x41 0xff 0xe2 0xe9 0x90 0x4c 0x8d 0x9d 0x90 0x0"}, {"addr": "0x147F188DF", "data": "0x41
0x50 0x4c 0x8d 0x5 0x45 0xc7 0xcc 0xc4 0x4d 0x8d 0x80 0x10 0x6f 0x33 0x3b 0x41 0xff 0xe0 0xe8 0x90 0x4c 0x8d
0x9d"}]

─ Worked for 1m 39s ────────────────────────────────────────────────────────────────────────────────────────────────────

• 这段我看过了,形态很典型,不是“运行时决定去哪里”的真实间接跳转,而是 LLVM/OLLVM 风格的块间跳转伪装。

在 0x147F188A4 (0x147F188A4) 这个块里,前面是寄存器扰动和保存现场,真正的跳转尾巴是:

147F188DF push r8
147F188E1 lea r8, cs:10CBE502Dh
147F188E8 lea r8, [r8+3B336F10h]
147F188EF jmp r8

这会跳到 0x147F1BF3D。目标块开头正好是:

147F1BF3D pop r8

也就是说这里的 push r8 只是为了把临时寄存器穿过跳转,真实语义就是“跳到常量地址的下一个块”。

下一跳也是同一套路:

147F1BF8F push r9
147F1BF91 mov r9, 0EF657195h
147F1BF9B pushfq
147F1BF9C add r9, 588C4116h
147F1BFA3 popfq
147F1BFA4 jmp r9

目标是 0x147F1B2AB,开头是 pop r9。所以去混淆的核心策略是:

1. 保留前面的 push scratch_reg
2. 把“构造地址 + jmp reg”改成直接 jmp real_target
3. 让 IDA 重新建代码和 CFG

下面这个 IDC 就是按这个思路写的。它从你给的地址开始,识别两种我已经在这段里确认过的模板,然后递归/链式地一路补成直跳。

#include <idc.idc>

static s32(v)
{
if (v & 0x80000000)
return v - 0x100000000;
return v;
}

static patch_rel_jmp(src, end_ea, dst)
{
auto len, rel, i;

len = end_ea - src;
if (len < 5)
{
warning("patch_rel_jmp: range too small at %a", src);
return 0;
}

del_items(src, DELIT_SIMPLE, len);

rel = dst - (src + 5);
patch_byte(src, 0xE9);
patch_dword(src + 1, rel & 0xFFFFFFFF);

for (i = 5; i < len; i = i + 1)
patch_byte(src + i, 0x90);

create_insn(src);
create_insn(dst);
auto_wait();
return 1;
}

static is_pop_reg(ea, reg)
{
create_insn(ea);
return strcmp(print_insn_mnem(ea), "pop") == 0 && strcmp(print_operand(ea, 0), reg) == 0;
}

static try_patch_block(block_ea)
{
auto ea, limit;
auto reg;
auto a, b, c, d, e;
auto target, base, imm;
auto cmt;

cmt = get_cmt(block_ea, 0);
if (cmt != "" && strstr(cmt, "patched_llvm_ijmp") != -1)
return BADADDR;

create_insn(block_ea);
limit = block_ea + 0x200;

for (ea = block_ea; ea != BADADDR && ea < limit; ea = next_head(ea, limit))
{
if (strcmp(print_insn_mnem(ea), "push") != 0)
continue;

reg = print_operand(ea, 0);

a = next_head(ea, limit);
b = next_head(a, limit);
c = next_head(b, limit);

// Pattern A:
// push reg
// lea reg, <abs>
// lea reg, [reg+imm32]
// jmp reg
if (a != BADADDR && b != BADADDR && c != BADADDR &&
strcmp(print_insn_mnem(a), "lea") == 0 &&
strcmp(print_insn_mnem(b), "lea") == 0 &&
strcmp(print_insn_mnem(c), "jmp") == 0 &&
strcmp(print_operand(a, 0), reg) == 0 &&
strcmp(print_operand(b, 0), reg) == 0 &&
strcmp(print_operand(c, 0), reg) == 0)
{
base = get_operand_value(a, 1);
imm = get_operand_value(b, 1);
target = base + s32(imm);

if (is_pop_reg(target, reg))
{
if (patch_rel_jmp(a, c + get_item_size(c), target))
{
set_cmt(block_ea, form("patched_llvm_ijmp -> %a", target), 0);
msg("[+] Pattern A patched: %a -> %a\n", block_ea, target);
return target;
}
}
}

d = next_head(c, limit);
e = next_head(d, limit);

// Pattern B:
// push reg
// mov reg, imm64
// pushfq
// add reg, imm32
// popfq
// jmp reg
if (a != BADADDR && b != BADADDR && c != BADADDR &&
d != BADADDR && e != BADADDR &&
strcmp(print_insn_mnem(a), "mov") == 0 &&
strcmp(print_insn_mnem(b), "pushfq") == 0 &&
strcmp(print_insn_mnem(c), "add") == 0 &&
strcmp(print_insn_mnem(d), "popfq") == 0 &&
strcmp(print_insn_mnem(e), "jmp") == 0 &&
strcmp(print_operand(a, 0), reg) == 0 &&
strcmp(print_operand(c, 0), reg) == 0 &&
strcmp(print_operand(e, 0), reg) == 0)
{
base = get_operand_value(a, 1);
imm = get_operand_value(c, 1);
target = base + s32(imm);

if (is_pop_reg(target, reg))
{
if (patch_rel_jmp(a, e + get_item_size(e), target))
{
set_cmt(block_ea, form("patched_llvm_ijmp -> %a", target), 0);
msg("[+] Pattern B patched: %a -> %a\n", block_ea, target);
return target;
}
}
}
}

msg("[-] no supported llvm-ijmp pattern at %a\n", block_ea);
return BADADDR;
}

static deobf_chain(start_ea, max_blocks)
{
auto cur, i, next;

cur = start_ea;
for (i = 0; i < max_blocks; i = i + 1)
{
next = try_patch_block(cur);
if (next == BADADDR)
break;
cur = next;
}
}

static main()
{
auto start_ea, max_blocks;

start_ea = 0x147F188A4;
max_blocks = 128;

deobf_chain(start_ea, max_blocks);
}

几点说明:

- 这个脚本做的是“线性化 CFG”,不是把前面的 not/xchg 也优化掉。
- 关键是保留那个 push reg,只改后半段,否则目标块开头的 pop reg 会把栈弄坏。
- 这段里我已经确认的跳转链至少有:
- 0x147F188A4 -> 0x147F1BF3D
- 0x147F1BF3D -> 0x147F1B2AB
- 0x147F1B2AB -> 0x147F1B513

如果你要,我下一步可以继续直接按这条链往下分析,把这整个 dispatcher/function 还原成更可读的块图,或者把这个 IDC 再扩
成“扫描整个 .tvm0 段批量去掉这类跳转”。
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
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
• 我再扩一圈看这条链和邻近块,不假设只有一种模板。重点检查几类尾跳:lea/lea/jmp reg、mov/add/jmp reg、带 pushfq/popfq 的
变体,以及是否存在从内存/表里取地址后 jmp reg 的真间接跳转。

• Called ida-pro-mcp.disasm({"addr":"0x147F188A4","max_instructions":220,"include_total":true})
└ {"addr": "0x147F188A4", "asm": {"name": "sub_147F188A4", "start_ea": "0x147f188a4", "lines": "sub_147F188A4 (.tvm0 @
0x147f188a4):\n147f188a4 lea rsp, [rsp-0BC8h]\n147f188ac mov [rsp+var_s10], rbp\n147f188b1 mov
rbp, rsp\n147f188b4 pushfq\n147f188b5 pop [rbp+var_s0]\n147f188b8 mov [rbp+var_s78], r14\n147f188bc
not r8\n147f188bf xchg r8, rdx\n147f188c2 mov [rbp+var_s30], r8\n147f188c6 not rdx\n147f188c9
xchg r8, rdx\n147f188cc not r8\n147f188cf xchg r8, r9\n147f188d2 mov [rbp+var_s50],
r8\n147f188d6 not ...

• Called ida-pro-mcp.disasm({"addr":"0x147F1BF3D","max_instructions":220,"include_total":true})
└ {"addr": "0x147F1BF3D", "asm": {"name": "<no function>", "start_ea": "0x147f1bf3d", "lines": "<no function> (.tvm0 @
0x147f1bf3d):\n147f1bf3d pop r8\n147f1bf40 xchg eax, ecx\n147f1bf41 mov [rbp+8], r9\n147f1bf45
not rax\n147f1bf48 xchg rax, r9\n147f1bf4a not r10\n147f1bf4d xchg r10, r11\n147f1bf50 mov
r13\n147f1bf6a not r11\...

• Called ida-pro-mcp.disasm({"addr":"0x147F1B2AB","max_instructions":220,"include_total":true})
48D2F748h\n147f1b2b9 xchg edx, ecx\n147f1b2bb mov [rbp+20h], rdx\n147f1b2bf not rcx\n147f1b2c2
xchg rdx, rcx\n147f1b2c5 pushfq\n147f1b2c6 add qword ptr [rbp+40h], 0BC8h\n147f1b2ce popfq\n147f1b2cf
lea r11, [rbp+90h]\n147f1b2d6 push qword ptr [rbp+8]\n147f1b2d9 pop qword ptr [r11]\n147f1b2dc
push qword ...

• Called
└ ida-pro-mcp.find_bytes({"patterns":["41 FF E0","41 FF E1","41 FF E2","41 FF E3","41 FF E4","41 FF E5","41 FF E6","41
FF E7","FF E0","FF E1","FF E2","FF E3","FF E4","FF E5","FF E6","FF E7"],"limit":200})
[{"pattern": "41 FF E0", "matches": ["0x144a1859e", "0x14589fc30", "0x147a8838f", "0x147a943c4", "0x147a98989",
"0x147aa5d4f", "0x147ab07fa", "0x147ab1053", "0x147ab5d41", "0x147aba4b1", "0x147aba5e2", "0x147abab66",
"0x147abac36", "0x147abad7e", "0x147abaf8b", "0x147abc298", "0x147abc776", "0x147abc927", "0x147abd84c",
"0x147abdb1b", "0x147abddda", "0x147abe880", "0x147abeace", "0x147abf133", "0x147abf8e7", "0x147abfb2f",
"0x147abfd73", "0x147ac01f2", "0x147ac0e97", "0x147ac167f", "0x147ac24d8", "0x147ac3751", "0x147ac3a98",
"0x147ac3f4e", "0x147ac451a", "0x147ac5e...

─ Worked for 2m 40s ────────────────────────────────────────────────────────────────────────────────────────────────────

• 你这个质疑是对的。全局看绝对不止一种 jmp r*,而且就你这条链附近,也已经不止一个模板。我再把当前函数后半段补几百条,看
它本地到底有几类尾跳。

• Called ida-pro-mcp.disasm({"addr":"0x147F1C230","max_instructions":180,"include_total":true})
└ {"addr": "0x147F1C230", "asm": {"name": "<no function>", "start_ea": "0x147f1c230", "lines": "<no function> (.tvm0 @
0x147f1c230):\n147f1c230 mov r11, [r11+58h]\n147f1c234 mov rsp, [rsp]\n147f1c238 call
cs:CloseHandle\n147f1c23e lea rsp, [rsp-0BC8h]\n147f1c246 mov [rsp+10h], rbp\n147f1c24b mov rbp,
rsp\n147f1c24e pushfq\n147f1c24f pop qword ptr [rbp+0]\n147f1c252 mov [rbp+78h], r14\n147f1c256 not
r10\n147f1c259 xchg r10, rdx\n147f1c25c mov [rbp+30h], r10\n147f1c260 not rdx\n147f1c263 xchg
r10, rdx\n147f1...

• Called
└ ida-pro-mcp.find_bytes({"patterns":["FF 23","FF 20","FF 21","FF 22","FF 24 24","48 FF 20","48 FF 21","48 FF 22","48
FF 23","41 FF 24 24"],"limit":100})
[{"pattern": "FF 23", "matches": ["0x1401a3e6d", "0x140603e75", "0x140806294", "0x1408062dd", "0x1408062f8",
"0x1409b3e6d", "0x1409d3e75", "0x141228442", "0x141743e75", "0x1418061a4", "0x1418062c2", "0x1418062e6",
"0x141806a89", "0x141a33e75", "0x141a6ce65", "0x142043e6d", "0x142163e75", "0x1421fc2c9", "0x1428062b9",
"0x14280632f", "0x142806a9c", "0x142806b15", "0x142a6cee5", "0x142c04611", "0x142d93e6d", "0x142de632b",
"0x142ee3e75", "0x14310ab2d", "0x14380627d", "0x143806298", "0x143a6cea5", "0x143d08957", "0x143e03e75",
"0x14433eefd", "0x144353e6d", "0x144609cbd"...

• Called ida-pro-mcp.find_regex({"pattern":"tvm0","limit":20})
└ {"n": 1, "matches": [{"addr": "0x145cca27f", "string": "B.tvm0"}], "cursor": {"done": true}}

─ Worked for 2m 01s ────────────────────────────────────────────────────────────────────────────────────────────────────

• 不确定,而且现在可以明确说:不是只有一种。

我刚把你这条链后半段继续看了,当前这片 .tvm0 里至少已经确认了两种 jmp r* 混淆模板,而且它们都出现在同一条执行链附近。

已确认的 jmp reg 模板:

1. push reg + lea/lea + jmp reg
在 /E:/:1 这段尾巴:

147F188DF push r8
147F188E1 lea r8, cs:10CBE502Dh
147F188E8 lea r8, [r8+3B336F10h]
147F188EF jmp r8

还有 /E:/:1:

147F1B306 push r10
147F1B308 lea r10, loc_147F1B425+2
147F1B30F lea r10, [r10+0ECh]
147F1B316 jmp r10

2. push reg + mov imm64 + pushfq/add/popfq + jmp reg
在 /E:/:1:

147F1BF8F push r9
147F1BF91 mov r9, 0EF657195h
147F1BF9B pushfq
147F1BF9C add r9, 588C4116h
147F1BFA3 popfq
147F1BFA4 jmp r9

在 /E:/:1:

147F1B549 push r11
147F1B54B mov r11, 1129371C4h
147F1B555 pushfq
147F1B556 add r11, 355E3EA6h
147F1B55D popfq
147F1B55E jmp r11

在 /E:/:1:

147F1C3F8 push rax
147F1C3F9 mov rax, 1392C5306h
147F1C403 pushfq
147F1C404 add rax, 0EC582AFh
147F1C40B popfq
147F1C40C jmp rax

所以结论很直接:

- 在你这段函数里,jmp reg 混淆至少有两种模板,不是一种。
- 全局看更不止这两种;我还扫到了大量 jmp rax/r9/r10/r11/...,以及很多 jmp [reg] / jmp [reg+disp] 形态。那些不一定都属
于同一种 OLLVM flatten stub,不能一刀切 patch。

更稳的去混淆策略应该改成:

- 不按“只有一种模板”写脚本。
- 先识别“块尾常量构造目标地址”的伪间接跳转。
- 再校验目标块开头是否有对应 pop 同一寄存器。
- 只有命中这组特征时,才把它改成直跳。
- jmp [reg]、jmp [reg+disp]、或前面没有匹配 push/pop 配对的,先跳过,避免误杀真实间接跳转。

如果你要,我下一步给你一版更靠谱的 IDC/Python 脚本,支持这两种已确认模板,并把“有 push reg/目标块有 pop reg”作为硬约
束。这样才适合在这片 .tvm0 里批量跑。

用 IDAPython 更稳,IDC 在这种场景下容易因为反汇编状态和类型转换出问题。下面这版脚本只处理我已经确认过的两类伪间接跳
转,并且加了硬约束:

- 块内必须先有 push reg
- 尾巴必须命中已知模板
- 目标地址必须可算出
- 目标块开头必须是 pop 同一个 reg

只有全部满足才会 patch 成直跳。

# IDAPython 3.x
# Tested logic for the patterns seen around 0x147F188A4

import ida_bytes
import ida_ua
import ida_idaapi
import ida_auto
import ida_funcs
import ida_kernwin
import ida_segment
import idc
import idaapi

BADADDR = ida_idaapi.BADADDR

def u64(x):
return x & 0xFFFFFFFFFFFFFFFF

def s32(x):
x &= 0xFFFFFFFF
return x - 0x100000000 if x & 0x80000000 else x

def insn_len(ea):
return idc.get_item_size(ea)

def ensure_code(ea):
if ea == BADADDR:
return False
flags = ida_bytes.get_flags(ea)
if ida_bytes.is_code(flags):
return True
return idc.create_insn(ea) > 0

def mnem(ea):
if not ensure_code(ea):
return ""
return idc.print_insn_mnem(ea)

def opnd(ea, n):
if not ensure_code(ea):
return ""
return idc.print_operand(ea, n)

def next_head_safe(ea, limit):
if ea == BADADDR:
return BADADDR
n = idc.next_head(ea, limit)
return n if n != ida_idaapi.BADADDR else BADADDR

def is_pop_reg(ea, reg):
return ensure_code(ea) and mnem(ea) == "pop" and opnd(ea, 0).lower() == reg.lower()

def add_cmt(ea, text):
old = idc.get_cmt(ea, 0) or ""
if old:
text = old + " | " + text
idc.set_cmt(ea, text, 0)

def patch_rel_jmp(src, end_ea, dst):
if src == BADADDR or end_ea == BADADDR or dst == BADADDR:
return False

length = end_ea - src
if length < 5:
print(f"[-] patch_rel_jmp: range too small at {src:#x}")
return False

rel = u64(dst - (src + 5)) & 0xFFFFFFFF

ida_bytes.del_items(src, ida_bytes.DELIT_SIMPLE, length)
ida_bytes.patch_byte(src, 0xE9)
ida_bytes.patch_dword(src + 1, rel)

for i in range(5, length):
ida_bytes.patch_byte(src + i, 0x90)

idc.create_insn(src)
idc.create_insn(dst)
ida_auto.auto_wait()
return True

def calc_pattern_a(a, b, c, reg):
# push reg
# lea reg, abs
# lea reg, [reg+imm]
# jmp reg
if mnem(a) != "lea" or mnem(b) != "lea" or mnem(c) != "jmp":
return None
if opnd(a, 0).lower() != reg.lower():
return None
if opnd(b, 0).lower() != reg.lower():
return None
if opnd(c, 0).lower() != reg.lower():
return None

base = idc.get_operand_value(a, 1)
imm = idc.get_operand_value(b, 1)
target = u64(base + s32(imm))
return target

def calc_pattern_b(a, b, c, d, e, reg):
# push reg
# mov reg, imm64
# pushfq
# add reg, imm32
# popfq
# jmp reg
if mnem(a) != "mov" or mnem(b) != "pushfq" or mnem(c) != "add" or mnem(d) != "popfq" or mnem(e) != "jmp":
return None
if opnd(a, 0).lower() != reg.lower():
return None
if opnd(c, 0).lower() != reg.lower():
return None
if opnd(e, 0).lower() != reg.lower():
return None

base = idc.get_operand_value(a, 1)
imm = idc.get_operand_value(c, 1)
target = u64(base + s32(imm))
return target

def try_patch_block(block_ea, search_limit=0x250):
if block_ea == BADADDR:
return BADADDR

cmt = idc.get_cmt(block_ea, 0) or ""
if "patched_llvm_ijmp" in cmt:
print(f"[*] already patched {block_ea:#x}")
return BADADDR

if not ensure_code(block_ea):
print(f"[-] failed to decode block start at {block_ea:#x}")
return BADADDR

limit = block_ea + search_limit
ea = block_ea

while ea != BADADDR and ea < limit:
if not ensure_code(ea):
ea = next_head_safe(ea, limit)
continue

if mnem(ea) != "push":
ea = next_head_safe(ea, limit)
continue

reg = opnd(ea, 0)
if not reg:
ea = next_head_safe(ea, limit)
continue

reg = reg.lower()

a = next_head_safe(ea, limit)
b = next_head_safe(a, limit)
c = next_head_safe(b, limit)
d = next_head_safe(c, limit)
e = next_head_safe(d, limit)

target = None
patch_start = None
patch_end = None
pat_name = None

if a != BADADDR and b != BADADDR and c != BADADDR:
target = calc_pattern_a(a, b, c, reg)
if target is not None and is_pop_reg(target, reg):
patch_start = a
patch_end = c + insn_len(c)
pat_name = "A"

if target is None and a != BADADDR and b != BADADDR and c != BADADDR and d != BADADDR and e != BADADDR:
target = calc_pattern_b(a, b, c, d, e, reg)
if target is not None and is_pop_reg(target, reg):
patch_start = a
patch_end = e + insn_len(e)
pat_name = "B"

if target is not None and patch_start is not None:
if patch_rel_jmp(patch_start, patch_end, target):
add_cmt(block_ea, f"patched_llvm_ijmp pattern_{pat_name} -> {target:#x}")
print(f"[+] patched block {block_ea:#x}: pattern {pat_name}, {reg} -> {target:#x}")
return target

ea = next_head_safe(ea, limit)

print(f"[-] no supported llvm-ijmp pattern at {block_ea:#x}")
return BADADDR

def deobf_chain(start_ea, max_blocks=64):
cur = start_ea
count = 0
seen = set()

while cur != BADADDR and count < max_blocks:
if cur in seen:
print(f"[*] loop detected at {cur:#x}")
break
seen.add(cur)

nxt = try_patch_block(cur)
if nxt == BADADDR:
break

cur = nxt
count += 1

print(f"[*] done after {count} block(s), stopped at {cur:#x}")

def dry_run_block(block_ea, search_limit=0x250):
limit = block_ea + search_limit
ea = block_ea

while ea != BADADDR and ea < limit:
if ensure_code(ea) and mnem(ea) == "push":
reg = opnd(ea, 0).lower()
a = next_head_safe(ea, limit)
b = next_head_safe(a, limit)
c = next_head_safe(b, limit)
d = next_head_safe(c, limit)
e = next_head_safe(d, limit)

if a != BADADDR and b != BADADDR and c != BADADDR:
t = calc_pattern_a(a, b, c, reg)
if t is not None:
print(f"[*] pattern A candidate at {block_ea:#x}: {reg} -> {t:#x}, pop_ok={is_pop_reg(t, reg)}")

if a != BADADDR and b != BADADDR and c != BADADDR and d != BADADDR and e != BADADDR:
t = calc_pattern_b(a, b, c, d, e, reg)
if t is not None:
print(f"[*] pattern B candidate at {block_ea:#x}: {reg} -> {t:#x}, pop_ok={is_pop_reg(t, reg)}")
ea = next_head_safe(ea, limit)

# 先 dry run,再 patch 单块,再决定要不要链式跑
start_ea = 0x147F188A4

dry_run_block(start_ea)
# try_patch_block(start_ea)

dry_run_block(0x147F188A4)

2. 输出确认 pop_ok=True 后,再只 patch 一个块:

3. 单块没问题,再链式跑:

deobf_chain(0x147F188A4, 64)

这版比之前稳的点:

- 支持两种已确认模板
- 只在 push reg -> ... -> jmp reg 且目标开头 pop reg 时 patch
- 不碰 jmp [reg] / jmp [reg+disp]
- 先有 dry_run_block(),可以先看识别结果,不直接改

如果你要,我下一条可以继续给你一个“扫描整段 .tvm0,只列出候选块地址、不做 patch”的版本,方便你先摸清这片里到底有多少种
可安全处理的块。

› [*] pattern A candidate at 0x147f188a4: r8 -> 0x147f1bf3d, pop_ok=True
[*] pattern B candidate at 0x147f1bf3d: r9 -> 0x147f1b2ab, pop_ok=False
[*] pattern A candidate at 0x147f188a4: r8 -> 0x147f1bf3d, pop_ok=True
[+] patched block 0x147f188a4: pattern A, r8 -> 0x147f1bf3d
[*] already patched 0x147f188a4
[*] done after 0 block(s), stopped at 0x147f188a4

• 结果说明两件事:

1. 第一个块 patch 成功了。
2. 链式没有继续,是因为你又从同一个起点 0x147F188A4 开始跑了,而这个块已经被标记成 patched_llvm_ijmp,脚本就直接停了。

也就是这段输出的含义:

[+] patched block 0x147f188a4: pattern A, r8 -> 0x147f1bf3d
[*] already patched 0x147f188a4
[*] done after 0 block(s), stopped at 0x147f188a4

正确继续方式是从下一个块开始:

try_patch_block(0x147F1BF3D)
# 或
deobf_chain(0x147F1BF3D, 64)

你看到的第二个问题也很关键:

[*] pattern B candidate at 0x147f1bf3d: r9 -> 0x147f1b2ab, pop_ok=False

这不是说目标算错了,大概率是 IDA 在 0x147F1B2AB 那里还没把字节正确切成:

41 59 pop r9

之前我看原始字节时,0x147F1B2AB 开头确实就是 41 59,所以这里更像是“IDA 当前反汇编状态不干净”,不是目标地址不对。

把 is_pop_reg() 换成基于原始字节的判断,更稳。直接改成这个:

def is_pop_reg(ea, reg):
reg = reg.lower()
b0 = ida_bytes.get_byte(ea)
b1 = ida_bytes.get_byte(ea + 1)

pop_map = {
"rax": (0x58, None),
"rcx": (0x59, None),
"rdx": (0x5A, None),
"rbx": (0x5B, None),
"rsp": (0x5C, None),
"rbp": (0x5D, None),
"rsi": (0x5E, None),
"rdi": (0x5F, None),
"r8": (0x41, 0x58),
"r9": (0x41, 0x59),
"r10": (0x41, 0x5A),
"r11": (0x41, 0x5B),
"r12": (0x41, 0x5C),
"r13": (0x41, 0x5D),
"r14": (0x41, 0x5E),
"r15": (0x41, 0x5F),
}

if reg not in pop_map:
return False

x = pop_map[reg]
if x[1] is None:
return b0 == x[0]
return b0 == x[0] and b1 == x[1]

然后手动继续:

dry_run_block(0x147F1BF3D)
try_patch_block(0x147F1BF3D)
deobf_chain(0x147F1B2AB, 64)

更顺一点的做法是直接这样:

nxt = try_patch_block(0x147F188A4)
print(hex(nxt))
deobf_chain(nxt, 64)

这样不会重复从已经 patch 过的块开始。

如果你要,我可以把整份脚本整理成一版“已修复 pop_ok 判断 + 自动从返回目标继续链式 patch”的完整可直接运行版本。
› 我要这个 如果你要,我下一条可以继续给你一个“扫描整段 .tvm0,只列出候选块地址、不做 patch”的版本,方便你先摸清这片里到
底有多少种
可安全处理的

• 下面是“扫描整段 .tvm0,只列出候选块地址、不做 patch”的 IDAPython 版本。它只识别目前已经确认比较安全的两类伪间接跳转:

- Pattern A: push reg; lea reg, abs; lea reg, [reg+imm]; jmp reg
- Pattern B: push reg; mov reg, imm64; pushfq; add reg, imm32; popfq; jmp reg

并按两档输出:

- safe: 目标块开头字节就是对应 pop reg
- weak: 能算出目标,但目标开头不是对应 pop reg,需要人工复核

import ida_bytes
import ida_idaapi
import ida_segment
import idc

BADADDR = ida_idaapi.BADADDR

def u64(x):
return x & 0xFFFFFFFFFFFFFFFF

def s32(x):
x &= 0xFFFFFFFF
return x - 0x100000000 if x & 0x80000000 else x

def ensure_code(ea):
if ea == BADADDR:
return False
flags = ida_bytes.get_flags(ea)
if ida_bytes.is_code(flags):
return True
return idc.create_insn(ea) > 0

def mnem(ea):
if not ensure_code(ea):
return ""
return idc.print_insn_mnem(ea)

def opnd(ea, n):
if not ensure_code(ea):
return ""
return idc.print_operand(ea, n)

def next_head_safe(ea, limit):
if ea == BADADDR:
return BADADDR
n = idc.next_head(ea, limit)
return n if n != BADADDR else BADADDR

def is_pop_reg_bytes(ea, reg):
reg = reg.lower()
b0 = ida_bytes.get_byte(ea)
b1 = ida_bytes.get_byte(ea + 1)

pop_map = {
"rax": (0x58, None),
"rcx": (0x59, None),
"rdx": (0x5A, None),
"rbx": (0x5B, None),
"rsp": (0x5C, None),
"rbp": (0x5D, None),
"rsi": (0x5E, None),
"rdi": (0x5F, None),
"r8": (0x41, 0x58),
"r9": (0x41, 0x59),
"r10": (0x41, 0x5A),
"r11": (0x41, 0x5B),
"r12": (0x41, 0x5C),
"r13": (0x41, 0x5D),
"r14": (0x41, 0x5E),
"r15": (0x41, 0x5F),
}

if reg not in pop_map:
return False

x = pop_map[reg]
if x[1] is None:
return b0 == x[0]
return b0 == x[0] and b1 == x[1]

def calc_pattern_a(push_ea, a, b, c, reg):
if mnem(push_ea) != "push":
return None
if mnem(a) != "lea" or mnem(b) != "lea" or mnem(c) != "jmp":
return None
if opnd(push_ea, 0).lower() != reg:
return None
if opnd(a, 0).lower() != reg:
return None
if opnd(b, 0).lower() != reg:
return None
if opnd(c, 0).lower() != reg:
return None

base = idc.get_operand_value(a, 1)
imm = idc.get_operand_value(b, 1)
return u64(base + s32(imm))

def calc_pattern_b(push_ea, a, b, c, d, e, reg):
if mnem(push_ea) != "push":
return None
if mnem(a) != "mov" or mnem(b) != "pushfq" or mnem(c) != "add" or mnem(d) != "popfq" or mnem(e) != "jmp":
return None
if opnd(push_ea, 0).lower() != reg:
return None
if opnd(a, 0).lower() != reg:
return None
if opnd(c, 0).lower() != reg:
return None
if opnd(e, 0).lower() != reg:
return None

base = idc.get_operand_value(a, 1)
imm = idc.get_operand_value(c, 1)
return u64(base + s32(imm))

def scan_block(block_ea, search_limit=0x250):
out = []
limit = block_ea + search_limit
ea = block_ea

while ea != BADADDR and ea < limit:
if not ensure_code(ea):
ea = next_head_safe(ea, limit)
continue

if mnem(ea) != "push":
ea = next_head_safe(ea, limit)
continue

reg = opnd(ea, 0).lower()
a = next_head_safe(ea, limit)
b = next_head_safe(a, limit)
c = next_head_safe(b, limit)
d = next_head_safe(c, limit)
e = next_head_safe(d, limit)

if a != BADADDR and b != BADADDR and c != BADADDR:
t = calc_pattern_a(ea, a, b, c, reg)
if t is not None:
out.append({
"block_ea": block_ea,
"push_ea": ea,
"pattern": "A",
"reg": reg,
"target": t,
"safe": is_pop_reg_bytes(t, reg),
})

if a != BADADDR and b != BADADDR and c != BADADDR and d != BADADDR and e != BADADDR:
t = calc_pattern_b(ea, a, b, c, d, e, reg)
if t is not None:
out.append({
"block_ea": block_ea,
"push_ea": ea,
"pattern": "B",
"reg": reg,
"target": t,
"safe": is_pop_reg_bytes(t, reg),
})

ea = next_head_safe(ea, limit)

return out

def scan_segment(seg_name=".tvm0", step=1):
seg = ida_segment.get_segm_by_name(seg_name)
if not seg:
print(f"[-] segment not found: {seg_name}")
return []

start = seg.start_ea
end = seg.end_ea
results = []

ea = start
while ea < end:
flags = ida_bytes.get_flags(ea)
if ida_bytes.is_code(flags):
hits = scan_block(ea)
for h in hits:
results.append(h)
ea = idc.next_head(ea, end)
if ea == BADADDR:
break

print(f"[*] total candidates: {len(results)}")

safe = [x for x in results if x["safe"]]
weak = [x for x in results if not x["safe"]]

print(f"[*] safe candidates: {len(safe)}")
for x in safe:
print(f"[SAFE] block={x['block_ea']:#x} push={x['push_ea']:#x} pat={x['pattern']} reg={x['reg']}
target={x['target']:#x}")

print(f"[*] weak candidates: {len(weak)}")
for x in weak:
print(f"[WEAK] block={x['block_ea']:#x} push={x['push_ea']:#x} pat={x['pattern']} reg={x['reg']}
target={x['target']:#x}")

return results

# 跑整段 .tvm0
scan_segment(".tvm0")

用法就是直接在 IDA Python 窗口跑:

scan_segment(".tvm0")

如果你只想先看某一小段,也可以手动扫单块:

scan_block(0x147F188A4)

你现在这类样本里,建议先看 SAFE 列表,再从里面挑几个连续块人工核对一遍。WEAK 不要直接 patch。
如果你要,我下一条可以继续给你一个“把 SAFE 结果导出到文件,或者按 target 串链显示”的版本。

› Su
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
# IDAPython 3.x

import ida_bytes
import ida_idaapi
import ida_segment
import idc

BADADDR = ida_idaapi.BADADDR

def u64(x):
return x & 0xFFFFFFFFFFFFFFFF

def s32(x):
x &= 0xFFFFFFFF
return x - 0x100000000 if x & 0x80000000 else x

def ensure_code(ea):
if ea == BADADDR:
return False
flags = ida_bytes.get_flags(ea)
if ida_bytes.is_code(flags):
return True
return idc.create_insn(ea) > 0

def mnem(ea):
if not ensure_code(ea):
return ""
return idc.print_insn_mnem(ea)

def opnd(ea, n):
if not ensure_code(ea):
return ""
return idc.print_operand(ea, n)

def next_head_safe(ea, limit):
if ea == BADADDR:
return BADADDR
n = idc.next_head(ea, limit)
return n if n != BADADDR else BADADDR

def is_pop_reg_bytes(ea, reg):
if ea == BADADDR:
return False

reg = (reg or "").lower()
b0 = ida_bytes.get_byte(ea)
b1 = ida_bytes.get_byte(ea + 1)

pop_map = {
"rax": (0x58, None),
"rcx": (0x59, None),
"rdx": (0x5A, None),
"rbx": (0x5B, None),
"rsp": (0x5C, None),
"rbp": (0x5D, None),
"rsi": (0x5E, None),
"rdi": (0x5F, None),
"r8": (0x41, 0x58),
"r9": (0x41, 0x59),
"r10": (0x41, 0x5A),
"r11": (0x41, 0x5B),
"r12": (0x41, 0x5C),
"r13": (0x41, 0x5D),
"r14": (0x41, 0x5E),
"r15": (0x41, 0x5F),
}

if reg not in pop_map:
return False

x = pop_map[reg]
if x[1] is None:
return b0 == x[0]
return b0 == x[0] and b1 == x[1]

def calc_pattern_a(push_ea, a, b, c, reg):
# push reg
# lea reg, abs
# lea reg, [reg+imm]
# jmp reg
reg = (reg or "").lower()

if mnem(push_ea) != "push":
return None
if mnem(a) != "lea" or mnem(b) != "lea" or mnem(c) != "jmp":
return None
if (opnd(push_ea, 0) or "").lower() != reg:
return None
if (opnd(a, 0) or "").lower() != reg:
return None
if (opnd(b, 0) or "").lower() != reg:
return None
if (opnd(c, 0) or "").lower() != reg:
return None

base = idc.get_operand_value(a, 1)
imm = idc.get_operand_value(b, 1)
return u64(base + s32(imm))

def calc_pattern_b(push_ea, a, b, c, d, e, reg):
# push reg
# mov reg, imm64
# pushfq
# add reg, imm32
# popfq
# jmp reg
reg = (reg or "").lower()

if mnem(push_ea) != "push":
return None
if mnem(a) != "mov" or mnem(b) != "pushfq" or mnem(c) != "add" or mnem(d) != "popfq" or mnem(e) != "jmp":
return None
if (opnd(push_ea, 0) or "").lower() != reg:
return None
if (opnd(a, 0) or "").lower() != reg:
return None
if (opnd(c, 0) or "").lower() != reg:
return None
if (opnd(e, 0) or "").lower() != reg:
return None

base = idc.get_operand_value(a, 1)
imm = idc.get_operand_value(c, 1)
return u64(base + s32(imm))

def scan_block(block_ea, search_limit=0x250):
out = []
limit = block_ea + search_limit
ea = block_ea

while ea != BADADDR and ea < limit:
if not ensure_code(ea):
ea = next_head_safe(ea, limit)
continue

if mnem(ea) != "push":
ea = next_head_safe(ea, limit)
continue

reg = (opnd(ea, 0) or "").lower()
if not reg:
ea = next_head_safe(ea, limit)
continue

a = next_head_safe(ea, limit)
b = next_head_safe(a, limit)
c = next_head_safe(b, limit)
d = next_head_safe(c, limit)
e = next_head_safe(d, limit)

if a != BADADDR and b != BADADDR and c != BADADDR:
t = calc_pattern_a(ea, a, b, c, reg)
if t is not None:
out.append({
"block_ea": block_ea,
"push_ea": ea,
"pattern": "A",
"reg": reg,
"target": t,
"safe": is_pop_reg_bytes(t, reg),
})

if a != BADADDR and b != BADADDR and c != BADADDR and d != BADADDR and e != BADADDR:
t = calc_pattern_b(ea, a, b, c, d, e, reg)
if t is not None:
out.append({
"block_ea": block_ea,
"push_ea": ea,
"pattern": "B",
"reg": reg,
"target": t,
"safe": is_pop_reg_bytes(t, reg),
})

ea = next_head_safe(ea, limit)

return out

def scan_segment(seg_name=".tvm0", step=1):
seg = ida_segment.get_segm_by_name(seg_name)
if not seg:
print(f"[-] segment not found: {seg_name}")
return []

start = seg.start_ea
end = seg.end_ea
results = []

ea = start
while ea < end:
flags = ida_bytes.get_flags(ea)
if ida_bytes.is_code(flags):
hits = scan_block(ea)
results.extend(hits)

ea = idc.next_head(ea, end)
if ea == BADADDR:
break

print(f"[*] total candidates: {len(results)}")

safe = [x for x in results if x["safe"]]
weak = [x for x in results if not x["safe"]]

print(f"[*] safe candidates: {len(safe)}")
for x in safe:
print(
f"[SAFE] block={x['block_ea']:#x} "
f"push={x['push_ea']:#x} "
f"pat={x['pattern']} "
f"reg={x['reg']} "
f"target={x['target']:#x}"
)

print(f"[*] weak candidates: {len(weak)}")
for x in weak:
print(
f"[WEAK] block={x['block_ea']:#x} "
f"push={x['push_ea']:#x} "
f"pat={x['pattern']} "
f"reg={x['reg']} "
f"target={x['target']:#x}"
)

return results

# 跑整段 .tvm0
scan_segment(".tvm0")
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
import ida_bytes
import ida_idaapi
import ida_auto
import ida_segment
import idc

BADADDR = ida_idaapi.BADADDR

def u64(x):
return x & 0xFFFFFFFFFFFFFFFF

def s32(x):
x &= 0xFFFFFFFF
return x - 0x100000000 if x & 0x80000000 else x

def insn_len(ea):
return idc.get_item_size(ea)

def ensure_code(ea):
if ea == BADADDR:
return False
flags = ida_bytes.get_flags(ea)
if ida_bytes.is_code(flags):
return True
return idc.create_insn(ea) > 0

def mnem(ea):
if not ensure_code(ea):
return ""
return idc.print_insn_mnem(ea)

def opnd(ea, n):
if not ensure_code(ea):
return ""
return idc.print_operand(ea, n)

def next_head_safe(ea, limit):
if ea == BADADDR:
return BADADDR
n = idc.next_head(ea, limit)
return n if n != BADADDR else BADADDR

def is_pop_reg_bytes(ea, reg):
if ea == BADADDR:
return False

reg = (reg or "").lower()
b0 = ida_bytes.get_byte(ea)
b1 = ida_bytes.get_byte(ea + 1)

pop_map = {
"rax": (0x58, None),
"rcx": (0x59, None),
"rdx": (0x5A, None),
"rbx": (0x5B, None),
"rsp": (0x5C, None),
"rbp": (0x5D, None),
"rsi": (0x5E, None),
"rdi": (0x5F, None),
"r8": (0x41, 0x58),
"r9": (0x41, 0x59),
"r10": (0x41, 0x5A),
"r11": (0x41, 0x5B),
"r12": (0x41, 0x5C),
"r13": (0x41, 0x5D),
"r14": (0x41, 0x5E),
"r15": (0x41, 0x5F),
}

if reg not in pop_map:
return False

x = pop_map[reg]
if x[1] is None:
return b0 == x[0]
return b0 == x[0] and b1 == x[1]

def add_cmt(ea, text):
old = idc.get_cmt(ea, 0) or ""
if old:
text = old + " | " + text
idc.set_cmt(ea, text, 0)

def patch_rel_jmp(src, end_ea, dst):
if src == BADADDR or end_ea == BADADDR or dst == BADADDR:
return False

length = end_ea - src
if length < 5:
return False

rel = (dst - (src + 5)) & 0xFFFFFFFF

ida_bytes.del_items(src, ida_bytes.DELIT_SIMPLE, length)
ida_bytes.patch_byte(src, 0xE9)
ida_bytes.patch_dword(src + 1, rel)

for i in range(5, length):
ida_bytes.patch_byte(src + i, 0x90)

idc.create_insn(src)
idc.create_insn(dst)
ida_auto.auto_wait()
return True

def calc_pattern_a(push_ea, a, b, c, reg):
reg = (reg or "").lower()

if mnem(push_ea) != "push":
return None
if mnem(a) != "lea" or mnem(b) != "lea" or mnem(c) != "jmp":
return None
if (opnd(push_ea, 0) or "").lower() != reg:
return None
if (opnd(a, 0) or "").lower() != reg:
return None
if (opnd(b, 0) or "").lower() != reg:
return None
if (opnd(c, 0) or "").lower() != reg:
return None

base = idc.get_operand_value(a, 1)
imm = idc.get_operand_value(b, 1)
return u64(base + s32(imm))

def calc_pattern_b(push_ea, a, b, c, d, e, reg):
reg = (reg or "").lower()

if mnem(push_ea) != "push":
return None
if mnem(a) != "mov" or mnem(b) != "pushfq" or mnem(c) != "add" or mnem(d) != "popfq" or mnem(e) != "jmp":
return None
if (opnd(push_ea, 0) or "").lower() != reg:
return None
if (opnd(a, 0) or "").lower() != reg:
return None
if (opnd(c, 0) or "").lower() != reg:
return None
if (opnd(e, 0) or "").lower() != reg:
return None

base = idc.get_operand_value(a, 1)
imm = idc.get_operand_value(c, 1)
return u64(base + s32(imm))

def try_patch_block(block_ea, search_limit=0x250):
if block_ea == BADADDR:
return False

cmt = idc.get_cmt(block_ea, 0) or ""
if "patched_llvm_ijmp" in cmt:
return False

if not ensure_code(block_ea):
return False

limit = block_ea + search_limit
ea = block_ea

while ea != BADADDR and ea < limit:
if not ensure_code(ea):
ea = next_head_safe(ea, limit)
continue

if mnem(ea) != "push":
ea = next_head_safe(ea, limit)
continue

reg = (opnd(ea, 0) or "").lower()
if not reg:
ea = next_head_safe(ea, limit)
continue

a = next_head_safe(ea, limit)
b = next_head_safe(a, limit)
c = next_head_safe(b, limit)
d = next_head_safe(c, limit)
e = next_head_safe(d, limit)

target = None
patch_start = None
patch_end = None
pat_name = None

if a != BADADDR and b != BADADDR and c != BADADDR:
t = calc_pattern_a(ea, a, b, c, reg)
if t is not None and is_pop_reg_bytes(t, reg):
target = t
patch_start = a
patch_end = c + insn_len(c)
pat_name = "A"

if target is None and a != BADADDR and b != BADADDR and c != BADADDR and d != BADADDR and e != BADADDR:
t = calc_pattern_b(ea, a, b, c, d, e, reg)
if t is not None and is_pop_reg_bytes(t, reg):
target = t
patch_start = a
patch_end = e + insn_len(e)
pat_name = "B"

if target is not None and patch_start is not None:
if patch_rel_jmp(patch_start, patch_end, target):
add_cmt(block_ea, f"patched_llvm_ijmp pattern_{pat_name} -> {target:#x}")
return True

ea = next_head_safe(ea, limit)

return False

def patch_segment(seg_name=".tvm0"):
seg = ida_segment.get_segm_by_name(seg_name)
if not seg:
print(f"segment not found: {seg_name}")
return

start = seg.start_ea
end = seg.end_ea
patched = 0

ea = start
while ea < end:
flags = ida_bytes.get_flags(ea)
if ida_bytes.is_code(flags):
if try_patch_block(ea):
patched += 1

ea = idc.next_head(ea, end)
if ea == BADADDR:
break

ida_auto.auto_wait()
print(f"patched: {patched}")

patch_segment(".tvm0")
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
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
› 从0000000147F1B0A4这里开始跟一下这个llvm,其他的jmp r*的llvm已经有去除脚本了,但是这个call开始的里面就没有脚本,你跟
一下看看
• 我先从 0x147F1B0A4 附近的指令和所在函数入手,确认这是哪种 LLVM 间接分发,再顺着 call 路径把真实控制流跟出来。

• Called ida-pro-mcp.lookup_funcs({"queries":"0x147F1B0A4"})
└ [{"query": "0x147F1B0A4", "fn": {"addr": "0x147f1b0a4", "name": "sub_147F188A4", "size": "0x36f2"}, "error": null}]

• Called ida-pro-mcp.xrefs_to({"addrs":"0x147F1B0A4","limit":50})
└ [{"addr": "0x147F1B0A4", "xrefs": [{"addr": "0x147f1b0a0", "type": "code", "fn": {"addr": "0x147f1b0a0", "name":
"sub_147F188A4", "size": "0x36f2"}}], "more": false}]

• Called ida-pro-mcp.disasm({"addr":"0x147F1B0A4","max_instructions":80,"include_total":true})
└ {"addr": "0x147F1B0A4", "asm": {"name": "sub_147F188A4", "start_ea": "0x147f1b0a4", "lines": "sub_147F188A4 (.tvm0 @
0x147f1b0a4):\n147f1b0a4 call near ptr sub_147F37D73\n147f1b0a9 int 3; Trap to Debugger\n147f1b0aa jmp
loc_147F1AE72\n147f1b0b0 nop\n147f1b0b1 lea r11, [rbp+var_s90]\n147f1b0b8 xchg rax, rcx\n147f1b0ba
mov rcx, [r11]\n147f1b0bd not rcx\n147f1b0c0 xchg rax, rcx\n147f1b0c2 not rax\n147f1b0c5 mov
rbx, [r11+8]\n147f1b0c9 xchg rcx, rdx; patched_llvm_ijmp pattern_A -> 0x147f1b513\n147f1b0cc mov rdx,
[...

─ Worked for 1m 03s ────────────────────────────────────────────────────────────────────────────────────────────────────

• 入口很像你说的那类“call 进调度器,再回到 VM/寄存器恢复”的变体。现在重点是 sub_147F37D73,我要把这个 call 目标拆开,确
认它怎么算下一跳。

• Called ida-pro-mcp.lookup_funcs({"queries":"0x147F37D73"})
└ [{"query": "0x147F37D73", "fn": {"addr": "0x147f37d73", "name": "sub_147F37D73", "size": "0x88956"}, "error": null}]

0x147f37d73):\n147f37d73 push rbx\n147f37d74 push rsi\n147f37d75 push rdi\n147f37d76 push
rbp\n147f37d77 push r15\n147f37d79 sub rsp, 68h\n147f37d7d mov rbp, rsp\n147f37d80 mov r11,
[rbp+98h]\n147f37d87 mov r10, [rbp+0A0h]\n147f37d8e call loc_147F68C9B\n147f37d93 jmp
loc_147F68C9B\n147f37d98 jmp near ptr 15138D6DEh\n147f38008 mov r10, 10E535306h; patched_llvm_ijmp
pattern_B -> 0x147fa2b59\n147...

• Called ida-pro-mcp.decompile({"addr":"0x147F37D73"})
└ {"addr": "0x147F37D73", "code": null, "error": "Decompilation failed"}

• Called ida-pro-mcp.xrefs_to({"addrs":"0x147F37D73","limit":100})
└ [{"addr": "0x147F37D73", "xrefs": [{"addr": "0x147f18ae0", "type": "code", "fn": {"addr": "0x147f18ae0", "name":
"sub_147F188A4", "size": "0x36f2"}}, {"addr": "0x147f18cd8", "type": "code", "fn": {"addr": "0x147f18cd8",
"name": "sub_147F188A4", "size": "0x36f2"}}, {"addr": "0x147f18ed1", "type": "code", "fn": {"addr":
"0x147f18ed1", "name": "sub_147F188A4", "size": "0x36f2"}}, {"addr": "0x147f190cb", "type": "code", "fn":
{"addr": "0x147f190cb", "name": "sub_147F188A4", "size": "0x36f2"}}, {"addr": "0x147f192c6", "type": "code",
"fn": {"addr": "0x147f192c6", "name":...

─ Worked for 1m 53s ────────────────────────────────────────────────────────────────────────────────────────────────────

• 这个大函数本身就是分发器,入口一上来就把栈上传进来的两个值取到 r11/r10,然后转到 loc_147F68C9B。我先盯这个入口块,看它
是否直接消费 call 压进去的返回地址。
└ {"addr": "0x147F68C9B", "asm": {"name": "sub_147F37D73", "start_ea": "0x147f68c9b", "lines": "sub_147F37D73 (.tvm0 @
0x147f68c9b):\n147f68c9b lea rsp, [rsp+8]\n147f68ca0 lea r9, loc_147FC06CB\n147f68ca7 mov
[rbp+0], r9\n147f68cab mov r9, r11\n147f68cae mov r8d, [r9]\n147f68cb1 mov rdx, r9\n147f68cb4
not rdx\n147f68cb7 mov rcx, r9\n147f68cba add rcx, cs:qword_147F65E9C\n147f68cc1 add rcx,
cs:qword_147FCB449\n147f68cc8 not rcx\n147f68ccb or rdx, rcx\n147f68cce not rdx\n147f68cd1 or
rdx, r9\n147f68...

• Called ida-pro-mcp.xrefs_to({"addrs":"0x147F68C9B","limit":50})
└ [{"addr": "0x147F68C9B", "xrefs": [{"addr": "0x147f37d8e", "type": "code", "fn": {"addr": "0x147f37d8e", "name":
"sub_147F37D73", "size": "0x88956"}}, {"addr": "0x147f37d93", "type": "code", "fn": {"addr": "0x147f37d93",
"name": "sub_147F37D73", "size": "0x88956"}}], "more": false}]

• Called ida-pro-mcp.basic_blocks({"addrs":"0x147F37D73","max_blocks":20,"offset":0})
└ [{"addr": "0x147F37D73", "blocks": [{"start": "0x147f37d73", "end": "0x147f37d74", "size": 1, "type": 0,
"successors": ["0x147f37d74"], "predecessors": []}, {"start": "0x147f37d74", "end": "0x147f37d75", "size": 1,
"type": 0, "successors": ["0x147f37d75"], "predecessors": ["0x147f37d73"]}, {"start": "0x147f37d75", "end":
"0x147f37d77", "size": 2, "type": 0, "successors": ["0x147f37d77"], "predecessors": ["0x147f37d74"]}, {"start":
"0x147f37d77", "end": "0x147f37d7d", "size": 6, "type": 0, "successors": ["0x147f37d7d"], "predecessors":
["0x147f37d75"]}, {"start": "0x...

─ Worked for 1m 35s ────────────────────────────────────────────────────────────────────────────────────────────────────

• 我需要把 call 发生时的栈形态补齐,不然只能看到分发器入口,看不到它实际用的是哪两个参数。现在回到 0x147F1B0A4 前一段,
把这个 call 前的准备动作接上。

• Called ida-pro-mcp.disasm({"addr":"0x147F1B05C","max_instructions":120,"include_total":true})
└ {"addr": "0x147F1B05C", "asm": {"name": "sub_147F188A4", "start_ea": "0x147f1b05c", "lines": "sub_147F188A4 (.tvm0 @
0x147f1b05c):\n147f1b05c call near ptr sub_147F37D73\n147f1b061 int 3; Trap to Debugger\n147f1b062
nop\n147f1b064 jmp loc_147F18EDF\n147f1b06a pop r11\n147f1b06c pop qword ptr [r11+70h]\n147f1b070
push [rbp+var_s80]\n147f1b076 pop qword ptr [r11+78h]\n147f1b07a push [rbp+var_s0]\n147f1b07d pop

• Called ida-pro-mcp.disasm({"addr":"0x147F1AFE0","max_instructions":180,"include_total":true})
└ {"addr": "0x147F1AFE0", "asm": {"name": "sub_147F188A4", "start_ea": "0x147f1afe0", "lines": "sub_147F188A4 (.tvm0 @
0x147f1afe0):\n147f1afe2 push qword ptr [rbp+var_s8+8]\n147f1afe5 pop qword ptr [r11+28h]\n147f1afe9
push [rbp+var_s38]\n147f1afec pop qword ptr [r11+30h]\n147f1aff0 push [rbp+var_s28]\n147f1aff3 pop
qword ptr [r11+38h]\n147f1aff7 push [rbp+var_s48]\n147f1affa pop qword ptr [r11+40h]\n147f1affe push
[rbp+var_s50]\n147f1b001 pop qword ptr [r11+48h]\n147f1b005 push [rbp+var_s58]\n147f1b008 pop
qword ...
└ [{"addr": "0x147F188A4", "blocks": [{"start": "0x147f192ce", "end": "0x147f192d3", "size": 5, "type": 0,
"successors": ["0x147f1bb4b"], "predecessors": []}, {"start": "0x147f192d4", "end": "0x147f194c4", "size": 496,
"type": 4, "successors": [], "predecessors": ["0x147f1bb44"]}, {"start": "0x147f194c4", "end": "0x147f194c5",
"size": 1, "type": 0, "successors": [], "predecessors": []}, {"start": "0x147f194c5", "end": "0x147f194c6",
"size": 1, "type": 0, "successors": ["0x147f194c6"], "predecessors": []}, {"start": "0x147f194c6", "end":
"0x147f194cc", "size": 6, "type...

─ Worked for 1m 34s ────────────────────────────────────────────────────────────────────────────────────────────────────

• 这个点前后的 VM 框架已经比较清楚了:r10 = 状态块(&var_s88),r11 = handler/字节码基址,然后把这两个值压栈给分发器。接下
来我看 0x147F1B513 和 0x147F058CE,把这条 call 实际落回来的逻辑串完整。

• Called ida-pro-mcp.disasm({"addr":"0x147F1B513","max_instructions":120,"include_total":true})
[r11+38h]\n147f1b51c push [rbp+var_s48]\n147f1b51f pop qword ptr [r11+40h]\n147f1b523 push
[rbp+var_s50]\n147f1b526 pop qword ptr [r11+48h]\n147f1b52a push [rbp+var_s58]\n147f1b52d pop
qword ptr [r11+50h]\n147f1b531 push [rbp+var_s60]\n147f1b534 pop qword ptr [r11+58h]\n147f1b538 push
[rbp+var_s68]\n147f1b53b p...

• Called ida-pro-mcp.get_bytes({"regions":[{"addr":"0x147F058CE","size":64},{"addr":"0x147F05EA2","size":64}]})
└ [{"addr": "0x147F058CE", "data": "0x30 0xd0 0xf4 0x7 0xc1 0x90 0x2 0xaa 0x3a 0xd2 0x24 0x8e 0x46 0xaa 0x6d 0x1f 0xa
0xe6 0x26 0xd0 0xf4 0x7 0x16 0xca 0x33 0x30 0x9b 0x29 0xbe 0x96 0x1f 0xd0 0xf4 0x7 0x4 0x72 0xb9 0x3c 0x26 0x5a
0x25 0x18 0xe 0x83 0xe8 0x4c 0x2d 0xc6 0xc8 0xe4 0xa0 0x95 0xb6 0xbd 0xfd 0xcf 0xf4 0x7 0x2 0x90 0x76 0x47 0x8f
0x81 0x3a 0x...

• Called
└ ida-pro-mcp.get_int({"queries":[{"addr":"0x147F058CE","ty":"u32le"},{"addr":"0x147F058D2","ty":"u32le"},
{"addr":"0x147F05EA2","ty":"u32le"},{"addr":"0x147F05EA6","ty":"u32le"}]})
[{"addr": "0x147F058CE", "ty": "u32le", "value": 133484592, "error": null}, {"addr": "0x147F058D2", "ty": "u32le",
"value": 2852294849, "error": null}, {"addr": "0x147F05EA2", "ty": "u32le", "value": 133483625, "error": null},
{"addr": "0x147F05EA6", "ty": "u32le", "value": 1375453863, "error": null}]

─ Worked for 1m 19s ────────────────────────────────────────────────────────────────────────────────────────────────────

• 我再把 0x147F058CE 当作字节码流看一下。现在已经能确认 call 不是普通子调用,而是把 handler_ptr 和 state_ptr 压栈后丢给
公共分发器;关键在这个 handler 头几个 dword 怎么被解释。

• Called ida-pro-mcp.disasm({"addr":"0x147F4D030","max_instructions":80,"include_total":true})
└ {"addr": "0x147F4D030", "asm": {"name": "sub_147F37D73", "start_ea": "0x147f4d030", "lines": "sub_147F37D73 (.tvm0 @
0x147f4d030):\n147f4e0b8 push r9; patched_llvm_ijmp pattern_B -> 0x147fb3321\n147f4e0ba jmp
r10\n147f4e161 movzx rdx, dx\n147f4e165 mov rcx, rdx\n147f4e168 and rcx, rdx\n147f4e16b or
rdx, r9\n147f4e16e and...

• Called ida-pro-mcp.disasm({"addr":"0x147F4CC69","max_instructions":80,"include_total":true})
└ {"addr": "0x147F4CC69", "asm": {"name": "sub_147F37D73", "start_ea": "0x147f4cc69", "lines": "sub_147F37D73 (.tvm0 @
0x147f4cc69):\n147f4e0b8 push r9; patched_llvm_ijmp pattern_B -> 0x147fb3321\n147f4e0ba jmp
loc_147FB3321; patched_llvm_ijmp pattern_B -> 0x147f6edbd\n147f4e0d0 jmp near ptr 1A8426C21h\n147f4e148
not ecx\n147f4e152 add eax, 2301Ah\n147f4e157 add r8, cs:qword_147FC1253\n147f4e15e mov r9,
r10\n147f4e161 movzx rdx, dx\n147f4e165 mov rcx, rdx\n147f4e168 and rcx, rdx\n147f4e16b or
rdx, r9\n147f4e16e and...

• Called ida-pro-mcp.xrefs_to({"addrs":["0x147F058CE","0x147F05EA2","0x147F4D030","0x147F4CC69"],"limit":20})
└ [{"addr": "0x147F058CE", "xrefs": [{"addr": "0x147f1b084", "type": "data", "fn": {"addr": "0x147f1b084", "name":
"sub_147F188A4", "size": "0x36f2"}}], "more": false}, {"addr": "0x147F05EA2", "xrefs": [{"addr": "0x147f1b03c",
"type": "data", "fn": {"addr": "0x147f1b03c", "name": "sub_147F188A4", "size": "0x36f2"}}], "more": false},
{"addr": "0x147F4D030", "xrefs": [], "more": false}, {"addr": "0x147F4CC69", "xrefs": [], "more": false}]

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

• 我把这条 call 的本地回落点也接上,看它回到 0x147F1AE72 之后做什么。这样能把“分发器做完事以后,本函数接着干什么”说明
白。

• Called ida-pro-mcp.disasm({"addr":"0x147F1AE72","max_instructions":140,"include_total":true})
└ {"addr": "0x147F1AE72", "asm": {"name": "sub_147F188A4", "start_ea": "0x147f1ae72", "lines": "sub_147F188A4 (.tvm0 @
0x147f1ae72):\n147f1ae72 nop\n147f1ae73 lea r11, [rbp+var_s90]\n147f1ae7a xchg rax, rdx\n147f1ae7c
mov rdx, [r11]\n147f1ae7f not rdx\n147f1ae82 xchg rax, rdx\n147f1ae84 not rax\n147f1ae87 mov
rbx, [r11+8]\n147f1ae8b xchg rcx, r8\n147f1ae8e mov r8, [r11+10h]\n147f1ae92 not r8\n147f1ae95
rbx, [r11+8]\n147f18ef8 xchg rcx, rdx\n147f18efb mov rdx, [r11+10h]\n147f18eff not rdx\n147f18f02
xchg rdx, rcx\n147f18f05 not rcx\n147f18f08 xchg rdx, r8\n147f18f0b mov r8,
[r11+18h]\n147f18f0f n...
"0x14000440c", "0x1400045bc", "0x14000476c", "...

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

• 我已经把两条 call 的落点串起来了:0x147F1B0A4 -> 0x147F1AE72,上一条对应 0x147F1B05C -> 0x147F18EDF。最后再确认一下分
发器里有没有显式 ret/retn 落到这些返回地址,这样这条 call 型模式就闭环了。

• Called
└ ida-pro-mcp.py_eval({"code":"import ida_funcs, ida_ua, idc\nf = ida_funcs.get_func(0x147F37D73)\nres = []\nea =
f.start_ea\nwhile ea < f.end_ea:\n insn = ida_ua.insn_t()\n size = ida_ua.decode_insn(insn, ea)\n if
size <= 0:\n ea += 1\n continue\n mnem = idc.print_insn_mnem(ea)\n if
mnem.startswith('ret'):\n res.append(hex(ea))\n if len(res) >= 120:\n break\n ea +=
size\nprint('\\n'.join(res[:120]))\nprint('count=', len(res))"})
{"result": "None", "stdout":
"0x147f3a9d8\n0x147f4461a\n0x147f53748\n0x147f53c58\n0x147f56d38\n0x147f5fb31\n0x147f688d6\n0x147f6eb97\n0x147f7
0d21\n0x147f75044\n0x147f75ee8\n0x147f7e070\n0x147f9583d\n0x147f9c546\n0x147fa1035\n0x147fa642c\n0x147faeea0\n0x
147fb36d5\n0x147fb5c19\n0x147fb6356\n0x147fb9b50\n0x147fb9b53\n0x147fbb6d8\ncount= 23\n", "stderr": ""}

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

• 我已经把这条 call 型分发的结构摸清了,准备在关键点留几条注释,方便你后面自己顺着看和写脚本。

• Called
└ ida-pro-mcp.set_comments({"items":[{"addr":"0x147F1B0A4","comment":"call-based LLVM dispatcher: stack-passes
handler_ptr=0x147F058CE and state_ptr=&var_s88 into sub_147F37D73; returns to 0x147F1B0A9 then jumps to
0x147F1AE72"},{"addr":"0x147F1B05C","comment":"same call-based dispatcher pattern: handler_ptr=0x147F05EA2,
state_ptr=&var_s88; returns to 0x147F1B061 then jumps to 0x147F18EDF"},{"addr":"0x147F37D73","comment":"shared
call-based LLVM VM dispatcher. Prolog loads r11=[rsp+8]=handler_ptr and r10=[rsp+10]=state_ptr from caller-
staged stack slots, then enters threaded VM at 0x147F68C9B"},{"addr":"0x147F68C9B","comment":"dispatcher entry
block: consumes handler stream pointed by r11 (first dword is RVA-like selector, second dword is encrypted
operand) and dispatches through the large VM body; this is the real target of the call-based LLVM wrapper"}]})
[{"addr": "0x147F1B0A4", "ok": true}, {"addr": "0x147F1B05C", "ok": true, "error": "Failed to set decompiler comment
at 0x147f1b05c"}, {"addr": "0x147F37D73", "ok": true}, {"addr": "0x147F68C9B", "ok": true}]

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

• 从 0x147F1B0A4 这条看,结论先说:

这不是普通 call,而是一个“把参数压栈后进入公共 LLVM VM 分发器”的包装。你现有去 jmp r* 的脚本没覆盖到它,是因为这里的入
口形态变成了 call sub_147F37D73,但本质还是同一个大分发器。

我跟下来的关键链路是:

- 0x147F1B084 把 r11 = 0x147F058CE
- 0x147F1B08B 把 r10 = &var_s88
- 0x147F1B092 / 0x147F1B09B 连续压两个 qword 到栈上
- 0x147F1B0A4 call sub_147F37D73

进入 [sub_147F37D73] 后,开头这几句:

- mov r11, [rbp+98h]
- mov r10, [rbp+0A0h]

正好把调用前人工压栈的两个值取回来,所以这里的语义就是:

- r11 = handler_ptr = 0x147F058CE
- r10 = state_ptr = &var_s88

真正的 VM 入口不是函数头,而是 0x147F68C9B。这里开始消费 r11 指向的数据流。对这个点:

- *(u32 *)0x147F058CE = 0x07F4D030
- *(u32 *)(0x147F058CE+4) = 0xAA0290C1

上一条同类入口 0x147F1B05C 也是完全同结构,只是 handler_ptr 换成了 0x147F05EA2。

这条 call 的本地回落点也接上了:

- 0x147F1B0A4 -> ret 到 0x147F1B0A9 -> jmp 0x147F1AE72

而 /absent 这块会恢复寄存器/状态,调用 sub_144A097F0,然后又继续组下一条同类 call 分发:

- 下一条是 0x147F1B05C
- 它的回落点是 0x147F18EDF

所以这个模式本质是“链式 call-based VM block”,不是普通子函数调用。

你脚本里可以单独补一个特征:

sub rsp, 8
mov [rsp], r10
sub rsp, 8
mov [rsp], r11
call sub_147F37D73
int3
jmp CONT

对这个模式,建议脚本直接提取三元组:

- handler_ptr
- state_ptr
- cont(这里是 0x147F1AE72)

这样先把它识别成“call-based llvm dispatch stub”,不要按普通 call 处理。

我已经在 IDA 里给这些点加了注释:

- 0x147F1B0A4
- 0x147F37D73
- 0x147F68C9B

如果你要,我下一步可以直接帮你把这个模式写成一段 IDAPython 识别/去除脚本。

› import ida_bytes
import ida_idaapi
import ida_auto
import ida_segment
import idc

BADADDR = ida_idaapi.BADADDR

def u64(x):
return x & 0xFFFFFFFFFFFFFFFF

def s32(x):
x &= 0xFFFFFFFF
return x - 0x100000000 if x & 0x80000000 else x

def insn_len(ea):
return idc.get_item_size(ea)

def ensure_code(ea):
if ea == BADADDR:
return False
flags = ida_bytes.get_flags(ea)
if ida_bytes.is_code(flags):
return True
return idc.create_insn(ea) > 0

def mnem(ea):
if not ensure_code(ea):
return ""
return idc.print_insn_mnem(ea)

def opnd(ea, n):
if not ensure_code(ea):
return ""
return idc.print_operand(ea, n)

def next_head_safe(ea, limit):
if ea == BADADDR:
return BADADDR
n = idc.next_head(ea, limit)
return n if n != BADADDR else BADADDR

def is_pop_reg_bytes(ea, reg):
if ea == BADADDR:
return False

reg = (reg or "").lower()
b0 = ida_bytes.get_byte(ea)
b1 = ida_bytes.get_byte(ea + 1)

pop_map = {
"rax": (0x58, None),
"rcx": (0x59, None),
"rdx": (0x5A, None),
"rbx": (0x5B, None),
"rsp": (0x5C, None),
"rbp": (0x5D, None),
"rsi": (0x5E, None),
"rdi": (0x5F, None),
"r8": (0x41, 0x58),
"r9": (0x41, 0x59),
"r10": (0x41, 0x5A),
"r11": (0x41, 0x5B),
"r12": (0x41, 0x5C),
"r13": (0x41, 0x5D),
"r14": (0x41, 0x5E),
"r15": (0x41, 0x5F),
}

if reg not in pop_map:
return False

x = pop_map[reg]
if x[1] is None:
return b0 == x[0]
return b0 == x[0] and b1 == x[1]

def add_cmt(ea, text):
old = idc.get_cmt(ea, 0) or ""
if old:
text = old + " | " + text
idc.set_cmt(ea, text, 0)

def patch_rel_jmp(src, end_ea, dst):
if src == BADADDR or end_ea == BADADDR or dst == BADADDR:
return False

length = end_ea - src
if length < 5:
return False

rel = (dst - (src + 5)) & 0xFFFFFFFF

ida_bytes.del_items(src, ida_bytes.DELIT_SIMPLE, length)
ida_bytes.patch_byte(src, 0xE9)
ida_bytes.patch_dword(src + 1, rel)

for i in range(5, length):
ida_bytes.patch_byte(src + i, 0x90)

idc.create_insn(src)
idc.create_insn(dst)
ida_auto.auto_wait()
return True

def calc_pattern_a(push_ea, a, b, c, reg):
reg = (reg or "").lower()

if mnem(push_ea) != "push":
return None
if mnem(a) != "lea" or mnem(b) != "lea" or mnem(c) != "jmp":
return None
if (opnd(push_ea, 0) or "").lower() != reg:
return None
if (opnd(a, 0) or "").lower() != reg:
return None
if (opnd(b, 0) or "").lower() != reg:
return None
if (opnd(c, 0) or "").lower() != reg:
return None

base = idc.get_operand_value(a, 1)
imm = idc.get_operand_value(b, 1)
return u64(base + s32(imm))

def calc_pattern_b(push_ea, a, b, c, d, e, reg):
reg = (reg or "").lower()

if mnem(push_ea) != "push":
return None
if mnem(a) != "mov" or mnem(b) != "pushfq" or mnem(c) != "add" or mnem(d) != "popfq" or mnem(e) != "jmp":
return None
if (opnd(push_ea, 0) or "").lower() != reg:
return None
if (opnd(a, 0) or "").lower() != reg:
return None
if (opnd(c, 0) or "").lower() != reg:
return None
if (opnd(e, 0) or "").lower() != reg:
return None

base = idc.get_operand_value(a, 1)
imm = idc.get_operand_value(c, 1)
return u64(base + s32(imm))

def try_patch_block(block_ea, search_limit=0x250):
if block_ea == BADADDR:
return False

cmt = idc.get_cmt(block_ea, 0) or ""
if "patched_llvm_ijmp" in cmt:
return False

if not ensure_code(block_ea):
return False

limit = block_ea + search_limit
ea = block_ea

while ea != BADADDR and ea < limit:
if not ensure_code(ea):
ea = next_head_safe(ea, limit)
continue

if mnem(ea) != "push":
ea = next_head_safe(ea, limit)
continue

reg = (opnd(ea, 0) or "").lower()
if not reg:
ea = next_head_safe(ea, limit)
continue

a = next_head_safe(ea, limit)
b = next_head_safe(a, limit)
c = next_head_safe(b, limit)
d = next_head_safe(c, limit)
e = next_head_safe(d, limit)

target = None
patch_start = None
patch_end = None
pat_name = None

if a != BADADDR and b != BADADDR and c != BADADDR:
t = calc_pattern_a(ea, a, b, c, reg)
if t is not None and is_pop_reg_bytes(t, reg):
target = t
patch_start = a
patch_end = c + insn_len(c)
pat_name = "A"

if target is None and a != BADADDR and b != BADADDR and c != BADADDR and d != BADADDR and e != BADADDR:
t = calc_pattern_b(ea, a, b, c, d, e, reg)
if t is not None and is_pop_reg_bytes(t, reg):
target = t
patch_start = a
patch_end = e + insn_len(e)
pat_name = "B"

if target is not None and patch_start is not None:
if patch_rel_jmp(patch_start, patch_end, target):
add_cmt(block_ea, f"patched_llvm_ijmp pattern_{pat_name} -> {target:#x}")
return True

ea = next_head_safe(ea, limit)

return False

def patch_segment(seg_name=".tvm0"):
seg = ida_segment.get_segm_by_name(seg_name)
if not seg:
print(f"segment not found: {seg_name}")
return
patched = 0

ea = start
while ea < end:
flags = ida_bytes.get_flags(ea)
patched += 1

print(f"patched: {patched}")

patch_segment(".tvm0")这是原来的,你来改造新的

• 我先在工作区里找这段脚本对应的文件,然后直接把 call-based 这类 LLVM stub 支持补进去,并尽量保持你现有脚本风格不变。

└ (no output)

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

• 全盘递归太慢,我改用更快的文件搜索把脚本定位出来。

• Running rg -n --glob "*.py" "patched_llvm_ijmp pattern_" E:\

• Ran rg -n --glob "*.py" "def patch_segment\(" E:\
└ rg: E:\WindowsApps: 拒绝访问。 (os error 5)
rg: E:\study\University\大二上\数据库\MicrosoftSQL\MSSQL10_50.SQLEXPRESS\MSSQL\Backup: 拒绝访问。 (os error 5)
rg: E:\study\University\大二上\数据库\MicrosoftSQL\MSSQL10_50.SQLEXPRESS\MSSQL\JOBS: 拒绝访问。 (os error 5)
rg: E:\study\University\大二上\数据库\MicrosoftSQL\MSSQL10_50.SQLEXPRESS\MSSQL\Log: 拒绝访问。 (os error 5)
rg: E:\study\University\大二上\数据库\MicrosoftSQL\MSSQL10_50.SQLEXPRESS\MSSQL\repldata: 拒绝访问。 (os error 5)

• Ran rg -n --glob "*.py" "patched_llvm_ijmp pattern_" E:\
└ rg: E:\WindowsApps: 拒绝访问。 (os error 5)
rg: E:\study\University\大二上\数据库\MicrosoftSQL\MSSQL10_50.SQLEXPRESS\MSSQL\Backup: 拒绝访问。 (os error 5)
rg: E:\study\University\大二上\数据库\MicrosoftSQL\MSSQL10_50.SQLEXPRESS\MSSQL\JOBS: 拒绝访问。 (os error 5)
rg: E:\study\University\大二上\数据库\MicrosoftSQL\MSSQL10_50.SQLEXPRESS\MSSQL\Log: 拒绝访问。 (os error 5)
rg: E:\study\University\大二上\数据库\MicrosoftSQL\MSSQL10_50.SQLEXPRESS\MSSQL\repldata: 拒绝访问。 (os error 5)

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

• 我按当前样本目录缩小范围搜,避免扫整盘。找到文件后我就直接改。

• Running rg -n --glob "*.py" "def patch_segment\(" "E:\ctf"

• Ran rg -n --glob "*.py" "def patch_segment\(" "E:\ctf"
└ (no output)

• Ran rg -n --glob "*.py" "patched_llvm_ijmp pattern_" "E:\ctf"
└ (no output)

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

• 我这边准备做成“保留原有 A/B 直接补丁能力 + 新增 call-based 分发识别/标注”的扩展版。这个模式目前我不建议直接盲 patch 成
jmp,因为它和普通 jmp r* 不一样,分发器会改栈并消费 handler/state。

• Added llvm_patch_extended.py (+406 -0)
1 +import ida_auto
2 +import ida_bytes
3 +import ida_idaapi
4 +import ida_nalt
5 +import ida_segment
6 +import idc
7 +
8 +BADADDR = ida_idaapi.BADADDR
9 +
10 +# call-based LLVM stub 目前默认只做识别和标注,不直接改字节。
11 +# 这类 stub 不是简单的 jmp r* 变体,分发器会消费 handler/state 并改写栈。
12 +PATCH_CALL_DISPATCH_TO_JMP = False
13 +
14 +
15 +def u64(x):
16 + return x & 0xFFFFFFFFFFFFFFFF
17 +
18 +
19 +def s32(x):
20 + x &= 0xFFFFFFFF
21 + return x - 0x100000000 if x & 0x80000000 else x
22 +
23 +
24 +def insn_len(ea):
25 + return idc.get_item_size(ea)
26 +
27 +
28 +def ensure_code(ea):
29 + if ea == BADADDR:
30 + return False
31 + flags = ida_bytes.get_flags(ea)
32 + if ida_bytes.is_code(flags):
33 + return True
34 + return idc.create_insn(ea) > 0
35 +
36 +
37 +def mnem(ea):
38 + if not ensure_code(ea):
39 + return ""
40 + return idc.print_insn_mnem(ea)
41 +
42 +
43 +def opnd(ea, n):
44 + if not ensure_code(ea):
45 + return ""
46 + return idc.print_operand(ea, n)
47 +
48 +
49 +def opval(ea, n):
50 + if not ensure_code(ea):
51 + return BADADDR
52 + return idc.get_operand_value(ea, n)
53 +
54 +
55 +def next_head_safe(ea, limit):
56 + if ea == BADADDR:
57 + return BADADDR
58 + n = idc.next_head(ea, limit)
59 + return n if n != BADADDR else BADADDR
60 +
61 +
62 +def is_pop_reg_bytes(ea, reg):
63 + if ea == BADADDR:
64 + return False
65 +
66 + reg = (reg or "").lower()
67 + b0 = ida_bytes.get_byte(ea)
68 + b1 = ida_bytes.get_byte(ea + 1)
69 +
70 + pop_map = {
71 + "rax": (0x58, None),
72 + "rcx": (0x59, None),
73 + "rdx": (0x5A, None),
74 + "rbx": (0x5B, None),
75 + "rsp": (0x5C, None),
76 + "rbp": (0x5D, None),
77 + "rsi": (0x5E, None),
78 + "rdi": (0x5F, None),
79 + "r8": (0x41, 0x58),
80 + "r9": (0x41, 0x59),
81 + "r10": (0x41, 0x5A),
82 + "r11": (0x41, 0x5B),
83 + "r12": (0x41, 0x5C),
84 + "r13": (0x41, 0x5D),
85 + "r14": (0x41, 0x5E),
86 + "r15": (0x41, 0x5F),
87 + }
88 +
89 + if reg not in pop_map:
90 + return False
91 +
92 + x = pop_map[reg]
93 + if x[1] is None:
94 + return b0 == x[0]
95 + return b0 == x[0] and b1 == x[1]
96 +
97 +
98 +def add_cmt(ea, text):
99 + old = idc.get_cmt(ea, 0) or ""
100 + if text in old:
101 + return
102 + if old:
103 + text = old + " | " + text
104 + idc.set_cmt(ea, text, 0)
105 +
106 +
107 +def patch_rel_jmp(src, end_ea, dst):
108 + if src == BADADDR or end_ea == BADADDR or dst == BADADDR:
109 + return False
110 +
111 + length = end_ea - src
112 + if length < 5:
113 + return False
114 +
115 + rel = (dst - (src + 5)) & 0xFFFFFFFF
116 +
117 + ida_bytes.del_items(src, ida_bytes.DELIT_SIMPLE, length)
118 + ida_bytes.patch_byte(src, 0xE9)
119 + ida_bytes.patch_dword(src + 1, rel)
120 +
121 + for i in range(5, length):
122 + ida_bytes.patch_byte(src + i, 0x90)
123 +
124 + idc.create_insn(src)
125 + idc.create_insn(dst)
126 + ida_auto.auto_wait()
127 + return True
128 +
129 +
130 +def calc_pattern_a(push_ea, a, b, c, reg):
131 + reg = (reg or "").lower()
132 +
133 + if mnem(push_ea) != "push":
134 + return None
135 + if mnem(a) != "lea" or mnem(b) != "lea" or mnem(c) != "jmp":
136 + return None
137 + if (opnd(push_ea, 0) or "").lower() != reg:
138 + return None
139 + if (opnd(a, 0) or "").lower() != reg:
140 + return None
141 + if (opnd(b, 0) or "").lower() != reg:
142 + return None
143 + if (opnd(c, 0) or "").lower() != reg:
144 + return None
145 +
146 + base = idc.get_operand_value(a, 1)
147 + imm = idc.get_operand_value(b, 1)
148 + return u64(base + s32(imm))
149 +
150 +
151 +def calc_pattern_b(push_ea, a, b, c, d, e, reg):
152 + reg = (reg or "").lower()
153 +
154 + if mnem(push_ea) != "push":
155 + return None
156 + if mnem(a) != "mov" or mnem(b) != "pushfq" or mnem(c) != "add" or mnem(d) != "popfq" or mnem(e) != "jmp":
157 + return None
158 + if (opnd(push_ea, 0) or "").lower() != reg:
159 + return None
160 + if (opnd(a, 0) or "").lower() != reg:
161 + return None
162 + if (opnd(c, 0) or "").lower() != reg:
163 + return None
164 + if (opnd(e, 0) or "").lower() != reg:
165 + return None
166 +
167 + base = idc.get_operand_value(a, 1)
168 + imm = idc.get_operand_value(c, 1)
169 + return u64(base + s32(imm))
170 +
171 +
172 +def try_patch_block(block_ea, search_limit=0x250):
173 + if block_ea == BADADDR:
174 + return False
175 +
176 + cmt = idc.get_cmt(block_ea, 0) or ""
177 + if "patched_llvm_ijmp" in cmt:
178 + return False
179 +
180 + if not ensure_code(block_ea):
181 + return False
182 +
183 + limit = block_ea + search_limit
184 + ea = block_ea
185 +
186 + while ea != BADADDR and ea < limit:
187 + if not ensure_code(ea):
188 + ea = next_head_safe(ea, limit)
189 + continue
190 +
191 + if mnem(ea) != "push":
192 + ea = next_head_safe(ea, limit)
193 + continue
194 +
195 + reg = (opnd(ea, 0) or "").lower()
196 + if not reg:
197 + ea = next_head_safe(ea, limit)
198 + continue
199 +
200 + a = next_head_safe(ea, limit)
201 + b = next_head_safe(a, limit)
202 + c = next_head_safe(b, limit)
203 + d = next_head_safe(c, limit)
204 + e = next_head_safe(d, limit)
205 +
206 + target = None
207 + patch_start = None
208 + patch_end = None
209 + pat_name = None
210 +
211 + if a != BADADDR and b != BADADDR and c != BADADDR:
212 + t = calc_pattern_a(ea, a, b, c, reg)
213 + if t is not None and is_pop_reg_bytes(t, reg):
214 + target = t
215 + patch_start = a
216 + patch_end = c + insn_len(c)
217 + pat_name = "A"
218 +
219 + if target is None and a != BADADDR and b != BADADDR and c != BADADDR and d != BADADDR and e != BADADDR:
220 + t = calc_pattern_b(ea, a, b, c, d, e, reg)
221 + if t is not None and is_pop_reg_bytes(t, reg):
222 + target = t
223 + patch_start = a
224 + patch_end = e + insn_len(e)
225 + pat_name = "B"
226 +
227 + if target is not None and patch_start is not None:
228 + if patch_rel_jmp(patch_start, patch_end, target):
229 + add_cmt(block_ea, f"patched_llvm_ijmp pattern_{pat_name} -> {target:#x}")
230 + return True
231 +
232 + ea = next_head_safe(ea, limit)
233 +
234 + return False
235 +
236 +
237 +def is_rsp_alloc_8(ea):
238 + mm = mnem(ea)
239 + if mm == "sub":
240 + return (opnd(ea, 0) or "").lower() == "rsp" and opval(ea, 1) == 8
241 + if mm == "lea":
242 + dst = (opnd(ea, 0) or "").lower()
243 + src = (opnd(ea, 1) or "").lower().replace(" ", "")
244 + return dst == "rsp" and src in ("[rsp-8]", "[esp-8]")
245 + return False
246 +
247 +
248 +def is_mov_stack_from_reg(ea, reg):
249 + if mnem(ea) != "mov":
250 + return False
251 + dst = (opnd(ea, 0) or "").lower().replace(" ", "")
252 + src = (opnd(ea, 1) or "").lower()
253 + return "[rsp" in dst and src == reg.lower()
254 +
255 +
256 +def read_u32(ea):
257 + if ea == BADADDR:
258 + return None
259 + return ida_bytes.get_dword(ea) & 0xFFFFFFFF
260 +
261 +
262 +def looks_like_call_dispatch(block_ea, search_limit=0x80):
263 + if block_ea == BADADDR or not ensure_code(block_ea):
264 + return None
265 +
266 + limit = block_ea + search_limit
267 + ea = block_ea
268 +
269 + while ea != BADADDR and ea < limit:
270 + if mnem(ea) != "lea" or (opnd(ea, 0) or "").lower() != "r11":
271 + ea = next_head_safe(ea, limit)
272 + continue
273 +
274 + handler_ptr = opval(ea, 1)
275 + a = next_head_safe(ea, limit)
276 + b = next_head_safe(a, limit)
277 + c = next_head_safe(b, limit)
278 + d = next_head_safe(c, limit)
279 + e = next_head_safe(d, limit)
280 + f = next_head_safe(e, limit)
281 + g = next_head_safe(f, limit)
282 +
283 + if a == BADADDR or b == BADADDR or c == BADADDR or d == BADADDR or e == BADADDR:
284 + ea = next_head_safe(ea, limit)
285 + continue
286 +
287 + if mnem(a) != "lea" or (opnd(a, 0) or "").lower() != "r10":
288 + ea = next_head_safe(ea, limit)
289 + continue
290 +
291 + state_expr = opnd(a, 1) or ""
292 + if "rbp" not in state_expr.lower():
293 + ea = next_head_safe(ea, limit)
294 + continue
295 +
296 + if not is_rsp_alloc_8(b):
297 + ea = next_head_safe(ea, limit)
298 + continue
299 +
300 + if not is_mov_stack_from_reg(c, "r10"):
301 + ea = next_head_safe(ea, limit)
302 + continue
303 +
304 + if not is_rsp_alloc_8(d):
305 + ea = next_head_safe(ea, limit)
306 + continue
307 +
308 + if not is_mov_stack_from_reg(e, "r11"):
309 + ea = next_head_safe(ea, limit)
310 + continue
311 +
312 + if f == BADADDR or mnem(f) != "call":
313 + ea = next_head_safe(ea, limit)
314 + continue
315 +
316 + call_target = opval(f, 0)
317 + cont = BADADDR
318 + ret_stub = next_head_safe(f, limit)
319 + jmp_stub = next_head_safe(ret_stub, limit) if ret_stub != BADADDR else BADADDR
320 +
321 + if ret_stub != BADADDR and mnem(ret_stub) == "int3" and jmp_stub != BADADDR and mnem(jmp_stub) == "jmp"
:
322 + cont = opval(jmp_stub, 0)
323 + end_ea = jmp_stub + insn_len(jmp_stub)
324 + elif ret_stub != BADADDR and mnem(ret_stub) == "jmp":
325 + cont = opval(ret_stub, 0)
326 + end_ea = ret_stub + insn_len(ret_stub)
327 + else:
328 + end_ea = f + insn_len(f)
329 +
330 + first_dword = read_u32(handler_ptr)
331 + vm_entry = BADADDR
332 + if first_dword is not None:
333 + vm_entry = u64(ida_nalt.get_imagebase() + first_dword)
334 +
335 + return {
336 + "block_ea": block_ea,
337 + "handler_lea": ea,
338 + "state_lea": a,
339 + "stack_a": b,
340 + "stack_b": d,
341 + "call_ea": f,
342 + "end_ea": end_ea,
343 + "handler_ptr": handler_ptr,
344 + "state_expr": state_expr,
345 + "call_target": call_target,
346 + "cont": cont,
347 + "vm_entry": vm_entry,
348 + }
349 +
350 + return None
351 +
352 +
353 +def try_mark_call_dispatch(block_ea):
354 + info = looks_like_call_dispatch(block_ea)
355 + if not info:
356 + return False
357 +
358 + cmt = idc.get_cmt(info["block_ea"], 0) or ""
359 + if "patched_llvm_call_dispatch" in cmt:
360 + return False
361 +
362 + text = (
363 + f"patched_llvm_call_dispatch handler={info['handler_ptr']:#x} "
364 + f"vm_entry={info['vm_entry']:#x} state={info['state_expr']} "
365 + f"call={info['call_target']:#x} cont={info['cont']:#x}"
366 + )
367 + add_cmt(info["block_ea"], text)
368 + add_cmt(info["call_ea"], text)
369 +
370 + if PATCH_CALL_DISPATCH_TO_JMP and info["cont"] != BADADDR:
371 + if patch_rel_jmp(info["handler_lea"], info["end_ea"], info["cont"]):
372 + add_cmt(info["block_ea"], f"patched_call_dispatch_to_jmp -> {info['cont']:#x}")
373 +
374 + return True
375 +
376 +
377 +def patch_segment(seg_name=".tvm0"):
378 + seg = ida_segment.get_segm_by_name(seg_name)
379 + if not seg:
380 + print(f"segment not found: {seg_name}")
381 + return
382 +
383 + start = seg.start_ea
386 + marked_call = 0
387 +
388 + ea = start
389 + while ea < end:

• Ran python -m py_compile E:\llvm_patch_extended.py
└ (no output)

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

• 新脚本已经写到 E:/llvm_patch_extended.py,并且做过 py_compile,语法没问题。

这版改动是:

- 保留你原来的 pattern_A / pattern_B 直接补丁逻辑。
- 新增 call-based LLVM dispatch 识别:
- 识别 lea r11, handler
- lea r10, [rbp+state]
- 两次给栈塞 r10/r11
- call sub_xxx
- int3/jmp cont
- 识别后会在 block 和 call 上加注释,标出:
- handler
- vm_entry
- state
- call target
- cont

我默认没有把这类 call 直接 patch 成 jmp,因为它和你原来那种 jmp r* 不一样,分发器会真正消费 handler/state 并动栈,直接
改跳转有风险。脚本里留了开关:

PATCH_CALL_DISPATCH_TO_JMP = False

如果你后面确认某一批样本里这么改是安全的,再改成 True 试。

你现在可以在 IDA 里直接运行:

exec(open(r"E:\llvm_patch_extended.py", "r", encoding="utf-8").read())

如果你要,我下一步可以继续把这类 call-based 模式再往前推进一层,直接根据 handler 头部的 dword 把 vm_entry 和本地回落块
做成更强的自动化归并。

› Find and fix a bug in @file

然后要打别的线下赛了,遗憾落幕了