シェルコード(英: Shellcode)とは、コンピュータセキュリティにおいて、ソフトウェアのセキュリティホールを利用するペイロードとして使われるコード断片である。侵入したマシンを攻撃者が制御できるようにするため、シェルを起動することが多いことから「シェルコード」と呼ぶ。シェルコードは機械語で書かれることが多いが、機械語でなくとも同様のタスクを実行できるコード断片はシェルコードと呼ばれる。シェルコードは単にシェルを起動するだけとは限らないため、シェルコードという名称は不十分だとも言われている[1]。しかし、他の用語は今のところ定着していない。
種類
シェルコードには、「ローカル型」と「リモート型」があり、攻撃者がシェルコードの動作するマシンの制御を獲得するのか(ローカル型)、それともネットワーク上の別のマシンの制御を獲得するのか(リモート型)で決まってくる。
ローカル型
ローカル型シェルコードは、攻撃者がそのマシンに限定的なアクセス権があるとき、より特権レベルの高いプロセスの脆弱性を利用するものである。成功すると、シェルコードによって攻撃者は攻撃対象プロセスと同等レベルの特権を得る。ローカル型シェルコードは比較的作成しやすい。この形式のシェルコードが行うのは、シェルの実行ファイルを実行することだけである。
リモート型
リモート型シェルコードは、LANやインターネット上の別のマシンで動作する脆弱性のあるプロセスを対象として攻撃するものである。成功すると、攻撃者はネットワーク経由で対象マシンにアクセスできるようになる。リモート型シェルコードは通常、標準のTCP/IPソケットコネクションを使い、攻撃者が対象マシン上のシェルにアクセスできるようにする。このコネクションの構築方法としては、以下のものがある。
- connect-back
- シェルコード自体がコネクションを確立する。シェルコードが攻撃者の使っているマシンに接続し返してくる (connects back) ため、このように呼ばれる。
- bindshell
- 攻撃者がコネクションを生成する必要がある場合。攻撃者が対象マシンを制御するために接続できる一定のポートにシェルコードがバインド (bind) することから、このように呼ばれる。
- socket-reuse
- シェルコードが実行される前にコネクションがクローズされないような脆弱性を持つプロセスを利用する場合に使われることがある。シェルコードはそのコネクションを再利用 (re-use) して攻撃者と通信する。この種のシェルコードは作成が難しいため珍しい。なぜなら、シェルコードは再利用できるコネクションを探す必要があり、マシン上には同時に多数のコネクションがオープンされていることがあるからである。
ファイアウォールは、connect-back方式のシェルコードが作った外に向かうコネクションを検出でき、bindshell方式のシェルコードで使う外から入ってくるコネクションを検出できる。従って、ファイアウォール内のマシン自体に脆弱性があっても、シェルコードに攻撃者がアクセスするのを防ぐことである種の保護を提供できる。socket-reuse方式のシェルコードは新たなコネクションを生成しないため、検出やブロックがより難しい。
ダウンロード実行型
リモート型シェルコードは、攻撃者が対象システムの中をいろいろ見るのに便利だが、攻撃者は単にその対象システムに何らかのマルウェアをインストールしたいだけということが多い。その場合ダウンロード実行型シェルコードがよく使われる。この種のシェルコードはシェルを起動せず、そのマシンにネットワーク経由で特定の実行ファイルをダウンロードさせ、それをディスクに格納させ、実行させる。これを現在ではドライブバイダウンロード攻撃と呼ぶ。これは犠牲者が悪意あるWebページを訪れたとき、ある種のシェルコードをダウンロードして実行させ、犠牲者のマシンにマルウェアをインストールさせようとするものである。
実行戦略
シェルコードを対象プロセスに注入する以前か同時に、プログラムカウンタへの制御を得られる脆弱性も利用する。プログラムカウンタはシェルコードを指すよう変更され、その後シェルコードが実行されタスクが行われる。シェルコードの注入は、脆弱なプロセスに対してネットワーク経由でデータ送信の形で行われたり、ローカル型では対象プロセスが読み込むファイルの中に仕込まれたり、コマンド行の内容に仕込まれたり、環境変数に仕込まれたりする。
符号化
多くのプロセスでは注入できるデータには制限(フィルター)があるため、シェルコードはその制限内で書く必要があり、それにはコードを小さくすること、ヌル文字を途中に含めないこと、英数字のみにすることなどが含まれる。このような制限に対処する方法はいくつかある。
- 設計と実装の最適化により、シェルコードを大きさを減らす。
- シェルコードで使えるバイトの範囲の制限に対応するため、実装を修正する。
- 通常なら注入できないバイトのパターンを生成するため、自己書き換えコードを使う。
命令コードをネットワーク経由でそのまま送信するとセキュリティソフトによって検出されるため、自己解凍コードやポリモルフィックコードで符号化されることが多い。
符号化方法
ブラウザを対象として利用する場合、シェルコードはパーセントエンコーディング、"\uXXXX"エンコード、HTMLでの文字エンコードを使ってJavaScriptの文字列として符号化される。例えば、IA-32アーキテクチャで2個のNOP命令を符号化することでどう見えるかを示す。まず符号化しない状態では、次のようになる。
90 NOP
90 NOP
これはパーセントエンコーディングで符号化した文字列とした場合、次のようになる(unescape()はデコード関数)。
unescape("%u9090");
"\uXXXX"エンコードで文字列とした場合、次のようになる。
"\u9090";
そして、HTMLでの文字エンコードで文字列とした場合、
"邐"
あるいは
"邐"
となる。
ヌル文字排除
一般にシェルコードはヌル文字を終端とする文字列として対象プロセスに注入されるため、ヌル文字(一般に0x00)をその途中で使うことはできない。途中にヌル文字があると、そこまでしか文字列としてコピーされない。従ってヌル文字に相当するコードがシェルコードの途中にある場合、シェルコードは最後まで実行されない。
途中にヌルバイトを含むシェルコードからヌルを含まないシェルコードを生成するには、同じ効果を持つ別の命令列に置き換える。例えば、IA-32アーキテクチャで以下の命令があるとする。
B8 01000000 MOV EAX,1 // Set the register EAX to 0x000000001
即値の1が符号拡張されるため、ヌルバイトが命令に含まれている。
33C0 XOR EAX,EAX // Set the register EAX to 0x000000000
40 INC EAX // Increase EAX to 0x00000001
この命令列は先の命令と同じ効果があるが、バイト数が少なくなるだけでなく、ヌルバイトも含まない。
英数字または印字可能文字
ある状況では、印字可能な文字(制御文字以外)や英数字だけしか注入できない場合がある。そのような状況ではシェルコードを書くのに使える命令の種類は大きく制限される。そのための技法は Phrack 57号で Rix が発表しており[2]、それによれば任意のコードを英数字のみのコードに変換することができる。よく使われる技法は自己書き換えコードで、デコーダ部分は制限されたコードのみで動作するよう書く必要がある。シェルコード本体も例えば英数字のみのコードに符号化しておき、デコーダがそれを注入後に書き換え、その後実行する。
Unicode
最近のプログラムでは、文字列にUnicodeを使っている。多くの場合、ASCII文字列は処理前にUnicodeに変換される。UTF-16なら各文字に2バイトを使用する(一部は4バイト)。ASCII文字列をUTF-16に変換すると、元の文字列の各バイトの後ろにゼロバイトが挿入される。Phrack 61号で Obscou が示したとおり[3]、この変換後も正しく動作するシェルコードを書くことは可能である。任意のシェルコードをUTF-16化したときに動作するよう自動変換するプログラムが存在している。これも、自己書き換えコードを基本としている。
プラットフォーム依存性
多くのシェルコードは機械語で書かれている。これは抽象度を低くしないと脆弱性をうまく利用できないためである。そのため、シェルコードはCPUとオペレーティングシステムのバージョンやサービスパックといったプラットフォーム毎に作成される。利用する脆弱性によっては、シェルコードの形式が非常に限定されることもある。ただし、1つのシェルコードが複数の脆弱性に対応して機能することはないし、オペレーティングシステムのバージョンやリビジョン、プロセッサの違う環境をまたいで動作することもない[4]。その種の汎用性を達成するには、各種プラットフォーム向けにシェルコードの複数のバージョンを作成し、正しいバージョンに分岐するヘッダ部を作成する。実行されると、そのコードはプラットフォームによって異なる動作をし、そのプラットフォームに適したシェルコードを実行する。
関連項目
脚注
外部リンク