目录
  1. 1. 一、Cydia Substrate 简介
    1. 1.1. 一.1 框架架构概览
    2. 1.2. 一.2 模块生命周期
  2. 2. 二、Inline Hook 原理深度剖析
    1. 2.1. 二.1 ARM 指令模式:ARM vs Thumb
    2. 2.2. 二.2 Inline Hook 的经典实现步骤
    3. 2.3. 二.3 MSHookFunction 内部实现解析
    4. 2.4. 二.4 ARM64 (AArch64) 下的 Inline Hook
    5. 2.5. 二.5 指令缓存刷新(cache coherency)
  3. 3. 三、核心 API 详解
    1. 3.1. 三.1 MSHookFunction
    2. 3.2. 三.2 MSHookMemory
    3. 3.3. 三.3 MSHookMessageEx(iOS 为主)
    4. 3.4. 三.4 MSInitialize 宏
  4. 4. 四、实战:Hook libc 函数
    1. 4.1. 四.1 Hook fopen 监控文件访问
    2. 4.2. 四.2 Hook dlopen 监控 so 加载
    3. 4.3. 四.3 Hook dlsym 监控函数解析
    4. 4.4. 四.4 Hook connect 监控网络连接
  5. 5. 五、Hook JNI 方法
    1. 5.1. 五.1 JNI 方法 Hook 的挑战
    2. 5.2. 五.2 Hook RegisterNatives 获取方法映射
    3. 5.3. 五.3 JNI_OnLoad 中的 Hook 示例
  6. 6. 六、高级技术:多线程安全的 Inline Hook
    1. 6.1. 六.1 并发问题
    2. 6.2. 六.2 递归 Hook 保护
  7. 7. 七、Cydia Substrate vs Frida vs Xposed / Dobby
    1. 7.1. 七.1 框架对比
    2. 7.2. 七.2 Dobby:Cydia Substrate 的现代替代品
  8. 8. 八、Substrate 模块开发完整示例
    1. 8.1. 八.1 项目结构
    2. 8.2. 八.2 Android.mk 配置
    3. 8.3. 八.3 完整模块代码
  9. 9. 九、如何检测和对抗 Cydia Substrate
    1. 9.1. 九.1 检测 Substrate 存在
    2. 9.2. 九.2 对抗 Substrate Hook
  10. 10. 十、AOSP 相关源码导读
    1. 10.1. 关键源码片段
  11. 11. 面试常考问题
【逆向安全技术-工具篇】Native层Hook神器Cydia Substrate

一、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) {
// 1. 获取目标地址的页信息
uintptr_t page_start = (uintptr_t)symbol & ~(PAGE_SIZE - 1);
size_t page_offset = (uintptr_t)symbol - page_start;

// 2. 修改页属性为可写可执行
mprotect((void*)page_start, PAGE_SIZE,
PROT_READ | PROT_WRITE | PROT_EXEC);

// 3. 确定指令模式(ARM/Thumb)
bool thumb_mode = ((uintptr_t)symbol & 1);
uintptr_t real_addr = (uintptr_t)symbol & ~1;

// 4. 备份原始指令(至少 2 条 ARM 或 4 字节 Thumb)
// 确保备份的指令边界完整
size_t backup_size = thumb_mode ? backup_thumb(real_addr, 2)
: backup_arm(real_addr, 2);

// 5. 分配 trampoline 内存(可执行 + 可写)
void* trampoline = mmap(NULL, 4096,
PROT_READ | PROT_WRITE | PROT_EXEC,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);

// 6. 写入 trampoline 代码
// [备份的指令] + [跳回原始函数] + 调用用户回调
write_trampoline(trampoline, backup_buf, backup_size,
real_addr + backup_size, hook, old);

// 7. 写入跳转指令到目标函数入口
write_jump(real_addr, trampoline, thumb_mode);

// 8. 刷新指令缓存
cacheflush(page_start, PAGE_SIZE, 0);

// 9. 恢复页属性
mprotect((void*)page_start, PAGE_SIZE,
PROT_READ | PROT_EXEC);

// 10. 保存原始函数指针(指向 trampoline 中可调用的入口)
*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)同步:

