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

kallsyms 逆向工程详解

2026-06-29 · 阅读 0

概述

kallsyms 是 Linux 内核的运行时符号表机制。当内核编译时开启 CONFIG_KALLSYMS=y,所有符号信息会被压缩并嵌入到内核镜像中。

KernelPatch 的核心技术之一就是从没有符号表的 stripped kernel image 中逆向还原出完整的符号表。

核心文件tools/kallsym.c(1151 行)


kallsyms 数据结构

内核中的 kallsyms 布局

// Linux kernel: kernel/kallsyms.c

// 绝对地址模式(内核 < 4.6)
kallsyms_addresses[]        // uint64_t[], 符号的虚拟地址
kallsyms_num_syms          // 符号总数
kallsyms_names[]           // 压缩的符号名称
kallsyms_markers[]         // 每 256 个符号的索引
kallsyms_token_table[]     // 压缩 token 字符串
kallsyms_token_index[]     // token 索引

// 相对偏移模式(内核 >= 4.6)
kallsyms_offsets[]         // int32_t[], 相对偏移
kallsyms_relative_base     // 基地址
kallsyms_num_syms
kallsyms_names[]
kallsyms_markers[]
kallsyms_token_table[]
kallsyms_token_index[]

符号压缩原理

为什么要压缩?

  • 内核有 10 万+ 符号,直接存储符号名会占用大量空间
  • 使用 token 表压缩,可减少 50% 以上体积

压缩方法

  1. Token Table:256 个常用字符串

    token[0] = "t"
    token[1] = "a"
    token[2] = "n"
    ...
    token[50] = "_init"
    token[51] = "_exit"
    ...
  2. 符号编码:将符号名拆分为 token 序列

    原始符号:t_start_kernel
    
    编码:
    [长度=9]
    [token_id: 't']  -> 符号类型
    [token_id: '_']
    [token_id: 's']
    [token_id: 't']
    [token_id: 'a']
    [token_id: 'r']
    [token_id: 't']
    
    存储:0x09 0x74 0x5F 0x73 0x74 0x61 0x72 0x74

逆向算法详解

主函数:analyze_kallsym_info()

v2.0 更新analyze_kallsym_info 现在使用三阶段重试机制,每个阶段独立复制镜像副本并调用 retry_relo()。三个阶段分别对应:正常流程(含重定位)、禁用重定位、使用默认 kernel_base。这使得算法在面对各种异构固件时具备更强的鲁棒性。

// tools/kallsym.c:850-894
int analyze_kallsym_info(kallsym_t *info, char *img, int32_t imglen, 
                         enum arch_type arch, int32_t is_64)
{
    // 初始化
    memset(info, 0, sizeof(kallsym_t));
    info->is_64 = is_64;
    info->asm_long_size = 4;
    info->asm_PTR_size = is_64 ? 8 : 4;
    if (arch == ARM64) info->try_relo = 1;  // 尝试应用重定位
    
    // ============ 第一阶段:基础搜索 ============
    static int32_t (*base_funcs[])(kallsym_t *, char *, int32_t) = {
        find_linux_banner,       // 查找 Linux 版本字符串
        find_token_table,        // 查找 token 表
        find_token_index,        // 查找 token 索引
    };
    
    for (int i = 0; i < sizeof(base_funcs) / sizeof(base_funcs[0]); i++) {
        if ((rc = base_funcs[i](info, img, imglen))) return rc;
    }
    
    // ============ 第二阶段:三阶段重试机制 ============
    
    // 复制一份镜像用于尝试
    char *copied_img = (char *)malloc(imglen);
    memcpy(copied_img, img, imglen);
    
    // 尝试 1:正常流程(可能应用重定位)
    rc = retry_relo(info, copied_img, imglen);
    if (!rc) goto out;
    
    // 尝试 2:不应用重定位
    if (!info->try_relo) {
        memcpy(copied_img, img, imglen);
        rc = retry_relo(info, copied_img, imglen);
        if (!rc) goto out;
    }
    
    // 尝试 3:使用默认 kernel_base
    if (info->kernel_base != ELF64_KERNEL_MIN_VA) {
        info->kernel_base = ELF64_KERNEL_MIN_VA;  // 0xffffffc008000000
        memcpy(copied_img, img, imglen);
        rc = retry_relo(info, copied_img, imglen);
    }
    
out:
    // 将处理后的镜像拷贝回去(可能已应用重定位)
    memcpy(img, copied_img, imglen);
    free(copied_img);
    return rc;
}

retry_relo() - 核心处理流程

// tools/kallsym.c:826-839
static int retry_relo(kallsym_t *info, char *img, int32_t imglen)
{
    static int32_t (*funcs[])(kallsym_t *, char *, int32_t) = {
        try_find_arm64_relo_table,      // 1. 尝试应用重定位
        find_markers,                    // 2. 查找 markers
        find_approx_addresses_or_offset, // 3. 查找地址/偏移表
        find_names,                      // 4. 查找符号名称表
        find_num_syms,                   // 5. 查找符号总数
        correct_addresses_or_offsets     // 6. 精确定位并验证
    };
    
    for (int i = 0; i < sizeof(funcs) / sizeof(funcs[0]); i++) {
        if ((rc = funcs[i](info, img, imglen))) break;
    }
    return rc;
}

步骤 1:find_linux_banner()

查找 Linux 版本字符串

// tools/kallsym.c:42-86
static int find_linux_banner(kallsym_t *info, char *img, int32_t imglen)
{
    char linux_banner_prefix[] = "Linux version ";
    size_t prefix_len = strlen(linux_banner_prefix);
    
    char *imgend = img + imglen;
    char *banner = (char *)img;
    info->banner_num = 0;
    
    // 搜索所有匹配的 banner
    while ((banner = memmem(banner + 1, imgend - banner - 1, 
                            linux_banner_prefix, prefix_len)) != NULL) {
        // 验证格式:Linux version X.Y.Z
        if (isdigit(*(banner + prefix_len)) && *(banner + prefix_len + 1) == '.') {
            info->linux_banner_offset[info->banner_num++] = (int32_t)(banner - img);
            tools_logi("linux_banner %d: %s", info->banner_num, banner);
        }
    }
    
    // 使用最后一个 banner
    banner = img + info->linux_banner_offset[info->banner_num - 1];
    
    // 解析版本号:Linux version 5.10.149-android13...
    char *uts_release_start = banner + prefix_len;
    char *dot = NULL;
    
    info->version.major = (uint8_t)strtoul(uts_release_start, &dot, 10);  // 5
    info->version.minor = (uint8_t)strtoul(dot + 1, &dot, 10);            // 10
    int32_t patch = (int32_t)strtoul(dot + 1, &dot, 10);                  // 149
    info->version.patch = patch <= 256 ? patch : 255;
    
    tools_logi("kernel version major: %d, minor: %d, patch: %d\n", 
               info->version.major, info->version.minor, info->version.patch);
    return 0;
}

