Easy Reversing
第八回CTF分科会
秋学期になりました。
今学期何するか決めましょう
- (A)春のようになんか僕がしゃべる(しゃべるなら引き続き暗号系)
- (B)去年の秋のように一週間に3人くらいが問題を分担して解説する
- (C)交代で誰かがしゃべる(Web誰か教えて)
- (D)他
今学期中にぼちぼちと出ようかなと思うCTF
HITCON
- https://ctftime.org/event/485
- 11/4 - 11/6
問題がたのしい(例年)。miscとかも充実しているため幅広い人が楽しめるはずなのでやりましょう(去年は囲碁もあった)。
SECCON
- https://ctftime.org/event/512
- 12/9 - 12/10
日本のやつ。去年TWが作問を手伝っていた。そういえばまだAlpha Comlex解いてない。。
今日に関して
今日は、微妙に要望があったため、バイナリ解析のめっちゃ簡単なやつを微妙に僕のわかる範囲でデモります。
バイナリ解析(ELF)の簡単な概要
バイナリ解析に多分必要なもの
- アセンブリを少し読める知識(まぁ知らない命令があればググれば良い)
- 機械語が実行される大まかな流れ(いうても僕もそんなには知らない)
- ある程度人権が保たれるディスアセンブラ(Hopperがおすすめです)(IDA Proがあることに越したことはない)
- 気合
バイナリ解析の大まかな流れ
バイナリを読むと一言に言っても色々な方面から攻めることで、できる限り労力を抑えて読む必要がある。極論を言えば、バイナリをディスアセンブルして静的な解析だけで”"”全てを把握”"”することも可能といえば可能だが、コストが大きい(アセンブリを読むのはそれなりに時間がかかるため)。
よくある工程としては、
1.fileコマンドでどんなバイナリかを知る
およそ以下の内容に注意する
- ELF? PE? (それともMach-O?)
- x86? x64? それ以外?
- アーキテクチャがつらいやつは僕がわからないため今回はx86/x64を考える
- stripped? not stripped?
- なんの言語のバイナリ?
- ここでJavaやC#などの逆コンパイルがしやすい言語などとC/C++系のちゃんと解析する必要があるバイナリとでは、だいぶ様相が異なり、今回は後者を考えている
- Pythonのバイトコードとか、Rubyのバイトコードみたいなものだと工程が以下とはだいぶ異なる
- そもそもよくわからんバイナリは、みんなよくわからんって感じなのでとりあえずググって方針を探る
- したがって以下は典型的なLinuxバイナリでCで書かれたプログラムを考えたい
2.stringsをしてみる
こんなんでフラグはさすがに見えないが、実行時にプロンプトに表示される文字列を見ると雰囲気がわかる
言語によっては大量にメソッド名が表示されたりする
3. とりあえずディスアセンブルしてみる
諦めるか、気合を出すか決める。気合を出す場合次に進む
4. 試し実行
とりあえず実行をしてみる
ここでバイナリがLinuxなら幸せだが、必ずしもそうでない場合があり、そうでない場合はqemuなどを使って実行する必要があるが、僕はそういう問題をスルーするので詳しくない(要するに、LinuxないしWindowsバイナリでない場合は気合を出さないのでダメ)今回はとりあえずLinuxであることを仮定する。
ここで、
- どんな操作をするとどんな結果が出るのか?
- Pwnなら脆弱性を探したりするし、
- Revならどういう形でフラグが取れそうなのかなぁというのを把握する
についてある程度把握する。
5. 以降解析
解析フェーズは、主に
- 動的解析
- 静的解析
の二つに分類される。 動的解析では、実際にgdbのようなデバッガを噛ませながら、特定のマシンコードがどんな風に実行されていくかをステップバイステップで調べる。 静的解析では、ディスアセンブルしたコードを気合で読んでいく。
バイナリを効率よく解析するには、読まなくてよいアセンブリをできるだけ読まないというのが、基本的には良いと思われる。
例えば、なんか明らかに入力文字列を変換する関数があるんだけど、何してるのか長いので読みにくいと思ったとき、gdbでその関数を実行し、結果の部分だけを見ると類推できることがある(base64など)。
以下ではこの部分について簡単な解説を入れる。
アセンブリの簡単な知識
アセンブリはEsolangだとすれば簡単な部類なので多分頑張れば読める
機械語が実行される大まかな流れ
#include <stdio.h>
int great_val() {
return 42;
}
int main(void) {
puts("Hello, %d", great_val());
return 0;
}
このようなプログラムをコンパイルし、
$ ls
fantastic_program
$ ./fantastic_program
Hello
このように実行されます。この間どんな風に動いているのかな、というのを少し知っておく必要がる(まぁ知らなくてもよいが、ある程度知っていると心が落ち着くため)。
とはいっても、OSがしてくれて隠蔽される処理に関しては深追いしません。このプログラムにeipが渡ってきたときにどんな風に動くのかについて簡単に説明する。
エントリポイント
プログラムにはエントリポイントというものがあり、基本的にはここから実行されるという認識で大丈夫なはずである(という気持ちで人生を送ってきた)。実際は共有オブジェクトがロードされたりGOTが再配置されたりする処理などが裏でなされているが基本的にRevをする限りはこの部分に気にすることはそんなにないはず(少なくとも僕はその程度の問題しか解けない)。
エントリポイントがどこか?ということに関しては、ELFのヘッダ部分に書いてある。しかしバイナリエディタでこれを探すのは面倒なので、例えばreadelfというコマンドを使うと見ることができる
$ readelf -h fantastic_program
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x400440
Start of program headers: 64 (bytes into file)
Start of section headers: 4512 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 9
Size of section headers: 64 (bytes)
Number of section headers: 30
Section header string table index: 27
どうやら0x400440というところからスタートするようである。実際にここにブレークポイントを仕掛けて見ていくこともできる。
とりあえず、一旦実行する前に落ち着いて、ディスアセンブラで静的解析をしてみることにする。
静的解析
ここからは実際にディスアセンブラを開いてどういう動作をするのかを一通り見ていくことにする。バイナリ自体はこれ以上ないというほど簡単なものであり、逆にいうとここがベースとなると言える。
さっきのエントリポイントはここである。この_startというのはが何をしているのか?というと、これはlibc_start_mainと呼ばれる、libcの中にある関数を呼び出している。
この_startはコンパイラ依存(コンパイル時に_startとしてリンクされる僕の環境におけるバイナリをリポジトリにアップロードした)だと思うがだいたい同じような構造をしていて、特に覚えておくとよいのは、libc_start_mainの第一引数がmain関数であることである。crt.oをディスアセンブルしてみるとわかる通り、中で_startとして、シンボルが付いているものがまるまる乗っていて、これがコンパイル時にリンクされるため、同じ環境で同じコンパイラでコンパイルしたバイナリには基本的に常に同じ_startが付随しているものだろうと考えられる。
しばしばstrippedなバイナリ(関数名が保持されていないバイナリ)では、どこがmainかわからないことがあるが、ここから類推することができる(もっとも僕がそう思っているだけでもっとよくできるのかもしれない)。
libc_start_main自体は、内部でmain関数にargc, argv, environを与えて実行してくれるようになっている。ここらへんの中身はlibcの実装をネットで漁るといくつか出てくるため読んでみるのも面白いかもしれない。
main関数自体は、今回の場合はめちゃめちゃ簡単で(それはそう)、
ただ、数字を吐き出すだけである。ここで、一つ気にしておくべきなのは、calling conventionである。つまり、関数の引数やretrunがどこに保持されるのか?が主に気になるポイントとなる。
詳しい話は、Wikipedia- 呼出規約や、[GDB] Linux X86-64 の呼出規約(calling Convention)を Gdb で確認するなどを見るとわかるが、よくある例として、x64やx86においてどうなっているのかについて説明する。
まず、x86では基本的に引数はstackに積み上げられ、返り値はeaxレジスタに格納される。
一方、x64では引数は整数と浮動小数点数で扱いが違うようだが、ポインタのような整数値についてはRDI, RSI, RDX, RCX, R8, R9, stackの順に入れられていく
ちなみに、浮動小数点数の場合様子が違うらしく、xmm系のレジスタが使われる(これはそもそも使われる命令セットが異なるためとくっきー氏が言っていました)
またそもそもWindowsとLinuxとではこれらは異なるので結局のところ出会ったバイナリによる部分はあります