happy new year勉強会

97
HappyNewYear勉強会 @potetisensei

Upload: yuki-koike

Post on 18-Jul-2015

1.497 views

Category:

Food


1 download

TRANSCRIPT

HappyNewYear勉強会

@potetisensei

自己紹介

● 小池悠生

● 灘校パソコン研究部部長

● CTF

– 普段はEpsilonDeltaとして

– 一応binjaリーダー

– Exploitationが専門です

– 引退視野

前回の反省

初心者向けではなかった!

前回の反省

● 完全に想定ミス

– 皆さんそもそもアセンブラを読んだことがない?

– 問題解説という段階ではなかった

– 飛ばしていくつもりだったけど流石に暴走だった

● 今日はバイナリを歩きましょう

アセンブラ

● 今日はx86アーキテクチャを使う

– 多分みなさんx86(_64)のパソコンだから

● C言語

– アセンブラを学ぶ上で重要

– 教養として知っておいたほうが恥ずかしくない

アセンブラ

● 読む“disassembler”と書く“assembler”

– 今日教えるのはアセンブラの書き方ではない

– コンパイラが吐き出したアセンブラを読む

● コンパイラのアセンブラにはパターンがある● プログラムが自動で吐き出しているので当たり前● 中でも理解しやすいのがC言語のコンパイラ

アセンブラ

● 覚えなければならないこと

– 命令● そんな全命令覚える必要はない● 慣れればググれます

– メモリ

– レジスタ● 分かって

– ディスアセンブラ/デバッガの使い方● 使って覚えるしか無いでしょう

● 今日伝えたいこと

– ある程度の知識

– 自分で勉強するための方法

– 読む上で何が重要か(※個人の見解)

機械語とアセンブラ

● 機械語

– CPUが実際に解釈、実行する命令列

– 16進数の数(0x00~0xff)の並びで表される

– 人が昔これを“そのまま”書いていたというのは有名

– 人間のすることじゃない● ではどうすれば人間向きなのか?

機械語とアセンブラ

● 命令列が数値なのがいけないのではないか?

– なら命令に名前を振って見やすくしては?

– 一緒に命令の引数も見やすくしよう

– という発想がアセンブラ

– つまりアセンブラは機械語の表示方法とみなせる

– (これは方便なので真に受けない)

アセンブラの登場人物

● メモリ(memory)

– データを記憶する場所

– 実行する機械語列やデータ等全ての情報がここにある

– 後でデバッガでどのようになってるか確認しましょう

アセンブラの登場人物

● レジスタ(register)

– 簡単に言ってしまえばアセンブラにおける変数

– 要はこれも数値を記憶するもの

– メモリと何が違う?● 値の操作などはほとんどこれを経由して行われる● アクセスがメモリよりも速い → 高速化

アセンブラの登場人物

● レジスタ(register)

– eax, ebx, ecx, edx, esp, ebp, edi, esi, eip, ...などがある

– それぞれ用途が違う

– 4byteの数値を記憶できる(0x00000000 ~ 0xffffffff)

– 後で1つずつ確認しましょう

アセンブラの登場人物

● 命令(instruction)

– CPUに解釈、実行される操作(そのまま)

– opcode (operand1), (operand2)

– 例: “ret”, “push eax”など

– 今からこれを読んでいきます

何もしないプログラム

● 初めにdo-nothingというプログラムを読みましょう

– 元となったソースコードはdo-nothing.c

– 自分でコンパイルしてもいいです● 最適化オプションは使用しないでください● 64bitの環境ならgcc -m32オプションを付けること● エラーが出る場合

– apt-get install libc6:i386とかでどうにかなる

user@user:$ objdump -d do-nothing | less

何もしないプログラム

何もしないプログラム

ELF32というファイルフォーマットでi386というアーキテクチャ向けの実行ファイルである

何もしないプログラム

サブルーチン名(関数のようなもの)

何もしないプログラム

機械語列が配置されるメモリアドレス

何もしないプログラム

配置されてる機械語

何もしないプログラム

機械語に対応するアセンブラ表記

何もしないプログラム

● main関数のサブルーチンを見てみましょう

– /<main>:と入力して探しましょう

– 何もしない関数なのにいくつか命令が!

何もしないプログラム

● 分かったこと

– なんとなくのobjdumpの見方– mainしか記述してないのに色々サブルーチンがある

