kallsyms 逆向工程详解
概述
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% 以上体积
压缩方法:
Token Table:256 个常用字符串
token[0] = "t" token[1] = "a" token[2] = "n" ... token[50] = "_init" token[51] = "_exit" ...符号编码:将符号名拆分为 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;
}
验证原理:
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
- 必须 2048 字节对齐(
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_ALL、CONFIG_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 逆向的核心技术:
- ✅ 特征搜索:利用数据结构的固有特征(对齐、递增、特定模式)
- ✅ 多重验证:通过 vectors、pid_vnr、linux_banner 交叉验证
- ✅ 版本适配:处理不同内核版本的数据结构变化
- ✅ 重定位处理:正确应用 ARM64 重定位表
- ✅ 鲁棒性:多策略尝试,容忍部分失败
这是一个教科书级别的二进制逆向工程实现,展示了:
- 深入的内核内部机制理解
- 精妙的搜索和验证算法
- 优秀的容错和适配能力
下一篇:04-镜像修补布局.md - 详解 kpimg 在内核镜像中的精确位置
文档版本:2.0
最后更新:2026-06-26
评论
- 还没有评论,来说点什么吧。