今時のShellcodeとセキュア/防御的プログラミング

(Last Updated On: 2018年10月12日)

コンピュータセキュリティのことを考えるとShellcode(シェルコード)のことを忘れる訳にはいきません。Shellcodeとはバッファーオーバーフローを利用してコンピューターに任意コードを実行させるコードの総称です。そもそもは/bin/shなどのシェルを奪うコードが主だったので、この種のコードはShellcodeと呼ばれています。現在はシェルを奪う物だけでなく、他の操作を行う物もShellcodeと呼ばれています

Shellcodeを作る方は、山があるから登るのと同じで、Shellcodeが作れるから作る、のだと思います。

私個人はShellcodeを作ること、使うことに全く興味はないです。しかし、Shellcodeとそれを利用した攻撃は、防御の為に興味を持っています。簡単に今どきのShellcodeはどのようになっているのかまとめます。

なぜShellcodeを知るのか?

Shellcodeを知る理由は効果的な入力バリデーションとはどういうものなのか?を理解するために必要です。C/C++でプログラムを作っていないから関係ない!と思われるかも知れませんが、スクリプト系言語を使っていても、VM系言語を使っていても、言語自体やライブラリがC/C++で書かれていて利用していることがほとんどです。

C/C++でコードを書いていなくてもShellcodeの脅威から解放されているわけではありません。

C/C++以外の言語の場合、攻撃者にとってメリットがあります。スクリプト系言語などの場合、C/C++と異り、文字列型データの中にNULL文字を普通に保存できるものが多いです。ShellcodeでNULL文字が使えると自由度が増します。つまり、より簡単にShellcodeが作れてしまえます。

Shellcodeデータベース

Shellcodeデータベースで直ぐに見つかるのはshell-storm.orgやexploite-db.comです。

このデータベースを見ると、様々なCPU/OS用にShellcodeが作られていることがわかります。

バッファーオーバーフローがある=Shellcodeの実行ができる、という訳ではありませんがShellcode自体を自作しなくても既に公開された物を使う事ができるようになっています。

攻撃の方法を簡単に説明します。スタックオーバーフローならスタック上に記録されているリターンアドレス、ヒープオーバーフローならハンドラーなどのアドレス、などを改竄して攻撃者のShellcodeを実行させます。細かい事は理解していなくても、脆弱性を利用してメモリを書き換えてShellcodeを実行させる、と理解してよいと思います。

メモリを書き換えれない脆弱性なら問題がないのか?というとそうではありません。メモリ改竄を検出するカナリア値※やアドレス情報は攻撃にとって重要です。通常見えないハズのメモリ内容が見えてしまうと、ただ見えるだけ、ではない大きなリスクがあると考えなければなりません。メモリの中身を自由に見れてしまうような脆弱性の場合、Webサーバーの秘密鍵を盗んでしまう、といった事も可能になります。

※ 鳥のカナリアが毒性ガスの検出に利用されていたのでこの名前が利用されている

進化しているShellcode

shell-storm.orgのShellcodeは普通のマシン語コードです。しかし、今どきのプログラムは入力バリデーションで文字エンコーディングが文字種をホワイトリストで限定しています。(していますよね?!)

ASCIIのNULL文字など下位の制御コードやASCII上位のコード、Unicode文字などに限定していると攻撃を行いづらくなります。しかし、攻撃できない訳ではありません。

NULL文字なし

NULL文字なしのShellcodeは随分前から知られています。と言うよりC/C++の文字列型(char *)ではNULLが文字列終端なので、バッファーオーバーフローを利用したShellcode実行でNULLを回避(使わない)ことはとても重要なので直ぐに対処策が考えられています。日本語版Wikipediaのシェルコードの項目に「ヌル文字排除」があるくらいです。

実際、Shellcodeデータベースに載っているShellcodeのほとんどにNULLはありません。(全て見ていないので全くない、と言えないだけです。恐らくないと思います)

