たのしいpwn
SSR_CTF_BU
CTFってなんやねん
•Capture The Flag
• 直訳するとはた取りゲーム
• FPSとかでも使われる単語らしい?
•機密文書という設定のテキストファイル(Flag)を読みだして提出すると点数に
• そのファイルは与えられた権限では読めなかったり
• そもそもサーバーに入れてもらえなかったり
• 暗号化されていたり
•という感じで普通には読めないファイルを†ハッキング†して読んでくれ、という
競技
例
•IFMMP DUG XPSME!
• これは暗号文です
• 解読してみよう
•アルファベットが一文字ずつずらされているだけ
• シーザー暗号
•HELLO CTF WORLD!
pwnってなんやねん
•さっき説明したCTFの問題ジャンルの一つ
• pwning,pwnablesとか言われる(ownが転じてpwnらしい)(どうでもいい)
• 昔iPhoneで遊ぶツールにpwnagetoolとかあったよね
•実行ファイルの脆弱性を突いて任意機械語を実行してね、という問
題ジャンル
• 大体シェル(/bin/sh)を開くことが多い
• CTFではファイルを読めれば勝ちなのでファイルを読むことも
• 個人的に一番†ハッキング†っぽいジャンルだと思います(何)
やってみる(その前に)
•そんなに難しいことをしているわけではない
•が、プログラミングを知らないと厳しい
• 必要な事前知識は解説しますが、時間の都合上駆け足にならざるを得ない
• 分からなくてもムリダナ(・x・)ってならないでほしい
• C言語講習会の後にもう一度見直すとまた違った見え方をするかも
• 「こんなことも出来るんだ!」という雰囲気を感じてほしい
アセンブリ言語ってなんや
(時間次第では飛ばす)
•C言語で書いたプログラムは機械語に変換されて動く
• 機械語は01列だが、01011101…だと読みづらいので16進数で書かれることが多い
• ex:31 ffで xor edi,edi
• ediレジスタ同士でXORを取り、その結果をediに代入するという意味、これを行うと
ediに0を入れることができる
•31 ffとかいきなり言われてもなんのことだかわからないので人間に
分かりやすいように書きたい、それがアセンブリ言語
•アセンブリ言語は機械語と一対一対応する
• 上記の例だと、31 ffは絶対xor edi , edi、それ以外にはならない
•C言語は機械語と一対一対応しない
• 同じプログラムでもコンパイラや最適化度合で違う機械語が出る(動作は同じ)
アセンブリ言語ってなんや2
gccによるコンパイル結果
x86アセンブリ速習(まさかりを投げるな)
•これからの話を聞くのに必要最低限の命令だけ列記
• オペランドの種類とか面倒な部分はカット(今回の主目的でないため)
•mov dst , src :srcをdstにコピー
•push src :srcをスタックにプッシュ
•pop dst :dstにスタックからポップ
•call arg :次の命令のアドレス(戻り先)をスタックにpushし、argにジャンプ
•ret :スタックから 戻り先を取り出してそこにジャンプ
スタック1(時間次第で飛ばす)
•スタックの話(x86の話です、スタックが無いアーキテクチャもあり)
• プログラムが実行される時、ほとんどの情報はスタックに乗る
• よくお皿を積み上げていくことに例えられるデータ構造
8
push 8 push 0push 1
1
8
0
1
8
1
8
pop (0が出てくる)
スタック2 (時間次第で飛ばす)
•関数を呼び出す時、引数の情報はスタックに乗る
• esp:スタックのてっぺん(stack pointer)
• ebp:スタックの底(base pointer)
esp
ebp
4
1
5
4
1
1
スタック3
以下のコードが実行される時のスタックの様子を追いかけてみる
0x08048430 <+6>: sub esp,0x10 #スタック領域を0x10バイト確保
0x08048433 <+9>: mov DWORD PTR [esp+0x4],0x202 #スタックに514を書き込み
0x0804843b <+17>: mov DWORD PTR [esp],0x72 #514の一個上に114を書き込み
0x08048442 <+24>: call 0x804841d <func> #func呼び出し
0x08048447 <+29>: mov DWORD PTR [esp+0x4],eax #funcの結果をスタックに書きこみ
0x0804844b <+33>: mov DWORD PTR [esp],0x80484f0 #文字列のアドレスをスタックに書き込み
0x08048452 <+40>: call 0x80482f0 <printf@plt> printf呼び出し
0x08048457 <+45>: leave #このプログラムのためのスタック領域を後始末
0x08048458 <+46>: ret #終
func関数の中身
0x0804841d <+0>: push ebp
0x0804841e <+1>: mov ebp,esp
0x08048420 <+3>: mov eax,DWORD PTR [ebp+0xc]
0x08048423 <+6>: mov edx,DWORD PTR [ebp+0x8]
0x08048426 <+9>: add eax,edx
0x08048428 <+11>:pop ebp
0x08048429 <+12>:ret
sub esp 0x10
0xffffd178
0xffffd160 esp
esp
新たに0x10(16)バイトのスタック
領域を確保
ebp
mov二つ
114(0x72)
514(0x202)
0xffffd178
0xffffd160 esp
mov DWORD PTR [esp+0x4],0x202
mov DWORD PTR [esp],0x72
ebp
esp+4
call func
0x8048447
114(0x72)
514(0x202)
0xffffd178
0xffffd160
esp
C言語で表すとfunc(114,514);
第二引数->第一引数の順で積まれ
ていることがわかる
ここでの0x8048447は戻り先(callの
次のアドレス)
手元でスライドを見れる人は見ると
callの次の命令のアドレスだ
ということが分かるはず
ここで、処理は関数func内に移るebp
push ebp (時間次第で飛ばす)
0xffffd178
0x8048447
114(0x72)
514(0x202)
0xffffd178
0xffffd160
esp
関数func用の新たなスタック領域を
用意するが、mainに戻った時の
ために古いebp(スタックの底)を
スタックに記録しておく
ebp
mov ebp,esp
0xffffd178
0x8048447
114(0x72)
514(0x202)
0xffffd178
0xffffd160
esp
スタックの底を更新
ebp
mov eax,DWORD PTR [ebp+0xc]
mov edx,DWORD PTR [ebp+0x8]
0xffffd178
0x8048447
114(0x72)
514(0x202)
0xffffd178
0xffffd160
esp
ebp+0xcとebp+0x8はmain関数で
スタックに積んだ引数です
それをそれぞれeaxレジスタとedx
レジスタに入れています
eax=514
edx=114
ebp
ebp+0x8
ebp+0xc
add eax,edx
0xffffd178
0x8048447
114(0x72)
514(0x202)
0xffffd178
0xffffd160
esp
func関数の足し算の本質部分です
さっき入れた二つの数値を足し算
してeaxに入れています
(eax=eax+edx)
ちなみに関数の戻り値はeaxで
返されます
ebp
ebp+0x8
ebp+0xc
pop ebp (時間次第で飛ばす)
0xffffd178
0x8048447
114(0x72)
514(0x202)
0xffffd178
0xffffd160
esp 関数の先頭で保存したebp(スタック
の底)をポップします
これにより、スタックの底は再度
main関数のものに戻ります
ebp
ret
0xffffd178
0x8048447
114(0x72)
514(0x202)
0xffffd178
0xffffd160
esp callによって保存した戻り先(リター
ンアドレス)を使って戻ります。
次の命令(espが指している部分に
格納されている)は0x8048447です。
ebp
mov DWORD PTR [esp+0x4],eax
0xffffd178
0x8048447
114(0x72)
628(0x274)
0xffffd178
0xffffd160 esp
無事戻ってくることが出来たので
処理は続き、printfの引数をスタック
に積みます。
この時、eaxにはfuncの戻り値が
入っています。
ebp
今回扱う脆弱性
•関数を呼び出す時には戻り先をスタックに載せているらしい
• もしも戻り先がユーザーによって書き換えられてしまったら任意の場所
に処理を飛ばせてしまうのでは?
•バッファーオーバーフロー
• 確保したメモリの領域を超えて書き込んでしまう事
• これにより実際に戻り先を任意の場所にできてしまう!
脆弱なコード
C言語講習会とかでもやるはず
#include <stdio.h>
int main(){
char buf[10];
printf("buf_addr=%pn",buf);
printf(“what is your name?n> ");
scanf("%s",buf);
printf("Hello %s!n" ,buf);
return 0;
}
脆弱性
•なにがやばいの><
• scanfは長さを確認しないで入力を受け取ってしまう。
• buf[10]で10バイトしか領域を確保していない(本当はアラインメント等でもっと確保さ
れることもある)ので、それ以上の入力が来たらさっきの戻り先を上書きしてしまうか
も。
• やってみよう
これならセーフ(本当はアウト)
AAAA
AAAA
AAAA
0xdeadbeef
…
bufの先頭アドレス
戻り先アドレス
リターンアドレスは壊れていな
いので正しく戻れるが、関数
先頭でスタックに保存されて
いるはずのebpの値は破壊さ
れているので、戻った後ebp
の値が0x41414141(AAAA)に
なります
こうなるとヤバイ
AAAA
AAAA
AAAA
AAAA(0x41414141)
…
bufの先頭アドレス
戻り先アドレス
scanfの入力が想定より大き
かったためリターンアドレスを
上書きしてしまった!
任意コード実行の方針
AAAA
AAAA
AAAA
0xbeefbabe
シェルを起動するための
機械語列
bufの先頭アドレス
戻り先アドレス
入力であらかじめシェル起動
のための機械語列を
読み込んでおいて、
戻り先アドレスをそこに
すれば良い。
0xbeefbabe
既にやってみたものがこちらになります
文字列が格納されるbufの
アドレスが毎回変わって
いる
なにこれ
脆弱性の緩和策
•典型的な脆弱性にはOS側で緩和策が取られている
• ASLR(address space layout randomization)
• DEP(data execution prevention)
• SSP(stack-smashing protection)
•実際のCTFではこれらを回避して任意コードを実行せねばならない
が、今回は簡単のため全部外す
ASLR
•先ほど述べたスタックや、mallocなどで割り当てられるヒープのアドレスをラン
ダムにする機構
•スタックのアドレスがランダムだと、スタックに機械語を読み込み、それを実行
する、ということが難しくなる(リターンアドレスをどこにすればいいかわからない
ため。)
•これがオンになっている場合は、必要なアドレスのリークから始める(writeで標
準出力に書きだすとか)
•$ sudo sysctl –w kernel.randomize_va_space=0で無効化できる
DEP
•データ実行防止
• スタック上の機械語を実行することは普通は無い
• じゃあスタックを実行不可能にすればさっきみたいな攻撃は防げるよね
•実行ファイルに入っている機械語は実行できるのでそれをつぎはぎすれば攻
撃可能(ROP)
• 現実のゲーム機(3〇Sとか)のハックなどでも使われているテク
•コンパイル時に以下のオプションを付ければスタックが実行可能になる
• gcc –z execstack
SSP
•なんとかしてバッファーが溢れているのを検知できないか?
• スタックの底部分に検査用の値を仕込んでおいて、その値が書き変わっていたら不
正としてプログラムを落とせばよくない?
• 検査用の値のことをcanaryという(炭鉱のカナリアが由来らしい)
•正直CTFでこれがオンになっていると相当しんどい(つまり優秀な緩和策)
• SECCON 2016 Online CTF checker
• シェルを取るのは諦めて、SSP由来の情報漏洩を狙う
•canaryをリークしたり、SSP由来の情報漏洩を狙ったり手はいろいろ
• 他の脆弱性を探す事も
•$ gcc -fno-stack-protector bof.c でオフに出来る
任意コード実行の方針(再掲)
AAAA
AAAA
AAAA
0xbeefbabe
シェルを起動するための
機械語列
bufの先頭アドレス
戻り先アドレス
入力であらかじめシェル起動
のための機械語列を
読み込んでおいて、
戻り先アドレスをそこに
すれば良い。
0xbeefbabe
実際にやってみます
•$ sudo sysctl –w kernel.randomize_va_space=0 でASLR無効化
•$ gcc –m32 -fno-stack-protector -z execstack bof.c でコンパイル
•(python o.py [bufのアドレス] ; cat) | ./a.out
• python o.pyの結果を流し込んだ後、catでターミナルから入力を受け取ってそれ
を./a.outに流し込む
• シェルが起動していることが確認できるはずです(失敗したらごめん)
• pythonを使うのは、キーボードから入力できない文字を入力するから
• 0xffとか
宣伝2
•ロボット技術研究会としてCTFの大会に参加しています
• SSR_CTF_BU(CTF部という意味)
• SECCONの決勝大会に行ったりもした
• 興味を持ったら(@ymduu)まで。
参考文献(おすすめの本とか)
•セキュリティコンテストチャレンジブック
• CTFを始めるならコレ
• バイナリ問題の入門には最適
• Webやるならちょっと足りないかも
•Hacking: 美しき策謀 第2版 ―脆弱性攻撃の理論と実際
• 表紙がかっこいい
• 暗号とかも載ってる
•ももテク(ももいろテクノロジー)
• ブログ
• いろんなexploitテクが載っている
• http://inaz2.hatenablog.com/entry/2014/03/14/151011
まとめ
•今回は一番簡単かつ王道な攻撃手法を紹介しました
• シェルコード実行
•実際のCTFではセキュリティ機構(緩和策)があったりそもそもバイナリが大き
かったりと色々な手法を要求されます
•楽しい
•CTFやろうぜ!
•おはり
質問とか
NOPスレッド
•言ってもbufのアドレス表示してるのズルくない?
• 返す言葉もございません
•普通はbufのアドレスは分からないのでROPとかでリークをする(説明は長くなる
のでまたの機会)
•分からなくてもアドレスを適当にやって当ててしまえばいいのだ
• 無理
•一点張りで当てるのは無理でもこの区間に当たればシェル起動する、とかなら
なんだか行ける気がする
• 何もしない命令でその区間を埋めてしまえばいいのでは?
NOPスレッド2
x90x90x90x90
x90x90x90x90
……
……
x90x90x90x90
シェルを起動する
為の機械語列
シェルコードの前にNOP(x90)*大量
を入れる
->どこのNOPにジャンプしてもいいので
アドレスを適当にしても成功率がUP
どこにretしてもOK!
0x????????
NOP:「何もしない」という命令

たのしいPwn 公開用