[off-by-one]–plaidctf 2015 plaiddb

前言

这个题做了很久,太累了,放暑假还是没得在学校学习效率高,不是不想学就是有其他的事情,准备做完这些堆题,开始转为re为主了,还是感觉PWN没什么钱途呀。

分析

1
2
3
4
5
6
7
8
[!] Could not populate PLT: invalid syntax (unicorn.py, line 110)
[*] '/ctf/work/off_by_one/PLAiDB/PlaidDB'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: '/tmp/ld-2.23.so'

不是很妙,因为防护全开了,但不是很影响,这些防护基本上只能防护防护栈溢出,对于堆溢出基本没什么用。

tips

这里有个小知识点,因为这个这个漏洞需要用到一些基于lib2.23的性质,后面的2.23以后的glibc中有tache的存在,于是我们需要用patchelf工具来替换文件的so文件

具体的命令如下,因为我用的是现成的pwndocker来做的,docker中本身就自带各种so文件,于是我可以直接用,https://github.com/coke-pwn/pwndocker.git这是该docker的下载地址,如何配置我这里就不再介绍了。

1
2
3
cp /glibc/2.23/64/lib/ld-2.23.so /tmp/ld-2.23.so
patchelf --set-interpreter /tmp/ld-2.23.so ./test
LD_PRELOAD=./libc.so.6 ./test

运行完命令后,用ldd来查看一下是否修改成功,可以看到以及修改成功了。

1
2
3
4
root@74f21669baa4:/ctf/work/off_by_one/PLAiDB # ldd  PlaidDB                                                                               
linux-vdso.so.1 (0x00007fff23bf0000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f430526b000)
/tmp/ld-2.23.so => /lib64/ld-linux-x86-64.so.2 (0x00007f430567a000)

漏洞分析

开始分析逆向后的文件,如果你还没有分析一次的话,建议先自己去分析一遍,不太懂的地方再回来看看文章。

从main函数开始

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void __fastcall __noreturn main(int a1, char **a2, char **a3)
{
_QWORD *v3; // rbx
_QWORD *v4; // rax
char *v5; // rax

setbuf(stdin, 0LL);
setbuf(stdout, 0LL);
v3 = malloc(0x38uLL);
v4 = malloc(8uLL);
if ( v4 )
*v4 = 'g4lf3ht'; // 给v4赋值
*v3 = v4; // 将v4的地址给v3存储
v5 = (char *)malloc(9uLL);
if ( v5 )
strcpy(v5, "youwish\n"); // 给v5赋值
v3[2] = v5;
v3[1] = 8LL;
two_x(v3); // 二叉树
puts("INFO: Welcome to the PlaidDB data storage service.");
puts("INFO: Valid commands are GET, PUT, DUMP, DEL, EXIT");
while ( 1 )
main1();
}

部分函数我已经重新命名了,可以看到main函数先是申请了三个空间,一个地址空间存储的g4lf3ht一个存储的youwish,最后一个0x38大小的空间存储了前两个空间的地址和一个数字。结合题目,我们可以分析出来这个就是一个key-value的数据库,two_x函数对于我来说还是太复杂了,其他师傅分析出来是v3空间里面写入一个二叉树的结构体,是用来寻值的时候用的。

然后main1

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
unsigned __int64 sub_1A20()
{
bool v0; // zf
const char *v1; // rdi
__int64 flag; // rcx
char *v3; // rsi
const char *v4; // rdi
__int64 v5; // rcx
char *v6; // rsi
const char *v7; // rdi
__int64 v8; // rcx
char *v9; // rsi
const char *v10; // rdi
__int64 v11; // rcx
char *v12; // rsi
const char *v13; // rdi
__int64 v14; // rcx
char *v15; // rsi
char v17[8]; // [rsp+0h] [rbp-18h] BYREF
unsigned __int64 v18; // [rsp+8h] [rbp-10h]

v18 = __readfsqword(0x28u); // 栈溢出保护
puts("PROMPT: Enter command:");
gets(v17, 8LL); // 获取输入,最多8个
v1 = "GET\n";
flag = 5LL;
v3 = v17;
do
{
if ( !flag ) // v2等于0,break
break;
v0 = *v3++ == *v1++;
--flag;
}
while ( v0 );
if ( v0 )
{
GET(v1, v3);
}
else
{
v4 = "PUT\n";
v5 = 5LL;
v6 = v17;
do
{
if ( !v5 )
break;
v0 = *v6++ == *v4++;
--v5;
}
while ( v0 );
if ( v0 )
{
PUT(v4, v6);
}
else
{
v7 = "DUMP\n";
v8 = 6LL;
v9 = v17;
do
{
if ( !v8 )
break;
v0 = *v9++ == *v7++;
--v8;
}
while ( v0 );
if ( v0 )
{
DUMP(v7, v9);
}
else
{
v10 = "DEL\n";
v11 = 5LL;
v12 = v17;
do
{
if ( !v11 )
break;
v0 = *v12++ == *v10++;
--v11;
}
while ( v0 );
if ( v0 )
{
DEL(v10, v12);
}
else
{
v13 = "EXIT\n";
v14 = 6LL;
v15 = v17;
do
{
if ( !v14 )
break;
v0 = *v15++ == *v13++;
--v14;
}
while ( v0 );
if ( v0 )
Goodbye(v13, v15);
__printf_chk(1LL, "ERROR: '%s' is not a valid command.\n", v17);
}
}
}
}
return __readfsqword(0x28u) ^ v18;
}

