memologue RSSフィード

書いている人

日記というよりは備忘録、ソフトウェア技術者の不定期メモ。あるいはバッドノウハウ集。プライベートで調査した細々した諸々のスナップショット。嘘が散りばめられています。ISO/IEC 14882(C++)とPOSIX, GCC, glibc, ELFの話ばかりで、WindowsやMacの話はありません。特に記載がなければLinux/x86とILP32が前提です。時間の経過と共に古い記事は埋もれてしまいます。検索エンジンから飛んできた場合は、ページ内検索をご利用いただくかgoogleキャッシュを閲覧してみてください。技術的な記事を書きためる場所として使っています。言及してもらえると喜びます。主要な記事の一覧を書いておきます:

にんきコンテンツ: [GCC] mainを一度も呼ばないばかりか蹂躙する / Binary2.0 Conference 2006 発表資料 / C++ で SICP / ついカッとなって実行バイナリにパッチ / hogetrace - 関数コールトレーサ

昔のPOSIX関係の記事: シグナルの送受に依存しない設計にしよう / シグナルハンドラで行ってよい処理を知ろう / マルチスレッドのプログラムでのforkはやめよう / スレッドの「非同期キャンセル」を行わない設計にしよう / スレッドの「遅延キャンセル」も出来る限り避けて通ろう / マルチスレッドプログラミングの「常識」を守ろう / C++でsynchronized methodを書くのは難しい / シグナルハンドラからのforkするのは安全か? / g++ の -fthreadsafe-statics ってオプション知ってます? / 非同期シグナルとanti-pattern / localtimeやstrtokは本当にスレッドセーフにできないのか / UNIXの規格について / マルチスレッドと共有変数 - volatile?なにそれ。 / type punning と strict aliasing / アセンブラで遊ぶ時に便利なgdb設定 / 最近の記事は一覧から

2006-08-20

[] 最近の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()もどき。非標準関数

スパム対策のためのダミーです。もし見えても何も入力しないでください
ゲスト


画像認証