为什么记录多个 banner?

  • 镜像中可能有多处包含 "Linux version" 字符串
  • 后续需要通过地址验证哪个是真正的 linux_banner 符号

步骤 2:find_token_table()

特征搜索算法

// tools/kallsym.c:101-160
static int find_token_table(kallsym_t *info, char *img, int32_t imglen)
{
    // 构造搜索模式
    char nums_syms[20] = { '\0' };
    for (int32_t i = 0; i < 10; i++)
        nums_syms[i * 2] = '0' + i;  // "0\x00 1\x00 2\x00 ... 9\x00"
    
    char letters_syms[20] = { '\0' };
    for (int32_t i = 0; i < 10; i++)
        letters_syms[i * 2] = 'a' + i;  // "a\x00 b\x00 c\x00 ... j\x00"
    
    // 搜索:数字序列 + 字母序列
    char *pos = img;
    char *num_start = NULL;
    for (; pos < imgend; pos = num_start + 1) {
        num_start = memmem(pos, imgend - pos, nums_syms, sizeof(nums_syms));
        if (!num_start) return -1;
        
        char *num_end = num_start + sizeof(nums_syms);
        if (!*num_end || !*(num_end + 1)) continue;
        
        // 检查后面是否跟着字母序列
        char *letter = num_end;
        for (int32_t i = 0; letter < imgend && i < 'a' - '9' - 1; letter++) {
            if (!*letter) i++;
        }
        if (letter == memmem(letter, sizeof(letters_syms), letters_syms, sizeof(letters_syms))) 
            break;
    }
    
    // 反向搜索找到起始位置
    pos = num_start;
    for (int32_t i = 0; pos > img && i < '0' + 1; pos--) {
        if (!*pos) i++;  // 向前找 '\0' 分隔符
    }
    
    int32_t offset = align_ceil(pos + 2 - img, 4);
    info->kallsyms_token_table_offset = offset;
    
    // 重建 token 表
    pos = img + offset;
    for (int32_t i = 0; i < KSYM_TOKEN_NUMS; i++) {
        info->kallsyms_token_table[i] = pos;
        while (*(pos++)) {};  // 跳到下一个 token
    }
    
    return 0;
}

Token Table 实际示例

地址      内容
0x1a3e610: "t\0"
0x1a3e612: "a\0"
0x1a3e614: "n\0"
0x1a3e616: "d\0"
0x1a3e618: "l\0"
0x1a3e61a: "e\0"
...
0x1a3e700: "_init\0"
0x1a3e706: "_exit\0"
0x1a3e70c: "kernel_\0"

步骤 3:find_token_index()

Token 索引表

// tools/kallsym.c:162-195
static int find_token_index(kallsym_t *info, char *img, int32_t imglen)
{
    uint16_t le_index[KSYM_TOKEN_NUMS] = { 0 };
    uint16_t be_index[KSYM_TOKEN_NUMS] = { 0 };
    
    int32_t start = info->kallsyms_token_table_offset;
    int32_t offset = start;
    
    // 构造理论上的 token_index
    for (int32_t i = 0; i < KSYM_TOKEN_NUMS; i++) {
        uint16_t token_index = offset - start;
        le_index[i] = u16le(token_index);  // 小端序
        be_index[i] = u16be(token_index);  // 大端序
        while (img[offset++]) {};
    }
    
    // 在镜像中搜索
    char *lepos = memmem(img, imglen, le_index, sizeof(le_index));
    char *bepos = memmem(img, imglen, be_index, sizeof(be_index));
    
    if (!lepos && !bepos) {
        tools_loge("kallsyms_token_index error\n");
        return -1;
    }
    
    // 确定字节序
    info->is_be = lepos ? 0 : 1;
    tools_logi("endian: %s\n", lepos ? "little" : "big");
    
    char *pos = lepos ? lepos : bepos;
    info->kallsyms_token_index_offset = pos - img;
    
    return 0;
}

Token Index 格式

uint16_t kallsyms_token_index[256] = {
    0x0000,  // token[0] 在 token_table 中的偏移
    0x0002,  // token[1] 的偏移
    0x0004,  // token[2] 的偏移
    ...
};

步骤 4:try_find_arm64_relo_table()

ARM64 重定位表处理

