安卓内核Hook技术实现分析与应用

admin 2026-04-16 05:30:36 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文深度分析基于APatch框架开源的KernelHook项目,详细阐述ARM64内核函数Hook技术实现。通过对比传统方案的局限性,重点介绍InlineHook方案的优势:支持安卓9-16全版本、不依赖内核编译选项、性能接近原生调用。文章从接口设计、指令重定位、中转桩、内存管理等维度解析技术细节,并提供完整的代码示例展示函数替换、回调链注册等实际应用方法。 综合评分: 85 文章分类: 移动安全,二进制安全,渗透测试,红队,内网渗透


cover_image

安卓内核Hook技术实现分析与应用

原创

非虫 非虫

软件安全与逆向分析

2026年4月13日 20:24 湖北

在小说阅读器读本章

去阅读

安卓内核Hook技术实现分析与应用

本文是基于知名Root框架APatch作者bmax121开源的KernelHook项目源码做的深度技术分析。本文从接口到实现,逐层拆解ARM64内核函数Hook的工程细节。

本文项目开源地址为:https://github.com/bmax121/KernelHook

本文作者:非虫([email protected])

1 引言

在安卓安全研究、性能分析和内核功能扩展等场景中,内核函数 Hook 是一项基础技术。它的核心目标是在不修改内核源码、不重新编译内核的前提下,拦截并改变目标内核函数的行为。

传统方案各有局限:

| 方案 | 原理 | 不足 | | — | — | — | | kprobes | 断点指令触发异常 | 性能开销大 | | ftrace | 函数入口处的 NOP 桩 | arm64在6.4版本内核后这功能才能用 | | 修改系统调用表 | 替换系统调用表项 | 6.9版本内核系统调用表需要调整处理 | | eBPF | 内核态虚拟机 | 只能观测不能修改控制流 |

KernelHook采用Inline Hook方案——直接修改目标函数入口处的机器码,将控制流导向自定义逻辑。相比上述方案,它不依赖内核编译选项,不受GKI 模块限制,支持安卓9到安卓16全版本。能Hook任意导出或未导出的内核函数,且性能接近原生调用。

本文以KernelHook项目的源码为基础,从接口设计、指令重定位、中转桩、内存管理、安全机制适配等维度,完整剖析其技术实现。

2 接口与示例

KernelHook 对外暴露一组 C 接口,覆盖四大类操作:符号查找、函数替换、回调链注册、函数指针 Hook。所有接口声明在 include/hook.h 和 include/ksyms.h

2.1 初始化

Freestanding模式(Mode A)下,模块加载后需依次完成符号系统、内存池、页表遍历器和代码写入器的初始化:

#include<hook.h>
#include<ksyms.h>
#include<kmod_compat.h>

staticint&nbsp;__init&nbsp;my_module_init(void)
{
// kallsyms_lookup_name_addr 由 kmod_loader 在加载时注入
int&nbsp;err = kmod_compat_init(kallsyms_lookup_name_addr);
if&nbsp;(err)&nbsp;return&nbsp;err;

&nbsp; &nbsp; err = kmod_hook_mem_init(); &nbsp;&nbsp;// ROX/RW 内存池
if&nbsp;(err)&nbsp;return&nbsp;err;

&nbsp; &nbsp; kh_pgtable_init(); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;// 页表遍历器(读取 TCR_EL1 检测页大小)
&nbsp; &nbsp; kh_write_insts_init(); &nbsp; &nbsp; &nbsp; &nbsp;// 解析 set_memory_rw/ro/x

return0;
}

Kbuild 模式(Mode C)使用内核构建系统,直接调用内核头文件中的函数,初始化链更短。

2.2 符号查找

ksyms_lookup 和 ksyms_lookup_cache 是运行时符号查找的核心接口:

#include<ksyms.h>

// 按名称查找内核符号地址
unsignedlong&nbsp;addr = ksyms_lookup("do_sys_openat2");

