本文是为了学习apple做的铺垫,两种利用手法的利用原理是基于ctfshow的pwn213和pwn214动调和一些师傅的博客总结得出,如有侵权联系删除

House of kiwi

利用条件

  1. 能够触发__malloc_assert
  2. 能够申请到_IO_file_sync 和 _IO_helper_jumps这两个位置并且修改。

利用原理

在函数sysmalloc中,有一个检查top chunk是否对其的代码片段:

1
2
3
4
assert ((old_top == initial_top (av) && old_size == 0) ||
((unsigned long) (old_size) >= MINSIZE &&
prev_inuse (old_top) &&
((unsigned long) old_end & (pagesize - 1)) == 0));

如果检查没有通过,会触发__malloc_assert:

1
2
3
4
5
6
7
8
9
10
11
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(stderr)中的_IO_SYNC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int
_IO_fflush (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;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
0x7ffff7e00208 <__GI__IO_fflush+88>:	lea    rdx,[rip+0x1967f1]        // 0x7ffff7f96a00 <_IO_helper_jumps>
0x7ffff7e0020f <__GI__IO_fflush+95>: lea rax,[rip+0x197552] // 0x7ffff7f97768
0x7ffff7e00216 <__GI__IO_fflush+102>: sub rax,rdx
0x7ffff7e00219 <__GI__IO_fflush+105>: mov rcx,rbp
0x7ffff7e0021c <__GI__IO_fflush+108>: sub rcx,rdx
0x7ffff7e0021f <__GI__IO_fflush+111>: cmp rax,rcx
0x7ffff7e00222 <__GI__IO_fflush+114>: jbe 0x7ffff7e00268 <__GI__IO_fflush+184>
0x7ffff7e00224 <__GI__IO_fflush+116>: mov rdi,rbx
0x7ffff7e00227 <__GI__IO_fflush+119>: call QWORD PTR [rbp+0x60] // 在此处下断点
0x7ffff7e0022a <__GI__IO_fflush+122>: neg eax
0x7ffff7e0022c <__GI__IO_fflush+124>: sbb r12d,r12d
0x7ffff7e0022f <__GI__IO_fflush+127>: test DWORD PTR [rbx],0x8000
0x7ffff7e00235 <__GI__IO_fflush+133>: jne 0x7ffff7e00258 <__GI__IO_fflush+168>
0x7ffff7e00237 <__GI__IO_fflush+135>: mov rdi,QWORD PTR [rbx+0x88]
0x7ffff7e0023e <__GI__IO_fflush+142>: mov eax,DWORD PTR [rdi+0x4]
0x7ffff7e00241 <__GI__IO_fflush+145>: sub eax,0x1
0x7ffff7e00244 <__GI__IO_fflush+148>: mov DWORD PTR [rdi+0x4],eax
0x7ffff7e00247 <__GI__IO_fflush+151>: jne 0x7ffff7e00258 <__GI__IO_fflush+168>
0x7ffff7e00249 <__GI__IO_fflush+153>: mov QWORD PTR [rdi+0x8],0x0

call QWORD PTR [rbp+0x60]处下断点,

1
*RBP  0x7ffff7f97600 (_IO_file_jumps) ◂— 0x0

bp为_IO_file_jumps也就是vtable,我们要利用的就是这里面的_IO_default_sync指针

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_wstrn_jumps attribute_hidden =
{
JUMP_INIT_DUMMY,
JUMP_INIT(finish, _IO_wstr_finish),
JUMP_INIT(overflow, (_IO_overflow_t) _IO_wstrn_overflow),
JUMP_INIT(underflow, (_IO_underflow_t) _IO_wstr_underflow),
JUMP_INIT(uflow, (_IO_underflow_t) _IO_wdefault_uflow),
JUMP_INIT(pbackfail, (_IO_pbackfail_t) _IO_wstr_pbackfail),
JUMP_INIT(xsputn, _IO_wdefault_xsputn),
JUMP_INIT(xsgetn, _IO_wdefault_xsgetn),
JUMP_INIT(seekoff, _IO_wstr_seekoff),
JUMP_INIT(seekpos, _IO_default_seekpos),
JUMP_INIT(setbuf, _IO_default_setbuf),
JUMP_INIT(sync, _IO_default_sync), # 利用这个函数指针
JUMP_INIT(doallocate, _IO_wdefault_doallocate),
JUMP_INIT(read, _IO_default_read),
JUMP_INIT(write, _IO_default_write),
JUMP_INIT(seek, _IO_default_seek),
JUMP_INIT(close, _IO_default_close),
JUMP_INIT(stat, _IO_default_stat),
JUMP_INIT(showmanyc, _IO_default_showmanyc),
JUMP_INIT(imbue, _IO_default_imbue)
};

如果我们将其修改为setcontext+61,就能触发这个magic gadget的汇编:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
.text:0000000000053A6D                 mov     rsp, [rdx+0A0h]
.text:0000000000053A74 mov rbx, [rdx+80h]
.text:0000000000053A7B mov rbp, [rdx+78h]
.text:0000000000053A7F mov r12, [rdx+48h]
.text:0000000000053A83 mov r13, [rdx+50h]
.text:0000000000053A87 mov r14, [rdx+58h]
.text:0000000000053A8B mov r15, [rdx+60h]
.text:0000000000053A8F test dword ptr fs:48h, 2
.text:0000000000053A9B jz loc_53B56
......
.text:0000000000053B56 mov rcx, [rdx+0A8h]
.text:0000000000053B5D push rcx
.text:0000000000053B5E mov rsi, [rdx+70h]
.text:0000000000053B62 mov rdi, [rdx+68h]
.text:0000000000053B66 mov rcx, [rdx+98h]
.text:0000000000053B6D mov r8, [rdx+28h]
.text:0000000000053B71 mov r9, [rdx+30h]
.text:0000000000053B75 mov rdx, [rdx+88h]
.text:0000000000053B75 ; } // starts at 53A30
.text:0000000000053B7C ; __unwind {
.text:0000000000053B7C xor eax, eax
.text:0000000000053B7E retn

call _IO_file_sync时,rdx的值实际上是_IO_helper_jumps的地址,这个值是稳定不变的,实际上这个结构体的地址就在_IO_file_jumps前面一点:

1
2
3
4
5
6
7
__libc_IO_vtables:0000000000215A00 qword_215A00    dq 0          ; DATA XREF: sub_45390+1A3↑o
__libc_IO_vtables:0000000000215A00 ; sub_5A980+1643↑o ...
__libc_IO_vtables:0000000000215A08 dq 0
__libc_IO_vtables:0000000000215A10 dq offset _IO_default_finish
__libc_IO_vtables:0000000000215A18 dq offset sub_722E0
__libc_IO_vtables:0000000000215A20 dq offset sub_8DDD0
__libc_IO_vtables:0000000000215A28 dq offset _IO_default_uflow

然后我们构造一个orw的rop链放在一个已知地址的位置,并让rsp = [_IO_helper_jumps+0xA0]等于ROP链的地址,[_IO_helper_jumps+0xA8]等于一条ret指令的地址,这样,在执行到push rcx后,rsp中是这样的

1
2
3
4
5
6
7
8
9
pwndbg> tele 0x555555605158
00:0000│ rsp 0x555555605158 —▸ 0x7ffff7c2a3e6 (iconv+198) ◂— ret
01:0008│ 0x555555605160 (ROP) —▸ 0x7ffff7c45eb0 (mblen+112) ◂— pop rax
02:0010│ 0x555555605168 (ROP+8) ◂— 0x2
03:0018│ 0x555555605170 (ROP+16) —▸ 0x7ffff7c2a3e5 (iconv+197) ◂— pop rdi
04:0020│ 0x555555605178 (ROP+24) —▸ 0x555555605020 (FLAG) ◂— '/ctfshow_flag'
05:0028│ 0x555555605180 (ROP+32) —▸ 0x7ffff7c2be51 ◂— pop rsi
06:0030│ 0x555555605188 (ROP+40) ◂— 0x0
07:0038│ 0x555555605190 (ROP+48) —▸ 0x7ffff7c29db4 ◂— syscall

在执行完setcontext后,就会ret到我们劫持的这个rcx的ret,从而执行我们的ROP链,也就是完成了栈迁移

House of emma

利用条件

1.可以进行两次任意地址写堆地址(通常是largebin attack)
2.可以触发fsop

攻击方法

劫持stderr指针为我们构造的fake_IO_FILE
__pointer_chk_guard 处写入已知内容,来绕过函数指针的保护

攻击限制

若stderr 的指针存放于 bss 段上,无法被我们修改,那么只能通过exit来触发FSOP,但由于我们的构造可能会导致异或内容被篡改后,exit无法正常执行,使得程序无法执行到我们构造的 IO流,需要攻击位于TLS结构体的_pointer_chk_guard,并且远程可能需要爆破TLS偏移

源码分析

vtable虚表中有_IO_cookie_jumps结构体,在_IO_cookie_jumps中包含着_IO_cookie_read_IO_cookie_write等一系列函数,这些函数存在着任意函数指针的调用,但是这些函数指针的调用被pointer_guard 进行了加密

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
static ssize_t
_IO_cookie_read (FILE *fp, void *buf, ssize_t size)
{
struct _IO_cookie_file *cfile = (struct _IO_cookie_file *) fp;
cookie_read_function_t *read_cb = cfile->__io_functions.read;
#ifdef PTR_DEMANGLE
PTR_DEMANGLE (read_cb);
#endif

if (read_cb == NULL)
return -1;

return read_cb (cfile->__cookie, buf, size);
}

static ssize_t
_IO_cookie_write (FILE *fp, const void *buf, ssize_t size)
{
struct _IO_cookie_file *cfile = (struct _IO_cookie_file *) fp;
cookie_write_function_t *write_cb = cfile->__io_functions.write;
#ifdef PTR_DEMANGLE
PTR_DEMANGLE (write_cb);
#endif

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

ssize_t n = write_cb (cfile->__cookie, buf, size);
if (n < size)
fp->_flags |= _IO_ERR_SEEN;

return n;
}

static off64_t
_IO_cookie_seek (FILE *fp, off64_t offset, int dir)
{
struct _IO_cookie_file *cfile = (struct _IO_cookie_file *) fp;
cookie_seek_function_t *seek_cb = cfile->__io_functions.seek;
#ifdef PTR_DEMANGLE
PTR_DEMANGLE (seek_cb);
#endif

return ((seek_cb == NULL || (seek_cb (cfile->__cookie, &offset, dir)== -1) || offset == (off64_t) -1) ? _IO_pos_BAD : offset);
}

static int
_IO_cookie_close (FILE *fp)
{
struct _IO_cookie_file *cfile = (struct _IO_cookie_file *) fp;
cookie_close_function_t *close_cb = cfile->__io_functions.close;
#ifdef PTR_DEMANGLE
PTR_DEMANGLE (close_cb);
#endif

if (close_cb == NULL)
return 0;

return close_cb (cfile->__cookie);
}

利用原理

1.伪造IO_file

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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;
#ifdef _IO_USE_OLD_IO_FILE
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
0x0   _flags
0x8 _IO_read_ptr
0x10 _IO_read_end
0x18 _IO_read_base
0x20 _IO_write_base
0x28 _IO_write_ptr
0x30 _IO_write_end
0x38 _IO_buf_base
0x40 _IO_buf_end
0x48 _IO_save_base
0x50 _IO_backup_base
0x58 _IO_save_end
0x60 _markers
0x68 _chain
0x70 _fileno
0x74 _flags2
0x78 _old_offset
0x80 _cur_column
0x82 _vtable_offset
0x83 _shortbuf
0x88 _lock
0x90 _offset
0x98 _codecvt
0xa0 _wide_data
0xa8 _freeres_list
0xb0 _freeres_buf
0xb8 __pad5
0xc0 _mode
0xc4 _unused2
0xd8 vtable

需要关注的地方有:

1.*vtable指针:

2.*_lock指针:这个指针的值应该为_IO_stdfile_1_lock,所以我们得知道libc_base,然后根据偏移计算出_IO_stdfile_1_lock的具体地址

2.修改pointer_guard的值为已知值

house of emma利用方式有一条完整的函数调用链,我们需要这个pointer_guard的值来引导rip到我们想要的函数

需要注意的是pointer guard的值并不在libc中,而是在libc的低地址处,如果使用pwndbg,你可以看到在libc前面有一个匿名的内存区域,大小为0x3000但是我实际动调时发现是在libc下方,大小为5000,这个应该影响不大

tls结构体就位于这个匿名的内存空间中,它包含有pointer_guard,更具体地说,pointer_guard的值应该位于(libc_base - 0x3000 + 0x770),实际上,这个结构体的名字是tcbhead_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
(line 36, /sysdeps/x86_64/nptl/tls.h)
typedef struct
{
void *tcb; /* Pointer to the TCB. Not necessarily the
thread descriptor used by libpthread. */
dtv_t *dtv;
void *self; /* Pointer to the thread descriptor. */
int multiple_threads;
int gscope_flag;
uintptr_t sysinfo;
uintptr_t stack_guard;
uintptr_t pointer_guard;
unsigned long int unused_vgetcpu_cache[2];
/* Bit 0: X86_FEATURE_1_IBT.
Bit 1: X86_FEATURE_1_SHSTK.
*/
unsigned int feature_1;
int __glibc_unused1;
/* Reservation of some values for the TM ABI. */
void *__private_tm[4];
/* GCC split stack support. */
void *__private_ss;
/* The lowest address of shadow stack, */
unsigned long long int ssp_base;
/* Must be kept even if it is no longer used by glibc since programs,
like AddressSanitizer, depend on the size of tcbhead_t. */
__128bits __glibc_unused2[8][4] __attribute__ ((aligned (32)));

void *__padding[8];
} tcbhead_t;

这里正好介绍一下怎么找到这个pointer_guard,我们都知道stack_guard是存放canary的地方,所以我们用canary和search命令就能很轻松的找到这个stack_guard,再往下8位就是我们要找的pointer_guard,而且,因为stack_guard就在pointer_guard上方,所以这里的值肯定是不能填错的

image-20240812010438210

另一篇博客中学习到的方法:

image-20240812013631925

比赛中我们可能不能获取到pointer_guard的值,但是可以利用一些手段将其改写为一个已知值,比如largebin attack或者hoa等等

还是跟kiwi一样,我们要利用top chunk检查时的宏定义,不过这次我们利用的是其中的__fxprintf函数

1
2
3
4
5
6
7
8
9
10
11
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 ();
}

