记录一次有趣的 Frida Hook 调试

Created by: N1rv0us Zhang
Created time: May 10, 2024 11:48 PM
Tags: android, debugger, essays

起初

最近需要调试一个比较特殊的 app 子进程。虽然我一直感觉frida对Android app的子进程调试有些缺陷,但是由于这次需要调试 Native 的代码,我还是习惯的把直接把frida attach到进程上;瞬间调试的目标进程崩溃了 …

哦?什么原因呢?被反调干掉了?还是有其他问题导致程序崩溃呢 ….

通常遇到 Frida 挂不上去的情况,我都会选择另辟蹊径,比如在主进程上找找进程间通信把问题解决了,不会正面硬刚。但这次要求必须调试这个该死的进程,于是,很多在平常看 blog 学到的技术第一次组合应用到了实践中。非常值得记录一番。

失败的尝试

虽然是失败了,但是也有很多不错的内容喔 ~

我先是想到可能是 frida-server 触发了一些异常。因为这算是最傻瓜式的 frida 调试方法了,随便一个最基本的反调试都会识别,同时 frida-server 的超高可用性也会向调试目标进程加入一堆不太必要的操作,这些操作也可能会让一个脆弱的子进程崩溃;

那么,我们尝试简化下 frida-server 注入进程的逻辑;改为直接把 frida-gadget 注入到目标进程里面后连接 PC 上的 frida-client

操作方法可以选择重打包目标 app 就和在 nonRoot 环境中使用 frida 的操作一致。我这次选择了另一种方案,直接通过一个小工具把 frida-gadget 注入到目标进程的内存中,免去重打包的操作:

我在平常乱翻 blog 的时候发现的一个不错的工具,用 Rust 编写的;无论是内存注入的思路还是代码实现都十分的优雅。Nice

https://github.com/erfur/linjector-rs

第一次使用 frida-gadget 遇到了个小坑,其使用和 frida-server 还是有些区别的,在 Frida 官方文档中也有使用说明:

Gadget

frida-gadget 的运行需要两个文件,一个从官网下载的对应架构的 frida-gadget.so,另一个为配置文件:

1
2
3
4
lib
└── arm64-v8a
├── libgadget.config.so
├── libgadget.so

内存注入时只需注入 libgadget.so 即可,它会在当前目录下加载对应的config;因此 frida-gadget.so 与其config的名称必须是按照格式匹配的;

config 按照如下的格式编写, 参数解析看官方文档即可:

1
2
3
4
5
6
7
8
9
{
"interaction": {
"type": "listen",
"address": "127.0.0.1",
"port": 27042,
"on_port_conflict": "fail",
"on_load": "wait"
}
}

然而,一顿操作把 frida-gadget 注入到目标进程之后,进程还是崩溃了;

Shit 怀疑人生开始了,我到底注入成功了没有?难不成真的遇到了顶级反 Frida ?

什么触发了 Crash

到这里我还是没搞明白,是什么导致了挂在 frida 的过程中必然 crash。但实际上无非两种大的思路:

  1. 被反调试之类的安全机制发现了,直接 kill 掉了进程;
  2. frida 自己把进程搞崩了;

于是,我向大哥咨询,大哥给了我一个很重要的 Hint :

这个进程不允许有 I/O 操作,只要有 I/O 类型的操作,就会马上 crash !

有了这个 hint 之后,我补充了一些测试,发现这个进程实际上是没有反调试的。崩溃的原因就是 frida 的运行导致了进程 crash .

那么反思一下,frida的注入过程中会触发 I/O 类型的操作么?答案是肯定的:

  • frida-server 也需要把 frida-gadget 注入到目标进程,虽然不记得具体流程,但也很容易触发到crash;
  • 直接注入 frida-gadget 的尝试,其功能的实现必须要读取当前目录下的文件,必然会有 I/O

此时,我需要一个不会有任何 I/O 行为的 Frida.

柳暗花明

那么,我要使用一个最小化运行 frida 的方式 —— 打包一个 frida-gum 到一个 Native so 文件中,然后使用前文的 linjector-rs 注入到目标进程中;

这里同样是使用以前玩过的一个不错的开源项目 FGum :

https://github.com/SeeFlowerX/FGum

直接将需要执行的 frida-script 也编入Native so中,这样 frida-script 会一起直接加载到内存中,执行结果重定向到 logcat 中,就避免了 I/O 操作;

当然,需要在 .init_array 或者 constructor 中加载 frida-script,以保证注入的脚本能正常跑起来。

虽然这里写的很轻松,但是,实现代码的时候还是提笔忘字Google了半天,所以呀,趁这个机会在强化下记忆吧 …

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#include <stdio.h>
#include <stdlib.h>

void preinit(int argc, char **argv, char **envp) {
printf("%s\n", __FUNCTION__);
}

void init(int argc, char **argv, char **envp) {
printf("%s\n", __FUNCTION__);
}

void fini() {
printf("%s\n", __FUNCTION__);
}

__attribute__((section(".init_array"))) typeof(init) *__init = init;
__attribute__((section(".preinit_array"))) typeof(preinit) *__preinit = preinit;
__attribute__((section(".fini_array"))) typeof(fini) *__fini = fini;

void __attribute__ ((constructor)) constructor() {
printf("%s\n", __FUNCTION__);
}

void __attribute__ ((constructor)) constructor_2() {
printf("%s\n", __FUNCTION__);
}
void __attribute__ ((destructor)) destructor() {
printf("%s\n", __FUNCTION__);
}

void __attribute__ ((destructor)) destructor_2() {
printf("%s\n", __FUNCTION__);
}
void my_atexit() {
printf("%s\n", __FUNCTION__);
}

void my_atexit2() {
printf("%s\n", __FUNCTION__);
}

int main() {
atexit(my_atexit);
atexit(my_atexit2);
printf("%s\n",__FUNCTION__);
}

函数间触发顺序为:

1
2
3
4
5
6
7
8
9
10
11
# ./test
preinit
init
constructor_2
constructor
main
my_atexit2
my_atexit
destructor
destructor_2
fini

测试中发现,在 .init_array session 中触发函数还是会莫名的崩溃,于是使用了 constructor ;

  • Final Code Below :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void my_init_func(void) __attribute__((constructor));

void my_init_func(void) {
LOGD("[N1rv0us] frida gum so loaded success !");

const char* js = "console.log('easy test for frida script ...')";

int status = gumjsHook(js);
LOGD("[N1rv0us] gumjs hook status ==> %d", status);
}

int hookFunc(const char *scriptpath) {
LOGD ("[*] gumjsHook()");
gum_init_embedded();
backend = gum_script_backend_obtain_qjs();
// char *js = readfile(scriptpath);
// if (!js) {
// return 1;
// }
const char *js = scriptpath;

script = gum_script_backend_create_sync(backend, "example", js, NULL, cancellable, &error);
...
}

编译完成之后,把 Native so 用 linjector-rs 注入到目标进程。正常输出了 frida-script 打印的日志;

Ok, 大功告成,搞定,下班儿 ~