// 带缓存版本:首次查找后缓存结果,后续直接命中
unsignedlong&nbsp;addr2 = ksyms_lookup_cache("vfs_read");

ksyms_lookup_cache 在全局缓存表中维护 (name, addr) 映射。对于反复查找同一符号的场景(如模块初始化期间多次引用),可显著减少开销。

2.3 函数替换

最直接的 Hook 方式:用自定义函数完全取代目标函数。

#include<hook.h>

staticunsignedlong&nbsp;orig_func;

// 替换函数,签名必须与目标函数一致
staticintmy_openat2(int&nbsp;dfd,&nbsp;constchar&nbsp;*filename,
struct&nbsp;open_how *how,&nbsp;size_t&nbsp;usize)
{
// 自定义前置逻辑 ...
int&nbsp;ret = ((typeof(&my_openat2))orig_func)(dfd, filename, how, usize);
// 自定义后置逻辑 ...
return&nbsp;ret;
}

staticint&nbsp;__init&nbsp;example_init(void)
{
unsignedlong&nbsp;target = ksyms_lookup("do_sys_openat2");
int&nbsp;err = hook((void&nbsp;*)target, (void&nbsp;*)my_openat2, (void&nbsp;**)&orig_func);
return&nbsp;err;
}

staticvoid&nbsp;__exit&nbsp;example_exit(void)
{
unsignedlong&nbsp;target = ksyms_lookup("do_sys_openat2");
&nbsp; &nbsp; unhook((void&nbsp;*)target);
}

hook() 在目标函数入口写入跳板指令,跳转到 my_openat2orig_func 指向一段经过重定位的代码——它执行被跳板覆盖的原始指令后,跳回原函数继续运行。

2.4 回调链

相比直接替换,回调链(Hook Wrap)更灵活——多个模块可以在同一函数上注册 before/after 回调,彼此互不干扰:

#include<hook.h>

// before 回调:在目标函数执行前调用
// hook_fargs4_t 表示目标函数有 4 个参数
staticvoidbefore_openat2(hook_fargs4_t&nbsp;*fargs,&nbsp;void&nbsp;*udata)
{
int&nbsp;dfd = (int)fargs->arg0;
constchar&nbsp;*filename = (constchar&nbsp;*)fargs->arg1;
&nbsp; &nbsp; logki("openat2: dfd=%d, file=%s", dfd, filename);

// 修改参数
// fargs->arg1 = (uint64_t)new_filename;

// 跳过原始函数并直接返回
// fargs->skip_origin = true;
// fargs->ret = -EPERM;
}

// after 回调:在目标函数执行后调用
staticvoidafter_openat2(hook_fargs4_t&nbsp;*fargs,&nbsp;void&nbsp;*udata)
{
long&nbsp;ret = fargs->ret;
&nbsp; &nbsp; logki("openat2 returned: %ld", ret);

// 修改返回值
// fargs->ret = -EACCES;
}

staticint&nbsp;__init&nbsp;example_init(void)
{
unsignedlong&nbsp;target = ksyms_lookup("do_sys_openat2");
// hook_wrap4: 4 参数的便捷宏,优先级默认为 0
int&nbsp;err = hook_wrap4(target, before_openat2, after_openat2,&nbsp;NULL);
return&nbsp;err;
}

staticvoid&nbsp;__exit&nbsp;example_exit(void)
{
unsignedlong&nbsp;target = ksyms_lookup("do_sys_openat2");
&nbsp; &nbsp; hook_unwrap(target, (void&nbsp;*)before_openat2, (void&nbsp;*)after_openat2);
}

hook_wrapN 是 hook_wrap 的便捷宏,N 表示目标函数的参数个数(0 到 12),默认优先级为 0。完整形式为:

inthook_wrap(void&nbsp;*func,&nbsp;int&nbsp;argno,&nbsp;void&nbsp;*before,&nbsp;void&nbsp;*after,
void&nbsp;*udata,&nbsp;int&nbsp;priority);

