kptools 详解 - 用户空间镜像修补工具
概述
kptools 是 KernelPatch 的用户空间工具,负责在不修改内核源码的情况下,对内核镜像进行分析和修补。它是整个 KernelPatch 工作流程的起点。
位置:tools/ 目录
主要文件:kptools.c, kallsym.c, patch.c, bootimg.c(0.13.0+新增), image.c, symbol.c
主程序入口 - kptools.c
命令行参数
kptools 支持两类命令接口:
getopt 参数模式(传统):
// tools/kptools.c:32-70
void print_usage(char **argv)
{
"Usage: %s COMMAND [Options...]\n"
"COMMAND:\n"
" -p, --patch Patch or Update patch of kernel image\n"
" -u, --unpatch Unpatch patched kernel image\n"
" -r, --reset-skey Reset superkey of patched image\n"
" -d, --dump Dump kallsyms informations\n"
" -f, --flag Dump ikconfig informations\n"
" -l, --list Print all patch informations\n"
"Options:\n"
" -i, --image PATH Kernel image path\n"
" -k, --kpimg PATH KernelPatch image path\n"
" -s, --skey KEY Set the superkey\n"
" -S, --root-skey KEY Set the root-superkey using hash\n"
" -o, --out PATH Patched image path\n"
" -a, --addition KEY=VALUE Add additional information\n"
" -M, --embed-extra-path PATH Embed new extra item\n"
" -T, --extra-type TYPE Set type of extra item (kpm/shell/exec/raw)\n"
" -N, --extra-name NAME Set name of extra item\n"
" -V, --extra-event EVENT Set trigger event\n"
" -A, --extra-args ARGS Set arguments\n"
}
位置参数模式(0.13.0+ 新增,用于 boot.img 操作):
kptools unpack <boot.img> # 解包 boot 镜像,提取并解压内核到 "kernel" 文件
kptools unpacknolog <boot.img> # 同上但不打印日志
kptools repack <boot.img> # 重打包,从 "kernel" 读取新内核,输出 "new-boot.img"
kptools sha1 <file> # 计算文件的 SHA1 哈希值
### 主函数流程
```c
// tools/kptools.c:73-214
int main(int argc, char *argv[])
{
version = (MAJOR << 16) + (MINOR << 8) + PATCH;
// 解析命令行参数
while ((opt = getopt_long(argc, argv, optstr, longopts, &opt_index)) != -1) {
switch (opt) {
case 'p': cmd = opt; break; // patch
case 'u': cmd = opt; break; // unpatch
case 'd': cmd = opt; break; // dump
case 'i': kimg_path = optarg; break;
case 'k': kpimg_path = optarg; break;
case 's': superkey = optarg; break;
case 'M': // 嵌入额外文件(KPM 等)
config = &extra_configs[extra_config_num++];
config->is_path = true;
config->path = optarg;
break;
// ... 其他参数
}
}
// 执行对应命令
if (cmd == 'p') {
ret = patch_update_img(kimg_path, kpimg_path, out_path,
superkey, root_skey, additional,
extra_configs, extra_config_num);
} else if (cmd == 'd') {
ret = dump_kallsym(kimg_path);
} else if (cmd == 'u') {
ret = unpatch_img(kimg_path, out_path);
} else if (cmd == 'l') {
ret = print_image_patch_info_path(kimg_path);
}
return ret;
}
镜像读取与解析 - image.c
kernel_file_t 结构
// tools/patch.c:27-70
typedef struct {
char *kfile; // 完整文件缓冲区
int kfile_len; // 完整文件长度
char *kimg; // 内核镜像起始(跳过可能的前缀)
int kimg_len; // 内核镜像长度
bool is_uncompressed_img; // 是否有 UNCOMPRESSED_IMG 头
} kernel_file_t;
void read_kernel_file(const char *path, kernel_file_t *kernel_file)
{
int img_offset = 0;
read_file(path, &kernel_file->kfile, &kernel_file->kfile_len);
// 检查是否有 UNCOMPRESSED_IMG 头(某些设备)
kernel_file->is_uncompressed_img =
kernel_file->kfile_len >= 20 &&
!strncmp("UNCOMPRESSED_IMG", kernel_file->kfile, 16);
if (kernel_file->is_uncompressed_img) img_offset = 20;
kernel_file->kimg = kernel_file->kfile + img_offset;
kernel_file->kimg_len = kernel_file->kfile_len - img_offset;
}
UNCOMPRESSED_IMG 格式:
偏移 0x00: "UNCOMPRESSED_IMG" (16 字节)
偏移 0x10: uint32_t kimg_len (小端序)
偏移 0x14: 内核镜像数据...
ARM64 内核镜像头部解析
// tools/image.c:18-56
typedef struct {
union {
struct {
uint8_t mz[4]; // "MZ" (UEFI)
uint32_t b_insn; // B 指令
} efi;
struct {
uint32_t b_insn; // B 指令(非 UEFI)
uint32_t reserved0;
} nefi;
} hdr;
uint64_t kernel_offset; // 加载偏移
uint64_t kernel_size_le; // 内核大小(小端序)
uint64_t kernel_flag_le; // 标志位
uint64_t reserved[3];
char magic[4]; // "ARM\x64"
uint64_t pe_offset; // PE 头偏移(UEFI)
} arm64_hdr_t; // 64 字节
int32_t get_kernel_info(kernel_info_t *kinfo, const char *img, int32_t imglen)
{
arm64_hdr_t *khdr = (arm64_hdr_t *)img;
// 1. 验证魔数
if (strncmp(khdr->magic, "ARM\x64", 4)) {
tools_loge_exit("kernel image magic error: %s\n", khdr->magic);
}
// 2. 检测 UEFI
kinfo->uefi = !strncmp((const char *)khdr->hdr.efi.mz, "MZ", 2);
// 3. 解析 B 指令
uint32_t b_insn = kinfo->uefi ? khdr->hdr.efi.b_insn : khdr->hdr.nefi.b_insn;
b_insn = u32le(b_insn);
// B 指令格式:0x14000000 | (offset >> 2)
if ((b_insn & 0xFC000000) != 0x14000000) {
tools_loge_exit("kernel primary entry: %x\n", b_insn);
} else {
uint32_t imm = (b_insn & 0x03ffffff) << 2;
kinfo->primary_entry_offset = imm + b_stext_insn_offset;
}
// 4. 解析其他字段
kinfo->load_offset = u64le(khdr->kernel_offset);
kinfo->kernel_size = u64le(khdr->kernel_size_le);
// 5. 解析页面大小
uint8_t flag = u64le(khdr->kernel_flag_le) & 0x0f;
kinfo->is_be = flag & 0x01; // 字节序
switch ((flag & 0b0110) >> 1) {
case 2: kinfo->page_shift = 14; break; // 16KB
case 3: kinfo->page_shift = 16; break; // 64KB
case 1:
default: kinfo->page_shift = 12; break; // 4KB
}
return 0;
}
核心功能:patch_update_img()
这是 kptools 最重要的函数,负责修补内核镜像。
完整流程
// tools/patch.c:339-619
int patch_update_img(const char *kimg_path, const char *kpimg_path,
const char *out_path, const char *superkey,
bool root_key, const char **additional,
extra_config_t *extra_configs, int extra_config_num)
{
// ============ 阶段 1:读取和解析 ============
// 1.1 读取内核镜像
kernel_file_t kernel_file;
read_kernel_file(kimg_path, &kernel_file);
// 1.2 解析镜像信息
patched_kimg_t pimg = { 0 };
parse_image_patch_info(kernel_file.kimg, kernel_file.kimg_len, &pimg);
// 1.3 分析 kallsyms
char *kallsym_kimg = (char *)malloc(pimg.ori_kimg_len);
memcpy(kallsym_kimg, pimg.kimg, pimg.ori_kimg_len);
kallsym_t kallsym = { 0 };
if (analyze_kallsym_info(&kallsym, kallsym_kimg, pimg.ori_kimg_len, ARM64, 1)) {
tools_loge_exit("analyze_kallsym_info error\n");
}
// 1.4 读取 kpimg
char *kpimg = NULL;
int kpimg_len = 0;
read_file_align(kpimg_path, &kpimg, &kpimg_len, 0x10);
// ============ 阶段 2:布局计算 ============
int ori_kimg_len = pimg.ori_kimg_len;
int align_kimg_len = align_ceil(ori_kimg_len, SZ_4K);
int out_img_len = align_kimg_len + kpimg_len;
int out_all_len = out_img_len + extra_size;
// 计算 start_offset(避免运行时内存覆盖)
int align_kernel_size = align_ceil(kinfo->kernel_size, SZ_4K);
int start_offset = align_kernel_size;
if (out_all_len > start_offset) {
// 如果 kpimg + extra 会覆盖内核运行时内存,需要调整
start_offset = align_ceil(out_all_len, SZ_4K);
tools_logi("patch overlap, move start from 0x%x to 0x%x\n",
align_kernel_size, start_offset);
}
tools_logi("layout kimg: 0x0,0x%x, kpimg: 0x%x,0x%x, "
"extra: 0x%x,0x%x, end: 0x%x, start: 0x%x\n",
ori_kimg_len, align_kimg_len, kpimg_len,
out_img_len, extra_size, out_all_len, start_offset);
// ============ 阶段 3:组装输出镜像 ============
kernel_file_t out_kernel_file;
new_kernel_file(&out_kernel_file, &kernel_file, out_all_len,
(bool)(is_be() ^ kinfo->is_be));
// 3.1 复制原始内核
memcpy(out_kernel_file.kimg, pimg.kimg, ori_kimg_len);
memset(out_kernel_file.kimg + ori_kimg_len, 0, align_kimg_len - ori_kimg_len);
// 3.2 附加 kpimg
memcpy(out_kernel_file.kimg + align_kimg_len, kpimg, kpimg_len);
// ============ 阶段 4:填充 preset ============
preset_t *preset = (preset_t *)(out_kernel_file.kimg + align_kimg_len);
setup_preset_t *setup = &preset->setup;
memset(setup, 0, sizeof(preset->setup));
// 4.1 内核信息
setup->kernel_version = kallsym.version;
setup->kimg_size = ori_kimg_len;
setup->kpimg_size = kpimg_len;
setup->kernel_size = kinfo->kernel_size;
setup->page_shift = kinfo->page_shift;
// 4.2 偏移信息
setup->setup_offset = align_kimg_len;
setup->start_offset = start_offset;
setup->extra_size = extra_size;
// 4.3 选择 map 区域
int map_start, map_max_size;
select_map_area(&kallsym, kallsym_kimg, &map_start, &map_max_size);
setup->map_offset = map_start;
setup->map_max_size = map_max_size;
// 4.4 符号地址
setup->kallsyms_lookup_name_offset =
get_symbol_offset_exit(&kallsym, kallsym_kimg, "kallsyms_lookup_name");
setup->printk_offset =
get_symbol_offset_zero(&kallsym, kallsym_kimg, "printk");
setup->paging_init_offset =
get_symbol_offset_exit(&kallsym, kallsym_kimg, "paging_init");
// 4.5 填充 map_symbol(memblock 函数)
fillin_map_symbol(&kallsym, kallsym_kimg, &setup->map_symbol, kinfo->is_be);
// 4.6 备份内核头部
memcpy(setup->header_backup, kallsym_kimg, sizeof(setup->header_backup));
// 4.7 填充 patch_config(要 hook 的函数)
fillin_patch_config(&kallsym, kallsym_kimg, ori_kimg_len,
&setup->patch_config, kinfo->is_be, 0);
// 4.8 设置 SuperKey
if (!root_key) {
strncpy((char *)setup->superkey, superkey, SUPER_KEY_LEN - 1);
} else {
// Root SuperKey 模式:存储哈希
BYTE buf[SHA256_BLOCK_SIZE];
SHA256_CTX ctx;
sha256_init(&ctx);
sha256_update(&ctx, (const BYTE *)superkey, strnlen(superkey, SUPER_KEY_LEN));
sha256_final(&ctx, buf);
memcpy(setup->root_superkey, buf, ROOT_SUPER_KEY_HASH_LEN);
}
// ============ 阶段 5:修改内核入口点 ============
// 将 paging_init 重定位(如果第一条指令是 B)
setup->paging_init_offset = relo_branch_func(kallsym_kimg, paging_init_offset);
// 计算 setup_entry 的位置
int text_offset = align_kimg_len + SZ_4K; // preset (4KB) 之后
// 生成 B 指令
b((uint32_t *)(out_kernel_file.kimg + kinfo->b_stext_insn_offset),
kinfo->b_stext_insn_offset, text_offset);
// ============ 阶段 6:附加 Extra Items ============
int current_offset = out_img_len;
for (int i = 0; i < extra_config_num; i++) {
extra_config_t *config = extra_configs + i;
patch_extra_item_t *item = config->item;
// 复制 item 头部
extra_append(out_kernel_file.kimg, (void *)item, sizeof(*item), ¤t_offset);
// 复制参数
if (item->args_size > 0) {
extra_append(out_kernel_file.kimg, (void *)config->set_args,
item->args_size, ¤t_offset);
}
// 复制内容(KPM 二进制或脚本等)
extra_append(out_kernel_file.kimg, (void *)config->data,
item->con_size, ¤t_offset);
}
// 空 item 作为结束标志
patch_extra_item_t empty_item = { 0 };
extra_append(out_kernel_file.kimg, (void *)&empty_item,
sizeof(empty_item), ¤t_offset);
// ============ 阶段 7:写入文件 ============
write_kernel_file(&out_kernel_file, out_path);
tools_logi("patch done: %s\n", out_path);
return 0;
}
关键函数详解
1. select_map_area() - 选择临时代码区
// tools/symbol.c:59-65
void select_map_area(kallsym_t *kallsym, char *image_buf,
int32_t *map_start, int32_t *max_size)
{
int32_t addr = 0x200;
addr = get_symbol_offset_exit(kallsym, image_buf, "tcp_init_sock");
*map_start = align_ceil(addr, 16);
*max_size = 0x800; // 2KB
}
为什么选择 tcp_init_sock:
- TCP socket 初始化函数
- 在
rest_init之前不会被调用 - 网络栈初始化发生在
kernel_init阶段 - 可以安全地临时覆盖
替代方案(如果遇到问题):
// 其他候选函数
addr = get_symbol_offset_exit(kallsym, image_buf, "calibrate_delay");
addr = get_symbol_offset_exit(kallsym, image_buf, "cpuidle_idle_call");
2. fillin_map_symbol() - 填充 memblock 函数地址
// tools/symbol.c:67-92
int fillin_map_symbol(kallsym_t *kallsym, char *img_buf,
map_symbol_t *symbol, int32_t target_is_be)
{
// 获取 memblock 函数地址
symbol->memblock_reserve_relo =
get_symbol_offset_exit(kallsym, img_buf, "memblock_reserve");
symbol->memblock_free_relo =
get_symbol_offset_exit(kallsym, img_buf, "memblock_free");
symbol->memblock_mark_nomap_relo =
get_symbol_offset_zero(kallsym, img_buf, "memblock_mark_nomap");
// 内存分配函数(版本兼容)
symbol->memblock_phys_alloc_relo =
get_symbol_offset_zero(kallsym, img_buf, "memblock_phys_alloc_try_nid");
symbol->memblock_virt_alloc_relo =
get_symbol_offset_zero(kallsym, img_buf, "memblock_virt_alloc_try_nid");
if (!symbol->memblock_phys_alloc_relo && !symbol->memblock_virt_alloc_relo) {
tools_loge_exit("no symbol memblock_alloc");
}
// 字节序转换(如果需要)
if ((is_be() ^ target_is_be)) {
for (int64_t *pos = (int64_t *)symbol; pos <= (int64_t *)symbol; pos++) {
*pos = i64swp(*pos);
}
}
return 0;
}
为什么需要这些函数:
_paging_init在页表初始化阶段运行- 需要调用 memblock API 分配物理内存
- 但此时还不能使用
kallsyms_lookup_name - 所以提前在 kptools 中获取这些函数的地址
3. fillin_patch_config() - 填充要 hook 的函数
// tools/symbol.c:104-137
int fillin_patch_config(kallsym_t *kallsym, char *img_buf, int imglen,
patch_config_t *symbol, int32_t target_is_be, bool is_android)
{
// 核心函数
symbol->panic = get_symbol_offset_zero(kallsym, img_buf, "panic");
symbol->rest_init = try_get_symbol_offset_zero(kallsym, img_buf, "rest_init");
if (!symbol->rest_init)
symbol->cgroup_init = try_get_symbol_offset_zero(kallsym, img_buf, "cgroup_init");
symbol->kernel_init = try_get_symbol_offset_zero(kallsym, img_buf, "kernel_init");
// KCFI 相关
symbol->report_cfi_failure = get_symbol_offset_zero(kallsym, img_buf, "report_cfi_failure");
symbol->__cfi_slowpath_diag = get_symbol_offset_zero(kallsym, img_buf, "__cfi_slowpath_diag");
symbol->__cfi_slowpath = get_symbol_offset_zero(kallsym, img_buf, "__cfi_slowpath");
// 任务管理
symbol->copy_process = try_get_symbol_offset_zero(kallsym, img_buf, "copy_process");
if (!symbol->copy_process)
symbol->cgroup_post_fork = get_symbol_offset_zero(kallsym, img_buf, "cgroup_post_fork");
// SELinux 相关(Android)
symbol->avc_denied = try_get_symbol_offset_zero(kallsym, img_buf, "avc_denied");
symbol->slow_avc_audit = try_get_symbol_offset_zero(kallsym, img_buf, "slow_avc_audit");
// 其他
symbol->input_handle_event = get_symbol_offset_zero(kallsym, img_buf, "input_handle_event");
return 0;
}
patch_config_t 结构:
// kernel/include/preset.h:106-134
struct patch_config {
uint64_t kallsyms_lookup_name; // 符号查找函数
uint64_t printk; // 打印函数
uint64_t panic; // Panic 处理
uint64_t rest_init; // 初始化入口 1
uint64_t cgroup_init; // 初始化入口 2(备用)
uint64_t kernel_init; // 内核初始化
uint64_t report_cfi_failure; // CFI 失败报告
uint64_t __cfi_slowpath_diag; // CFI 慢路径
uint64_t __cfi_slowpath; // CFI 慢路径
uint64_t copy_process; // 进程创建
uint64_t cgroup_post_fork; // Fork 后回调
uint64_t avc_denied; // SELinux 拒绝
uint64_t slow_avc_audit; // SELinux 审计
uint64_t input_handle_event; // 输入事件处理
uint8_t patch_su_config; // SU 配置
};
4. 修改内核入口点
// tools/patch.c:553-557
int paging_init_offset = get_symbol_offset_exit(&kallsym, kallsym_kimg, "paging_init");
setup->paging_init_offset = relo_branch_func(kallsym_kimg, paging_init_offset);
int text_offset = align_kimg_len + SZ_4K;
b((uint32_t *)(out_kernel_file.kimg + kinfo->b_stext_insn_offset),
kinfo->b_stext_insn_offset, text_offset);
relo_branch_func() - 处理函数开头的跳转:
// tools/common.c:27-38
int32_t relo_branch_func(const char *img, int32_t func_offset)
{
uint32_t inst = *(uint32_t *)(img + func_offset);
int32_t relo_offset = func_offset;
// 如果函数第一条指令是 B(跳转到实际代码)
if (INSN_IS_B(inst)) {
uint64_t imm26 = bits32(inst, 25, 0);
uint64_t imm64 = sign64_extend(imm26 << 2u, 28u);
relo_offset = func_offset + (int32_t)imm64;
tools_logi("relocate branch function 0x%x to 0x%x\n",
func_offset, relo_offset);
}
return relo_offset;
}
b() - 生成 B 跳转指令:
// tools/common.c:18-25
int b(uint32_t *buf, uint64_t from, uint64_t to)
{
if (can_b_imm(from, to)) {
// B 指令:0x14000000 | ((offset >> 2) & 0x3FFFFFF)
buf[0] = 0x14000000u | (((to - from) & 0x0FFFFFFFu) >> 2u);
return 4;
}
return 0;
}
效果:
修补前:
0x0000: 14020000 B 0x80000 ; 跳到 _stext
修补后:
0x0000: 141D0400 B 0x1341000 ; 跳到 setup_entry
Extra Items - 嵌入额外资源
Extra Item 结构
// kernel/include/preset.h:177-195
struct _patch_extra_item {
char magic[4]; // "kpe"
int32_t priority; // 优先级(加载顺序)
int32_t args_size; // 参数大小
int32_t con_size; // 内容大小
extra_item_type type; // 类型
char name[32]; // 名称
char event[32]; // 触发事件
// 后面跟着:
// - args[args_size]
// - content[con_size]
};
// 类型定义
#define EXTRA_TYPE_KPM 1 // KPM 模块
#define EXTRA_TYPE_SHELL 2 // Shell 脚本
#define EXTRA_TYPE_EXEC 3 // 可执行文件
#define EXTRA_TYPE_RAW 4 // 原始数据
#define EXTRA_TYPE_ANDROID_RC 5 // Android init.rc
嵌入示例
# 嵌入 KPM 模块(在 pre-kernel-init 事件加载)
kptools -p -i Image -k kpimg -s mykey \
-M hook.kpm -T kpm -N "my-hook" -V "pre-kernel-init" \
-o Image.patched
# 嵌入 Shell 脚本(在 post-kernel-init 执行)
kptools -p -i Image -k kpimg -s mykey \
-M init.sh -T shell -V "post-kernel-init" \
-o Image.patched
# 嵌入多个 items
kptools -p -i Image -k kpimg -s mykey \
-M mod1.kpm -T kpm -V "pre-kernel-init" \
-M mod2.kpm -T kpm -V "pre-kernel-init" \
-M script.sh -T shell -V "post-kernel-init" \
-o Image.patched
加载时机
// 事件定义(kernel/include/preset.h:162-175)
#define EXTRA_EVENT_PAGING_INIT "paging-init"
#define EXTRA_EVENT_PRE_KERNEL_INIT "pre-kernel-init" // KPM 默认
#define EXTRA_EVENT_POST_KERNEL_INIT "post-kernel-init"
#define EXTRA_EVENT_PRE_FIRST_STAGE "pre-init-first-stage"
#define EXTRA_EVENT_POST_FIRST_STAGE "post-init-first-stage"
#define EXTRA_EVENT_PRE_EXEC_INIT "pre-exec-init"
#define EXTRA_EVENT_POST_EXEC_INIT "post-exec-init"
其他重要功能
unpatch_img() - 还原镜像
// tools/patch.c:621-640
int unpatch_img(const char *kimg_path, const char *out_path)
{
kernel_file_t kernel_file;
read_kernel_file(kimg_path, &kernel_file);
// 查找 preset
preset_t *preset = get_preset(kernel_file.kimg, kernel_file.kimg_len);
if (!preset) tools_loge_exit("not patched kernel image\n");
// 恢复头部
memcpy(kernel_file.kimg, preset->setup.header_backup,
sizeof(preset->setup.header_backup));
// 截断到原始大小
int kimg_size = preset->setup.kimg_size ?: ((char *)preset - kernel_file.kimg);
update_kernel_file_img_len(&kernel_file, kimg_size, false);
// 写入
write_kernel_file(&kernel_file, out_path);
return 0;
}
dump_kallsym() - 导出符号表
// tools/patch.c:669-686
int dump_kallsym(const char *kimg_path)
{
kernel_file_t kernel_file;
read_kernel_file(kimg_path, &kernel_file);
kallsym_t kallsym;
if (analyze_kallsym_info(&kallsym, kernel_file.kimg,
kernel_file.kimg_len, ARM64, 1)) {
fprintf(stdout, "analyze_kallsym_info error\n");
return -1;
}
dump_all_symbols(&kallsym, kernel_file.kimg);
return 0;
}
输出格式:
0x00080000 T _stext
0x00080040 T _text
0x00080100 t el1h_64_sync
0x00982b0 T kallsyms_lookup_name
...
reset_key() - 重置 SuperKey
// tools/patch.c:642-667
int reset_key(const char *kimg_path, const char *out_path, const char *superkey)
{
kernel_file_t kernel_file;
read_kernel_file(kimg_path, &kernel_file);
preset_t *preset = get_preset(kernel_file.kimg, kernel_file.kimg_len);
if (!preset) tools_loge_exit("not patched kernel image\n");
// 修改 superkey
char *origin_key = strdup((char *)preset->setup.superkey);
strcpy((char *)preset->setup.superkey, superkey);
tools_logi("reset superkey: %s -> %s\n", origin_key, preset->setup.superkey);
write_kernel_file(&kernel_file, out_path);
return 0;
}
实际使用案例
案例 1:修补 Pixel 6 内核
# 1. 提取内核
unpack_bootimg.py --boot_img boot.img
# 输出:kernel, ramdisk, dtb, ...
# 2. 分析内核
./kptools -d -i kernel > symbols.txt
head symbols.txt
# 0x00080000 T _stext
# 0x00982b0 T kallsyms_lookup_name
# ...
# 3. 修补
./kptools -p -i kernel -k ../kernel/kpimg -s "my_secret_key_123" -o kernel.patched
# layout kimg: 0x0,0x1ab2c38, kpimg: 0x1ab3000,0x1f2d40,
# extra: 0x1ca5d40,0x0, end: 0x1ca5d40, start: 0x1b00000
# patch done: kernel.patched
# 4. 重新打包
mkbootimg --kernel kernel.patched \
--ramdisk ramdisk \
--dtb dtb \
--header_version 3 \
--os_version 13.0.0 \
--os_patch_level 2024-01 \
-o boot.patched.img
# 5. 刷入
fastboot flash boot boot.patched.img
fastboot reboot
案例 2:嵌入自定义 KPM
# 编译 KPM
cd kpms/my-module
make
# 修补时嵌入
cd ../../tools
./kptools -p -i kernel -k ../kernel/kpimg -s mykey \
-M ../kpms/my-module/module.kpm \
-T kpm \
-N "my-custom-module" \
-V "pre-kernel-init" \
-A "arg1=value1 arg2=value2" \
-o kernel.patched
# 查看嵌入的 items
./kptools -l -i kernel.patched
# [EXTRA]
# num=1
# [EXTRA-0]
# index=0
# type=kpm
# name=my-custom-module
# event=pre-kernel-init
# args=arg1=value1 arg2=value2
Boot 镜像处理 - bootimg.c(0.13.0+ 新增)
bootimg.c(1092 行)是 0.13.0 新增的核心文件,提供 Android boot.img 的完整解包/重打包能力。
支持的压缩格式
| 格式 | Magic bytes | 压缩 | 解压 |
|---|---|---|---|
| GZIP | 1F 8B |
level 9 | zlib |
| LZ4 Frame | 04 22 4D 18 |
LZ4F | LZ4F |
| LZ4 Legacy | 02 21 4C 18 |
LZ4_compress_HC (level 12) | LZ4 |
| BZIP2 | 42 5A 68 |
- | bzlib |
| XZ | FD 37 7A 58 |
- | xz_dec |
| LZMA | 5D 00 00 |
- | xz_dec |
| ZSTD | 28 B5 2F FD |
识别但未实现 | - |
| Raw | 无压缩 | - | - |
核心函数
// 从 boot.img 提取内核(自动检测压缩格式并解压)
int extract_kernel(const char *bootimg_path); // 第 649 行
// 重打包 boot.img(自动用原格式重压缩)
int repack_bootimg(const char *orig_boot_path, // 第 726 行
const char *new_kernel_path,
const char *out_boot_path);
// 自动检测压缩格式
int detect_compress_method(compress_head data); // 第 692 行
AVB Footer 处理
重打包时自动处理 AVB (Android Verified Boot) 签名:
- 搜索 "AVB0" 标记(尝试版本 0x00/0x01/0x02)
- 找到最后一个 AVB footer
- 更新
data_size字段反映新镜像大小
SHA 校验和
根据原始镜像使用的哈希算法(SHA256 或 SHA1),重新计算校验和,包含 kernel + ramdisk + second + dtb 等所有组件。
使用示例
# 完整的 boot.img 修补流程
kptools unpack boot.img # 提取 kernel 文件
kptools -p -i kernel -k kpimg -s key -o kernel # 修补
kptools repack boot.img # 输出 new-boot.img
fastboot flash boot new-boot.img # 刷入
符号锚点查找 - symbol.c 新增机制(0.13.0+)
symbol.c 新增了 select_symbol_lookup_anchor_offset() 函数,通过运行时扫描替代 preset 中的硬编码偏移来定位 kallsyms_lookup_name。
锚点候选符号(14 个)
// tools/symbol.c:97-112
const char *anchors[] = {
"show_stack", "dump_backtrace", "nmi_panic", "panic",
"show_freq_kernel_log", "input_handle_event",
"slow_avc_audit", "avc_denied",
"tcp_init_sock", "udp_init_sock",
"inet_create", "inet_release", "sock_init_data", "sk_alloc"
};
运行时扫描原理
在内核启动时,使用 sprintf("%pSb", addr) 格式化锚点地址,获取符号名+偏移+大小信息,然后向前扫描直到找到 kallsyms_lookup_name。
编译 kptools
使用 Makefile
cd tools
make
# 输出:kptools 可执行文件
使用 CMake
cd tools
mkdir build && cd build
cmake ..
make
# 输出:build/kptools
Windows 编译
# 需要 MinGW 或 MSYS2
cd tools
mkdir build && cd build
cmake -G "MinGW Makefiles" ..
mingw32-make
故障排查
问题 1:找不到符号
./kptools -p -i kernel -k kpimg -s key -o out.img
# Error: no symbol kallsyms_lookup_name
原因:
- 内核未开启
CONFIG_KALLSYMS=y - 内核镜像损坏
解决:
# 检查内核配置
./kptools -f -i kernel | grep KALLSYMS
# 应该有:CONFIG_KALLSYMS=y
问题 2:patch overlap 警告
./kptools -p -i kernel -k kpimg -s key -o out.img
# Warning: patch overlap, move start from 0x1400000 to 0x1640000
原因:
- kpimg + extra 的总大小 > kernel_size
- 在磁盘镜像中会覆盖内核运行时内存区域
解决:
- 这是正常的自动调整,无需担心
- kptools 会自动计算安全的 start_offset
问题 3:UEFI vs 非 UEFI
# UEFI 内核
./kptools -p -i kernel -k kpimg -s key -o out.img
# kernel uefi header: true
# kernel primary entry: 0x80040 (偏移 0x4 处的 B 指令)
# 非 UEFI 内核
# kernel uefi header: false
# kernel primary entry: 0x80000 (偏移 0x0 处的 B 指令)
kptools 自动检测并处理:
// tools/image.c:66-77
kinfo->uefi = !strncmp((const char *)khdr->hdr.efi.mz, "MZ", 2);
if (kinfo->uefi) {
b_primary_entry_insn = khdr->hdr.efi.b_insn;
b_stext_insn_offset = 4; // MZ 后面 4 字节
} else {
b_primary_entry_insn = khdr->hdr.nefi.b_insn;
b_stext_insn_offset = 0; // 直接从 0 开始
}
工具输出详解
完整的 patch 输出
$ ./kptools -p -i Image -k kpimg -s mykey123 -o Image.patched
kernel image_size: 0x01ab2c38
kernel uefi header: false
kernel load_offset: 0x00000000
kernel kernel_size: 0x01b00000
kernel page_shift: 12
linux_banner 1: Linux version 5.10.149-android13...
linux_banner offset: 0x1a96258
kernel version major: 5, minor: 10, patch: 149
kallsyms_token_table offset: 0x01a3e610
kallsyms_token_index offset: 0x01a3ea14
arm64 relocation kernel_va: 0xffffffc008000000
apply 0x00011a3f relocation entries
kallsyms_markers range: [0x01a44c80, 0x01a45080), count: 0x00000100
approximate kallsyms_offsets range: [0x00f9be60, 0x01a3e608) count: 0x000280e9
kallsyms_names offset: 0x01a3c418
kallsyms_num_syms offset: 0x01a44c7c, value: 0x000280e9
names table vector index: 0x0000002c, pid_vnr index: 0x0001e5a6
vectors index: 44, offset: 0x00080800
pid_vnr offset: 0x004b4c00
pid_vnr verfied sp_el0, insn: 0xd53bd040
kallsyms_offsets offset: 0x00f9be60
kallsyms_lookup_name: type: T, offset: 0x00982b0
map_start: 0xd73550, max_size: 0x800
layout kimg: 0x0,0x1ab2c38, kpimg: 0x1ab3000,0x1f2d40,
extra: 0x1ca5d40,0x0, end: 0x1ca5d40, start: 0x1b00000
kpimg version: c00
kpimg compile time: 16:23:45 Nov 2 2025
kpimg config: android, release
printk: type: T, offset: 0x0028eab0
paging_init: type: t, offset: 0x01abe5d0
panic: type: T, offset: 0x0028db4c
rest_init: type: t, offset: 0x01abd710
kernel_init: type: t, offset: 0x01abd820
copy_process: type: t, offset: 0x002c16d8
avc_denied: type: t, offset: 0x009f9f04
patch done: Image.patched
关键信息解读:
kernel_size: 0x01b00000(27MB):内核运行时大小kallsyms_lookup_name: 0x00982b0:关键符号地址map_start: 0xd73550:tcp_init_sock 地址layout ... start: 0x1b00000:kpimg 运行时位置(27MB)- 成功获取所有需要 hook 的函数地址
数据流图
┌──────────────┐
│ Image │
│ (原始内核) │
└──────┬───────┘
│
├─ read_kernel_file()
│ └─ 检测 UNCOMPRESSED_IMG
│
├─ get_kernel_info()
│ ├─ 解析 ARM64 头部
│ ├─ 检测 UEFI
│ ├─ 提取 kernel_size
│ └─ 提取 page_shift
│
├─ analyze_kallsym_info()
│ ├─ find_linux_banner() → 获取版本
│ ├─ find_token_table() → 解析符号压缩表
│ ├─ try_find_arm64_relo_table() → 应用重定位
│ ├─ find_markers() → 定位标记表
│ ├─ find_names() → 定位符号名称表
│ └─ correct_addresses_or_offsets() → 验证并修正
│
└─ get_symbol_offset()
└─ 查询任意符号的偏移
┌──────────────┐ ┌──────────────┐
│ kpimg │ + │ Extra Items │
│ (patch代码) │ │ (KPM等) │
└──────┬───────┘ └──────┬───────┘
│ │
└──────────┬───────────┘
│
├─ new_kernel_file()
│ └─ 分配新镜像缓冲区
│
├─ 复制原始内核
├─ 附加 kpimg
├─ 附加 extra items
│
├─ 填充 preset
│ ├─ setup->setup_offset
│ ├─ setup->start_offset
│ ├─ setup->map_offset
│ ├─ setup->kallsyms_lookup_name_offset
│ ├─ setup->patch_config
│ ├─ setup->superkey
│ └─ setup->header_backup
│
├─ 修改入口点
│ └─ b(kimg[0], 0, setup_entry_offset)
│
└─ write_kernel_file()
┌──────────────┐
│ Image.patched│
│ (修补后镜像) │
└──────────────┘
总结
kptools 的核心职责:
- ✅ kallsyms 逆向:从 stripped kernel 获取所有符号
- ✅ 布局计算:确定 kpimg 的存储和运行时位置
- ✅ 镜像组装:将 kpimg、extra items 附加到内核
- ✅ 配置填充:准备 kpimg 运行所需的所有数据
- ✅ 入口劫持:修改内核第一条指令
- ✅ 镜像管理:patch/unpatch/dump/list 等功能
kptools 的输出:
- 一个完整的、可启动的、已修补的内核镜像
- 其中包含了 kpimg 运行所需的所有信息
- 启动时会自动接管内核初始化流程
下一篇:03-kallsyms逆向工程.md - 深入解析如何从二进制镜像中提取符号表
文档版本:2.0
最后更新:2026-06-26
评论
- 还没有评论,来说点什么吧。