// tools/kallsym.c:227-319
static int try_find_arm64_relo_table(kallsym_t *info, char *img, int32_t imglen)
{
    if (!info->try_relo) return 0;
    
    uint64_t min_va = ELF64_KERNEL_MIN_VA;  // 0xffffffc008000000
    uint64_t max_va = ELF64_KERNEL_MAX_VA;  // 0xffffffffffffffff
    uint64_t kernel_va = max_va;
    
    int32_t cand = 0;
    int rela_num = 0;
    
    // ============ 搜索重定位表 ============
    
    while (cand < imglen - 24) {
        uint64_t r_offset = uint_unpack(img + cand, 8, info->is_be);
        uint64_t r_info = uint_unpack(img + cand + 8, 8, info->is_be);
        uint64_t r_addend = uint_unpack(img + cand + 16, 8, info->is_be);
        
        // 检查 R_AARCH64_RELATIVE (0x403)
        if ((r_offset & 0xffff000000000000) == 0xffff000000000000 && 
            r_info == 0x403) {
            // 从 r_addend 推断 kernel_va
            if (!(r_addend & 0xfff) && r_addend >= min_va && r_addend < kernel_va)
                kernel_va = r_addend;
            cand += 24;
            rela_num++;
        } else if (rela_num && !r_offset && !r_info && !r_addend) {
            // 空项
            cand += 24;
            rela_num++;
        } else {
            if (rela_num >= ARM64_RELO_MIN_NUM) break;
            cand += 8;
            rela_num = 0;
            kernel_va = max_va;
        }
    }
    
    if (info->kernel_base) {
        tools_logi("arm64 relocation kernel_va: 0x%" PRIx64 ", try: %" PRIx64 "\n", 
                   kernel_va, info->kernel_base);
        kernel_va = info->kernel_base;
    } else {
        info->kernel_base = kernel_va;
        tools_logi("arm64 relocation kernel_va: 0x%" PRIx64 "\n", kernel_va);
    }
    
    
    // ============ 应用重定位 ============
    
    int32_t cand_start = cand - 24 * rela_num;
    int32_t cand_end = cand - 24;
    
    // 去除末尾的空项
    while (1) {
        if (*(uint64_t *)(img + cand_end) && 
            *(uint64_t *)(img + cand_end + 8) && 
            *(uint64_t *)(img + cand_end + 16))
            break;
        cand_end -= 24;
    }
    cand_end += 24;
    
    rela_num = (cand_end - cand_start) / 24;
    tools_logi("arm64 relocation table range: [0x%08x, 0x%08x), count: 0x%08x\n", 
               cand_start, cand_end, rela_num);
    
    // 应用每个重定位项
    int32_t max_offset = imglen - 8;
    int32_t apply_num = 0;
    
    for (cand = cand_start; cand < cand_end; cand += 24) {
        uint64_t r_offset = uint_unpack(img + cand, 8, info->is_be);
        uint64_t r_info = uint_unpack(img + cand + 8, 8, info->is_be);
        uint64_t r_addend = uint_unpack(img + cand + 16, 8, info->is_be);
        
        if (!r_offset && !r_info && !r_addend) continue;
        if (r_offset <= kernel_va || r_offset >= max_va - imglen) continue;
        
        int32_t offset = r_offset - kernel_va;
        if (offset < 0 || offset >= max_offset) {
            tools_logw("bad rela offset: 0x%" PRIx64 "\n", r_offset);
            info->try_relo = 0;
            return -1;
        }
        
        // 修正地址
        uint64_t value = uint_unpack(img + offset, 8, info->is_be);
        if (value == r_addend) continue;
        *(uint64_t *)(img + offset) = value + r_addend;
        apply_num++;
    }
    
    tools_logi("apply 0x%08x relocation entries\n", apply_num);
    if (apply_num) info->relo_applied = 1;
    
    return 0;
}

重定位表示例

ELF64_Rela[0]:
  r_offset = 0xffffffc008f9be60  (kallsyms_offsets 的虚拟地址)
  r_info   = 0x0000000000000403  (R_AARCH64_RELATIVE)
  r_addend = 0xffffffc008000000  (kernel_va)

应用重定位:
  *(img + 0xf9be60) += 0xffffffc008000000

为什么需要重定位?

  • ARM64 内核使用 KASLR(地址随机化)
  • 镜像中的地址是相对的,需要加上实际的 kernel_va
  • 应用重定位后,kallsyms_addresses 才是正确的绝对地址

步骤 5:find_markers()

反向搜索标记表

// tools/kallsym.c:478-520
static int find_markers_internal(kallsym_t *info, char *img, int32_t imglen, 
                                 int32_t elem_size)
{
    int32_t cand = info->kallsyms_token_table_offset;
    
    int64_t marker, last_marker = imglen;
    int count = 0;
    
    // 从 token_table 往前搜索
    while (cand > 0x10000) {
        marker = int_unpack(img + cand, elem_size, info->is_be);
        
        // marker 必须递减
        if (last_marker > marker) {
            count++;
            if (!marker && count > KSYM_MIN_MARKER) break;
        } else {
            count = 0;
            last_marker = imglen;
        }
        
        last_marker = marker;
        cand -= elem_size;
    }
    
    if (count < KSYM_MIN_MARKER) {
        tools_logw("find kallsyms_markers error\n");
        return -1;
    }
    
    info->kallsyms_markers_offset = cand;
    info->_marker_num = count;
    info->kallsyms_markers_elem_size = elem_size;
    
    tools_logi("kallsyms_markers range: [0x%08x, 0x%08x), count: 0x%08x\n", 
               cand, marker_end, count);
    return 0;
}

Markers 的作用

// 每 256 个符号设置一个标记
kallsyms_markers[0] = 0;        // 第 0 个符号在 names 中的偏移
kallsyms_markers[1] = 0x1234;   // 第 256 个符号的偏移
kallsyms_markers[2] = 0x2468;   // 第 512 个符号的偏移
...

为什么需要 markers?

  • 快速跳转到任意符号
  • 不需要从头开始解码所有符号名

步骤 6:find_approx_addresses_or_offset()

查找地址/偏移表

v2.0 更新:ARM64 架构现在优先尝试 offsets 模式,不再仅依据内核版本号判断。即使是 4.6 以下的内核,只要镜像中存在有效的 offsets 数据结构,也会被正确识别。这一改变提高了对非标准编译配置内核的兼容性。

// tools/kallsym.c:435-445
static int32_t find_approx_addresses_or_offset(kallsym_t *info, char *img, int32_t imglen)
{
    int32_t ret = 0;
    
    // ARM64 优先尝试 offsets 模式(不再仅依据版本号)
    // 内核 >= 4.6 或 ARM64 架构:尝试查找 offsets
    if (info->version.major > 4 || 
        (info->version.major == 4 && info->version.minor >= 6)) {
        ret = find_approx_offsets(info, img, imglen);
        if (!ret) return 0;
    }
    
    // 回退:查找 addresses
    ret = find_approx_addresses(info, img, imglen);
    return ret;
}

find_approx_offsets() - 查找相对偏移表