fxprintf函数会间接调用到vxprintf_internal函数,后者会调用_IO_cookie_read函数:

1
<__vfprintf_internal+280>    call   qword ptr [r12 + 0x38]

而这里的r12寄存器的值就是(_IO_cookie_jumps + 0x38), 这就是我们前面写的*vtable值

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

_IO_cookie_read函数

1
2
3
4
5
6
7
8
9
10
11
12
<_IO_cookie_read>:	 endbr64 
<_IO_cookie_read+4>: mov rax,QWORD PTR [rdi+0xe8]

<_IO_cookie_read+11>: ror rax,0x11
<_IO_cookie_read+15>: xor rax,QWORD PTR fs:0x30
<_IO_cookie_read+24>: test rax,rax
<_IO_cookie_read+27>: je <_IO_cookie_read+38>
<_IO_cookie_read+29>: mov rdi,QWORD PTR [rdi+0xe0]

<_IO_cookie_read+36>: jmp rax
<_IO_cookie_read+38>: mov rax,0xffffffffffffffff
<_IO_cookie_read+45>: ret

先看_IO_cookie_read的中间几行汇编,这是高版本glibc的一种保护方式–将地址进行简单加密,这两条指令实际上是在解密rax,首先循环右移0x11位,然后异或fs:0x30h,也就是异或pointer_guard,加密方式很好反推出来:首先异或pointer_guard,然后循环左移0x11位

