一、Cydia Substrate 简介 Cydia Substrate(http://www.cydiasubstrate.com/)是 saurik 开发的跨平台 Native 层 Hook 框架。在 Android 上,它提供了类似 Xposed 的模块化 Hook 能力,但作用在 C/C++ 层。核心 API 包括 MSHookFunction(inline hook)和 MSHookMessageEx(Objective-C 消息 Hook,主要用于 iOS)。
相比 Frida,Cydia Substrate 的 Hook 是持久化的(设备重启后仍生效),适合开发可发布的 Native Hook 模块。
一.1 框架架构概览 Cydia Substrate 在 Android 上的架构分为三层:
┌─────────────────────────────────────────────────────┐ │ Substrate APK (Java) │ │ - 管理模块列表 │ │ - 注入目标进程 │ │ - 提供用户界面 │ └──────────────────────┬──────────────────────────────┘ │ inject / dlopen ┌──────────────────────▼──────────────────────────────┐ │ libsubstrate.so (Native 核心) │ │ - MSHookFunction (inline hook 实现) │ │ - MSHookMessageEx (OC 消息转发) │ │ - MSJavaHook (JNI 方法 Hook) │ │ - SubstrateMemory (内存操作) │ └──────────────────────┬──────────────────────────────┘ │ ┌──────────────────────▼──────────────────────────────┐ │ libsubstrate-dvm.so (ART/DVM 适配) │ │ - 与 Android Runtime 交互 │ │ - Java 方法编译/解释入口拦截 │ │ - JNI 桥接处理 │ └─────────────────────────────────────────────────────┘
一.2 模块生命周期 Cydia Substrate 模块(.cy.so 文件)的加载链路:
1. APK 安装 → 系统包管理器扫描 /system/lib/ 下的 .cy.so 2. Zygote 进程 fork → 新进程创建 3. Substrate 向新进程注入 libsubstrate.so 4. libsubstrate.so 扫描 /data/data/com.saurik.substrate/ 下已启用的模块 5. 依次 dlopen 每个 .cy.so 模块 6. 每个模块的 MSInitialize 被自动调用(通过 __attribute__((constructor))) 7. MSInitialize 中调用 MSHookFunction 等 API 安装 Hook 8. 进程执行 → Hook 生效
二、Inline Hook 原理深度剖析 MSHookFunction 使用标准的 inline hook 技术。要理解其本质,需要先了解 ARM 指令集的特性。
二.1 ARM 指令模式:ARM vs Thumb ARM 处理器支持两套指令集:
ARM 模式 :每条指令固定 4 字节,完整功能集。寻址时地址最低 2 bits 必须为 00。
Thumb-2 模式 :指令 2 或 4 字节,高代码密度。寻址时地址最低 bit 必须为 1(LSB=1 标识 Thumb 模式)。
这是 inline hook 中最容易出错的点:如果目标函数是 Thumb 模式,Hook 跳板必须在 Thumb 模式下执行,否则处理器会触发未定义指令异常。
判断函数模式的方法: address & 1 == 1 → Thumb 模式 address & 1 == 0 → ARM 模式 函数指针实际使用时应清除 LSB: real_addr = (void*)((uintptr_t)addr & ~1);
二.2 Inline Hook 的经典实现步骤 Step 1: 保存目标函数开头的 N 条指令(通常 4-8 字节) ↓ Step 2: 构造跳板(trampoline) - 执行被保存的原始指令 - 跳回原始函数的 (N+1) 条指令位置 ↓ Step 3: 将目标函数开头替换为跳转指令 ARM: LDR PC, [PC, #-4] .word <hook_function_address> Thumb: LDR R0, [PC, #8] BX R0 .word <hook_function_address> ↓ Step 4: 刷新指令缓存(cacheflush / __clear_cache) 确保 CPU 取指单元看到的是新写入的指令
二.3 MSHookFunction 内部实现解析 MSHookFunction 的 C 语言伪实现:
void MSHookFunction (void * symbol, void * hook, void ** old) { uintptr_t page_start = (uintptr_t )symbol & ~(PAGE_SIZE - 1 ); size_t page_offset = (uintptr_t )symbol - page_start; mprotect((void *)page_start, PAGE_SIZE, PROT_READ | PROT_WRITE | PROT_EXEC); bool thumb_mode = ((uintptr_t )symbol & 1 ); uintptr_t real_addr = (uintptr_t )symbol & ~1 ; size_t backup_size = thumb_mode ? backup_thumb(real_addr, 2 ) : backup_arm(real_addr, 2 ); void * trampoline = mmap(NULL , 4096 , PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS, -1 , 0 ); write_trampoline(trampoline, backup_buf, backup_size, real_addr + backup_size, hook, old); write_jump(real_addr, trampoline, thumb_mode); cacheflush(page_start, PAGE_SIZE, 0 ); mprotect((void *)page_start, PAGE_SIZE, PROT_READ | PROT_EXEC); *old = trampoline; }
二.4 ARM64 (AArch64) 下的 Inline Hook 现代 Android 设备几乎全部使用 ARM64 (AArch64)。AArch64 下的 inline hook 与 ARM32 有显著差异:
AArch64 特点: - 指令固定 4 字节,无 Thumb/ARM 模式切换问题 - PC 不能直接作为通用寄存器,不能通过 LDR PC 跳转 - 使用 BR / BLR 寄存器间接跳转 AArch64 跳转指令构造(使用 BR + 字面量): LDR X16, #8 ; 从 PC+8 处加载地址到 X16 BR X16 ; 跳转到 X16 指向的地址 .quad <hook_addr> ; 8 字节地址字面量(共 16 字节) 更紧凑的方案(使用 B 指令,±128MB 范围内): B <offset> ; 若目标在 ±128MB 范围内可用
二.5 指令缓存刷新(cache coherency) 这是 inline hook 中容易被忽略但至关重要的步骤。修改了内存中的指令后,必须确保 CPU 的指令缓存(I-Cache)与数据缓存(D-Cache)同步:
void cacheflush (long addr, long nbytes, long cache) ;__builtin___clear_cache((char *)start, (char *)end); #if defined(__arm__) cacheflush((long )addr, (long )size, 0 ); #elif defined(__aarch64__) __builtin___clear_cache((char *)addr, (char *)(addr + size)); #endif
ARM 体系结构参考手册中规定:对于自修改代码(Self-Modifying Code),必须先执行 Data Synchronization Barrier(DSB),再执行 Instruction Synchronization Barrier(ISB),确保指令缓存与数据缓存的一致性。
三、核心 API 详解 三.1 MSHookFunction #include <substrate.h> typedef int (*original_func_t ) (const char * str, int flags) ;static original_func_t original_func = NULL ;int hooked_func (const char * str, int flags) { printf ("[Hook] Called with: %s, flags=%d\n" , str, flags); int result = original_func(str, flags); printf ("[Hook] Return value: %d\n" , result); return result; } void install_hook () { void * target_addr = dlsym(RTLD_DEFAULT, "target_function_name" ); MSHookFunction( target_addr, (void *)hooked_func, (void **)&original_func ); }
三.2 MSHookMemory MSHookMemory 允许 patch 内存中的任意数据:
void patch_check_value () { void * target = dlsym(RTLD_DEFAULT, "g_license_valid" ); if (target) { int new_value = 1 ; MSHookMemory(target, &new_value, sizeof (new_value)); } }
三.3 MSHookMessageEx(iOS 为主) MSHookMessageEx( [targetClass class ], @selector (targetMethod:), imp_implementationWithBlock(replacementBlock), (IMP*)&original_imp );
三.4 MSInitialize 宏 MSInitialize 是每个 Substrate 模块的入口点,被定义为 __attribute__((constructor)) 函数:
static void __attribute__((constructor)) my_module_init() { MSHookFunction((void *)target_func, (void *)hook_func, (void **)&orig_func); } MSInitialize { MSHookFunction((void *)target, (void *)hook, (void **)&orig); }
四、实战:Hook libc 函数 四.1 Hook fopen 监控文件访问 #include <substrate.h> #include <stdio.h> #include <string.h> #include <dlfcn.h> static FILE* (*original_fopen)(const char * path, const char * mode) = NULL ;FILE* hooked_fopen (const char * path, const char * mode) { const char * safe_path = path ? path : "(null)" ; printf ("[fopen] Path: %s, Mode: %s\n" , safe_path, mode); if (path && strstr (path, "/sdcard/secret" )) { printf ("[fopen] Blocked access to secret file!\n" ); return NULL ; } if (path && strstr (path, "/data/data/com.target" )) { printf ("[fopen] App private dir access detected\n" ); } return original_fopen(path, mode); } MSInitialize { MSHookFunction((void *)fopen, (void *)hooked_fopen, (void **)&original_fopen); }
四.2 Hook dlopen 监控 so 加载 #include <dlfcn.h> static void * (*original_dlopen)(const char * filename, int flags) = NULL ;void * hooked_dlopen (const char * filename, int flags) { printf ("[dlopen] Loading: %s, flags=0x%x\n" , filename, flags); if (filename && strstr (filename, "libjiagu" )) { printf ("[dlopen] *** 检测到 360 加固库加载 ***\n" ); } if (filename && strstr (filename, "libSecShell" )) { printf ("[dlopen] *** 检测到梆梆加固库加载 ***\n" ); } void * handle = original_dlopen(filename, flags); if (handle) { printf ("[dlopen] Handle: %p\n" , handle); Dl_info info; if (dladdr(handle, &info) && info.dli_fbase) { printf ("[dlopen] Base address: %p\n" , info.dli_fbase); } } else { printf ("[dlopen] Failed to load: %s\n" , dlerror()); } return handle; } MSInitialize { MSHookFunction((void *)dlopen, (void *)hooked_dlopen, (void **)&original_dlopen); }
四.3 Hook dlsym 监控函数解析 static void * (*original_dlsym)(void * handle, const char * symbol) = NULL ;void * hooked_dlsym (void * handle, const char * symbol) { void * result = original_dlsym(handle, symbol); static const char * sensitive_syms[] = { "JNI_OnLoad" , "ptrace" , "dlopen" , "mmap" , "mprotect" , NULL }; for (int i = 0 ; sensitive_syms[i]; i++) { if (symbol && strcmp (symbol, sensitive_syms[i]) == 0 ) { printf ("[dlsym] Sensitive symbol resolved: %s → %p\n" , symbol, result); break ; } } return result; } MSInitialize { MSHookFunction((void *)dlsym, (void *)hooked_dlsym, (void **)&original_dlsym); }
四.4 Hook connect 监控网络连接 #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> static int (*original_connect) (int sockfd, const struct sockaddr* addr, socklen_t addrlen) = NULL ;int hooked_connect (int sockfd, const struct sockaddr* addr, socklen_t addrlen) { if (addr->sa_family == AF_INET) { struct sockaddr_in * sin = (struct sockaddr_in*)addr; char ip_str[INET_ADDRSTRLEN]; inet_ntop(AF_INET, &sin ->sin_addr, ip_str, sizeof (ip_str)); printf ("[connect] → %s:%d\n" , ip_str, ntohs(sin ->sin_port)); if (strcmp (ip_str, "10.0.2.2" ) == 0 ) { printf ("[connect] Blocked frida-server port\n" ); return -1 ; } } return original_connect(sockfd, addr, addrlen); } MSInitialize { MSHookFunction((void *)connect, (void *)hooked_connect, (void **)&original_connect); }
五、Hook JNI 方法 五.1 JNI 方法 Hook 的挑战 Hook JNI 方法的难点在于获取其 Native 函数地址。Java 层声明 native 方法后,Native 函数的链接方式有两种:
静态注册: 函数名遵循 JNI 命名规范 Java_<包名>_<类名>_<方法名>,可以通过 dlsym 直接获取地址。
void * jni_addr = dlsym(RTLD_DEFAULT, "Java_com_example_app_NativeLib_calculate" );
动态注册: 通过 RegisterNatives 在 JNI_OnLoad 中注册,无具名符号,更难定位。
JNINativeMethod methods[] = { {"calculate" , "(II)I" , (void *)native_calculate}, {"encrypt" , "([B)[B" , (void *)native_encrypt}, }; (*env)->RegisterNatives(env, clazz, methods, sizeof (methods)/sizeof (methods[0 ]));
五.2 Hook RegisterNatives 获取方法映射 #include <jni.h> #include <pthread.h> typedef jint (*RegisterNatives_t) (JNIEnv*, jclass, const JNINativeMethod*, jint) ;static RegisterNatives_t original_RegisterNatives = NULL ;jint hooked_RegisterNatives (JNIEnv* env, jclass clazz, const JNINativeMethod* methods, jint nMethods) { jclass class_class = (*env)->GetObjectClass(env, (jobject)clazz); jmethodID getName = (*env)->GetMethodID(env, class_class, "getName" , "()Ljava/lang/String;" ); jstring className = (jstring)(*env)->CallObjectMethod(env, (jobject)clazz, getName); const char * class_name = (*env)->GetStringUTFChars(env, className, NULL ); for (int i = 0 ; i < nMethods; i++) { printf ("[RegisterNatives] %s.%s → %p\n" , class_name, methods[i].name, methods[i].fnPtr); } (*env)->ReleaseStringUTFChars(env, className, class_name); return original_RegisterNatives(env, clazz, methods, nMethods); } MSInitialize { void * handle = dlopen("libart.so" , RTLD_NOW); if (handle) { void * register_natives = dlsym(handle, "RegisterNatives" ); if (register_natives) { MSHookFunction(register_natives, (void *)hooked_RegisterNatives, (void **)&original_RegisterNatives); } dlclose(handle); } }
五.3 JNI_OnLoad 中的 Hook 示例 #include <jni.h> jint JNI_OnLoad (JavaVM* vm, void * reserved) { JNIEnv* env; (*vm)->GetEnv(vm, (void **)&env, JNI_VERSION_1_6); jclass targetClass = (*env)->FindClass(env, "com/example/MyClass" ); jmethodID methodId = (*env)->GetMethodID(env, targetClass, "myMethod" , "()V" ); void * native_addr = dlsym(RTLD_DEFAULT, "Java_com_example_MyClass_myMethod" ); if (native_addr) { MSHookFunction(native_addr, (void *)hook_func, (void **)&original_func); } return JNI_VERSION_1_6; }
六、高级技术:多线程安全的 Inline Hook 六.1 并发问题 当 Hook 正在安装时,目标函数可能正在被其他线程执行。如果恰好在线程执行到被修改的指令位置时发生指令替换,会导致不可预知的 crash。
Cydia Substrate 没有很好地处理这个问题(这是 Frida 后来改进的地方),但理解其原理很重要:
void safe_mshookfunction (void * target, void * hook, void ** old) { suspend_all_threads(); for each thread: if (thread->pc in range(target, target+overwritten_size)): single_step(thread); do_inline_hook(target, hook, old); flush_cache(); resume_all_threads(); }
Frida 的 Interceptor.attach 内部使用了类似的机制(通过 Gum 运行时实现),并进一步优化了暂停策略以降低性能影响。
六.2 递归 Hook 保护 当 Hook 函数内部可能触发对原始函数的调用时需要防止无限递归:
static __thread int hook_depth = 0 ;int hooked_func (int arg) { if (hook_depth > 0 ) { return original_func(arg); } hook_depth++; printf ("[Hook] arg=%d\n" , arg); int result = original_func(arg); printf ("[Hook] result=%d\n" , result); hook_depth--; return result; }
七、Cydia Substrate vs Frida vs Xposed / Dobby 七.1 框架对比
特性
Cydia Substrate
Frida
Dobby
Xposed
Hook 层级
Native 层
Native + Java 层
Native 层
Java 层
持久化
是(重启有效)
否(需重新 attach)
可集成到 so
是
脚本语言
C/C++
JavaScript
C/C++
Java
Android 兼容
4.0 - 7.x(已过时)
4.2 - 14(持续更新)
5.0 - 14
5.0 - 14(LSPosed)
灵活度
低(需编译 so)
高(动态注入 JS)
中(嵌入 so)
中(需编译 APK)
线程安全
不完善
完善(safepoint)
一般
N/A
x86 支持
有限
完整
完整
N/A
ARM64 支持
有限
完整
完整
通过 ART
仓库
cydia.saurik.com
frida.re
github.com/jmpews/Dobby
-
七.2 Dobby:Cydia Substrate 的现代替代品 Dobby(https://github.com/jmpews/Dobby)是当前最推荐的轻量级 inline hook 框架:
#include "dobby.h" static int (*orig_func) (int a, int b) ;int fake_func (int a, int b) { printf ("Hook! a=%d, b=%d\n" , a, b); return orig_func(a, b); } void install_hook () { DobbyHook((void *)target_func, (void *)fake_func, (void **)&orig_func); }
Dobby 的优势:
原生支持 ARM32、AArch64、x86、x86_64
支持符号查找(通过 MachO/ELF 解析)
指令修复使用全功能 assembler,而非简单的指令拷贝
支持 Near Branch Tampoline(更小的跳转开销)
活跃维护,持续兼容新 Android 版本
八、Substrate 模块开发完整示例 八.1 项目结构 my_substrate_module/ ├── Android.mk ├── Application.mk ├── main.c # 模块入口 ├── hook_file.c # 文件操作 Hook ├── hook_network.c # 网络操作 Hook ├── hook_jni.c # JNI 方法 Hook ├── util.c # 工具函数 └── util.h
八.2 Android.mk 配置 LOCAL_PATH := $(call my-dir ) include $(CLEAR_VARS) LOCAL_MODULE := my_module.cy LOCAL_SRC_FILES := main.c hook_file.c hook_network.c hook_jni.c util.c LOCAL_LDLIBS := -llog -lsubstrate LOCAL_CFLAGS := -Wall -O2 -fvisibility=hidden include $(BUILD_SHARED_LIBRARY)
八.3 完整模块代码 #include <substrate.h> #include <android/log.h> #include <dlfcn.h> #include <stdio.h> #include <string.h> #include <stdlib.h> #define TAG "SubstrateModule" #define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, TAG, __VA_ARGS__) #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__) static FILE* (*original_fopen)(const char *, const char *) = NULL ;FILE* hooked_fopen (const char * path, const char * mode) { if (path) { LOGD("fopen: %s (mode=%s)" , path, mode); } return original_fopen(path, mode); } static int (*original_connect) (int , const struct sockaddr*, socklen_t ) = NULL ;int hooked_connect (int fd, const struct sockaddr* addr, socklen_t len) { if (addr && addr->sa_family == AF_INET) { struct sockaddr_in * sin = (struct sockaddr_in*)addr; LOGD("connect: %s:%d" , inet_ntoa(sin ->sin_addr), ntohs(sin ->sin_port)); } return original_connect(fd, addr, len); } static long (*original_ptrace) (int , pid_t , void *, void *) = NULL ;long hooked_ptrace (int request, pid_t pid, void * addr, void * data) { LOGD("ptrace(request=0x%x, pid=%d)" , request, pid); if (request == 0 ) { LOGD("Application called PTRACE_TRACEME (anti-debugging)" ); } return original_ptrace(request, pid, addr, data); } MSInitialize { LOGD("Module initializing..." ); char version[PROP_VALUE_MAX]; if (__system_property_get("ro.build.version.sdk" , version)) { LOGD("Android SDK version: %s" , version); } MSHookFunction((void *)fopen, (void *)hooked_fopen, (void **)&original_fopen); MSHookFunction((void *)connect, (void *)hooked_connect, (void **)&original_connect); MSHookFunction((void *)ptrace, (void *)hooked_ptrace, (void **)&original_ptrace); LOGD("Module initialized successfully" ); }
九、如何检测和对抗 Cydia Substrate 九.1 检测 Substrate 存在 bool detect_substrate_by_maps () { FILE* fp = fopen("/proc/self/maps" , "r" ); char line[512 ]; while (fgets(line, sizeof (line), fp)) { if (strstr (line, "substrate" ) || strstr (line, "libsubstrate" ) || strstr (line, ".cy.so" )) { fclose(fp); return true ; } } fclose(fp); return false ; } bool detect_inline_hook_by_prologue () { extern void __start_of_text[]; static const uint8_t expected_prologue[] = { 0x2d , 0xe9 , 0xf0 , 0x4f }; void * func_addr = (void *)my_important_function; if (memcmp (func_addr, expected_prologue, sizeof (expected_prologue)) != 0 ) { LOGE("Function prologue modified - inline hook detected!" ); return true ; } return false ; } bool detect_substrate_by_symbol () { void * handle = dlopen("libsubstrate.so" , RTLD_NOLOAD); if (handle) { void * MSHookFunction_ptr = dlsym(handle, "MSHookFunction" ); dlclose(handle); return MSHookFunction_ptr != NULL ; } return false ; }
九.2 对抗 Substrate Hook static int (*safe_compute) (int ) = NULL ;void __attribute__((constructor)) save_original_funcs() { safe_compute = (int (*)(int ))dlsym(RTLD_DEFAULT, "compute" ); } void restore_inline_hook (void * func) { } int my_open (const char * path, int flags, mode_t mode) { #ifdef __aarch64__ register long x8 asm ("x8" ) = __NR_openat; register long x0 asm ("x0" ) = AT_FDCWD; register long x1 asm ("x1" ) = (long )path; register long x2 asm ("x2" ) = flags; register long x3 asm ("x3" ) = mode; asm volatile ("svc #0" : "=r" (x0) : "r" (x8), "r" (x0), "r" (x1), "r" (x2), "r" (x3) : "memory" ) ; return (int )x0; #else return open(path, flags, mode); #endif }
十、AOSP 相关源码导读 理解 Android 底层有助于掌握 Native Hook 的边界条件和限制:
模块
源码路径
关键内容
Bionic libc
/bionic/libc/
ptrace, dlopen, fopen 等实现
Dynamic Linker
/bionic/linker/linker.cpp
so 加载流程, .init_array 执行
ART Runtime
/art/runtime/
dex2oat, JNI 桥接, 方法调用
Debuggerd
/system/core/debuggerd/
调试器附加流程
Process
/system/core/libprocessgroup/
/proc 信息读取
关键源码片段 linker 中的 .init_array 执行(/bionic/linker/linker.cpp):
void soinfo::call_constructors () { if (dynamic != nullptr ) { for (ElfW (Dyn)* d = dynamic; d->d_tag != DT_NULL; ++d) { if (d->d_tag == DT_INIT_ARRAY) { ElfW (Addr)* array = reinterpret_cast <ElfW (Addr)*>(d->d_un.d_ptr); size_t count = ... for (size_t i = 0 ; i < count; ++i) { reinterpret_cast <void (*)()>(array[i] + load_bias)(); } } } } }
面试常考问题 Q1:inline hook 的原理是什么?有哪些实现方式?
A:inline hook 通过修改目标函数的开头指令,替换为跳转指令跳向 Hook 函数。实现方式根据指令集不同分为:(1)ARM32:使用 LDR PC, [PC, #-4] 或 LDR R0, [PC, #4]; BX R0;(2)AArch64:使用 LDR X16, #8; BR X16 或 B <offset>;(3)x86/x86_64:使用 JMP rel32 或 MOV RAX, addr; JMP RAX。
关键挑战包括:(1)需保存并恢复被覆盖的指令(通过 trampoline 机制);(2)需处理 ARM Thumb/ARM 模式切换(地址最低位为 1 表示 Thumb 模式);(3)需刷新指令缓存(cacheflush 或 __builtin___clear_cache)确保处理器看到新指令;(4)需处理多线程并发(防止 Hook 安装时其他线程执行到被修改区域);(5)需正确修复 trampoline 中 PC 相对寻址指令。
常见实现:MSHookFunction(Substrate)、Interceptor(Frida/Gum)、DobbyHook(Dobby)、bhook(ByteDance)。
Q2:Cydia Substrate 已停止维护,为什么还要学习它?
A:(1)理解 inline hook 原理是 Native 逆向的基础,Cydia Substrate 的代码是最清晰的参考实现之一,其源码量小、逻辑清晰,是学习 inline hook 技术的最佳教材;(2)很多旧版加固方案仍使用 Substrate 进行对抗(如旧版 360 加固、梆梆加固的内部测试模块);(3)它的架构设计(模块化、持久化、跨进程注入)影响了后来的 Hook 框架,理解它有助于理解 Frida Gum、Dobby 等的设计演进;(4)在分析一些 2016-2018 年的旧应用时可能直接遇到 Substrate 的模块代码(.cy.so 文件),需要能读懂其 Hook 逻辑;(5)Substrate 中的指令修复(instruction fixup)逻辑是 ARM 体系结构学习的绝佳实战案例。
Q3:Hook 一个被混淆的 so 中的函数,如何确定函数地址?
A:方法一:使用 IDA Pro / Ghidra 静态分析 so 的导出表和 String Xref,找到目标函数偏移,运行时加上 so 的基址(从 /proc/self/maps 获取)得到绝对地址。方法二:Frida 中调用 Module.findExportByName 或 Module.getExportByName 获取地址。方法三:通过 JNI 方法签名反向查找——Hook RegisterNatives 获取 Java 方法到 Native 函数的映射表。方法四:通过特征字节码(函数序言如 ARM32 的 PUSH {R4-R11,LR} 即 2D E9 F0 4F 或 AArch64 的 STP X29, X30, [SP, #-0x...]!)在 memory range 中搜索定位。方法五:对于启用符号表的 so(即使被 strip 了 .symtab,.dynsym 中的导出符号仍然存在),直接通过 dlsym 查找。
Q4:trampoline(跳板)在 inline hook 中的作用是什么?如何构造?
A:trampoline 是 inline hook 的核心组件,其作用是”衔接”原始函数和 Hook 函数。构造过程:分配一段可执行内存 → 写入被覆盖的原始指令 → 对所有 PC 相对寻址的指令进行修复(将相对偏移调整为从 trampoline 位置计算的偏移)→ 添加跳转指令跳回原始函数被覆盖指令之后的位置 → 可选地在前面插入对 Hook 回调函数的调用逻辑。
具体修复技术举例:ARM32 中 B <offset> 指令(条件跳转),其偏移是从当前 PC 算起的。当这条指令被复制到 trampoline 后,目标地址不变但 PC 变了,需要重新计算偏移量。对于 BL(带链接的跳转,用于函数调用),修复时要确保 LR 寄存器的操作语义正确。AArch64 中的 ADRP + ADD(地址生成对)也需要重新计算 page 偏移。
Q5:如果目标函数的前几条指令中存在一条向后跳转的 B 指令(例如循环头),MSHookFunction 如何处理?
A:这是 inline hook 中最棘手的情况之一。当一个向后跳转的 B 指令被复制到 trampoline,其跳转目标(原始函数开头范围内)被我们修改为跳转指令,直接跳回去将导致不可预期的行为(可能再次进入 Hook 函数,形成无限循环;或执行到错误的指令)。
解决方案有几种:(1)增加备份指令的数量,确保覆盖整个”危险区域”——如果向后跳转的目标在备份范围内,则备份到跳转目标之后;(2)使用指令重编译(reassemble):将跳转指令替换为等效的、跳转到 trampoline 副本中对应位置的版本;(3)对于短循环,完全在 trampoline 中展开循环(unroll),但这会显著增加 trampoline 大小;(4)Frida 的 Interceptor 采用更激进的策略:使用 Capstone 反汇编引擎分析每条指令,精确修复所有控制流指令。
这也是为什么简单的 MSHookFunction 在某些极端情况下会失败的深层原因——它假设函数开头是可”安全备份”的线性代码。对于经过 OLLVM 混淆或人工精心构造的”反 inline hook”函数(入口处就是向后跳的循环),简单的指令备份+修复方案会直接崩溃。