「たのしいバイナリの歩き方」 第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)

観点

「ランダム化されていないモジュール内にあるアセンブラコードを使って、
 うまく実行させたい処理をつなぎ合わせられないだろうか?」

コラム:セキュリティがいたちごっこになる理由

セキュリティにおいては、必ず明確な「敵」が存在する。
サーバを攻撃する人たちや、マルウェアを作ってお金儲けをする人たちがその「敵」である。
敵が新しい攻撃手法を考えたら、こちらも新しい防御手法を考えなければならないし、
敵が新しいシステムをねらったら、こちらも攻撃を防ぐ方法を考案しなければならない。
米国や韓国だと「サイバー戦争」という言葉が一般的で、軍事や国防とセットで語られたりする。