patch漏洞修补
Zach0ry Lv4

AWDP一般分为两个板块,Break(自己的payload打通)和Fix(让主办方的payload打不通)

patch:指防御。

checker:指在部署题目的时候,检查选手提交的补丁(patch文件)是否符合预设条件的专门脚本。常见的非常规防御的例子就是把二进制文件直接删除、把关键漏洞语句直接删除、在堆利用的题目中将free语句直接删除等,checker就是用于检查选手是否存在这些非常规补丁的。它主要是通过和提交的二进制文件通信来判断这个二进制文件是否有异常。

打补丁有下面2个基本原则:

❑不改变程序的正常运行流程。

❑能够防御想要防御的攻击

keypatch下载

tar -czvf patch.tar.gz notekeeper patch.sh

patch实例入门

image

这个题的漏洞主要是get函数的无限制读入

1
2
3
4
5
6
text:00000000004009CC                  call    _puts
.text:00000000004009D1 lea rax, [rbp+s]
.text:00000000004009D5 mov rdi, rax
.text:00000000004009D8 mov eax, 0
.text:00000000004009DD call _gets
.text:00000000004009E2 jmp loc_400AB4

要patch就要把它改为有限制的输入

改为read(0, s, 0x20);或者fgets(s, 0x20, stdin);

但问题来了:❌ 原地改不了

gets(s) 只需要:一个参数:s

read(0, s, 0x20) 需要:3 个参数:fdbufsize

所以要从call gets

变化为(修改为read类型)

1
2
3
4
5
6
mov rax,0
mov rsi, rdi
mov rdi,0
mov rdx,0x30h
syscall
ret

原来的位置放不下,所以要让程序跳转到其他位置去执行

在很多 CTF / AWDP 程序中:

选取跳转地址有两个要求

1.有x权限

2.更改之后不会影响函数正常运行

一般会选取eh_frame或者eh_frame_hdr

image

“填补 NOP 指令直到下一条指令边界”

作用:
当用一条新指令替换原来的多条指令、或长度更短时,用 NOP剩余空间补齐,避免留下半条旧指令的字节。

在代码段(.text)里 patch 指令,且新指令比原指令短:
原来:

1
2
>mov rax, rbx   ; 3字节
>add rax, rcx ; 3字节

改成:

1
>xor eax, eax   ; 2字节

→ 剩下 4 字节会残留旧机器码
→ 必须用 NOP 填满
👉 需要勾

你现在这种情况:在 .eh_frame 写 read

  • .eh_frame 原本是 db xx(纯数据)
  • 不是指令流
  • 没有“下一条指令边界”的概念

在_eh_frame上放read函数

image

patch call get过去到jmp_addr

image

之后应用就可以了

image

基础前置

字节

在 x86 指令中,mov 指令是一个极其通用的操作。当你写 mov [rax+rdx], 0 时,CPU 产生歧义:

  • 你是要存 1 个字节(byte)?
  • 还是 2 个字节(word)?
  • 还是 4 个字节(dword)?
  • 还是 8 个字节(qword)?

CPU 不知道该怎么分配内存宽度。 如果它猜测错了,可能会覆盖掉周围不该覆盖的数据。因此,汇编编译器(Assembler)会直接报错,因为该指令的语义是不完整的(Ambiguous)。

mov qword ptr [rax+rdx], 0 :ok:

mov [rax+rdx], 0 :x:

跳转

