
CTF pwnチャレンジの書き起こしで、CVE-2021-4034 (pkexec) をヒープ操作で悪用し、Ghidraベースのリバースエンジニアリングとカスタムのshelly.soヘルパーバイナリを使用しています。
これは、私がCで書いたCTFのpwnチャレンジであり、ユーザーがCVE-2021-4034の脆弱性を悪用する必要があります。プレイヤーには、このリポジトリのchallengeディレクトリにある2つのバイナリが与えられます。chalバイナリはCTFチャレンジを実装し、shelly.soは補助バイナリです。
このwriteupを作成した時点では、Dockerfileはまだ完成していません。ライブCTF中にこのチャレンジをデプロイするにはDockerfileが必要ですが、ローカルでは不要です。以下のようにユーザー権限を設定し、脆弱なパッケージをインストールすることで、ローカルでこのチャレンジをエミュレートできます。
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つの実行ファイルを生成します。そのうちの1つが、私たちに与えられたヘルパーバイナリファイルです:```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
実行可能ファイルには入力が含まれています。
他の関数をいじってバグに遭遇することを期待することもできますが(可能性は高いですが)、本題に入り、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 通りの可能性を試す必要があります。これを突破するのはお疲れ様です(笑)。