日記

日本語の勉強のためのブログ

シェルコードの入手&実行

以下の記事を読んで自分なりに整理した内容を書く。

inaz2.hatenablog.com

note.com

シェルを起動するアセンブリコードを書く

シェルを起動するだけのプログラムを作成した。

#include <unistd.h>

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

これをgcc -static hello.cでスタティックリンクしてコンパイルし、objdumpmain関数と__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
$