Native 层 Hook 技巧 (Native Hooking Patterns)

Native 层 Hook 技巧 (Native Hooking Patterns)

📚 前置知识

本配方涉及以下核心技术,建议先阅读相关章节:

在 Android 逆向中,Native 层 (C/C++) 的分析往往比 Java 层更具挑战性。Hook 标准 C 库 (libc) 函数是理解 Native 层行为、脱壳和还原算法的重要手段。


1. 文件操作监控 (File I/O)

监控文件操作可以帮助我们发现 App 读取了哪些配置文件、加载了哪些 Dex/So 文件,或者将解密后的数据写入到了哪里。

Hook open / openat

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// Hook open and openat to trace file access
function hookFileOpen() {
  // Intercept 'open'
  var openPtr = Module.findExportByName("libc.so", "open");
  if (openPtr) {
    Interceptor.attach(openPtr, {
      onEnter: function (args) {
        this.path = args[0].readCString();
        this.flags = args[1].toInt32();
      },
      onLeave: function (retval) {
        if (
          this.path &&
          (this.path.endsWith(".dex") || this.path.endsWith(".so"))
        ) {
          console.log("[open] FD: " + retval + " Path: " + this.path);
        }
      },
    });
  }

  // Intercept 'openat' (commonly used on newer android versions)
  var openatPtr = Module.findExportByName("libc.so", "openat");
  if (openatPtr) {
    Interceptor.attach(openatPtr, {
      onEnter: function (args) {
        // args[0] is dirfd, args[1] is path
        this.path = args[1].readCString();
        this.flags = args[2].toInt32();
      },
      onLeave: function (retval) {
        if (
          this.path &&
          (this.path.indexOf("base.apk") >= 0 || this.path.indexOf(".dex") >= 0)
        ) {
          console.log("[openat] FD: " + retval + " Path: " + this.path);
        }
      },
    });
  }
}

hookFileOpen();

2. 动态库加载监控 (dlopen)

监控 dlopen 可以帮助我们发现 App 动态加载了哪些 SO 文件,这对于分析加壳应用非常有用。

Hook dlopen / android_dlopen_ext

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
function hookDlopen() {
  var dlopen = Module.findExportByName(null, "dlopen");
  var android_dlopen_ext = Module.findExportByName(null, "android_dlopen_ext");

  if (dlopen) {
    Interceptor.attach(dlopen, {
      onEnter: function (args) {
        this.path = args[0].readCString();
      },
      onLeave: function (retval) {
        if (this.path) {
          console.log("[dlopen] " + this.path + " -> Handle: " + retval);
          if (this.path.indexOf("libnative-lib.so") >= 0) {
            // Library loaded, ready to hook functions inside it
            console.log("[+] Target library loaded!");
          }
        }
      },
    });
  }

  if (android_dlopen_ext) {
    Interceptor.attach(android_dlopen_ext, {
      onEnter: function (args) {
        this.path = args[0].readCString();
      },
      onLeave: function (retval) {
        if (this.path) {
          console.log(
            "[android_dlopen_ext] " + this.path + " -> Handle: " + retval
          );
        }
      },
    });
  }
}

hookDlopen();

3. 内存操作监控 (memcpy)

监控 memcpy 可以帮助我们发现内存中的数据拷贝,特别是在脱壳时可以捕获解密后的 DEX 文件。

Hook memcpy

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
function hookMemcpy() {
  var memcpy = Module.findExportByName("libc.so", "memcpy");

  Interceptor.attach(memcpy, {
    onEnter: function (args) {
      this.dest = args[0];
      this.src = args[1];
      this.n = args[2].toInt32();
    },
    onLeave: function (retval) {
      // Filter by size or content to reduce noise
      if (this.n > 100 && this.n < 200) {
        // Check if source contains specific magic bytes (e.g., ELF header)
        try {
          var magic = this.src.readU32();
          if (magic == 0x464c457f) {
            // .ELF
            console.log("[memcpy] ELF header detected! Size: " + this.n);
            console.log(hexdump(this.src, { length: 32 }));
          }
        } catch (e) {}
      }

      // Check for DEX magic (dex\n035)
      if (this.n > 1000) {
        try {
          var dexMagic = this.src.readUtf8String(4);
          if (dexMagic === "dex\n") {
            console.log("[memcpy] DEX file detected! Size: " + this.n);
            // Dump DEX file
            var dexData = this.src.readByteArray(this.n);
            var filename = "/data/local/tmp/dump_" + Date.now() + ".dex";
            var file = new File(filename, "wb");
            file.write(dexData);
            file.close();
            console.log("[+] DEX dumped to: " + filename);
          }
        } catch (e) {}
      }
    },
  });
}

