首页
社区
课程
招聘
[推荐]新版Frida-Zymbiote注入机制
发表于: 2天前 1441

[推荐]新版Frida-Zymbiote注入机制

2天前
1441

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];                    // MAGIC
   
  void    ** art_method_slot;       // ArtMethod 的 entry_point 地址
  void    (* original_set_argv0)(); // 原始 JNI 函数地址
   
  // libc 函数指针
  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];                    // MAGIC
   
  void    ** art_method_slot;       // ArtMethod 的 entry_point 地址
  void    (* original_set_argv0)(); // 原始 JNI 函数地址
   
  // libc 函数指针
  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;
 
  // 1. 立即恢复原始 entry_point(避免重复触发)
  *frida.art_method_slot = frida.original_set_argv0;
 
  // 2. 调用原始函数(保证 app 正常运行)
  frida.original_set_argv0 (env, clazz, name);
 
  // 3. 创建 Unix socket
  fd = frida.socket (AF_UNIX, SOCK_STREAM, 0);
  if (fd == -1)
    goto beach;
 
  // 4. 构造 socket 地址(abstract namespace)
  addr.sun_family = AF_UNIX;
  addr.sun_path[0] = '\0'// abstract socket 标志
 
  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);
 
  // 5. 连接 Frida Server
  if (frida_connect (fd, (const struct sockaddr *) &addr, addrlen) == -1)
    goto beach;
 
  // 6. 发送进程信息
  {
    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);
  }
 
  // 7. 等待 Frida Server 确认
  {
    uint8_t rx;
    if (frida_recv (fd, &rx, 1, 0) != 1)
      goto beach;
  }
 
  success = true;
 
beach:
  if (fd != -1)
    frida.close (fd);
 
  // 8. 暂停进程
  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;
 
  // 1. 立即恢复原始 entry_point(避免重复触发)
  *frida.art_method_slot = frida.original_set_argv0;
 
  // 2. 调用原始函数(保证 app 正常运行)
  frida.original_set_argv0 (env, clazz, name);
 
  // 3. 创建 Unix socket
  fd = frida.socket (AF_UNIX, SOCK_STREAM, 0);
  if (fd == -1)
    goto beach;
 
  // 4. 构造 socket 地址(abstract namespace)
  addr.sun_family = AF_UNIX;
  addr.sun_path[0] = '\0'// abstract socket 标志
 
  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);
 
  // 5. 连接 Frida Server
  if (frida_connect (fd, (const struct sockaddr *) &addr, addrlen) == -1)
    goto beach;
 
  // 6. 发送进程信息
  {
    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++;
 

传播安全知识、拓宽行业人脉——看雪讲师团队等你加入!

收藏
免费 97
支持
分享
最新回复 (54)
雪    币: 1916
活跃值: (1841)
能力值: ( LV4,RANK:40 )
在线值:
发帖
回帖
粉丝
2
看雪的markdown解析器好像不支持vala的,,,
2天前
0
雪    币: 104
活跃值: (7441)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
3
tql
2天前
0
雪    币: 6
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
4
感谢分享
2天前
0
雪    币: 124
活跃值: (1445)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
5
666
2天前
0
雪    币: 8810
活跃值: (6642)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
6
感谢分享
2天前
0
雪    币: 260
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
7
11
2天前
0
雪    币: 255
活跃值: (856)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
8
6666
2天前
0
雪    币: 329
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
9
666
2天前
0
雪    币: 930
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
10
感谢分享
2天前
0
雪    币: 0
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
11
为你点赞!
2天前
0
雪    币: 0
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
12
6666666
2天前
0
雪    币: 1780
活跃值: (1355)
能力值: ( LV4,RANK:40 )
在线值:
发帖
回帖
粉丝
13
为你点赞!
2天前
0
雪    币: 5152
活跃值: (5284)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
14
666
2天前
0
雪    币: 209
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
15
66
2天前
0
雪    币: 5
活跃值: (3705)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
16
6666
2天前
0
雪    币: 0
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
17
6666
2天前
0
雪    币: 210
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
18

2天前
0
雪    币: 5540
活跃值: (3190)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
19
看看
2天前
0
雪    币: 184
活跃值: (577)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
20
学习
2天前
0
雪    币: 1796
活跃值: (2507)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
21
大侠强啊
2天前
0
雪    币: 1916
活跃值: (1841)
能力值: ( LV4,RANK:40 )
在线值:
发帖
回帖
粉丝
22
蜕无痕 大侠强啊
还是造哥更吊
2天前
0
雪    币: 126
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
23
666
2天前
0
雪    币: 293
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
24
66666666666
2天前
0
雪    币: 0
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
25
666
2天前
0
游客
登录 | 注册 方可回帖
返回