「たのしいバイナリの歩き方」 第3章 勉強メモ
「たのしいバイナリの歩き方」の勉強メモ第3弾です。
[VMWare Player]
http://www.vmware.com/products/player/
[FreeBSD-8.3]
http://07c00.com/tmp/FreeBSD_8.3_binbook.zip
[Ubuntu-12.04]
http://07c00.com/tmp/Ubuntu-12.04_binbook.zip
3.1 バッファオーバーフローを利用して任意のコードが実行される仕組み
最も有名なセキュリティホールの一つが、バッファーオーバーフロー。
バッファーオーバーフローとは、「プログラマが想定したメモリ領域を超えてデータが入力された場合に、データがあふれ、プログラムが暴走してしまうこと」。
setuidとは、実行ユーザーではなく、そのプログラムの所有者の権限でプログラムを動作させる仕組み。
アクセス権の「s」という文字が、setuidが有効になっているプログラムであることを意味する。
以下は表示例
# ls -l /usr/bin/passwd -r-sr-xr-x 2 root wheel 6360 Apr 10 2012 /usr/bin/passwd
スタック
「筒や積み上げられる皿のような使われ方をするメモリ空間」
メモリアドレスの低位(減算方向)に向かって成長する。
情報工学的には、スタックをLIFO(Last In, First Out)、
キューのような最初に push したものを最初に pop するものを FIFO((First In, First Out)と呼ぶ。
gccコマンドで-Sオプションをつけてコンパイルすると、アセンブラにしたものが生成される。
C言語において引数に渡しているデータが、アセンブラではfuncがcallされる前にスタックへ格納される。
スタックに引数を格納し、callでサブルーチンを呼び出す。
callはjmpと違い、自分が呼び出されたアドレスを覚えておかなければならないため、サブルーチンへジャンプする前に、戻るべきリターンアドレスをスタックへpushする。
リターンアドレスは、処理が終了したあと、mainへ戻るためのアドレスが格納されている。
これが上書きされると、攻撃者はあらゆる場所へ処理をジャンプさせることができる。
攻撃者が用意したコードに飛べるとしたら、それは「任意のコードが実行できる脆弱性」となる。
gdb
UNIX系OSにおける有用なデバッガ(コマンドラインツール)。
主要なコマンド
コマンド名 | 説明 |
---|---|
r | プログラムを実行する(そのまま引数も渡せる) |
b | ブレイクポイントをセットする(アスタリスクをつけて、アドレスを渡す) |
c | ブレイクポイントで止まったあと、そのまま処理を実行させる |
x/[数字]i | 任意の命令数だけ、逆アセンブルする |
disas | 同上 |
x/[数字]x | 任意の数だけ、データを表示する ※引数には、アドレスもレジスタも渡せる ※レジスタを渡したい場合は、$をつける |
x/[数字]s | 任意の数だけ、文字列として表示する |
i r | レジスタの値を表示する |
set | レジスタやメモリへの値を書き込む |
q | デバッグを終了する |
gdbでプログラムをデバッグする方法には、以下の2つがある。
プログラムのパスをgdbの引数に渡す
$ gdb test00
gdbからプロセスをアタッチする
(gdb) attach 1111 Attaching to process 1111
攻撃者が実行したいコードのことを、shellcodeと呼ぶ。
基本的に、/bin/sh が起動できれば何でもできるため、これを起動するための最小のマシン語のことを指したりもする。
gccコマンドで-staticオプションをつけてコンパイルすると、execveなどの関数本体も実行ファイルに含まれる。gdbでexecveを逆アセンブルすると、int $0x80が呼び出されていることがわかる。
int $0x80は、システムコール呼び出し。
0x3bはexecveシステムコール番号で、カーネル側でそれぞれのシステムコールを識別するするために使われる。
/usr/include/sys/syscall.hでシステムコールの番号を確認することができる。
Linuxの場合は、/usr/src/linux/include/asm/unistd.hにシステムコール番号が定義されていて、
execveは11である。
システムコール番号は環境によって異なるため、shellcodeを作成する際には注意が必要。
さらにいえばshellcodeはOSによって異なるため、環境に応じて作る必要がある。
shellcodeに0x00が使われていると、strcpyが0x00を終端として認識するため、shellcode全体をプログラムにコピーできない。
これを解決する方法はいくつかあるが、一般的なものは以下の通り。
・/bin/sh を、/bin//sh という8バイトの文字列にする。 コマンドを実行するにあたって、スラッシュは複数あっても問題ない。 ・その前に、push $0をしておく。 終端文字0x00を、xorとpushを併用してスタックに積んでおく。
通常は、shellcode がターゲットプロセスのどのアドレスにあるかわからないので、推測しなければならない。メモリを可能な限りNOP(0x90)で埋めて、最後にshellcodeを置き、shellcodeが実行される確率(成功率)を上げることで対応する。
近年はデフォルトでさまざまなセキュリティ機能がオンになっており、このような典型的なバッファオーバーフローはブロックされるようになっている。
コラム:pritf系関数に起因するフォーマットストリングバグ
以下のコードは、printfの引数に好きなデータ列を入れられるなら、任意のコードが実行できてしまう。
#include <stdio.h> void main(int argc, char *argv[]) { printf(agv[1]);
printf系関数には、%nという、引数に渡されたポインタへデータサイズを書き込む変換指定文字がある。これを利用することで、任意のアドレスへ任意の値を書き込める。
3.2 攻撃を防ぐ技術
ASLR(Address Space Layout Randomization)
スタックや各モジュール、動的に確保したメモリなどのアドレス(配置先)をランダムに決定する仕組み。
Ubuntu-12.04では/proc/sys/kernel/randomize_va_spaceで設定を確認/変更できる。
0:無効 1:ヒープ以外をランダム化 2:すべてランダム化(デフォルト)
Exec-Shield
メモリ領域の読み書き実行権限を制限する仕組み。
スタックとして使用されているメモリ領域に実行すべきマシン語が置かれることは、通常ありえないので、スタック領域は、読み書きのみを許可して、実行を不可にするのが一般的。
コードセンクションにはマシン語が置かれているが、そのマシン語を書き換える必要は一般的なソフトウェアではまずないので、そのメモリ領域は書き込み不可にする。
仮にスタックにshellcodeをコピーできても、それが実行できなければ、Segmentation faultでプログラムは終了する。
任意のプログラム内のメモリ領域の読み書き実行権限を確認するには、プログラムを実行した状態で、/proc/<PID>/mapsを出力する。
StackGuard
「コンパイル時に、各関数の入口と出口にスタックが破壊されたことを検知するマシン語を挿入する」という手法(コンパイラの機能)。
要するに、ebpやリターンアドレスを守るための仕組みであり、典型的なスタックバッファオーバーフローに対しての防御手法なわけである。
StackGuardを無効にするには、コンパイル時に-fno-stack-protectorオプションを付ける。
3.3 セキュリティ機能を迂回する技術
Return-into-libc
Exec-Shieldの攻略法として考えられた。
基本的な考え方
「任意のコード(shellcode)を実行できなくとも、 最終的に任意のプログラムを実行できれば権限を奪える」 ↓ 「うまく引数を設定し、スタックを調整して、libc.soの中にあるsystem関数や exec系関数へジャンプさせれば、/bin/shなどのプログラムを実行できる」
libc.soは、ほとんどのプログラムで実行時にロードされるか、あるいはコンパイル時に静的にリンクされているため、libcの中にあるsystem関数やexec系関数をうまいこと呼び出せれば、権限を奪えてしまう。
ASLRによってロードされるモジュール群が実行ごとにランダム化されている場合、systemやexecのアドレスがわからないため、攻撃は失敗する。
ROP(Return-Oriented-Programming)
観点
「ランダム化されていないモジュール内にあるアセンブラコードを使って、 うまく実行させたい処理をつなぎ合わせられないだろうか?」
コラム:セキュリティがいたちごっこになる理由
セキュリティにおいては、必ず明確な「敵」が存在する。
サーバを攻撃する人たちや、マルウェアを作ってお金儲けをする人たちがその「敵」である。
敵が新しい攻撃手法を考えたら、こちらも新しい防御手法を考えなければならないし、
敵が新しいシステムをねらったら、こちらも攻撃を防ぐ方法を考案しなければならない。
米国や韓国だと「サイバー戦争」という言葉が一般的で、軍事や国防とセットで語られたりする。
「たのしいバイナリの歩き方」 第2章 勉強メモ
「たのしいバイナリの歩き方」の勉強メモ第2弾です。
2.1 メモリダンプを読み解く
汎用プロセスメモリエディタの紹介。
[うさみみハリケーン]
http://hp.vector.co.jp/authors/VA028184/#TOOL
http://www.vector.co.jp/soft/win95/prog/se375830.html
Windows Vista以降の場合のメモリダンプの取得方法
- [Ctrl]+[Alt]+[Del]でタスクマネージャを起動
- 該当するプロセスで右クリック
- 「ダンプファイルを作成」を選択
すると、C:\Users\ユーザー名\AppData\Local\Temp\***.DMP が作成される。
Windows XPまでの場合のメモリダンプの取得方法
プロセスが異常終了した時にワトソン博士というメモリダンプツールが起動する。
Just-In-Timeデバッグ
プロセスが継続不能なエラーによって終了した場合に、自動的にデバッガが立ち上がり、そのプロセスへアタッチするよう設定できる。
OllyDbgでもこの設定ができる。メニューからオプション→シャストインタイムデバッグを選択すると設定用のウィンドウが表示され、そこで「ジャスト・イン・タイム デバッガ 新規設定」をクリックすると、OllyDbgがJust-In-Timeデバッガとして設定される。
guitest.exeでメニューのヘルプからバージョン情報を開き、 そのウィンドウを閉じて異常終了させても、OllyDbgが起動しなかった。 設定では次のような画面になったが、何か問題があるのだろうか?
ダンプファイルはWinDbgで解析できる。
[Ctrl]+[D]:ダンプファイルを開く [Alt]+[6]:Call Stackウィンドウの表示 [Alt]+[7]:Disassenblyウィンドウの表示 [Alt]+[4]:Registersウィンドウの表示 [Alt]+[5]:Memoryウィンドウの表示
2.2 動作を解析されないようにするには
デバッガ検知のAPI関数の例
- IsDebuggerPresent
- CheckRemoteDebuggerPresent
コラム:デバッグを検知するさまざまな手法
- popfとSINGLE_STEP例外を利用したデバッガ検知手法
- int 2dhを使った検知手法
デバッグ対策は効果的であるが、逆アセンブラで静的に解析され、検知処理を走らせている箇所を特定されてしまうと、かんたんに潰される。
コードを解析されないようにするための手段が「難読化」である。
コラム:難読化に関する話題
難読化に関する有益な論文の紹介。
Obfuscation of executable code to improve resistance to static disassembly.
http://www.cs.arizona.edu/solar/papers/CCS2003.pdf
Binary Obfuscation Using Signals.
https://www.cs.arizona.edu/solar/papers/obf-signal.pdf
アンチデバッギング、難読化のほかにも、実行ファイルを実行できる状態のまま圧縮することで解析を防ぐことができる。
そのためのソフトをパッカーと呼ぶ。パッカーには次のようなものがある。
[UPX]
http://upx.sourceforge.net/
実行ファイル内のコードやデータを圧縮し、それを展開するコードを先頭に付加して、あらたな実行ファイルとして出力する。
[ASPack]
http://www.aspack.com/
アンチデバッギングを目的としたパッカー。P2Pファイル交換ソフトWinnyで使用されていた。
パッカーで圧縮された実行ファイルを展開するツールをアンパッカーと呼ぶ。
- UPXの場合は、-dオプションで実行すれば展開できる。
- UPX以外の場合は、公式にはアンパッカーが存在しない。
UPXを手動でアンパックして仕組みを理解するために、
OllyDumpというOllyDbg用のプラグインを使用する。
[OllyDump]
http://www.openrce.org/downloads/details/108/OllyDump
CALL packed.XXXXXXXXが完了してから、
OllyDbgメニューからプラグイン→OllyDump→Dump debugged processを選択、
ファイルの保存すると、アンパックしたファイルになっている。
最初にあったpushadと最後にあったpopad、これらに挟まれた処理が展開ルーチンであった。
ほとんどのパッカーはこのような仕組みを採用しており、必ずどこかのタイミングで展開ルーチンが終わり、本処理へ進む。
手動アンパックとは「展開ルーチンが終わる瞬間(箇所)を見つける作業」といえるかもしれない。
ソフトウェアブレイクポイント
デバッガが該当アドレスの命令を0xCC(int3h)に書きかえる。
プロセッサは、「0xCCという命令を見つけたら、OSをとおしてデバッガに例外を通知する」という仕組みになっている。
ユーザーの気のすむまでセットできる。
ハードウェアブレイクポイント
0xCCではなく、止めてほしい場所(アドレス)を、直接レジスタ(DRレジスタ)に書き込む。
「このアドレスに書き込みが起こったら停止させる」
「このアドレスに読み込みが起こったら停止させる」
ソフトウェアブレイクポイントよりも高性能。
4つしかセットできない(プロセッサに4つ分しか実装されていない)。
OllyDbgのショートカット。
[Ctrl]+[G]:指定アドレスにジャンプ [Ctrl]+[A]:コード解析
コラム:.NET製アプリを解析するには
逆コンパイラの紹介
.NET Reflector 8
http://www.red-gate.com/products/dotnet-development/reflector/
Javaや.NET製アプリケーションのリバースエンジニアリングは、デバッガや逆アセンブラを使って少しずつ解析していくのではなく、「どこまで本来のソースコードに近い形に戻せるか」という点が重要。
「たのしいバイナリの歩き方」 第1章 勉強メモ
CTFを勉強する上でおすすめの本だったので、「たのしいバイナリの歩き方」を購入し、勉強を始めました。
自分の理解を深めるためにも勉強メモとして記録しておこうと思います。
1.1 まずは解析の流れを体感してみよう
次の3点に注目!
- ファイルの作成/変更/削除を行う
- レジストリキーの作成/変更/削除を行う
- ネットワーク通信を行う
監視するためのツールの紹介。
[Stirling](バイナリエディタ)
http://www.vector.co.jp/soft/win95/util/se079072.html
[Process Monitor](ファイルとレジストリの監視)
http://technet.microsoft.com/en-us/sysinternals/bb896645
[Wireshark](ネットワーク監視)
http://www.wireshark.org/
sample_mal.exeで0.exeと1.exeはコピーされていましたが、 レジストリへの書き込みは確認できませんでした。 UACが関係しているのか???
コラム:リバースエンジニアリングの技術力を競う大会
1.2 静的解析をやってみよう
バイナリエディタの紹介。
[Stirling]
http://www.vector.co.jp/soft/win95/util/se079072.html
[BZエディタ]
http://www.vector.co.jp/soft/win95/util/se032859.html
コラム:StirlingとBZエディタの違い
- Stirling:高機能。さまざまな用途に使える。数GBを超えるファイルは扱えない。
- BZエディタ:どんな巨大なファイルでも開くことができる。
[IDA 6.2 Demo version, IDA 5.0 Freeware version]
http://www.hex-rays.com/products/ida/support/download.shtml
wsample01a.exeをIDAで開いても、 wWinMainという関数名が見つかりませんでした。
1.3 動的解析をやってみよう
デバッガの紹介。
[OllyDbg]
http://www.ollydbg.de/
OllyDbgのショートカット。
[F2]:ブレークポイントのセット [F7]:ステップイン [F8]:ステップアウト [F9]:実行
コラム:レジスタとは
- レジスタ:CPUが内部に持つ記憶領域。EAX、ECX、EDX、EBX、ESP、EBP、ESI、EDI、EIPといった名前がついている。
- ESP/EBP:スタックの管理に使用される。
- EIP:現在実行中の命令を指す。
- フラグ:EIPの下にあるC、P、A、Z、S、T、D、Oという文字。
最初にStirlingやIDAで全体をざっと眺めたあとに、
気になったポイントをOllyDbgでステップ実行していく、
という流れで解析することが多い。
コラム:好みのデバッガを選ぼう
[OllyDbg]
http://www.ollydbg.de/
[Immunity Debugger]
http://www.immunityinc.com/products/debugger/
[WinDbg(32ビット版)]
http://msdn.microsoft.com/ja-jp/windows/hardware/gg463016
[WinDbg(64ビット版)]
http://msdn.microsoft.com/ja-jp/windows/hardware/gg463012
OllyDbgが特に人気。
Immunity Debuggerも同じUIで使え、Pythonと親和性が高い。
WinDbgは初心者向けではないが、唯一カーネルランドで動作するプログラムのデバッギングに対応している。
1.4 最低限のアセンブラ命令だけざっくり把握する
実際に覚えなければいけない命令は20~50ほど。
よく使われるアセンブラ命令
命令 | 例 | 意味 | 説明 |
---|---|---|---|
MOV | MOV EAX,ECX | EAX = ECX | ECXの値をEAXへ格納 |
ADD | ADD EAX,ECX | EAX += ECX | EAXにECXを加算 |
SUB | SUB EAX,ECX | EAX -= ECX | EAXからECXを減算 |
INC | INC EAX | EAX++ | EAXに1を加算 |
DEC | DEC EAX | EAX-- | EAXから1を減算 |
LEA | LEA EAX,[ECX+4] | EAX = ECX+4 | ECX+4をEAXへ格納 |
CMP | CMP EAX, ECX | if(EAX == ECX) ZF=1 else ZF=0 |
値と比較してフラグへ反映 EAXとECXが同じならばZF=1 EAXとECXが違うならばZF=0 |
TEST | TEST EAX,EAX | if(EAX == 0) ZF=1 else ZF=0 |
値を0と比較してフラグへ反映 EAXが0ならばZF=1 EAXが0以外ならばZF=0 |
JE(JZ) | JE 04001000 | if(ZF==1) GOTO 04001000 |
ZFが1なら04001000へジャンプ |
JNE(JNZ) | JNE 04001000 | if(ZF==0) GOTO 04001000 |
ZFが0なら04001000へジャンプ |
JMP | JMP 04001000 | GOTO 04001000 | 無条件で04001000へジャンプ |
CALL | CALL lstrcmpW | lstrcmpWの呼び出し | |
PUSH | PUSH 00000001 | スタックへ00000001を格納 | |
POP | POP EAX | スタックからEAXへ値を格納 |
test命令は、ほとんどの場合において、test eax,eax や test ecx,ecx といったように、同じレジスタが2つ渡される形で使われる。
同じレジスタが渡されているtest命令を見つけたら、それは条件分岐であり、「レジスタが0ならば、ZFを1にするんだな」と覚えておけば大丈夫。
戻り値はeaxに格納される。これはある種の決まりごとであり、多くのプロセッサでは常識的に扱われている。
サブルーチンに渡す引数は、push命令を使い、スタックに格納する。
「引数はスタックに積まれる」と覚えておいて問題ない。
1.5 アセンブラ命令から動作を把握しよう
OllyDbgの関数一覧で、TypeがExportと書かれている関数をダブルクリックすると、その関数の先頭にジャンプすることができる。
OllyDbgのショートカット。
[Ctrl]+[F9]:リターンまで実行 [Alt]+[F9]:ユーザーコードまで実行
リバースエンジニアリングでは、「重要な個所は時間をかけてじっくりと理解する」「それ以外は全体の雰囲気からなんとなくこんな感じかなぁとざっくり把握する」ぐらいでよい。
コラム:アセンブラを書こう
アセンブラを書くことで、読み書き両方の能力が鍛えられる。
[NASM](アセンブラ)
http://www.nasm.us/
[ALINK](リンカ)
http://alink.sourceforge.net/download.html
実行例 C:\>nasm -fwin32 hello32.asm C:\>alink -oPE hello32 win32.lib -entry main