hookMemcpy();

4. 符号解析监控 (dlsym)

监控 dlsym 可以帮助我们发现 App 动态查找了哪些函数,这对于分析混淆代码非常有用。

Hook dlsym

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
function hookDlsym() {
  var dlsym = Module.findExportByName(null, "dlsym");

  Interceptor.attach(dlsym, {
    onEnter: function (args) {
      this.handle = args[0];
      this.symbol = args[1].readCString();
    },
    onLeave: function (retval) {
      if (this.symbol) {
        console.log(
          "[dlsym] Symbol: " + this.symbol + " -> Address: " + retval
        );

        // Hook specific functions when resolved
        if (this.symbol === "encrypt" && !retval.isNull()) {
          console.log("[+] Found encrypt function, hooking...");
          hookNativeFunction(retval, "encrypt");
        }
      }
    },
  });
}

function hookNativeFunction(addr, name) {
  Interceptor.attach(addr, {
    onEnter: function (args) {
      console.log("[" + name + "] called");
      console.log("  arg0: " + args[0]);
      console.log("  arg1: " + args[1]);
    },
    onLeave: function (retval) {
      console.log("  retval: " + retval);
    },
  });
}

hookDlsym();

5. 字符串比较监控 (strcmp)

监控字符串比较函数可以帮助我们发现 App 的校验逻辑,如 Root 检测、调试检测等。

Hook strcmp / strstr

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
function hookStringFunctions() {
  // Hook strcmp
  var strcmp = Module.findExportByName("libc.so", "strcmp");
  if (strcmp) {
    Interceptor.attach(strcmp, {
      onEnter: function (args) {
        var s1 = args[0].readCString();
        var s2 = args[1].readCString();

        // Filter for interesting strings
        var keywords = ["root", "su", "magisk", "frida", "xposed", "debug"];
        for (var i = 0; i < keywords.length; i++) {
          if (
            (s1 && s1.toLowerCase().indexOf(keywords[i]) >= 0) ||
            (s2 && s2.toLowerCase().indexOf(keywords[i]) >= 0)
          ) {
            console.log("[strcmp] " + s1 + " VS " + s2);
            break;
          }
        }
      },
    });
  }

  // Hook strstr
  var strstr = Module.findExportByName("libc.so", "strstr");
  if (strstr) {
    Interceptor.attach(strstr, {
      onEnter: function (args) {
        this.haystack = args[0].readCString();
        this.needle = args[1].readCString();
      },
      onLeave: function (retval) {
        if (this.needle && this.needle.toLowerCase().indexOf("frida") >= 0) {
          console.log(
            "[strstr] Searching for: " +
              this.needle +
              " in: " +
              this.haystack.substring(0, 50)
          );
          // Return NULL to bypass detection
          retval.replace(ptr(0));
        }
      },
    });
  }
}

hookStringFunctions();

6. 系统调用监控

监控系统调用可以帮助我们发现 App 的底层行为。

Hook syscall

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
function hookSyscall() {
  var syscall = Module.findExportByName("libc.so", "syscall");

  if (syscall) {
    Interceptor.attach(syscall, {
      onEnter: function (args) {
        var syscallNumber = args[0].toInt32();

        // Common syscall numbers on ARM64
        var syscallNames = {
          56: "openat",
          57: "close",
          63: "read",
          64: "write",
          78: "readlinkat",
          79: "fstatat",
          101: "nanosleep",
          172: "getpid",
          174: "getuid",
        };

        if (syscallNames[syscallNumber]) {
          console.log("[syscall] " + syscallNames[syscallNumber]);
        }
      },
    });
  }
}

hookSyscall();

7. 综合示例:脱壳辅助

