Frida 脚本无 Root 一键持久化方案
最近在开发某款 APP 的功能扩展时,我使用 Frida 编写了大量逻辑,但不想再逐行翻译成 Xposed 代码。于是决定将 Frida 直接固化到 APP 中。
最初尝试使用 frida-gadget,但在 Android 16 上遇到问题:脚本完全不执行,也没有任何错误提示,只好放弃。
在网上搜索后,没有找到便捷的持久化方案,于是决定自己动手实现。
我的目标是:只需运行一行命令,就能自动打包出 Xposed 模块、已固化 Frida 脚本的 APP 包,以及可注入运行 Frida 脚本的 .so 和 .dll 文件。
最终,这个目标顺利实现了。如果你只是想打包 Frida 脚本,而不关心具体原理,可以直接使用以下工具:
https://github.com/std-microblock/fripack/
1
自编译 Frida
既然要自己动手,自然要做得尽善尽美。一方面需要掩盖 Frida 的某些特征,另一方面也要尽量减小体积。此外,我还希望将脚本直接嵌入二进制文件中,而不是作为独立文件存在。于是,我开始研究如何自行编译 Frida,类似于实现一个自定义的 frida-gadget。
首先,我们来看一下 Frida 的代码架构:

(注:此为简化示意图。实际上,frida-inject 和 frida-server 注入的是 frida-agent,但整体结构类似。)
可以看到,Frida Gadget 实际上是通过调用 Frida-GumJS 来执行我们的脚本。因此,我们也可以编写一个程序,调用 Frida-GumJS 来执行脚本。为了实现这一点,首先需要编译出 Frida-GumJS 库。
Frida-GumJS 通常包含两个引擎:V8 和 QuickJS。V8 执行效率高,但体积较大;QuickJS 体积较小。为了减小最终生成的二进制文件体积,我关闭了 V8 和内置的 Database,并参考 Florida 的 CI 流程,编写了一个 GitHub CI 来编译 Frida-GumJS:
https://github.com/FriRebuild/fripack-inject
接着,我创建了一个 xmake 项目,首先实现调用 GumJS 执行脚本的功能:
为了便于后续嵌入 JS 脚本,我设计了一个带有特定 Magic 标记的结构体:
这样,在生成 .so 文件时,就可以通过扫描 Magic 标记找到这个配置结构,然后通过设置data_size和data_offset来指定脚本数据在二进制文件中的存储位置。
接下来,我们需要编写一个 CLI 工具,将脚本数据嵌入到二进制文件中。这里我选择了使用 Rust 来实现。
2
将数据嵌入二进制文件
## ELF 文件编辑
ELF 文件的主要结构包括 Section 和 Segment,其中数据通过 Segment 映射到内存中。我们只需新建一个 Segment,将脚本数据放入其中,并确保其加载到内存中。然后,计算出该数据映射的虚拟地址与g_embedded_config虚拟地址之间的偏移量,这样我们自制的 frida-gadget 就能正确加载数据了。
听起来很简单,我们来实现一下:
需要特别注意:对于需要加载到内存中的 Segment(PT_LOAD),其vaddr必须按 4K 或 16K 对齐,否则dl可能不会正确映射,且不会报错。
同时,由于新增了 Segment 和 Section,我们还需要扩展 ELF 文件头的大小:
运行后却发现报错:.note sh_offset at 0x270, must be larger than 0x28a。这是怎么回事?原来是因为新增的 Segment 和 Section 导致 ELF 头变大,与原有的部分 Section 发生了重叠。我们需要将这些重叠的 Section 在文件中的数据及其p_offset移动到文件末尾:

再次运行,这次没有报错了。但将文件放到手机中加载时,又出现了新错误:PT_PHDR segment is not covered by a PT_LOAD segment。这个问题比较奇怪,网上资料也很少。我猜测 PT_PHDR Segment 本身不会被映射到内存中,它需要一个 PT_LOAD Segment 来帮助加载。于是,我们找到覆盖 PT_PHDR Segment 的那个 Segment,并将其扩展的大小同步到 PT_PHDR Segment:
接着,我们找到 Magic 标记在二进制文件中的位置,定位其所在的 Segment,并获取其vaddr:
计算出偏移量后,将其填入二进制文件中:
## PE 文件编辑
PE 文件的处理逻辑类似。虽然编辑 ELF 花了大半天时间,踩了不少坑,但编写 PE 编辑代码只用了十分钟。不得不说,PE 的设计比 ELF 更优秀(除了那个强制要求 ordinal 的 EAT),而且几乎没有遇到什么坑。以下是相关代码:
至此,我们得到了一个无需任何外部文件依赖、加载即自动执行脚本的二进制文件。
3
加载二进制文件到目标进程
接下来,只需要想办法将这个二进制文件加载到目标进程中。实现方式有很多,例如:
- 重新打包 APK,对 APP 自带的 .so 文件使用 patchelf,将我们的 .so 加入其依赖项。
- 重新打包 APK,在某个初始化类中添加静态初始化代码,调用System.loadLibrary加载我们的 .so。
- 编写 Xposed 模块,利用其在目标进程中执行代码的特性来加载 .so。
- 对于 Windows,使用多种 DLL 注入手段加载我们的 DLL。
- 对于 Linux,使用LD_PRELOAD或其他映射方法加载我们的 .so。
- 编写 Zygisk 模块,直接对特定 APP 加载我们的 .so。
目前,我已实现了前五种加载方式。受篇幅所限,这里仅介绍 Xposed 的实现方式。
4
Xposed 模块来加载.so
Xposed 模块会在目标进程中执行代码,因此我们只需在xposed_init类的initZygote方法中获取模块的 APK 路径,然后使用System.load加载其中的 .so 文件即可。
但问题在于,我们需要生成的是一个 APK 文件。我不想依赖过于笨重的 gradlew,也不想手动处理安卓特有的压缩格式、aapt2 编码、zipalign 等步骤。因此,我选择直接生成 apktool 工程,然后使用 apktool 构建 Xposed 模块的 APK。这样只需安装 apktool,即可方便地完成生成。
Xposed 模块的主要特征包括:AndroidManifest 中的几个特殊 metadata,以及/assets/文件夹中的xposed_init和native_init文件。我们直接手动构建 apktool 的文件结构,并编写一段 Smali 代码,继承IXposedHookZygoteInit和IXposedHookLoadPackage,以实现加载 .so 的逻辑:
最后使用apktool b命令,即可构建出 Xposed 模块。
至此,我们已经实现了从 Frida 脚本一键打包生成 Xposed 模块的大部分逻辑。

看雪ID:_MicroBlock
https://bbs.kanxue.com/user-home-1052870.htm
*本文为看雪论坛优秀文章,由 _MicroBlock原创,转载请注明来自看雪社区
黑白之道发布、转载的文章中所涉及的技术、思路和工具仅供以安全为目的的学习交流使用,任何人不得将其用于非法用途及盈利等目的,否则后果自行承担!
如侵权请私聊我们删文
END
华盟君