// Android Bionic 提供的函数(<unistd.h> 或 <sys/cachectl.h>)
// ARM32:
void cacheflush(long addr, long nbytes, long cache);

// ARM64:
// 使用 __builtin___clear_cache (GCC/Clang builtin)
__builtin___clear_cache((char*)start, (char*)end);

// 通用方案(Cydia Substrate 内部实际使用):
#if defined(__arm__)
cacheflush((long)addr, (long)size, 0);
#elif defined(__aarch64__)
// AArch64 使用 DC CVAU + IC IVAU + DSB + ISB
__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;

// Hook 替换函数
int hooked_func(const char* str, int flags) {
// before: 查看/修改参数
printf("[Hook] Called with: %s, flags=%d\n", str, flags);

// 调用原始函数
int result = original_func(str, flags);

// after: 查看/修改返回值
printf("[Hook] Return value: %d\n", result);

return result; // 可以修改返回值
}

// 安装 Hook(在 so 加载时调用)
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 内存中的任意数据:

// 将内存中的某个整数 const 修改为新值
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 为主)

// 主要用于 hook Objective-C 消息(iOS),Android 上较少使用
MSHookMessageEx(
[targetClass class],
@selector(targetMethod:),
imp_implementationWithBlock(replacementBlock),
(IMP*)&original_imp
);

三.4 MSInitialize 宏

MSInitialize 是每个 Substrate 模块的入口点,被定义为 __attribute__((constructor)) 函数:

// MSInitialize 实际上展开为:
// static void __attribute__((constructor)) _substrate_init_##__LINE__()
//
// 等价于:
static void __attribute__((constructor)) my_module_init() {
// 在 so 被 dlopen 加载时自动执行
// 此处调用 MSHookFunction 安装所有 Hook
MSHookFunction((void*)target_func, (void*)hook_func, (void**)&orig_func);
}

// Substrate 的宏语法糖:
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)";

// 记录日志(生产环境可用 __android_log_print)
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 {
// fopen 通常由 libc.so 导出
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));

// 阻止连接特定 IP
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 直接获取地址。

// 静态注册的 JNI 方法:可直接通过符号名获取
// 函数名为: Java_com_example_app_NativeLib_calculate
void* jni_addr = dlsym(RTLD_DEFAULT,
"Java_com_example_app_NativeLib_calculate");

动态注册: 通过 RegisterNativesJNI_OnLoad 中注册,无具名符号,更难定位。

// 动态注册:在 JNI_OnLoad 中注册
// 需要通过 hook RegisterNatives 截获映射关系
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);

// 记录每个注册的方法及其 Native 地址
for (int i = 0; i < nMethods; i++) {
printf("[RegisterNatives] %s.%s → %p\n",
class_name, methods[i].name, methods[i].fnPtr);

// 此时可以对 methods[i].fnPtr 进行 inline hook
// 但要注意:hook 函数和原始函数签名必须完全匹配
}

(*env)->ReleaseStringUTFChars(env, className, class_name);
return original_RegisterNatives(env, clazz, methods, nMethods);
}

MSInitialize {
// dlsym 获取 JNI 的 RegisterNatives 函数地址
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>

// JNI_OnLoad 中实现
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");

// 获取 native 方法的地址
// 方法1:通过函数名约定
void* native_addr = dlsym(RTLD_DEFAULT,
"Java_com_example_MyClass_myMethod");

// 方法2:遍历 .dynsym section 查找
// (需要自行解析 ELF)

// 方法3:通过对 .text 段的特征码搜索定位

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 后来改进的地方),但理解其原理很重要:

// 线程安全的 inline hook 伪实现
void safe_mshookfunction(void* target, void* hook, void** old) {
// 1. 暂停所有其他线程(类似 GC 的 safepoint)
suspend_all_threads();

// 2. 确保没有线程的 PC 落在将被修改的指令范围内
for each thread:
if (thread->pc in range(target, target+overwritten_size)):
// 单步执行该线程,使其走出危险区域
single_step(thread);

// 3. 安装 hook
do_inline_hook(target, hook, old);

// 4. 刷新指令缓存
flush_cache();

// 5. 恢复所有线程
resume_all_threads();
}

