x86-64で簡単なシェルコードを作ってみる
最近portswiggerにハマっててVulnhubの勉強が滞りがちだったのでたまには低レイヤーをやります。Web系楽しいんじゃあ^〜
とはいってもあまりにも多くの人がチャレンジしてるような内容ですが。
基本的にはこちらの、ももいろテクノロジーさんの記事にある方法をなぞっているだけです。
https://inaz2.hatenablog.com/entry/2014/03/13/013056
┌─[parrot@parrot-tobefilledbyoem]─[~/Desktop/shellcode]
└──╼ $uname -a
Linux parrot-tobefilledbyoem 5.10.0-6parrot1-amd64 #1 SMP Debian 5.10.28-6parrot1 (2021-04-12) x86_64 GNU/Linux
└──╼ $nasm --version
NASM version 2.15.05
環境はこんな感じ(Parrot OS推し)。
まずc言語を使って、execveで"/bin/sh"を呼び出すプログラムを作る。
// int execve(const char *pathname, char *const argv[], char *const envp[]);
#include <unistd.h>
void main() {
char *argv[] = {"/bin/sh", NULL};
execve(argv[0], argv, NULL);
}
これをgcc -static bash.cで実行ファイルにコンパイルし、gdbでコードを見てみる。
Dump of assembler code for function main:
0x0000000000401c2d <+0>: push %rbp
0x0000000000401c2e <+1>: mov %rsp,%rbp
=> 0x0000000000401c31 <+4>: sub $0x10,%rsp
0x0000000000401c35 <+8>: lea 0x7f3c8(%rip),%rax # 0x481004
0x0000000000401c3c <+15>: mov %rax,-0x10(%rbp)
0x0000000000401c40 <+19>: movq $0x0,-0x8(%rbp)
0x0000000000401c48 <+27>: mov -0x10(%rbp),%rax
0x0000000000401c4c <+31>: lea -0x10(%rbp),%rcx
0x0000000000401c50 <+35>: mov $0x0,%edx
0x0000000000401c55 <+40>: mov %rcx,%rsi
0x0000000000401c58 <+43>: mov %rax,%rdi
0x0000000000401c5b <+46>: call 0x43d440 <execve>
0x000000000043d440 in execve ()
(gdb) disas
Dump of assembler code for function execve:
=> 0x000000000043d440 <+0>: mov $0x3b,%eax
0x000000000043d445 <+5>: syscall
この時のレジスタの状態を見てみる。
(gdb) i r
rax 0x3b 59
rbx 0x400488 4195464
rcx 0x7fffffffde80 140737488346752
rdx 0x0 0
rsi 0x7fffffffde80 140737488346752
rdi 0x481004 4722692
rbp 0x7fffffffde90 0x7fffffffde90
rsp 0x7fffffffde78 0x7fffffffde78
r8 0x6 6
r9 0x0 0
(以下略)
(rcx, rsiレジスタの中身)
0x7fffffffde80: 0x00481004 0x00000000 0x00000000 0x00000000
(rdiレジスタの中身)
(gdb) x/10x $rdi
0x481004: 0x6e69622f 0x0068732f 0x6f657800 0x68705f6e
(gdb) x/10s $rdi
0x481004: "/bin/sh"
以上より
rax: 0x3b(システムコール番号)
rdi: execve関数のchar *pathname に対応("/bin/sh"へのポインタ)
rcx, rsi: execve関数の char *argv[]に対応("/bin/sh"へのポインタのポインタ)
rdx: 0 つまりNULL
という対応関係がわかる。問題はrcx,rsiレジスタのどちらが引数渡しに使われるかということだが、以下のページの通りレジスタは rdi, rsi, rdx,...の順に使われる。よってrsiレジスタがchar *argv[]に対応している。
あとは以上の通りになるようレジスタをセットして、システムコールを呼び出すようなアセンブラコードを書けば良い。
section .text
global _start
_start:
xor rdx, rdx ; *envp[]
push rdx
mov rax, 0x68732f6e69622f ; /bin/sh
push rax
mov rdi, rsp ; *pathname
push rdx
push rdi
mov rsi, rsp ; *argv[]
xor rax, rax
mov al, 0x3b ; syscall 0x3b
syscall
上のコードでは一旦raxレジスタに0x68732f 6e69622fを代入してスタックにpushしている。リトルエンディアンなので"/sh /bin"の順序で代入している。
あとはこのアセンブリファイル(bash.s)を
nasm -f elf64 bash.s
ld -o bas bash.o
で実行ファイルにするとシェルを起動することができた。
objdumpを使えばこの実行ファイルをアセンブリに戻すことができ、ももいろテクノロジーさんのサイトにあるワンライナーを使うことで以下のようなシェルコードを作成できる。
└──╼ $objdump -M intel -d ./bas | grep '^ ' | cut -f2 | perl -pe 's/(\w{2})\s+/\\x\1/g'
\x48\x31\xd2\x52\x48\xb8\x2f\x62\x69\x6e\x2f\x73\x68\x00\x50\x48\x89\xe7\x52\x57\x48\x89\xe6\x48\x31\xc0\xb0\x3b\x0f\x05
このシェルコードを呼び出す、テスト用のプログラムを作って動かしてみる。
void main() {
char const shellcode[] = "\x48\x31\xd2\x52\x48\xb8\x2f\x62\x69\x6e\x2f\x73\x68\x00\x50\x48\x89\xe7\x52\x57\x48\x89\xe6\x48\x31\xc0\xb0\x3b\x0f\x05";
(*(void (*)())shellcode)();
}
あとはこれをコンパイルすると、
┌─[✗]─[parrot@parrot-tobefilledbyoem]─[~/Desktop/shellcode]
└──╼ $gcc -fno-stack-protector -z execstack ./bas.c
┌─[parrot@parrot-tobefilledbyoem]─[~/Desktop/shellcode]
└──╼ $./a.out
$ exit
無事にシェルコードの動作を確認できた。
ちなみに上記のテストプログラムで、shellcode[]をmain関数の外側に書くとSIGSEGVでセグメントエラーになる。なぜだろう…
【追記】
main関数の外側にshellcodeを置くと、shellcodeはスタックではなく.rodataセクションに配置される。この.rodataには実行の権限は無いのでセグメンテーションフォルトになる。
【反省】
初歩の初歩といった内容でしたが自分の頭で考えてシェルコードを作ることができました。実はテストプログラムの (*(void (*)())shellcode)(); の意味がよく理解できていないので、勉強しておきたいです。
精進します。