「たのしいバイナリの歩き方」勉強メモ#3
ソフトウェアの脆弱性はこうして攻撃される
2014/09/05 : CTF
「たのしいバイナリの歩き方」の勉強メモ第3弾です。
実行環境は、Windows7(64bit版)です。
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 バッファオーバーフローを利用して任意のコードが実行される仕組み
ソフトウェアの脆弱性は、「セキュリティホール」と呼ばれる。
最も有名なセキュリティホールの1つが「バッファオーバーフロー(Buffer Overflow)」である。
setuid:実行ユーザーではなく、そのプログラムの所有者の権限でプログラムを動作させる仕組み
アクセス権の「s」が、setuid が有効になっているプログラムであることを意味する(例:/usr/bin/passwd)。
[名前]
execve 関数 - 実行形式のプログラムファイルを実行する
[書式]
#include
int execve( const char * filename, char *const argv[], char *const envp[] ) ;
[説明]
filename:実行形式のプログラムファイルのパス名を指定する。
argv:実行するプログラムに渡す引数を、char 型のポインタの配列として指定する。
envp:実行するプログラムの環境変数を、char 型のポインタの配列として指定する(伝統的に、key = value の形式)。
argv と envp の最後の要素は NULL でなければならない。
処理が成功した場合は戻ってこない。失敗した場合は -1 を返す。
スタックとは、「筒や積み上げられる皿のような使われ方をするメモリ空間」。
スタックは、メモリアドレスの低位(減算方向)に向かって成長する。
push を実行し続ければ、その分だけ、メモリの低位に向かって値が格納されていく。
情報工学的には、
スタックをLIFO( Last In, First Out )、
キューのような最初に push したものを最初に pop するものを FIFO( First In, First Out )
と呼ぶ。
ここまでで感じた疑問
コンパイル(ビルド)と chmod(実行権限の変更)を「su」後に root 権限で行っているが、コンパイル(ビルド)はともかく、chmod(実行権限の変更)ができない(通常の)状況であれば、脆弱性のあるプログラムは実行できないのではないのか?
実行権限を変更してプログラムを実行するために root を乗っ取れるのであれば、遠回りをしなくても任意のプログラムが実行できるのではないか?
gcc で「-S」オプションをつけてコンパイルすると、アセンブラになった「.s」ファイルができる。
C言語において引数に渡しているデータが、アセンブラでは func が call される前にスタックへ格納される。
スタックに引数を格納し、call でサブルーチンを呼び出す。
call は jmp と違い、自分が呼び出されたアドレスを覚えておかなければならないため、サブルーチンへジャンプする前に、戻るべきリターンアドレスをスタックへ push する。
リターンアドレスは、処理が終了したあと、main へ戻るためのアドレスが格納されている。
これが上書きされると、攻撃者はあらゆる場所へ処理をジャンプさせることができる。
攻撃者が用意したコードに飛べるとしたら、それは「任意のコードが実行できる脆弱性」となる。
gdb は、UNIX 系 OS における有用なデバッガ(コマンドラインツール)。
主要コマンド
コマンド名 |
説明 |
r |
プログラムを実行する(そのまま引数も渡せる) |
b |
ブレイクポイントをセットする(*をつけてアドレスを渡す) |
c |
ブレイクポイントで止まった後、そのまま処理を実行させる |
x/[数字]i |
任意の命令数だけ、逆アセンブルする |
disas |
同上 |
x/[数字]s |
任意の数だけ、データを表示する
※引数には、アドレスもレジスタも渡せる
※レジスタを渡す場合は、$をつける
|
i r |
レジスタの値を表示する |
set |
レジスタうあメモリへ値を書き込む |
q |
デバッガを終了する |
gdb でプログラムをデバッグする2つの方法
①プログラムのパスを gdb の引数に渡して起動する
(例)gdb test00
②gdb を起動してからプロセスをアタッチする
(例)(gdb)attach 1234
攻撃者が実行したいコードのことを、shellcode と呼ぶ。
基本的に、/bin/sh が起動できれば何でもできるため、これを起動するための最小のマシン語のことを指したりもする。
gcc でコンパイルするときに、static オプションを指定すると、execve(関数)本体も実行ファイルにリンクされる。
スタティックリンクでコンパイル←→ダイナミックリンクでコンパイル
gdb で execve を逆アセンブルすると、「int $0x80」が呼び出されていることがわかる。
int $0x80 は、システムコール呼び出し。
usr/include/sys/syscall.h に、カーネルがそれぞれのシステムコールを識別するためのシステムコール番号の一覧がある。
execve のシステムコール番号は「59(16進数で0x3b)」。
Linux 環境では、/usr/src/linux/include/asm/unistd.h にシステムコール番号の一覧がある。
execve のシステムコール番号は「11(16進数で0x0b)」。
このように、
・システムコール番号は環境によって異なる
・shellcode は OS によって異なる
→環境に応じて作る必要がある。
x86 CPU で稼働している CentOS6 では、/usr/include/asm/unistd_32.h に記述があった。
execve のシステムコール番号は「11(16進数で0x0b)」になっていた。
shellcode に (マシン語における)0x00 が使われていると、strcpy が 0x00 を終端として認識するため、shellcode 全体をプログラムにコピーできない。
↓
[解決策]
①/bin/sh を、/bin//sh という8バイトの文字列にする
→コマンドを実行するにあたって、スラッシュは複数あっても問題ない。
②その前に、push $0 をしておく
→終端文字 0x00 を、xor と push を併用してスタックに積んでおく。
通常は、shellcode がターゲットプロセスのどのアドレスにあるかわからないので、推測しなければならない。
メモリを可能な限り NOP(0x90) で埋めて、最後に shellcode を置き、shellcode が実行される確率(成功率)を上げる。
近年はデフォルトでさまざまなセキュリティ機能がオンになっており、このような典型的なバッファオーバーフローはブロックされるようになっている。
コラム:printf 系関数に起因するフォーマットストリングバグ
printf 系関数に起因するフォーマットストリングバグ(format string bug)
→printf 系関数には、引数に渡されたポインタへデータサイズを書き込む変換指定文字があり、これを利用することで、任意のアドレスへ任意の値を書き込める。
3.2 攻撃を防ぐ技術
ASLR(Address Space Layout Randomization)
スタックや各モジュール、動的に確保したメモリなどのアドレス(配置先)をランダムに決定する仕組み
/proc/sys/kernel/randomize_va_space で設定を確認/変更できる。
0:無効
1:ヒープ以外をランダム化
2:すべてランダム化(デフォルト)
CentOS6 では、デフォルトで「2」になっていた。
Exec-Shield
メモリ領域の読み書き実行権限を制限する仕組み
スタックとして使用されているメモリ領域に実行すべきマシン語が置かれることは、通常ありえないので、スタック領域は、読み書きのみを許可して、実行を不可にするのが一般的。
コードセンクションにはマシン語が置かれているが、そのマシン語を書き換える必要は一般的なソフトウェアではまずないので、そのメモリ領域は書き込み不可にする。
仮にスタックに shellcode をコピーできても、それが実行できなければ、Segmentation fault でプログラムは終了する。
任意のプログラム内のメモリ領域の読み書き実行権限を確認するには、プログラムを実行した状態で、/proc/<PID>/maps を出力する(<PID> は、ps コマンドで確認する)。
CentOS6 では、/proc/sys/kernel/exec-shield で設定を確認/変更できる。
0:常に無効
1:マークされたバイナリを有効にし、以外は無効
2:マークされたバイナリを無効にし、以外は有効
3:常に有効
デフォルトで「1」になっていた。
StackGuard
コンパイル時に、各関数の入口と出口にスタックが破壊されたことを検知するマシン語を挿入する(コンパイラの機能)。
ebp や ret_addr を守るための仕組みで、典型的なスタックバッファオーバーフローに対する防御手法。
3.3 セキュリティ機能を迂回する技術
Return-into-libc
Exec-Shield の攻略法として考えられた、shellcode の代わりにライブラリ(libc)を利用する攻撃手法。
「(Exec-Shield 等のセキュリティ機能のせいで)任意のコード(shellcode)を実行できなくとも、最終的に任意のプログラムを実行できれば権限を奪える」
↓
「うまく引数を設定し、スタックを調整して、libc.so の中にある system 関数や exec 系関数へジャンプさせれば、/bin/sh などのプログラムを実行できる」
ldd コマンドを使うと、プログラムが実行時にロードするライブラリを確認できる。
ldd : 指定したプログラムの実行に必要な共有ライブラリを表示する。
構文
ldd [オプション] FILE...
オプション |
説明 |
--help |
使用法を表示する |
--version |
ldd のバージョン番号を表示する |
-d, --data-relocs |
リロケーション(メモリ上のアドレス書き換え処理)を行い、見つからないオブジェクトを表示する(ELF のみ) |
-r, --function-relocs |
データオブジェクトと関数に対してリロケーションを行い、見つからないオブジェクトや関数を表示する(ELF のみ) |
-u, --unused |
使われていない直接の依存関係を表示する(glibc 2.3.4以降) |
-v, --verbose |
シンボルのバージョン情報などを含めた詳細な情報を表示する |
FILE |
実行ファイル名を指定する |
※ELF(Executable and Linkable Format):コンパイラが生成するオブジェクト、および、ライブラリとリンクされた実行ファイルのファイルフォーマット
libc.so は、ほとんどのプログラムで実行時にロードされるか、コンパイル時に静的にリンクされているため、libc の中にある system 関数や exec 系関数をうまく呼び出せれば、権限を奪える。
ASLR によってロードされるモジュール群のアドレスが実行ごとにランダム化されていると、system や exec のアドレスがわからないために、攻撃は失敗する。
ROP(Return-Oriented-Programming)
「ランダム化されていないモジュール内にあるアセンブラコードを使って、うまく実行させたい処理をつなぎ合わせられないだろうか?」
詳細は、5章で。
コラム:セキュリティがいたちごっこになる理由
セキュリティにおいては、必ず明確な「敵」が存在する。
米国や韓国では「サイバー戦争」という言葉が一般的で、軍事や国防とセットで語られたりする。
記述に際しては、細心の注意をしたつもりですが、間違いやご指摘がありましたら、こちらからお知らせいただけると幸いです。
←「たのしいバイナリの歩き方」勉強メモ#4
←「たのしいバイナリの歩き方」勉強メモ#2
« 戻る