其中 before 和 after 均可为 NULL——只传 before 可以做纯拦截,只传 after 可以做纯审计。

多回调与优先级

同一目标函数上可注册最多 8 个回调。每个回调携带一个priority值,决定执行顺序:

  • before 回调

    :按 priority 降序执行——值越大越先执行

  • after 回调

    :按 priority 升序执行——值越小越先执行

这形成了洋葱式的包裹结构:

before(100) -> before(50) -> before(0)
&nbsp; -> 原始函数 ->
after(0) -> after(50) -> after(100)

多回调注册示例:

unsignedlong&nbsp;target = ksyms_lookup("do_sys_openat2");

// 高 priority 值的 before 回调先执行
hook_wrap(target,&nbsp;4, (void&nbsp;*)audit_callback, &nbsp;NULL,&nbsp;NULL,&nbsp;100);
hook_wrap(target,&nbsp;4, (void&nbsp;*)filter_callback,&nbsp;NULL,&nbsp;NULL,&nbsp;50);
hook_wrap(target,&nbsp;4, (void&nbsp;*)log_callback, &nbsp; &nbsp;NULL,&nbsp;NULL,&nbsp;0);

// before 执行顺序:audit(100) -> filter(50) -> log(0) -> 原始函数
// after 执行顺序:log(0) -> filter(50) -> audit(100)

回调间数据传递

before/after 回调对可通过 hook_local_t 共享数据:

staticvoidbefore_func(hook_fargs4_t&nbsp;*fargs,&nbsp;void&nbsp;*udata)
{
hook_local_t&nbsp;*local = &fargs->chain.local;
&nbsp; &nbsp; local->data0 = ktime_get_ns(); &nbsp;// 记录进入时间戳
}

staticvoidafter_func(hook_fargs4_t&nbsp;*fargs,&nbsp;void&nbsp;*udata)
{
hook_local_t&nbsp;*local = &fargs->chain.local;
uint64_t&nbsp;elapsed = ktime_get_ns() - local->data0;
&nbsp; &nbsp; logki("function took %llu ns", elapsed);
}

hook_local_t 提供 4 个 uint64_t 字段(data0 ~ data3),在同一次调用的 before 和 after 之间共享。不同回调槽位的local相互独立。

如需在回调中手动调用原始函数(较少见),可通过以下接口获取函数指针:

void&nbsp;*orig = wrap_get_origin_func(fargs); &nbsp; &nbsp;// inline hook 场景
void&nbsp;*orig = fp_get_origin_func(fargs); &nbsp; &nbsp; &nbsp;// 函数指针 hook 场景

2.5 函数指针 Hook

函数指针 Hook 用于拦截通过函数指针表(如 struct file_operations)间接调用的函数。与 Inline Hook 不同,它不修改目标函数的代码,而是替换指针本身的值。

直接替换

#include<hook.h>

staticunsignedlong&nbsp;orig_read;

staticssize_tmy_read(struct&nbsp;file *filp,&nbsp;char&nbsp;__user *buf,
size_t&nbsp;count,&nbsp;loff_t&nbsp;*pos)
{
&nbsp; &nbsp; logki("read intercepted: count=%zu", count);
return&nbsp;((typeof(&my_read))orig_read)(filp, buf, count, pos);
}

staticint&nbsp;__init&nbsp;example_init(void)
{
// fp_addr 指向某个 file_operations 结构体的 read 字段
void&nbsp;**fp_addr = get_target_fops_read_ptr();
int&nbsp;err = fp_hook(fp_addr, (void&nbsp;*)my_read, (void&nbsp;**)&orig_read);
return&nbsp;err;
}

staticvoid&nbsp;__exit&nbsp;example_exit(void)
{
void&nbsp;**fp_addr = get_target_fops_read_ptr();
&nbsp; &nbsp; fp_unhook(fp_addr, (void&nbsp;*)orig_read);
}

回调链与Inline Hook 类似,但最多支持16个回调:

