news 2026/5/2 12:52:26

ARM64 Linux 内核 Hook 实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
ARM64 Linux 内核 Hook 实战

背景

手头有一台基于 Linux 的精简系统设备(BusyBox),提取并修改 system 分区后,设备出现开机约 5 分钟自动重启的异常。经全面排查与多轮测试,最终确认问题根源是内核层面的 system 分区完整性校验机制,因此决定通过内核 Hook 方式,修改并绕过该校验逻辑。

第一步:通过内核日志定位问题

核心目的

设备反复重启,首要任务是定位触发重启的核心进程,内核日志是最直接的排查入口(记录重启前最后活动)。

实操步骤

执行命令抓取内核实时日志(优于 dmesg,可捕捉重启前最后时刻信息):

adb shell cat /dev/kmsg

持续观察日志输出,重点关注重启前最后几行内容。

关键发现

重启前日志中反复出现两个内核线程名:file_check_threadmonitor_thread,二者均为文件完整性校验相关线程。

结论

重启原因:内核级 system 分区完整性校验,篡改后触发重启,需从内核层面 Hook 绕过。

第二步:找到内核物理加载地址

核心目的

Hook 内核函数的前提,是明确内核在物理内存中的加载位置(ARM64 架构下内核加载地址固定)。

实操步骤

通过/proc/iomem查看物理地址空间布局(内核导出的硬件地址映射表),执行命令:

adb shell "cat /proc/iomem | grep -i 'system ram\|kernel'"

输出结果解析

80000000-9fffffff : System RAM 80080000-8098ffff : Kernel code 80b10000-80c1efff : Kernel data

缩进表示包含关系(Kernel code 是 System RAM 的子区域),关键解读:

  • System RAM:物理内存起始地址0x80000000(由硬件决定);

  • Kernel code:内核代码段物理地址范围(Hook 目标所在区域);

  • Kernel data:内核数据段物理地址范围。

结论与原理

内核物理加载地址 =0x80080000,符合 ARM64 Linux 内核固定加载公式:

内核物理加载地址 = PHYS_OFFSET + TEXT_OFFSET = 0x80000000 + 0x80000 = 0x80080000

参数

含义

PHYS_OFFSET

System RAM 物理起始地址,由硬件决定

0x80000000

TEXT_OFFSET

ARM64 内核固定偏移(512KB),定义在内核 Image 头部

0x80000

第三步:通过物理地址反查虚拟地址

核心目的

IDA Pro 反编译内核需使用虚拟地址作为基准地址(base address),需通过物理地址反查对应虚拟地址。

核心工具选择

精简系统(BusyBox)通常裁剪了/proc/kallsyms(内核符号表),但/proc/vmallocinfo几乎必存在——其记录了所有虚拟地址与物理地址的映射关系,其中phys=字段对应物理地址。

实操步骤

以第二步获取的物理地址0x80080000为过滤条件,执行命令:

adb shell "cat /proc/vmallocinfo | grep phys=.*80080000"

输出结果解析

0xffffff8008080000-0xffffff8008830000 8060928 0xffffff800899511c phys=0x0000000080080000 vmap

关键解读(对应关系):

  • 虚拟地址范围:0xffffff8008080000 - 0xffffff8008830000(IDA Pro 需使用的基准地址);

  • 物理地址:0x80080000(与第二步 Kernel code 起始地址一致,确认是内核代码段映射)。

结论

IDA Pro 加载内核 Image 时,base address =0xffffff8008080000

原理补充

内核启动时,会将自身代码段从物理地址通过 vmap 映射到虚拟地址空间,/proc/vmallocinfo记录该映射关系,通过物理地址 grep 可直接过滤出对应虚拟地址。

第四步:还原魔改 Boot 镜像

问题现象

将提取的 boot 镜像直接拖入 IDA Pro,出现反编译报错——厂商对内核镜像进行了魔改,头部格式非标准 ARM64 Image,导致 IDA 无法识别。

分析过程

用 Hex 编辑器(如 010 Editor / HxD)打开 boot 镜像,对比标准 ARM64 内核 Image 魔数(magic):

  • 标准 ARM64 Image 头部:偏移 0x38 处为 magic 标识ARM\x64

  • 魔改镜像问题:真正的 Image 头部前被嵌入一段私有数据,导致 IDA 识别失败。

