AIDEMOIRE

【アイデモワール】

脆弱性に関する実験:スタックオーバーフロー(その05)

スタックオーバーフローの脆弱性に関する考察

一応、スタックオーバーフローを起こしてシェルを起動することはできましたので、脆弱性に関する実験のスタック オーバーフロー編はこの辺で一旦終わりにしておきたいと思います。やり出したら切りが有りませんから。

幾つか雑感と、脆弱性を低く保つ方法などについて書いておきます。

今回は「スタック オーバーフローと利用したプログラムの実行って“具体的に”どうすれば出来るのだろう?」という自分自身への問いから始まりました。この手の話はGoogleれば山ほど見つかるでしょう。もっと本格的な(実践的な?)サイトもあると思います。しかし、初めから答えを見て問題を解いても面白くもありませんし、技術も身に付きません。そこで今回は試行錯誤して解く、と言うのがテーマで、その過程をメモ書きしてみた次第です。

実は、最初は1日もあれば出来るだろうと思っていたのですが、最近のシステムはやはり、それなりのプロテクションが実装されているので少々手間取ってしまいました。「スタックに機械語を積んで、そこにジャンプすれば直ぐにできるじゃん」と思ったのですがNXビットなる新規術(といってももう10年位前の技術ですか)に阻まれました。

スタックオーバーフローを使ってクラッキングされることはあるのか?

Linuxの標準のライブラリを適切使って、かつ余計なコンパイラ オプションを付けなければ、スタックオーバーフローを故意に起こすことは非常に難しいと思います。スタック オーバーフローによる脆弱性に関してはもう何十年も言われてきた事ですから、標準のライブラリなどは対策済みと考えてもよいのではないかと考えます。

実験では敢えて“-fno-stack-protector”を指定していますが、通常にアプリケーションを作成する場合、このオプションを敢えて指定するということは殆どないと思います。関数呼出しの度にチェックが発生するので余程性能を重視するケースではあるかと思いますが、それでもコアとなるエンジンの部分だけ指定してコンパイルすればよいので、アプリケーション全体をチェック無しでコンパイルするのはあり得ないでしょう。

また、コンパイラも賢くて、シーケンシャルなスタック オーバーフローの可能性が無い関数については監視用のコードを生成しません。さらに、仮にこのオプションを指定しても“-O3”などのオプションを指定すると、(自作の関数側ではなく)ライブラリ側でチェックすることもあります。例えば先日の最終プログラムを最適化オプション付きでコンパイルして実行すると“bcopy”のライブラリ側でオーバーフローを検知して動作をストップします。

ubuntu@ubuntu1304d64:~/Temp$ cc -o tricky4 tricky4.c -fno-stack-protector -static -fno-strict-overflow -O3
In file included from /usr/include/string.h:638:0,
                 from tricky4.c:2:
In function ‘bcopy’,
    inlined from ‘func’ at tricky4.c:16:7:
