Frida 17.6.0在Android端的Zygote注入机制上进行了一次值得关注的重构。它展示了一种更加稳定的注入设计思路
传统的ptrace注入方案虽然在功能上强大,但在实际应用中常常面临稳定性挑战:在子进程中残留痕迹容易被检测、与其他工具的兼容性也时有冲突等。而新引入的“zymbiote”机制采用了完全不同的实现路径——通过外部内存操作和轻量级通信,在几乎不留下痕迹的情况下完成了进程监控。
理解这套机制不仅能帮助我们更好地使用新版工具,更重要的是,它为我们思考Android系统层级的动态分析技术提供了新的视角。本文将详细解析zymbiote的技术原理和实现细节
去年我在这篇文章中介绍了spawn模式注入so的实现原理:spawn模式注入so
它的核心思想是先向zygote进程注入一个mon.so,这样它运行在zygote进程,就可以轻而易举的实现对zygote进程中函数的hook,通过hook fork系列函数,在fork触发之后再安装对setArgV0的hook,在setArgV0函数触发时判断是否是目标app,从而确定是否dlopen需要注入的so,现在看来这套设计是有问题的,因为setArgV0函数是用于设置进程名的,在app启动时它会被稳定触发,所以完全可以跳过对fork函数的hook,直接hook setArgV0。其次就是zygote里驻留的so不是很好清理,以及selinux的问题处理的也很粗糙
当然,Frida之前的注入也有这些问题,server一启动,对libc的hook就安装了,这会导致很多app打开就闪退,我在校时吃饭需要用到完美校园这个app,经常付钱的时候才想起来刚用过frida,手机还没重启,就会很尴尬
可以看到之前的注入方式(开源的,公开的)大多都是通过ptrace注入zygote,其中ptrace负责实现远程读写+远程调用,再通过hook监控fork相关的函数实现的,而这次Frida更新,带来了一种新的思路
但是核心逻辑是不变的,仍然需要远程读写+hook,但是这里它两个模块都做了优化
远程读写:
当前frida采用直接读写/proc/<zygote>/mem的形式进行远程读写(这个操作挂圈好像用的很多)。在我的理解中/proc/[pid]/mem是通过文件系统接口暴露的进程虚拟内存空间的直接读写通道,它像一个窗户,窗户内部是进程的完整虚拟地址空间,那么要如何精准的在这块空间里找到我们想要的东西呢?那就需要先去读/proc/[pid]/maps,map翻译过来是地图的意思,事实也的确如此,maps就像是这块空间的一张地图,通过它就可以定位到我们想要的位置
hook
再次回顾一下之前,我们的安装hook是通过运行在zygote进程内部的代码 修改目标函数开始处的指令为跳转指令,从而使运行到该函数时自动跳转到我们自己的hook函数的
frida选择的是setArgV0函数作为触发点,这个函数之前介绍过的,它会在app启动时稳定被调用
值得注意的是,这是一个JNI函数,对于JNI函数,它在java层就会有一个对应的java函数,那我们直接hook对应的java函数就好了。这个就很熟悉了,直接修改对应java函数的ArtMethod的entry_point即可,完全不需要inline hook了,frida就是这样做的,当然对于JNI函数,还有别的hook方法,但是frida在这个注入场景,修改entry_point是最简洁的做法
那么hook如何安装呢?回顾刚刚的hook方案,核心是修改目标函数的entry_point,而目标函数是系统库里的函数,那他在开机之后,app启动之前,就存在zygote进程的内存里面了。那么我们只需要在zygote的内存里找到目标函数的entry_point字段,然后把它的值修改到我们的hook函数的地址就行了,那么现在就需要解决两个问题
Frida 采用了一个巧妙的方法来定位 android_os_Process_setArgV0 对应的 ArtMethod 的 entry_point:
首先,通过解析 libandroid_runtime.so 这个ELF 文件,找到 JNI 函数的符号地址:
这里得到的 set_argv0_address 是函数在内存中的实际地址
Frida 通过读取 zygote 进程的堆内存,搜索包含这个地址的位置:
这样就拿到了art_method_slot的地址,即存储了指向android_os_Process_setArgV0地址的字段的地址,接下来通过上面提到的,读写/proc/<zygote>/mem来实现修改该字段,但在修改之前,需要备份一下原始指向的函数地址,后面注入成功/失败之后需要unhook,会用到原本的值
那么应该把art_method_slot里存储的值改成什么呢?这就该解决第二个问题了
传统的做法是通过 ptrace 注入一个 .so 文件到 zygote 进程,hook 函数就在这个 .so 里。但 Frida 的 zymbiote 机制采用了不同的思路:不注入 .so,而是注入一小段精心设计的机器码(payload)
这个payload该如何设计我们放到后面再来讨论,目前先解决该把它放在哪的问题
payload 应该放在哪里?这个位置需要满足几个条件:
Frida 选择了 libstagefright.so 的最后一页:
选择libstagefright.so的原因很简单,因为它的最后一页往往有未使用的空间,足够我们写入payload
先看一下frida的zymbiote吧,源代码 zymbiote.c
这里定义了一些libc里的函数,运行时会被用到。但是由于这段代码并不是像linker加载so那样被加载进内存的,所以它缺少重定位步骤,后续需要自己手动重定位
这个就是android_os_Process_setArgV0的hook函数,流程总结下来就是 恢复 hook → 调用原始函数 → 连接 Server → 发送信息 → 等待确认 → 暂停进程
这个是暂停函数,用于发送SIGSTOP暂停进程。其他的辅助函数就不介绍了,感兴趣的自己去看
前面提到,_FridaApi 结构体里定义了一些 libc 函数指针,但这些指针在编译时都是空的。因为 payload 是纯机器码,不会经过动态链接器的重定位过程,所以需要 Frida Server 手动填充这些函数地址。
重定位过程:
填充完数据后,就可以注入了:
这里直接指向payload是因为链接脚本将hook函数所在的段放在了payload的起始位置(偏移 0)。通过在函数定义时指定section,再配合链接脚本控制段的顺序,就能确保hook函数位于payload开头。
至此就完成了payload的注入与android_os_Process_setArgV0函数的hook
做完上面的工作,setArgV0就被替换了。在app启动之后,setArgV0被调用,然后走到frida_zymbiote_replacement_set_argv0里,执行里面的逻辑。这个函数上面介绍过了,Frida并没有选择直接在这里进行注入,而是仅仅获取了这个“时机”,拿到了这个时机之后,就启动Socket与外部通信,在外部通过ptrace把frida-gadget.so注入进目标进程,注入成功后再清理掉zygote进程里libandroid_runtime.so最后一页的数据,以此恢复对zygote的污染,从而避免了之前那种一启动server,一堆加壳软件就打不开的尴尬问题
经过之前的学习,可以发现这种注入方式和之前的相比简洁了很多,核心代码也没有多少,而且只做注入的话,还有优化的空间,所以直接开抄,但是明天要上班了,我抄不动了
void android_os_Process_setArgV0(JNIEnv* env, jobject clazz, jstring name)
void android_os_Process_setArgV0(JNIEnv* env, jobject clazz, jstring name)
struct _FridaApi
{
char name[64];
void ** art_method_slot;
void (* original_set_argv0)();
int (* socket) (...);
int (* connect) (...);
int * (* __errno) (void);
pid_t (* getpid) (void);
pid_t (* getppid) (void);
ssize_t (* sendmsg) (...);
ssize_t (* recv) (...);
int (* close) (int fd);
int (* raise) (int sig);
};
static volatile const FridaApi frida = {
.name = "/frida-zymbiote-00000000000000000000000000000000",
};
struct _FridaApi
{
char name[64];
void ** art_method_slot;
void (* original_set_argv0)();
int (* socket) (...);
int (* connect) (...);
int * (* __errno) (void);
pid_t (* getpid) (void);
pid_t (* getppid) (void);
ssize_t (* sendmsg) (...);
ssize_t (* recv) (...);
int (* close) (int fd);
int (* raise) (int sig);
};
static volatile const FridaApi frida = {
.name = "/frida-zymbiote-00000000000000000000000000000000",
};
__attribute__ ((section (".text.entrypoint")))
__attribute__ ((visibility ("default")))
int
frida_zymbiote_replacement_set_argv0 (JNIEnv * env, jobject clazz, jstring name)
{
bool success = false;
int fd = -1;
struct sockaddr_un addr;
socklen_t addrlen;
unsigned int name_len;
*frida.art_method_slot = frida.original_set_argv0;
frida.original_set_argv0 (env, clazz, name);
fd = frida.socket (AF_UNIX, SOCK_STREAM, 0);
if (fd == -1)
goto beach;
addr.sun_family = AF_UNIX;
addr.sun_path[0] = '\0';
name_len = 0;
for (unsigned int i = 0; i != sizeof (frida.name); i++)
{
if (frida.name[i] == '\0')
break;
if (1u + i >= sizeof (addr.sun_path))
break;
addr.sun_path[1u + i] = frida.name[i];
name_len++;
}
addrlen = (socklen_t) (offsetof (struct sockaddr_un, sun_path) + 1u + name_len);
if (frida_connect (fd, (const struct sockaddr *) &addr, addrlen) == -1)
goto beach;
{
const char * name_utf8;
struct
{
uint32_t pid;
uint32_t ppid;
uint32_t package_name_len;
} header;
struct iovec iov[2];
name_utf8 = (*env)->GetStringUTFChars (env, name, NULL);
header.pid = frida.getpid ();
header.ppid = frida.getppid ();
header.package_name_len = 0;
while (name_utf8[header.package_name_len] != '\0')
header.package_name_len++;
iov[0].iov_base = &header;
iov[0].iov_len = sizeof (header);
iov[1].iov_base = (void *) name_utf8;
iov[1].iov_len = header.package_name_len;
if (!frida_sendmsg_all (fd, iov, 2, MSG_NOSIGNAL))
goto beach;
(*env)->ReleaseStringUTFChars (env, name, name_utf8);
}
{
uint8_t rx;
if (frida_recv (fd, &rx, 1, 0) != 1)
goto beach;
}
success = true;
beach:
if (fd != -1)
frida.close (fd);
if (success)
{
__attribute__ ((musttail))
return frida_stop_and_return (env, clazz, name);
}
return 0;
}
__attribute__ ((section (".text.entrypoint")))
__attribute__ ((visibility ("default")))
int
frida_zymbiote_replacement_set_argv0 (JNIEnv * env, jobject clazz, jstring name)
{
bool success = false;
int fd = -1;
struct sockaddr_un addr;
socklen_t addrlen;
unsigned int name_len;
*frida.art_method_slot = frida.original_set_argv0;
frida.original_set_argv0 (env, clazz, name);
fd = frida.socket (AF_UNIX, SOCK_STREAM, 0);
if (fd == -1)
goto beach;
addr.sun_family = AF_UNIX;
addr.sun_path[0] = '\0';
name_len = 0;
for (unsigned int i = 0; i != sizeof (frida.name); i++)
{
if (frida.name[i] == '\0')
break;
if (1u + i >= sizeof (addr.sun_path))
break;
addr.sun_path[1u + i] = frida.name[i];
name_len++;
}
addrlen = (socklen_t) (offsetof (struct sockaddr_un, sun_path) + 1u + name_len);
if (frida_connect (fd, (const struct sockaddr *) &addr, addrlen) == -1)
goto beach;
{
const char * name_utf8;
struct
{
uint32_t pid;
uint32_t ppid;
uint32_t package_name_len;
} header;
struct iovec iov[2];
name_utf8 = (*env)->GetStringUTFChars (env, name, NULL);
header.pid = frida.getpid ();
header.ppid = frida.getppid ();
header.package_name_len = 0;
while (name_utf8[header.package_name_len] != '\0')
header.package_name_len++;
传播安全知识、拓宽行业人脉——看雪讲师团队等你加入!