ASLR, setarch -RL, prelink, PIE and LD_USE_LOAD_BIAS

独り言です。(慣れないkernelの)コードをざーっと調べて、あまりまじめには検証しないで書いてます。FC5限定なので細かい記述の賞味期間はあと30日くらいでしょうか。

ASLR


私の使っているFC5のkernel/glibcには、一般にASLR(address space layout randomization)とか呼ばれている、日々のhackの邪魔になるセキュリティ的に大変よろしい機能があります。ASLRをフルに機能させると、プロセスの動作するアドレス空間において

  1. DSOをどこに貼り付けるか (glibcの elf/dl-load.c の _dl_map_object_from_fd())
  2. ELFのinterp (/lib/ld.so) をどこに貼り付けるか (kernel の fs/binfmt_elf.c の load_elf_interp())
  3. 実行ファイルをどこに貼り付けるか (fs/binfmt_elf.c の load_elf_binary())
  4. heapをどのへんから開始するか (fs/binfmt_elf.c の load_elf_binary())
  5. PROT_EXECではないmmapで返却するアドレスをどのへんにするか (arch/i386/mm/mmap.c の mmap_base())
  6. stackをどのへんから開始するか (fs/binfmt_elf.c の load_elf_binary())
  7. vDSOをどこを置くか (arch/i386/kernel/sysenter.c の arch_setup_additional_pages() あたり。細かいので以下では追わない)

くらいがランダムになります。カッコ内はrandomizationのおよその実装位置。ASLRが効いていると、プロセスへの攻撃が成功しにくいとかいろいろ嬉しいことがあるそーです。

FC5のこまった現象


で、このrandomize機能は /proc/sys/kernel/randomize_va_space を0にすれば完全にoffになると思っていたのですが、どうもそうはならない模様。。。stackやheapのアドレスは無事固定されるのですが*1、上記1-7のうちのいくつかのアドレスは依然randomizeされてしまうのですよ。たとえばまず、prelinkを完全に切った状態で、randomize_va_space を0にし、non-PIEなバイナリを実行すると、バイナリがリンクしているDSOのロードされるアドレスが毎回変化しちゃうんですよね。更にわるいことに、PIEなバイナリだと、DSOのアドレスのみならず、バイナリ自身のアドレス、dynamic linker*2のアドレスまでもが毎回変化しちゃいます。えーー。いろいろ遊ぶ上でいろいろ不都合が。


バグなのか仕様なのかはよくわかりませんが、どうなってるのか見てみました。ASLRがどのように実装されているか、思えばちゃんと見ていなかったので良い機会かなというのもあり。以下で参照しているkernelは、rpmbuild --target i686 -bp SPECS/kernel-2.6.spec にて展開およびpatchされた 2.6.17-1.2174_FC5 のものです。

stack開始位置 -1-


stack開始位置をランダムにしているのは、まず、linux-2.6.x/fs/binfmt_elf.c の randomize_stack_top()のようです。普通の開始位置(ユーザ/カーネルの3G/1Gの境界位置である0xbfffffffあたり)から0x0方向に最大8MB(ただしページ単位, 最大2048ページ)だけずれる模様。これが行われるのは、current->flags に PF_RANDOMIZE が含まれるときだけです。


このflagを立てているのは、同じく fs/binfmt_elf.c の load_elf_binary() の

        if ( !(current->personality & ADDR_NO_RANDOMIZE) && randomize_va_space)
                current->flags |= PF_RANDOMIZE;

っぽいです。ですから、

  • /proc/sys/kernel/randomize_va_space を 0 にしOS全体のランダム化をoffにするか
  • 親プロセスがpersonality(2)でADDR_NO_RANDOMIZEをsetしてから当該プロセスをexec

すれば、スタック開始位置のランダム化は無効になります。後者については、setarch(8)という便利コマンドがあるんで、自分でfork&exec処理をかかなくても、

% /usr/bin/setarch i686 -R cat /proc/self/maps

とするだけで(per-process basisで)ランダム化を切った状態でコマンド(ここではcat)を実行できます。setarch(8)はマイナーなコマンドっぽいので、紹介しておきます。そういえば、ASLRがonだとうまく動かないプログラムがあるそうですが(emacsの一部機能とか)、randomize_va_spaceで対処するのではなく、setarchで対処するほうがスマートかもしれませんね。

stack開始位置 -2-


そのあと、fs/exec.c の setup_arg_pages() で、 arch/i386/kernel/process.c の arch_align_stack() を呼ぶことでスタック開始位置を0-8192バイトの範囲(ただし16bytes単位)でさらにランダム化しています。

