通过 LR 返回地址的隐蔽内联钩子检测
发布: (2025年12月13日 GMT+8 11:15)
3 min read
原文: Dev.to
Source: Dev.to
背景:传统的 Inline Hook 检测为何失效
在 Android 上,传统的 Inline Hook 检测通常依赖于:
- CRC 或代码哈希校验
- 比较函数序言字节(例如,检测
LDR/BR跳板)
这些方法存在主要缺陷:
- 它们 读取代码页,容易被内存断点追踪。
- 攻击者可以 钩住检测函数本身。
- 它们的 “签名” 众所周知,容易绕过。
因此,在真实的攻防场景中,这类检测既显眼又高风险。
基于 LR 返回地址的隐蔽 Inline Hook 检测
所提方法 不读取任何指令,不扫描内存,且极难被 Frida(或类似框架)绕过。
Inline‑hook 框架(Frida、Dobby、xhook 等)通常会:
- 覆写 LR 寄存器。
- 将
RET重定向到存放在 自定义分配的内存页(位于原模块之外)的跳板。
关键观察:
如果函数的 LR 指向的地址不在其所属模块的内存范围内,则该函数被 inline‑hook。
优势
- 不读取
.text代码段。 - 无法通过硬件断点追踪。
- 支持 ARM64 与 x86_64(Android 与 Windows)。
检测流程
步骤 1 – 获取当前模块范围
static uint64_t g_begin = 0;
static uint64_t g_end = 0;
__attribute__((constructor))
static void init_module_range() {
Dl_info info;
if (dladdr((void*)init_module_range, &info)) {
g_begin = (uint64_t)info.dli_fbase;
g_end = g_begin + get_module_size(info.dli_fname);
}
}
步骤 2 – 读取链接寄存器 (LR)
__attribute__((always_inline))
uint64_t get_lr() {
uint64_t lr;
asm volatile(
"mov x10, x29 \n"
"ldr %0, [x10, #8]\n"
: "=r"(lr)
:
: "x10"
);
return lr;
}
步骤 3 – 检测 Inline Hook
void check_inline_hook() {
uint64_t lr = get_lr();
if (lr g_end) {
LOGD("[!] Inline‑hook detected. lr = %llx", lr);
} else {
LOGD("[+] Function not hooked.");
}
}
反调试加固(可选)
为了让调试几乎不可能,保护代码可以破坏栈并跳转到非法地址:
void anti_debug_crash() {
uint64_t fp;
asm volatile("mov %0, x29" : "=r"(fp));
memset((void*)fp, 0xCC, 2048); // 覆盖栈内存
((void(*)())0x12345678)(); // 跳转到非法地址
}
这会破坏堆栈跟踪,阻碍对保护逻辑的逆向分析。
小结
- 基于 LR 的 inline hook 检测 不读取代码页,几乎不受断点影响。
- 能可靠检测 Frida、Dobby、xhook 等跳板机制。
- 兼容 Android 与 Windows,支持 ARM64 与 x86_64 架构。
该技术为移动安全、反作弊系统以及运行时保护提供了新的思路。