结合多个 Hook 点进行脱壳分析。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
// Comprehensive unpacking helper
function unpackHelper() {
  console.log("[+] Unpacking helper started");

  var dexCount = 0;

  // Hook mmap to catch memory mapping
  var mmap = Module.findExportByName("libc.so", "mmap");
  Interceptor.attach(mmap, {
    onEnter: function (args) {
      this.addr = args[0];
      this.length = args[1].toInt32();
      this.prot = args[2].toInt32();
    },
    onLeave: function (retval) {
      if (this.length > 100000 && this.prot == 5) {
        // PROT_READ | PROT_EXEC
        try {
          var magic = retval.readUtf8String(4);
          if (magic === "dex\n") {
            console.log(
              "[mmap] DEX detected! Size: " + this.length + " Address: " + retval
            );
            dexCount++;
            var filename =
              "/data/local/tmp/unpack_" + dexCount + "_" + Date.now() + ".dex";
            var file = new File(filename, "wb");
            file.write(retval.readByteArray(this.length));
            file.close();
            console.log("[+] Dumped to: " + filename);
          }
        } catch (e) {}
      }
    },
  });

  // Hook dvmDexFileOpenPartial (for Dalvik)
  // Hook art::DexFile::Open (for ART)
  var artDexOpen = Module.findExportByName(
    "libart.so",
    "_ZN3art7DexFile10OpenMemoryEPKhjRKNSt3__112basic_stringIcNS3_11char_traitsIcEENS3_9allocatorIcEEEEjPNS_6MemMapEPKNS_7OatFileEPS9_"
  );
  if (artDexOpen) {
    Interceptor.attach(artDexOpen, {
      onEnter: function (args) {
        console.log("[art::DexFile::OpenMemory] called");
        this.base = args[0];
        this.size = args[1].toInt32();
      },
      onLeave: function (retval) {
        if (this.size > 0) {
          console.log("[+] ART DEX loaded, size: " + this.size);
        }
      },
    });
  }
}

unpackHelper();

8. Inline Hook 技术详解

Inline Hook 是 Native 层 Hook 的核心技术,通过直接修改目标函数的机器指令来实现函数拦截。与 GOT/PLT Hook 不同,Inline Hook 可以 Hook 任意函数地址,包括内部函数。

8.1 Inline Hook 原理

Inline Hook 的基本原理是:

  1. 备份原指令:保存目标函数开头的若干字节指令
  2. 写入跳转指令:在原位置写入跳转到 Hook 函数的指令
  3. 执行 Hook 函数:Hook 函数执行自定义逻辑
  4. 执行原函数:通过 Trampoline 跳回执行原始指令
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
原始函数:                      Hook 后:
+------------------+           +------------------+
| 原始指令 1       |  ───>     | JMP hook_func    |
| 原始指令 2       |           | (覆盖原指令)      |
| 原始指令 3       |           | 原始指令 3       |
| ...              |           | ...              |
+------------------+           +------------------+

Hook 函数:                     Trampoline:
+------------------+           +------------------+
| 自定义逻辑       |           | 原始指令 1       |
| call_original()  |  ───>     | 原始指令 2       |
| 返回             |           | JMP 原函数+偏移  |
+------------------+           +------------------+

8.2 ARM64 跳转指令

在 ARM64 架构上,常用的跳转方式:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// 方式1: B 指令 (±128MB 范围)
// B <offset>
// 4 字节,范围有限

// 方式2: LDR + BR 组合 (任意地址)
// LDR X17, #8    ; 加载后面的地址到 X17
// BR X17         ; 跳转到 X17
// .quad <addr>   ; 64位目标地址
// 共 16 字节

// 方式3: ADRP + ADD + BR (±4GB 范围)
// ADRP X17, <page>
// ADD X17, X17, <offset>
// BR X17
// 12 字节

8.3 主流 Inline Hook 框架

Substrate (Cydia Substrate)

最早的 iOS/Android Hook 框架,业界标准。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// Substrate API
#include <substrate.h>

// 原函数指针
static int (*orig_open)(const char *path, int flags, ...);

// Hook 函数
int hook_open(const char *path, int flags, ...) {
    // 自定义逻辑
    __android_log_print(ANDROID_LOG_DEBUG, "HOOK", "open: %s", path);

    // 调用原函数
    return orig_open(path, flags);
}