经验判断:删除0x0 ~ 0x9F0范围的 Hex 数据,剩余内容即为标准内核 Image。

实操步骤

  1. 打开 Hex 编辑器,加载魔改 boot 镜像;

  2. 选中0x000 ~ 0x9F0地址范围;

  3. 删除选中区域,将修改后的文件另存为新文件。

至此,得到标准 ARM64 内核 Image,可正常加载到 IDA Pro 进行反编译。

第五步:IDA Pro 反编译定位校验函数

第一步:加载内核 Image 到 IDA Pro

将还原后的标准内核 Image 拖入 IDA Pro,加载选项配置如下:

  • Processor type:ARM Little-endian [ARM];

  • Base address:填入第三步获取的虚拟地址0xffffff8008080000

第二步:搜索关键字符串定位校验函数

  1. Shift+F12打开字符串窗口;

  2. 搜索第一步发现的关键线程名file_check_thread

  3. 找到该字符串后,按快捷键X查看交叉引用,定位到引用该字符串的函数。

关键发现与目标

该交叉引用指向的函数,即为 system 分区完整性校验核心逻辑——内核启动后,由file_check_thread线程周期性调用,检测到 system 分区被篡改后,立即触发设备重启。

Hook 目标:修改该校验函数,使其被调用时直接返回,不执行任何校验逻辑。

第六步:编写内核模块实现 Hook

核心思路:仅需写入一条 RET 指令

ARM64 函数调用约定中,返回值通过 X0 寄存器传递。我们无需让校验函数“返回成功”,只需让其不执行任何校验逻辑——将函数入口第一条指令改为 RET,函数被调用时立即返回,X0 保留调用前的值(通常为 0),对校验线程而言,即视为“无异常”,不触发重启。

对比示意:

原始函数: Hook 后: ┌─────────────────┐ ┌─────────────────┐ │ STP X29, X30... │ │ RET │ ← 直接返回,跳过所有校验逻辑 │ 校验逻辑... │ │ (dead code...) │ │ 发现异常→重启 │ │ │ │ RET │ │ │ └─────────────────┘ └─────────────────┘

关键细节:ARM64 RET 指令的固定机器码为0xC0, 0x03, 0x5F, 0xD6(4 字节)。

核心问题:内核代码段只读保护

内核 .text 段(代码段)的页表属性为只读 + 可执行,直接通过 memcpy 写入 RET 指令会触发内存保护异常。因此,写入前需先修改目标地址所在页的页表项属性,临时改为可写。

核心前提:需先判断目标地址的映射粒度(ARM64 多级页表,不同粒度对应不同页表级别,修改方式不同)。

前置知识:ARM64 页表层级与映射粒度

ARM64 使用多级页表管理虚拟地址→物理地址映射,每级页表项(entry)的类型由 bit[1:0] 决定,不同级别对应不同映射粒度:

1. 页表项类型(bit[1:0])

bit[1:0]

类型

含义

0b01

Block entry

块映射,到此为止,修改该级页表

0b11

Table entry

表描述符,指向下一级,继续遍历

0b00

Invalid

无效,地址未映射

2. 各级页表映射粒度

级别

映射粒度

说明

PUD

1GB block

极少用于内核代码

PMD

2MB block

内核 .text 段通常使用该级(section mapping),优化性能

PTE

4KB page

多用于用户空间或特殊映射

注意:不可直接假设映射粒度,需通过代码实际诊断。

第一步:诊断目标地址的页表级别(编写诊断模块)

编写简单内核模块,通过 insmod 加载后,查看 dmesg 输出,确认目标地址的映射级别(需修改的页表级)。