再看上下,它直接jmp rax,也就是jmp QWORD PTR [rdi+0xe8],这里的rdi实际上就是假的_IO_FILE_plus结构体的地址,因此我们可以将任意可执行的地址写入到[rdi+0xe8],如果这里没有沙箱,我们可以让[rdi+0xe8]等于system函数地址,[rdi+0xe0]等于字符串/bin/sh的地址,而开启了沙箱的程序需要我们利用magic gadget来利用rdx来完成栈迁移,但是这里跟kiwi有一些不同,kiwi中利用的_IO_file_sync,在执行到_IO_file_sync时rdx为_IO_helper_jumps是可控的,而这里的rdx是不可控的,所以需要用到另一个gadget,称之为mg2

1
2
3
4
5
pwndbg> p/x 0x1675b0 + 0x7ffff7c00000
$5 = 0x7ffff7d675b0
pwndbg> tele 0x7ffff7d675b0
00:0000│ 0x7ffff7d675b0 ◂— mov rdx, qword ptr [rdi + 8]
01:0008│ 0x7ffff7d675b8 ◂— call qword ptr [rdx + 0x20]

这个gadget怎么找?

这个gadget能让rdx=[rdi+0xe8],然后call [rdx+0x20],也就是call [rdi+0x28]

而这里的rdi就是fake_io_file,我们在fake_io_file+0xe8处写mg2的地址,fake_io_file+0xe0处写一个地址-0x28(假设是bss-0x28),这样程序会将rdi更改为[fake_io_file+0xe0],然后jmp到mg2,mg2中将rdx设置为[rdi+8]也就是bss-0x20,接着call [rdx+0x20]也就是call [bss+0x20],我们只需要在这里存setcontext+61的地址即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
.text:0000000000053A6D                 mov     rsp, [rdx+0A0h]
.text:0000000000053A74 mov rbx, [rdx+80h]
.text:0000000000053A7B mov rbp, [rdx+78h]
.text:0000000000053A7F mov r12, [rdx+48h]
.text:0000000000053A83 mov r13, [rdx+50h]
.text:0000000000053A87 mov r14, [rdx+58h]
.text:0000000000053A8B mov r15, [rdx+60h]
.text:0000000000053A8F test dword ptr fs:48h, 2
.text:0000000000053A9B jz loc_53B56
......
.text:0000000000053B56 mov rcx, [rdx+0A8h]
.text:0000000000053B5D push rcx
.text:0000000000053B5E mov rsi, [rdx+70h]
.text:0000000000053B62 mov rdi, [rdx+68h]
.text:0000000000053B66 mov rcx, [rdx+98h]
.text:0000000000053B6D mov r8, [rdx+28h]
.text:0000000000053B71 mov r9, [rdx+30h]
.text:0000000000053B75 mov rdx, [rdx+88h]
.text:0000000000053B75 ; } // starts at 53A30
.text:0000000000053B7C ; __unwind {
.text:0000000000053B7C xor eax, eax
.text:0000000000053B7E retn

跟kiwi中一样,我们需要将rsp设置为rop链的地址,rcx设置为ret,也就是将[rdx+0A0h]设置为rop链的地址,[rdx+0A8h]设置为ret

此时,rdx=[fake_io_file+0xe0]=bss-0x28,所以我们在bss-0x28+0xa0=bss+0x78处写rop链的地址,bss-0x28+0xa8=bss+0x80处写ret就能完成栈迁移从而进行orw(无沙箱就是分别填system和ret,但是要多一个bss-0x28处填/bin/sh地址)