fp_hook_wrap4(fp_addr, before_read, after_read,&nbsp;NULL);
// ...
fp_hook_unwrap(fp_addr, (void&nbsp;*)before_read, (void&nbsp;*)after_read);

2.6 接口一览

| 接口 | 功能 | | — | — | | hook(func, replace, &backup) | 替换目标函数 | | unhook(func) | 还原目标函数 | | hook_wrap(func, argno, before, after, udata, pri) | 注册回调链 | | hook_unwrap(func, before, after) | 移除回调 | | hook_wrapN(func, before, after, udata) | 便捷宏(N=0..12,pri=0) | | fp_hook(fp_addr, replace, &backup) | 替换函数指针 | | fp_unhook(fp_addr, backup) | 还原函数指针 | | fp_hook_wrap(fp_addr, argno, before, after, udata, pri) | 函数指针回调链 | | fp_hook_unwrap(fp_addr, before, after) | 移除函数指针回调 | | fp_hook_wrapN(fp_addr, before, after, udata) | 便捷宏(N=0..12) | | ksyms_lookup(name) | 查找内核符号地址 | | ksyms_lookup_cache(name) | 带缓存的符号查找 |

3 Inline Hook 原理

Inline Hook 的核心思路:覆盖目标函数入口处的若干条指令,替换为跳板指令(trampoline),将控制流引向自定义代码。被覆盖的原始指令经过重定位后保存在另一块内存中,执行完毕后跳回原函数继续运行。

3.1 跳板结构

ARM64 上,KernelHook 使用 4 条指令(16 字节)构造跳板:

MOV &nbsp;X16,&nbsp;#imm16_low &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ; 目标地址低 16 位
MOVK X16,&nbsp;#imm16_mid, LSL&nbsp;#16&nbsp; ; 中 16 位
MOVK X16,&nbsp;#imm16_high, LSL&nbsp;#32&nbsp;; 高 16 位
BR &nbsp; X16 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;; 无条件间接跳转

如果目标函数的首条指令是 BTI 或 PAC 指令(BTI JC / PACIASP / PACIBSP),跳板扩展为 5 条,首条保留为 BTI JC

BTI &nbsp;JC &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ; 保留分支目标标识
MOV &nbsp;X16,&nbsp;#imm16_low
MOVK X16,&nbsp;#imm16_mid, LSL&nbsp;#16
MOVK X16,&nbsp;#imm16_high, LSL&nbsp;#32
BR &nbsp; X16

选择 X16 是因为 ARM64 调用约定将其定义为 IP0(Intra-Procedure-call scratch register):不被调用者保存,且 BTI 允许 BR X16 作为合法的间接分支目标。

3.2 指令重定位

被跳板覆盖的原始指令不能简单复制到新地址执行——ARM64 中大量指令使用 PC 相对寻址,复制后偏移量会指向错误位置。

KernelHook 的指令重定位引擎(src/arch/arm64/inline.c)识别并处理 17 种指令类型:

| 指令类型 | 寻址方式 | 重定位策略 | | — | — | — | | B | PC +/- 128 MB | 重算偏移或展开为绝对跳转 | | B.cond | PC +/- 1 MB | 反转条件 + 绝对跳转 | | BL | PC +/- 128 MB | 绝对跳转 + 手动设置 LR | | ADR | PC +/- 1 MB | 替换为 MOV/MOVK 绝对地址序列 | | ADRP | PC +/- 4 GB (页对齐) | 同上 | | LDR (literal) | PC +/- 1 MB (32/64/SIMD) | 转为寄存器间接加载 | | LDRSW (literal) | PC +/- 1 MB | 同上 | | CBZCBNZ | PC +/- 1 MB | 反转条件 + 绝对跳转 | | TBZTBNZ | PC +/- 32 KB | 反转条件 + 绝对跳转 | | PRFM (literal) | PC +/- 1 MB | 转为寄存器间接预取 | | 其他 | 无 PC 相对寻址 | 直接复制 |

