JS `Node.js` `Native Addons` (`C++`) `Reverse Engineering`

各位观众,早上好/下午好/晚上好! 今天咱们来聊点刺激的:Node.js Native Addons 的逆向工程。 这玩意儿,说白了,就是用 C++ 给 Node.js 写插件。 听起来就很硬核是不是? 别怕,今天咱们尽量把它掰开了揉碎了讲,争取让大家听完之后,下次遇到这种玩意儿,不至于两眼一抹黑。

一、 啥是 Node.js Native Addons?

先来解决一下“我是谁?我从哪儿来?我要到哪儿去?” 的哲学问题。

Node.js 是个好东西,JavaScript 写起来也很舒服。 但是,JavaScript 有时候力不从心,比如:

  • 性能瓶颈: 某些计算密集型任务,JS 跑起来慢如蜗牛。
  • 硬件访问: 直接操作硬件? JS 比较困难。
  • 现有 C/C++ 库: 已经有现成的 C/C++ 库,不想用 JS 重写一遍。

这时候,Native Addons 就派上用场了。 它可以让你用 C++ 写代码,然后像 JS 模块一样在 Node.js 中使用。 就像给 Node.js 插上了一双翅膀,一下子就变得高大上了。

二、 逆向工程? 为什么要逆向?

“正向”开发我们都懂,那“逆向”又是啥?

简单来说,逆向工程就是把一个已经编译好的东西,反过来分析它的原理和实现。 就像考古学家挖坟一样,一层一层地剥开,看看里面到底藏了什么宝贝。

为什么要逆向 Native Addons 呢? 原因有很多:

  • 学习: 想知道别人是怎么用 C++ 写出高性能的 Node.js 模块的。
  • 安全: 想看看这个模块有没有啥安全漏洞,会不会偷偷摸摸干坏事。
  • 兼容: 想让一个旧的 Native Addon 在新的 Node.js 版本上跑起来。
  • 破解: (此处省略一万字)。

总之,逆向工程是一项很有用的技能,可以让你更深入地了解软件的本质。

三、 逆向前的准备工作

工欲善其事,必先利其器。 在开始逆向之前,我们需要准备一些工具:

工具名称 作用
Node.js 运行和调试 Addon 的环境。
GDB Linux 下强大的调试器,可以用来单步调试 C++ 代码。
LLDB macOS 下的调试器,和 GDB 类似。
IDA Pro/Ghidra 反汇编器和反编译器,可以将二进制代码转换成汇编代码或者 C 代码,方便分析。
objdump 可以用来查看目标文件的各种信息,比如符号表、段信息等。
nm 用来列出一个目标文件的符号表。
ltrace/strace 跟踪进程的库函数调用和系统调用。
Node Inspector Node.js 的调试工具,可以用来调试 JavaScript 代码,也可以用来查看 Native Addon 的内存情况。

这些工具各有各的用处,我们需要根据具体情况选择合适的工具。 当然,熟练掌握这些工具需要时间,大家可以慢慢学习。

四、 Hello World 的逆向之旅

光说不练假把式。 咱们先从一个最简单的 Hello World 的 Native Addon 开始,来体验一下逆向的乐趣。

1. 编写 Hello World Addon

首先,创建一个 hello.cc 文件:

#include <node.h>

namespace demo {

using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::Object;
using v8::String;
using v8::Value;

void Method(const FunctionCallbackInfo<Value>& args) {
  Isolate* isolate = args.GetIsolate();
  args.GetReturnValue().Set(String::NewFromUtf8(isolate, "world").ToLocalChecked());
}

void Initialize(Local<Object> exports) {
  NODE_SET_METHOD(exports, "hello", Method);
}

NODE_MODULE(NODE_GYP_MODULE_NAME, Initialize)

}  // namespace demo

然后,创建一个 binding.gyp 文件:

{
  "targets": [
    {
      "target_name": "hello",
      "sources": [ "hello.cc" ]
    }
  ]
}

最后,使用 node-gyp 构建 Addon:

npm install -g node-gyp
node-gyp configure
node-gyp build

构建成功后,会在 build/Release 目录下生成一个 hello.node 文件。

2. 编写 JavaScript 代码调用 Addon

创建一个 index.js 文件:

const addon = require('./build/Release/hello');

console.log(addon.hello()); // Prints: 'world'

3. 逆向分析