分类 指令 条件(EFLAGS) 含义 常用 Patch
无条件 JMP 一定跳
相等类 JE / JZ ZF=1 相等 ↔ JNE / NOP / JMP
相等类 JNE / JNZ ZF=0 不等 ↔ JE / NOP / JMP
无符号 JA CF=0 & ZF=0 > ↔ JBE
无符号 JAE CF=0 ↔ JB
无符号 JB CF=1 < ↔ JAE
无符号 JBE CF=1 | ZF=1 ↔ JA
有符号 JG ZF=0 & SF=OF > ↔ JLE
有符号 JGE SF=OF ↔ JL
有符号 JL SF≠OF < ↔ JGE
有符号 JLE ZF=1 | SF≠OF ↔ JG
标志位 JC CF=1 进位 ↔ JNC
标志位 JNC CF=0 无进位 ↔ JC
标志位 JO OF=1 溢出 ↔ JNO
标志位 JNO OF=0 无溢出 ↔ JO
标志位 JS SF=1 负数 ↔ JNS
标志位 JNS SF=0 正数 ↔ JS
标志位 JP PF=1 奇偶 ↔ JNP
标志位 JNP PF=0 非奇偶 ↔ JP
特殊 JECXZ ECX=0 ECX为0跳 改逻辑 / NOP
特殊 JRCXZ RCX=0 RCX为0跳 改逻辑 / NOP

压缩指令

1.常见短指令

❑push/pop指令。

❑xor eax,eax,等价于mov eax,0,但是比mov eax,0短。

❑xchg eax,ebx,表示交换两个寄存器的值,这里是交换eax,ebx寄存器的值。

比如,看到mov eax,0指令,就要想到用xor eax,eax这种指令来替换它,这样可以节省出3字节的空间,用来放置修补需要的指令(mov eax,0占5字节,xor eax,eax只占2字节)。

2.合并指令

1
2
lea          rax ,[rbp+buf]
mov rsi ,rax

缩为lea rsi , [rbp+buf]

这样就可以腾出3字节的空间(mov rsi,rax占3字节)。

常见漏洞

uaf

1.跳转到可执行段进行指针清空

0x15AC 前12字节改成48 C7 04 10 00 00 00 00 90 90 90 90

mov qword ptr [rax+rdx], 0指针清空

image

image

这里放一个ciscncatchme的patch演示

2.直接重写free操作

image

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
; --- 第一部分:寻址与取值 (Locating the Pointer) ---

mov eax, [rbp+var_14] ; 从栈中加载用户输入的数组索引 (index) 到 eax
cdqe ; 将 32 位 eax 符号扩展为 64 位 rax,为 64 位寻址做准备
lea rdx, ds:0[rax*8] ; 计算偏移量:index * 8 (每个指针占 8 字节),存入 rdx
lea rax, unk_4060 ; 加载全局指针数组的基地址到 rax
mov rax, [rdx+rax] ; 从数组中取出存储的内存块地址:rax = array[index]

; --- 第二部分:执行释放 (Executing Free) ---

mov rdi, rax ; 将目标地址存入 rdi (x64 下第一个参数寄存器)
call _free ; 调用标准库函数 free(ptr) 释放堆内存

; --- 第三部分:安全补丁 (Security Patch/Mitigation) ---
; 注意:这是防止 UAF 和 Double Free 的核心操作,原题中通常缺失

lea rax, unk_4060 ; 再次加载数组基地址
mov eax, [rbp+var_14] ; 再次获取索引
cdqe ; 扩展为 64 位
lea rdx, ds:0[rax*8] ; 重新定位该指针在数组中的物理偏移
mov qword ptr [rdx+rax], 0 ; 将数组中的指针清零 (ptr = NULL),切断悬挂指针

; --- 第四部分:返回与检查 (Cleanup & Safety) ---

lea rdi, aDone ; 加载 "done" 字符串地址
call _puts ; 打印提示信息
nop ; 指令对齐
mov rax, [rbp+var_8] ; 获取栈上的 Canary 值
xor rax, fs:28h ; 校验 Stack Canary 是否被篡改
jz short locret_15CA ; 如果校验通过 (结果为 0),跳转至函数返回逻辑
1
2
3
4
5
6
7
8
9
mov rax, qword ptr [rbp+var_4]  ; 取idx
shl eax, 3 ; idx 乘以 8
lea rdx, qword ptr [0x4060] ; 取 heap_list 的地址
add rdx, rax ; 计算索引的地址
push rdx ; 将堆指针 压到stack 上
mov rdi,[rdx] ; 取heap地址
call _free ; free heap ;ret 后 rdi 寄存器是空的
pop rdx ; 取出指向heap的指针
mov [rdx], rdi ; 清空指针的内容