● Cにおいてはmainからプログラムはスタートしますが

– アセンブラでは何からスタートする?

何もしないプログラム

● エントリーポイント?

– 命令の実行が開始されるアドレス

– readelf -h do-nothingで調べてみましょう

何もしないプログラム

何もしないプログラム

● エントリーポイント?

– 命令の実行が開始されるアドレス

– readelf -h do-nothingで調べてみましょう

– 0x80482f0らしい

– objdumpを使って80482f0がどこか調べてみましょう

● /80482f0 で検索出来ます

何もしないプログラム

何もしないプログラム

● エントリーポイント?

– 命令の実行が開始されるアドレス

– readelf -h do-nothingで調べてみましょう

– 0x80482f0らしい

– objdumpを使って80482f0がどこか調べてみましょう

● /80482f0 で検索出来ます

– _startというサブルーチンからバイナリは始まる

何もしないプログラム

● まとめ

– 実行はエントリーポイントと呼ばれる所から始まる

– 関数はそのままサブルーチンになる

– mainの前に色々サブルーチンが実行される● これらはmainを実行するための準備です● 従って見る必要がないのです

メモリに代入するプログラム

● 次にassignmentというプログラムを読みましょう

– 元となったソースコードはassignment.c

– 出来れば自分でコンパイルしないでください● コンパイラによって吐かれる命令が変わり厄介です

– objdump -d assignment | less

– サブルーチンmainを見ましょう● 準備用の他のルーチンは見なくても大丈夫です。

メモリに代入するプログラム

メモリに代入するプログラム

● 今度は全ての命令をしっかり読んでいきます

– Cのソースコードと照らし合わせるとわかりやすいはず

メモリに代入するプログラム

push %ebpmov %esp,%ebpsub $0x10,%esp

後で説明しますが、関数の先頭には必ずついています。 ebpとespにはメモリアドレスが入っている、            ということだけ覚えてください。

メモリに代入するプログラム

movl $0x1,-0x4(%ebp)

movは代入命令です。メモリやレジスタに、 定数やメモリ、レジスタの値をコピーします。先ほど述べたようにebpにはメモリアドレスが入っていて、ここではebp-4が指すメモリに1を代入しています。Cのコードのvar = 1に対応する命令であると分かります。すなわち、ebp-4がvarのメモリアドレスということです。

メモリに代入するプログラム

addl $0x2,-0x4(%ebp)

addは加算命令です。メモリやレジスタに、 定数やメモリ、レジスタの値を加算します。ここではebp-4が指すメモリに2を加算していることから、Cのコードのvar += 2に対応する命令です。

メモリに代入するプログラム

mov -0x4(%ebp),%eax

ebp-4が指すメモリの中身をeaxにコピーします。(%reg)という表記は常に「メモリの中身」を意味します。

メモリに代入するプログラム

imul $0x64,%eax,%eax

mulは乗算命令です。今回の様にoperandが3つある場合には operand3 = operand1(定数) * operand2というように、3つめのoperandが代入先です。operandが1つの場合には、 edx:eax *= operand1(上位4byteはedxへ)を意味します。すなわち、 var *= 100に対応しています。

メモリに代入するプログラム

mov -0x4(%ebp),%ecxmov $0x66666667,%edxmov %ecx,%eaximul %edx

ebp-4が指すメモリの中身をecxに代入、0x66666667をedxに代入、ecxの値をeaxに代入、eax * edxの結果をedx:eaxに代入

メモリに代入するプログラム

mov -0x4(%ebp),%ecxmov $0x66666667,%edxmov %ecx,%eaximul %edx

edx = var * 0x66666667 / 0x100000000;eax = var * 0x66666667 % 0x100000000;

メモリに代入するプログラム

mov -0x4(%ebp),%ecxmov $0x66666667,%edxmov %ecx,%eaximul %edx

edx = var * 2/5; (∵ 0x66666667 ÷ 0x100000000 ≒ 0.4)eax = var * 0x66666667 % 0x100000000;

メモリに代入するプログラム

mov -0x4(%ebp),%ecxmov $0x66666667,%edxmov %ecx,%eaximul %edx

edx = var * 2/5; (∵ 0x66666667 ÷ 0x100000000 ≒ 0.4)eax = var * 3/5; (∵ A%B = A – A/B*B)