参考: http://kernel.org/pub/linux/kernel/people/arjan/randomize/08-stack-top-randomize

        stack_base = arch_align_stack(stack_top - MAX_ARG_PAGES*PAGE_SIZE);
        stack_base = PAGE_ALIGN(stack_base);

2度に分けている理由はここ。でも、直後でPAGE_ALIGNしてしまっているんで、意味があるんだかないんだかよくわかんない。あと、arch_align_stack() はrandomize_va_space だけで ADDR_NO_RANDOMIZE は見てない。うーん。ちなみに、ARG_PAGESというのはプログラムへの引数などを格納する場所で、デフォルトで32ページ、128kbytes。

heap開始位置


stackと同じく、load_elf_binary()から呼ばれている arch/i386/kernel/process.c の randomize_brk() でランダム化されているのかな? たぶん。振れ幅は32MB(0x02000000)らしい。

#ifdef __HAVE_ARCH_RANDOMIZE_BRK
        if (current->flags & PF_RANDOMIZE)
                randomize_brk(elf_brk);
#endif

実行ファイルの位置


fs/binfmt_elf.c の load_elf_binary() の、このへん:

                vaddr = elf_ppnt->p_vaddr;
                if (loc->elf_ex.e_type == ET_EXEC || load_addr_set)
                        elf_flags |= MAP_FIXED;
                else if (loc->elf_ex.e_type == ET_DYN)
#ifdef __i386__
                        load_bias = 0;
#else
                        load_bias = ELF_PAGESTART(ELF_ET_DYN_BASE - vaddr);
#endif

                error = elf_map(bprm->file, load_bias + vaddr, elf_ppnt, elf_prot, elf_flags, 0);

ELFヘッダ(eu-readelf -h で確認可)の種別がET_EXECなら(=普通の実行ファイルなら)、MAP_FIXEDでプログラムヘッダのVirtAddrの位置(eu-readelf -l で出てくるPT_LOADのVirtAddrのこと)にマップ。


ET_DYNなら(=PIEな実行ファイルなら)、

  1. PIEがprelinkされていたら、なるべくそのアドレスにマップされる
  2. されていなければ(VirtAddrが0x0なら)、ランダムな位置にマップされる

のかな。後者の場合は、elf_map()から、include/linux/mm.h のdo_mmap() と、mm/mmap.c の do_mmap_pgoff() を経由して、同ファイルの arch_get_unmapped_exec_area() に到達して、

#define SHLIB_BASE             0x00111000
...
        if (!addr && !(flags & MAP_FIXED))
                addr = randomize_range(SHLIB_BASE, 0x01000000, len);

のような感じで drivers/char/random.c の randomize_range() で得られた適当なアドレスにマップされる感じです。このとき、/proc/sys/kernel/randomize_va_space なり、PF_RANDOMIZE なりはチェックされていない模様。実際、randmmize_va_spaceを0にしても、PIEなバイナリは毎回違ったアドレスにマップされてしまいます...(しょぼーん)。バグなのかなんなのか。

interpの位置


load_elf_interp() は、load_elf_binary() から呼ばれています。interpの位置決めはこのへん:

            vaddr = eppnt->p_vaddr;
            if (interp_elf_ex->e_type == ET_EXEC || load_addr_set)
                elf_type |= MAP_FIXED;
            else if (no_base && interp_elf_ex->e_type == ET_DYN)
                load_addr = -vaddr;

            map_addr = elf_map(interpreter, load_addr + vaddr, eppnt, elf_prot, elf_type, total_size);