// 安装 Hook
MSHookFunction((void *)open, (void *)hook_open, (void **)&orig_open);

Dobby

跨平台的轻量级 Hook 框架,支持 ARM/ARM64/x86/x86_64。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// Dobby API
#include "dobby.h"

// 原函数指针
static int (*orig_open)(const char *path, int flags, mode_t mode);

// Hook 函数
int hook_open(const char *path, int flags, mode_t mode) {
    LOG("open: %s", path);
    return orig_open(path, flags, mode);
}

// 安装 Hook
DobbyHook((void *)open, (void *)hook_open, (void **)&orig_open);

// 卸载 Hook
DobbyDestroy((void *)open);

xHook (爱奇艺开源)

基于 PLT/GOT 的 Hook 框架,稳定性好,但只能 Hook 外部函数调用。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// xHook API
#include "xhook.h"

// Hook 函数
int my_open(const char *path, int flags, ...) {
    LOG("xhook open: %s", path);
    // 调用原函数需要使用 xhook 的方式
    return XHOOK_CALL_ORIG(open, path, flags);
}

// 注册 Hook
xhook_register(".*\\.so$", "open", my_open, NULL);

// 刷新 Hook
xhook_refresh(0);

bhook (字节跳动开源)

PLT Hook 框架,支持自动管理 Hook 代理。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// bhook API
#include "bytehook.h"

// Hook 函数
int my_open(const char *path, int flags, mode_t mode) {
    LOG("bhook open: %s", path);

    // 调用原函数
    BYTEHOOK_CALL_PREV(my_open, path, flags, mode);
    return result;
}

// 注册 Hook
bytehook_hook_single(
    "libc.so",           // 目标库
    NULL,                // 调用者 (NULL = 所有)
    "open",              // 函数名
    (void *)my_open,     // Hook 函数
    NULL,                // 回调
    NULL                 // 用户数据
);

ShadowHook (字节跳动开源)

真正的 Inline Hook 框架,支持 Hook 任意函数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// ShadowHook API
#include "shadowhook.h"

// 原函数类型
typedef int (*open_t)(const char *, int, mode_t);
static open_t orig_open;

// Hook 函数
int hook_open(const char *path, int flags, mode_t mode) {
    LOG("shadowhook open: %s", path);
    return orig_open(path, flags, mode);
}

// 安装 Inline Hook
void *stub = shadowhook_hook_func_addr(
    (void *)open,           // 目标函数地址
    (void *)hook_open,      // Hook 函数
    (void **)&orig_open     // 原函数指针
);

// 卸载 Hook
shadowhook_unhook(stub);

And-Hook

轻量级 Android Inline Hook 库。

1
2
3
4
5
// And-Hook API
#include "And64InlineHook.hpp"

// Hook
A64HookFunction((void *)target_func, (void *)hook_func, (void **)&orig_func);

Whale

支持多种 Hook 模式的框架。

1
2
3
4
5
// Whale API - Inline Hook
WInlineHookFunction((void *)open, (void *)hook_open, (void **)&orig_open);

// Whale API - Import Hook (类似 PLT Hook)
WImportHookFunction("libnative.so", "open", (void *)hook_open, (void **)&orig_open);

8.4 框架选型对比

框架Hook 类型架构支持稳定性性能适用场景
SubstrateInlineARM/ARM64/x86★★★★★★★★★通用 Hook,Xposed/Cydia
DobbyInlineARM/ARM64/x86/x64★★★★★★★★★跨平台,轻量级
xHookPLT/GOTARM/ARM64/x86/x64★★★★★★★★★外部函数 Hook
bhookPLT/GOTARM/ARM64★★★★★★★★★★外部函数 Hook,自动代理
ShadowHookInlineARM/ARM64★★★★★★★★★任意函数 Hook
And-HookInlineARM/ARM64★★★★★★★简单场景
WhaleInline/ImportARM/ARM64★★★★★★多模式需求

