目录
前言
how2heap(https://github.com/shellphish/how2heap)可以说是目前"权威"的glibc中的heap exploitation techniques文档。在学术界近年的一些顶会文章也有引用这个github项目。
攻防对抗是一直持续发展的,很容易在学习中,复现一些过时的攻击技术出现困难,原因就是已经有补丁打上了。本文大致总结一些在比赛中常见的过时技术,持续更新…吧
不再好用的unsorted bin attack
2021.4
都2021了还能在一些人的wp上看到如果有unsorted bin attack可以怎么怎么样。
how2heap上的描述是<libc-2.29。实测该补丁已经给到了glibc2.27-3ubuntu1.4。
CTFWiKi中描述
Unsorted Bin Attack 可以达到的效果是实现修改任意地址值为一个较大的数值。
已经不可能实现任意地址的修改了。
新的unsorted bin attack要求修改的地址存放者的是一个特定的指针,类似新unlink技术。补丁如下:
+ if (__glibc_unlikely (bck->fd != victim)
+ || __glibc_unlikely (victim->fd != unsorted_chunks (av)))
+ malloc_printerr ("malloc(): unsorted double linked list corrupted");
一个普遍替代的方案,适用于libc-2.31,那就是large_bin_attack。
其效果可以把一个任意地址的内容写成一个largin bin的地址
另一个替代方案,适用与libc-2.31,[tcache_stashing_unlink],可以参考我另一个文章。
其效果有多种,其中之一可以把一个任意地址的内容写成一个main_area的地址(很大的数)。
严格检查的tcache double free
2021.4
tcache对double free有了严格的检查,其补丁已经给到了libc2.27。可以说在只有tache内的double free的情况下,必定能检查出来。
这里做一个笔记吧,一个很巧妙的检查方法。如果要严格检查,思路很简单,那就暴力查找这个链上所有的free chunk。那么这个补丁是怎么权衡性能开销和安全的?
//第一次free的时候,走到这里时
static __always_inline void
tcache_put (mchunkptr chunk, size_t tc_idx)
{
tcache_entry *e = (tcache_entry *) chunk2mem (chunk);
assert (tc_idx < TCACHE_MAX_BINS);
/* Mark this chunk as "in the tcache" so the test in _int_free will
detect a double free. */
e->key = tcache; //关键, key被赋值成一个地址,可以理解为一个随机数
e->next = tcache->entries[tc_idx];
tcache->entries[tc_idx] = e;
++(tcache->counts[tc_idx]);
}
//第二次free的时候,走到这里时
static void
_int_free (mstate av, mchunkptr p, int have_lock) {
...
...
...
if (tcache != NULL && tc_idx < mp_.tcache_bins)
{
/* Check to see if it's already in the tcache. */
tcache_entry *e = (tcache_entry *) chunk2mem (p);
/* This test succeeds on double free. However, we don't 100%
trust it (it also matches random payload data at a 1 in
2^<size_t> chance), so verify it's not an unlikely
coincidence before aborting. */
if (__glibc_unlikely (e->key == tcache)) // 关键,检查这个数。一般来说用户使用后这个随机数会被覆盖。如果没有被覆盖的话,进一步检查。
{
tcache_entry *tmp;
//暴力检查链上所有的chunks
for (tmp = tcache->entries[tc_idx];
tmp;
tmp = tmp->next)
if (tmp == e)
malloc_printerr ("free(): double free detected in tcache 2");
/* If we get here, it was a coincidence. We've wasted a
few cycles, but don't abort. */
}
...
...
}
...
...
...
}
劫持IO_FILE的vtable,_IO_strfile不好用
2021.7
libc2.24以后,vtable会增加检查,所有的vtable跳转都会经过IO_validate_vtable()
,添加的逻辑是在宏定义修改的。实测目前最新的libc-2.23-0ubuntu11.2
仍然未加小补丁,估计以后也不会加。
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;
uintptr_t ptr = (uintptr_t) vtable;
uintptr_t offset = ptr - (uintptr_t) __start___libc_IO_vtables;
if (__glibc_unlikely (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;
}
ctfwiki介绍的一个绕开的思路是使用_IO_strfile
的vtable。可惜其中的函数指针libc>=2.29
已经弃用,修改的代码如下。实测libc-2.27-3ubuntu1.4
已经打上这个补丁,故也无效了。
int
_IO_str_overflow (FILE *fp, int c)
{
...
//new_buf
= (char *) (*((_IO_strfile *) fp)->_s._allocate_buffer) (new_size);
//写死成malloc()了,变成绝对地址call
new_buf = malloc (new_size);
...
}
struct _IO_str_fields
{
/* These members are preserved for ABI compatibility. The glibc
implementation always calls malloc/free for user buffers if
_IO_USER_BUF or _IO_FLAGS2_USER_WBUF are not set. */
_IO_alloc_type _allocate_buffer_unused;
_IO_free_type _free_buffer_unused;
};
#一个判断是否能用的方法
from pwn import *
f = libc.functions['_IO_str_overflow']
print(libc.disasm(f.address, f.size))
#这样的就是能用的 libc-2.23
7cd0e: 4c 89 f7 mov rdi, r14
7cd11: ff 93 e0 00 00 00 call QWORD PTR [rbx+0xe0]
7cd17: 48 85 c0 test rax, rax
7cd1a: 49 89 c7 mov r15, rax
7cd1d: 0f 84 cd 00 00 00 je 0x7cdf0
7cd23: 4d 85 e4 test r12, r12
7cd26: 74 1f je 0x7cd47
7cd28: 4c 89 ea mov rdx, r13
7cd2b: 4c 89 e6 mov rsi, r12
7cd2e: 48 89 c7 mov rdi, rax
7cd31: e8 ba 77 01 00 call 0x944f0
7cd36: 4c 89 e7 mov rdi, r12
7cd39: ff 93 e8 00 00 00 call QWORD PTR [rbx+0xe8]
#这样的是不能用的 libc-2.27
900ae: 4c 89 f7 mov rdi, r14
900b1: e8 0a 12 f9 ff call 0x212c0 #call malloc@plt 不可写的got
900b6: 48 85 c0 test rax, rax
900b9: 49 89 c5 mov r13, rax
900bc: 0f 84 fe 00 00 00 je 0x901c0
900c2: 4d 85 e4 test r12, r12
900c5: 74 1e je 0x900e5
900c7: 4c 89 fa mov rdx, r15
900ca: 4c 89 e6 mov rsi, r12
900cd: 48 89 c7 mov rdi, rax
900d0: e8 8b 10 f9 ff call 0x21160
900d5: 4c 89 e7 mov rdi, r12
900d8: e8 eb 11 f9 ff call 0x212c8 #call free@plt 不可写的got
严格来说,如果假设攻击者有更多的能力,那么IO_str_file在libc-2.31仍然也是可以使用的。
例如,假设已经劫持了malloc_hook或者free_hook,就可以通过_IO_str_overflow来触发。
其触发的参数都是_IO_FILE内相关,很容易控制
具体可参考这篇文章[https://www.anquanke.com/post/id/216290]
glibc2.29以后的vtable是处于可写的数据区。当有足够的写能力时,不必纠结于IO_FILE中的vtable ptr,直接写vtable里面的函数指针即可。
fastbin在malloc_consolidate()也要求chunk_size正确
2021.5
实测libc-2.27.so已有如下代码(但是libc-2.23.so没有补丁),所以如同CTFWIFI中所说的house of rabbit已经过时了
而house of rabbit 就利用了在 malloc consolidate 的时候 fastbin 中的堆块进行合并时 size 没有进行检查从而伪造一个假的堆块,为进一步的利用做准备。
static void malloc_consolidate(mstate av) {
...
...
{
unsigned int idx = fastbin_index (chunksize (p));
if ((&fastbin (av, idx)) != fb)
malloc_printerr ("malloc_consolidate(): invalid chunk size");
}
abort() 不再刷新标准输入输出缓存
2021.8
偶然发现一些萌新(我也是)分享FSOP或者是house of orange时,会错误提到其触发_IO_flush_all_lockp()
的条件可以是abort()
,ctf-wiki也是如此说明,然而现在看来这是不对的。
自libc-2.27以来abort()
不再fflush(NULL)
,libc-2.26及以下倒是还在。
这意味着:
- malloc_printerr()里面走的是abort(),此路不通。
- 想继续利用FSOP的思路,可能只能走exit()了。
// glibc-2.23
// in stdlib/abort.c
void
abort (void)
{
...
/* Flush all streams. We cannot close them now because the user
might have registered a handler for SIGABRT. */
if (stage == 1)
{
++stage;
fflush (NULL);
}
...
/* Now close the streams which also flushes the output the user
defined handler might has produced. */
if (stage == 4)
{
++stage;
__fcloseall ();
}
...
}
// glibc-2.27以后
// in stdlib/abort.c
void
abort (void)
{
//相关flush代码已被删除
...
}
为啥现在libc出错不会打印 Backtrace?
2021.8
萌新(我也是)在打glibc-2.23的时候可能会发现这样的现象:
实际上在比赛中也会发现服务器打印出这些Backtrace,如此不就是轻松知道基地址了吗?就算开了ALSR,这样的信息有助于寻找libc的小版本。然而,glibc-2.27以后就消失了,这是为啥?
// in glibc-2.23
/* Abort with an error message. */
void
__libc_message (enum __libc_message_action action, const char *fmt, ...){
...
...
if (do_abort)
{
BEFORE_ABORT (do_abort, written, fd);
/* Kill the application. */
abort ();
}
}
// in glibc-2.27
/* Abort with an error message. */
void
__libc_message (enum __libc_message_action action, const char *fmt, ...){
...
...
if ((action & do_abort))
{
if ((action & do_backtrace))
BEFORE_ABORT (do_abort, written, fd);
/* Kill the application. */
abort ();
}
}
// in glibc-2.31
/* Abort with an error message. */
void
__libc_message (enum __libc_message_action action, const char *fmt, ...){
...
...
if ((action & do_abort))
/* Kill the application. */
abort ();
}
这是一个悲伤的故事,我本以为是abort()变换,实际上是被哪个大佬把这块给咔嚓了。glibc-2.23的时候是无条件BEFORE_ABORT()
的,这里会打印Backtrace。glibc-2.27加了额外的判断,走malloc_printerr()
是不打印的,(但是实测这里似乎写的有点问题)。glibc-2.31就直接删掉了。自然Backtrace消失了。下面贴一下BEFORE_ABORT()
的实现方便理解。
static void
backtrace_and_maps (int do_abort, bool written, int fd)
{
if (do_abort > 1 && written)
{
void *addrs[64];
#define naddrs (sizeof (addrs) / sizeof (addrs[0]))
int n = __backtrace (addrs, naddrs);
if (n > 2)
{
#define strnsize(str) str, strlen (str)
#define writestr(str) write_not_cancel (fd, str)
writestr (strnsize ("======= Backtrace: =========\n"));
__backtrace_symbols_fd (addrs + 1, n - 1, fd);
writestr (strnsize ("======= Memory map: ========\n"));
int fd2 = open_not_cancel_2 ("/proc/self/maps", O_RDONLY);
char buf[1024];
ssize_t n2;
while ((n2 = read_not_cancel (fd2, buf, sizeof (buf))) > 0)
if (write_not_cancel (fd, buf, n2) != n2)
break;
close_not_cancel_no_status (fd2);
}
}
}
#define BEFORE_ABORT backtrace_and_maps