溺的文档
KernelPatch · 第 2 篇 / 共 22 篇

kptools 详解 - 用户空间镜像修补工具

2026-06-29 · 阅读 2

概述

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), &current_offset);
        
        // 复制参数
        if (item->args_size > 0) {
            extra_append(out_kernel_file.kimg, (void *)config->set_args, 
                         item->args_size, &current_offset);
        }
        
        // 复制内容(KPM 二进制或脚本等)
        extra_append(out_kernel_file.kimg, (void *)config->data, 
                     item->con_size, &current_offset);
    }
    
    // 空 item 作为结束标志
    patch_extra_item_t empty_item = { 0 };
    extra_append(out_kernel_file.kimg, (void *)&empty_item, 
                 sizeof(empty_item), &current_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 (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 的核心职责

  1. kallsyms 逆向:从 stripped kernel 获取所有符号
  2. 布局计算:确定 kpimg 的存储和运行时位置
  3. 镜像组装:将 kpimg、extra items 附加到内核
  4. 配置填充:准备 kpimg 运行所需的所有数据
  5. 入口劫持:修改内核第一条指令
  6. 镜像管理:patch/unpatch/dump/list 等功能

kptools 的输出

  • 一个完整的、可启动的、已修补的内核镜像
  • 其中包含了 kpimg 运行所需的所有信息
  • 启动时会自动接管内核初始化流程

下一篇03-kallsyms逆向工程.md - 深入解析如何从二进制镜像中提取符号表


文档版本:2.0
最后更新:2026-06-26

评论

  • 还没有评论,来说点什么吧。

无需注册或登录,填个昵称即可评论。