__PAGEZEROセグメントでヌルぽ体験
前回、Mach-Oのセグメント定義をロードしたときに__PAGEZEROセグメントと言うのが出てきた。興味を引かれたのでもうちょっと触ってみる。環境はx86_64。
__PAGEZEROセグメントに関して、アップルのリファレンスにはこう書いてある。
__PAGEZEROセグメントは実行形式ファイルの最初のセグメントとして静的リンカによって作成される。このセグメントは仮想メモリアドレス 0 に配置され、メモリ保護属性は全て禁止と設定される。このために、大抵の場合はプログラミングのミスである、NULL へのアクセスが発生するとプログラムは直ちに異常終了することになる。
__PAGEZEROセグメントのサイズは現在のアーキテクチャでの仮想メモリページの1ページ分である。(Intel, PowerPC系ならば 4096 バイト) __PAGEZEROにはデータが含まれないため、ファイル上でのサイズは 0 となる(segment command の filesize が 0)。
つまり、OS X のC言語では NULL は 0 であり、NULLポインタ経由でデータやコードにアクセスすると、仮想メモリアドレス 0 の位置にアクセスすることになり、でもその場所は読み書き実行全て禁止されているのでメモリ保護違反でプロセスは強制終了させられますよー。ということかな。
セグメントが配置される仮想メモリの保護属性は segment_command_64 構造体の以下のメンバで表される。
vm_prot_t maxprot; /* maximum VM protection */ vm_prot_t initprot; /* initial VM protection */
ビットごとに保護の意味が定義されていて、 mach/vm_prot.h で確認できる。
(抜粋。ほんとはもっとコメントがいっぱい) #define VM_PROT_NONE ((vm_prot_t) 0x00) #define VM_PROT_READ ((vm_prot_t) 0x01) /* read permission */ #define VM_PROT_WRITE ((vm_prot_t) 0x02) /* write permission */ #define VM_PROT_EXECUTE ((vm_prot_t) 0x04) /* execute permission */ #define VM_PROT_DEFAULT (VM_PROT_READ|VM_PROT_WRITE) #define VM_PROT_ALL (VM_PROT_READ|VM_PROT_WRITE|VM_PROT_EXECUTE) #define VM_PROT_NO_CHANGE ((vm_prot_t) 0x08) #define VM_PROT_COPY ((vm_prot_t) 0x10) #define VM_PROT_WANTS_COPY ((vm_prot_t) 0x10)
実験
次の事を確認するために実験してみる
- 実行形式ファイルの__PAGEZERO セグメントのメモリ保護属性は 「全て禁止」になっているか
- 仮想メモリアドレス 0 にアクセスするとプログラムは異常終了するか
- メモリ保護属性を 「全て許可」 に変更すると 仮想メモリアドレス 0 にアクセスしても異常終了しなくなるか
次のようなコードで実験してみる。(zero.s)
01: .text 02: .globl start 03: start: 04: movq $0, %rax # NULL 05: movq hoge(%rip), %rdx # prepare data 06: movq %rdx, (%rax) # write to *NULL 07: xorq %rdx, %rdx 08: movq (%rax), %rdx # load from *NULL 09: 10: movq $0x2000001, %rax 11: movq hoge(%rip), %rdi 12: syscall 13: .data 14: hoge: 15: .word 0x1234
ところで、メモリアクセスにはセグメント機構というものを経由するはずだが、このコードで仮想メモリアドレス 0 にアクセスすることになるのかな??
なります。x86_64ではフラットメモリモデルなので全てのセグメントのベースは 0。つまりリニアアドレス=実効アドレス。(注: ここで言うセグメントはMach-Oのセグメントとは別物。GDTで定義されるセグメントのこと。)
(Intel 64 and IA-32 Architectures Software Developer’s Manual Volume 1参照)
$ as -o zero.o zero.s && ld -o zero zero.o
__PAGEZERO セグメントのメモリ保護属性を確認
$ otool -l zero zero: Load command 0 cmd LC_SEGMENT_64 cmdsize 72 segname __PAGEZERO vmaddr 0x0000000000000000 vmsize 0x0000000000001000 fileoff 0 filesize 0 maxprot 0x00000000 initprot 0x00000000 nsects 0 flags 0x0 (略)
確かにmaxprot, initprotは全てのフラグがクリアされていて、全てのアクセスが禁止となっている。後でこの値を (VM_PROT_READ | VM_PROT_WRITE | VM_PROT_EXECUTE) (=7) に変えてやると「全て許可」の場合の実験が出来そうだな。
実行
$ ./zero Segmentation fault
gdbで実行
(gdb) disas start Dump of assembler code for function start: 0x0000000000001fd9: mov $0x0,%rax 0x0000000000001fe0 : mov 0x19(%rip),%rdx # 0x2000 0x0000000000001fe7 : mov %rdx,(%rax) 0x0000000000001fea : xor %rdx,%rdx 0x0000000000001fed : mov (%rax),%rdx 0x0000000000001ff0 : mov $0x2000001,%rax 0x0000000000001ff7 : mov 0x2(%rip),%rdi # 0x2000 0x0000000000001ffe : syscall End of assembler dump. (gdb) run Starting program: zero Program received signal EXC_BAD_ACCESS, Could not access memory. Reason: KERN_INVALID_ADDRESS at address: 0x0000000000000000 0x0000000000001fe7 in start () (gdb)
確かに0x1fe7で仮想メモリアドレス 0 に書き込みに行くところで 不正なアクセス として終了させられている。
Mach-Oファイル中の__PAGEZEROセグメントのメモリ保護属性を「全て許可(=7)」に変更する。
ハイライト部分が maxprot と initprot (各4バイト)
これを↓こう変更する。
otoolで確認
$ otool -l zero zero: Load command 0 cmd LC_SEGMENT_64 cmdsize 72 segname __PAGEZERO vmaddr 0x0000000000000000 vmsize 0x0000000000001000 fileoff 0 filesize 0 maxprot 0x00000007 initprot 0x00000007 nsects 0 flags 0x0
maxprot, initprot共に「全て許可(=7)」になっている。
実行
$ .zero $ echo $? 52
異常終了しなくなった!!!!!!!!!! 終了コードの52というのは 0x1234 の下位バイト 0x34 を10進数にしたものだな。
gdbでも実行
01: (gdb) disas start 02: Dump of assembler code for function start: 03: 0x0000000000001fd9: mov $0x0,%rax 04: 0x0000000000001fe0 : mov 0x19(%rip),%rdx # 0x2000 05: 0x0000000000001fe7 : mov %rdx,(%rax) 06: 0x0000000000001fea : xor %rdx,%rdx 07: 0x0000000000001fed : mov (%rax),%rdx 08: 0x0000000000001ff0 : mov $0x2000001,%rax 09: 0x0000000000001ff7 : mov 0x2(%rip),%rdi # 0x2000 10: 0x0000000000001ffe : syscall 11: End of assembler dump. 12: (gdb) b *0x1fe7 13: Breakpoint 1 at 0x1fe7 14: (gdb) run 15: Starting program: zero 16: 17: Breakpoint 1, 0x0000000000001fe7 in start () 18: (gdb) i reg rdx rax 19: rdx 0x1234 4660 20: rax 0x0 0 21: (gdb) si 22: 0x0000000000001fea in start () 23: (gdb) si 24: 0x0000000000001fed in start () 25: (gdb) i reg rdx rax 26: rdx 0x0 0 27: rax 0x0 0 28: (gdb) si 29: 0x0000000000001ff0 in start () 30: (gdb) i reg rdx rax 31: rdx 0x1234 4660 32: rax 0x0 0 33: (gdb) c 34: Program exited with code 064. 35: (gdb)
- 17行目: さっき落ちた 0x1fe7 でブレーク
- 18〜20行目: 書き込む値は 0x1234, 書き込むアドレスは 0x0
- 21行目: 書き込み実行
- 23行目: 読み込みに備えて rdx を 0 にクリア
- 25行目〜27行目: 読み込み先のレジスタ(rdx)は値0, 読み込み元のアドレスは 0
- 28行目: 読み込み実行
- 30〜32行目: 先程アドレス0の位置に書き込んだ値(0x1234)がrdxに読み込まれている。
- 34行目: 終了コードがなぜ064・・・・。
おおお。仮想メモリアドレス 0 の場所に読み書きできてる!!!
Mach-Oファイルのメモリ保護属性を変更すれば__PAGEZEROセグメントへの読み書きが可能で有ることがわかったので、次はコードが実行できるかどうかを試す。
レジスタの値しか参照しないポータブルな関数を用意して、そのコードを__PAGEZEROセグメントにコピーし、絶対アドレス指定のcall命令により仮想メモリアドレス 0 に有るコードを実行してみる。
ソース(zero_ex.s)
01: .text 02: .globl start 03: start: 04: movq $0, %rdi # copy to 05: leaq func(%rip), %rsi # copy from 06: movq func_size(%rip), %rcx # size in bytes 07: cld # clear direction flag 08: rep movsb # copyyyyyyy 09: 10: movq $0x1, %rdi # prepare arguments 11: movq $0x2, %rsi 12: 13: movq $0, %rdx # call func at address 0 14: call *%rdx # absolute address call 15: 16: movq $0x2000001, %rax # sys exit 17: xorq %rdi, %rdi # exit code 18: syscall 19: jmp . # infinite loop 20: func: 21: addq %rdi, %rsi 22: movq %rsi, %rax 23: ret 24: .data 25: func_size: 26: .long . - func
- 4〜8行目: memcopy の様なもの。func関数を仮想メモリアドレス 0 にコピーしている
- 10〜11行目: funcの引数を用意。ただの足し算関数なので 1 と 2 を用意
- 13〜14行目: 絶対アドレス 0 への関数呼び出し
- 20〜23行目: コピーされる関数。rdi,rsiレジスタの値を足しあわせてraxレジスタに入れるだけ
$ as -o zero_ex.o zero_ex.s && ld -o zero_ex zero_ex.o
__PAGEZEROセグメントのメモリ保護属性
$ otool -l zero_ex zero_ex: Load command 0 cmd LC_SEGMENT_64 cmdsize 72 segname __PAGEZERO vmaddr 0x0000000000000000 vmsize 0x0000000000001000 fileoff 0 filesize 0 maxprot 0x00000000 initprot 0x00000000 nsects 0 flags 0x0
メモリ保護属性は「全て禁止」
実行
$ ./zero_ex Segmentation fault
gdbで実行(抜粋)
(gdb) disas start Dump of assembler code for function start: 0x0000000000001fbc: mov $0x0,%rdi 0x0000000000001fc3 : lea 0x2f(%rip),%rsi # 0x1ff9 0x0000000000001fca : mov 0x2f(%rip),%rcx # 0x2000 0x0000000000001fd1 : cld 0x0000000000001fd2 : rep movsb %ds:(%rsi),%es:(%rdi) 0x0000000000001fd4 : mov $0x1,%rdi 0x0000000000001fdb : mov $0x2,%rsi 0x0000000000001fe2 : mov $0x0,%rdx 0x0000000000001fe9 : callq *%rdx 0x0000000000001feb : mov $0x2000001,%rax 0x0000000000001ff2 : xor %rdi,%rdi 0x0000000000001ff5 : syscall 0x0000000000001ff7 : jmp 0x1ff7 End of assembler dump. (gdb) run Starting program: zero_ex Program received signal EXC_BAD_ACCESS, Could not access memory. Reason: KERN_INVALID_ADDRESS at address: 0x0000000000000000 0x0000000000001fd2 in start ()
予想通り、0x1fd2 の movsb 命令で アドレス 0 に書き込もうとしたところで落ちている。
バイナリエディタでメモリ保護属性を、maxprot, initprot共に「全て許可(=7)」に変更して、otoolでメモリ保護属性を確認。
$ otool -l ./zero_ex ./zero_ex: Load command 0 cmd LC_SEGMENT_64 cmdsize 72 segname __PAGEZERO vmaddr 0x0000000000000000 vmsize 0x0000000000001000 fileoff 0 filesize 0 maxprot 0x00000007 initprot 0x00000007 nsects 0 flags 0x0
maxprot,initprot共に「全て許可」に変わっている。
実行。
$ ./zero_ex $ echo $? 0
実行できた!!!!
gdbで実行
(gdb) disas start Dump of assembler code for function start: 0x0000000000001fbc: mov $0x0,%rdi 0x0000000000001fc3 : lea 0x2f(%rip),%rsi # 0x1ff9 0x0000000000001fca : mov 0x2f(%rip),%rcx # 0x2000 0x0000000000001fd1 : cld 0x0000000000001fd2 : rep movsb %ds:(%rsi),%es:(%rdi) 0x0000000000001fd4 : mov $0x1,%rdi 0x0000000000001fdb : mov $0x2,%rsi 0x0000000000001fe2 : mov $0x0,%rdx 0x0000000000001fe9 : callq *%rdx 0x0000000000001feb : mov $0x2000001,%rax 0x0000000000001ff2 : xor %rdi,%rdi 0x0000000000001ff5 : syscall 0x0000000000001ff7 : jmp 0x1ff7 (gdb) disas func Dump of assembler code for function func: 0x0000000000001ff9 : add %rdi,%rsi 0x0000000000001ffc : mov %rsi,%rax 0x0000000000001fff : retq End of assembler dump. (gdb) b *0x1fd2 ← 関数をコピするところでブレイク Breakpoint 1 at 0x1fd2 (gdb) run Starting program: /Users/teru/Documents/low_level/blog_macho_struct/zero_ex Breakpoint 1, 0x0000000000001fd2 in start () (gdb) i reg rsi rdi rcx rsi 0x1ff9 8185 ← コピー元アドレス(funcのアドレス) rdi 0x0 0 ← コピー先アドレス rcx 0x7 7 ← ループカウンタのような物 (gdb) si 0x0000000000001fd2 in start () (gdb) i reg rsi rdi rcx rsi 0x1ffa 8186 rdi 0x1 1 rcx 0x6 6 (gdb) si 6 0x0000000000001fd4 in start () (gdb) i reg rsi rdi rcx rsi 0x2000 8192 rdi 0x7 7 rcx 0x0 0 ← 0 になるまで (rsi)から(rdi)へ1バイトずつコピーした (gdb) si 3 0x0000000000001fe9 in start () (gdb) i reg rdi rsi rdx rip rdi 0x1 1 ← これと rsi 0x2 2 ← これはアドレス0にコピーしたfuncへの引数 rdx 0x0 0 ← 絶対アドレス指定のcallはレジスタ経由でないとアドレス指定できないので飛び先(=0)を設定 rip 0x1fe9 0x1fe9 ← 飛びますよ〜〜〜 (gdb) si 0x0000000000000000 in ?? () ← 0 来た!!! (gdb) i rip rip 0x0 0 ← でもripは次の命令のアドレスなのでまだfuncは実行されていない (gdb) si 0x0000000000000003 in ?? () ← アドレス0の命令が実行された!!! (gdb) i reg rdi rsi rax rdi 0x1 1 rsi 0x3 3 ← 1 + 2 = 3 の値が入っている rax 0x1fbc 8124 (gdb) si 2 0x0000000000001feb in start () ← 関数数呼び出しの次の命令に戻った。 (gdb) c Continuing. Program exited normally.
という感じで、__PAGEZEROセグメントでコード実行することもできた。
長くなってきたので詳細の記述は省くけど、同様の方法で「実行だけ許可しない(VM_PROT_READ | VM_PROT_WRITE)(=3)」などのメモリ保護属性でもその通りの動作をすることも確認できた。
実験結果
まとめ
- __PAGEZEROセグメントのおかげで NULL へのアクセスが有ったときに直ちにプログラムを異常終了させることができる
- セグメントコマンド中の保護属性を書き換えてしまえば異常終了しなくなる。保護属性ってちゃんと効いてるんだなー(当たり前か ^^;)
- 関数を別の場所にコピーしてそれを実行するとか、実は初体験だった。
- ストリング命令も初体験だった。便利だ。