/usr/include/x86_64-linux-gnu/bits/string3.h:91:3: warning: call to __builtin___memmove_chk will always overflow destination buffer [enabled by default]
ubuntu@ubuntu1304d64:~/Temp$ ./tricky4
*** buffer overflow detected ***: ./tricky4 terminated
======= Backtrace: =========
[0x410f62]
[0x410f0e]
[0x40118e]
[0x400dbe]
[0x40137c]
[0x401085]
======= Memory map: ========
00400000-004c4000 r-xp 00000000 08:01 407306                             /home/ubuntu/Temp/tricky4
006c3000-006c6000 rw-p 000c3000 08:01 407306                             /home/ubuntu/Temp/tricky4
006c6000-006c9000 rw-p 00000000 00:00 0
0071b000-0073e000 rw-p 00000000 00:00 0                                  [heap]
7fcf0542e000-7fcf0542f000 rw-p 00000000 00:00 0
7fff95518000-7fff95539000 rw-p 00000000 00:00 0                          [stack]
7fff955c2000-7fff955c4000 r-xp 00000000 00:00 0                          [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0                  [vsyscall]
Aborted (core dumped)
ubuntu@ubuntu1304d64:~/Temp$

では、未だに多くのスタック オーバーフローの脆弱性が見つかるのはなぜなのでしょうか? ライブラリを使ったバッファ間でのデータコピーではなく、ユーザ(もしくはアプリ開発者)が自らコーディングしたロジックの中に可能性が発生するのではないかと考えます。例えば、バッファsrcからバッファdstにデータをコピーする際に、単純にコピーするのではなく、srcの値によってスキップしたり、データを追加したり、またはdstへの挿入位置を変えたりする際に発生する可能性は高いと思います。

あとは昔のシステムを使い続けている場合にも注意が必要かもしれません。セキュリティの矛と盾は日々進化しているので、最近のシステムではハードウェアの支援も含めて、対策が施されていますが、昔のシステムはそうは限らないでしょう。一度、サービスを始めると10年以上も動き続けるシステムもザラにありますから、自身のシステムにどの様な脆弱性があるのかを確認することは大切かと思います。

スタック オーバーフローが可能だとして悪用できるのか?

“スタック オーバーフロー”という事に関して言えば、プログラムが入手できない限りは難しいと思います。例えば、Webサーバの裏側で動いている自作のアプリケーションに対して、この手法を使ってのクラッキングは難しいでしょう。出来ないとは言えませんが、労多くして実少なし、と思います。

ただし、プログラムが入手可能な場合は、可能性はゼロではありません。別にソースコードである必要もなく、実行形式のプログラムでも色々なツールを駆使することでロジックを追いかけることはできますから。

今回の実験を通して雑感、色々

/bin/sh”って文字列は7文字なんです。だから文字列終端のNULL文字を入れても1ワード(64ビット)に収まってしまうのですね。そのため、レジスタに書き込んだりスタックに積んだりも簡単にできます。また、1ワードのデータを扱うための命令も山ほどあります。コマンド名は短く!というのがUNIXの基本コンセプトなので痛し痒しですが。(個人的にはこのコンセプトは大好きです。)

  • コマンドパスは分割しておいた方がいいね

アプリケーションからシェルを呼び出して使う必要がある場合、“/bin/sh”や“/bin/bash”という文字列を定数として持っておくのは、脆弱性が少しだけ増えているのはないかと思います。プログラム内にはパスとコマンド名を分けて置いておいて、必要に応じてパスを生成する方が、ほんのちょっとだけだけど良いかも知れません。(その分、バグの入る可能性は増しますが。)

execveシステムコールはrdiレジスタに実行させたいコマンド名のアドレスをセットするだけで、簡単に実行できてしまうので、これもちょっとシンドイです。例えば構造体へのポインタとかにすれば少しは悪用され難くなるかと思います。とは言え、OSの基本的なインタフェースですから変更するわけにもいきません。Linux系は暫くは変わらないのでしょう。また、execveに限らずsystemシステムコールも悪用されやすいAPIの一つですね。(私はWindowsは詳しくないので知りませんが、そう大差はないのではと想像しています。ハードウェア(CPU)が提供するマシン語LinuxもWindowsも変わりなく、それを使わなければAPIを実装することはできませんから。)

脆弱性に関する実験:スタックオーバーフロー編(リンク)

脆弱性に関する実験:スタックオーバーフロー(その01) - AIDEMOIRE
スタックへのマシン語の書き込みとその実行に関する実験


脆弱性に関する実験:スタックオーバーフロー(その02) - AIDEMOIRE
戻りアドレスの書き換えによるロジックの変更に関する実験


脆弱性に関する実験:スタックオーバーフロー(その03) - AIDEMOIRE
プログラムによる自分自身のプログラムの変更に関する実験


脆弱性に関する実験:スタックオーバーフロー(その04) - AIDEMOIRE
スタックの書き換えによるシェルの起動


脆弱性に関する実験:スタックオーバーフロー(その05) - AIDEMOIRE
スタックオーバーフローの脆弱性に関する考察


脆弱性に関する実験:スタックオーバーフロー(その06) - AIDEMOIRE
スタック上のプログラムを実行する方法