AWDP中uaf的patch研究
字数 2887
更新时间 2026-03-23 16:06:51

AWDP中UAF漏洞的Patch研究教学文档

一、 背景与目标

本文档基于一篇关于AWDP(Attack-Defense CTF比赛)中针对Use-After-Free(UAF)漏洞进行Patch(补丁)研究的文章,旨在详细讲解对存在UAF漏洞的二进制程序进行人工修补的思路、方法与具体步骤。教学目标是使读者能够掌握在汇编层面定位、分析并修复UAF漏洞的技能。

二、 漏洞程序分析

  1. 目标程序:以一道CTF题目(SWPUCTF_2019_p1KkHeap)为例进行分析。题目链接可通过文内百度网盘获取。
  2. 漏洞定位:通过逆向分析,在函数 sub_FD1() 中发现了UAF漏洞。
  3. 漏洞代码
    int sub_FD1() {
        unsigned __int64 index; // 用户输入的索引值
        ...
        printf("id: ");
        index = (int)sub_1076(); // 读取用户输入的索引
        if ( index > 7 )
            sub_E04();
        free(*((void **)&heaplist + index)); // 释放堆块
        sign[index] = 0; // 将标志位清零
        --count;
        return puts("Done!");
    }
    
  4. 漏洞成因:该函数释放了 heaplist 数组中指定索引指向的堆块,但并未将数组中存储该指针的槽位(即 heaplist[index] )本身清零。这导致该指针成为了一个“悬垂指针”,后续如果程序错误地再次使用了这个指针,就会引发Use-After-Free。

三、 Patch的核心目标

Patch的目的不是去清零已被释放的堆块内存内容,而是将存储该堆块指针的变量(即 heaplist[index])置为NULL,从而消除悬垂指针,从根本上杜绝UAF的发生。

四、 关键汇编语法知识(Intel风格)

在进行汇编Patch前,必须理解以下关键语法,这些是阅读和修改IDA Pro反汇编代码的基础:

  1. 操作数顺序mov dst, src,目的操作数在前,源操作数在后。
  2. 内存访问:中括号 [] 表示访问该地址处的内存值。
    • mov rax, [rbp+index]:计算地址 rbp+index,取出该地址处的值,放入 rax。这是在读取栈上的变量
  3. 取有效地址指令 lealea 用于计算地址,并不访问该地址的内存。
    • lea rdx, ds:0[rax*4]:计算表达式 rax*4 的结果,存入 rdx。这常用于计算数组元素的偏移(index * sizeof(int))。
    • lea rax, sign:将全局数组 sign 的首地址存入 rax。这与 mov rax, [sign](取出sign第一个元素的值)截然不同。
  4. 操作数大小指定dword ptr (4字节)、qword ptr (8字节) 等用于明确内存操作的大小。
    • mov dword ptr [rdx+rax], 0:向地址 rdx+rax 处写入一个4字节的0。此例中即 sign[index] = 0
  5. 通用地址表达式[base + index*scale + disp]
    • base:基址寄存器
    • index:索引寄存器
    • scale:缩放因子(1, 2, 4, 8)
    • disp:常量偏移
    • 此结构是汇编中实现数组访问(array[i])的核心。
  6. 全局变量访问mov eax, cs:count 表示读取全局变量 count 的值到 eaxcs: 是段前缀,理解时可忽略,重点在于识别是对全局变量的操作。
  7. 函数调用约定(64位Linux):遵循System V AMD64 ABI,函数第一个参数通过 rdi 寄存器传递。因此,在调用 free() 之前,rdi 寄存器中存放的值就是要释放的堆块地址。

五、 Patch实施步骤详解

  1. 定位与备份

    • 在IDA中打开目标二进制文件,定位到存在漏洞的 free() 调用所在的汇编代码位置。
    • 强烈建议在修改前,备份原始的汇编指令,以便出错时恢复。
  2. 分析上下文与寄存器状态

    • 使用GDB在 free 调用处设断点,运行程序并输入一个测试值(建议为非零值,便于观察)。
    • 观察此时各寄存器的状态,特别是:
      • rdi:存放要释放的堆块地址(即 heaplist[index] 的值)。
      • 哪个寄存器或内存位置存放着 heaplist 数组的基地址。
      • 哪个寄存器存放着用户输入的 index 值。
    • 目标:理清如何通过 index 计算出 heaplist 数组元素 heaplist[index] 的地址。
  3. 设计Patch代码

    • 核心任务:在 free 调用之后,插入汇编指令,将 heaplist[index] 这个内存单元(通常是一个8字节的指针)清零。
    • 这通常需要:
      1. 获取 heaplist 基地址。
      2. 根据 index 计算偏移(index * 8,因为是指针数组)。
      3. 将计算得到的地址处的8字节内容置为0(mov qword ptr [healist_base + index*8], 0)。
  4. 写入Patch

    • 在IDA的汇编视图中,找到 free 调用之后的合适位置(通常是在 sign[index] = 0 操作附近,但要确保逻辑正确)。
    • 利用IDA的Edit -> Patch program -> Assemble功能,写入设计好的汇编指令。
    • 注意:IDA的汇编器对语法要求严格,可能需要多次尝试。确保指令格式正确,特别是 qword ptr[] 的使用。如果直接输入 mov [rdx+rax], 0 可能无法识别,需要写成 mov qword ptr [rdx+rax], 0
  5. 空间处理与代码压缩

    • 插入的Patch代码需要占用空间。原程序的代码段可能没有预留空白区域。
    • 策略一(压缩):尝试优化、压缩原有的非关键功能代码(例如一些非核心的逻辑判断、输出字符串等),为Patch腾出空间。目标是使修改后的代码块总长度不变或巧妙利用原有的指令间隙。
    • 策略二(替换):如果空间确实紧张,可以寻找程序中未被使用或可被替代的函数(例如一些无关紧要的辅助函数或“漏洞函数”),将其指令替换为 nop 或直接改写为我们需要的Patch代码,并修改调用逻辑。
  6. 验证与测试

    • Patch完成后,在IDA中按F5查看反编译的伪代码,确认 heaplist[index]free 后被正确置零。
    • 将修改后的程序保存到新文件。
    • 运行测试用例,验证UAF漏洞是否已被成功修补(例如,尝试再次使用被释放的指针应导致崩溃或返回空指针,而非实现利用)。

六、 总结与技巧

  1. 核心思想:Patch的目标是消除悬垂指针,而非处理已释放的内存。
  2. 重点难点:在于精确计算目标指针的存储地址,这需要对程序的数据结构(全局数组布局)和汇编层次的地址计算有清晰理解。
  3. 空间策略:将Patch代码设计得尽可能紧凑,并优先考虑压缩原有非核心代码来获取空间。在AWDP这类比赛中,Patch检测通常不严格,合理利用 nop 指令或未用函数空间是常用技巧。
  4. 调试是关键:务必使用GDB动态调试,观察运行时寄存器和内存的值,这是确保Patch逻辑正确的唯一可靠方法。不要仅仅依赖静态分析。
相似文章
相似文章
 全屏