最近のglibcではatexit関数やjmp_bufを狙った攻撃は効かない (PTR_MANGLE)


小ネタ。MSの中の人のblogをなんとなく眺めていたら、

という記事がありました。要約すると、「長生きするポインタ(特に関数ポインタ)は悪用されやすいので、値を素のまま格納しないほうがよい」という話です。長生きなポインタというのは、

  • 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っぽくないので(?)略)、

  1. アドレスを事前に決めない (ASLR+DSO, ASLR+PIE)
  2. 書き込みを禁止する (RELRO)
  3. 実行を禁止する (NX)
  4. 予測不可能な値でのマーキング/エンコード (SSP)
  5. 標準関数から危険な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以降で

って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のサポートが切られる予定だから、その布石か。

*1:NX(Exec-Shield)が効いていない環境ならさらに話は簡単で、送り込んだshellcodeに制御を移すことで任意コード実行ができます

*2:SunOS4由来のatexit()もどき。非標準関数