参考:

制御文字なし

NULL以外の制御文字がバリデーションで排除されていてもShellcodeを実行できます。基本的にはNULLなしのShellcodeを書くテクニックである、自己書き換え型のコードで実現できます。

参考:

Unicodeのみ

自己書き換え型コードを使えば、Shellcodeの自由度は格段に増します。UnicodeのみでもShellcodeを書くことが可能です。UTF-16だと自動的に変換するプログラムもあります。

参考:

英数字のみ

実はUnicodeのみのShellcodeより先に英数字のみのShellcodeが発表されています。

参考:

その他のテクニック

これらのシェルコードを使ってより制限がある状況でも、シェルコードを実行するテクニックも考案されています。

  • ステージ型:オーバーフローが小さい場合に、シェルコードをロードするローダーで読み込むステージと読み込んだシェルコードを実行するステージの2段階で攻撃
  • エッグハント型:バッファーオーバーフローが大きい場合でも実際の制御を奪うことが困難な場合、大きなオーバーフロー中に埋め込んだShellcodeを制御が奪える小さなオーバーフローから見つけ出して攻撃
  • オムレツ型:複数の小さなオーバーフローを組み合わせてShellcodeを実行する攻撃

WAFなどのIDS/IPSによる検出を防ぐために自己書き換え型コードを利用する場合もあります。自己書き換え型で偽装されると、シグニチャベースのIDS/IPSだと検出の難易度が高くなります。Web環境の攻撃だとURLエンコーディングやUnicodeエスケープなどを利用して検出を困難にすることも可能です。

攻撃側も色々考えていて、ステガノグラフィーを使ったり色々なテクニックが日々新しく開発されています。詳しくは参考リンクをご覧ください。

参考

最初、投稿したときにはDEP回避やALSR回避はWebアプリプログラマにはあまり重要ではないので書いていませんでした。しかし、カナリア値(これの解説のしていませんが)、DEPやALSRといった防御策も様々な手法で回避する方法も考案されています。つまり、バッファオーバーフロー脆弱性など、防御策があってもメモリ破壊攻撃が可能な場合は任意コードが実行されるリスクは存在します。そもそもバッファオーファーフローを起こさせないことが重要です。DEP、ALSRなどの回避方法はWebアプリプログラマのリスク対策にとってあまり重要ではありません。一般のWebアプリプログラマが可能なバッファオーバーフロー対策はまとめに書いています。

Webアプリプログラマが行える対策

WebアプリケーションをC/C++で書いている方はほんの一握りだと思います。多くの方はメモリ管理/バッファーオーバーフローに注意しなくても良い言語でWebアプリを書いていると思います。しかし、前述の通りWebアプリプログラマであっても言語やライブラリに潜んでいる脆弱性の影響を受けます。バッファーオーバーフロー攻撃に対する緩和策を行うメリットは十分にあります。

「ASCII英数字だけでShellcodeが作れるなら何をやっても無意味では?」と思うかも知れません。しかし、セキュア/防御的プログラミングを行うとリスクを削減できることに変わりはありません。Shellcodeは名前の通り、システムのシェル(コマンドプロンプト)を実行するだけの攻撃コードです。ASCII英数字だけで任意コードを実行するには高度な技術が必要な上、より長いコードが必要になります。適切な長さ制限だけでも十分有用な対策になります。

ちょうどタイムリーにPCRE(Perl互換正規表現ライブラリ)に任意コード実行を許してしまうヒープオーバーフロー脆弱性が公開されています。PHPもPCREライブラリをバンドルしているので2015年5月のリリースでバンドル版PCREライブラリがバージョンアップされています。

  • PCRE:
    • Upgraded pcrelib to 8.37. (CVE-2015-2325, CVE-2015-2326)

ユーザーが自由な正規表現と対象データを入力できる公開Webアプリはあまり無いとは思いますが、こういった脆弱性の影響を受けにくいアプリを作ることは可能です。