每种类型的重定位产出长度不同(2 ~ 8 条 uint32_t 指令)。引擎预先扫描全部被覆盖指令,计算总输出长度,一次性分配缓冲区,再逐条写入。

重定位后的代码布局:

+--------------------------------------+
| BTI JC &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; | <- 入口(满足 BTI)
+--------------------------------------+
| NOP padding &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;| <- 对齐填充
+--------------------------------------+
| 重定位后的指令序列 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;| <- 原始指令的等价实现
+--------------------------------------+
| MOV X16,&nbsp;#addr; BR X16 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;| <- 跳回原函数(跳板之后)
+--------------------------------------+

3.3 kCFI 哈希

GKI 6.1+ 内核启用了 kCFI(Kernel Control Flow Integrity):Clang 在每个函数入口前 4 字节写入类型哈希值。间接调用前,编译器检查目标地址 -4 处的哈希是否匹配,不匹配则 panic。

KernelHook 将原始函数入口前 4 字节的 kCFI 哈希复制到重定位代码入口前 4 字节(_relo_cfi_hash 字段)。中转桩入口同样携带正确的哈希值,使 kCFI 检查正常通过。

hook_chain_rox_t 的内存布局:

+---------------------+
| _relo_cfi_hash &nbsp; &nbsp; &nbsp;| <- 复制自原函数的 kCFI 哈希
+---------------------+
| relo_insts[] &nbsp; &nbsp; &nbsp; &nbsp;| <- 重定位后的指令序列
+---------------------+
| hook_t &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;| <- Hook 状态
+---------------------+
| rw_ptr &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;| <- 指向 RW 区域
+---------------------+
| transit[] &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; | <- 中转桩(64 字节对齐)
+---------------------+

4 中转桩

4.1 设计目标

直接替换模式(hook / unhook)下,跳板直接跳到替换函数。但回调链模式(hook_wrap)需要中间层——中转桩(transit stub)——来调度 before/after 回调、管理参数传递和返回值。

4.2 汇编模板

中转桩的汇编模板定义在 src/arch/arm64/transit.c,编译时作为独立代码模板存在。每次注册新 Hook 时,模板被复制到 hook_chain_rox_t.transit[] 缓冲区,同时将 transit[0..1] 写入指向所属 hook_chain_rox_t 的自引用指针。

中转桩执行流程:

