Skip to content

逆向工程指南

本文介绍如何独立逆向 NTQQ 的 wrapper.node 原生模块,不依赖其他开源项目的代码。

WARNING

本文仅供学习和研究目的。请遵守相关法律法规。逆向工程结果不建议公开发布。

方法论

wrapper.node 是一个标准的 Node.js native addon(.node 文件),底层是编译好的 C++ 共享库。逆向它主要有三种途径:

1. 运行时内省(最高效)

由于 wrapper.node 被加载到 Node.js 进程中,可以直接用 JavaScript 枚举所有导出:

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 表面:

json
{ "action": "__introspect", "params": {}, "echo": "1" }

返回所有类、所有方法、参数个数。

2. 二进制字符串提取

从 .node 文件中提取 ASCII 字符串,可以发现:

  • Proto 文件名im_msg_body.protogoogle/protobuf/descriptor.proto
  • 协议路由OidbSvcTrpcTcp.0x88d_0trpc.msg.msg_svc.MsgService.SsoC2CRecallMsg
  • 错误信息:包含方法名和参数提示
  • 日志字符串:包含内部函数名和数据格式
javascript
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 函数:

bash
pip install frida-tools
frida -p <QQ_PID> -l hook-script.js

WARNING

Frida 注入 Electron 进程可能被拒绝(我们的测试中 QQ 进程拒绝了 frida-agent 加载)。替代方案是使用 LD_PRELOAD 或在进程启动时注入。

已知的 API 表面

通过上述方法,我们从 QQ 3.2.27 的 wrapper.node 中提取到:

类别数量
导出类85
实例方法2,318+
Session Services48
OidbSvc 协议命令159
trpc 路由133
.proto 文件引用164

详细列表见 NTQQ API 总览Session Services协议路由

推断参数类型的技巧

native 方法的 .length 属性通常返回 0,无法直接获取参数信息。但可以通过以下方式推断:

1. 错误消息

传入错误参数时,有时会得到有用的错误信息:

javascript
try {
  service.someMethod("wrong");
} catch (e) {
  console.log(e.message);
  // → "expected object at argument 0"
}

2. 观察 Listener 回调数据

调用某个方法后,观察对应 Listener 收到的回调数据,可以反推该方法触发了什么操作:

javascript
// 调用 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 逆向方法:

  1. 捕获序列化数据:在 Listener 回调中拦截 raw buffer,使用 protobuf-inspectorblackboxprotobuf 解码
  2. 符号表分析:使用 nm -D wrapper.node 查找 protobuf 相关的符号
  3. 描述符提取:protobuf 的 FileDescriptorProto 可能嵌入在二进制中,可以用工具提取
bash
# 查找 protobuf 相关导出符号
nm -D /opt/QQ/resources/app/wrapper.node | grep -i proto

Released under the Apache 2.0 License.