対策は簡単です。

  • 厳格な入力バリデーションを行い、必ず合理的な上限(データ量、数値。下限も忘れずに!)を設定する

コードがバッファーオーバーフローに脆弱だったとしても、外部からバッファーオーバーフローできないようになっていれば攻撃できません。そもそもバッファーオーバーフローを作ろうと思って作っている開発者居ません。誤ってバグを作ってしまっただけです。これに対処するには入力バリデーションが最も有効です。

バッファーオーバーフローは大きく分けて2種類があります。

  • バッファの大きさを越えてオーバーフロー
  • バッファの計算を間違えてオーバーフロー

どちらも似たような物、同じではないか、と思うかも知れません。しかし、この微妙な違いが重要です。前者の「バッファの大きさを超える」はバッファへの書き込み時の問題です。後者の「バッファの計算を間違える」はバッファサイズの計算時の問題です。

データ量だけに気を取られているとバッファ計算に利用される数値の計算ミス(主に整数オーバーフローが原因)によるオーバーフローに対する効果的な緩和策となる入力バリデーションができません。整数オーバーフローはコードを書いたプログラマが予期していないような大きな数値/データが渡される場合にバグが顕在化します。「数値だったらOK」と緩いバリデーションにするのではなく、「数値かつ妥当な範囲だったらOK」とバリデーションすることで対策となります。

紹介したPCREの脆弱性の内容は詳しく把握していませんが、”正規表現を限定する”(例えば、許可するメタ文字を”.”と”*”に限定)、”検索対象の文字列を限定する”のどちらかまたは両方を行うことによって回避/緩和できる可能性は十分にある※と思います。 今までにあったバッファーオーバーフロー脆弱性でこのようなケースはとても多かったからです。

※ 正規表現ライブラリはあまり例として適当ではないですね。今旬の脆弱性が思いつかなかったので例として挙げています。このブログをMOPBで検索するとバリデーションが役立つPHPの古いバッファーオーバーフロー脆弱性がいろいろ出てきます。

厳格な入力バリデーションを行うことによるWebアプリの安全化

システムが利用しているC/C++のコードを意識しなくても、厳格な入力バリデーションはセキュアなWebアプリケーションの実現に役立ちます。ライブラリなどのC/C++コードの問題はWebアプリプログラマの責任範囲外と言えますが、Webアプリのコードは完全にWebアプリプログラマの責任範囲内です。

危険なコードの多くは、出力時に出力先のシステム/コードが誤作動しないような安全対策を疎かにしていることが原因です。出力時に「出力先のシステム/コードが誤作動しないようなコード」を書くことはセキュア/防御的プログラミングの基本です。しかし、これが守られていないことは良くあります。

  • 整数だから、出力安全対策は要らない
  • 英数のみだから、出力安全対策は要らない
  • そもそも、出力安全対策が要ることを知らない
    • APIがないから必要性がないと思った
    • APIがあっても単純にAPIを知らなかった
    • 危険なAPIだと知らなかった
      • APIの仕様を理解せずに使っていた
      • 安全なAPIだと聞いていて、APIの制限を知らなかった

といった間違いはコード検査で頻繁に見つけます。3番目の「安全対策が要ることを知らない」も無視できない程よく見つかります。

入力バリデーションによる入力安全対策と出力時の出力安全対策でよく誤解されていることに、入力バリデーションは出力時の安全性に全く責任を持たない、があります。入力時にバリデーションされる入力がどのようなコンテクストで出力されるのか、入力バリデーションは責任を持ちません。安全に出力する責任は出力を行うコードにあります

入力時のセキュリティ対策と出力時のセキュリティ対策は、全く独立したセキュリティ対策である、と正しく理解することがセキュア/防御的プログラミングに欠かせません。