现在,我们来逆向分析一下 hello.node 文件。

  • 查看符号表:

    使用 nm hello.node 命令可以查看 hello.node 的符号表。 我们可以看到 MethodInitialize 等函数的符号。

    $ nm ./build/Release/hello.node
    0000000000001000 T _ZN4demo6MethodEN2v825FunctionCallbackInfoINS1_5ValueEEE
    0000000000001100 T _ZN4demo10InitializeEN2v85LocalINS1_6ObjectEEE
                     U _ZN2v87Isolate11GetCurrentEv
                     U _ZN2v86String11NewFromUtf8EPNS_7IsolateEPKcNS_13NewStringTypeE
                     U _ZN2v85Value10ToStringEPNS_7LocalINS_6ContextEEE
                     U _ZN2v813ReturnValueScope3SetENS_5LocalINS_5ValueEEE
                     U _node_module_register
                     U _Unwind_Resume
                     U __cxa_allocate_exception
                     U __cxa_begin_catch
                     U __cxa_end_catch
                     U __cxa_free_exception
                     U __cxa_throw
                     U _malloc
                     U _free
  • 反汇编:

    使用 objdump -d hello.node 命令可以反汇编 hello.node 文件。 我们可以看到 MethodInitialize 等函数的汇编代码。

    (或者使用 IDA Pro / Ghidra,它们的反编译功能更强大,可以直接将汇编代码转换成 C 代码)

    0000000000001000 <_ZN4demo6MethodEN2v825FunctionCallbackInfoINS1_5ValueEEE>:
       1000:       55                      push   %rbp
       1001:       48 89 e5                mov    %rsp,%rbp
       1004:       48 83 ec 10             sub    $0x10,%rsp
       1008:       48 89 7d f8             mov    %rdi,-0x8(%rbp)
       100c:       48 8b 7d f8             mov    -0x8(%rbp),%rdi
       1010:       e8 00 00 00 00          callq  1015 <_ZN4demo6MethodEN2v825FunctionCallbackInfoINS1_5ValueEEE+0x15>
       1015:       48 8b 05 00 00 00 00    mov    0x0(%rip),%rax        # 101c <_ZN4demo6MethodEN2v825FunctionCallbackInfoINS1_5ValueEEE+0x1c>
       101c:       48 8b 00                mov    (%rax),%rax
       101f:       ba 05 00 00 00          mov    $0x5,%edx
       1024:       be 00 00 00 00          mov    $0x0,%esi
       1029:       48 8d 3d 00 00 00 00    lea    0x0(%rip),%rdi        # 1030 <_ZN4demo6MethodEN2v825FunctionCallbackInfoINS1_5ValueEEE+0x30>
       1030:       e8 00 00 00 00          callq  1035 <_ZN4demo6MethodEN2v825FunctionCallbackInfoINS1_5ValueEEE+0x35>
       1035:       48 8b 7d f8             mov    -0x8(%rbp),%rdi
       1039:       e8 00 00 00 00          callq  103e <_ZN4demo6MethodEN2v825FunctionCallbackInfoINS1_5ValueEEE+0x3e>
       103e:       90                      nop
       103f:       c9                      leaveq
       1040:       c3                      retq

    通过分析汇编代码,我们可以看到 Method 函数的实现: 它调用了 v8 的 API 来创建一个字符串 "world",然后将其作为返回值返回。

  • 动态调试:

    使用 gdblldb 可以对 hello.node 进行动态调试。 我们可以设置断点,单步执行代码,查看变量的值。

    1. 启动 Node.js 进程,并使其加载 hello.node
    2. 使用 gdblldb attach 到 Node.js 进程。
    3. Method 函数的入口处设置断点。
    4. 运行 JavaScript 代码,触发 Method 函数的调用。
    5. 单步执行代码,查看变量的值。

    例如,使用 gdb 的步骤如下:

    gdb -p $(pidof node)
    (gdb) break hello.cc:10
    (gdb) continue

五、 进阶: 复杂 Addon 的逆向

Hello World 太简单了,没什么挑战性。 接下来,我们来挑战一个稍微复杂一点的 Addon。 比如,一个可以计算 MD5 值的 Addon。

1. 编写 MD5 Addon

创建一个 md5.cc 文件:

#include <node.h>
#include <node_buffer.h>
#include <openssl/md5.h>

namespace demo {

using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::Object;
using v8::String;
using v8::Value;
using v8::Exception;
using node::Buffer;

void MD5(const FunctionCallbackInfo<Value>& args) {
  Isolate* isolate = args.GetIsolate();

  // Check the number of arguments.
  if (args.Length() != 1) {
    // Throw an Error that is passed back to JavaScript
    isolate->ThrowException(Exception::TypeError(
        String::NewFromUtf8(isolate, "Wrong number of arguments").ToLocalChecked()));
    return;
  }

  // Check the argument type
  if (!Buffer::HasInstance(args[0])) {
    isolate->ThrowException(Exception::TypeError(
        String::NewFromUtf8(isolate, "Argument must be a buffer").ToLocalChecked()));
    return;
  }

  Local<Object> bufferObj = args[0]->ToObject(isolate).ToLocalChecked();
  char* bufferData = Buffer::Data(bufferObj);
  size_t bufferLength = Buffer::Length(bufferObj);

  unsigned char hash[MD5_DIGEST_LENGTH];
  MD5_CTX md5Context;
  MD5_Init(&md5Context);
  MD5_Update(&md5Context, bufferData, bufferLength);
  MD5_Final(hash, &md5Context);

  char hexString[2 * MD5_DIGEST_LENGTH + 1];
  for (int i = 0; i < MD5_DIGEST_LENGTH; i++) {
    sprintf(&hexString[i * 2], "%02x", hash[i]);
  }
  hexString[2 * MD5_DIGEST_LENGTH] = '';

  args.GetReturnValue().Set(String::NewFromUtf8(isolate, hexString).ToLocalChecked());
}

void Initialize(Local<Object> exports) {
  NODE_SET_METHOD(exports, "md5", MD5);
}

NODE_MODULE(NODE_GYP_MODULE_NAME, Initialize)

}  // namespace demo