// tools/kallsym.c:379-433
static int find_approx_offsets(kallsym_t *info, char *img, int32_t imglen)
{
    int32_t sym_num = 0;
    int32_t elem_size = info->asm_long_size;  // 4 字节
    int64_t prev_offset = 0;
    int32_t cand = 0;
    
    // 搜索连续递增的 int32_t 序列
    for (; cand < imglen - KSYM_MIN_NEQ_SYMS * elem_size; cand += elem_size) {
        int64_t offset = int_unpack(img + cand, elem_size, info->is_be);
        
        if (offset == prev_offset) {
            continue;  // 0 offset(同地址符号)
        } else if (offset > prev_offset) {
            prev_offset = offset;
            if (sym_num++ >= KSYM_MIN_NEQ_SYMS) break;
        } else {
            prev_offset = 0;
            sym_num = 0;
        }
    }
    
    // 向前找到真正的起始位置(可能有前导 0)
    cand -= KSYM_MIN_NEQ_SYMS * elem_size;
    for (;; cand -= elem_size)
        if (!int_unpack(img + cand, elem_size, info->is_be)) break;
    
    for (;; cand -= elem_size) {
        if (int_unpack(img + cand, elem_size, info->is_be)) break;
        if (zero_offset_num++ >= MAX_ZERO_OFFSET_NUM) break;
    }
    cand += elem_size;
    
    int32_t approx_offset = cand;
    info->_approx_addresses_or_offsets_offset = approx_offset;
    
    // 找到结束位置
    prev_offset = 0;
    for (; cand < imglen; cand += elem_size) {
        int64_t offset = int_unpack(img + cand, elem_size, info->is_be);
        if (offset < prev_offset) break;  // 不再递增
        prev_offset = offset;
    }
    
    info->_approx_addresses_or_offsets_end = cand;
    info->has_relative_base = 1;
    int32_t approx_num_syms = (cand - approx_offset) / elem_size;
    info->_approx_addresses_or_offsets_num = approx_num_syms;
    
    tools_logi("approximate kallsyms_offsets range: [0x%08x, 0x%08x) count: 0x%08x\n",
               approx_offset, cand, approx_num_syms);
    return 0;
}

为什么叫 "approximate"?

  • 此时只是找到了近似的范围
  • 可能包含了一些无关的递增数据
  • 需要后续通过特征验证精确定位

步骤 7:find_names()

符号名称表验证

// tools/kallsym.c:559-607
static int find_names(kallsym_t *info, char *img, int32_t imglen)
{
    int32_t marker_elem_size = get_markers_elem_size(info);
    int32_t cand = 0x4000;  // 从 16KB 开始搜索
    int32_t test_marker_num = -1;
    
    for (; cand < info->kallsyms_markers_offset; cand++) {
        int32_t pos = cand;
        test_marker_num = KSYM_FIND_NAMES_USED_MARKER;  // 验证 N 个 marker
        
        // 尝试解码符号名称
        for (int32_t i = 0;; i++) {
            // 读取长度字节
            int32_t len = *(uint8_t *)(img + pos++);
            if (len > 0x7F)  // 两字节编码
                len = (len & 0x7F) + (*(uint8_t *)(img + pos++) << 7);
            
            if (!len || len >= KSYM_SYMBOL_LEN) break;  // 不合法
            
            pos += len;
            if (pos >= info->kallsyms_markers_offset) break;
            
            // 每 256 个符号,验证 marker
            if (i && (i & 0xFF) == 0xFF) {
                int32_t mark_len = int_unpack(
                    img + info->kallsyms_markers_offset + 
                    ((i >> 8) + 1) * marker_elem_size,
                    marker_elem_size, info->is_be);
                
                // marker 记录的长度必须匹配
                if (pos - cand != mark_len) break;
                if (!--test_marker_num) break;
            }
        }
        
        if (!test_marker_num) break;  // 验证成功
    }
    
    if (test_marker_num) {
        tools_loge("find kallsyms_names error\n");
        return -1;
    }
    
    info->kallsyms_names_offset = cand;
    tools_logi("kallsyms_names offset: 0x%08x\n", cand);
    return 0;
}

验证逻辑

假设从 0x4000 开始尝试:
  pos = 0x4000
  
  解码第 0 个符号:len=9, pos += 9 -> 0x4009
  解码第 1 个符号:len=7, pos += 7 -> 0x4010
  ...
  解码第 255 个符号:pos = 0x4100
  解码第 256 个符号:pos = 0x4109
  
  验证:markers[1] == 0x4109 - 0x4000 = 0x109 ✅
  
  继续解码第 512 个符号...
  验证:markers[2] == pos - 0x4000 ✅
  
  重复多次,全部匹配 → 找到 kallsyms_names!

步骤 8:correct_addresses_or_offsets_by_vectors()

通过特征符号精确定位

