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

tcp_init_sock 与 map_start 完整流程详解

2026-06-29 · 阅读 1

概述

本文档详细解释 KernelPatch 中 tcp_init_sock 区域作为临时存储空间的完整操作流程,包括备份、覆盖、恢复的每个步骤,以及 PAC/SCS 指令的处理机制。


整体架构

为什么使用 tcp_init_sock?

问题:_paging_init 代码需要在内核虚拟地址空间执行
     但此时内存管理还未初始化,无法分配虚拟内存

解决方案:借用 tcp_init_sock 函数的内存区域
          - tcp_init_sock 在内核启动早期不会被调用
          - 完成内存映射后立即恢复
          - 网络初始化在恢复之后才进行

关键数据结构

// kernel/base/start.h:12-28
typedef struct {
    setup_header_t header;
    version_t kernel_version;
    int64_t kallsyms_lookup_name_offset;
    int64_t kernel_size;
    int64_t start_offset;
    int64_t extra_size;
    uint64_t kernel_pa;
    int64_t map_offset;              // ← tcp_init_sock 的偏移
    int64_t map_backup_len;          // ← 备份的大小
    uint8_t map_backup[MAP_MAX_SIZE]; // ← 备份的数据(2KB)
    // ...
} start_preset_t;

阶段 1:地址计算与选择(tools/symbol.c)

选择 map_area

// todo
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");
    // 使用向下对齐确保函数开头(包括PAC验证指令)被包含在备份区域中
    // 如果使用向上对齐,函数开头可能被跳过,导致PAC指令未备份而AUC指令被NOP,造成崩溃
    *map_start = align_floor(addr, 16);
    // 增加max_size以补偿向下对齐可能包含的前置字节
    *max_size = 0x800 + 16;

    //打印tcp_init_sock附件的16条指令
    uint32_t *code = (uint32_t *)(image_buf + addr);
    for (int i = 0; i < 16; i++) {
        tools_logi("  [%02x]: %08x", i*4, code[i]);
    }
    tools_logi("\n");

}

关键点

  1. 向下对齐(align_floor):确保函数开头的 PAC/SCS 指令被包含
  2. 增加 max_size:补偿向下对齐可能包含的前置字节
  3. 打印前 16 条指令:用于调试,查看 PAC/SCS 指令

为什么向下对齐?

函数实际地址:0xD73553
向下对齐到 16:0xD73550
           ↓
备份区域:[0xD73550, 0xD73550 + 0x810)
          包含完整的函数开头(包括 PAC/SCS 指令)✅

如果向上对齐:
函数实际地址:0xD73553
向上对齐到 16:0xD73560
           ↓
备份区域:[0xD73560, 0xD73560 + 0x800)
          可能跳过函数开头的指令 ❌

阶段 2:备份阶段(start_prepare)