8.5 选型建议

  1. 只需要 Hook 外部函数调用 → 优先选择 bhookxHook

    • 稳定性高,兼容性好
    • 不需要处理指令重定位
  2. 需要 Hook 任意函数地址 → 选择 ShadowHookDobby

    • ShadowHook 对 Android 优化更好
    • Dobby 跨平台能力强
  3. Xposed 模块开发 → 使用 SubstrateDobby

    • Substrate 是 Xposed 默认使用的框架
    • Dobby 作为替代方案也很成熟
  4. 性能敏感场景bhook > ShadowHook > Dobby

    • PLT Hook 性能开销最小
    • Inline Hook 需要更多处理

8.6 Frida 中的 Inline Hook

Frida 的 Interceptor.attach 内部也是使用 Inline Hook 实现的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// Frida 使用内置的 Inline Hook 引擎
Interceptor.attach(Module.findExportByName("libc.so", "open"), {
    onEnter: function(args) {
        console.log("open:", args[0].readCString());
    },
    onLeave: function(retval) {
        console.log("  -> fd:", retval);
    }
});

// Frida 也支持直接替换函数
Interceptor.replace(targetAddr, new NativeCallback(function(arg0, arg1) {
    console.log("Replaced function called");
    return 0;
}, 'int', ['pointer', 'int']));

8.7 实战示例:使用 Dobby Hook JNI 函数

💡 思路一句话: Dobby 是内联 hook 框架,适合 hook native 函数入口 — 找到目标函数偏移 → 编写 hook 替换函数 → 在替换函数中记录参数/修改返回值 → 调用原始函数保持功能正常。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include <jni.h>
#include <android/log.h>
#include "dobby.h"

#define LOG(...) __android_log_print(ANDROID_LOG_DEBUG, "DobbyHook", __VA_ARGS__)

// 原函数指针
static jstring (*orig_NewStringUTF)(JNIEnv *env, const char *bytes);

// Hook 函数
jstring hook_NewStringUTF(JNIEnv *env, const char *bytes) {
    LOG("NewStringUTF: %s", bytes);

    // 可以修改字符串
    if (strstr(bytes, "secret") != NULL) {
        return orig_NewStringUTF(env, "hooked!");
    }

    return orig_NewStringUTF(env, bytes);
}

// 获取 JNI 函数地址
void *get_jni_func(JNIEnv *env, const char *name) {
    // JNINativeInterface 结构体偏移
    // NewStringUTF 在 JNINativeInterface 中的偏移是 167
    void **jni_funcs = *(void ***)env;
    return jni_funcs[167];  // NewStringUTF offset
}

// 初始化 Hook
void init_hooks(JNIEnv *env) {
    void *NewStringUTF_addr = get_jni_func(env, "NewStringUTF");

    DobbyHook(
        NewStringUTF_addr,
        (void *)hook_NewStringUTF,
        (void **)&orig_NewStringUTF
    );

    LOG("Hook installed at %p", NewStringUTF_addr);
}

JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *reserved) {
    JNIEnv *env;
    vm->GetEnv((void **)&env, JNI_VERSION_1_6);

    init_hooks(env);

    return JNI_VERSION_1_6;
}

8.8 注意事项

  1. 线程安全:Hook 安装时需要考虑多线程并发执行
  2. 指令对齐:ARM 指令需要 4 字节对齐,Thumb 需要 2 字节对齐
  3. 指令重定位:被覆盖的指令如果包含 PC 相对寻址,需要重新计算
  4. 缓存刷新:修改代码后需要刷新 CPU 指令缓存
  5. 权限检查:需要确保内存页有可写权限 (mprotect)

总结

Native 层 Hook 是 Android 逆向的核心技能之一。通过 Hook libc 函数(如 open、dlopen、memcpy、strcmp 等),我们可以:

  1. 监控文件操作,发现配置文件和动态加载的库
  2. 跟踪内存操作,捕获解密后的数据
  3. 分析字符串比较,绕过安全检测
  4. 进行脱壳分析,提取被保护的 DEX 文件

在技术选型时:

  • PLT/GOT Hook (xHook, bhook):稳定性好,适合 Hook 外部函数调用
  • Inline Hook (Dobby, ShadowHook):灵活性强,可以 Hook 任意地址
  • Frida Interceptor:开发效率高,适合快速分析和原型验证

在实践中,需要根据目标应用的具体行为和需求来选择合适的 Hook 技术和框架。


相关内容

如果这篇文章对你有帮助,请我喝杯咖啡吧~
+5 支付宝支付宝
+5 微信微信