运行时劫持与插桩

概念总览(一句话)

运行时修改就是在程序运行时改变它的行为——通过替换函数入口、拦截调用、修改内存数据或插入中间层——而不去改磁盘上的二进制文件。常见手段:注入(inject)拦截/Hook重定向(LD_PRELOAD/PLT/GOT)内存打补丁(inline patch)虚拟文件/挂载覆盖(systemless)代理层

  1. 运行时修补 / 运行时打补丁(Runtime Patching / Hot-patching)

    • 强调“在程序运行时修改行为而不改磁盘文件”。适合描述 inline hook、内存打补丁、trampoline 等。
  2. Hooking / 劫持(Hooking / Function Hooking)

    • 强调“拦截函数调用并替换/包裹它们”。覆盖 LD_PRELOAD、PLT/GOT 改写、Java/ART 方法替换、Frida 的 method hook 等。
  3. 代码注入 / 进程注入(Code Injection / Process Injection)

    • 强调“把自定义代码或库加载到目标进程”。例如 remote thread、动态加载 so、Frida agent、DLL 注入 等。
  4. 二进制插桩 / 动态插桩 / 动态二进制翻译(Instrumentation / Dynamic Binary Instrumentation)

    • 强调“在运行时插入监测/修改逻辑以观测或控制程序行为”。代表工具:Frida、DynamoRIO、PIN。
  5. 调试与进程控制(Debugging / ptrace-based Manipulation)

    • 指通过 ptrace / gdbserver 等调试 API 暂停、读写内存、设置断点等操作(这类既用于调试也能用于修改运行时状态)。

一、为什么可行(基本原理)

  1. 程序运行时是内存中的一堆可修改数据:代码段、数据段、堆、栈、内核结构。操作系统把这些映射到进程地址空间。
  2. 调试/注入/ptrace 等机制允许一个外部实体读取/写入另一个进程的内存、寄存器,或让目标进程执行指定代码。
  3. 链接与调用不是“不可变”的:函数调用通过跳转表、PLT/GOT、虚表(C++)、JNI 层、方法表(Java/ART)等实现,这些表通常可以被替换或拦截。
  4. 现代系统虽有保护(W^X、ASLR、DEP、签名、SELinux),但仍存在受控方式实现运行时修改(前提是有足够权限或利用已允许的调试接口)。

二、主流技术与原理(按常见/安全实践分类)

1) 动态库注入 + 函数替换(LD_PRELOAD、DYLD_INSERT_LIBRARIES)

原理:在程序启动时(或通过特定注入方式)把自制共享库放入动态链接器的搜索/加载顺序前面,重写某些符号(函数),从而“劫持”原函数调用。

  • 适用:Linux、Android(glibc/ld.so)、macOS(dyld)。
  • 优点:实现简单,源代码级替换友好,适合拦截 libc 接口(open/read/socket)。
  • 限制:只能在动态加载且遵循动态链接流程的函数上生效;对静态链接或早期初始化代码无效。需要在进程启动阶段生效(或通过注入让 ld 加载库)。
  • 简短示例(概念):实现一个替代 open 的函数,打印路径再调用真实 open

2) PLT/GOT 替换(进程内表项修改)

原理:ELF 可执行/共享对象通过 PLT(过程链接表)与 GOT(全局偏移表)实现函数动态解析。修改 GOT 中指向函数的地址,就能把调用重定向到你的函数。

  • 优点:比 LD_PRELOAD 更“细粒度”,可在运行时对已加载模块生效。
  • 限制:需要可写性(GOT 可能被设为只读,需要改变页权限);对内联或静态链接无效。

3) Inline Hook / Trampoline(指令替换)

原理:在目标函数入口写入一条跳转指令(jmp/bx/…)到你的“hook”实现;你的实现完成后通常要跳回被替换指令后的地址(trampoline)。

  • 优点:可拦截任意可执行地址(包括 native 本地函数)。

  • 技术点:

    • 需要把代码页设为可写(mprotect),然后写入跳转指令,再恢复执行权限。
    • 需要保存被覆盖的原始指令到 trampoline 中以便返回。
  • 风险:如果写错会导致崩溃;在执行被覆盖位置的其他线程可能竞态;有防篡改/代码校验的程序会检测到。

4) 硬件断点 / 观察点(CPU 调试寄存器)

原理:使用 CPU 的硬件断点(Debug Registers)来在某个地址或内存访问事件发生时触发异常,由调试器处理。

  • 优点:不修改目标内存代码;能监控内存读/写/执行。
  • 限制:数量有限(例如 x86 通常只有 4 个);需要 ptrace / gdbserver 等权限。

5) ptrace / Debug API(进程附加控制)

原理:Unix 的 ptrace 系统调用(或 Windows 的 Debug APIs)允许一个“父”进程附加到目标进程、暂停它、读写内存/寄存器、接收信号并控制执行(单步/继续)。

  • 用法:调试器(gdb/gdbserver)通常就是通过 ptrace 实现断点、读写内存、单步等。
  • 能做的:把代码页修改为可写、打软断点(写入 trap 指令)、注入寄存器值让目标跑到你放的 shellcode 等。
  • 注意:ptrace 操作需要权限(通常 root 或相同 UID),并且可以被目标检测(TracerPid)。