然后,创建一个 binding.gyp 文件:

{
  "targets": [
    {
      "target_name": "md5",
      "sources": [ "md5.cc" ],
      "include_dirs": [
        "<!@(node -p "require('node-addon-api').include")"
      ],
      "libraries": [
        "-lssl",
        "-lcrypto"
      ]
    }
  ]
}

最后,使用 node-gyp 构建 Addon:

node-gyp configure
node-gyp build

2. 编写 JavaScript 代码调用 Addon

创建一个 index.js 文件:

const addon = require('./build/Release/md5');
const buffer = Buffer.from('hello world');

console.log(addon.md5(buffer)); // Prints: 'b10a8db164e0754105b7a99be72e3fe5'

3. 逆向分析

这个 Addon 比 Hello World 复杂多了,我们需要更高级的技巧。

  • 分析依赖库:

    md5.cc 中使用了 OpenSSL 库来计算 MD5 值。 我们需要了解 OpenSSL 的 API,才能更好地理解代码。

  • 查看字符串:

    使用 strings md5.node 命令可以查看 md5.node 文件中的字符串。 我们可以看到一些有用的信息,比如错误信息、函数名等。

    $ strings ./build/Release/md5.node
    Wrong number of arguments
    Argument must be a buffer
    md5
  • 反编译:

    使用 IDA Pro 或 Ghidra 可以将 MD5 函数反编译成 C 代码。 这样可以更方便地理解代码的逻辑。

    反编译后的代码可能如下所示 (简化版):

    const char* MD5(char* bufferData, size_t bufferLength) {
        unsigned char hash[MD5_DIGEST_LENGTH];
        MD5_CTX md5Context;
        MD5_Init(&md5Context);
        MD5_Update(&md5Context, bufferData, bufferLength);
        MD5_Final(hash, &md5Context);
    
        char hexString[2 * MD5_DIGEST_LENGTH + 1];
        for (int i = 0; i < MD5_DIGEST_LENGTH; i++) {
            sprintf(&hexString[i * 2], "%02x", hash[i]);
        }
        hexString[2 * MD5_DIGEST_LENGTH] = '';
    
        return hexString;
    }

    通过反编译的代码,我们可以清晰地看到 MD5 值的计算过程。

  • 动态跟踪:

    使用 ltracestrace 可以跟踪 md5.node 的库函数调用和系统调用。 我们可以看到 MD5_InitMD5_UpdateMD5_Final 等函数的调用。

    使用 ltrace 的命令如下:

    ltrace -p $(pidof node)

    通过动态跟踪,我们可以了解 Addon 内部的运作机制。

六、 常见问题与技巧

在逆向 Native Addons 的过程中,我们可能会遇到各种各样的问题。 这里总结一些常见的问题和技巧:

  • 符号表被 stripped:

    有些 Addon 在编译时会去掉符号表,这会给逆向带来很大的困难。 可以使用一些工具来尝试恢复符号表,比如 unstrip

  • 代码被混淆:

    有些 Addon 会使用代码混淆技术来增加逆向的难度。 可以使用一些反混淆工具来尝试还原代码。

  • 动态加载:

    有些 Addon 会动态加载其他的库,这会增加逆向的复杂度。 可以使用 ltracestrace 来跟踪动态加载的过程。

  • 调试信息:

    在编译 Addon 时,可以添加调试信息。 这会给逆向带来很大的帮助。 可以使用 -g 选项来添加调试信息。

  • 多平台兼容性:

    Native Addons 需要针对不同的平台进行编译。 逆向时需要注意平台的差异。

七、 总结

Node.js Native Addons 的逆向工程是一项充满挑战性的任务。 需要掌握一定的 C++ 知识、汇编知识和调试技巧。

但是,只要我们掌握了正确的方法和工具,就可以揭开 Native Addons 的神秘面纱,了解其内部的运作机制。

希望今天的讲座能对大家有所帮助。 谢谢大家!

补充说明:

  • 实际的逆向工程可能会更加复杂,需要根据具体情况进行分析。
  • 逆向工程可能涉及到法律问题,请务必遵守相关法律法规。
  • 本文只是一个入门教程,更深入的学习需要大家自己去探索。

祝大家逆向愉快!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注