BTI &nbsp;JC &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;; 满足 BTI 要求
ADR &nbsp;X16,&nbsp;#0&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;; 取当前 PC
SUB &nbsp;X16, X16,&nbsp;#offset&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;; 回算 transit[] 基址
LDR &nbsp;X15, [X16] &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;; 加载 rox_ptr(自引用)
LDR &nbsp;X14, [X15,&nbsp;#rw_offset] &nbsp; &nbsp; &nbsp;; 加载 rw_ptr

STP &nbsp;X29, X30, [SP, #-frame]! &nbsp; &nbsp;; 保存帧

; 参数右移:X7->栈, X6->X7, ..., X1->X2, X0->X1
; 腾出 X0 传递 rw_ptr
MOV &nbsp;X0, X14
BLR &nbsp;transit_body &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ; 调用 C 调度函数

LDP &nbsp;X29, X30, [SP],&nbsp;#frame&nbsp; &nbsp; &nbsp; ; 恢复帧
RET

4.3 调度逻辑

transit_body() 是纯 C 函数,负责组装回调上下文并按序遍历回调链:

  1. 从 rw_ptr 读取 sorted_indices[] 和回调列表

  2. 构建 hook_fargs 结构体,填入参数、返回值、本地存储

  3. 正序

    遍历 sorted_indices[],依次调用每个 before 回调

  4. 若无回调设置 skip_origin = true,调用重定位后的原始函数

  5. 逆序

    遍历 sorted_indices[],依次调用每个 after 回调

  6. 返回 fargs.ret

函数指针 Hook 有独立的 fp_transit_body(),步骤 4 调用保存的原始函数指针而非重定位代码。

5 内存管理

5.1 内存池

KernelHook 自行管理两个内存池,避免频繁调用内核的 vmalloc / vfree

| 池 | 用途 | 容量 | 块大小 | 权限 | | — | — | — | — | — | | ROX | Hook 结构体、重定位代码、中转桩 | 1 MB | 64 字节 | 读 + 执行 | | RW | 回调链数据(槽位、排序索引等) | 512 KB | 64 字节 | 读 + 写 |

每个池用位图追踪块的分配状态。分配时线性扫描找到连续空闲块,释放时清除对应位。

hook_chain_rox_t(含 64 字节对齐的 transit 缓冲区)分配在 ROX 池,占用多个连续块。hook_chain_rw_t 分配在 RW 池,包含最多 8 个回调槽位和优先级排序索引数组。

5.2 来源映射

origin_map 是 128 项的线性表,记录原始函数地址到 ROX 结构体指针的映射。unhook 和 hook_unwrap 通过此表快速定位对应的 Hook 结构体。

5.3 页表遍历

Freestanding 模式下无法使用 set_memory_rw/ro/x,需直接操作页表修改代码段权限。

页表遍历器(src/arch/arm64/pgtable.c)初始化步骤:

  1. 读取 TCR_EL1.TG1,判断页大小(4K / 16K / 64K)
  2. 读取 TCR_EL1.T1SZ,计算虚拟地址位宽和页表级数
  3. 通过 ksyms 解析 swapper_pg_dirkimage_voffsetmemstart_addr

代码写入流程:

  1. 遍历页表,找到目标虚拟地址的 PTE
  2. 清除 PTE_RDONLY,设置 PTE_DBM,使页面可写
  3. TLBI 刷新 TLB
  4. 写入指令
  5. 恢复 PTE 原始权限
  6. IC IVAU 刷新指令缓存

如果通过 ksyms 找到了 set_memory_rw / set_memory_ro / set_memory_x,KernelHook 优先使用这些 API。回退到页表直接操作仅在上述函数不可用时发生。

6 安全机制适配

ARM64 和 GKI 内核引入了多层安全机制。KernelHook 需逐一适配,否则 Hook 操作会触发 panic。

6.1 CFI

安卓 GKI 内核有两代 CFI:

  • Shadow CFI

    (GKI 5.4 ~ 5.15):编译器在间接调用前插入运行时类型检查。Hook 本身不直接受影响,但替换函数的类型签名须与目标一致。

  • kCFI

    (GKI 6.1+):函数入口前 4 字节是类型哈希。间接调用前检查哈希匹配,不匹配则 panic。

kCFI 的适配策略已在第 3.3 节说明。

6.2 PAC 与 BTI

  • PAC

    (Pointer Authentication,ARMv8.3+):入口处 PACIASP 对返回地址签名,返回时 AUTIASP 验证。中转桩自行配对 STP/LDP X29, X30,不破坏签名链。

  • BTI

    (Branch Target Identification,ARMv8.5+):间接分支目标须为 BTI 指令。KernelHook 在跳板入口、重定位代码入口和中转桩入口均放置 BTI JC

6.3 SCS

Shadow Call Stack(影子调用栈):GKI 内核在专用栈中保存返回地址副本,返回时比对。KernelHook 的中转桩通过标准 STP/LDP X29, X30 保存恢复帧,不破坏影子栈一致性。

7 构建与加载

7.1 三种模式

| 模式 | 依赖 | 适用场景 | | — | — | — | | A:Freestanding | 无内核头文件 | 一个 .ko 适配多个内核版本 | | B:SDK | 预加载的 kernelhook.ko | 多业务模块共享 Hook 基础设施 | | C:Kbuild | 完整内核源码 / 头文件 | 有目标内核源码的开发环境 |

Freestanding 构建(CMake + NDK):

mkdir&nbsp;build &&&nbsp;cd&nbsp;build
cmake -DCMAKE_TOOLCHAIN_FILE=$NDK/build/cmake/android.toolchain.cmake \
&nbsp; &nbsp; &nbsp; -DANDROID_ABI=arm64-v8a \
&nbsp; &nbsp; &nbsp; -DANDROID_PLATFORM=android-30 \
&nbsp; &nbsp; &nbsp; ..
make

Kbuild 构建

make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- \
&nbsp; &nbsp; &nbsp;KERNEL_DIR=/path/to/kernel/source

7.2 kmod_loader

安卓内核对第三方 .ko 有严格的加载校验:vermagic 须匹配、导入符号 CRC 须一致、struct module 布局须正确。kmod_loader 是用户态 ELF 修补工具,在加载前自动解决这些问题。

它的值解析采用策略链架构——每个需要解析的值有独立的策略链,按优先级依次尝试:

cli_override &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; <- 命令行直接指定
&nbsp; -> probe_loaded_module &nbsp; <- 从已加载模块提取
&nbsp; -> probe_ondisk_module &nbsp; <- 从磁盘 .ko 提取
&nbsp; -> probe_procfs &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;<- 从 /proc 提取
&nbsp; -> config_explicit &nbsp; &nbsp; &nbsp; <- 精确设备匹配(内置设备表)
&nbsp; -> config_automatch &nbsp; &nbsp; &nbsp;<- 自动设备匹配
&nbsp; -> config_fuzzy &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;<- 模糊匹配
&nbsp; -> probe_disasm &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;<- 反汇编 /proc/kcore
&nbsp; -> probe_binary_search &nbsp; <- 内存二进制搜索

第一个成功返回的策略“获胜”,后续跳过。核心解析值:

| 值 | 说明 | | — | — | | module_layout_crc | module_layout 的 CRC | | _printk_crcmemcpy_crc / memset_crc | 常用导入符号 CRC | | vermagic | 内核版本与配置标识串 | | this_module_size | struct module 大小 | | module_init_offsetmodule_exit_offset | init/exit 在结构体中的偏移 | | kallsyms_lookup_name_addr | kallsyms_lookup_name 内核地址 |

修补完成后,kmod_loader 通过 init_module 系统调用加载 .ko,同时将 kallsyms_lookup_name 地址注入模块,作为 Freestanding 模式初始化链的起点。

8 兼容性

8.1 内核版本

| 内核 | 安卓版本 | 关键特性 | | — | — | — | | 4.4 | 9 (API 28) | 基线 | | 4.9 | 10 (API 29) | – | | 4.14 | 11 (API 30) | – | | 4.19 | 12 (API 31) | – | | 5.4 | 12 (API 31) | Shadow CFI | | 5.10 | 13 (API 33) | GKI 模块限制 | | 5.15 | 14 (API 34) | – | | 6.1 | 14 (API 34) | kCFI | | 6.6 | 15 (API 35) | – | | 6.12 | 16 (API 36/37) | 16K 页 |

8.2 符号差异

内核函数名在版本间可能变化。KernelHook维护回退列表:

  • vmalloc

    vmalloc_noprof

  • set_memory_x

    set_memory_exec

  • __flush_dcache_area

    dcache_clean_inval_poc

查找时依次尝试主名称和回退名称,首个命中即采用。

8.3 16K页

安卓16设备使用16K页内核。KernelHook在 kh_pgtable_init中通过TCR_EL1.TG1动态检测页大小,所有页表操作基于运行时值而非编译时常量,天然兼容4K和16K。

最后,如果本文对您有帮助,欢迎点赞关注与转发,感谢您的阅读。

如果您对安卓安全相关内核模块开发感兴趣,可以关注我的安卓软件开发与逆向分析系列课程,第一阶段有LKM开发,第二阶段有KPM开发,第四阶段有安全对抗应用实战。


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:软件安全与逆向分析 非虫 非虫《安卓内核Hook技术实现分析与应用》

评论:0   参与:  0