// tools/kallsym.c:633-720
static int correct_addresses_or_offsets_by_vectors(kallsym_t *info, char *img, int32_t imglen)
{
    // ============ 步骤 1:在 names 表中找到特征符号的索引 ============
    
    int32_t pos = info->kallsyms_names_offset;
    int32_t index = 0, vector_index = 0, pid_vnr_index = 0;
    char symbol[KSYM_SYMBOL_LEN] = { '\0' };
    
    while (pos < info->kallsyms_markers_offset) {
        memset(symbol, 0, sizeof(symbol));
        decompress_symbol_name(info, img, &pos, NULL, symbol);
        
        if (!strcmp(symbol, "vectors")) {
            vector_index = index;
        } else if (!strcmp(symbol, "pid_vnr")) {
            pid_vnr_index = index;
        }
        
        if (vector_index && pid_vnr_index) break;
        index++;
    }
    
    
    // ============ 步骤 2:搜索正确的地址表起始位置 ============
    
    int32_t elem_size = info->has_relative_base ? 
                        get_offsets_elem_size(info) : 
                        get_addresses_elem_size(info);
    
    uint64_t base_cand[3] = { 0 };
    int base_cand_num = 1;
    
    if (!info->has_relative_base) {
        uint64_t base = uint_unpack(img + info->_approx_addresses_or_offsets_offset, 
                                    elem_size, info->is_be);
        base_cand[0] = base;
        if (info->kernel_base) base_cand[base_cand_num++] = info->kernel_base;
        if (info->kernel_base != ELF64_KERNEL_MIN_VA) 
            base_cand[base_cand_num++] = ELF64_KERNEL_MIN_VA;
    }
    
    int32_t search_start = info->_approx_addresses_or_offsets_offset;
    int32_t search_end = info->_approx_addresses_or_offsets_end - 
                         pid_vnr_index * elem_size;
    
    
    // ============ 步骤 3:验证 vectors 和 pid_vnr ============
    
    int break_flag = 0;
    for (int i = 0; i < base_cand_num; i++) {
        uint64_t base = base_cand[i];
        
        for (pos = search_start; pos < search_end; pos += elem_size) {
            // 读取 vectors 符号的地址
            int32_t vector_offset = 
                uint_unpack(img + pos + vector_index * elem_size, elem_size, info->is_be) - base;
            
            int32_t vector_next_offset = 
                uint_unpack(img + pos + vector_index * elem_size + elem_size, 
                            elem_size, info->is_be) - base;
            
            // vectors 必须 2KB 对齐 (.align 11)
            if (vector_next_offset - vector_offset >= 0x600 && 
                (vector_offset & ((1 << 11) - 1)) == 0) {
                
                // 验证 pid_vnr
                int32_t pid_vnr_offset = 
                    uint_unpack(img + pos + pid_vnr_index * elem_size, 
                                elem_size, info->is_be) - base;
                
                if (!arm64_verify_pid_vnr(info, img, pid_vnr_offset)) {
                    tools_logi("vectors index: %d, offset: 0x%08x\n", 
                               vector_index, vector_offset);
                    tools_logi("pid_vnr offset: 0x%08x\n", pid_vnr_offset);
                    info->kernel_base = base;
                    break_flag = 1;
                    break;
                }
            }
        }
        
        if (break_flag) break;
    }
    
    if (pos >= search_end) {
        tools_loge("can't locate vectors\n");
        return -1;
    }
    
    // 成功找到!
    if (info->has_relative_base) {
        info->kallsyms_offsets_offset = pos;
        tools_logi("kallsyms_offsets offset: 0x%08x\n", pos);
    } else {
        info->kallsyms_addresses_offset = pos;
        tools_logi("kallsyms_addresses offset: 0x%08x\n", pos);
        tools_logi("kernel base address: 0x%08llx\n", info->kernel_base);
    }
    
    return 0;
}

