
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 通りの可能性を試す必要があります。これを突破するのはお疲れ様です(笑)。
たとえすべての可能性を突破したとしても、プログラムには権限がないため、フラグを読み取ることはできません。この `cat flag.txt` 文は意図的なもので、経験の浅いプレイヤーをこの `0x539` メニューオプションに誘導して惑わすだけでなく、プレイヤーが単純にこの関数を呼び出してフラグを読み取ることを防ぐためでもあり、これについては後で詳しく説明します。
***
## Add```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バイト長で、現在のチャンクの notes を指します。puVar2[5] = 0; という行によって、1バイトのオーバーフローは結局NULLで上書きされます。DAT_00105140 == 0 の場合、新しいチャンクを設定するだけなので、これはリストの head ポインタでしょうか?... print("Enter order index: "); __isoc99_scanf(&DAT_001030c5,&local_20); getc(stdin); if ((local_20 < 0) || (order_cnt <= local_20)) { print("Invalid index!\n"); } else { local_18 = head; for (local_1c = 0; local_1c != local_20; local_1c = local_1c + 1) { local_18 = (undefined8 *)local_18[5]; } print("Pick your bread: "); readline(local_18 + 1,8); print("Select your spread: "); readline(local_18 + 2,8); print("Choose your veg: "); readline(local_18 + 3,8);WE print("Slam your meat & egg: "); readline(local_18 + 4,9); print("Any side notes for the cook? "); readline(*local_18,0x30); } ...
入力インデックスのチェックがあるため、任意の場所を編集することはできません。
**ただし、4番目のフィールドの9バイト読み取りが5番目のフィールドに1バイトオーバーフローする問題は依然として存在します!!! `nxt` ポインタに1バイト上書きできます。**
***
## 表示```C
...
if (local_18 != (undefined8 *)0x0) {
printf("%s, %s, %s, %s, %s\n",local_18 + 1,local_18 + 2,local_18 + 3,local_18 + 4,*local_18);
}
...
%s は NULL 文字まで出力されるため、もし 4 番目のフィールドが 8 バイト長である場合、4 番目の %s によってチャンクの 4 番目のフィールドに加えて、5 番目のフィールドの nxt ポインタ値が出力されます。
=> ** ヒープ リークを取得!!!***
ここには特に興味深いものはありません。
カートを作成し、それをチェックアウトしてフラグを取得するという概念のみです。```C strcpy(local_d8,dir_name); sVar2 = strlen(dir_name); strcpy(local_d8 + sVar2,"/recipe"); creat(local_d8,0x1c0); iVar1 = open(local_d8,2); if (iVar1 == -1) { print("Error opening file f.\n"); /* WARNING: Subroutine does not return / exit(0); } chmod(local_d8,0x1f8); strcpy(local_98,dir_name); sVar2 = strlen(dir_name); strcpy(local_98 + sVar2,"/notes"); printf("%s %s\n","/notes",local_98); creat(local_98,0x1c0); __fd = open(local_98,2); if (__fd == -1) { print("Error opening file f_notes.\n"); / WARNING: Subroutine does not return */ exit(0); } chmod(local_98,0x1f8);
したがって、入力名のディレクトリにファイル `recipe` と `notes` を作成します。`chmod` ステートメントはこれらのファイルを実行可能モードに設定します: `0x1f8` と `0x1c0` はそれぞれ8進数で `0700` と `0770` です。```C
...
for (local_ec = 0; (local_e0 != (char **)0x0 && (local_ec < order_cnt)); local_ec = local_ec + 1)
{
sVar2 = strlen((char *)(local_e0 + 1));
write(iVar1,local_e0 + 1,sVar2);
write(iVar1,&DAT_00103127,1);
sVar2 = strlen((char *)(local_e0 + 2));
write(iVar1,local_e0 + 2,sVar2);
write(iVar1,&DAT_00103127,1);
sVar2 = strlen((char *)(local_e0 + 3));
write(iVar1,local_e0 + 3,sVar2);
write(iVar1,&DAT_00103127,1);
sVar2 = strlen((char *)(local_e0 + 1));
write(iVar1,local_e0 + 4,sVar2);
write(iVar1,&DAT_00103127,1);
sVar2 = strlen(*local_e0);
write(__fd,*local_e0,sVar2);
write(__fd,&DAT_00103127,1);
local_e0 = (char **)local_e0[5];
}
iVar1 = close(iVar1);
if (-1 < iVar1) {
iVar1 = close(__fd);
if (-1 < iVar1) {
local_58 = 0x2f706d742f207063;
local_50 = 0x732e796c6c656873;
local_48 = 0x206f;
local_40 = 0;
local_38 = 0;
local_30 = 0;
local_28 = 0;
local_20 = 0;
strcpy((char *)((long)&local_48 + 2),dir_name);
system((char *)&local_58);
if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return;
}
}
...
この巨大で乱雑なコードブロックは基本的に、チャンク内のコンテンツをこれらのファイルに書き込み、その後 cp /tmp/shelly.so dir_name というコマンドを実行します。ここで dir_name はプログラムの開始時に指定する名前です。
void FUN_00101e84(void)
{ long in_FS_OFFSET; char *local_18; long local_10;
local_10 = *(long *)(in_FS_OFFSET + 0x28); puts("HOLD UP, LET HIM COOK."); local_18 = (char *)0x0; execve("/usr/bin/pkexec",&local_18,(char **)&ptr_array); if (local_10 != *(long )(in_FS_OFFSET + 0x28)) { / WARNING: Subroutine does not return */ __stack_chk_fail(); } return; }
これは興味深い関数です。`NULL argv` と環境変数としての `notes` 配列ポインタを指定して `pkexec` を呼び出しており、これが CVE-2021-4034 の脆弱性を持つコマンドです。これは、何らかの方法で `notes` を細工し、実行をこの関数にリダイレクトする必要があることを示唆していますか?
***
## shelly.so
これを Ghidra で開くと、明らかに `set-UID-root` ライブラリであり、CVE エクスプロイトで使用されたものと類似していることがわかります。
***
# 概要
主なポイント:
1. プログラムの先頭でスタックリークがあります(注文番号)。これは、ファイル `recipe` の作成に使用される定数文字列 `/recipe` のアドレスを保持しています。
2. チャンクの0番目のフィールドは `notes` ポインタです。
3. チャンクの5番目のフィールドは、リンクリスト内の次のチャンクへの `nxt` ポインタです。
4. `Show` は、チャンクの5番目のフィールドであるヒープアドレスを漏洩できます。
5. `Edit` は5番目のフィールドに1バイトを上書きできます。**これが唯一の任意書き込みバグです!!**
6. `FUN_00101e84` は、NULL argv と我々が制御する環境変数(`notes` 配列)を指定して `pkexec` を呼び出します。
***
# エクスプロイト
## ヒープチャンクのレイアウト
gdb で簡単に調べると、プログラム内の異なるチャンク間のオフセットがわかります:```gdb
...
Pick your bread: aaaa
Select your spread: a
Choose your veg: a
Slam your meat & egg: a
Any side notes for the cook? 0000
1. ADD NEW ORDER
2. EDIT ORDER
3. SHOW ORDER
4. CANCEL ORDER
5. CHECKOUT
6. DONE
1
Pick your bread: bbbb
Select your spread: b
Choose your veg: b
Slam your meat & egg: b
Any side notes for the cook? 1111
...
gef➤ search-pattern aaaa
[+] Searching 'aaaa' in memory
[+] In '[heap]'(0x55555555a000-0x55555557b000), permission=rw-
0x55555555aae8 - 0x55555555aaec → "aaaa"
gef➤ x/30gx 0x55555555aae8-0x18
0x55555555aad0: 0x0000000000000000 0x0000000000000041
0x55555555aae0: 0x000055555555ab20 0x0000000061616161
0x55555555aaf0: 0x0000000000000061 0x0000000000000061
0x55555555ab00: 0x0000000000000061 0x000055555555ab60
0x55555555ab10: 0x0000000000000000 0x0000000000000041
0x55555555ab20: 0x0000000030303030 0x0000000000000000
0x55555555ab30: 0x0000000000000000 0x0000000000000000
0x55555555ab40: 0x0000000000000000 0x0000000000000000
0x55555555ab50: 0x0000000000000000 0x0000000000000041
0x55555555ab60: 0x000055555555aba0 0x0000000062626262
0x55555555ab70: 0x0000000000000062 0x0000000000000062
0x55555555ab80: 0x0000000000000062 0x0000000000000000
0x55555555ab90: 0x0000000000000000 0x0000000000000041
0x55555555aba0: 0x0000000031313131 0x0000000000000000
0x55555555abb0: 0x0000000000000000 0x0000000000000000
したがって、チャンクのサイズは 0x40(0x10 バイトのメタデータを含む)で、現在のチャンクの先頭から次のチャンクの先頭までのオフセットは 0x40+0x40 = 0x80 です。これは、notes がチャンクを割り当てるたびに割り当てられるため、連続するチャンクの間に常に配置されるからです。
エクスプロイトに進む前に、1 バイトのオーバーフローバグを悪用して任意のアドレスに書き込む方法を説明します。以下の戦略を使用できます。
nxt ポインタを漏洩します。
C です。B = C-0x80 になります。B の最下位バイトを x とします。x + 8 をオーバーフローさせます。結果の nxt ポインタは B + 8 になります。
A = 0x55555555aae0 とすると、漏洩したアドレスは B = 0x55555555ab60 になり、x = 0xe8 となります。A の 5 番目のフィールド(0x55555555ab08)の最後のバイトを x で上書きすると、結果の ポインタは となり、これは望む とは異なります。この関数にジャンプする方法があります(詳細は説明しません)。ただし、これを試す前に Dockerfile を確認してください。flag.txt について何か気づきましたか?```
...
chown root:root /home/ctf/flag.txt
...
USER peasant
CMD ["/home/ctf/start.sh"]
***これはルートが所有しており、プログラムは非特権ユーザーによって実行され所有されています。***
したがって、`cat flag.txt` を実行しても、プログラムがファイルにアクセスする権限がないため、フラグは表示されません。つまり、フラグを読むためにはルートに昇格する必要があります。
## 2つ目の方法 - CVE-2021-4034
CVE-2021-4034 を悪用するには、以下の設定が必要です。
1. 名前 `GCONV_PATH=.` のディレクトリ
2. このディレクトリ内に、`gconv-modules` 設定ファイルが置かれるディレクトリの名前である実行可能ファイルが必要です。このチャレンジではこれを `recipe` とします。
3. `recipe` ディレクトリ内に、内容を制御できる `gconv-modules` という名前のファイルと、シェルを起動する動的ライブラリ(おそらく与えられたバイナリ `shelly.so` でしょうか?)が必要です。
4. 次に、`gconv-modules` 内に以下のステップ5と同じ CHARSET と、動的ライブラリ `shelly.so` の名前を次のように記述する必要があります:`module UTF-8// SHELLY// shelly 2`。
5. これらすべてを行った後、NULL argv と細工された環境変数配列 `{"recipe", "PATH=GCONV_PATH=.", "CHARSET=SHELLY", "SHELL=shelly", NULL}` で `pkexec` を呼び出します。
さて、上記の設定を得るためにこのチャレンジを悪用します。
***
### 目標 1-2
ステップ1-2は簡単です:ユーザー名 `GCONV_PATH=.` でログインしてこのディレクトリを作成するだけです。その後、`checkout` を実行してこのディレクトリ内に実行可能ファイル `recipe` を作成します。その後、最初のメニューに戻ります。
***
### 目標 3-4
ユーザー名 `recipe` でログインしてこのディレクトリを作成します。ここで、`gconv-modules` という名前のファイルを作成する*必要*がありますが、`checkout` は `recipe` と `notes` のファイルしか作成しません。
もし文字列 `/notes` のメモリ位置を上書きして `/gconv-modules` にしたらどうでしょうか?これはうまくいくかもしれませんが、そのメモリが書き込み可能である必要があります。gdb で確認してみましょう:```gdb
gef➤ search-pattern /notes
[+] Searching '/notes' in memory
[+] In '/home/peasant/Desktop/chal'(0x555555559000-0x55555555a000), permission=rw-
0x555555559010 - 0x555555559016 → "/notes"
そしてそれは書き込み可能です!
書き込みはどのように行うのでしょうか?
/recipeの場所がわかります。/recipeから/notesへのオフセットが0x10であることがわかります。ステップ1で得た/recipeのアドレスからこれを減算して、/notesのアドレスを取得します。/notesを/gconv-modulesに上書きします。gconv-modulesファイルに正しい内容を書き込むことについては、checkout時にチャンクのnotes内容が/notes(現在は/gconv-modules)に書き込まれることがわかっています。確実にするために、すべてのnotesに文字列module UTF-8// SHELLY// shelly 2を提供すればよいです。
したがって、checkoutを呼び出すたびに、recipeとgconv-modulesの2つのファイルが作成され、gconv-modulesファイルにはmodule UTF-8// SHELLY// shelly 2(shellyはset-uid-rootライブラリとして与えられた.soの名前であるため使用)の行がいくつか書き込まれます。また、同じディレクトリにshelly.soがコピーされます。
notesが対応する環境変数である4つのチャンクを割り当てます。notesポインタ配列には、これら4つのnotesへの4つのポインタと、末尾のNULLポインタが含まれ、これがまさに必要なものです。main関数のRIPを秘密の関数FUN_00101e84に上書きします。
RIPの場所をどうやって知るのでしょうか?プログラム開始時にリークしたスタックアドレスを覚えていますか?gdbに入ってそのアドレスからRIPへのオフセットを調べ、そのオフセットを実際のリークアドレスに加算してRIPを取得します。mainの場所をどうやって知るのでしょうか?Goal 3-4のステップ2でリークした/recipeのアドレスを覚えていますか?ここでも同じことを行います!コメント付きの解答スクリプトはこのリポジトリにあります。 ご質問があればお気軽にご連絡ください :)
nxt0x55555555**ab**e80x55555555**aa**e8C ではなく B + 8 に配置されました。チャンク B を編集して、その 1 番目のフィールド(bread)に書き込み先の target アドレスを設定します。B + 8 から始まる内容が上書きされます。この 3 番目のチャンクの notes ポインタは target になります! したがって、notes に書き込んだものはすべて target に書き込まれます!