入力バリデーションを厳格に行う事により

  • そもそも、出力安全対策が要ることを知らない
    • APIがないから必要性がないと思った
    • APIがあっても単純にAPIを知らなかった
    • 危険なAPIだと知らなかった
      • APIの仕様を理解せずに使っていた
      • 安全なAPIだと聞いていて、APIの制限を知らなかった

のような状況であっても、結果的にアプリケーションが脆弱にならないケースは非常に多いです。非常に多いのでセキュア/防御的プログラミングのガイドで「入力バリデーションは最も重要なセキュリティ対策」として紹介されています。

システム上で利用しているC/C++で書かれたライブラリ/コードの脆弱性は、通常、Webアプリプログラマの責任範囲外です。しかし、入力バリデーションを厳格に行うとWebアプリプログラマが知らなかった/うっかりした問題だけでなく、他のコードで発生した「ついうっかりバグ」にまで対応できてしまうケースは枚挙にいとまがありません。繰り返しになりますが、Webアプリコードの問題はWebアプリプログラマの責任です。

正しく入力バリデーションを行うとWebアプリプログラムの脆弱性に対応できるだけでなく、おまけとしてシステムに潜むメモリ破壊攻撃にも対応できてしまうメリットがあります。オーバーフローさせない為には無用に大きなデータ、大きな値などを受け入れない、つまり入力バリデーションで拒否することが必要です。数値なら16バイトを超えるようなデータ、あり得ないメガバイト単位のデータ、億単位のピクセルや負の高さ、不正な文字エンコーディングなど普通はそもそも受け入れる必要がありません。

まとめ

Shellcode対策としての入力バリデーションでは

  • データ量を制限する
  • 数値の上限/下限を制限する

が最も有効であることが分かります。これらを実施してバッファーオーバーフローが起こせなければ攻撃できません。(注:Use After Freeはオーバーフロー無しでも攻撃できます)

  • 数字だけに制限する
  • 英字だけに制限する

もかなり効果的な緩和策になります。もう少し緩い制限にして制御コード無しのUnicodeにしても攻撃者が越えなければならないハードルが高くなります。攻撃者が楽に攻撃できる入り口を作って助ける必要は全くありません。できる限り厳しい入力バリデーションを行うと入り口でリスクを排除できます。インターフェースはコードに較べて変化しないので、インターフェースでできる限りのリスクを削減するのは論理的/合理的です。

入力バリデーションはセキュア/防御的プログラミング、一番のセキュリティ対策です。そして防御的プログラミングは現在のプログラミングでは原則と考えられています。プログラミング原則の第一番目のセキュリティ対策を行わないで開発すると、SQLインジェクション対策でエスケープをしていなかった会社が瑕疵担保以上の賠償金を請求されたように、問題発生時に大きな損失を生むリスクが発生します。

そもそも言語やライブラリに脆弱性がなければ良いし、アプリで入力バリデーションしても対応できないモノは多い、と思った方も居るかも知れません。その通りです。言語やライブラリが正しく入力バリデーション/結果のバリデーションをせずにメモリ破壊攻撃を受けることが悪いです。しかし、この考え方はアプリにも適用されます。正しく入力バリデーションしていたら防げる問題を入力バリデーションで防いでいなかったらそのコードが悪いのです。

一部では「入力バリデーションはセキュリティ対策ではない」と理解し難い主張をされる専門家や開発者も居ますが、セキュア/防御的プログラミングを実践してどんどんリスクを削減してください。

脆弱性スキャンサービスを提供しているサービスプロバイダーさんに「入力バリデーションをバリデーションするスキャンサービスを提供してください」とお願いしています。近い将来、こういったサービスが利用できる日も近いと思います。お客様が「君達の作ったWebアプリは入力バリデーションが全くなっていないじゃないか」と言われないよう、今から実践してください :)

参考: 入力バリデーションをしていないアプリは脆弱なアプリになりました。

投稿者: yohgaki