代码位置

    // backup map area
    // memcpy(start_preset.map_backup, kernel_pa + setup_preset.map_offset, (uint64_t)_map_end - (uint64_t)_map_start)
    adrp x13, _map_end
    add x13, x13, :lo12:_map_end
    adrp x14, _map_start
    add x14, x14, :lo12:_map_start
    sub x2, x13, x14

    // start_preset.map_backup_len = (uint64_t)_map_end - (uint64_t)_map_start
    str x2, [x11, #start_map_backup_len_offset]
    add x0, x11, #start_map_backup_offset
    add x1, x19, x20
    bl memcpy8

详细步骤

步骤 1:计算备份大小

adrp x13, _map_end
add x13, x13, :lo12:_map_end        // x13 = _map_end 的链接地址
adrp x14, _map_start
add x14, x14, :lo12:_map_start      // x14 = _map_start 的链接地址
sub x2, x13, x14                     // x2 = _map_end - _map_start (~2KB)

说明

  • _map_start_map_end 是链接器符号,定义在 kpimg.lds
  • 它们标记了 _paging_init 代码的起始和结束
  • 备份大小通常为 2-3KB

步骤 2:保存备份大小

str x2, [x11, #start_map_backup_len_offset]  // 保存到 start_preset.map_backup_len

存储位置

  • start_preset.map_backup_lenstart_preset_t 结构体)
  • 用于后续恢复时确定恢复范围

步骤 3:执行备份

add x0, x11, #start_map_backup_offset  // x0 = start_preset.map_backup (目标)
add x1, x19, x20                        // x1 = kernel_pa + map_offset (源)
bl memcpy8                              // 执行备份

内存布局

物理内存(备份时):
  [kernel_pa + map_offset]:
    tcp_init_sock 的原始机器码
    ├─ 可能包含:PACIASP (0xD503233F) 或 str x30, [x18], #8 (0xF85F8A5E)
    ├─ 函数序言:STP X29, X30, [SP, #-16]!
    ├─ 函数体:BL 指令、数据访问等
    └─ 函数结尾:AUTIASP (0xD503231F) 或 ldr x30, [x18, #-8]! (0xF85F8A1E)

备份到:
  [start_preset.map_backup] (在 kpimg 数据段):
    完整的 tcp_init_sock 机器码副本

关键点

  • 备份的是物理内存内容kernel_pa + map_offset
  • 包含完整的 PAC/SCS 指令(如果存在)
  • 备份发生在覆盖之前,确保数据完整

阶段 3:覆盖阶段(map_prepare)

代码位置

    // relocate map
    // memcpy(preset.map_offset + kernel_pa, (uint64_t)_map_start, (int64_t)(_map_end - (uint64_t)_map_start));
    adrp x2, _map_end
    add x2, x2, :lo12:_map_end
    adrp x1, _map_start
    add x1, x1, :lo12:_map_start
    sub x2, x2, x1
    add x0, x19, x14
    bl memcpy8

详细步骤

步骤 1:计算 _map_start 和 _map_end 的位置

adrp x2, _map_end
add x2, x2, :lo12:_map_end        // x2 = _map_end 的链接地址
adrp x1, _map_start
add x1, x1, :lo12:_map_start      // x1 = _map_start 的链接地址
sub x2, x2, x1                     // x2 = _map_end - _map_start (代码大小)

说明

  • _map_start_map_end 是链接器符号
  • 它们标记了 _paging_init 代码的边界
  • 代码已经在编译时链接到 kpimg 中

步骤 2:执行覆盖

add x0, x19, x14                   // x0 = kernel_pa + map_offset (目标)
bl memcpy8                         // 复制 _paging_init 代码

内存布局(覆盖后)

物理内存(覆盖后):
  [kernel_pa + map_offset]:
    _paging_init 的机器码
    ├─ get_myva() 函数
    ├─ get_data() 函数
    ├─ mem_proc() 函数
    ├─ get_or_create_pte() 函数
    └─ _paging_init() 主函数

关键点

  • tcp_init_sock 的原始代码被覆盖
  • 备份已经保存在 start_preset.map_backup
  • 覆盖发生在备份之后,数据安全

阶段 4:执行阶段(_paging_init)

代码执行流程

T1: 内核调用 paging_init
    ↓
T2: paging_init[0] = B _paging_init (劫持指令)
    ↓
T3: 跳转到 _paging_init(位于 tcp_init_sock 位置)
    ↓
T4: _paging_init() 执行
    ├─ 分配新的物理内存
    ├─ 创建页表映射
    ├─ 复制 kpimg 到新位置(包括 map_backup)
    ├─ 恢复 paging_init 原始指令
    ├─ 调用原始 paging_init()
    └─ 调用 start() 继续启动

关键操作:复制 kpimg

    for (uint64_t i = start_va; i < start_va + all_size; i += 8) {
        *(uint64_t *)i = 0;
    }
    for (uint64_t i = 0; i < data->start_img_size; i += 8) {
        *(uint64_t *)(start_va + i) = *(uint64_t *)(old_start_va + i);
    }
    for (uint64_t i = 0; i < data->extra_size; i += 8) {
        *(uint64_t *)(start_va + data->start_size + i) = *(uint64_t *)(old_start_va + data->start_img_size + i);
    }

    flush_icache_all();

说明

  • old_start_va:原始 kpimg 的虚拟地址(物理地址 + linear_voffset)
  • start_va:新的 kpimg 的虚拟地址(新物理地址 + kimage_voffset)
  • 复制包括 start_preset.map_backup(在 start_preset_t 结构体中)

阶段 5:恢复阶段(restore_map)

代码位置

static void restore_map()
{
    uint64_t start = kernel_va + start_preset.map_offset;
    uint64_t end = start + start_preset.map_backup_len;
    log_boot("Restore: %llx, %llx\n", start, end);

    for (uint64_t i = start; i < align_ceil(end, page_size); i += page_size) {
        uint64_t *pte = pgtable_entry_kernel(i);
        uint64_t orig = *pte;
        *pte = (orig | PTE_DBM) & ~PTE_RDONLY;
        flush_tlb_kernel_page(i);
        for (uint64_t j = i; j >= start && j < end && j < i + page_size; j += 8) {
            *(uint64_t *)j = *(uint64_t *)(start_preset.map_backup + (j - start));
        }
        *pte = orig;
        flush_tlb_kernel_page(i);
    }
    flush_icache_all();
}

详细步骤

步骤 1:计算恢复范围

uint64_t start = kernel_va + start_preset.map_offset;  // tcp_init_sock 的虚拟地址
uint64_t end = start + start_preset.map_backup_len;    // 结束地址

说明

  • kernel_va:内核的虚拟基地址(KASLR 后的)
  • start_preset.map_offset:tcp_init_sock 的偏移(来自 setup 阶段)
  • start_preset.map_backup_len:备份的大小(来自备份阶段)

步骤 2:逐页恢复

for (uint64_t i = start; i < align_ceil(end, page_size); i += page_size) {
    // 1. 获取页表项
    uint64_t *pte = pgtable_entry_kernel(i);
    uint64_t orig = *pte;
    
    // 2. 临时设置为可写
    *pte = (orig | PTE_DBM) & ~PTE_RDONLY;
    flush_tlb_kernel_page(i);
    
    // 3. 恢复数据
    for (uint64_t j = i; j >= start && j < end && j < i + page_size; j += 8) {
        *(uint64_t *)j = *(uint64_t *)(start_preset.map_backup + (j - start));
    }
    
    // 4. 恢复原始权限
    *pte = orig;
    flush_tlb_kernel_page(i);
}

为什么逐页处理?

  • tcp_init_sock 可能跨越多个页面
  • 不同页面的权限可能不同(代码段通常是只读)
  • 必须临时修改权限才能写入,然后恢复原始权限

循环条件分析

for (uint64_t j = i; j >= start && j < end && j < i + page_size; j += 8)

问题j >= start 可能导致跳过第一个页的部分内容

修复建议

uint64_t j_start = (i > start) ? i : start;  // max(i, start)
uint64_t j_end = (end < i + page_size) ? end : (i + page_size);  // min(end, i + page_size)

for (uint64_t j = j_start; j < j_end; j += 8) {
    *(uint64_t *)j = *(uint64_t *)(start_preset.map_backup + (j - start));
}

步骤 3:刷新指令缓存

flush_icache_all();

说明

  • ARM64 有独立的指令缓存和数据缓存
  • 修改代码后必须刷新 I-cache
  • 否则 CPU 可能执行旧的缓存指令

PAC/SCS 指令处理

问题:是否需要 NOP 化 PAC/SCS 指令?

答案:不需要,而且不应该!

原因分析

1. 备份的是运行时指令

编译时(kernel 镜像):
  tcp_init_sock:
    PACIASP (0xD503233F)

运行时(KASLR 后,alternatives 替换后):
  tcp_init_sock:
    str x30, [x18], #8 (0xF85F8A5E)  ← 可能已被 SCS 替换

说明

  • 备份发生在运行时,备份的是实际的运行时指令
  • 如果系统使用 SCS,paciasp 已经被替换为 str x30, [x18], #8
  • 恢复时应该恢复运行时的指令,而不是编译时的指令

2. NOP 化会导致验证失败

函数正常流程(使用 PAC):
  PACIASP              ; 签名 X30
  STP X29, X30, [SP, #-16]!
  // ... 函数体 ...
  LDP X29, X30, [SP], #16
  AUTIASP              ; 验证签名
  RET

如果 AUTIASP 被 NOP 化:
  PACIASP              ; 签名 X30
  STP X29, X30, [SP, #-16]!
  // ... 函数体 ...
  LDP X29, X30, [SP], #16
  NOP                  ; ❌ 验证被跳过
  RET                  ; X30 仍带签名
                       ; → PAC 验证失败
                       ; → X30 被修改为非法地址
                       ; → 崩溃!

3. 正确的处理方式

方案 1:保持原样(推荐)

// 备份时:直接备份,不做任何修改
memcpy(start_preset.map_backup, 
       kernel_pa + map_offset, 
       map_backup_len);

// 恢复时:直接恢复,不做任何修改
memcpy(kernel_va + map_offset,
       start_preset.map_backup,
       map_backup_len);

优点

  • ✅ 简单可靠
  • ✅ 保持函数的原始保护机制
  • ✅ 兼容 PAC 和 SCS

方案 2:检测并处理(复杂,不推荐)

// 检测 PAC/SCS 指令
bool is_pac_or_scs(uint32_t insn) {
    // PAC
    if (insn == 0xD503233F) return true;  // PACIASP
    if (insn == 0xD503231F) return true;  // AUTIASP
    
    // SCS
    if (insn == 0xF85F8A5E) return true;  // str x30, [x18], #8
    if (insn == 0xF85F8A1E) return true;  // ldr x30, [x18, #-8]!
    
    return false;
}

// 备份时:记录但不修改
void backup_map_area() {
    memcpy(backup, source, size);
    // 不修改任何指令
}

// 恢复时:直接恢复
void restore_map_area() {
    memcpy(dest, backup, size);
    // 不修改任何指令
}

当前实现的问题

问题 1:setup1.S 中的错误逻辑

    mov w3, #0x201F
    movk w3, #0xD503, lsl#16
    orr w1, w3, #0x100
    mov w2, #0xFFFFFD1F
    and w0, w12, w2
    // if ((map_data.paging_init_backup & 0xFFFFFD1F) == 0xD503211F)
    cmp w0, w1
    b.ne .backup
    // map_data.paging_init_backup = NOP
    mov w12, w3
    // uint32_t *p = (uint32_t *)paging_init_pa + 1;
    add x11, x13, #4
.cmp_auti:
    // while ((*p & 0xFFFFFD1F) != 0xD503211F) ++p;
    ldr w0, [x11], #4
    and w0, w0, w2
    cmp w0, w1
    b.ne .cmp_auti
    // *p = NOP
    stur w3, [x11, #-4]

问题

  1. ❌ 这段代码是针对 paging_init 的,不是 tcp_init_sock
  2. ❌ 检测逻辑错误(实际检测的是 AUTIASP,不是 BTI C)
  3. ❌ 会 NOP 化 AUTIASP,导致 PAC 验证失败

修复

  • 删除这段代码
  • 直接备份原始指令

问题 2:tcp_init_sock 备份的正确性

当前实现(正确)

// start_prepare: 直接备份,不做修改
add x0, x11, #start_map_backup_offset
add x1, x19, x20                        // kernel_pa + map_offset
bl memcpy8                              // 直接备份

优点

  • ✅ 备份完整的原始代码
  • ✅ 包含 PAC/SCS 指令(如果存在)
  • ✅ 不做任何修改

问题 3:恢复时的循环条件

        for (uint64_t j = i; j >= start && j < end && j < i + page_size; j += 8) {
            *(uint64_t *)j = *(uint64_t *)(start_preset.map_backup + (j - start));
        }

问题

  • j >= start 条件可能导致跳过第一个页的部分内容
  • 如果 i < start,第一次循环会被跳过

修复

uint64_t j_start = (i < start) ? start : i;
uint64_t j_end = (end < i + page_size) ? end : (i + page_size);

for (uint64_t j = j_start; j < j_end; j += 8) {
    *(uint64_t *)j = *(uint64_t *)(start_preset.map_backup + (j - start));
}

完整时序图

┌─────────────────────────────────────────────────────────┐
│ T1: setup() 阶段 - start_prepare()                      │
└─────────────────────────────────────────────────────────┘
  操作:
    ✅ 备份 tcp_init_sock 到 start_preset.map_backup
    ✅ 备份大小:_map_end - _map_start (~2KB)
    ✅ 备份内容:完整的原始机器码(包括 PAC/SCS)
  
  物理内存:
    [kernel_pa + map_offset]: tcp_init_sock 原始代码 ✅

┌─────────────────────────────────────────────────────────┐
│ T2: setup() 阶段 - map_prepare()                        │
└─────────────────────────────────────────────────────────┘
  操作:
    ✅ 覆盖 tcp_init_sock 为 _paging_init 代码
    ✅ 备份 paging_init[0] 到 map_data.paging_init_backup
    ✅ 写入 B _paging_init 到 paging_init[0]
  
  物理内存:
    [kernel_pa + map_offset]: _paging_init 代码 ❌ (覆盖)
    [kernel_pa + paging_init_offset]: B _paging_init ❌ (劫持)

┌─────────────────────────────────────────────────────────┐
│ T3: 内核启动,到达 paging_init                           │
└─────────────────────────────────────────────────────────┘
  执行:
    paging_init() → B _paging_init → 跳转到 _paging_init ✅

┌─────────────────────────────────────────────────────────┐
│ T4: _paging_init() 执行                                  │
└─────────────────────────────────────────────────────────┘
  操作:
    ✅ 分配新的物理内存
    ✅ 创建页表映射
    ✅ 复制 kpimg 到新位置(包括 map_backup)
    ✅ 恢复 paging_init[0] = map_data.paging_init_backup
    ✅ 调用原始 paging_init()
    ✅ 调用 start()
  
  虚拟内存:
    [kernel_va + map_offset]: _paging_init 代码 ❌ (仍被覆盖)
    [kernel_va + paging_init_offset]: 原始指令 ✅ (已恢复)

┌─────────────────────────────────────────────────────────┐
│ T5: start() 阶段 - restore_map()                        │
└─────────────────────────────────────────────────────────┘
  操作:
    ✅ 从 start_preset.map_backup 恢复 tcp_init_sock
    ✅ 逐页恢复,临时修改权限
    ✅ 刷新指令缓存
  
  虚拟内存:
    [kernel_va + map_offset]: tcp_init_sock 原始代码 ✅ (已恢复)

┌─────────────────────────────────────────────────────────┐
│ T6: 网络初始化 - 调用 tcp_init_sock                      │
└─────────────────────────────────────────────────────────┘
  执行:
    tcp_init_sock() 正常执行 ✅
    - PAC/SCS 指令正常工作
    - 函数功能完全正常

关键要点总结

1. 备份时机

  • 在覆盖之前备份(start_prepare 在 map_prepare 之前)
  • 备份完整的原始代码(包括 PAC/SCS 指令)
  • 不做任何修改(直接 memcpy)

2. 恢复时机

  • 在 start() 早期恢复(在任何网络初始化之前)
  • 恢复完整的原始代码(不做任何修改)
  • 刷新指令缓存(确保 CPU 看到新代码)

3. PAC/SCS 指令处理

  • 不需要 NOP 化(保持原样)
  • 备份运行时指令(可能已被 SCS 替换)
  • 恢复运行时指令(保持一致性)

4. 地址对齐

  • 向下对齐(align_floor)确保包含函数开头
  • 增加备份大小(补偿对齐可能的前置字节)
  • 修复恢复循环(确保不跳过任何字节)

修复建议

1. 修复恢复循环条件

// kernel/base/start.c:317-335(修复版)
static void restore_map()
{
    uint64_t start = kernel_va + start_preset.map_offset;
    uint64_t end = start + start_preset.map_backup_len;
    log_boot("Restore: %llx, %llx\n", start, end);

    for (uint64_t i = align_floor(start, page_size); 
         i < align_ceil(end, page_size); 
         i += page_size) {
        uint64_t *pte = pgtable_entry_kernel(i);
        uint64_t orig = *pte;
        *pte = (orig | PTE_DBM) & ~PTE_RDONLY;
        flush_tlb_kernel_page(i);
        
        // 修复:确保不跳过任何字节
        uint64_t j_start = (i < start) ? start : i;
        uint64_t j_end = (end < i + page_size) ? end : (i + page_size);
        
        for (uint64_t j = j_start; j < j_end; j += 8) {
            *(uint64_t *)j = *(uint64_t *)(start_preset.map_backup + (j - start));
        }
        
        *pte = orig;
        flush_tlb_kernel_page(i);
    }
    flush_icache_all();
}

2. 删除 setup1.S 中的错误逻辑

// kernel/base/setup1.S:221-249(修复版)
map_prepare:
    // ... 前面的代码 ...
    
    // ============ 备份和劫持 paging_init ============
    
    // 计算 paging_init 物理地址
    add x13, x15, x19              // x13 = paging_init_offset + kernel_pa
    
    // 读取第一条指令
    ldr w12, [x13]                 // w12 = paging_init[0]
    
    // ✅ 直接备份,不做任何修改
    str w12, [x9, #map_paging_init_backup_offset]
    dsb ish
    
    // ... 生成 B 指令并写入 ...

3. 添加验证机制

// 在 restore_map() 后添加验证
void verify_map_restore()
{
    uint64_t start = kernel_va + start_preset.map_offset;
    uint32_t *actual = (uint32_t *)start;
    uint32_t *backup = (uint32_t *)start_preset.map_backup;
    
    int mismatch = 0;
    for (int i = 0; i < start_preset.map_backup_len / 4; i++) {
        if (actual[i] != backup[i]) {
            log_boot("Mismatch at offset %x: actual=%08x, backup=%08x\n",
                     i * 4, actual[i], backup[i]);
            mismatch++;
        }
    }
    
    if (mismatch) {
        log_boot("❌ Restore failed: %d words mismatch\n", mismatch);
    } else {
        log_boot("✅ Restore verified: all words match\n");
    }
}

总结

核心原则

  1. 备份时:直接备份,不做任何修改
  2. 恢复时:直接恢复,不做任何修改
  3. PAC/SCS:保持原样,不需要 NOP 化
  4. 对齐:向下对齐,确保包含函数开头
  5. 验证:恢复后验证,确保数据正确

关键点

  • tcp_init_sock 用作临时存储,必须在网络初始化前恢复
  • ✅ 备份发生在覆盖之前,数据安全
  • ✅ 恢复发生在 start() 早期,时机正确
  • ✅ PAC/SCS 指令应该保持原样,不需要 NOP 化
  • ✅ 恢复循环需要修复,确保不跳过任何字节

参考资料


文档版本:2.0
最后更新:2026-06-26
相关 KernelPatch 版本:0.13.2+

评论

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

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