Frida 的 Interceptor.attach 内部使用了类似的机制(通过 Gum 运行时实现),并进一步优化了暂停策略以降低性能影响。

六.2 递归 Hook 保护

当 Hook 函数内部可能触发对原始函数的调用时需要防止无限递归:

// 使用 thread-local storage 防止递归
static __thread int hook_depth = 0;

int hooked_func(int arg) {
if (hook_depth > 0) {
// 已经在 Hook 中,直接调用原始函数避免递归
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 框架:

// Dobby 用法(类似 MSHookFunction 但更现代)
#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 会自动处理 ARM/Thumb/AArch64 模式
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 完整模块代码

// main.c - Substrate 模块主文件
#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) {
// 记录所有 ptrace 调用
LOGD("ptrace(request=0x%x, pid=%d)", request, pid);

// 如果应用自身尝试 ptrace(PTRACE_TRACEME),记录但放行
if (request == 0) { // PTRACE_TRACEME
LOGD("Application called PTRACE_TRACEME (anti-debugging)");
}

return original_ptrace(request, pid, addr, data);
}

// ====== 模块初始化 ======
MSInitialize {
LOGD("Module initializing...");

// 检查 Android 版本
char version[PROP_VALUE_MAX];
if (__system_property_get("ro.build.version.sdk", version)) {
LOGD("Android SDK version: %s", version);
}

// 安装文件监控 Hook
MSHookFunction((void*)fopen, (void*)hooked_fopen,
(void**)&original_fopen);

// 安装网络监控 Hook
MSHookFunction((void*)connect, (void*)hooked_connect,
(void**)&original_connect);

// 安装 ptrace 监控 Hook
MSHookFunction((void*)ptrace, (void*)hooked_ptrace,
(void**)&original_ptrace);

LOGD("Module initialized successfully");
}

九、如何检测和对抗 Cydia Substrate

九.1 检测 Substrate 存在

// 检测 1:检查 /proc/self/maps 中是否加载了 Substrate 相关 so
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;
}

// 检测 2:检查自身函数是否被 inline hook 修改
// 比较函数入口指令与编译时记录的期望值
bool detect_inline_hook_by_prologue() {
extern void __start_of_text[];

// 特定函数序言的特征字节(编译时确定)
static const uint8_t expected_prologue[] = {
0x2d, 0xe9, 0xf0, 0x4f // PUSH {R4-R11,LR}
};

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;
}

// 检测 3:通过函数指针遍历检查
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

// 对抗方法 1:在 hook 之前保存原始函数指针
static int (*safe_compute)(int) = NULL;

void __attribute__((constructor)) save_original_funcs() {
// 在 Substrate 加载之前保存关键函数地址
// 使用 .init_array 优先级控制(优先级高于 Substrate 模块)
safe_compute = (int(*)(int))dlsym(RTLD_DEFAULT, "compute");
}

// 对抗方法 2:运行时恢复被修改的指令
void restore_inline_hook(void* func) {
// 从应用包内的备份 so 中读取原始指令
// 重新写入到目标函数入口
// 刷新指令缓存
}

// 对抗方法 3:通过直接系统调用绕过 libc hook
int my_open(const char* path, int flags, mode_t mode) {
// 直接使用 svc 指令(避免 libc 层的 hook)
// ARM32: SVC 0(syscall number in R7)
// AArch64: SVC 0(syscall number in X8)
#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):

// soinfo::call_constructors() 中遍历 .init_array
void soinfo::call_constructors() {
// ...
// DT_INIT_ARRAY
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 = ... // from DT_INIT_ARRAYSZ
for (size_t i = 0; i < count; ++i) {
// 调用每个 constructor,这是 MSInitialize 被调用的地方
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 X16B <offset>;(3)x86/x86_64:使用 JMP rel32MOV 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.findExportByNameModule.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”函数(入口处就是向后跳的循环),简单的指令备份+修复方案会直接崩溃。

打赏
  • 微信
  • 支付宝

评论