main1函数就是读取一个输入,然后再根据不同的输入来进入相应的函数,分别是PUT,GET,DUMP,DEL,EXIT,PUT函数是用来读取一个key-value的数据进入数据库,GET是从数据库中根据key来读取value,DUMP是输出所有的key-value数据,EXIT就是退出数据库。

PUT函数

因为PUT函数是输入的函数,是比较重要的函数,我们先分析,这种函数一般也是出现漏洞比较多的函数。

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
void sub_1240()
{
void **stuct; // rbx
unsigned __int64 v1; // rax
void *v2; // rax
__int64 v3; // rbp
char v4[24]; // [rsp+0h] [rbp-38h] BYREF
unsigned __int64 v5; // [rsp+18h] [rbp-20h]

v5 = __readfsqword(40u);
stuct = (void **)malloc(0x38uLL); // 先申请0x38个字节的空间
if ( !stuct ) // 申请失败
{
puts("FATAL: Can't allocate a row");
exit(-1);
}
puts("PROMPT: Enter row key:");
*stuct = getkey(); // 获取键值
puts("PROMPT: Enter data size:");
gets(v4, 16LL);
v1 = strtoul(v4, 0LL, '\0'); // 把参数 str 所指向的字符串根据给定的 base 转换为一个无符号长整数(类型为 unsigned long int 型),
// base 必须介于 2 和 36(包含)之间,或者是特殊值 0。
stuct[1] = (void *)v1; // 数据大小
v2 = malloc(v1);
stuct[2] = v2; // 数据堆块的地址
if ( v2 )
{
puts("PROMPT: Enter data:");
fread_1(stuct[2], (size_t)stuct[1]);
v3 = two_x(stuct);
if ( v3 )
{
free(*stuct);
free(*(void **)(v3 + 16));
*(_QWORD *)(v3 + 8) = stuct[1];
*(_QWORD *)(v3 + 16) = stuct[2];
free(stuct);
puts("INFO: Update successful.");
}
else
{
puts("INFO: Insert successful.");
}
}
else
{
puts("ERROR: Can't store that much data.");
free(*stuct);
free(stuct);
}
}

看完函数其实就是,读取key,读取size,再读取value,然后分别将key-size-value输入到结构体中去,结构体中还存在一个二叉树。具体的结构体如下:

1
2
3
4
5
6
7
8
9
struct Node {
char *key;
long data_size;
char *data;
struct Node *left;
struct Node *right;
long dummy;
long dummy1;
}

然后我们发现在读取key的时候,作者自己实现了一个读取输入的函数getkey,于是我们去看看该函数

getkey(漏洞函数)

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
// 该函数的作用主要是将输入的字符串放到一个堆块里面去,然后返回一个地址
char *getkey()
{
char *memory_1; // r12
char *memory_1_copy; // rbx
size_t real_memory; // r14
char IO_char; // al
char IO_char_copy; // bp
__int64 subtract_between; // r13
char *realloc_1; // rax

memory_1 = (char *)malloc(8uLL); // 一开始分配8个空间
memory_1_copy = memory_1;
real_memory = malloc_usable_size(memory_1); // Linux下获取malloc实际分配的内存大小,8在64位对应的是24
while ( 1 )
{
IO_char = _IO_getc(stdin); // 读取下一个字符 并将其作为无符号字符转换为 int 或EOF在文件结尾或错误时返回。
IO_char_copy = IO_char;
if ( IO_char == -1 ) // EOF就是读取错误,就直接返回
Goodbye();
if ( IO_char == '\n' ) // 如果读取的为换行符,就跳出循环
break;
subtract_between = memory_1_copy - memory_1;
if ( real_memory <= memory_1_copy - memory_1 )
{
realloc_1 = (char *)realloc(memory_1, 2 * real_memory);
memory_1 = realloc_1;
if ( !realloc_1 ) // 如果realloc不成功就报错
{
puts("FATAL: Out of memory");
exit(-1);
}
memory_1_copy = &realloc_1[subtract_between];
real_memory = malloc_usable_size(realloc_1);
}
*memory_1_copy++ = IO_char_copy; // 漏洞所在,这里有可能访问到本来访问不到的地址。漏洞所在,此时 v3 作为索引,指向了下一个位置,
// 如果位置全部使用完毕则会指向下一个本应该不可写位置
}
*memory_1_copy = '\0'; // 将最后的一位给置为0,也就是存在0字节溢出
return memory_1;
}

通读完源代码,发现在最后面对将从_IO_getc函数读取的字符赋值给申请的空间上的时候存在判断错误。函数的漏洞点位于0X1040处, 用于获取键值,当输入换行符时,会将其替换成 null 字节,如果输入长度为 chunk usable size 且最后一个字节为换行符的字符串,则会触发 off-by-one。就会将下一个chunk的size更改。

关键的漏洞函数已经分析完了,剩下的函数分不分析都差不多了。

现在开始利用漏洞

大概思路是这样的

首先利用off-by-one漏洞泄露libc的地址再说

然后利用fastbin attack来修改__malloc_hook的指向为one_gagedt