用 Agent 逆向 IDA:定位 emulator 报错
2026–08–18
在研究 Windows ARM 二进制时,F5 报错 emulator:,搜索了一圈无果,故尝试逆向 IDA 相关二进制对这个问题进行溯源和解决。
Decompilation failure:
1800F45F0: emulator:
有个玄学方法,先点击一个小函数,成功 F5 了后,再点击 1800F45F0 函数就可以成功反编译。不过这样也挺烦的,需要一个一个函数的点。
先搜索字符串:
/opt/ida-pro
> rg -a "emulator:" -l -g '*.so'
plugins/hexx64.so
plugins/hexrv.so
plugins/hexmips.so
plugins/hexarc.so
plugins/hexppc.so
plugins/hexarm.so
plugins/hexv850.so
我正在研究 arm 的二进制,那就直接调查 hexarm.so 了,发现字符串有两处函数调用,分别 hook 函数。
const printf = console.log;
function hook() {
var so = Process.findModuleByName("hexarm.so").base;
Interceptor.attach(so.add(0x331470), {
onEnter: function(args) {
printf("0x331470");
}
});
Interceptor.attach(so.add(0x336B50), {
onEnter: function(args) {
printf("0x336B50");
}
});
}
setTimeout(hook, 5000);
frida -l hook.js -f /usr/bin/ida KernelBase.dll.i64 > frida-hooklog-$(date +%Y%m%d-%H%M%S).txt
-> 0x331470
打印堆栈
const printf = console.log;
function hook() {
var so = Process.findModuleByName("hexarm.so").base;
Interceptor.attach(so.add(0x331470), {
onEnter: function(args) {
printf("0x331470");
printf(Thread.backtrace(this.context, Backtracer.FUZZY)
.map(DebugSymbol.fromAddress)
.join("\n"));
}
});
}
setTimeout(hook, 5000);
0x7f31d173c755 hexarm.so!0x33c755
0x7f31d1795846 hexarm.so!0x395846
0x7f322d905c75 libida.so!0x505c75
0x7f31d179b644 hexarm.so!0x39b644
0x7f31d17ba435 hexarm.so!0x3ba435
0x7f31d17bd71c hexarm.so!0x3bd71c
0x55fa23f61e80 ida!0x277e80
0x55fa23e68fff ida!0x17efff
0x55fa24162119 ida!0x478119
0x55fa23e6e78d ida!0x18478d
0x55fa2421addb ida!0x530ddb
0x55fa23fee93b ida!0x30493b
0x7f323040089e libQt6Core.so.6!0x20089e
0x7f323125b3f6 libQt6Gui.so.6!_ZN2QT7QAction9triggeredEb+0x46
0x7f323125e405 libQt6Gui.so.6!_ZN2QT7QAction8activateENS0_11ActionEventE+0xf5
0x7f323125f4b0 libQt6Gui.so.6!_ZN2QT7QAction5eventEPNS_6QEventE+0x70
调用 Agent 开始逆向。
现在我在分析,为什么我在 ida-pro 中按 F5 了之后会报错:
Decompilation failure:
1800F45F0: emulator:
我搜索 emulator: 找到了 hexarm.so 中的 0x331470 函数,里面有对一个字符串列表的调用,其中一个项就是 "emulator: %s",我用 frida hook 了之后打印堆栈。请你调用 ida-mcp 对程序进行分析,并找到根本原因,如果你需要更多的运行上下文,请修改 hook.js 让我 hook。将报告写入 report/
0x7f31d173c755 hexarm.so!0x33c755
0x7f31d1795846 hexarm.so!0x395846
0x7f322d905c75 libida.so!0x505c75
0x7f31d179b644 hexarm.so!0x39b644
0x7f31d17ba435 hexarm.so!0x3ba435
0x7f31d17bd71c hexarm.so!0x3bd71c
0x55fa23f61e80 ida!0x277e80
0x55fa23e68fff ida!0x17efff
0x55fa24162119 ida!0x478119
0x55fa23e6e78d ida!0x18478d
0x55fa2421addb ida!0x530ddb
0x55fa23fee93b ida!0x30493b
0x7f323040089e libQt6Core.so.6!0x20089e
0x7f323125b3f6 libQt6Gui.so.6!_ZN2QT7QAction9triggeredEb+0x46
0x7f323125e405 libQt6Gui.so.6!_ZN2QT7QAction8activateENS0_11ActionEventE+0xf5
0x7f323125f4b0 libQt6Gui.so.6!_ZN2QT7QAction5eventEPNS_6QEventE+0x70
Agent 找到了是 hrtng.so 的锅,升级之后就没问题了!
Agent’s blog
背景
在 IDA Pro 中反编译 Windows ARM64 的 KernelBase.dll,对一个函数按 F5 时,弹出了如下错误:
Decompilation failure:
1800F45F0: emulator:
KernelBase.dll 的基址是 0x180000000,所以 1800F45F0 对应 RVA 0xF45F0 的一个函数。比较反常的是 emulator: 后面没有任何描述文本。本文记录排查过程:从这条报错出发,顺藤摸瓜,定位到报错的实际来源——一个第三方插件 hrtng.so(KasperskyLab 出品)在微码处理器链上的 handler。
报错信息的来源
先在反编译器二进制里搜索 emulator 字符串。在 hexarm.so(ARM64 反编译器)里找到唯一一处 emulator: %s,地址 0x414297,它被一个名为 error_str_list 的字符串表引用。
注意:
hexarm.so尽管名字带 arm,实际是 ARM64 反编译器。它内部包含"ARM64 Decompiler"、"HEXARM64"等字符串。
引用 emulator: %s 的函数是 sub_331470,反编译后关键逻辑如下:
__int64 sub_331470(__int64 a1 /*输出*/, __int64 a2 /*错误码指针*/, __int64 a3)
{
const char *msg = ""; // 默认空
if (*(a2 + 24) != 0) // 若携带了额外消息
msg = *(const char **)(a2 + 16);
int idx = 1 - *(int *)a2; // 错误码 -> 表下标
qsnprintf(s, 1024, error_str_list[idx], msg); // 查表拼字符串
}
sub_331470 是一个错误消息格式化器:读一个错误码,在 error_str_list 中查格式串,拼出最终消息。
先 hook 它,打印调用栈,确定是谁在报错:
Interceptor.attach(base.add(0x331470), {
onEnter: function(args) {
console.log(Thread.backtrace(this.context, Backtracer.FUZZY)
.map(DebugSymbol.fromAddress).join("\n"));
}
});
得到的调用栈如下(地址均为模块偏移):
0x7f31d173c755 hexarm.so!0x33c755
0x7f31d1795846 hexarm.so!0x395846
0x7f322d905c75 libida.so!0x505c75
0x7f31d179b644 hexarm.so!0x39b644
0x7f31d17ba435 hexarm.so!0x3ba435
0x7f31d17bd71c hexarm.so!0x3bd71c
0x55fa23f61e80 ida!0x277e80
...
0x7f323125b3f6 libQt6Gui.so.6!_ZN2QT7QAction9triggeredEb+0x46
路径是:F5 热键(Qt QAction)→ ida 主程序 → hexarm.so 的反编译流程。
错误码 -35(m_emulator)
把 error_str_list(0x4DE080)完整 dump 出来,是一张 37 项的错误码到格式串的映射表,关键几项:
下标 2 错误码 -1 INTERR: %s
下标 3 错误码 -2 cannot convert to microcode
下标 8 错误码 -7 switch analysis failed: %s
下标 35 错误码 -34 cloud: %s
下标 36 错误码 -35 emulator: %s
因此 emulator: 对应的错误码是 -35,即 Hex-Rays 的 m_emulator。%s 是报错时附带的具体描述。
sub_331470 的调用者 sub_33C710 是打印 Decompilation failure 的错误报告器:
// sub_33C710 —— 错误报告器
sub_331470(&buf, err + 8, err); // 拼出 "emulator: %s"
sub_6CAE0("%a: %s\n", *(err + 16), buf); // 写进反编译注释
if (!batch)
sub_2B5270("Decompilation failure:\n%a: %s\n\n"
"Please refer to the manual...", *(err + 16), buf);
其中 %a 是函数地址 0x1800F45F0,%s 是格式化后的 emulator: ...。到这里报错文本的结构已经清楚,剩下两个问题:
- 错误码 -35 是谁、在哪一步产生的?
- 为什么
%s是空的?
反编译调用链
从 sub_33C710 向上追,得到反编译的驱动链(均为 hexarm.so 偏移):
sub_3BD6C0 (插件动作) → sub_3BA420 → sub_39B5A0 (反编译驱动)
→ sub_394E60 (反编译主循环)
→ sub_14C840 → sub_14B9F0 (生成微码)
sub_14B9F0 是微码生成的主函数。问题被收敛到微码生成阶段。
一个误导:内层指令/操作数模拟器
搜索立即数 -35(0xFFFFFFDD),在二进制里找到 7 处 mov esi, 0FFFFFFDDh,都围绕着一个异常构造函数 sub_136DB0。它是 vd_failure_t(值定义失败)异常的构造函数:
// sub_136DB0 —— vd_failure_t 异常构造
*(int*)(a1 + 8) = a2; // 错误码
*(a1 + 16) = a3; // 地址
*(a1 + 24) = copy(s); // 消息
这 7 处调用点分布在 4 个函数里,各自携带不同的消息:
sub_319160 (指令模拟器) -> "no support for floating point"
sub_319160 (指令模拟器) -> "unhandled opcode"
sub_319E50 (操作数模拟器) -> "unknown operand type"
sub_319E50 (操作数模拟器) -> "too big operand"
最初判断这就是答案:反编译器的指令/操作数模拟器遇到不支持的指令,抛出 m_emulator。
但 hook sub_136DB0 后,发现整个反编译过程中它只在 0x3c44b 被调用了一次,而且消息为空。也就是说,上面那 4 处带消息的 throw 点根本没有执行。
这提醒了一点:静态分析能找到「能抛出这个错误的代码」,但不等于「本次实际抛出这个错误的代码」。需要运行时证据来确认。
实际路径:-35 是返回值,不是异常
重新看 hook 到的栈:sub_136DB0 的调用点 0x3c44b 位于 sub_14B9F0 的尾块,它上面的代码是:
0x14bd8d: call sub_33E5E0 ; 微码处理器链分发
0x14bd92: mov r14d, eax ; 返回值 -> r14d
0x14bd95: test eax, eax
0x14bd97: jz <成功路径>
...
0x14bdd3: jz 0x3c422 ; 返回 -1 则跳 throw 块
关键在于 sub_33E5E0,反编译如下:
__int64 sub_33E5E0(a1, unsigned phase, ...)
{
node = *(void**)(a1 + 120); // 处理器链链表头
while (node) {
result = node->handler(node->ctx, phase, va); // handler(ctx, phase, va_list)
if (result) return result; // 返回第一个非零错误码
node = node->next;
}
return 0;
}
这是一个处理器链分发器:遍历链表,逐个调用 handler,谁先返回非零就用谁的。sub_14B9F0 在 phase=7 时调用它。-35 不是某个函数 throw 出来的,而是某个 handler 的返回值,它不带消息,因此 %s 为空。这也解释了报错中 emulator: 后面空白的原因。
处理器链上的模块
hook sub_33E5E0,把处理器链 dump 出来:
Interceptor.attach(base.add(0x33E5E0), {
onEnter: function(args) {
console.log("=== dispatcher phase=" + args[1].toInt32() + " ===");
var node = args[0].add(120).readPointer(), i = 0;
while (!node.isNull() && i < 64) {
console.log(" [" + i + "] " + DebugSymbol.fromAddress(node.add(8).readPointer()));
node = node.readPointer(); i++;
}
},
onLeave: function(retval) {
var r = retval.toInt32();
if (r !== 0) console.log("=== dispatcher returns " + r + " ===");
}
});
结果:这条链有 5 个 handler,分布在 4 个动态库:
[0] _ida_hexrays.so + 0x185020
[1] hrtng.so + 0x49cc0
[2] goomba.so + 0xe290
[3] hexarm.so + 0x3eaa00
[4] hexarm.so + 0x3d91a0
这条链其实是 Hex-Rays 的微码扩展机制:核心、架构后端、以及第三方插件,都会把自己的 handler 注册进这条链,按 phase 依次被调用。其中 _ida_hexrays.so 是核心,hexarm.so 是 ARM64 架构后端,而 hrtng.so 是卡巴斯基(KasperskyLab)出品的第三方插件。
hexarm.so 里那两个 handler 在 IDA 中的入口分别是 cmp esi,15 和 cmp esi,74h,只处理 phase 15 和 phase 116,不处理 phase 7。因此返回 -35 的 handler 只能在前三个模块里。
日志同时给出了失败阶段:
=== dispatcher phase=7 ===
=== dispatcher phase=18 === ← phase 7 内部嵌套
=== dispatcher returns -35 (0xffffffdd) ===
失败发生在 phase 7(微码 emulator/优化阶段)。
逐个 hook,定位到 hrtng.so
最后逐个 hook 5 个 handler,看谁在 phase 7 返回 -35:
function hookHandler(mod, off, label) {
var target = Process.getModuleByName(mod).base.add(off);
Interceptor.attach(target, {
onEnter: function(args) { this._phase = args[1].toInt32(); },
onLeave: function(retval) {
var r = retval.toInt32();
if (r !== 0)
console.log(">>> handler " + label + " phase=" + this._phase + " returns " + r);
}
});
}
hookHandler("_ida_hexrays.so", 0x185020, "[0] core");
hookHandler("hrtng.so", 0x49cc0, "[1] hrtng");
hookHandler("goomba.so", 0xe290, "[2] goomba");
hookHandler("hexarm.so", 0x3eaa00, "[3] hexarm-15");
hookHandler("hexarm.so", 0x3d91a0, "[4] hexarm-116");
运行一次 F5,日志输出:
>>> handler [1] hrtng (hrtng.so+0x49cc0) phase=7 returns -35 (0xffffffdd)
!!! THIS handler returned m_emulator=-35 !!!
返回 -35(m_emulator)的是 hrtng.so 在 phase 7 的 handler,而不是 hexarm.so 的内层指令模拟器,也不是 goomba.so。
根因与解决
hrtng(https://github.com/KasperskyLab/hrtng)是卡巴斯基实验室开源的 IDA Pro 插件,定位是「逆向分析工具集」,包含大量基于 Hex-Rays 微码的功能——去混淆(控制流反扁平化)、微码优化、微码签名、AVX 提升等。它通过 Hex-Rays 的微码扩展机制,把自定义 handler 注册进了上面这条处理器链。
因此,报错的根因是:hrtng 插件自己的微码优化器在 phase 7(emulator 阶段)无法处理目标函数 KernelBase.dll!0xF45F0 里的某条指令,于是返回 m_emulator(-35)。Hex-Rays 核心拿到这个非零错误码后,重抛 vd_failure_t,最终以空消息的形式弹出 Decompilation failure。
解决方式是更新 hrtng 插件(或临时禁用),更新后该函数即可正常反编译。hrtng.so 位于 ~/.idapro/plugins/hrtng.so。
总结
- 「能抛出这个错误的代码」和「本次实际抛出这个错误的代码」是两回事。静态分析找到的 4 处带详细消息的 throw 点(浮点不支持、未知 opcode 等)实际并未执行,真正的错误是一条返回裸错误码、不带消息的路径。需要运行时证据来区分。
- 报错信息本身是线索。
emulator: %s中空的%s暗示错误来自「返回值」而非「抛异常」的路径,因为该路径不携带描述文本。 - Hex-Rays 的微码处理器链(
sub_33E5E0遍历的链表)是一个扩展点,核心、架构后端、第三方插件都会注册 handler 进去。这次的错误并非 Hex-Rays 本身的问题,而是第三方插件hrtng的优化器返回的m_emulator。 - 排查方法:字符串定位 → 错误码破译 → 调用链回溯 → 动态 hook 验证 → 处理器链枚举 → 逐个 handler 定位。静态分析提供地图,动态插桩提供事实,两者结合。
最终,这条 Decompilation failure: 1800F45F0: emulator: 被完整还原为:第三方插件 hrtng 在微码处理器链 phase 7 的 handler 返回 m_emulator(-35),sub_14B9F0 拿到后重抛 vd_failure_t,最终以空消息的形式弹出该报错。