static int __init diag_init(void) { // 目标地址:后续定位到的校验函数虚拟地址(示例值,需替换) unsigned long addr = 0xFFFFFF800841C020; // init_mm 地址(内核全局页表,后续定位,示例值) struct mm_struct *mm = (struct mm_struct *)0xFFFFFF8008B04ED8; pgd_t *pgdp = pgd_offset(mm, addr); if (pgd_none(READ_ONCE(*pgdp))) { printk("[hook] 0x%lx: PGD 无效,未映射\n", addr); return 0; } pud_t *pudp = pud_offset(pgdp, addr); if (pud_none(READ_ONCE(*pudp))) { printk("[hook] 0x%lx: PUD 无效\n", addr); return 0; } if (pud_sect(*pudp)) { printk("[hook] 0x%lx: PUD 级 block(1GB 映射)\n", addr); return 0; } pmd_t *pmdp = pmd_offset(pudp, addr); if (pmd_none(READ_ONCE(*pmdp))) { printk("[hook] 0x%lx: PMD 无效\n", addr); return 0; } if (pmd_sect(*pmdp)) { printk("[hook] 0x%lx: PMD 级 block(2MB section 映射)← 改 PMD\n", addr); return 0; } pte_t *ptep = pte_offset_kernel(pmdp, addr); if (pte_none(READ_ONCE(*ptep))) { printk("[hook] 0x%lx: PTE 无效\n", addr); return 0; } printk("[hook] 0x%lx: PTE 级(4KB page 映射)← 改 PTE\n", addr); return 0; } module_init(diag_init); MODULE_LICENSE("GPL");

诊断结果查看

加载诊断模块后,执行命令查看输出:

adb shell dmesg | grep hook

两种常见结果(对应不同修改方式):

  • 结果 A(大多数内核):[hook] 0xffffff800841c020: PMD 级 block(2MB section 映射)← 改 PMD

  • 结果 B(少数内核):[hook] 0xffffff800841c020: PTE 级(4KB page 映射)← 改 PTE

第二步:根据诊断结果编写 Hook 代码(以 PMD 级为例)

核心逻辑:遍历页表→修改页表属性(解除只读)→写入 RET 指令→完成 Hook。

#include <linux/init.h> #include <linux/module.h> #include <linux/mm.h> #include <linux/pgtable.h> static int __init lkm_init(void) { // 1. 目标函数虚拟地址(IDA 定位到的 file_check 校验函数入口,需替换) unsigned long file_check_addr = 0xFFFFFF800841C020; // 2. init_mm 地址(内核全局页表结构体,IDA 定位,需替换) struct mm_struct *mm = (struct mm_struct *)0xFFFFFF8008B04ED8; pgd_t *pgdp; pud_t *pudp; pmd_t *pmdp; // ===== 阶段1:逐级遍历页表,找到目标地址的 PMD 页表项 ===== pgdp = pgd_offset(mm, file_check_addr); if (pgd_none(READ_ONCE(*pgdp))) return -EINVAL; pudp = pud_offset(pgdp, file_check_addr); if (pud_none(READ_ONCE(*pudp))) return -EINVAL; pmdp = pmd_offset(pudp, file_check_addr); if (pmd_none(READ_ONCE(*pmdp))) return -EINVAL; // ===== 阶段2:修改页表属性,解除只读保护 ===== pmd_t pmd_value = READ_ONCE(*pmdp); pmd_value = pmd_mkwrite(pmd_value); // 添加可写权限 set_pmd(pmdp, pmd_value); // 将修改后的值写回页表 __flush_tlb_kernel_pgtable(file_check_addr); // 刷新 TLB,使权限修改生效 // ===== 阶段3:写入 RET 指令,完成 Hook ===== unsigned char retInstruction[] = {0xC0, 0x03, 0x5F, 0xD6}; // ARM64 RET 机器码 memcpy((void *)file_check_addr, retInstruction, sizeof(retInstruction)); printk("[hook] 校验函数 Hook 成功!\n"); return 0; } static void __exit lkm_exit(void) { // 可选:卸载模块时恢复原指令(需提前保存原指令内容) printk("[hook] 模块卸载\n"); } module_init(lkm_init); module_exit(lkm_exit); MODULE_LICENSE("GPL");

关键地址说明(必看)

代码中两个核心地址需根据实际情况替换,获取方式如下:

地址

含义

获取方式

0xFFFFFF800841C020

校验函数(file_check)入口虚拟地址

IDA Pro 中,通过字符串“file_check_thread”交叉引用定位

0xFFFFFF8008B04ED8

init_mm(内核全局页表结构体)地址

IDA Pro 中搜索“swapper”字符串,交叉引用定位(详见下方补充)

补充:init_mm 地址定位方法(精简系统无 kallsyms 时)

内核源码中存在固定引用逻辑(所有 ARM64 内核通用),可借助该逻辑定位 init_mm:

// arch/arm64/mm/fault.c(具体行号需结合自身内核源码) mm == &init_mm ? "swapper" : "user"

IDA Pro 操作步骤:

  1. Shift+F12 打开字符串窗口,搜索“swapper”;

  2. 按 X 查看交叉引用,跳转到引用该字符串的函数;

  3. 反编译结果中,会出现类似逻辑:v4 = &unk_FFFFFF8008B04ED8; swapper = "swapper";,其中unk_FFFFFF8008B04ED8即为 init_mm 的虚拟地址。

第三步:编译与加载 Hook 模块

编译前提

需获取对应设备的内核源码编译工具链(不同设备配置不同,需适配自身内核版本)。

加载操作

编译生成 .ko 模块后,在设备重启前通过 adb 加载(加载后立即生效):

adb shell insmod /path/to/hook.ko

生效验证

加载成功后,校验函数被 RET 指令覆盖,file_check_thread线程每次调用都会直接返回,不再执行校验逻辑,设备不会再因 system 分区篡改而重启。

重要注意事项

此方法仅适用于未开启内核保护机制的设备,若内核启用以下保护,会导致加载失败或 Hook 无效:

  • CONFIG_STRICT_MODULE_RWX(模块读写执行权限限制);

  • 模块签名校验(CONFIG_MODULE_SIG_FORCE,未签名模块无法 insmod);

  • SELinux 强制模式(会阻止页表修改、模块加载等操作)。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/10 20:41:14

PPTist:浏览器中打造专业演示文稿的终极解决方案

PPTist&#xff1a;浏览器中打造专业演示文稿的终极解决方案 【免费下载链接】PPTist PowerPoint-ist&#xff08;/pauəpɔintist/&#xff09;, An online presentation application that replicates most of the commonly used features of MS PowerPoint, allowing for the…

作者头像 李华
网站建设 2026/4/10 20:41:13

顶流集结!HOW 2026 分论坛讲师阵容正式亮相

两天时间&#xff0c;十二个分论坛&#xff0c;70议题。 继分论坛出品人阵容公布之后&#xff0c;这一次&#xff0c;我们把目光放到这些即将登场的演讲者。 他们中&#xff0c;有长期活跃在 PostgreSQL 社区的贡献者&#xff0c;也有深耕一线的数据库工程师&#xff0c;还有…

作者头像 李华
网站建设 2026/4/10 20:39:30

分析车辆电耗变化

import pandas as pddf pd.read_excel(rD:\lhy\data\车辆行驶里程表-1.xlsx, engineopenpyxl)# 查看数据 print(df.head())车辆ID 启动时间 停止时间 启动时剩余电量 停止时剩余电量 启动时电池温度 \ 0 1.0 2020-02-20 11:31:27 2020-02…

作者头像 李华
网站建设 2026/4/10 20:38:14

SRWE:打破Windows窗口限制的实时编辑神器

SRWE&#xff1a;打破Windows窗口限制的实时编辑神器 【免费下载链接】SRWE Simple Runtime Window Editor 项目地址: https://gitcode.com/gh_mirrors/sr/SRWE 你是否曾经因为Windows应用程序的窗口尺寸限制而感到困扰&#xff1f;无论是游戏玩家需要超高分辨率截图&am…

作者头像 李华
网站建设 2026/4/10 20:38:14

伏羲气象大模型与STM32嵌入式系统集成:实现微型气象站智能预报

伏羲气象大模型与STM32嵌入式系统集成&#xff1a;实现微型气象站智能预报 你有没有想过&#xff0c;自己动手做一个能预测未来几天天气的微型气象站&#xff1f;不是那种只能显示当前温度、湿度的简单设备&#xff0c;而是真正能告诉你“明天下午会不会下雨”、“后天风力有多…

作者头像 李华
网站建设 2026/4/10 20:35:49

Block Copy 的内存布局详解导

核心摘要&#xff1a;这篇文章能帮你 ?? 1. 彻底搞懂条件分支与循环的适用场景&#xff0c;告别选择困难。 ?? 2. 掌握遍历DOM集合修改属性的标准姿势与性能窍门。 ?? 3. 识别流程控制中的常见“坑”&#xff0c;并学会如何优雅地绕过去。 ?? 主要内容脉络 ?? 一、痛…

作者头像 李华