JS `Deno` `FFI` (Foreign Function Interface) `Native Call` 追踪与逆向

各位观众老爷,晚上好!我是你们的老朋友,今天咱们不聊风花雪月,来点硬核的,聊聊Deno的FFI(Foreign Function Interface)原生调用,以及怎么追踪和逆向这玩意儿。准备好爆米花,咱们开车了!

第一章:Deno FFI 扫盲班:你以为的“魔法”,其实是科学

啥是FFI?简单来说,就是让你的JavaScript代码能直接调用用其他语言写的代码,比如C、C++、Rust等等。Deno的FFI就像一扇传送门,让你的JS代码瞬间拥有了C语言级别的性能,想想是不是有点小激动?

为什么要用FFI?JS很强大,但有些时候还是力不从心。比如:

  • 性能敏感型任务: 图像处理、音视频编解码、科学计算,这些对性能要求极高的任务,交给C/C++等底层语言更靠谱。
  • 访问底层系统资源: JS的沙箱环境限制了它直接访问操作系统底层资源,而FFI可以让你突破这个限制。
  • 重用现有代码库: 很多成熟的C/C++库已经存在,用FFI可以避免重复造轮子。

Deno FFI的基本流程是这样的:

  1. 定义外部函数接口: 告诉Deno你要调用的C函数的名称、参数类型和返回值类型。
  2. 加载动态链接库: 加载包含C函数的动态链接库(.so、.dll、.dylib)。
  3. 调用外部函数: 在JS代码中像调用普通函数一样调用C函数。

代码示例:Hello, World! FFI版

首先,写一个简单的C代码:

// hello.c
#include <stdio.h>

__attribute__((visibility("default")))
void hello(const char* name) {
  printf("Hello, %s from C!n", name);
}

编译成动态链接库:

gcc -shared -o libhello.so hello.c -fPIC

然后,是Deno代码:

// hello.ts
const lib = Deno.dlopen("./libhello.so", {
  "hello": { parameters: ["pointer"], result: "void" },
});

const name = "Deno";
const namePtr = new TextEncoder().encode(name + ""); // C字符串需要null结尾
lib.symbols.hello(namePtr);

lib.close(); // 释放资源

运行:

deno run --allow-read --allow-ffi hello.ts

你会看到控制台输出了 Hello, Deno from C!。恭喜你,迈出了Deno FFI的第一步!

第二章:追踪 Deno FFI 的足迹:像侦探一样寻找线索

现在,我们已经知道怎么用FFI了。但是,如果C代码出了问题,或者你想深入了解Deno FFI的底层机制,该怎么办呢?这时候就需要追踪了。

1. 日志大法好:console.log() 是你的好朋友

最简单粗暴的方法,就是在C代码和Deno代码中加入大量的console.log()printf()。虽然原始,但非常有效。

比如,可以在C代码中打印参数值:

// hello.c
#include <stdio.h>

__attribute__((visibility("default")))
void hello(const char* name) {
  printf("C: Received name = %sn", name); // 打印参数值
  printf("Hello, %s from C!n", name);
}

2. Deno 内置的调试工具:Deno.inspect()--inspect-brk

Deno提供了强大的调试工具。你可以使用Deno.inspect()在代码中设置断点,或者使用--inspect-brk参数启动Deno,然后在Chrome DevTools中进行调试。

// hello.ts
const lib = Deno.dlopen("./libhello.so", {
  "hello": { parameters: ["pointer"], result: "void" },
});

const name = "Deno";
const namePtr = new TextEncoder().encode(name + "");
Deno.inspect(namePtr); // 设置断点,查看namePtr的值
lib.symbols.hello(namePtr);

lib.close();

运行:

deno run --allow-read --allow-ffi --inspect-brk hello.ts

打开Chrome DevTools,连接到Deno的调试端口,你就可以单步调试Deno代码,查看变量的值,甚至可以修改变量的值。

3. 系统调用追踪:stracedtruss

strace (Linux) 和 dtruss (macOS) 是强大的系统调用追踪工具。它们可以让你看到Deno进程在执行过程中都调用了哪些系统调用,比如open()read()write()mmap()等等。这对于了解Deno FFI的底层行为非常有帮助。

strace -f -e trace=file,process deno run --allow-read --allow-ffi hello.ts

或者

dtruss -f deno run --allow-read --allow-ffi hello.ts

你会看到大量的系统调用信息,包括Deno加载动态链接库、分配内存、调用外部函数等等。

4. 使用 GDB 调试 C 代码

如果你需要调试C代码本身,可以使用GDB。首先,编译C代码时要包含调试信息:

gcc -shared -o libhello.so hello.c -fPIC -g

然后,启动Deno时,使用--debug参数:

deno run --allow-read --allow-ffi --debug hello.ts

接下来,用GDB附加到Deno进程:

gdb -p <Deno进程ID>

在GDB中,你可以设置断点、单步调试、查看变量的值等等。

break hello
run
next
print name

第三章:Deno FFI 逆向工程:解开黑盒子的秘密

逆向工程听起来很高大上,其实就是通过分析程序的行为和代码,来了解它的内部工作原理。对于Deno FFI来说,逆向工程可以帮助你:

  • 理解Deno FFI的底层实现细节。
  • 分析第三方Deno FFI库的安全性。
  • 发现潜在的漏洞。

1. 反汇编:把机器码变成汇编代码

反汇编是将机器码转换成汇编代码的过程。汇编代码比机器码更容易阅读和理解。你可以使用objdump (Linux) 或 otool (macOS) 来反汇编动态链接库。

objdump -d libhello.so

