这是一个我用 C 语言编写的 CTF pwn 挑战,要求用户利用 CVE-2021-4034 漏洞。玩家会在此仓库的 challenge 目录中获得 2 个二进制文件。chal 二进制实现了该 CTF 挑战,而 shelly.so 是一个辅助二进制。
在撰写本文时,Dockerfile 尚未完成。Dockerfile 是现场 CTF 中部署此挑战所必需的,但在本地并非必需:你仍然可以通过设置用户权限并安装易受攻击的软件包来在本地模拟此挑战,具体步骤如下:
libpolkit-gobject-1-0=0.105-26ubuntu1 libpolkit-agent-1-0=0.105-26ubuntu1 policykit-1=0.105-26ubuntu1。root:root 的 flag.txt 文件。challenge 文件夹中的文件下载到当前目录。chal。运行 chal 二进制可以让我们大致了解该二进制的作用:```
WELCOME TO THE HUB CTRL+ALT+DELICIOUS
We're not just a sandwich hub. We are the beacon of flavors, serving a symphony in every byte
在 `Add` 函数中调整输入大小,以及调整 `Cancel` 函数的索引,都没有给我们带来什么特别的结果(没有溢出或段错误)。不过,有一些有趣的地方:
- 订单似乎被放入一个列表(可能是链表)中,并且是从 0 开始索引的?
- 这里有 `Order number: 0x7ffde93681f0`,似乎打印的是栈上的某个地址?
- 另外,程序会创建一个以我们输入名称命名的目录,同时还有 3 个可执行文件,其中一个是给定的 helper 二进制文件:```sh
peasant@Tin-VM:~/Desktop$ ls
chal chal.c Dockerfile shelly.so solve.py tin
peasant@Tin-VM:~/Desktop$ ls -l tin/
total 28
-rwxrwx--- 1 peasant vboxsf 5 Feb 4 14:33 notes
-rwxrwx--- 1 peasant vboxsf 20 Feb 4 14:33 recipe
-rwxr-x--- 1 peasant vboxsf 16488 Feb 4 14:33 shelly.so
peasant@Tin-VM:~/Desktop$ cat tin/notes
0000
peasant@Tin-VM:~/Desktop$ cat tin/recipe
aaaa
bbbb
cccc
dddc
可执行文件包含我们的输入。
你可以尝试使用其他函数,并希望能碰到一些 bug(这很有可能),但我要直接切入正题,在 Ghidra 中打开该程序。
对比程序运行期间出现的字符串与 Ghidra 中存在的字符串,我们可以将一些 FUN_* 函数重命名为熟悉的名称:```C
undefined8 main(void)
{ int iVar1; size_t sVar2; undefined2 *puVar3; long in_FS_OFFSET; int opt; int local_1c; char *local_18; long local_10;
local_10 = *(long *)(in_FS_OFFSET + 0x28); local_18 = "/recipe"; print("WELCOME TO THE HUB CTRL+ALT+DELICIOUS\n"); print( "We're not just a sandwich hub. We are the beacon of flavors, serving a symphony in every by te\n\n" ); while( true ) { print("1. ENTER THE HUB\n"); print("2. QUIT\n"); __isoc99_scanf(&DAT_001030c5,&opt); getc(stdin); if (opt != 1) break; printf("Order number: %p\n",&local_18); print("Enter your name: "); __isoc99_scanf(&DAT_0010334f,&DAT_00105120); sVar2 = strlen(&DAT_00105120); puVar3 = (undefined2 *)malloc(sVar2 + 2); DAT_00105100 = puVar3; *puVar3 = 0x2f2e; *(undefined *)(puVar3 + 1) = 0; strcpy((char *)(DAT_00105100 + 1),&DAT_00105120); iVar1 = FUN_001022f0(DAT_00105100,&DAT_00105060); if (iVar1 == -1) { mkdir((char *)DAT_00105100,0x1c0); } DAT_00105140 = 0; order_cnt = 0; for (local_1c = 0; local_1c < 10; local_1c = local_1c + 1) { *(undefined8 *)(&ptr_array + (long)local_1c * 8) = 0; } main_menu(); } print("Come again :)\n");
该 `printf("Order number: %p\n",&local_18);` **打印出栈上某个局部变量的位置。**
在 gdb 中快速检查表明,泄露的地址是指向常量字符串 `/recipe` 的指针的地址:```gdb
...
Order number: 0x7fffffffdfc0
...
gef➤ x/gx 0x7fffffffdfc0
0x7fffffffdfc0: 0x0000555555559020
gef➤ x/s 0x0000555555559020
0x555555559020: "/recipe"
我们看到一个 mkdir 调用,它在当前目录中创建一个以我们输入名称命名的目录。
这些与我们运行程序时的观察一致。然后,它在调用 main_menu 函数之前初始化了一些变量,其大致内容如下:```C
while( true ) {
while( true ) {
while( true ) {
while( true ) {
while( true ) {
while( true ) {
print("1. ADD NEW ORDER\n");
print("2. EDIT ORDER\n");
print("3. SHOW ORDER\n");
print("4. CANCEL ORDER\n");
print("5. CHECKOUT\n");
print("6. DONE\n");
__isoc99_scanf(&DAT_001030c5,&local_40);
getc(stdin);
if (local_40 != 1) break;
add_order();
}
if (local_40 != 2) break;
edit_order();
}
if (local_40 != 3) break;
show_order();
}
if (local_40 != 4) break;
cancel_order();
}
if (local_40 != 5) break;
checkout();
}
if (local_40 == 6) break;
if (local_40 == 0x539) {
print(
"\nGORDON RAMSAY: Finally, a worthy opponent, our battle will be legendary! I BET YOU CAN 'T GUESS THE SECRET RECIPE.\n"
);
fgets(inp,0x20,stdin);
getrandom(random-bytes,0x10,0);
for (local_3c = 0; local_3c < 0x10; local_3c = local_3c + 1) {
if (inp[local_3c] != random-bytes[local_3c]) {
print("...Nuh Uh!...\n");
/* WARNING: Subroutine does not return */
exit(0);
}
print("...Ooh Yes.. sCruMpTioUs...");
}
print("Fine... I'll give you a taste.\n");
FUN_00101504();
}
如果你不熟悉 REV,这就是 C 语言中 `switch` 语句的反编译结果。有一个有趣的选项,即 `0x539`。 它允许玩家猜测 `0x10` 个随机字节。如果所有字节都相等,那么它会调用 `FUN_00101504();`,而该函数会调用 `system("cat flag.txt");`。否则,程序退出。
然而,暴力破解 16 个随机字节等同于尝试所有 256**16 = 340282366920938463463374607431768211456 种可能性。这个嘛,祝你好运 lol。
即使你遍历了所有可能性,程序仍然没有特权,因此它无法读取 flag。这个 `cat flag.txt` 语句是故意的,不仅是为了诱骗没有经验的玩家选择 `0x539` 这个菜单选项,也是为了防止玩家直接调用这个函数来读取 flag,我们稍后会深入探讨这一点。
***
## 添加```C
int iVar1;
undefined8 *puVar2;
void *pvVar3;
undefined8 *ptr2;
if (order_cnt < 10) {
puVar2 = (undefined8 *)malloc(0x30);
print("Pick your bread: ");
readline(puVar2 + 1,8);
print("Select your spread: ");
readline(puVar2 + 2,8);
print("Choose your veg: ");
readline(puVar2 + 3,8);
print("Slam your meat & egg: ");
readline(puVar2 + 4,9);
iVar1 = order_cnt;
pvVar3 = malloc(0x30);
*(void **)(&ptr_array + (long)iVar1 * 8) = pvVar3;
*puVar2 = *(undefined8 *)(&ptr_array + (long)order_cnt * 8);
print("Any side notes for the cook? ");
readline(*puVar2,0x30);
puVar2[5] = 0;
if (DAT_00105140 != (undefined8 *)0x0) {
for (ptr2 = DAT_00105140; ptr2[5] != 0; ptr2 = (undefined8 *)ptr2[5]) {
}
ptr2[5] = puVar2;
puVar2 = DAT_00105140;
}
DAT_00105140 = puVar2;
order_cnt = order_cnt + 1;
}
...
如果你觉得这些变量看起来不太容易读,那是因为我花了一些时间把它们重命名成这样,你在逆向工程时也应该这样做,以便跟踪各个变量的用途。好了,让我们进入要点:
puVar2 是一个 0x30 大小的 malloc 块。puVar3 指针,长度为 8 字节,指向当前 chunk 的 notes。puVar2[5] = 0; 这一行仍然会把我们溢出的 1 个字节改写为 NULL。DAT_00105140 == 0,我们就直接将它设置为新 chunk,所以这可能是指向链表头部的 head 指针?