逆向工程指南
本文介绍如何独立逆向 NTQQ 的 wrapper.node 原生模块,不依赖其他开源项目的代码。
WARNING
本文仅供学习和研究目的。请遵守相关法律法规。逆向工程结果不建议公开发布。
方法论
wrapper.node 是一个标准的 Node.js native addon(.node 文件),底层是编译好的 C++ 共享库。逆向它主要有三种途径:
1. 运行时内省(最高效)
由于 wrapper.node 被加载到 Node.js 进程中,可以直接用 JavaScript 枚举所有导出:
// 加载模块
const wrapper = require("/opt/QQ/resources/app/wrapper.node");
// 枚举所有顶级导出(类名)
console.log(Object.keys(wrapper));
// → ["NodeIQQNTWrapperEngine", "NodeIKernelMsgService", ...]
// 枚举某个类的实例方法
const engine = wrapper.NodeIQQNTWrapperEngine.get();
const methods = Object.getOwnPropertyNames(Object.getPrototypeOf(engine));
console.log(methods);
// → ["initWithDeskTopConfig", "initWithMobileConfig", ...]
// 检查方法参数个数
console.log(engine.initWithDeskTopConfig.length);
// → 0 (native 方法的 .length 通常为 0)Waylay 内置了 __introspect Action,可以一键导出完整的 API 表面:
{ "action": "__introspect", "params": {}, "echo": "1" }返回所有类、所有方法、参数个数。
2. 二进制字符串提取
从 .node 文件中提取 ASCII 字符串,可以发现:
- Proto 文件名:
im_msg_body.proto、google/protobuf/descriptor.proto - 协议路由:
OidbSvcTrpcTcp.0x88d_0、trpc.msg.msg_svc.MsgService.SsoC2CRecallMsg - 错误信息:包含方法名和参数提示
- 日志字符串:包含内部函数名和数据格式
const fs = require("fs");
const buf = fs.readFileSync("/opt/QQ/resources/app/wrapper.node");
const strings = [];
let current = "";
for (let i = 0; i < buf.length; i++) {
const c = buf[i];
if (c >= 32 && c < 127) current += String.fromCharCode(c);
else { if (current.length >= 8) strings.push(current); current = ""; }
}3. 动态 Hook(Frida)
Frida 可以 hook 运行中进程的 native 函数:
pip install frida-tools
frida -p <QQ_PID> -l hook-script.jsWARNING
Frida 注入 Electron 进程可能被拒绝(我们的测试中 QQ 进程拒绝了 frida-agent 加载)。替代方案是使用 LD_PRELOAD 或在进程启动时注入。
已知的 API 表面
通过上述方法,我们从 QQ 3.2.27 的 wrapper.node 中提取到:
| 类别 | 数量 |
|---|---|
| 导出类 | 85 |
| 实例方法 | 2,318+ |
| Session Services | 48 |
| OidbSvc 协议命令 | 159 |
| trpc 路由 | 133 |
| .proto 文件引用 | 164 |
详细列表见 NTQQ API 总览、Session Services、协议路由。
推断参数类型的技巧
native 方法的 .length 属性通常返回 0,无法直接获取参数信息。但可以通过以下方式推断:
1. 错误消息
传入错误参数时,有时会得到有用的错误信息:
try {
service.someMethod("wrong");
} catch (e) {
console.log(e.message);
// → "expected object at argument 0"
}2. 观察 Listener 回调数据
调用某个方法后,观察对应 Listener 收到的回调数据,可以反推该方法触发了什么操作:
// 调用 getGroupList
session.getGroupService().getGroupList(true);
// 观察 nodeIKernelGroupListener.onGroupListUpdate 回调中的数据3. 日志字符串对照
wrapper.node 内的日志字符串经常包含方法名和参数名:
BuddyService:approvalFriendRequest
BuddyList OnBuddyListChange:{}, line:{}
oidb_0xfe1 by uin return ok, but respBuf empty!Protobuf 逆向
wrapper.node 内部使用 Google Protobuf。从二进制中发现了 im_msg_body.proto 等引用。
目前可行的 protobuf 逆向方法:
- 捕获序列化数据:在 Listener 回调中拦截 raw buffer,使用
protobuf-inspector或blackboxprotobuf解码 - 符号表分析:使用
nm -D wrapper.node查找 protobuf 相关的符号 - 描述符提取:protobuf 的
FileDescriptorProto可能嵌入在二进制中,可以用工具提取
# 查找 protobuf 相关导出符号
nm -D /opt/QQ/resources/app/wrapper.node | grep -i proto