ELF実行ファイルのメモリ配置はどのように決まるのか
Linux x64環境において、ELF実行ファイル、共有ライブラリ、スタック領域、ヒープ領域のアドレスがどのように決まるのかについてのメモ。
環境
Ubuntu 12.04 LTS 64bit版
$ uname -a Linux vm-ubuntu64 3.11.0-15-generic #25~precise1-Ubuntu SMP Thu Jan 30 17:39:31 UTC 2014 x86_64 x86_64 x86_64 GNU/Linux $ lsb_release -a No LSB modules are available. Distributor ID: Ubuntu Description: Ubuntu 12.04.4 LTS Release: 12.04 Codename: precise $ gcc --version gcc (Ubuntu/Linaro 4.6.3-1ubuntu5) 4.6.3
実行時のメモリマップを確認してみる
まずは、ヒープ領域を使う適当なプログラムを用意する。
/* hello.c */ #include <stdio.h> #include <stdlib.h> #include <string.h> int main() { char *buf; buf = malloc(20); strncpy(buf, "Hello, world!", 20); puts(buf); return 0; }
$ gcc hello.c $ gdb -q a.out Reading symbols from /home/user/tmp/a.out...(no debugging symbols found)...done. (gdb) b puts Breakpoint 1 at 0x400470 (gdb) r Starting program: /home/user/tmp/a.out warning: no loadable sections found in added symbol-file system-supplied DSO at 0x7ffff7ffa000 Breakpoint 1, 0x00007ffff7a8ace0 in puts () from /lib/x86_64-linux-gnu/libc.so.6 (gdb) i proc process 5927 cmdline = '/home/user/tmp/a.out' cwd = '/home/user/tmp' exe = '/home/user/tmp/a.out' (gdb) shell cat /proc/5927/maps 00400000-00401000 r-xp 00000000 08:01 1182040 /home/user/tmp/a.out 00600000-00601000 r--p 00000000 08:01 1182040 /home/user/tmp/a.out 00601000-00602000 rw-p 00001000 08:01 1182040 /home/user/tmp/a.out 00602000-00623000 rw-p 00000000 00:00 0 [heap] 7ffff7a1a000-7ffff7bcf000 r-xp 00000000 08:01 2097169 /lib/x86_64-linux-gnu/libc-2.15.so 7ffff7bcf000-7ffff7dcf000 ---p 001b5000 08:01 2097169 /lib/x86_64-linux-gnu/libc-2.15.so 7ffff7dcf000-7ffff7dd3000 r--p 001b5000 08:01 2097169 /lib/x86_64-linux-gnu/libc-2.15.so 7ffff7dd3000-7ffff7dd5000 rw-p 001b9000 08:01 2097169 /lib/x86_64-linux-gnu/libc-2.15.so 7ffff7dd5000-7ffff7dda000 rw-p 00000000 00:00 0 7ffff7dda000-7ffff7dfc000 r-xp 00000000 08:01 2097185 /lib/x86_64-linux-gnu/ld-2.15.so 7ffff7fee000-7ffff7ff1000 rw-p 00000000 00:00 0 7ffff7ff8000-7ffff7ffa000 rw-p 00000000 00:00 0 7ffff7ffa000-7ffff7ffc000 r-xp 00000000 00:00 0 [vdso] 7ffff7ffc000-7ffff7ffd000 r--p 00022000 08:01 2097185 /lib/x86_64-linux-gnu/ld-2.15.so 7ffff7ffd000-7ffff7fff000 rw-p 00023000 08:01 2097185 /lib/x86_64-linux-gnu/ld-2.15.so 7ffffffde000-7ffffffff000 rw-p 00000000 00:00 0 [stack] ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall] (gdb) quit
ここで、gdbがASLRを無効にしていることに注意する。 上の結果から、主なアドレスをまとめると次のようになる。
- 0x400000: 実行ファイルの実行可能領域
- 0x600000: 実行ファイルの実行不可領域
- 0x602000: ヒープ領域
- 0x7ffff7a1a000: 共有ライブラリ(libc)
- 0x7ffffffde000: スタック領域
実行ファイルのメモリ配置について
実行ファイルのメモリ配置は、PIEでない場合リンク時に決まる。 readelfコマンドでセクション情報を表示させると、アドレスの項目に実際のアドレスが入っていることがわかる。
$ readelf -S a.out There are 30 section headers, starting at offset 0x1148: Section Headers: [Nr] Name Type Address Offset Size EntSize Flags Link Info Align [ 0] NULL 0000000000000000 00000000 0000000000000000 0000000000000000 0 0 0 [ 1] .interp PROGBITS 0000000000400238 00000238 000000000000001c 0000000000000000 A 0 0 1 [ 2] .note.ABI-tag NOTE 0000000000400254 00000254 0000000000000020 0000000000000000 A 0 0 4 (snip) [18] .ctors PROGBITS 0000000000600e28 00000e28 0000000000000010 0000000000000000 WA 0 0 8 [19] .dtors PROGBITS 0000000000600e38 00000e38 0000000000000010 0000000000000000 WA 0 0 8 (snip) [25] .bss NOBITS 0000000000601030 00001030 0000000000000010 0000000000000000 WA 0 0 8 (snip)
このアドレスは、リンカが使うリンカスクリプトによって決められている。 コンパイル時にリンカにverboseオプションをつけると、使われているリンカスクリプトを表示させることができる。
$ gcc -Wl,--verbose hello.c GNU ld (GNU Binutils for Ubuntu) 2.22 Supported emulations: elf_x86_64 elf32_x86_64 elf_i386 i386linux elf_l1om elf_k1om using internal linker script: ================================================== /* Script for -z combreloc: combine and sort reloc sections */ OUTPUT_FORMAT("elf64-x86-64", "elf64-x86-64", "elf64-x86-64") OUTPUT_ARCH(i386:x86-64) ENTRY(_start) SEARCH_DIR("/usr/x86_64-linux-gnu/lib64"); SEARCH_DIR("=/usr/local/lib/x86_64-linux-gnu"); SEARCH_DIR("=/usr/local/lib64"); SEARCH_DIR("=/lib/x86_64-linux-gnu"); SEARCH_DIR("=/lib64"); SEARCH_DIR(" =/usr/lib/x86_64-linux-gnu"); SEARCH_DIR("=/usr/lib64"); SEARCH_DIR("=/usr/local/lib"); SEARCH_DIR("=/lib"); SEARCH_DIR("=/usr/lib"); SECTIONS { /* Read-only sections, merged into text segment: */ PROVIDE (__executable_start = SEGMENT_START("text-segment", 0x400000)); . = SEGMENT_START("text-segment", 0x400000) + SIZEOF_HEADERS; .interp : { *(.interp) } .note.gnu.build-id : { *(.note.gnu.build-id) } .hash : { *(.hash) } .gnu.hash : { *(.gnu.hash) } .dynsym : { *(.dynsym) } .dynstr : { *(.dynstr) } (snip) /* Adjust the address for the data segment. We want to adjust up to the same address within the page on the next page up. */ . = ALIGN (CONSTANT (MAXPAGESIZE)) - ((CONSTANT (MAXPAGESIZE) - .) & (CONSTANT (MAXPAGESIZE) - 1)); . = DATA_SEGMENT_ALIGN (CONSTANT (MAXPAGESIZE), CONSTANT (COMMONPAGESIZE)); (snip) } ================================================== attempt to open /usr/lib/gcc/x86_64-linux-gnu/4.6/../../../x86_64-linux-gnu/crt1.o succeeded /usr/lib/gcc/x86_64-linux-gnu/4.6/../../../x86_64-linux-gnu/crt1.o attempt to open /usr/lib/gcc/x86_64-linux-gnu/4.6/../../../x86_64-linux-gnu/crti.o succeeded /usr/lib/gcc/x86_64-linux-gnu/4.6/../../../x86_64-linux-gnu/crti.o (snip) attempt to open /usr/lib/gcc/x86_64-linux-gnu/4.6/../../../x86_64-linux-gnu/crtn.o succeeded /usr/lib/gcc/x86_64-linux-gnu/4.6/../../../x86_64-linux-gnu/crtn.o ld-linux-x86-64.so.2 needed by /lib/x86_64-linux-gnu/libc.so.6 found ld-linux-x86-64.so.2 at /lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
リンカスクリプトを見ると、read-onlyセクションがtextセクションとして0x400000から配置されていることがわかる。 また、データセグメントは次のメモリページとなるように調整されていることがわかる。
対応するソースコードは、GNU binutilsのldディレクトリにある。 リンカスクリプトのELF用テンプレートは次のファイルである。
テンプレート中で参照されているTEXT_START_ADDR
は次のファイルで定義されている。
TEXT_START_ADDR=0x400000 MAXPAGESIZE="CONSTANT (MAXPAGESIZE)" COMMONPAGESIZE="CONSTANT (COMMONPAGESIZE)"
ここで、CONSTANT (MAXPAGESIZE)
、CONSTANT (COMMONPAGESIZE)
の値には、次のファイルの定義が参照される。
#define ELF_MAXPAGESIZE 0x200000 #define ELF_MINPAGESIZE 0x1000 #define ELF_COMMONPAGESIZE 0x1000
上の定義においてELF_MAXPAGESIZE
が0x200000であるため、データセグメントは0x400000+0x200000=0x600000から始まることとなる。
なお、これらの値はリンカオプションにより変更することも可能である。
$ gcc -Wl,-Ttext-segment=0x8048000,-z,max-page-size=0x1000 hello.c $ readelf -S a.out There are 30 section headers, starting at offset 0x1158: Section Headers: [Nr] Name Type Address Offset Size EntSize Flags Link Info Align [ 0] NULL 0000000000000000 00000000 0000000000000000 0000000000000000 0 0 0 [ 1] .interp PROGBITS 0000000008048238 00000238 000000000000001c 0000000000000000 A 0 0 1 [ 2] .note.ABI-tag NOTE 0000000008048254 00000254 0000000000000020 0000000000000000 A 0 0 4 ... [18] .ctors PROGBITS 0000000008049e28 00000e28 0000000000000010 0000000000000000 WA 0 0 8 [19] .dtors PROGBITS 0000000008049e38 00000e38 0000000000000010 0000000000000000 WA 0 0 8 ...
共有ライブラリのメモリ配置について
共有ライブラリは、実行ファイルで指定されたELFインタプリタ(/lib/ld-linux.so.2)がmmapにより配置する。 具体的には、次のような順序で関数が呼ばれていく。
[sysdeps/x86_64/dl-machine.h] _start [elf/rtld.c] _dl_start [elf/rtld.c] _dl_start_final [elf/dl-sysdep.c] _dl_sysdep_start [elf/rtld.c] dl_main [elf/dl-deps.c] _dl_map_object_deps [include/dlfcn.h] _dl_catch_error [elf/dl-deps.c] openaux [elf/dl-load.c] _dl_map_object [elf/dl-load.c] _dl_map_object_from_fd
setarchコマンドでASLRを無効化した上で、straceコマンドを使い実行時のシステムコールをトレースしてみる。
$ setarch x86_64 -R strace ./a.out >/dev/null execve("./a.out", ["./a.out"], [/* 18 vars */]) = 0 brk(0) = 0x1402000 access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory) mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7ffff7ff8000 access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory) open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3 fstat(3, {st_mode=S_IFREG|0644, st_size=26780, ...}) = 0 mmap(NULL, 26780, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7ffff7ff1000 close(3) = 0 access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory) open("/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3 read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\200\30\2\0\0\0\0\0"..., 832) = 832 fstat(3, {st_mode=S_IFREG|0755, st_size=1815224, ...}) = 0 mmap(NULL, 3929304, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7ffff7a1a000 mprotect(0x7ffff7bcf000, 2097152, PROT_NONE) = 0 mmap(0x7ffff7dcf000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1b5000) = 0x7ffff7dcf000 mmap(0x7ffff7dd5000, 17624, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7ffff7dd5000 close(3) = 0 ... write(1, "Hello, world!\n", 14) = 14 exit_group(0) = ?
上の結果から、libcのメモリを確保する際におけるmmapの第一引数はNULLとなっており、実際に配置されるアドレス0x7ffff7a1a000はmmapが決めていることがわかる。
mmapが確保するメモリアドレスの初期値は、LinuxカーネルがELF実行ファイルを読み込む際に決められる。 具体的には、次のような順序で関数が呼ばれていく。
[fs/binfmt_elf.c] load_elf_binary [fs/exec.c] setup_new_exec [arch/x86/mm/mmap.c] arch_pick_mmap_layout [arch/x86/mm/mmap.c] mmap_base
ここで、mmap_base関数は次のようになっている。
54 #define MIN_GAP (128*1024*1024UL + stack_maxrandom_size()) 55 #define MAX_GAP (TASK_SIZE/6*5) ... 85 static unsigned long mmap_base(void) 86 { 87 unsigned long gap = rlimit(RLIMIT_STACK); 88 89 if (gap < MIN_GAP) 90 gap = MIN_GAP; 91 else if (gap > MAX_GAP) 92 gap = MAX_GAP; 93 94 return PAGE_ALIGN(TASK_SIZE - gap - mmap_rnd()); 95 }
ASLRが無効の場合、stack_maxrandom_size()
かつmmap_rnd() == 0
となる。
ここで、rlimit(RLIMIT_STACK)
の値をulimitコマンドで調べてみると次のようになる。
$ ulimit -s 8192
ulimitコマンドはキロバイト単位、rlimit(RLIMIT_STACK)
はバイト単位であるため、この場合gap == 8192*1024
となる。
これはMIN_GAP
よりも小さいため、gapはMIN_GAP
に補正される。
また、TASK_SIZE
に関する定義をまとめると次のようになる。
- Linux/arch/x86/include/asm/processor.h
- Linux/arch/x86/include/asm/page_types.h
- Linux/include/uapi/linux/const.h
833 #ifdef CONFIG_X86_32 ... 893 #else 894 /* 895 * User space process size. 47bits minus one guard page. 896 */ 897 #define TASK_SIZE_MAX ((1UL << 47) - PAGE_SIZE) 898 899 /* This decides where the kernel will search for a free chunk of vm 900 * space during mmap's. 901 */ 902 #define IA32_PAGE_OFFSET ((current->personality & ADDR_LIMIT_3GB) ? \ 903 0xc0000000 : 0xFFFFe000) 904 905 #define TASK_SIZE (test_thread_flag(TIF_ADDR32) ? \ 906 IA32_PAGE_OFFSET : TASK_SIZE_MAX) 907 #define TASK_SIZE_OF(child) ((test_tsk_thread_flag(child, TIF_ADDR32)) ? \ 908 IA32_PAGE_OFFSET : TASK_SIZE_MAX) 909 910 #define STACK_TOP TASK_SIZE 911 #define STACK_TOP_MAX TASK_SIZE_MAX ... 935 #endif /* CONFIG_X86_64 */
7 /* PAGE_SHIFT determines the page size */ 8 #define PAGE_SHIFT 12 9 #define PAGE_SIZE (_AC(1,UL) << PAGE_SHIFT) 10 #define PAGE_MASK (~(PAGE_SIZE-1))
19 #define __AC(X,Y) (X##Y) 20 #define _AC(X,Y) __AC(X,Y)
以上をもとに、mmap_baseが返すアドレスを計算してみる。
$ python Python 2.7.3 (default, Feb 27 2014, 19:58:35) [GCC 4.6.3] on linux2 Type "help", "copyright", "credits" or "license" for more information. >>> PAGE_SHIFT = 12 >>> PAGE_SIZE = 1L << PAGE_SHIFT >>> TASK_SIZE_MAX = (1L << 47) - PAGE_SIZE >>> TASK_SIZE = TASK_SIZE_MAX >>> MIN_GAP = 128*1024*1024L >>> hex(TASK_SIZE) '0x7ffffffff000L' >>> hex(TASK_SIZE - MIN_GAP) '0x7ffff7fff000L'
これは、一番最初にmmapで配置されるld-linux.soのメモリ領域の底となっている。
スタック領域のメモリ配置について
スタック領域のメモリ配置も、LinuxカーネルがELF実行ファイルを読み込む際に決められる。 具体的には、binfmt_elf.cのload_elf_binary関数にある次のコードが対応する。
737 /* Do this so that we can load the interpreter, if need be. We will 738 change some of these later */ 739 retval = setup_arg_pages(bprm, randomize_stack_top(STACK_TOP), 740 executable_stack); 741 if (retval < 0) { 742 send_sig(SIGKILL, current, 0); 743 goto out_free_dentry; 744 } 745 746 current->mm->start_stack = bprm->p;
ASLRが無効な場合、randomize_stack_top(STACK_TOP) == STACK_TOP
となる。
共有ライブラリのパートで示した定義を参照すると、STACK_TOP == TASK_SIZE == 0x7ffffffff000
であることがわかる。
これはスタック領域の底となっている。
この後は、setup_arg_pages関数にある次のコードによりスタックサイズが決められ、その分の領域が確保される。
723 stack_expand = 131072UL; /* randomly 32*4k (or 2*64k) pages */ 724 stack_size = vma->vm_end - vma->vm_start; 725 /* 726 * Align this down to a page boundary as expand_stack 727 * will align it up. 728 */ 729 rlim_stack = rlimit(RLIMIT_STACK) & PAGE_MASK; 730 #ifdef CONFIG_STACK_GROWSUP 731 if (stack_size + stack_expand > rlim_stack) 732 stack_base = vma->vm_start + rlim_stack; 733 else 734 stack_base = vma->vm_end + stack_expand; 735 #else 736 if (stack_size + stack_expand > rlim_stack) 737 stack_base = vma->vm_end - rlim_stack; 738 else 739 stack_base = vma->vm_start - stack_expand; 740 #endif 741 current->mm->start_stack = bprm->p; 742 ret = expand_stack(vma, stack_base);
ここではstack_expand = 131072UL == 0x20000UL
かつrlim_stack == 0x800000
であるから、stack_base == vma->vm_start - stack_expand
となる。
これにguard pageとして確保される領域のサイズ0x1000をさらに引くと0x7ffffffde000となり、確認したスタック領域のアドレスと一致する。
ヒープ領域のメモリ配置について
ヒープ領域のベースアドレスは、基本的にはbssセグメントのあるページの次のページとなる。 これは、ヒープ領域がbrkシステムコールを使って確保されるためである。 brkシステムコールはデータセグメントの境界を変更するものであり、ヒープ領域はこの境界を拡張する形で確保される。
ヒープ領域の確保は、初めてmalloc関数が呼ばれたタイミングで行われる。 具体的には、mallocのエイリアスであるpublic_mALLOcから、_int_malloc、sYSMALLOcの順の進み、次のコードでメモリ領域が確保されることになる。
/* Request enough space for nb + pad + overhead */ size = nb + mp_.top_pad + MINSIZE; /* If contiguous, we can subtract out existing space that we hope to combine with new space. We add it back later only if we don't actually get contiguous space. */ if (contiguous(av)) size -= old_size; /* Round to a multiple of page size. If MORECORE is not contiguous, this ensures that we only call it with whole-page arguments. And if MORECORE is contiguous and this is not first time through, this preserves page-alignment of previous calls. Otherwise, we correct to page-align below. */ size = (size + pagemask) & ~pagemask; /* Don't try to call MORECORE if argument is so big as to appear negative. Note that since mmap takes size_t arg, it may succeed below even if we cannot call MORECORE. */ if (size > 0) brk = (char*)(MORECORE(size));
ここで、nb + mp_.top_pad + MINSIZE
は「mallocで要求されたサイズ+領域を取得する際のベースサイズ+malloc_chunkの最小サイズ」を意味する。
最初に書いたコードではnb == 20
であり、malloc_chunkの定義からMINSIZE == 32
である。
そして、mp_.top_pad
のデフォルト値はDEFAULT_TOP_PAD
であり、これは次のように定義されている。
#ifndef DEFAULT_TOP_PAD # define DEFAULT_TOP_PAD 131072 #endif
したがって、size == 20 + 131072 + 32 == 0x20034
となり、ページサイズの倍数に揃えられることでこれは0x21000となる。
メモリの確保はMORECORE(size)
により行われるが、これに関する定義をまとめると次のようになる。
#define MORECORE (*__morecore) void *(*__morecore)(ptrdiff_t) = __default_morecore;
/* Allocate INCREMENT more bytes of data space, and return the start of data space, or NULL on errors. If INCREMENT is negative, shrink data space. */ __malloc_ptr_t __default_morecore (increment) __malloc_ptrdiff_t increment; { __malloc_ptr_t result = (__malloc_ptr_t) __sbrk (increment); if (result == (__malloc_ptr_t) -1) return NULL; return result; } libc_hidden_def (__default_morecore)
/* Extend the process's data space by INCREMENT. If INCREMENT is negative, shrink data space by - INCREMENT. Return start of new space allocated, or -1 for errors. */ void * __sbrk (intptr_t increment) { void *oldbrk; /* If this is not part of the dynamic library or the library is used via dynamic loading in a statically linked program update __curbrk from the kernel's brk value. That way two separate instances of __brk and __sbrk can share the heap, returning interleaved pieces of it. */ if (__curbrk == NULL || __libc_multiple_libcs) if (__brk (0) < 0) /* Initialize the break. */ return (void *) -1; if (increment == 0) return __curbrk; oldbrk = __curbrk; if ((increment > 0 ? ((uintptr_t) oldbrk + (uintptr_t) increment < (uintptr_t) oldbrk) : ((uintptr_t) oldbrk < (uintptr_t) -increment)) || __brk (oldbrk + increment) < 0) return (void *) -1; return oldbrk; } libc_hidden_def (__sbrk) weak_alias (__sbrk, sbrk)
つまり、sbrk関数を経由してbrkシステムコールが呼ばれることになる。 brkシステムコールは変更後の境界のアドレスを引数に取る。 一方、sbrk関数は増分を引数に取り、brkシステムコールを2度呼ぶことにより境界のアドレスを増減させる関数である。
setarchコマンドでASLRを無効にした上で、straceコマンドにより実行時のシステムコールをトレースしてみる。
$ setarch x86_64 -R strace ./a.out >/dev/null execve("./a.out", ["./a.out"], [/* 18 vars */]) = 0 ... brk(0) = 0x602000 brk(0x623000) = 0x623000 fstat(1, {st_mode=S_IFCHR|0666, st_rdev=makedev(1, 3), ...}) = 0 ioctl(1, SNDCTL_TMR_TIMEBASE or TCGETS, 0x7fffffffe478) = -1 ENOTTY (Inappropriate ioctl for device) mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7ffff7ff7000 write(1, "Hello, world!\n", 14) = 14 exit_group(0) = ?
上の結果から、mallocが呼ばれるタイミングでbrkシステムコールが2度呼ばれ、0x21000だけヒープ領域が確保されていることがわかる。 具体的には、一度目のbrkで現在の境界アドレスを取得し、二度目のbrkで増減後の境界アドレスがセットされている。
ASLRによるアドレスのランダム化
基本的に、binfmt_elf.cのload_elf_binary関数でELFファイルが読み込まれる際に行われる。
PIEの場合の実行ファイル
load_elf_binary関数の次の箇所が関係する。
795 vaddr = elf_ppnt->p_vaddr; 796 if (loc->elf_ex.e_type == ET_EXEC || load_addr_set) { 797 elf_flags |= MAP_FIXED; 798 } else if (loc->elf_ex.e_type == ET_DYN) { 799 /* Try and get dynamic programs out of the way of the 800 * default mmap base, as well as whatever program they 801 * might try to exec. This is because the brk will 802 * follow the loader, and is not movable. */ 803 #ifdef CONFIG_ARCH_BINFMT_ELF_RANDOMIZE_PIE 804 /* Memory randomization might have been switched off 805 * in runtime via sysctl or explicit setting of 806 * personality flags. 807 * If that is the case, retain the original non-zero 808 * load_bias value in order to establish proper 809 * non-randomized mappings. 810 */ 811 if (current->flags & PF_RANDOMIZE) 812 load_bias = 0; 813 else 814 load_bias = ELF_PAGESTART(ELF_ET_DYN_BASE - vaddr); 815 #else 816 load_bias = ELF_PAGESTART(ELF_ET_DYN_BASE - vaddr); 817 #endif 818 } 819 820 error = elf_map(bprm->file, load_bias + vaddr, elf_ppnt, 821 elf_prot, elf_flags, 0); 822 if (BAD_ADDR(error)) { 823 send_sig(SIGKILL, current, 0); 824 retval = IS_ERR((void *)error) ? 825 PTR_ERR((void*)error) : -EINVAL; 826 goto out_free_dentry; 827 }
これはELFのプログラムヘッダでタイプがPT_LOADとなっているエントリを読み込む箇所である。
PIEの場合ELF自身のタイプはET_DYNとなるため、load_bias
は0となる。
そして、次に示すelf_map関数から、mmapによってメモリ領域が確保される。
335 static unsigned long elf_map(struct file *filep, unsigned long addr, 336 struct elf_phdr *eppnt, int prot, int type, 337 unsigned long total_size) 338 { 339 unsigned long map_addr; 340 unsigned long size = eppnt->p_filesz + ELF_PAGEOFFSET(eppnt->p_vaddr); 341 unsigned long off = eppnt->p_offset - ELF_PAGEOFFSET(eppnt->p_vaddr); 342 addr = ELF_PAGESTART(addr); 343 size = ELF_PAGEALIGN(size); 344 345 /* mmap() will return -EINVAL if given a zero size, but a 346 * segment with zero filesize is perfectly valid */ 347 if (!size) 348 return addr; 349 350 /* 351 * total_size is the size of the ELF (interpreter) image. 352 * The _first_ mmap needs to know the full size, otherwise 353 * randomization might put this image into an overlapping 354 * position with the ELF binary image. (since size < total_size) 355 * So we first map the 'big' image - and unmap the remainder at 356 * the end. (which unmap is needed for ELF images with holes.) 357 */ 358 if (total_size) { 359 total_size = ELF_PAGEALIGN(total_size); 360 map_addr = vm_mmap(filep, addr, total_size, prot, type, off); 361 if (!BAD_ADDR(map_addr)) 362 vm_munmap(map_addr+size, total_size-size); 363 } else 364 map_addr = vm_mmap(filep, addr, size, prot, type, off); 365 366 return(map_addr); 367 }
ここで、PIEでコンパイルした実行ファイルのプログラムヘッダを調べてみる。
$ gcc -fPIE -pie hello.c $ readelf -l a.out Elf file type is DYN (Shared object file) Entry point 0x6c0 There are 9 program headers, starting at offset 64 Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flags Align PHDR 0x0000000000000040 0x0000000000000040 0x0000000000000040 0x00000000000001f8 0x00000000000001f8 R E 8 INTERP 0x0000000000000238 0x0000000000000238 0x0000000000000238 0x000000000000001c 0x000000000000001c R 1 [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2] LOAD 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x00000000000009cc 0x00000000000009cc R E 200000 LOAD 0x0000000000000e00 0x0000000000200e00 0x0000000000200e00 0x0000000000000238 0x0000000000000248 RW 200000 DYNAMIC 0x0000000000000e28 0x0000000000200e28 0x0000000000200e28 0x0000000000000190 0x0000000000000190 RW 8 NOTE 0x0000000000000254 0x0000000000000254 0x0000000000000254 0x0000000000000044 0x0000000000000044 R 4 GNU_EH_FRAME 0x00000000000008fc 0x00000000000008fc 0x00000000000008fc 0x000000000000002c 0x000000000000002c R 4 GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 RW 8 GNU_RELRO 0x0000000000000e00 0x0000000000200e00 0x0000000000200e00 0x0000000000000200 0x0000000000000200 R 1 Section to Segment mapping: Segment Sections... 00 01 .interp 02 .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt .init .plt .text .fini .rodata .eh_frame_hdr .eh_frame 03 .ctors .dtors .jcr .dynamic .got .got.plt .data .bss 04 .dynamic 05 .note.ABI-tag .note.gnu.build-id 06 .eh_frame_hdr 07 08 .ctors .dtors .jcr .dynamic .got
LOAD
となっているエントリは二つあり、それぞれ実行可能領域、実行不可領域に対応している。
また、VirtAddr (vaddr)、FileSiz (size) はそれぞれ0、0x9ccおよび0x200e00、0x238となっている。
したがって、ページ境界へのアラインメントも考慮すると、まずaddr = mmap(0, 0x1000, ...)
に相当する処理が呼ばれることになる。
この後、load_addr_set = 1
およびload_bias = addr
が行われるため、次のPT_LOADエントリについてはelf_flagsにMAP_FIXEDがセットされた上でmmap(load_bias + 0x200000, 0x1000, ...)
が呼ばれる。
よって、ランダム化が行われるbit数はmmapで確保される場合のbit数と同じである。
具体的なbit数については次で説明する。
共有ライブラリなどmmapで確保される領域
load_elf_binary関数から、setup_new_exec関数、arch_pick_mmap_layout関数、mmap_base関数の順に進みランダム化される。 32bit環境の場合8bit、64bit環境の場合28bit。
735 setup_new_exec(bprm);
1103 arch_pick_mmap_layout(current->mm);
68 static unsigned long mmap_rnd(void) 69 { 70 unsigned long rnd = 0; 71 72 /* 73 * 8 bits of randomness in 32bit mmaps, 20 address space bits 74 * 28 bits of randomness in 64bit mmaps, 40 address space bits 75 */ 76 if (current->flags & PF_RANDOMIZE) { 77 if (mmap_is_ia32()) 78 rnd = get_random_int() % (1<<8); 79 else 80 rnd = get_random_int() % (1<<28); 81 } 82 return rnd << PAGE_SHIFT; 83 } 84 85 static unsigned long mmap_base(void) 86 { 87 unsigned long gap = rlimit(RLIMIT_STACK); 88 89 if (gap < MIN_GAP) 90 gap = MIN_GAP; 91 else if (gap > MAX_GAP) 92 gap = MAX_GAP; 93 94 return PAGE_ALIGN(TASK_SIZE - gap - mmap_rnd()); 95 } ... 109 /* 110 * This function, called very early during the creation of a new 111 * process VM image, sets up which VM layout function to use: 112 */ 113 void arch_pick_mmap_layout(struct mm_struct *mm) 114 { 115 mm->mmap_legacy_base = mmap_legacy_base(); 116 mm->mmap_base = mmap_base(); 117 118 if (mmap_is_legacy()) { 119 mm->mmap_base = mm->mmap_legacy_base; 120 mm->get_unmapped_area = arch_get_unmapped_area; 121 } else { 122 mm->get_unmapped_area = arch_get_unmapped_area_topdown; 123 } 124 }
スタック領域
load_elf_binary関数において、randomize_stack_top関数によりページ単位でランダム化が行われる。 ただし、ランダム値がunsigned intすなわち32bit符号なし整数として定義されているため、ランダム化の上限は20bitとなる。 さらに、setup_arg_pages関数内で、arch_align_stack関数によりランダム化が行われるが、ページ境界へのアラインメントが行われるため全体には影響しない。 32bit環境の場合11bit、64bit環境の場合20bit。
- Linux/fs/binfmt_elf.c
- Linux/fs/exec.c
- Linux/arch/x86/kernel/process.c
- Linux/arch/x86/include/asm/elf.h
- Linux/arch/x86/include/asm/page_types.h
- Linux/include/linux/mm.h
- Linux/include/linux/kernel.h
- Linux/include/uapi/linux/kernel.h
555 static unsigned long randomize_stack_top(unsigned long stack_top) 556 { 557 unsigned int random_variable = 0; 558 559 if ((current->flags & PF_RANDOMIZE) && 560 !(current->personality & ADDR_NO_RANDOMIZE)) { 561 random_variable = get_random_int() & STACK_RND_MASK; 562 random_variable <<= PAGE_SHIFT; 563 } 564 #ifdef CONFIG_STACK_GROWSUP 565 return PAGE_ALIGN(stack_top) + random_variable; 566 #else 567 return PAGE_ALIGN(stack_top) - random_variable; 568 #endif 569 } 570 571 static int load_elf_binary(struct linux_binprm *bprm) 572 { ... 737 /* Do this so that we can load the interpreter, if need be. We will 738 change some of these later */ 739 retval = setup_arg_pages(bprm, randomize_stack_top(STACK_TOP), 740 executable_stack); 741 if (retval < 0) { 742 send_sig(SIGKILL, current, 0); 743 goto out_free_dentry; 744 } 745 746 current->mm->start_stack = bprm->p; ... 1007 }
675 stack_top = arch_align_stack(stack_top); 676 stack_top = PAGE_ALIGN(stack_top); 677 678 if (unlikely(stack_top < mmap_min_addr) || 679 unlikely(vma->vm_end - vma->vm_start >= stack_top - mmap_min_addr)) 680 return -ENOMEM; 681 682 stack_shift = vma->vm_end - stack_top; 683 684 bprm->p -= stack_shift; 685 mm->arg_start = bprm->p;
456 unsigned long arch_align_stack(unsigned long sp) 457 { 458 if (!(current->personality & ADDR_NO_RANDOMIZE) && randomize_va_space) 459 sp -= get_random_int() % 8192; 460 return sp & ~0xf; 461 }
290 /* 1GB for 64bit, 8MB for 32bit */ 291 #define STACK_RND_MASK (test_thread_flag(TIF_ADDR32) ? 0x7ff : 0x3fffff)
8 #define PAGE_SHIFT 12 9 #define PAGE_SIZE (_AC(1,UL) << PAGE_SHIFT)
73 #define PAGE_ALIGN(addr) ALIGN(addr, PAGE_SIZE)
49 #define ALIGN(x, a) __ALIGN_KERNEL((x), (a))
9 #define __ALIGN_KERNEL(x, a) __ALIGN_KERNEL_MASK(x, (typeof(x))(a) - 1) 10 #define __ALIGN_KERNEL_MASK(x, mask) (((x) + (mask)) & ~(mask))
ヒープ領域
load_elf_binary関数において、arch_randomize_brk関数によってランダム化される。 32bit環境、64bit環境どちらの場合も13bit。
957 #ifdef arch_randomize_brk 958 if ((current->flags & PF_RANDOMIZE) && (randomize_va_space > 1)) { 959 current->mm->brk = current->mm->start_brk = 960 arch_randomize_brk(current->mm); 961 #ifdef CONFIG_COMPAT_BRK 962 current->brk_randomized = 1; 963 #endif 964 } 965 #endif
463 unsigned long arch_randomize_brk(struct mm_struct *mm) 464 { 465 unsigned long range_end = mm->brk + 0x02000000; 466 return randomize_range(mm->brk, range_end, 0) ? : mm->brk; 467 }
1691 /* 1692 * randomize_range() returns a start address such that 1693 * 1694 * [...... <range> .....] 1695 * start end 1696 * 1697 * a <range> with size "len" starting at the return value is inside in the 1698 * area defined by [start, end], but is otherwise randomized. 1699 */ 1700 unsigned long 1701 randomize_range(unsigned long start, unsigned long end, unsigned long len) 1702 { 1703 unsigned long range = end - len - start; 1704 1705 if (end <= start + len) 1706 return 0; 1707 return PAGE_ALIGN(get_random_int() % range + start); 1708 }
ランダム化bit数のまとめ
PIE実行ファイル 共有ライブラリ | スタック領域 | ヒープ領域 | |
---|---|---|---|
32bit | 8bit | 11bit | 13bit |
64bit | 28bit | 20bit | 13bit |
関連リンク
- H. J. Lu - PATCH: Getting MAXPAGESIZE from ELF_MAXPAGESIZE
- プログラムはどう動くのか? ~ ELFの黒魔術をかいまみる
- malloc(3)のメモリ管理構造 | VA Linux Systems Japan株式会社
- ASLR, setarch -RL, prelink, PIE and LD_USE_LOAD_BIAS - memologue
- Linux kernel ASLR Implementation | xorl %eax, %eax
- Stack Smashing as of Today (Black Hat Europe 2009)