2016.12.28
シェルコードを書いてみる
書籍『セキュリティコンテストチャレンジブック』2章 pwnの関連記事としてシェルコードに関する記事を3回にわたって掲載、今回は実際にシェルコードを書く段階に入ります。
はじめに
この連載では『セキュリティコンテストチャレンジブック』2章「pwn」の関連記事としてシェルコードに関する記事を計3回にわたって掲載します。(書籍未掲載記事です!) 前回の記事『シェルコードとは』はこちら
3 シェルコードを書いてみる
それでは実際にシェルコードを書く段階に入ります。まずは x86 でシェルを起動するシェルコードを書いてみましょう。シェルを起動するためには execve システムコールを使って /bin/sh
を実行します。 execve システムコールの引数を考えると、execve("/bin/sh", ["sh", 0], [0]);
と同等の処理を行うことになるのですが、実はLinuxにおいては execve
の第2, 第3引数に NULL
を与えても問題なく実行されるため、 execve("/bin/sh", 0, 0)
と同等の処理を行います。 もう1つ考慮することとして、シェルコードは独立して実行できることが望ましいので、 /bin/sh
の文字列もシェルコード内に入れ込むことになります。
文字列をどう扱うかはいったん置いておいて、その他の部分のシェルコードを構築してしまいます。
BITS 32 global _start _start: mov eax, 11 mov ebx, ; ['/bin/sh'のアドレス] mov ecx, 0 mov edx, 0 int 0x80
/bin/sh
のアドレスを ebx にセットしなければなりません。方法はいくつかあるのですが、まずは jmp-call を使ってアドレスを入手する方法を紹介します。
call 命令は指定された関数へジャンプするとともに call 命令の位置 + 5
のアドレス、すなわち call 命令で飛んだ先からリターンするためのリターンアドレスをスタックに push します。うまく call 命令を利用すれば、スタックの一番上に /bin/sh
の文字列へのアドレスがある状態でシェルコードを継続することができるのです。実際に jmp-call を使ってレジスタに /bin/sh
のアドレスをセットするアセンブラを書いてみましょう。
; binsh.s BITS 32 global _start _start: mov eax, 11 jmp buf setebx: pop ebx mov ecx, 0 mov edx, 0 int 0x80 buf: call setebx db '/bin/sh', 0
/bin/sh
という文字列が機械語として実行されてしまうと困るので、シェルコードの末尾に配置しています。途中で buf
へジャンプし、そこから setebx
を call することで /bin/sh
のアドレスをスタックに載せてシェルコードを継続させています。あとは pop 命令を使ってそのアドレスを ebx にセットするだけです。
アセンブラのコードが書けたのでこれを NASM を使ってアセンブルしてみます。アセンブルが完了したら ndisasm
コマンドを使って機械語を逆アセンブルしてみます。また、後々のためシェルコードのサイズも確認しておきましょう。
$ nasm binsh.s $ ndisasm -b 32 binsh 00000000 B80B000000 mov eax,0xb 00000005 EB0D jmp short 0x14 00000007 5B pop ebx 00000008 B900000000 mov ecx,0x0 0000000D BA00000000 mov edx,0x0 00000012 CD80 int 0x80 00000014 E8EEFFFFFF call dword 0x7 00000019 2F das 0000001A 62696E bound ebp,[ecx+0x6e] 0000001D 2F das 0000001E 7368 jnc 0x88 00000020 00 db $ wc binsh 0 1 33 binsh
完成したシェルコードを実際に動かして動作を確認してみたいところですが、このままでは NASM の出力形式がバイナリファイルになっているため動かすことができません。Linux 上で動かすためにはオブジェクト形式で出力し、更にリンカを実行して実行形式のファイルを作成する必要があります。
$ nasm -f aout binsh.s $ ld -m elf_i386 binsh.o $ ./a.out $ ls a.out binsh.o binsh.s $ exit
シェルから ./a.out
を実行して起動するのもシェルであるためわかりにくくなっていますが、シェルコードが実際に動いている様子を確認することができます。