はじめに
部内 CTF 初心者会用に作った picoGym Exclusive の Writeup です。
もくじ
- はじめに
- もくじ
- First Find (General Skills)
- Big Zip (General Skills)
- ASCII FTW (Reverse Engineering)
- ASCII Numbers (General Skills)
- Bit-O-Asm-1 (Reverse Engineering)
- Bit-O-Asm-2 (Reverse Engineering)
- Bit-O-Asm-3 (Reverse Engineering)
- Bit-O-Asm-4 (Reverse Engineering)
- GDB baby step 1 (Reverse Engineering)
- GDB baby step 2 (Reverse Engineering)
- GDB baby step 3 (Reverse Engineering)
- GDB baby step 4 (Reverse Engineering)
- Local Target (Binary Exploitation)
- Picker I (Reverse Engineering)
- Picker II (Reverse Engineering)
- Picker III (Reverse Engineering)
- Picker IV (Binary Exploitation)
- WPA-ing Out (Forensics)
- JAuth (Web Exploitation)
First Find (General Skills)
uber-secret.txt
というファイルを探します。 zipファイルを解答したらエクスプローラーなどで、解答したフォルダの中で uber-secret.txt
を検索してみましょう。
フラグ
Big Zip (General Skills)
big-zip-files.zip
を解答して、 grep
コマンドを使ってフラグを探します。
-r
オプションはディレクトリ内のファイルを再帰的に検索するオプションです。picoCTF{.*}
は正規表現で、picoCTF{
で始まり、}
で終わる文字列を検索します。.
はカレントディレクトリを表します。
フラグ
ASCII FTW (Reverse Engineering)
バイナリエディタ使って asciiftw
を見たら 0x00001188
付近にフラグがありました。4バイトずつ読んでいくとフラグが見つかります。
バイナリエディタは個人的には VSCode
の Hex Editor
拡張機能がおすすめです。
フラグ
ASCII Numbers (General Skills)
ASCIIコード表を使って文字に直す方法もありますが、 Python のコードを書いて楽にフラグを取得することもできます。
ubuntu なら、python3 がインストールされているので、solve.py
というファイルにコードを書いたら、以下のコマンドで実行できます。
フラグ
Bit-O-Asm-1 (Reverse Engineering)
eax
はアキュムレータと呼ばれるレジスタです。mov
はアセンブリ言語で値を代入する命令です。 この問題では <+15>
で eax
に 0x30
が代入されてます。 10進数に直すのを忘れないでください。
フラグ
Bit-O-Asm-2 (Reverse Engineering)
今度は <+22>
で eax
には DWORD PTR [rbp-0x4]
に格納されている値が代入されています。DWORD PTR [rbp-0x4]
は rbp
から 0x4
バイト分離れたアドレスに格納されている値を指します。rbp
はベースポインタと呼ばれるレジスタです。この問題では DWORD PTR [rbp-0x4]
には <+15>
で 0x9fe1a
が代入されています。10進数に直すのを忘れないでください。
フラグ
Bit-O-Asm-3 (Reverse Engineering)
imul
は掛け算をする命令です。 add
は足し算をする命令です。 <+15>
、<+22>
で DWORD PTR [rbp-0xc]
には 0x9fe1a
、DWORD PTR [rbp-0x8]
には 0x4
が格納されています。 <+29>
で eax
に DWORD PTR [rbp-0xc]
を代入し、 <+32>
で eax
に DWORD PTR [rbp-0x8]
を掛けて、 <+36>
で eax
に 0x1f5
を足しています。 <+41>
、<+44>
では、DWORD PTR [rbp-0x4]
に eax
を代入した後、 eax
に代入しているので、eax
は値が変わりません。プログラムっぽく書くと以下のようになります。
これらのことを考えると、以下の計算式が成り立ちます。16進数を10進数に直すのを忘れないでください。
フラグ
Bit-O-Asm-4 (Reverse Engineering)
<+22>
の cmp
命令は比較する命令です。次の <+29>
にある jle
は cmp
で比較した結果が <=
の場合に指定されたアドレス(ここでは<+37>
)にジャンプする命令です。 jmp
命令は無条件に指定されたアドレス(ここでは<+41>
)にジャンプする命令です。このアセンブリをプログラムっぽく書くと以下のようになります。
*(rbp-0x4)
は 0x9fe1a
で 0x2710
より大きいので、0x65
引かれます。10進数に直すのを忘れないでください。
フラグ
GDB baby step 1 (Reverse Engineering)
渡されたELFファイルで、eax
に代入されている値を答えるようです。ちなみに実行したらエラーを吐いてプログラムが終了しました。gdb
を使うと、ELFファイルのアセンブリを見ることができます。以下のコマンドで gdb
を起動します。
main 関数のアセンブリを見るには以下のコマンドを実行します。
ここまでこれれば、eax
に代入されている値がわかります。16進数を10進数に直すのを忘れないでください。
フラグ
GDB baby step 2 (Reverse Engineering)
先程の問題同様、gdb
でELFファイルのアセンブリを見ます。以下のコマンドで gdb
を起動します。
main 関数のアセンブリを見ましょう。
ループしてて計算がめんどいですね。<main+59>
にブレークポイントを指し、そこでプログラムを一時停止させ、eax
の値を見れば良さそうです。以下のコマンドで <main+59>
にブレークポイントを設定します。
以下のコマンドでプログラムを実行します。
Breakpoint 1, 0x0000000000401141 in main ()
と表示されたら、以下のコマンドで eax
の値を見ます。16進数を10進数に直すのを忘れないでください。
フラグ
GDB baby step 3 (Reverse Engineering)
0x2262c96b
をメモリに書き込んだら、メモリの中身を見てみてその並び順を答える問題です。以下のコマンドで gdb
を起動します。
main 関数のアセンブリを見ます。
<main+15>
で 0x2262c96b
が -0x4(%rbp)
に代入されています。<main+22>
にブレークポイントを指し、そこでプログラムを一時停止させ、-0x4(%rbp)
のメモリダンプを見ます。以下のコマンドで <main+22>
にブレークポイントを設定します。
以下のコマンドでプログラムを実行します。
Breakpoint 1, 0x000000000040111c in main ()
と表示されたら、以下のコマンドで -0x4(%rbp)
のメモリダンプを4byte分見ます。
フラグ
メモリにはリトルエンディアンで書き込まれるので、0x6b
, 0xc9
, 0x62
, 0x22
の順番でフラグが書き込まれています。逆に、0x22
, 0x62
, 0xc9
, 0x6b
で書き込まれるのはビッグエンディアンです。
GDB baby step 4 (Reverse Engineering)
main 関数で別の関数を呼び出しているようです。gdb
でその関数のアセンブリを見ます。以下のコマンドで gdb
を起動します。
main 関数のアセンブリを見ます。
<main+38>
で func1
が呼び出されています。func1
のアセンブリを見ます。
<func1+14>
で定数が掛けられています。16進数を10進数に直すのを忘れないでください。
フラグ
Local Target (Binary Exploitation)
launch instance
を押すとソースコードとバイナリが渡されるので、ソースコードを見ながら実行してみましょう。
任意の文字列を入れたら、変数 input
に gets
関数で代入され、変数 num
の値が出力されます。プログラムを見ると、 num
が 65
であれば、flag.txt
を読み込んでフラグが出力されるようです。
num
は ソースコードの 11 行目で 64
を代入されてから更新されていまっせん。そのため、num
に 65
を代入する方法を考えます。
gdb を使えば、簡単に代入できますが、今回はフラグがリモートにあるので、gdb は使えません。そこで、メモリ破壊をします。
文字列 "ABC" を入力したらときのメモリ上のイメージを以下に示します。
input
の領域には "ABC" が格納されています。num
の領域には 64
がリトルエンディアンで格納されています。ASCIIコード表はこちらにあります。
ここで、gets
関数ですが、gets
関数は標準入力から文字列を受け取り、ヌル文字を見つけるまで文字列を格納します。つまり、gets
関数は文字列の長さをチェックしません。そのため、16 文字列以上入れると input
の領域を超えて書き込むことが可能です。そのため、ある程度の長さを入れれば、num
の領域に文字列を書き込めます。とりあえず、'a' を 16 文字入れて、0~9までを2回繰り返す36文字を入力してみます。
出力は以下のようになります。
num
が 825243960
になりました。825243960
をリトルエンディアンで表すと 0x38
, 0x39
, 0x30
, 0x31
です。これは ASCII コードで文字列 8901
です。このことから、メモリ上のイメージ図は以下のようになります。
num
の領域に文字列 8901
が格納されています。num
が 65
になるように、num
の領域に A
を書き込めば良さそうです。つまり、以下のようなイメージ図になります。
こうすると、 num
の領域は 0x40
, 0x00
, 0x00
, 0x00
になります。これはリトルエンディアンで 64
です。以下のようなものを入力すれば良さそうです。
launch instance
を押してリモートにアクセスしましょう。ne
コマンドが出るのでコピペして実行します。人によって 00000
の数字が異なります。表示されたコマンドを使ってください。
フラグ
Picker I (Reverse Engineering)
launch instance
を押すとソースコードとバイナリが渡されるので、ソースコードを見ながら実行してみましょう。Try entering "getRandomNumber" without the double quotes
と出てくるので、getRandomNumber
と入力すると 4
が出てきます。
プログラムは Python のコードが渡されます。入力した文字列は eval
関数に ()
をつけて渡されます。Python の eval
関数は文字列を Python のコードとして実行します。例えば以下のPythonコードを考えます。
このコードは 1 + 2
を実行して 3
を返します。
getRandomNumber
関数では 4
を出力する関数になっています。
そのため、getRandomNumber
と入力すると 4
が出力されます。
ここで、win
関数を見てみましょう。win
関数では、 flag.txt
を読み込んでフラグを出力します。このことから、win
を入力すれば、 eval
関数で win
関数が実行され、フラグが出力されそうです。
フラグは 16 進数で表示されるので、以下のようなPythonコードを作ってあげると文字列の形でフラグを取得できます。
フラグ
Picker II (Reverse Engineering)
launch instance
を押すとソースコードとバイナリが渡されるので、ソースコードを見ながら実行してみましょう。filter
関数では、入力した文字列に win
が含まれているかを判定します。そのため、win
と入力すると、Illegal input
と出力され、win
関数が実行されません。
このことから、win
の文字列をそのまま入力するのではなく、分割して、"wi" + "n"
で入力してあげましょう。文字列の足し算を実行させてから、win
関数を実行させる必要があるため、入力にも eval
関数を使います。よって、入力は以下のようにすれば、フラグが16進数で出てきます。
'NoneType' object is not callable
と出てきますが、これは、もともとの eval
関数で実行する文字列が "eval("wi"+"n()")()"
となり、eval("wi"+"n()")
が None
を返すためです。フラグは出てくるので問題ありません。
フラグ
Picker III (Reverse Engineering)
launch instance
を押すとソースコードとバイナリが渡されるので、ソースコードを見ながら実行してみましょう。
1
, 2
, 3
, 4
の入力すると、func_table
に書かれている関数名の関数を実行するそうです。初期では、1
, 2
, 3
, 4
を入力するとそれぞれ、 print_table
, read_variable
, write_variable
, getRandomNumber
が実行されます。なんとかして win
関数を実行させましょう。
func_table
に win
を書き込んで、1
を入力すれば、 win
関数を呼び出せそうです。
write_variable
では、任意の変数に値を代入できそうです。ちなみに、func_table
と win
を入力したら、global func_table; func_table = win
になって、 func_table
には、文字列 win
ではなく、win
関数が代入され、func_table
が文字列ではなくなって、TypeError: object of type 'function' has no len()
となり正しく動作しなくなります。
さらに、call_func
では、func_table
に書かれている文字数を判定していて、128 文字でないと実行できません。そのため write_variable
での入力は以下の2つです。win
を入力するときは、 ダブルクォーテーションで囲み、中にwin
と空白合わせて 128 文字入れます。
これで、func_table
に win
が入るので、 1
を入力すれば、win
関数が実行されます。
フラグ
Picker IV (Binary Exploitation)
launch instance
を押すとソースコードとバイナリが渡されるので、ソースコードを見ながら実行してみましょう。
main
関数の val
に入ってる数値をアドレスとして、そのアドレスの関数を実行するそうです。win
関数のアドレスを確認しましょう。gdb
を使って確認します。
以下のコマンドで win
関数のアドレスを確認します。
win
関数のアドレスは 0x000000000040129e
のようです。この値を val
に入れてあげましょう。
フラグ
WPA-ing Out (Forensics)
wpa-ing_out.pcap
を開いてみてください (WiresharkでOKです)。このパケットの中身は WiFi の通信です。Aircrack-ng
を使ってパスワードを解読します。パスワードを解読する際、rockyou.txt
というパスワードリストを使って辞書攻撃をします。rockyou.txt
とは簡単に言えばよく使われるパスワードのリストです(詳しくはぐぐると出てきます)。Google等で rockyou.txt
と検索するとダウンロードできます。
Aircrack-ng
をダウンロードしましょう。
wpa-ing_out.pcap
と rockyou.txt
を同じディレクトリに入れ、そのディレクトリで以下のコマンドを実行します。
フラグ
🐀
JAuth (Web Exploitation)
JWTでは署名検証回避をすることができます。
JWT
とは、JSON Web Token
の略で、ヘッダー.ペイロード.署名
の構文で、JSONデータに署名と暗号化を施したものです。くわしくはwikiみてね
launch instance
を押すと開けるサイトを開いてください。サンプルのユーザーとパスワードの test
, Test123!
を入力してログインします。ログインすると、テストページが開けます。
テストページを開いたら開発者ツールを開いて、Application
タブを開きます。そして、 Storage
の Cookies
に token
があります。この token
をいじって admin 画面を開きましょう。
token.dev を使うと編集できます。JWT String
に token を入れてください。
上にある Algorithm
を none
にして、Payload
の role
を "user"
から "admin"
にしてください。そうすると JWT String
が変わります。変わったら、最後に .
をつけてください。JWT の構文は ヘッダー.ペイロード.署名
なので、署名が空文字だと、最後が .
で終わっている必要があります。この変わった token でブラウザの開発者ツールの Cookies
の token を上書きしてください。そして再読み込みするとフラグが出てきます。