メモリに代入するプログラム

mov -0x4(%ebp),%ecxmov $0x66666667,%edxmov %ecx,%eaximul %edx

よって、これらはvarの2/5と3/5を求める命令列です。

メモリに代入するプログラム

sar %edx

sar, shrは右シフト命令。operandが1つの場合には、 operand1 >>= 1; (⇔ operand1 /= 2;operandが2つの場合には、 operand2 >>= operand1;を表します。sal, shlは左シフト命令。つまり、edx = var * 2/5だから、edx /= 2すると、edxにはvar * 1/5が代入されるのことになります。

メモリに代入するプログラム

mov %ecx,%eaxsar $0x1f,%eaxsub %eax,%edx

subは減算命令です。ecxにはvarの値が入っていたから、edx -= var >> 31をしていることになります。varは4byte=32bitの変数ですから、31回右シフトをすると最上位の1bitの値を求めることになります。これはvarがsigned intであるため、varが負数の場合にはedxから1を引かなくてはなりません。(cf. 2の補数表現)

メモリに代入するプログラム

mov %edx,%eaxmov %eax,-0x4(%ebp)

edxの値、var * 1/5をvarに代入します。よって、これらの命令群でvar /= 5が行われています。今回は乗算によって除算が行われましたが、divという除算命令が別にあります。

メモリに代入するプログラム

● デバッガで確認しましょう

– 命令を解説するより、実際に動かした方が早い

– 動かしつつレジスタやメモリの状態を見れる

– Linuxにはgdbと呼ばれるGNUのデバッガがあります

user@user:$ gdb ./assignment -q

メモリに代入するプログラム

メモリに代入するプログラム

● デバッガの基本

– ブレークポイントを仕掛けてプログラムを中断

– ステップ実行で逐次実行

– コマンドでレジスタやメモリの値を確認、変更

メモリに代入するプログラム

● break mainでmain関数の先頭にブレークポイント

● runで実行

– 当然main関数の先頭で止まる

● コマンドは省略可

– b main, rなど

メモリに代入するプログラム

● disasなんかも出来る

メモリに代入するプログラム

● imul %edxの後のレジスタを確認したいなら

– b *0x08048413でアドレスをブレーク

– continue(c)でブレークするまで実行

– ブレークしたらinfo registers

メモリに代入するプログラム

● eaxとedxだけ確認したいみたいな時

– pコマンドで値を表示

● 本来はp 1+1とか電卓的な役割– $regでレジスタを参照

– p $eaxで10進法表記でeaxを表示

– p/x $edxで16進法表記でedxを表示

メモリに代入するプログラム

● sar %edx後のedxの値を確認

– ちょうどそこでブレークしているからステップ実行

– stepi(si)で1行だけ実行できる

メモリに代入するプログラム

● 最終的なvarの値を確認したい

– b *0x08048421で最後の代入の後でブレーク

– cでブレークするまで実行

– xコマンドでメモリの中身が確認できます

メモリに代入するプログラム

● xコマンドのフォーマット

– x/(format)で指定した書式で値を表示

– x/dwでword(4byte)サイズで10進数表記

– x/xgでgiga word(8byte)サイズで16進数表記

– サイズはb, h, w, g、表記はd, u, o, x, c, sなど

メモリに代入するプログラム

● まとめ

– mov命令で代入できる

– offset(%reg)は%reg+offsetが指すメモリの中身

– 四則演算はadd, sub, mul, divでできる● 主にこれらにはeaxとedxが利用されます。● eax ・・・ extended accumulator register ● edx ・・・ extended data register

関数を呼び出すプログラム

● 次にcall-funcというプログラムを読みましょう

– objdump -d call-func してください

– mainのサブルーチンを見てみましょう

関数を呼び出すプログラム

● call <foo>がfoo関数を呼んでいそう?

– 関数の呼び出しを見る時はスタックを意識します

今日のテーマ(最重要)

スタックと関数

スタック

● データ構造の一種

– Push: 新しいデータを積むこと

– Pop: 一番上のデータを取り出す

スタック

● コンパイラはCのコードをアセンブラで表現する

– コンパイラを作る人の気持ちになってください

– アセンブリ命令作る人の気持ちにもなるとなお良し

● Cの関数を表現する上で必要なことはなにか?

– ローカル変数用の領域は呼び出しごとに取る必要あり

– 引数も与えれないと困る

– 関数を呼び出した位置にreturnで戻れる必要がある

スタック

● それってスタックが使えるのでは?

– 関数を呼び出す時に引数と呼び出した位置をpush

– 関数からreturnする際にpopしてその位置に戻る

– 呼び出しの入れ子や再帰にも対応できる

コードの10行目から3行目の関数を呼ぶ時、スタックには引数、11をpush、関数内では引数のみpop、return時に位置をpopして11行目に戻るBasicのgotoみたいな。

イメージ:

スタック

● でもどうやってプログラム上でスタックを表現する?

– メモリならいくらでもある

– 上底と下底の位置で擬似的に表現できるんじゃ?

スタック

11行目

11行目

15行目

3行目

20行目

10行目

上底

下底

メモリ

スタック

● でもどうやってプログラム上でスタックを表現する?

– メモリならいくらでもある

– 上底と下底の位置で擬似的に表現できるんじゃ?

– それらの位置はレジスタに記憶させればいい● → espとebp

– esp ・・・ extended stack pointer(上底)– ebp ・・・ extended base pointer(下底)

スタック

● つまり

– 関数を呼び出す時には戻る位置をpushする● espが指すアドレスに位置を入れてespの値をdec

– 関数からreturnする時はpopして位置を変える● espが指すアドレスから値を取ってespの値をinc

スタック

● ついでにローカル変数用のメモリ領域を取りたい

– 呼び出しごとに必要ならスタック上に取るのが自然

– 関数を呼び出した時にスタックの中に取っては?● 少し余分にespをdecすればいいこと

– これで全ての問題をクリア出来た!

イメージ図

関数fooへの引数

戻りアドレス

fooのローカル変数

関数putsへの引数

戻りアドレス

関数を呼び出すプログラム

● これを踏まえてfoo関数の呼び出し部分を見てみます

movl $0x80484e0,(%esp)call 804841d <foo>

関数を呼び出すプログラム

● これを踏まえてfoo関数の呼び出し部分を見てみます

movl $0x80484e0,(%esp)call 804841d <foo>

引数(char*=アドレス)をスタックにpush

関数を呼び出すプログラム

● これを踏まえてfoo関数の呼び出し部分を見てみます

movl $0x80484e0,(%esp)call 804841d <foo>

戻るためのアドレスをスタックにpushして関数fooにjmpする

関数を呼び出すプログラム

8048439: movl $0x80484e0,(%esp)8048440: call 804841d <foo>

esp

関数を呼び出すプログラム

8048439: movl $0x80484e0,(%esp)8048440: call 804841d <foo>

0x80484e0 esp

関数を呼び出すプログラム

8048439: movl $0x80484e0,(%esp)8048440: call 804841d <foo>

0x80484e0

0x8048445 esp

関数を呼び出すプログラム

● movl $0x80484e0,(%esp)はpushではないのでは?

– 確かにmovだけではpushにならない● 「push A」 = 「sub $0x4, %esp; mov A, (%esp)」

– ポイントは、関数呼び出しの前のsub $0x10,%esp● 「sub $0xC,%esp; push A」と● 「sub $0x10,%esp; mov A,(%esp)」は同値● Linuxのバイナリでは関数の頭で予め引くことが多い

関数を呼び出すプログラム

● fooの引数参照

– 戻りアドレスと引数をpushしてfooが呼び出された

– 引数の参照を何で行う?● イチイチpopするのは非効率● メモリとして参照したほうが賢い● アドレスをregisterで記憶する必要がある● registerは数が限られている● ebpを上手く使えないか?● 戻りアドレスあたりを記憶しておくと効率良さそう

イメージ図

関数fooへの引数

fooのローカル変数

関数barへの引数

戻りアドレス

イメージ図

関数fooへの引数

fooのローカル変数

関数barへの引数

戻りアドレス

esp

イメージ図

関数fooへの引数

fooのローカル変数

関数barへの引数

戻りアドレス

esp

ebp

関数を呼び出すプログラム

● fooの引数参照

– これでebp+offsetで引数のアドレスが求められそう

– でも複数回関数が呼ばれるとマズイ● ebpの値を呼び出すごとに変える必要あり● return時には元の関数に合う様戻さないといけない

– ebpの値もstackに記憶してはどうか?● 呼び出し時に戻りアドレスと一緒にpush● 新しく呼ばれた関数に値を合わせる● return時には戻りアドレスと一緒にpop

イメージ図

関数fooへの引数

戻りアドレス

fooのローカル変数

関数putsへの引数

戻りアドレス

元のebpの値

元のebpの値

関数を呼び出すプログラム

● 実際の呼び出し過程

– 引数をpush

– callにより戻りアドレスをpushし呼び出し先へjmp

– 元のebpの値をpush

– ebpをespの値に設定

– espから必要な分値を引く

関数を呼び出すプログラム

● 実際のreturn過程

– espをebpの値に設定

– ebpへ元のebpの値をpop

– 戻りアドレスへjmp(ret命令)

– 必要であれば引数をpop

関数を呼び出すプログラム

● デバッガで確認しましょう

– gdb ./call-func -q

– b fooでfoo関数を設定

– rで実行

– スタックの中身を表示してみてください

● x/xw $espでしたね

● x/8xw $espのように複数表示することも出来ます

関数を呼び出すプログラム

● ひと目でローカル変数の領域がどこか分かりますか?

– 冒頭のsub $0x18,%espから24byteだと分かる

– つまり下絵の初め6つがローカル変数用の領域

– 残り2つはそれぞれ前のebpとreturn address

– 引数はreturn addressの下だからebp+8だと分かる

関数を呼び出すプログラム

● fooの引数はなんでしょう?

– ソースコードから、char*であることは分かる

– gdbで確認してみましょう

– 引数はebp+8なのでx/xw $ebp+8

– 表示された値はchar*だからこれもアドレス

– x/s 0x080484e0で文字列として表示できます

● x/s *(char**)($ebp+8)でもOK

メモリに代入するプログラム

● まとめ

– 関数の呼び出しは一種のスタック操作になっている

– ebpで引数やローカル変数を参照して効率化している

– スタックのイメージがexploitationでは重要になる

演習

● problemというファイルを読んでください。

– /dev/nullに書き込んでいる内容を答えとします

– 少し難しいかもしれません● 今までの内容をよく思い出しましょう

演習

● Hint1

– main関数がない!– 難読化(?)っぽいのがかかってる

– stripと言い、gccの標準機能です● サブルーチン名は人間用の情報なので削除可能

– libc_start_mainの第1引数はmainのアドレスです

演習

● Hint2

– 呼ばれる標準関数はpltセクションにかいてあります

– objdumpで「セクション .plt の逆アセンブル:」を確認

– ファイルに書き込みを行う関数はどれ?

● Hint2

– 呼ばれる標準関数はpltセクションにかいてあります

– objdumpで「セクション .plt の逆アセンブル:」を確認

– ファイルに書き込みを行う関数はどれ?

演習

● Hint3

– fwriteの引数はなんでしょうか?

– 第1引数がファイルへ書き込むデータです

– どうやら

lea 0x20(%esp),%eax

mov %eax,(%esp)

のeaxが第1引数らしい– lea命令とは一体?

演習

● Hint4

– lea命令はメモリのアドレスの代入です

● lea 0x20(%esp),%eaxならeax = esp+0x20

– esp+0x20とは一体...● 実は最適化でローカル変数をespで参照しています● なのでesp+0x20はただのローカル変数● この中の値は後はデバッガか何かで調べられる

演習

● 答え

– “THIS_IS_FLAGGGG\x00”

演習

● まとめ

– objdumpによる逆アセンブルを学んだ

– 簡単なデバッガの使い方を学んだ

自分で勉強するには

● とにかくバイナリを読みましょう

– 僕はCTFをしまくった

– 自分でコンパイルしたプログラムなどがオススメ● 分からなくなったらソースコードを見ればいい● forやwhileはどうなるの?ifはどうなる?switchは?

– 本を買うのもいいかもしれません● 楽しいバイナリの歩き方● x86アセンブラ入門

– ググることも忘れないで下さい

意識すると良いこと

● C言語ではどのように対応するのか

– ほとんどのバイナリはC, C++からコンパイルされる

– つまりバイナリをCのコードで表すことが出来るはず

– 読みながらCのコードに直していくのもアリ

● スタックの状況

– これはexploit、あるいは一部の難読化に関して

– Return Oriented Programmingなどではこれが不可欠

御静聴ありがとうございました