最近のglibcではatexit関数やjmp_bufを狙った攻撃は効かない (PTR_MANGLE)
小ネタ。MSの中の人のblogをなんとなく眺めていたら、
- Address Space Layout Randomization in Windows Vista - Function Pointer Obfuscation
- Protecting against Pointer Subterfuge
という記事がありました。要約すると、「長生きするポインタ(特に関数ポインタ)は悪用されやすいので、値を素のまま格納しないほうがよい」という話です。長生きなポインタというのは、
- atexit関数で登録された関数へのポインタ (プロセス終了時まで保持される)
- heapの管理情報内のポインタ
- jmp_buf内のポインタ (longjmpする可能性のある長い処理の間保持される)
などです。こういうポインタは、ソフトウェアの脆弱性(メモリ空間内の任意の4バイトを書き換え可能な脆弱性)を悪用して上書きされる対象になりやすく、上書きが即、攻撃成功につながります。たとえば、atexit関数で登録された関数ポインタを「シェルを起動する関数のポインタ」にすり替えておけば、プロセス終了時にシェルが起動してしまうことでしょう*1。
いつだかに書いた、Linuxの .dtors overwrite 攻撃なんかだと、セクションをまるごとreadonlyにする(RELRO)ことで書き換え攻撃から身を守ることができるわけですが、atexit関数のポインタ覚え先などは、readonlyにするわけにもいきませんから厄介です。で、MSとしては「ならポインタの値を予測不可能な値でエンコード(XOR)してしまえばいいんじゃね?」という結論になったみたいです。Vistaからは、多くの長生きポインタがエンコードされて格納されます。
最近のバイナリ系攻撃(って何?)に対する防御技術は、WindowsでもUNIXでもおおよそ次の5つくらいに分類されるような気がしますが(括弧内はFedoraでの例、MACとかLeast Priv.とか署名はbinaryっぽくないので(?)略)、
- アドレスを事前に決めない (ASLR+DSO, ASLR+PIE)
- 書き込みを禁止する (RELRO)
- 実行を禁止する (NX)
- 予測不可能な値でのマーキング/エンコード (SSP)
- 標準関数から危険なfeatureを取り去る (FORTIFY_SOURCE)
今回のポインタの件では 4. の方法をとったということですね。
glibcでどうなってるか見てみた
我らがglibcで、長寿命ポインタの扱いがどうなっているか見てみました。FC5(x86)です。まずはatexit()のソースコードから。atexit()の実体は__cxa_atexit()なんで、そちらを見ます。
glibc-20060306T1239% lv stdlib/cxa_atexit.c ...(略)... int __cxa_atexit (void (*func) (void *), void *arg, void *d) { struct exit_function *new = __new_exitfn (); if (new == NULL) return -1; #ifdef PTR_MANGLE PTR_MANGLE (func); #endif ...(略)...
グ・・グレート。PTR_MANGLEというマクロで、きっちりエンコードされています。atexit()の他、on_exit()*2、setjmp系関数、iconv系関数でもPTR_MANGLEが使われている模様。2005年の12月頃に入った変更のようですね。PTR_MANGLE は、sysdeps/unix/sysv/linux/i386/sysdep.h で定義されています。簡単に書くと、
% objdump -d /lib/libc-2.4.so | grep -A 20 __cxa_atexit ... 0012bb2b <__cxa_atexit>: ... 12bb4a: 85 d2 test %edx,%edx 12bb4c: 74 1e je 12bb6c <__cxa_atexit+0x41> 12bb4e: 65 33 35 18 00 00 00 xor %gs:0x18,%esi <--- これ ...
PTR_MANGLE は(i386では)素のポインタの値と %gs:0x18 の値をXORするマクロです。もう一度PTR_MANGLEすれば元に戻ります。%gs:0x18 の値は、elf/rtld.c のdl_main()で決められます。ダイナミックリンカの冒頭部分ですね。
/* Set up the stack checker's canary. */ uintptr_t stack_chk_guard = _dl_setup_stack_chk_guard (); THREAD_SET_STACK_GUARD (stack_chk_guard); /* Set up the pointer guard as well, if necessary. */ if (GLRO(dl_pointer_guard)) { // XXX If it is cheap, we should use a separate value. uintptr_t pointer_chk_guard = stack_chk_guard; THREAD_SET_POINTER_GUARD (pointer_chk_guard);
一部略記していますが、だいたいこんな感じ。SSP(ProPolice)用のカナリアの値と同じ値が使われるのですね(うーん...大丈夫なのかな)。_dl_setup_stack_chk_guard()は、シンプルに /dev/urandom からポインタのバイト数だけ値を拾ってくる関数です。
また、LD_POINTER_GUARD なる環境変数も新設されているようです。env LD_POINTER_GUARD=0 ./hoge とすると、このセキュリティ強化機構を無効にできる模様。ガードの値が0x0になります。
動作を一応確認
#include <stdio.h> #include <stdint.h> #include <stdlib.h> uint32_t get_pointer_guard() { uint32_t r; asm("movl %%gs:0x18, %%eax" : "=a"(r)); //行儀よく return r; } void foo() {} int main() { atexit(foo); return 0; }
のようなコードを用意して、これをgdb上で実行します。foo関数の登録までステップ実行した後、__exit_funcs変数(atexit関数がポインタを登録する先)をダンプしてみます。
(gdb) x/12x __exit_funcs 0x230c80 <initial>: 0x00000000 0x00000002 0x00000004 0xa764ec0b 0x230c90 <initial+16>: 0x00000000 0x00000000 0x00000004 0xaf265083 0x230ca0 <initial+32>: 0x00000000 0x00000000 0x00000000 0x00000000
0xa764ec0b と 0xaf265083 の2つのポインタ(エンコード済)が登録されているようです。前者は、glibcが勝手に登録する関数のアドレスです。一方、ガードの値はというと、
// gdbで直接 %gs:0x18 の値を表示する方法がわからなかった... orz (gdb) call/x get_pointer_guard() $2 = 0xa722d33b
のようです。この値と、登録されているポインタの値をXORして(デコード)してみると、
(gdb) p/x 0xa764ec0b ^ $2 $3 = 0x463f30 (gdb) x/4i $3 0x463f30 <_dl_fini>: push %ebp 0x463f31 <_dl_fini+1>: mov %esp,%ebp 0x463f33 <_dl_fini+3>: push %edi 0x463f34 <_dl_fini+4>: push %esi (gdb) p/x 0xaf265083 ^ $2 $4 = 0x80483b8 (gdb) x/4i $4 0x80483b8 <foo>: push %ebp 0x80483b9 <foo+1>: mov %esp,%ebp 0x80483bb <foo+3>: pop %ebp 0x80483bc <foo+4>: ret
見事に _dl_fini と foo のアドレスになりました。
というわけで (まとめ)
世間に出回っている、単純なatexit関数の悪用法は、最近のglibcには効きません。
コードを書くときのプラクティス
自分でコードを書くときも、関数ポインタへの値の格納はエンコードしたほうがよいかもわからんですね。static(静的記憶域期間)なポインタの場合は特に。Windowsだと、XP SP2以降で
- EncodePointer (http://msdn.microsoft.com/library/default.asp?url=/library/en-us/devnotes/winprog/encodepointer_func.asp)
- DecodePointer (http://msdn.microsoft.com/library/default.asp?url=/library/en-us/devnotes/winprog/decodepointer_func.asp)
ってAPIが使えるようです。Linuxで似たようなことがしたいなら、
// x86用 void* encode_pointer(void* p) { __asm__("xorl %%gs:0x18, %0" : "=r"(p): "0"(p)); return p; } extern __typeof(encode_pointer) decode_pointer __attribute__((__alias__("encode_pointer")));
とかですかね。エンコードとデコードは同じ処理になります。最終行で、簡単なことを難しく実現しようとしているように見えるのは気のせいです。
(補足) 0x18というのは、glibcが内部で使ってる構造体の先頭からのオフセット値なんで、将来変化するかも。offsetofでほげるほうがいいです。あと、攻撃者も便利に使える関数を作ってしまうのはよくないかも? どうなんだろ。
全然関係ないんですがFC5でのLD_ASSUME_KERNELについて
上の方でlibcを逆アセンブルしていてふと。
FC5から、LD_ASSUME_KERNELによるlibc切り替えができなくなっていますね。FC4までだと確か、libcのsoは三種類入っていて、それぞれ
- /lib のものは .note.ABI-tag section の OS ABI version が 2.2.5 (LinuxThreads)
- /lib/i686 のものは 2.4.1 (LinuxThreads)
- /lib/tls のものは 2.4.20 (NPTL)
でした(デフォルトで使われるのは/lib/tlsのもの)。FC5では、i686/ と tls/ はカラになって、
% eu-readelf -n /lib64/libc-2.4.so Note segment of 32 bytes at offset 0x270: Owner Data size Type GNU 16 VERSION OS: Linux, ABI: 2.6.9 % readelf -x .note.ABI-tag /lib64/libc-2.4.so セクション '.note.ABI-tag' の 16 進数ダンプ: 0x3419c00270 00554e47 00000001 00000010 00000004 ............GNU. 0x3419c00280 00000009 00000006 00000002 00000000 ................
/lib直下のlibc.soのOS ABIバージョンが2.6.9になりました(上はx86_64ですが、x86でも同じです)。FC5以降では、export LD_ASSUME_KERNEL=2.2.5 しているシェルスクリプトなんかは一切動かなくなるので要注意。
# なるほど、RHEL5でLinuxThreadsのサポートが切られる予定だから、その布石か。