happy new year勉強会
TRANSCRIPT
前回の反省
● 完全に想定ミス
– 皆さんそもそもアセンブラを読んだことがない?
– 問題解説という段階ではなかった
– 飛ばしていくつもりだったけど流石に暴走だった
● 今日はバイナリを歩きましょう
アセンブラ
● 読む“disassembler”と書く“assembler”
– 今日教えるのはアセンブラの書き方ではない
– コンパイラが吐き出したアセンブラを読む
● コンパイラのアセンブラにはパターンがある● プログラムが自動で吐き出しているので当たり前● 中でも理解しやすいのがC言語のコンパイラ
アセンブラ
● 覚えなければならないこと
– 命令● そんな全命令覚える必要はない● 慣れればググれます
– メモリ
– レジスタ● 分かって
– ディスアセンブラ/デバッガの使い方● 使って覚えるしか無いでしょう
機械語とアセンブラ
● 機械語
– CPUが実際に解釈、実行する命令列
– 16進数の数(0x00~0xff)の並びで表される
– 人が昔これを“そのまま”書いていたというのは有名
– 人間のすることじゃない● ではどうすれば人間向きなのか?
機械語とアセンブラ
● 命令列が数値なのがいけないのではないか?
– なら命令に名前を振って見やすくしては?
– 一緒に命令の引数も見やすくしよう
– という発想がアセンブラ
– つまりアセンブラは機械語の表示方法とみなせる
– (これは方便なので真に受けない)
アセンブラの登場人物
● レジスタ(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
何もしないプログラム
● 分かったこと
– なんとなくのobjdumpの見方– mainしか記述してないのに色々サブルーチンがある
● Cにおいてはmainからプログラムはスタートしますが
– アセンブラでは何からスタートする?
何もしないプログラム
● エントリーポイント?
– 命令の実行が開始されるアドレス
– 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を見ましょう● 準備用の他のルーチンは見なくても大丈夫です。
メモリに代入するプログラム
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に対応する命令です。
メモリに代入するプログラム
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
メモリに代入するプログラム
● imul %edxの後のレジスタを確認したいなら
– b *0x08048413でアドレスをブレーク
– continue(c)でブレークするまで実行
– ブレークしたらinfo registers
メモリに代入するプログラム
● eaxとedxだけ確認したいみたいな時
– pコマンドで値を表示
● 本来はp 1+1とか電卓的な役割– $regでレジスタを参照
– p $eaxで10進法表記でeaxを表示
– p/x $edxで16進法表記でedxを表示
メモリに代入するプログラム
● 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
スタック
● コンパイラはCのコードをアセンブラで表現する
– コンパイラを作る人の気持ちになってください
– アセンブリ命令作る人の気持ちにもなるとなお良し
● Cの関数を表現する上で必要なことはなにか?
– ローカル変数用の領域は呼び出しごとに取る必要あり
– 引数も与えれないと困る
– 関数を呼び出した位置にreturnで戻れる必要がある
スタック
● それってスタックが使えるのでは?
– 関数を呼び出す時に引数と呼び出した位置をpush
– 関数からreturnする際にpopしてその位置に戻る
– 呼び出しの入れ子や再帰にも対応できる
コードの10行目から3行目の関数を呼ぶ時、スタックには引数、11をpush、関数内では引数のみpop、return時に位置をpopして11行目に戻るBasicのgotoみたいな。
イメージ:
スタック
● でもどうやってプログラム上でスタックを表現する?
– メモリならいくらでもある
– 上底と下底の位置で擬似的に表現できるんじゃ?
– それらの位置はレジスタに記憶させればいい● → espとebp
– esp ・・・ extended stack pointer(上底)– ebp ・・・ extended base pointer(下底)
スタック
● つまり
– 関数を呼び出す時には戻る位置をpushする● espが指すアドレスに位置を入れてespの値をdec
– 関数からreturnする時はpopして位置を変える● espが指すアドレスから値を取ってespの値をinc
スタック
● ついでにローカル変数用のメモリ領域を取りたい
– 呼び出しごとに必要ならスタック上に取るのが自然
– 関数を呼び出した時にスタックの中に取っては?● 少し余分にespをdecすればいいこと
– これで全ての問題をクリア出来た!
関数を呼び出すプログラム
● これを踏まえてfoo関数の呼び出し部分を見てみます
movl $0x80484e0,(%esp)call 804841d <foo>
引数(char*=アドレス)をスタックにpush
関数を呼び出すプログラム
● これを踏まえてfoo関数の呼び出し部分を見てみます
movl $0x80484e0,(%esp)call 804841d <foo>
戻るためのアドレスをスタックにpushして関数fooにjmpする
関数を呼び出すプログラム
● 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の引数参照
– これでebp+offsetで引数のアドレスが求められそう
– でも複数回関数が呼ばれるとマズイ● ebpの値を呼び出すごとに変える必要あり● return時には元の関数に合う様戻さないといけない
– ebpの値もstackに記憶してはどうか?● 呼び出し時に戻りアドレスと一緒にpush● 新しく呼ばれた関数に値を合わせる● return時には戻りアドレスと一緒にpop
関数を呼び出すプログラム
● 実際の呼び出し過程
– 引数をpush
– callにより戻りアドレスをpushし呼び出し先へjmp
– 元のebpの値をpush
– ebpをespの値に設定
– espから必要な分値を引く
関数を呼び出すプログラム
● デバッガで確認しましょう
– 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では重要になる
演習
● 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はただのローカル変数● この中の値は後はデバッガか何かで調べられる
自分で勉強するには
● とにかくバイナリを読みましょう
– 僕はCTFをしまくった
– 自分でコンパイルしたプログラムなどがオススメ● 分からなくなったらソースコードを見ればいい● forやwhileはどうなるの?ifはどうなる?switchは?
– 本を買うのもいいかもしれません● 楽しいバイナリの歩き方● x86アセンブラ入門
– ググることも忘れないで下さい
意識すると良いこと
● C言語ではどのように対応するのか
– ほとんどのバイナリはC, C++からコンパイルされる
– つまりバイナリをCのコードで表すことが出来るはず
– 読みながらCのコードに直していくのもアリ
● スタックの状況
– これはexploit、あるいは一部の難読化に関して
– Return Oriented Programmingなどではこれが不可欠