6) 代码注入 / DLL 注入 / Remote Thread(创建远程线程)

原理(Windows 举例):在目标进程内分配内存、写入库路径或 shellcode,然后通过 CreateRemoteThread 调用 LoadLibrary 或执行 shellcode,使目标进程加载你的代码。Linux 下等价是通过 ptrace 或 injectso 类工具。

  • 优点:能把任意逻辑注入目标进程。
  • 风险与限制:权限要求高;容易被安全软件检测。

7) Java/ART 层 Hook(Xposed / Hook 框架 / Method Swizzling)

原理(托管语言):拦截虚拟机层的调用表或替换方法表项(method pointers),或在类加载/方法解析阶段插入代理。

  • Android:Xposed、LSPosed、Frida 的 Java.perform() 能在 ART 层 hook Java 方法。
  • 优点:source-like 的 hook,易读易写,适合高层逻辑修改。
  • 限制:需要平台支持(例如 root/Xposed),或使用 Frida 注入运行时脚本。

8) 动态二进制插桩 / 动态翻译(Frida / DynamoRIO / PIN)

原理:在进程内部插入一个运行时代理(或替代执行引擎),在指令执行时逐条或按块地插入钩子逻辑(instrumentation),可以在任意点拦截行为。

  • 工具:Frida(脚本化 API,方便)、DynamoRIO、Intel PIN。
  • 优点:非常灵活,可做函数级、指令级或内存访问级别的动态分析/修改。
  • 缺点:性能开销,且通常需注入 agent。

9) 虚拟文件系统 / Overlay(systemless)

原理:通过挂载层(overlay、bind mount、magisk 的虚拟挂载等)来“替换”文件而不改原始镜像。对程序来说,某些文件看起来被修改了,但磁盘上的真实文件未变。

  • 典型用途:Magisk 的 systemless 模块就是这种思路,避免修改 /system 分区直接影响 OTA 签名判断。

10) 代理/中间人(network proxy / shim)

原理:不在程序内部改动,而在进出程序的边界(网络、IPC、库)放代理或替身:例如用代理替换远端服务、或用包装库替换系统库。

  • 优点:不涉及内存补丁,风险较低。
  • 示例:用 mitmproxy 做 HTTPS 拦截,或用 LD_PRELOAD 写一个替身库代理某个系统调用。

三、实现时必须注意的系统防护与问题

  • W^X / DEP / NX:代码页默认不可写,这要求在写入代码之前调用 mprotect 改变权限。
  • ASLR:地址随机化导致地址不可预测,需要解析 /proc//maps 或利用符号解析来定位函数地址。
  • 签名校验 / 完整性检查:某些程序会校验自身代码或校验加载的库,运行时篡改可能被检测到。
  • SELinux / 沙盒 / 权限模型:在 Android 上,普通应用很难跨应用 attach 或访问 /data,通常需要 root 或调试签名权限。
  • 多线程竞态:在替换代码时,其他线程可能正在执行被修改函数,需小心同步(常用的方法是先暂停线程或用原子替换并使用 trampolines)。
  • 保护机制/反调试:Target 可能检测 TracerPid、时间延迟、代码页被修改等并采取反制。

四、示例(高层安全示意,不是绕过保护脚本)

LD_PRELOAD 概念示例(Linux,伪代码)

1
2
3
4
5
6
7
8
9
10
// mypreload.c
#include <stdio.h>
#include <dlfcn.h>
typedef int (*open_t)(const char*, int, ...);
int open(const char *path, int flags, ...) {
static open_t real_open = NULL;
if (!real_open) real_open = (open_t)dlsym(RTLD_NEXT, "open");
printf("open called: %s\n", path);
return real_open(path, flags);
}

把编译成 libmypreload.so,在启动目标时 LD_PRELOAD=./libmypreload.so ./target。(说明:若目标受保护或是 suid 程序,LD_PRELOAD 被忽略)

Frida Hook Java(高层示例)

1
2
3
4
5
6
7
8
// frida script
Java.perform(function() {
var MyClass = Java.use('com.example.MyClass');
MyClass.myMethod.implementation = function(arg) {
console.log('myMethod called, arg:', arg);
return this.myMethod(arg); // 可选择修改参数/返回值
};
});

Frida 需要注入 agent 到进程,适合做动态分析与研究。

五、如何安全/合规地使用这些技术

  • 只在你有权限受控实验环境中使用(你的设备、开源/练习的应用、或得到书面授权的目标)。
  • 不要用来绕过支付/版权/安全保护或未经授权地访问他人数据。
  • 在公司/组织做测试时,走授权流程并写测试计划,保留日志与回滚方案。

六、学习路线建议(实践但合规)

  1. 从 LD_PRELOAD / wrapper 库 与 Frida 的简单 hook 开始(在你自己的示例程序上练习)。
  2. 学习 ELF/PE 的符号解析与 PLT/GOT 机制,再做 GOT 重写实验。
  3. 学习 ptrace/gdbserver 的工作流(以调试为主,不做滥用)。
  4. 理解 inline hook / trampoline 的实现原理并在本地构造小 demo(非生产/受保护程序)。
  5. 了解现代防护(ASLR、DEP、签名、SELinux)如何限制这些技术。