Native层Hook利器Cydia Substrate深度解析与实战
简介:Cydia Substrate是一款强大的Native层Hook框架,由Saurik开发,最初用于iOS,后扩展至Android平台。它允许在不修改原始二进制文件的前提下,通过运行时函数替换实现对应用行为的动态修改,广泛应用于调试、性能监控、安全分析和插件化开发。该工具通过干预Dalvik/ART运行时机制,支持C/C++编写的注入代码,具备低侵入性和高灵活性。本文结合cydia_substrate压缩包中的库文件、头文件与示例代码,深入讲解其工作原理与使用方法,帮助开发者掌握在Android系统中实现函数Hook的核心技术。
Cydia Substrate 与 Native Hook 技术深度解析:从原理到实战的全链路实践
在移动安全、逆向工程和系统级定制的世界里,有一项技术如同“暗影中的手术刀”——它能在不修改原始代码的前提下,悄无声息地改变程序行为。这项技术就是 Cydia Substrate ,一个深藏于越狱生态底层却影响深远的运行时 Hook 框架。
你是否曾好奇过:
- 那些自动抢红包的插件是如何“看穿”微信界面并点击弹窗的?
- 安全研究人员如何绕过 SSL Pinning 来抓取加密流量?
- 游戏外挂为何能实时修改内存数值而不崩溃?
答案都指向同一个核心技术: Native 层函数 Hook 。而 Cydia Substrate 正是这把万能钥匙的制造者之一。
今天,我们就来揭开它的神秘面纱,从 ARM 架构下的寄存器约定讲起,一路深入 ELF 文件结构、GOT/PLT 机制、Zygote 注入流程,最终亲手打造一个能够监控敏感文件访问的 Substrate 插件。准备好了吗?🚀
我们先从最基础的问题开始:当一个函数被调用时,CPU 到底发生了什么?
🧠 函数调用的本质:不只是跳转那么简单
想象一下你在写 C 语言:
int add(int a, int b) {
return a + b;
}
看似简单的一行 return ,背后其实是一整套精密协作的硬件与软件规则。尤其是在 Android 广泛使用的 ARM32 架构上,这套规则被称为 AAPCS(ARM Architecture Procedure Call Standard) 。
根据 AAPCS,前四个参数通过 r0 ~ r3 寄存器传递,返回值也放在 r0 中。如果超过四个参数,则多余的部分会压入栈中。例如:
add:
push {r11, lr} @ 保存帧指针和返回地址
add r0, r0, r1 @ r0 = r0 + r1 (a + b)
pop {r11, pc} @ 恢复并返回(pc 接收 lr 值)
注意这里的 lr (链接寄存器),它保存了函数调用后的返回地址。一旦你在 Hook 时不小心破坏了这个寄存器的状态,整个调用链就会断裂,导致应用直接 crash 💥。
所以任何 Hook 操作的第一铁律是: 尊重调用约定,保护现场状态 。
📦 ELF 文件:.so 库的“解剖图”
Android 的 Native 库以 .so 形式存在,本质是一个 ELF(Executable and Linkable Format) 文件。要精准定位目标函数,必须读懂它的内部结构。
你可以用 readelf 工具查看一个典型的 .so 文件:
readelf -s libnative.so | grep encrypt_data
输出可能如下:
124: 0001a3f0 76 FUNC GLOBAL DEFAULT 12 encrypt_data
这意味着 encrypt_data 函数位于偏移 0x1A3F0 处。但在真实进程中,由于 ASLR(地址空间布局随机化),实际地址需要加上模块基址。
怎么获取基址呢?很简单,读 /proc/self/maps 就行:
void* get_module_base(pid_t pid, const char* module_name) {
FILE* fp;
long addr = 0;
char filename[32];
char line[1024];
snprintf(filename, sizeof(filename), "/proc/%d/maps", pid);
fp = fopen(filename, "r");
if (fp != NULL) {
while (fgets(line, sizeof(line), fp)) {
if (strstr(line, module_name)) {
char* pch = strtok(line, "-");
addr = strtoul(pch, NULL, 16);
break;
}
}
fclose(fp);
}
return (void*)addr;
}
然后计算真实地址:
void* base = get_module_base(getpid(), "libnative.so");
void* target_func = (char*)base + 0x1A3F0;
搞定!现在你已经拿到了函数的“物理坐标”,接下来就可以动手改写了。
🔗 GOT/PLT:延迟绑定的秘密通道
现代共享库广泛使用 延迟绑定(Lazy Binding) ,也就是只有第一次调用某个外部函数(如 printf )时才去查找其真实地址。这种机制依赖两个关键表: GOT(Global Offset Table) 和 PLT(Procedure Linkage Table) 。
它们的工作流程可以用下面这张图表示:
sequenceDiagram
participant App
participant PLT
participant GOT
participant Linker
App->>PLT: call plt_entry@printf
PLT->>GOT: jmp *got_entry@printf
alt 第一次调用
GOT-->>Linker: 指向 resolve_stub
Linker->>Linker: 查找 printf 地址
Linker->>GOT: 填写真实地址
Linker->>App: 跳转至 printf
else 后续调用
GOT->>App: 直接跳转至 printf
end
看到了吗?中间有个“中间商” GOT。既然如此,我们能不能偷偷改掉 GOT 里的地址,让它指向我们的代理函数?
当然可以!这就是所谓的 GOT Hook 。
✅ GOT Hook 示例代码:
#include <link.h>
#include <dlfcn.h>
int got_hook(const char *symbol, void *new_func, void **old_func) {
struct link_map *lm;
dlinfo(RTLD_DEFAULT, RTLD_DI_LINKMAP, &lm);
for (; lm; lm = lm->l_next) {
Elf32_Dyn *dyn = lm->l_ld;
Elf32_Sym *symtab = NULL;
const char *strtab = NULL;
Elf32_Rel *plt_rel = NULL;
int rel_cnt = 0;
while (dyn->d_tag != DT_NULL) {
switch (dyn->d_tag) {
case DT_SYMTAB: symtab = (Elf32_Sym *)dyn->d_un.d_ptr; break;
case DT_STRTAB: strtab = (const char *)dyn->d_un.d_ptr; break;
case DT_JMPREL: plt_rel = (Elf32_Rel *)dyn->d_un.d_ptr; break;
case DT_PLTRELSZ: rel_cnt = dyn->d_un.d_val / sizeof(Elf32_Rel); break;
}
dyn++;
}
if (!symtab || !strtab || !plt_rel) continue;
for (int i = 0; i < rel_cnt; i++) {
Elf32_Word sym_idx = ELF32_R_SYM(plt_rel[i].r_info);
const char *name = strtab + symtab[sym_idx].st_name;
if (strcmp(name, symbol) == 0) {
void **got_entry = (void **)(lm->l_addr + plt_rel[i].r_offset);
*old_func = *got_entry;
mprotect(got_entry, sizeof(void*), PROT_READ | PROT_WRITE);
*got_entry = new_func;
return 0;
}
}
}
return -1;
}
调用方式也很简单:
void* my_open_proxy(const char* path, int flags, mode_t mode);
void* orig_open;
got_hook("open", my_open_proxy, &orig_open);
从此以后,所有对 open 的调用都会先经过你的代理函数。是不是很酷?😎
但要注意:GOT Hook 只能用于外部函数调用(即导入函数),对于库内部定义的静态函数无效。这时候就得靠另一种更暴力的方式——Inline Hook。
🔪 Inline Hook:直接篡改函数开头
Inline Hook 的思路非常直接:在目标函数开头插入一条跳转指令,强制控制流转向你的 Hook 函数。为了保证原逻辑还能执行,你还得备份被覆盖的原始指令,并构建一个“跳板”(Trampoline)来恢复执行。
以 ARM32 为例,最短跳转指令是 ldr pc, [pc, #-4] ,占 4 字节。所以我们至少要备份 4 字节原指令。
🛠️ 实现步骤:
- 分配可执行内存作为 Trampoline
- 备份原函数前 N 字节指令
- 写入跳转指令指向 Hook 函数
- 在 Trampoline 中重建上下文并跳回原函数剩余部分
void inline_hook(void *target_func, void *hook_func, void **orig_ptr) {
void *trampoline = mmap(NULL, 0x1000,
PROT_READ | PROT_WRITE | PROT_EXEC,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
uint32_t original_code[2];
memcpy(original_code, target_func, 8);
uint32_t jump_insn = 0xE51FF004; // ldr pc, [pc, #-4]
uint32_t target_addr = (uint32_t)hook_func;
mprotect(target_func, 8, PROT_READ | PROT_WRITE | PROT_EXEC);
*(uint32_t*)target_func = jump_insn;
*((uint32_t*)target_func + 1) = (uint32_t)&target_addr;
uint32_t *t = (uint32_t*)trampoline;
memcpy(t, original_code, 8);
t[2] = 0xE51FF004;
t[3] = (uint32_t)((char*)target_func + 8);
*orig_ptr = trampoline;
}
这种方式适用于所有本地函数,包括那些没有导出符号的私有函数。但它也有风险:如果多个线程同时进入被 Hook 的函数,可能会出现竞态条件。因此建议配合信号量或内存屏障使用。
⚖️ 三种 Hook 方式的对比
| 方法 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| Inline Hook | 修改函数开头为跳转指令 | 精确控制单个函数 | 易受并发影响,需备份原指令 |
| GOT Hook | 修改 GOT 表项指向新地址 | 全局生效,安全性高 | 仅能 Hook 导入函数 |
| PLT Hook | 操作 PLT 段而非 GOT | 可绕过某些加固检测 | 不同 ABI 差异大,兼容性差 |
实际项目中往往结合多种方式协同工作。比如先用 GOT Hook 拦截 dlopen ,再在 dlopen 回调中对新加载的库进行 Inline Hook。
🔄 Cydia Substrate 是如何做到跨平台兼容的?
Substrate 最厉害的地方在于它能同时支持 Dalvik 和 ART 运行时环境。要知道,从 Dalvik 到 ART 的转变不仅仅是 JIT → AOT 编译那么简单,整个 JNI 调用机制都变了。
Dalvik:解释执行时代的自由
在 Dalvik 时代,Java 方法大多通过 RegisterNatives 动态注册到 Native 函数。这意味着只要在 JNI_OnLoad 之后、方法首次调用之前完成 Hook,就能成功替换函数指针。
而且 Dalvik 对内存保护较弱,允许 mprotect(PROT_EXEC) 修改 .text 段权限,Inline Hook 很容易实现。
graph TD
A[Zygote Fork] --> B[加载 libdvm.so]
B --> C[调用 System.loadLibrary]
C --> D[执行 JNI_OnLoad]
D --> E[注册 Native 方法]
E --> F[MSHookFunction 拦截 dlopen/dlsym]
F --> G[替换目标函数]
整个流程清晰可控,Substrate 可以轻松注入并建立 Hook。
ART:AOT 编译带来的挑战
到了 ART 时代,问题变得复杂了。应用安装时就被编译成 .oat 文件,Java 方法可能已经被内联或静态绑定。更重要的是,VM 维护了一张 jniCode 表,缓存了 native 方法的入口地址。一旦方法被调用过一次,后续就不再走符号查找流程。
这就意味着: 你必须在方法尚未被调用前完成 Hook,否则将失效 。
为此,Substrate 引入了 延迟初始化机制 :通过 Hook art::ClassLinker::DefineClass ,监听类加载事件,在类定义完成后、任何方法执行前插入 Hook。
此外,ART 默认禁止写 .text 段,必须调用 mprotect() 提升权限。而在 Android 7+ 上,SELinux 策略还会进一步限制此类操作。解决办法通常是借助 Magisk 模块以 systemless 方式部署,避开系统分区写入限制。
🔍 OAT 文件结构与 Hook 点识别
OAT 文件本质上是一个 ELF 格式的可执行镜像,包含预编译的机器码和元数据。我们可以通过 readelf 查看其结构:
readelf -S liboat.so
输出示例:
Section Headers:
[Nr] Name Type Addr Off Size
[ 1] .oat.data PROGBITS 00001000 001000 002000
[ 2] .text PROGBITS 00003000 003000 008000
.text 存放编译后的代码, .oat.data 包含方法元信息。Substrate 会遍历 .oat.data 中的 OatMethodOffsets 数组,检查每个方法是否调用了目标函数(如 open )。如果是,则在其调用点前后插入探针或重定向控制流。
虽然工作量更大,但精度更高,避免盲目 Hook 导致崩溃。
🚀 Substrate 的启动与注入流程
Substrate 的注入依赖 Zygote 进程。Zygote 是所有应用进程的父进程,Substrate 利用这一点,在 Zygote 启动时加载 libsubstrate.so ,并通过 LD_PRELOAD 或 init 脚本将其注入全局环境。
具体流程如下:
- 设备开机后,init 进程读取
init.rc或 Magisk 模块脚本; - 当 Zygote 启动时,系统自动加载
libsubstrate.so; JNI_OnLoad被调用,Substrate 初始化钩子引擎;- 扫描
/system/lib(64)/substrate.d/目录下的插件.so文件; - 根据
MSConfig声明的目标包名匹配当前进程; - 若匹配成功,则加载插件并执行 Hook 操作。
这种“一次注入,处处生效”的机制极大提高了效率。
验证是否注入成功也很简单:
adb shell ps | grep zygote
adb shell cat /proc/<zygote_pid>/maps | grep substrate
如果有 libsubstrate.so 的映射记录,说明注入成功 ✅。
🧩 Java 层与 Native 层的双向 Hook 支持
Substrate 不止能 Hook Native 函数,还能深入 Java 层,实现真正的“全栈 Hook”。
✨ MSHookActivityClass 实现 Java 方法拦截
static void (*origin_onCreate)(JNIEnv*, jobject, jobject);
void my_onCreate(JNIEnv *env, jobject thiz, jobject bundle) {
LOGD("Before onCreate");
origin_onCreate(env, thiz, bundle);
LOGD("After onCreate");
}
MSInitialize {
MSAndroidMHookClassLoad("com/example/MainActivity", +[](void *self, JNIEnv *env, jclass clazz) {
jmethodID method = env->GetMethodID(clazz, "onCreate", "(Landroid/os/Bundle;)V");
MSHookMethod(env, clazz, method, (void*)my_onCreate, (void**)&origin_onCreate);
});
}
这段代码会在 MainActivity 加载时自动替换 onCreate 方法,实现生命周期监控。
🔗 JNI 函数表替换
对于声明为 native 的方法,还可以直接 Hook RegisterNatives :
static jint (*original_RegisterNatives)(JNIEnv*, jclass, const JNINativeMethod*, jint);
jint my_RegisterNatives(JNIEnv* env, jclass clazz, const JNINativeMethod* methods, jint nMethods) {
for (int i = 0; i < nMethods; ++i) {
if (strcmp(methods[i].name, "encrypt") == 0) {
((JNINativeMethod*)(methods))[i].fnPtr = my_encrypt_impl;
}
}
return original_RegisterNatives(env, clazz, methods, nMethods);
}
// Hook RegisterNatives 自身
MSHookFunction((void*)env->functions->RegisterNatives,
(void*)my_RegisterNatives,
(void**)&original_RegisterNatives);
这样一来,所有通过 native 关键字声明的方法都会被提前重定向。
💻 实战项目:开发一个 Substrate 插件拦截 open 调用
我们现在来动手做一个完整的插件,目标是监控所有对敏感文件(如 keystore、password)的访问。
1️⃣ 开发环境准备
使用 NDK 生成独立工具链:
python3 $ANDROID_NDK/build/tools/make_standalone_toolchain.py \
--arch arm \
--api 21 \
--install-dir /opt/arm-toolchain
设置环境变量:
export CC=/opt/arm-toolchain/bin/arm-linux-androideabi-gcc
export CXX=/opt/arm-toolchain/bin/arm-linux-androideabi-g++
2️⃣ 编写插件代码
#include <substrate.h>
#include <android/log.h>
#define LOG_TAG "OpenHook"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
typedef int (*original_open_t)(const char*, int, ...);
original_open_t original_open = nullptr;
int my_open(const char* pathname, int flags, mode_t mode) {
if (pathname && (strstr(pathname, "password") || strstr(pathname, "keystore"))) {
LOGI("[SECURITY] Sensitive file access: %s", pathname);
}
return original_open(pathname, flags, mode);
}
MSConfig(MSFilterPackage, "com.android.chrome"); // 仅作用于 Chrome
MSConfig(MSConfigVersion, 0x00020000);
__attribute__((constructor))
static void initialize() {
LOGI("Substrate plugin loaded.");
MSImageRef image = MSGetImageByName("/system/lib/libc.so");
void* open_sym = MSFindSymbol(image, "open");
if (open_sym) {
MSHookFunction(open_sym, (void*)my_open, (void**)&original_open);
LOGI("Hook installed on open().");
} else {
LOGI("Failed to find open().");
}
}
3️⃣ 打包为 .deb 安装包
创建目录结构:
myhook_1.0_arm.deb/
├── DEBIAN/
│ ├── control
│ ├── postinst
├── Library/
└── MobileSubstrate/
└── DynamicLibraries/
├── MyHook.plist
└── libhook.so
DEBIAN/control 内容:
Package: com.example.myhook
Name: File Access Monitor
Version: 1.0-1
Architecture: arm
Description: Logs sensitive file access via open()
Maintainer: dev@example.com
Depends: mobilesubstrate
Section: Tweaks
MyHook.plist :
<dict>
<key>Filter</key>
<dict>
<key>Bundles</key>
<array>
<string>com.android.chrome</string>
</array>
</dict>
</dict>
打包安装:
dpkg-deb -Zlzma -b myhook_1.0_arm.deb
adb push myhook_1.0_arm.deb /tmp
adb shell dpkg -i /tmp/myhook_1.0_arm.deb
adb shell killall com.android.chrome
4️⃣ 验证效果
打开 logcat:
adb logcat | grep OpenHook
你会看到类似输出:
I/OpenHook(12345): [SECURITY] Sensitive file access: /data/data/com.chrome/app_keystore
🎉 成功捕获敏感文件访问!
🌐 总结:Substrate 的价值与未来趋势
Cydia Substrate 虽然诞生于越狱 iOS 生态,但它所代表的 运行时动态插件化思想 正在深刻影响着现代 Android 安全与自动化领域。
尽管随着 Android 版本升级(尤其是 Scoped Storage、SELinux 强化、dlopen 符号隐藏等机制),传统 Hook 技术面临越来越多限制,但 Substrate 通过以下方式持续进化:
- Magisk 模块化部署 :实现 systemless 注入,无需修改系统分区;
- 多阶段 Hook 时机控制 :精确把握类加载、库加载等关键节点;
- 统一 API 抽象层 :屏蔽 Dalvik/ART 差异,提升兼容性;
- Java-Native 双向穿透能力 :构建全栈行为监控体系。
未来,这类技术不仅用于逆向分析,也将更多应用于:
- 移动安全产品的动态防护引擎
- 自动化测试框架的行为模拟
- 性能埋点与用户体验优化
- 游戏辅助与无障碍增强
无论你是安全研究员、逆向工程师,还是系统开发者,掌握 Substrate 与 Native Hook 技术,都将为你打开一扇通往 Android 底层世界的大门。
毕竟,真正理解一个系统的最好方式,就是学会如何“悄悄地改变它”。😉
简介:Cydia Substrate是一款强大的Native层Hook框架,由Saurik开发,最初用于iOS,后扩展至Android平台。它允许在不修改原始二进制文件的前提下,通过运行时函数替换实现对应用行为的动态修改,广泛应用于调试、性能监控、安全分析和插件化开发。该工具通过干预Dalvik/ART运行时机制,支持C/C++编写的注入代码,具备低侵入性和高灵活性。本文结合cydia_substrate压缩包中的库文件、头文件与示例代码,深入讲解其工作原理与使用方法,帮助开发者掌握在Android系统中实现函数Hook的核心技术。
更多推荐




所有评论(0)