CModule
Android逆向-Frida-CModule
入门教程
CModule API 允许我们传递一串 C 代码,并在内存中将其编译为机器代码。但需要注意的是,该功能是在 tinycc 下编译的,因此有一定的局限性。
CModule 对于实现需要以最高性能模式运行的函数非常有用。此外,CModule 还可用于实现 Interceptor 和 Stalker 的热回调,以提高性能或方便与 C 对象和指针的交互。
CModule 语法:
1 | new CModule(source, [, symbols]) |
Source 是包含 C 代码的字符串,symbols 是一个对象,可用于指定其他符号名称及其 NativePointer 值。
建议定义
1 | void init(void) |
作为初始化和内存清理方法。当我们要对 CModule 对象进行 GC 时,如果不想在脚本卸载时将其销毁,我们可以使用 CModule 对象的 .dispose()
方法。
1 | const openImpl = Module.getExportByName(null, 'open'); |
在本示例中,我们使用 CModule 来检测 open() 函数。我们用 C 代码替换 JavaScript 回调。
我们需要包含 frida-gum 库和标准库。void onEnter(GumInvocationContext *ic)
是一种方法,Gum能识别并向我们提供 InvocationContext
(函数被调用但尚未执行时的可用信息)。
有了这个 InvocationContext,我们就可以调用 gum_invocation_context_get_ngth_argument(ic,N)
(本例中 N 为 0)来获取第一个参数。然后,我们可以使用 <stdio.h> 的 printf 函数将该值打印到屏幕上。
不过,这有悖于在 JavaScript 中编写仪表代码的初衷,因此在真正需要性能或执行更复杂的任务时才使用它。
CModule: 实际用例(onEnter/onLeave间传参)
在本例中,我们将使用 UNIX 库 <sys/time.h>,其结构与我们之前看到的相同(timeval)。
在本例中,我们的目标是能够轻松读取 timeval 结构,但如前所述,我们无法访问 tinycc 之外的库,但可以将 CModule 作为参数 {toolchain: "external"}
作为参数传递给 CModule,这样它就能使用系统库,但需要注意的是,截至本文撰写之时,只有 MacOS 和(部分)Linux 系统支持此功能–我在 MacOs 11.1 和 Debian 10 下进行了测试。
这是一个 “隐藏 “的参数(因为没有太多的文档,你必须阅读测试用例才能知道它的存在),你可以使用它,而且确实非常有用:
1 | new CModule(`c_code_goes_here`, symbols, {toolchain: 'external|internal|any'}); |
好了,我们有了与 SYSCALL 结构部分相同的程序。我们将使用它作为测试此功能的基础程序。
我们想通过这个示例实现: - 复制 onEnter 行为 - 在回调中保存调用状态(即共享参数、线程状态等) - 在 onLeave 回调中打印 tv_secs 结构的第一个参数。- 注意:本例中使用的 void * arg 并不推荐,而应使用 gpointer,但我认为这是更常见的情况。
1 |
|
注意:这种编码方式与标准C程序有些不同,因为我们要使用回调进行操作。我们使用带有两个反斜线的 \n,因为该字符串位于 JavaScript 多行字符串中。
首先,我们包含 <gum/guminterceptor.h>,这样就能访问 onEnter 和 onLeave 回调。
我们还需要在回调之间存储 InvocationState,因此我们创建了一个名为 IcState 的结构,其中存储了一个成员:
1 | void *arg; |
现在我们有了 C 语言的 onEnter 回调:
1 | void onEnter(GumInvocationContext) |
有了这个上下文,我们就可以使用 gum API 的辅助函数,如 GUM_IC_GET_INVOCATION_DATA
(我们将使用它来初始化结构体)和 gum_invocation_context_get_nth_argument
(获取参数)。
我们将第一个参数存储在 IcState 结构中:
1 | is->arg = gum_invocation_context_get_nth_argument(ic, 0); |
然后我们就可以在 onLeave 回调中使用它了,但首先我们需要对参数进行转换,以便使用该结构:
1 | struct timeval * t = (struct timeval*)is->arg; |
然后我们就可以使用 t.tv_secs
和 t.tv_usecs
访问timeval
结构参数了。
这应该就是预期的输出结果:
1 | [Local::a.out]-> %resume |
CModule: 读取返回值
可以从 CModule 中读取嵌入函数的返回值。现在我们来看一个简单的示例:
1 | void |
本例假定返回值为整数,但有一种更简洁的方法可以解决这个问题:
1 | void |
看到 GPOINTER_TO_INT
?gum_invocation_context_get_return_value
返回的不是返回值本身,而是一个指针(这就是为什么我们在 JS 中工作时总是使用 NativePointers)–我们需要将其转换为整数,无论是使用 (int) 还是使用 GPOINTER_TO_INT API。
CModule 与 JavaScript 代理的性能比较
为了测试 CModule 与 JavaScript 中的相同脚本相比性能如何,让我们使用下面的 C 程序:
1 |
|
这个程序只是从 for 循环中获取一个数字,然后计算它的平方根。在未Hook的情况下,执行该程序需要 0.002 秒。
现在,为了测试Frida对性能的影响,我们使用了下面的脚本:
1 | var localSqrt_offset = ptr(0x1581) |
此脚本只需调用 double localSqrtPtr(double)
函数,并在屏幕上打印返回值。使用 QuickJS 运行时执行此脚本时,需要 5.8 秒才能完成。使用 JavaScript 的 V8 作为运行时,则需要 5.321 秒。另一方面,让我们看看 CModule 用于相同目的时的情况:
1 | Interceptor.attach(localSqrtPtr, new CModule(` |
如果在脚本中使用 CModule 执行并打印所有参数,则需要 1.3 秒才能完成。这种差异非常明显,因此在编写注入脚本时,建议首先用 JavaScript 编写所有逻辑,然后看看注入目标的性能如何。如果性能足以完成任务,则无需进一步优化,但如果需要更高的性能,CModule 则为优化任务提供了一个新的天地。
需要注意的是,虚拟机退出时的性能降低是针对每个事务的。这意味着,每当 onEnter 或 onLeave 回调结束,注入脚本试图访问 CModule 中的变量时,都会造成性能降低,从而影响 CModule 的性能。在前面显示的示例中,所有功能都包含在 CModule 中(它不向 JavaScript 返回值),因此性能提升非常明显。
CModule: 在 JS 和 C 之间共享状态
在注入一些二进制文件时,我们最终可能会遇到一些热点调用,这意味着这些调用每秒被调用的次数过多,并因使用 Frida 的 JS 端注入而付出了高昂的性能代价。Frida 允许我们使用 CModule 注入代码,并且只在需要时访问被注入函数的所需值,从而降低对被注入二进制性能的影响,同时还允许用户保留其 JS 注入代码。
为此,我们将使用之前重复调用 sqrt 函数的程序,并与我们的 JS 代码共享返回值。为了应对这种情况,JS 代码的第一步是分配一个缓冲区与 CModule 共享:
1 | const sqrtReturnPtr = Memory.alloc(4); |
这将创建一个与 CModule 代码共享的 NativePointer。这样,我们的 CModule 代码看起来就像这样了:
1 | const myCm = new CModule(` |
CModule 构造函数的第二个参数允许通过以下语法传递符号:
1 | new CModule(/c code/, { symbol_1, symbol_2, ...<n> }); |
C 代码声明了一个外部变量,该变量在我们的 JS 注入代码和 C 代码之间共享。我们的 JS 代码将该变量视为 NativePointer,只有在访问该变量时才会付出性能代价。为了测试这一点,我们将增加 for 循环的大小,以确保应用程序需要更长时间才能完成,并调用 setTimeout 在 2 秒后获取当前返回值:
1 | Interceptor.attach(localSqrtPtr, cm); |
sqrtReturnPtr 是 CModule 和 JS 代码之间的共享指针,因此要获取真实值,需要调用 .readDouble
API 来获取值。其他数据类型也是如此:int、char[]、float… 最后,在注入上述应用程序时,将得到以下输出结果:
1 | [Local::a.out ]-> sqrt value after 2 seconds: 8915179 |
共享状态也可以通过 onEnter 回调或使用与 CModule 代码交互的 NativeFunctions 来实现。请谨慎使用!
在两个 CModule 对象之间共享状态(通过CModule的第二个的参数)
前面的示例展示了如何在我们的 JS 代码和 C 代码之间共享状态/变量,但如果有两个不同的函数要注入,并且需要共享它们的状态,该怎么办呢?这可以通过使用 CModule 构造函数的第二个参数来实现,如前所述。在本例中,重复使用了与上一节相同的代码。不过,这次增加了一个额外的函数:
1 | const cmFunction = new CModule(` |
这段代码暴露了 void printCurrentValue()
函数,它可以打印 sqrtReturnPtr 的当前共享值。但是,要从我们的 JS 代码中调用该函数,需要使用 NativeFunction:
1 | const printCurrentValue = new NativeFunction(cmFunction.printCurrentValue, 'int', []); |
cmFunction.printCurrentValue
返回函数指针,而 NativeFunction 构造函数复制了它的定义,从 JS 返回一个可调用函数。这样,上述调用.setTimeout
的代码就可以替换为对 CModule 函数的调用:
1 | setTimeout(() => { |
从 C 代码发出通知
使用 CModule 时的另一个用例可能是让 C 代码独立工作,只在需要时向 JS 报告反馈。这可以通过在创建 CModule 时传递一个 NativeCallback 来实现,然后在 C 代码中调用该函数,从而在 JS 端触发 NativeCallback。
为了说明这个示例,我们将重复使用之前的平方根示例,目的是只在平方根运算 modulo 10000 的结果为 0 时通知 JS 代码。第一步是在 CModule 代码中添加通知 JS 代码的函数的外部声明:
1 | extern void notify_from_c(const double * value); |
现在可以从 CModule 端调用该函数 notify_from_c(&value);
。下一步是在 CModule 符号的 JS 端添加一个回调函数,用于接收 notify_from_c 函数的值并对其采取行动。具体方法是在 CModule 构造函数中扩展 symbols 参数:
1 | const cm = new CModule(`/* code goes here*/`, { |
有了这个设置,只要 10000 的平方根值模数为零,我们 CModule 中的 onLeave 回调函数就会调用 notify_from_c 函数:
1 | const cm = new CModule(` |
在针对目标程序执行该脚本时,我们会得到以下输出结果:
1 | [Local::a.out ]-> notification from C code, value: 0 |
只要 C 代码能独立完成大部分工作,只需要将数据发送回 JS 或对接收到的数据采取行动,那么使用这些通知就很有意思。
CModule 模板
到目前为止,我们只暴露了我在示例中展示的 GUM 应用程序接口,这是因为在编写 CModule 时,还需要根据源代码检查类型和函数,不过自 frida 14.2.12 起,可以为 CModule 生成包含最常用方法的模板:
1 | frida-create -t cmodule|agent |
agent:创建 TypeScript 代理的模板。
cmodule: 创建 CModule 的模板,其中包含所有内置头文件,因此可以使用外部工具链。这样就可以在你选择的编辑器中支持代码自动完成。
请注意,该命令会在当前工作目录下创建模板!
一旦执行了 frida-create -t cmodule
,你就会得到这样的模板:
1 | $ ls |
而 .c 文件应该是这样的:
1 |
|
现在,我们可以将基本方法包含在内,也可以根据需要对其进行修改,还可以访问 GumInvocationContext
成员并进行类型检查。
要构建 CModule,需要执行以下命令:
1 | $ meson build && ninja -C build |
生成 cmodule.so 文件后,可通过以下方式将其注入目标进程:
1 | frida -C cmodule.so <PID |
参考
https://learnfrida.info/advanced_usage/
- 本文作者: Taardis
- 本文链接: https://taardisaa.github.io/2023/10/09/Android逆向-Frida-CModule/
- 版权声明: 本博客所有文章除特别声明外,均采用 Apache License 2.0 许可协议。转载请注明出处!