验证原理

  1. vectors 特征

    // arch/arm64/kernel/entry.S
    .align 11                    ; 2048 字节对齐
    SYM_CODE_START(vectors)
        ventry  el1h_sync        ; 0x000
        ventry  el1h_irq         ; 0x080
        ventry  el1h_fiq         ; 0x100
        ventry  el1h_error       ; 0x180
        ...                      ; 总共 16 个入口,每个 128 字节
    SYM_CODE_END(vectors)        ; 大小 = 0x800 (2KB)
    • 必须 2048 字节对齐(offset & 0x7FF == 0
    • 大小至少 0x600
  2. pid_vnr 特征

    // kernel/pid.c
    pid_t pid_vnr(struct pid *pid)
    {
        MRS X0, SP_EL0        ; 或 MOV X0, SP
        LDR X1, [X0, #offset] ; 访问 current->nsproxy
        ...
    }
    • 必定访问 current(存储在 sp_el0 或 sp)
    • 通过指令模式识别

arm64_verify_pid_vnr() - 验证函数

// tools/kallsym.c:609-631
static int arm64_verify_pid_vnr(kallsym_t *info, char *img, int32_t offset)
{
    for (int i = 0; i < 6; i++) {
        int32_t insn_offset = offset + i * 4;
        uint32_t insn = uint_unpack(img + insn_offset, 4, 0);
        enum aarch64_insn_encoding_class enc = aarch64_get_insn_class(insn);
        
        // 检查系统寄存器访问指令
        if (enc == AARCH64_INSN_CLS_BR_SYS) {
            if (aarch64_insn_extract_system_reg(insn) == AARCH64_INSN_SPCLREG_SP_EL0) {
                tools_logi("pid_vnr verfied sp_el0, insn: 0x%x\n", insn);
                info->current_type = SP_EL0;
                return 0;
            }
        } 
        // 检查立即数指令(使用 SP 寄存器)
        else if (enc == AARCH64_INSN_CLS_DP_IMM) {
            u32 rn = aarch64_insn_decode_register(AARCH64_INSN_REGTYPE_RN, insn);
            if (rn == AARCH64_INSN_REG_SP) {
                tools_logi("pid_vnr verfied sp, insn: 0x%x\n", insn);
                info->current_type = SP;
                return 0;
            }
        }
    }
    return -1;
}

指令示例

pid_vnr:
    0x000: D53BD040    MRS X0, SP_EL0           ; ✅ 匹配
    0x004: F9400001    LDR X1, [X0]
    ...

或:
pid_vnr:
    0x000: 910003E0    MOV X0, SP               ; ✅ 匹配(rn == SP)
    0x004: F9400001    LDR X1, [X0]
    ...

符号解压缩

decompress_symbol_name()

v2.0 更新:符号名称编码现在支持大于 0x7F 的可变长度编码。当第一个字节的最高位(bit 7)被设置时,长度字段扩展为两字节编码:len = (byte0 & 0x7F) + (byte1 << 7)。这允许单个符号名长度超过 127 字节,以应对极长的 C++ 符号(如模板实例化后的 mangled name)和某些编译器生成的带有长后缀的符号。

// tools/kallsym.c:522-540
static int decompress_symbol_name(kallsym_t *info, char *img, int32_t *pos_to_next, 
                                  char *out_type, char *out_symbol)
{
    int32_t pos = *pos_to_next;
    
    // 1. 读取长度(支持 >0x7F 的可变长度编码)
    int32_t len = *(uint8_t *)(img + pos++);
    if (len > 0x7F)  // 两字节编码:最高位为标志位
        len = (len & 0x7F) + (*(uint8_t *)(img + pos++) << 7);
    
    if (!len || len >= KSYM_SYMBOL_LEN) return -1;
    
    *pos_to_next = pos + len;
    
    // 2. 解压缩
    for (int32_t i = 0; i < len; i++) {
        int32_t tokidx = *(uint8_t *)(img + pos + i);
        char *token = info->kallsyms_token_table[tokidx];
        
        if (!i) {  // 第一个字符是符号类型
            if (out_type) *out_type = *token;
            token++;  // 跳过类型
        }
        
        if (out_symbol) strcat(out_symbol, token);
    }
    
    return 0;
}

可变长度编码细节

单字节编码(len <= 0x7F = 127):
  [len]
  示例:0x09 -> len = 9

双字节编码(len > 0x7F):
  [byte0 | 0x80] [byte1]
  len = (byte0 & 0x7F) + (byte1 << 7)
  
  示例:0x85 0x01 -> len = (0x85 & 0x7F) + (0x01 << 7) = 5 + 128 = 133
  
  最大可编码长度:127 + (255 << 7) = 127 + 32640 = 32767

符号类型

't' / 'T' = text (代码段)
'd' / 'D' = data (数据段)
'b' / 'B' = bss (未初始化数据)
'r' / 'R' = rodata (只读数据)
'a' / 'A' = absolute (绝对符号)
...
小写 = local,大写 = global

解压示例

原始数据:0x09 0x74 0x5F 0x73 0x74 0x61 0x72 0x74

解析:
  len = 0x09 (9 字节)
  token[0] = 0x74 -> "t"  (类型 = 't')
  token[1] = 0x5F -> "_"
  token[2] = 0x73 -> "s"
  token[3] = 0x74 -> "t"
  token[4] = 0x61 -> "a"
  token[5] = 0x72 -> "r"
  token[6] = 0x74 -> "t"

结果:type='t', name="_start"

符号查询接口

get_symbol_offset()

// tools/kallsym.c:940-957
int get_symbol_offset(kallsym_t *info, char *img, char *symbol)
{
    char decomp[KSYM_SYMBOL_LEN] = { '\0' };
    char type = 0;
    int32_t pos = info->kallsyms_names_offset;
    
    // 遍历所有符号
    for (int32_t i = 0; i < info->kallsyms_num_syms; i++) {
        memset(decomp, 0, sizeof(decomp));
        decompress_symbol_name(info, img, &pos, &type, decomp);
        
        if (!strcmp(decomp, symbol)) {
            int32_t offset = get_symbol_index_offset(info, img, i);
            tools_logi("%s: type: %c, offset: 0x%08x\n", symbol, type, offset);
            return offset;
        }
    }
    
    tools_logw("no symbol: %s\n", symbol);
    return -1;
}

get_symbol_index_offset()

// tools/kallsym.c:896-910
int32_t get_symbol_index_offset(kallsym_t *info, char *img, int32_t index)
{
    int32_t elem_size;
    int32_t pos;
    
    if (info->has_relative_base) {
        // 使用 offsets(内核 >= 4.6)
        elem_size = get_offsets_elem_size(info);  // 4 字节
        pos = info->kallsyms_offsets_offset;
        uint64_t target = uint_unpack(img + pos + index * elem_size, elem_size, info->is_be);
        return target;  // 返回相对偏移
    } else {
        // 使用绝对地址(内核 < 4.6)
        elem_size = get_addresses_elem_size(info);  // 8 字节
        pos = info->kallsyms_addresses_offset;
        uint64_t target = uint_unpack(img + pos + index * elem_size, elem_size, info->is_be);
        return (int32_t)(target - info->kernel_base);  // 转换为偏移
    }
}

完整的逆向流程图

analyze_kallsym_info()
  │
  ├─ 初始化结构体
  │   ├─ is_64 = 1
  │   ├─ asm_long_size = 4
  │   ├─ asm_PTR_size = 8
  │   └─ try_relo = 1
  │
  ├─ 【第一阶段:基础搜索】
  │   │
  │   ├─ find_linux_banner()
  │   │   ├─ 搜索 "Linux version "
  │   │   ├─ 提取版本号 (5.10.149)
  │   │   └─ 记录所有 banner 位置
  │   │
  │   ├─ find_token_table()
  │   │   ├─ 搜索 "0\x00 1\x00 ... a\x00 b\x00 ..."
  │   │   ├─ 反向找到起始位置
  │   │   └─ 重建 256 个 token 字符串
  │   │
  │   └─ find_token_index()
  │       ├─ 构造理论 index
  │       ├─ 在镜像中搜索匹配
  │       └─ 确定字节序 (little/big endian)
  │
  ├─ 【第二阶段:多次尝试】
  │   │
  │   ├─ 复制镜像到临时缓冲区
  │   │
  │   ├─ retry_relo() - 尝试 1
  │   │   ├─ try_find_arm64_relo_table()
  │   │   │   ├─ 搜索重定位表项 (r_info == 0x403)
  │   │   │   ├─ 推断 kernel_va
  │   │   │   └─ 应用重定位修正地址
  │   │   │
  │   │   ├─ find_markers()
  │   │   │   ├─ 从 token_table 往前搜索
  │   │   │   └─ 找到递减的 marker 序列
  │   │   │
  │   │   ├─ find_approx_addresses_or_offset()
  │   │   │   ├─ [内核 >= 4.6] find_approx_offsets()
  │   │   │   │   └─ 搜索递增的 int32_t 序列
  │   │   │   └─ [内核 < 4.6] find_approx_addresses()
  │   │   │       └─ 搜索递增的 uint64_t 序列
  │   │   │
  │   │   ├─ find_names()
  │   │   │   ├─ 从 0x4000 开始枚举
  │   │   │   ├─ 尝试解码符号名
  │   │   │   └─ 验证 markers 匹配
  │   │   │
  │   │   ├─ find_num_syms()
  │   │   │   └─ 在 names 前搜索符号总数
  │   │   │
  │   │   └─ correct_addresses_or_offsets()
  │   │       ├─ correct_by_banner()
  │   │       │   └─ 通过 linux_banner 地址验证
  │   │       └─ correct_by_vectors()
  │   │           ├─ 验证 vectors 的 2KB 对齐
  │   │           └─ 验证 pid_vnr 的指令模式
  │   │
  │   ├─ retry_relo() - 尝试 2(如果失败)
  │   │   └─ 不应用重定位
  │   │
  │   └─ retry_relo() - 尝试 3(如果仍失败)
  │       └─ 使用默认 kernel_base
  │
  └─ 【完成】
      ├─ 将处理后的镜像拷贝回原缓冲区
      └─ 所有符号信息已就绪

数据结构定义

kallsym_t 结构

// tools/kallsym.h
typedef struct {
    // 版本信息
    version_t version;              // 内核版本
    int banner_num;                 // 找到的 banner 数量
    int32_t linux_banner_offset[10]; // banner 位置
    int symbol_banner_idx;          // 实际使用的 banner 索引
    
    // 架构信息
    int8_t is_64;                   // 64 位 = 1
    int8_t is_be;                   // 大端序 = 1
    int32_t asm_long_size;          // 4
    int32_t asm_PTR_size;           // 8 (64位)
    
    // Token 相关
    int32_t kallsyms_token_table_offset;
    char *kallsyms_token_table[KSYM_TOKEN_NUMS];  // 256 个 token
    int32_t kallsyms_token_index_offset;
    
    // Markers
    int32_t kallsyms_markers_offset;
    int32_t kallsyms_markers_elem_size;  // 4 或 8
    int32_t _marker_num;
    
    // Addresses/Offsets
    int32_t has_relative_base;      // 0=addresses, 1=offsets
    int32_t kallsyms_addresses_offset;  // 绝对地址表
    int32_t kallsyms_offsets_offset;    // 相对偏移表
    int32_t _approx_addresses_or_offsets_offset;
    int32_t _approx_addresses_or_offsets_end;
    int32_t _approx_addresses_or_offsets_num;
    
    // Names
    int32_t kallsyms_names_offset;
    
    // Num syms
    int32_t kallsyms_num_syms_offset;
    int32_t kallsyms_num_syms;      // 符号总数
    
    // 其他
    uint64_t kernel_base;           // 内核基地址
    int32_t try_relo;               // 是否尝试重定位
    int32_t relo_applied;           // 是否已应用重定位
    int is_kallsysms_all_yes;       // CONFIG_KALLSYMS_ALL
    int current_type;               // SP_EL0 或 SP
} kallsym_t;

实际案例分析

案例:Pixel 6 内核 (5.10.149)

步骤 1: find_linux_banner()
  ✅ 找到 banner: Linux version 5.10.149-android13-4-00123...
  ✅ 版本: 5.10.149
  ✅ banner 偏移: 0x1a96258

步骤 2: find_token_table()
  ✅ 搜索到模式: "0\x00 1\x00 ... a\x00 b\x00 ..."
  ✅ kallsyms_token_table 偏移: 0x01a3e610
  ✅ 重建 256 个 token

步骤 3: find_token_index()
  ✅ 构造 index: [0x0000, 0x0002, 0x0004, ...]
  ✅ 找到匹配: 0x01a3ea14
  ✅ 字节序: little endian

步骤 4: try_find_arm64_relo_table()
  ✅ 找到重定位表: [0x01234000, 0x01256000)
  ✅ 推断 kernel_va: 0xffffffc008000000
  ✅ 应用 0x11a3f 个重定位项

步骤 5: find_markers()
  ✅ 从 0x01a3e610 往前搜索
  ✅ 找到递减序列: [0x01a44c80, 0x01a45080)
  ✅ marker 数量: 0x100 (256 个)

步骤 6: find_approx_offsets()
  ✅ 搜索递增的 int32_t 序列
  ✅ 近似范围: [0x00f9be60, 0x01a3e608)
  ✅ 近似符号数: 0x280e9

步骤 7: find_names()
  ✅ 从 0x4000 开始枚举
  ✅ 验证 marker[1..N] 全部匹配
  ✅ kallsyms_names 偏移: 0x01a3c418

步骤 8: find_num_syms()
  ✅ 在 names 前搜索
  ✅ kallsyms_num_syms: 0x280e9 (163,049 个符号)

步骤 9: correct_addresses_or_offsets_by_vectors()
  ✅ 在 names 表中找到 vectors (index=44)
  ✅ 在 names 表中找到 pid_vnr (index=124326)
  ✅ 搜索地址表...
  ✅ 找到 vectors 偏移: 0x00080800 (2KB 对齐 ✅)
  ✅ 验证 pid_vnr 偏移: 0x004b4c00
  ✅ 检测到 MRS X0, SP_EL0 指令 ✅
  ✅ kallsyms_offsets 偏移: 0x00f9be60

完成!所有符号已解析。

边界情况处理

1. 内核版本差异

// tools/kallsym.c:197-215
static int get_markers_elem_size(kallsym_t *info)
{
    if (info->kallsyms_markers_elem_size) 
        return info->kallsyms_markers_elem_size;
    
    int32_t elem_size = info->asm_long_size;
    
    // 内核 < 4.20:使用 PTR_size
    if (info->version.major < 4 || 
        (info->version.major == 4 && info->version.minor < 20))
        elem_size = info->asm_PTR_size;
    
    return elem_size;
}

变化历史

  • 内核 < 4.6:kallsyms_addresses (8 字节)
  • 内核 4.6-4.19:kallsyms_offsets (4 字节) + markers (8 字节)
  • 内核 >= 4.20:kallsyms_offsets (4 字节) + markers (4 字节)

2. CONFIG_KALLSYMS_ALL

// tools/kallsym.c:792-805
static int correct_addresses_or_offsets(kallsym_t *info, char *img, int32_t imglen)
{
    int rc = 0;
    
    // 尝试通过 linux_banner 验证
    rc = correct_addresses_or_offsets_by_banner(info, img, imglen);
    info->is_kallsysms_all_yes = 1;
    
    if (rc) {
        // 如果失败,说明 CONFIG_KALLSYMS_ALL=n
        info->is_kallsysms_all_yes = 0;
        tools_logw("no linux_banner, CONFIG_KALLSYMS_ALL=n\n");
        
        // 使用 vectors 验证(只包含函数符号)
        rc = correct_addresses_or_offsets_by_vectors(info, img, imglen);
    }
    
    return rc;
}

区别

  • CONFIG_KALLSYMS_ALL=y:包含所有符号(变量、函数)
  • CONFIG_KALLSYMS_ALL=n:只包含函数符号(text 段)

3. 符号名称后缀处理

某些编译器优化会给符号添加后缀:

// tools/symbol.c:15-26
static int32_t on_each_symbol_callbackup(int32_t index, char type, 
                                          const char *symbol, int32_t offset, 
                                          void *userdata)
{
    struct on_each_symbol_struct *data = userdata;
    int len = strlen(data->symbol);
    
    // 匹配符号名 + 后缀(.xxx 或 $xxx)
    if (strstr(symbol, data->symbol) == symbol && 
        (symbol[len] == '.' || symbol[len] == '$') &&
        !strstr(symbol, ".cfi_jt")) {
        tools_logi("%s -> %s: type: %c, offset: 0x%08x\n", 
                   data->symbol, symbol, type, offset);
        data->addr = offset;
        return 1;
    }
    return 0;
}

示例

查找:avc_denied

可能的符号名:
  avc_denied.isra.5     (GCC IPA-SRA 优化)
  avc_denied.constprop.8
  avc_denied.part.12
  avc_denied$local      (LLVM)

性能优化

1. 近似搜索

// 不是从 0 开始遍历整个镜像
// 而是利用已知信息缩小搜索范围

// find_names: 从 0x4000 开始(而不是 0)
int32_t cand = 0x4000;

// find_markers: 从 token_table 往前搜索(而不是从头搜索)
int32_t cand = info->kallsyms_token_table_offset;

// find_num_syms: 在 names 附近搜索(而不是全镜像)
for (int32_t cand = approx_end; cand > approx_end - 4096; cand -= 4) {
    // ...
}

2. 早期验证失败

// 如果某个步骤失败,立即返回,避免无用计算
if (count < KSYM_MIN_MARKER) {
    tools_logw("find kallsyms_markers error\n");
    return -1;  // 早期失败
}

故障排查

问题 1:找不到 token_table

Error: find token_table error

原因

  • 内核未开启 CONFIG_KALLSYMS=y
  • 镜像被压缩或加密
  • 镜像格式不是标准 ARM64 格式

调试

# 检查镜像类型
file Image
# 应该输出:Linux kernel ARM64 boot executable Image

# 尝试解压
extract-vmlinux Image > vmlinux
kptools -d -i vmlinux

问题 2:重定位失败

Warning: bad rela offset: 0x...
Warning: can't find arm64 relocation table

原因

  • 内核未包含重定位表
  • kernel_va 推断错误

解决

// 手动指定 kernel_base
info->kernel_base = 0xffffffc008000000;  // 标准地址

问题 3:验证失败

Error: can't locate vectors

原因

  • 地址表范围估计错误
  • vectors 符号在 names 表中的位置不对
  • kernel_base 错误

调试

# 查看所有符号
kptools -d -i Image | grep vectors
# 应该能找到 vectors 符号

# 检查对齐
kptools -d -i Image | grep vectors
# 0x00080800 T vectors  <- 0x80800 & 0x7FF == 0 ✅

ikconfig 转储支持

v2.0 新增:KernelPatch 现在支持通过 dump_all_ikconfig() 函数从内核镜像中提取嵌入的内核配置(ikconfig)。当内核编译时启用了 CONFIG_IKCONFIG=y,内核配置会以 gzip 压缩形式嵌入到内核镜像中。

dump_all_ikconfig()

dump_all_ikconfig() 函数通过搜索 ikconfig 的魔数标记来定位嵌入的配置数据:

ikconfig 数据格式:
  ┌──────────────────────────────────────────┐
  │ "IKCFG_ST" (8 字节魔数标记)              │
  ├──────────────────────────────────────────┤
  │ gzip 压缩的 .config 内容                 │
  ├──────────────────────────────────────────┤
  │ "IKCFG_ED" (8 字节结束标记)              │
  └──────────────────────────────────────────┘

用途

  • 确认内核编译选项(如 CONFIG_KALLSYMS_ALLCONFIG_RANDOMIZE_BASE 等)
  • 辅助判断 kallsyms 数据结构的变体
  • 调试符号解析失败的原因

使用示例

# 从内核镜像中提取配置
kptools -d -i Image --ikconfig > kernel_config.txt

输出与使用

dump_all_symbols()

// tools/kallsym.c:959-972
int dump_all_symbols(kallsym_t *info, char *img)
{
    char symbol[KSYM_SYMBOL_LEN] = { '\0' };
    char type = 0;
    int32_t pos = info->kallsyms_names_offset;
    
    for (int32_t i = 0; i < info->kallsyms_num_syms; i++) {
        memset(symbol, 0, sizeof(symbol));
        decompress_symbol_name(info, img, &pos, &type, symbol);
        int32_t offset = get_symbol_index_offset(info, img, i);
        
        fprintf(stdout, "0x%08x %c %s\n", offset, type, symbol);
    }
    
    return 0;
}

输出示例

0x00080000 T _stext
0x00080040 T _text
0x00080100 t el1h_64_sync
0x00080180 t el1h_64_irq
0x00080800 T vectors
0x00982b0 T kallsyms_lookup_name
0x00982f0 T kallsyms_on_each_symbol
0x0028eab0 T printk
0x0028db4c T panic
0x01abd710 t rest_init
0x01abd820 t kernel_init
0x004b4c00 T pid_vnr
0xd73550 t tcp_init_sock

技术总结

kallsyms 逆向的核心技术

  1. 特征搜索:利用数据结构的固有特征(对齐、递增、特定模式)
  2. 多重验证:通过 vectors、pid_vnr、linux_banner 交叉验证
  3. 版本适配:处理不同内核版本的数据结构变化
  4. 重定位处理:正确应用 ARM64 重定位表
  5. 鲁棒性:多策略尝试,容忍部分失败

这是一个教科书级别的二进制逆向工程实现,展示了:

  • 深入的内核内部机制理解
  • 精妙的搜索和验证算法
  • 优秀的容错和适配能力

下一篇04-镜像修补布局.md - 详解 kpimg 在内核镜像中的精确位置


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

评论

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

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