PT_INTERPに変態チックなことを書いていない限り(普通に /lib/ld.so を使っている限り)、e_typeはET_DYNです。で、no_baseがtrueかどうかで5行目に到達するかが決まるんですが、どうも、実行ファイルがPIEの場合のみ、no_baseがtrueになるっぽいですね。load_elf_binary()のこのへんによります:

                        if (loc->elf_ex.e_type == ET_DYN) {
                                load_bias += error -
                                             ELF_PAGESTART(load_bias + vaddr);

というわけで、

  • PIEだとinterpもelf_map(0x0);されることになり*3、そのアドレスがランダムに決まることになります
  • interpがprelinkされていたら、そのアドレスになることが多いです
  • interpがprelinkされてなかったら、PIEかどうかにかかわらず、やっぱりランダムに決まることになります

ランダムに決まるケース2つについて、/proc/sys/kernel/randomize_va_space なり、PF_RANDOMIZE なりはやっぱり参照されてませんね。ショボーン2。

DSOの位置


DSOは、dynamic linkerがマップします。次の1-4の順で条件をみていき、さいしょにひっかかったところの方法でマップされるようです。

  1. 環境変数LD_USE_LOAD_BIASが1で、prelinkされていたら、そのアドレス
  2. 実行ファイルがPIEなら、ランダム
  3. DSOがprelinkされていたら、そのアドレス
  4. ランダム

DSOのマップはカーネルではなくて、dynamic linker (interp, ld.so) がやるわけなので、LD_USE_LOAD_BIASなる環境変数でちょっとだけ操作可能になってます。ランダムって書いてあるところは、実際には非MAP_FIXEDのPROT_EXECでmmap(0x0)する感じなんですが、結局行き着く先はカーネルのarch_get_unmapped_exec_area() なので、例によって randomize_va_space なり、PF_RANDOMIZE なりは参照されずにランダムになっちゃいます。ショボーン3。


prelinkなPIEの場合は...知りません。もうちょいglibcのソースを追わないと。ここに書いてあるような気もしますが、確認していません。

PROT_EXECではないmmap位置


PF_RANDOMIZEな場合だけ、arch/i386/mm/mmap.c のmmap_base()で、get_random_int() % (1024*1024) しています。

        if (current->flags & PF_RANDOMIZE)
                random_factor = get_random_int() % (1024*1024);
...
        return PAGE_ALIGN(TASK_SIZE - gap - random_factor);

この値は mm->mmap_base として覚えられ、PROT_EXECではないmmapの戻りを計算する際に使われます (mm/mmap.c参照)。

まとめ (ASLR万歳派向け)


ASLRが大好きな皆様は、randomize_va_spaceを1にしておくのは当然として、次の(1)(2)を行うとよいと思います。これはFC5固有の挙動に依存した話ではないので、暫くは一般的に使える話だと思います。


(1) ネットワークデーモンや、suidプログラムは、かならずPIEとして作成しましょう


がお薦めです。これでリスクの高いプロセスについては、fully randomized確定です。PIEかどうかが、DSOを常にrandomizeするかのスイッチになる感じです。DSOとinterpのprelinkは無視されます。Fedoraなんかだと、たとえばhttpdなんかは既にPIEとしてコンパイルされるようになっていますね。しかし、もしなんでもかんでもPIEにするのが難しいのであれば、


(2) prelinkをdisableしちゃいましょう


Fedoraなら、vi /etc/sysconfig/prelink で PRELINKING=no にして、一晩待てばOK。cron様がどうにかしてくれます。実行ファイルのアドレス以外は全てランダムになります。えー、prelinkにも実はrandomize機能(-R)がありまして、一応、他人の箱とは違ったアドレスにDSOがマップされるようにはなっていますし、Exec-Shieldがインスコされている箱の場合は、なるべくASCII-Armor領域*4にDSOをマップするようになってます。しかしながら、local exploitも視野にいれるならprelinkはdisableするほうが確実です。

まとめ (ASLR逝ってよし派向け 1)


ASLRが日々のhackの邪魔でしょうがないという皆様は、私も自宅マシンについてはそうだったりしますけど、うーん、、、


気持ち悪いworkaroundとしては、まず、/proc/sys/kernel/randomize_va_space を 0 にするだけでなく/proc/sys/vm/legacy_va_layout も 1 にしておくという手があります。これを行うと、仮想アドレス空間の使われ方がkernel2.4のころ(2.6.7まで?)と同じになり、randomizeもされなくなるようです(legacy_va_layout については、コードは見てません。レイアウトについては、LWNのReorganizing the address spaceという記事が図入りでgoodです。右上のレイアウトのpngが以前のレイアウト。左下のあたらしいほうはflexible-mmapと呼ばれているのかな??)。あるいは、setarch i686 -R -L でバイナリを実行するのでも(ほぼ、カーネルのpersonalityのハンドリングがバグってなければ)同じことです。

参考: http://gcc.gnu.org/wiki/Randomization


これで、一応、ランダムな部分はなくなります。

まとめ (ASLR逝ってよし派向け 2)


でもlegacy_va_layoutは気持ち悪い。DSOが妙なアドレスから張り付くのはキモすぎ、legacyという言葉が気に食わない、健全かつ完全にrandomizeをoffにしたい、というなら、確実なのは kernel のmm/mmap.cの下記部分をコメントにしてしまうことだと思います。確認してませんが...。

        if (!addr && !(flags & MAP_FIXED))
                addr = randomize_range(SHLIB_BASE, 0x01000000, len);

この状態で randomize_va_space を0にすればOKで、多分完全にランダム性はなくなります*5。kernelいじるのがめんどくさければ、

  • interpもDSOも全部prelinkして、
  • randomize_va_space を0にして
  • PIE使わない

ですかねえ。


なお、万歳と逝ってよしの中間は難しそう。「妙なコードを書いているときだけrandomizeを無効にしたいけど、普段はprelinkは切っておきたい」とか。kernelを好みに応じて調整するしかなさそうですね。上記のコードのifでPF_RANDOMIZEを見るとか。...やっぱり setarch -RL かなぁ。

(おまけ) 2.6.17-1.2174_FC5 で (PIE + no-randomize) だとheapの位置決めがおかしい?


heap上にコードを置いて実行するプログラムを作って、PIEにしておきます。
短くて便利なのでshinhさんの195ネタでいきます。

% cat heap_exec.c
#include <stdio.h>
#include <stdlib.h>
int main() {
  unsigned char* p = malloc(1);
  *p = 195;
  printf("heap = %p\n", p);
  ((void (*)())p)();
  return 0;
}

__attribute__((constructor)) void dump_maps() {
  FILE* fp;
  int c;
  fp = fopen("/proc/self/maps", "rb");
  while ((c = fgetc(fp)) != EOF) {
    putchar(c);
  }
  fclose(fp);
}

% gcc -fPIE -pie heap_exec.c

で、/proc/sys/kernel/exec_shield はon、randomize_va_spaceはoffで実行すると、通常heapには実行権限がないので

./a.out
004dc000-004dd000 r-xp 004dc000 00:00 0          [vdso]
004dd000-004f6000 r-xp 00000000 fd:00 1399431    /lib/ld-2.4.so
004f6000-004f7000 r-xp 00018000 fd:00 1399431    /lib/ld-2.4.so
004f7000-004f8000 rwxp 00019000 fd:00 1399431    /lib/ld-2.4.so
00881000-009ae000 r-xp 00000000 fd:00 1399445    /lib/libc-2.4.so
009ae000-009b0000 r-xp 0012d000 fd:00 1399445    /lib/libc-2.4.so
009b0000-009b1000 rwxp 0012f000 fd:00 1399445    /lib/libc-2.4.so
009b1000-009b4000 rwxp 009b1000 00:00 0
00fa1000-00fa2000 r-xp 00000000 fd:00 1530108    /home/yupo/a.out
00fa2000-00fa3000 rw-p 00000000 fd:00 1530108    /home/yupo/a.out
00fa3000-00fc4000 rw-p 00fa3000 00:00 0          [heap]
b7fef000-b7ff0000 rw-p b7fef000 00:00 0
b7ffe000-b8000000 rw-p b7ffe000 00:00 0
bffea000-bffff000 rw-p bffea000 00:00 0          [stack]
heap = 0xfa3008
Segmentation fault

こんな感じでsegvるわけなんですけど、3回に2回くらい、こうなります。

% ./a.out
009d1000-00afe000 r-xp 00000000 fd:00 1399445    /lib/libc-2.4.so
00afe000-00b00000 r-xp 0012d000 fd:00 1399445    /lib/libc-2.4.so
00b00000-00b01000 rwxp 0012f000 fd:00 1399445    /lib/libc-2.4.so
00b01000-00b04000 rwxp 00b01000 00:00 0
00ccb000-00ccc000 r-xp 00000000 fd:00 1530108    /home/yupo/a.out
00ccc000-00ccd000 rwxp 00000000 fd:00 1530108    /home/yupo/a.out
00ccd000-00cee000 rwxp 00ccd000 00:00 0          [heap]
00db4000-00db5000 r-xp 00db4000 00:00 0          [vdso]
00db5000-00dce000 r-xp 00000000 fd:00 1399431    /lib/ld-2.4.so
00dce000-00dcf000 r-xp 00018000 fd:00 1399431    /lib/ld-2.4.so
00dcf000-00dd0000 rwxp 00019000 fd:00 1399431    /lib/ld-2.4.so
b7fef000-b7ff0000 rw-p b7fef000 00:00 0
b7ffe000-b8000000 rw-p b7ffe000 00:00 0
bffea000-bffff000 rw-p bffea000 00:00 0          [stack]
heap = 0xccd008
%

[heap]が[vdso]とひっついていて、ろくにメモリ確保できないし、そもそも何故か[heap]がrwxだしで、まずい。
実際、 p=195 が実行できちゃってます。なんなんでしょうこれ。DSOとinterpはprelinkされていてもされていなくても再現しました。調べ中。

結論/総まとめ


毛嫌いしないでカーネルをたまに眺めてみるのも楽しい。

*1:実際にはFC5ではheapは固定されないことがあった、バグっぽい、後述

*2:以下interpと呼んだりもします。平たくいうと/lib/ld.soのことです

*3:load_addr + vaddr == 0x0になるから

*4:論理アドレス空間の先頭16MB @i386

*5:FC5では念のためPIEも使わない方が良いかも。heapの位置がおかしくなることがあった、後述