或者

otool -tv libhello.dylib

你会看到大量的汇编代码,包括函数的入口点、指令、寄存器操作等等。虽然汇编代码比较难懂,但它是了解程序底层行为的基础。

2. 静态分析:IDA Pro 和 Ghidra

IDA Pro 和 Ghidra 是强大的静态分析工具。它们可以自动分析程序的代码结构、函数调用关系、数据流等等。它们还提供了图形界面,方便你浏览和理解代码。

使用IDA Pro或Ghidra打开动态链接库,你可以:

  • 查看函数的控制流图。
  • 查找字符串和常量。
  • 反编译代码(将汇编代码转换成更高级的伪代码)。
  • 进行交叉引用分析。

3. 动态分析:GDB 和 LLDB

除了静态分析,还可以使用GDB和LLDB进行动态分析。动态分析是在程序运行过程中分析程序的行为。你可以设置断点、单步调试、查看内存的值等等。

动态分析可以帮助你:

  • 理解程序的运行流程。
  • 观察程序的变量值。
  • 发现程序中的错误。

4. Deno 源码:最终的真相

如果你想深入了解Deno FFI的底层实现,最好的方法就是阅读Deno的源码。Deno是用Rust编写的,你可以从Deno的GitHub仓库中找到FFI相关的代码。

Deno FFI的核心代码主要在deno_core crate中。你可以重点关注以下几个文件:

  • ext/ffi/lib.rs: 定义了FFI相关的API。
  • runtime/ops/ffi.rs: 实现了FFI相关的操作。

阅读源码可以帮助你理解Deno FFI的内部机制,包括如何加载动态链接库、如何传递参数、如何调用外部函数等等。

案例分析:逆向一个简单的Deno FFI库

假设我们有一个Deno FFI库,用于计算两个数的和:

// add.c
#include <stdio.h>

__attribute__((visibility("default")))
int add(int a, int b) {
  printf("C: a = %d, b = %dn", a, b);
  return a + b;
}

编译成动态链接库:

gcc -shared -o libadd.so add.c -fPIC

Deno代码:

// add.ts
const lib = Deno.dlopen("./libadd.so", {
  "add": { parameters: ["i32", "i32"], result: "i32" },
});

const a = 10;
const b = 20;
const result = lib.symbols.add(a, b);
console.log(`Result: ${result}`);

lib.close();

现在,我们来逆向这个库。

  1. 反汇编 libadd.so:
objdump -d libadd.so

通过反汇编代码,我们可以看到add函数的汇编代码,包括参数的传递、加法运算、返回值的设置等等。

  1. 使用 GDB 调试 add.ts:

启动Deno时使用--debug参数,然后用GDB附加到Deno进程。在add函数处设置断点,可以查看ab的值,以及返回值result的值。

  1. 分析Deno源码:

阅读Deno源码,可以了解Deno FFI如何将JS的i32类型转换成C的int类型,以及如何将C的int类型转换成JS的i32类型。

总结:Deno FFI 追踪与逆向的工具箱

工具名称 功能 使用场景
console.log()/printf() 打印日志 快速定位问题,查看变量值
Deno.inspect()/--inspect-brk Deno调试工具 单步调试Deno代码,查看变量值,修改变量值
strace/dtruss 系统调用追踪 了解Deno进程的底层行为,比如加载动态链接库、分配内存、调用外部函数等等
GDB/LLDB C代码调试 单步调试C代码,查看变量值,设置断点
objdump/otool 反汇编 将机器码转换成汇编代码,了解程序的底层实现
IDA Pro/Ghidra 静态分析 分析程序的代码结构、函数调用关系、数据流等等,反编译代码,进行交叉引用分析
Deno 源码 了解Deno FFI的底层实现 深入理解Deno FFI的内部机制,包括如何加载动态链接库、如何传递参数、如何调用外部函数等等

第四章:Deno FFI 的坑:小心脚下!

Deno FFI虽然强大,但也充满了坑。一不小心就会掉进去,爬都爬不出来。

  • 内存管理: C语言的内存管理是出了名的复杂。如果C代码中存在内存泄漏或野指针,会导致Deno进程崩溃。
  • 类型安全: Deno FFI的类型检查相对较弱。如果参数类型不匹配,会导致程序出错。
  • 安全性: Deno FFI允许JS代码直接调用C代码,这可能会引入安全风险。如果C代码存在漏洞,攻击者可以通过Deno FFI来攻击你的系统。
  • ABI 兼容性: 不同的操作系统、不同的编译器、不同的编译选项,都会影响C代码的ABI(Application Binary Interface)。如果ABI不兼容,会导致Deno FFI调用失败。

防止掉坑指南:

  • 使用内存安全的C库: 尽量使用经过严格测试的内存安全的C库,比如jemalloc、mimalloc等等。
  • 进行严格的类型检查: 在Deno代码中进行严格的类型检查,确保参数类型与C函数的参数类型匹配。
  • 使用沙箱环境: 将Deno FFI代码运行在沙箱环境中,限制其访问系统资源的权限。
  • 进行代码审查: 对Deno FFI代码进行严格的代码审查,发现潜在的漏洞。

总结:

Deno FFI是一把双刃剑。用好了,可以大大提高程序的性能和灵活性。用不好,可能会引入安全风险和稳定性问题。所以,在使用Deno FFI时,一定要小心谨慎,充分了解其原理和潜在的风险。

好了,今天的讲座就到这里。希望大家能够掌握Deno FFI的追踪与逆向技巧,成为Deno FFI的高手。记住,技术是无止境的,学习永不止步!下课!

发表回复

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