image

strcpy

strcpy会把最后的/0也收进去造成of by null

把strcpy改成memcpy,按字节收入

strcpy(dest, src);

memcpy(dest, src, 3);

image

空间不足,让它到其它地方

1
2
3
4
5
6
7
8
patch_stub:
lea rcx, [rbp+s]#换一个寄存器
mov rax, [rbp+dest]
mov rdx, [rbp+size]#把size放进去
mov rsi, rcx
mov rdi, rax
call memcpy
jmp return_addr

fmt

方法一

把printf改为puts,比较简单。但是puts函数与printf函数还是有一点点区别的,因为puts函数是自动在输出的字符串尾部加入一个回车符,在有些比赛的check脚本中是通过比较两次输入与输出是否全等,就会导致这种patch方法不能过关。

方法二

给printf函数添加参数

首先需要在.eh_frame写入参数%s

image

然后改变printf参数

改变前

image

改变后

image

整数溢出

image

n0x100是unsorted int,在malloc的时候会转化为int,若输入是一个负数会转为非常大的数

所以,漏洞是由于没有检查输入的size有负数的情况造成的。完成修补

有两个思路:

❑将判断改为size>0x100||size<0。

❑将size改为无符号数。

这里需要探讨的是jle这种指令的形式,可以简称为J指令。

J指令分为两种:

❑短跳转J指令。

❑长跳转J指令。

短跳转J指令只有2个字节。第1个字节为指令标识,表示什么情况下跳转,如大于、小于、等于等;第2个字节为跳转的偏移,这里的偏移也是下一条指令的地址到目标地址的偏移。

逻辑含义 有符号 (Signed / int) 操作码 无符号 (Unsigned / uint) 操作码
大于 / 高于 JG (Greater) $7F JA (Above) $77
大于等于 / 不低于 JGE (Greater or Equal) $7D JAE (Above or Equal) $73
小于 / 低于 JL (Less) $7C JB (Below) $72
小于等于 / 不高于 JLE (Less or Equal) $7E JBE (Below or Equal) $76

当把一个 32 位的值放入 64 位寄存器时,处理方式完全不同。

操作类型 有符号指令 (int -> long) 功能描述 无符号指令 (uint -> ulong) 功能描述
位数扩展 MOVSXD / CDQE 符号扩展:高位补符号位(正补0,负补1)。 MOV (32位到64位) 零扩展:高位直接补 0,不看正负。

虽然加减法指令(ADD/SUB)是通用的,但乘除法在底层是严格区分的。

运算类型 有符号指令 (Signed) 解释 无符号指令 (Unsigned) 解释
乘法 IMUL 考虑补码符号的乘法。 MUL 纯二进制数值乘法。
除法 IDIV 考虑补码符号的除法。 DIV 纯二进制数值除法。
右移 SAR (算术右移) 保留符号位 SHR (逻辑右移) 高位补 0,不看符号。

把int改成unsorted int

image

通防

沙箱通防

把文件放入该目录下

1
python3 evilPatcher.py ./文件名 sandboxs/对应的asm脚本名

awdp赛后复盘

第十七届CISCN总决赛-AWDP-PWN部分题解

2025CCB_CISCN_半决赛-AWDP-Pwn

2024全国大学生信息安全竞赛(ciscn)半决赛华东北赛区Pwn

参考链接

pwn patch ciscn_2019_c_1 - 叶际参差 - 博客园

 请作者喝杯咖啡
 Comments
Comment plugin failed to load
Loading comment plugin
Powered by Hexo & Theme Keep
Unique Visitor Page View