セキュリティ クイズ
あなたのセキュリティ IQ をテストする
Michael Howard および Bryan Sullivan

目次
著者はどちらもコードでセキュリティ バグを探すのを楽しんでいます。バグ探しならお手のもの、とさえ言っていいかもしれません。だれにも負けないとまでは言いませんが、たいていは多くのバグをたちまち発見できます。あなたはどうですか。
セキュリティ バグを見たらそうとわかりますか。このクイズで試してみてください。どのコード サンプルにも、少なくとも 1 つのセキュリティ脆弱性が存在します。バグを見つけて、何点くらい取れるか試してみてください。コードの後で、脆弱性の要点と若干のコメント、および必要に応じてセキュリティ開発ライフサイクル (SDL) でそのバグを発見する方法を説明しています。情報とコード サンプルを提供してくれた Peter Torr と Eric Lippert に感謝します。
バグ #1 (C または C++)
void func(char *s1, char *s2) {
char d[32];
strncpy(d,s1,sizeof d - 1);
strncat(d,s2,sizeof d - 1);
...
}
解答 まず手始めは、とても簡単なものということで、お馴染みのバッファ オーバーランです。多くの場合、制限付きの strncpy 関数および strncat 関数を使用しているコードは、安全で問題がないように見えます。しかし、これらの関数は、バッファ サイズが正しい場合にのみ安全であり、この例ではバッファ サイズが間違っています。まったく違います。
技術的には、1 番目の呼び出しは安全ですが、2 番目の呼び出しは正しくありません。strncpy 関数および strncat 関数の最後の引数はバッファに残された領域の大きさであり、strncpy を呼び出すことでその領域の一部または全部を使い果たしています。バッファ オーバーフロー。Michael が、
このバグの種類について 2004 年のブログで的確に解説しています。
Visual C++ 2005 以降では、警告 C4996 によって、不正な関数呼び出しを安全な呼び出しに変えるように指摘されます。また、/analyze オプションを指定すると、strncat が文字列をゼロで終了しない可能性があることを示す C6053 の警告が発生します。
率直に言うと、strncpy と strncat (およびその "n" の同類) は、いくつかの理由で strcpy と strcat (およびその同類) より劣っています。第 1 に、戻り値に問題があります。バッファへのポインタが返されますが、バッファは有効かもしれないし、無効かもしれません。それを知る方法はないのです。第 2 に、対象のバッファのサイズを正確に取得するのは非常に困難です。このバグを見つけられたら、1 ポイント獲得です。
バグ #2 (C または C++)
int main(int argc, char* argv[]) {
char d[32];
if (argc==2)
strcpy(d,argv[1]);
...
return 0;
}
解答 これは有害なバグです。バッファ オーバーランの例としてこのバグがよく使用されているのを目にしますが、多くの場合、コードにセキュリティ バグがあるのかどうかを判別するのは不可能です。すべてはコードの使用方法しだいです。
これが標準の Win32 EXE であれば、最悪でも自分で自分を攻撃し、自分でコードを実行する程度なので、セキュリティ バグにはなりません。
一方、このコードが、たとえば SYSTEM として実行する Windows サービスの ServiceMain にある場合、または Linux アプリケーションの setuid root のメイン関数にある場合には、コードは紛れもなくセキュリティ バグになります。
コードが setuid root としてマークされた Linux アプリケーションであるものとします。普通のユーザーがアプリケーションを起動すると、アプリケーションは実際には root として実行し、これはローカル特権昇格の脆弱性を意味します。
バグ #1 で示したコード例と同じように、strcpy の呼び出しに対しては C4996 の警告が発生し、/analyze ではバッファ オーバーランの可能性を示す C6204 が発生します。"なるほど。もっと状況がわからないと答えられないな" と解答した人は 2 ポイント獲得です。それ以外の人はポイントなしです。
バグ #3 (すべての言語 — 例は C#)
byte[] GetKey(UInt32 keySize) {
byte[] key = null;
try {
key = new byte[keySize];
RNGCryptoServiceProvider.Create().GetBytes(key);
}
catch (Exception e) {
Random r = new Random();
r.NextBytes(key);
}
return key;
}
解答 このひどいキー生成コードには 2 つのバグがあります。1 つ目はすぐにわかります。暗号的には問題のない乱数生成関数の呼び出しが失敗すると、コードは例外をキャッチして、まったくお粗末な予測できてしまう乱数生成関数を呼び出します。この点を見つけられたら、1 ポイント差し上げます。暗号化された乱数を使用してキーを生成することは、SDL の要件です。
しかし、バグはこれだけではありません。このコードはすべての例外をキャッチします。まれな場合を除き、C++ の例外、Microsoft .NET Framework の例外、または Windows の構造化例外処理のどれでも見境なくすべての例外をキャッチしてしまうと、本当のエラーが隠れてしまいます。ですから、このようなことはしないでください。
すべての例外 (バッファ オーバーランのようなアクセス保護エラーを含む) をキャッチする C または C++ の構造化例外ハンドラは、/analyze を指定してコンパイルすると C6320 警告が発生します。
アニメーション化されたカーソルのバグ MS07-017 に対して攻撃者が攻撃を繰り返すことを可能にしたのは、この種の設計でした。例外処理のバグを見つけられたら、さらに 1 ポイント獲得です。
バグ #4
void func(const char *s) {
if (!s) return;
char t[3];
memcpy(t,s,3);
t[3] = 0;
...
}
解答 このようなバグを発見したのは、数年前、まだ開発中の Windows Vista においてでした。しかし、これはセキュリティ バグでしょうか。コーディングのバグであることは明らかです。コードは配列の 4 番目の要素に書き込んでいますが、配列には要素 3 個分の長さしかありません。配列は 0 から始まるのであって、1 ではないことを思い出してください。攻撃者は何も制御できないのだからこれはセキュリティ バグではないと、私は強く主張します。
一方、攻撃者が i を制御するこのようなかたちのバグの場合は、攻撃者がメモリ内のどこにでもゼロを書き込めることを意味します。これは正真正銘のセキュリティ バグです。
void func(const char *s, int i) {
if (!s) return;
char t[3];
memcpy(t,s,3);
t[i] = 0;
...
}
このコードを /analyze でコンパイルすると、C6201 "有効なインデックス範囲外" のエラーが発生します。"セキュリティ バグではない" と答えられたら、1 ポイント獲得です。
バグ #5
public class Barrel {
// By default, a barrel contains one rhesus monkey.
private static Monkey[] defaultMonkeys =
new[] { new RhesusMonkey() };
// backing store for property.
private IEnumerable<Monkey> monkeys = null;
public IEnumerable<Monkey> Monkeys {
get {
if (monkeys == null) {
if (MonkeysReady())
monkeys = PopulateMonkeys();
else
monkeys = defaultMonkeys;
}
return monkeys;
}
}
}
解答 これは難しい問題です。このクラスの作成者は、コードが安全かつ効率的であると考えています。補助ストアはプライベートで、プロパティは読み取り専用で、プロパティの型は IEnumerable<T> なので、呼び出し元は Barrel の状態を読み取る以外に何もできません。
しかし、敵意ある攻撃者はプロパティの戻り値を Monkey[] にキャストしてみる可能性があることを、作成者は忘れています。2 つの Barrel があり、それぞれに既定の Monkey リストがあるとすると、そのうちの 1 つを手に入れた敵意ある攻撃者は、既定の静的リストの RhesusMonkey を他の任意の Monkey または null に置き換えることができ、それによって他の Barrel の状態を実質的に変更できます。
この場合の解決策は、ReadOnlyCollection<T> をキャッシュするか、または敵意ある呼び出し元による変更から基の配列を保護する真に読み取り専用の記憶域を使用することです。このことに気付いたら、2 ポイント差し上げます。
バグ #6 (C#)
protected void Page_Load(object sender, EventArgs e) {
string lastLogin = Request["LastLogin"];
if (String.IsNullOrEmpty(lastLogin)) {
HttpCookie lastLoginCookie = new HttpCookie("LastLogin",
DateTime.Now.ToShortDateString());
lastLoginCookie.Expires = DateTime.Now.AddYears(1);
Response.Cookies.Add(lastLoginCookie);
}
else {
Response.Write("Welcome back! You last logged in on " + lastLogin);
Response.Cookies["LastLogin"].Value =
DateTime.Now.ToShortDateString();
}
}
解答 これは、Web で最も一般的な脆弱性である、簡単なクロス サイト スクリプティング脆弱性です。このコードは lastLogin の値が常に Cookie から取得されることをほのめかしているように見えますが、実際には、HttpRequest.Item プロパティは Cookie の値よりクエリ文字列の値を優先します。
言い換えると、lastLogin Cookie にどのような値が設定されていたとしても、攻撃者が lastLogin=<script>alert('0wned!')</script> という名前/値ペアをクエリ文字列に追加すると、アプリケーションは lastLogin 変数の値に対して悪意のあるスクリプトの入力を選択してしまいます。XSS と解答したあなたには、1 ポイント差し上げます。
バグ #7 (C#)
private decimal? lookupPrice(XmlDocument doc) {
XmlNode node = doc.SelectSingleNode(
@"//products/product[id/text()='" +
Request["itemId"] + "']/price");
if (node == null)
return null;
else
return (Convert.ToDecimal(node.InnerText));
}
解答 XPath インジェクションと解答したら、1 ポイント獲得です。XPath インジェクションは、はるかに (悪) 名高い同類の SQL インジェクションとまったく同じ原理で動作します。XPath コードと、検証もエスケープもされていないユーザー入力を結合するクエリを作成することで、このコードはインジェクション攻撃に対して脆弱です。後で何らかの形式の操作の実行に使用されるテキストを扱うすべてのアプリケーションは、インジェクション脆弱性の対象になります。
バグ #8 (C#)
public class CustomSessionIDManager : System.Web.Session State.SessionIDManager
{
private static object lockObject = new object();
public override string CreateSessionID(HttpContext context)
{
lock (lockObject)
{
Int32? lastSessionId = (int?)context.Application ["LastSessionId"];
if (lastSessionId == null)
lastSessionId = 1;
else
lastSessionId++;
context.Application["LastSessionId"] = lastSessionId;
return lastSessionId.ToString();
}
}
}
解答 このコードには 2 つの主な問題があります。このコードは、アプリケーションのロジックの周囲にロックを適切に適用し、2 つのスレッドが同時に同じセッション ID を作成しないようにしていますが、それでもサーバー ファームに展開するには安全ではありません。HttpContext.Application オブジェクトによって参照されているアプリケーション状態が、サーバー間で共有されていません。このアプリケーションをサーバー ファームに展開すると、セッション ID の競合が発生する可能性があります。このバグがわかったら、1 ポイント獲得です。
もう 1 つの重大な問題は、このクラスによって生成されるセッション ID が、簡単に推測される連続した整数であることです。ユーザーが自分のセッション トークンを見て、セッション ID が 100 であることがわかると、簡単なブラウザ ユーティリティを使用してセッション ID を 99 や 98 または他のもっと小さい値に変更し、そのユーザーのセッションをハイジャックできます。
この場合に開発者が使用できるもっとよい方法は、GUID または他の大きくてランダムな文字と数字から成る文字列を使用することです。セッション ID トークンとして連続した整数が不適切な選択であることがわかった場合は、1 ポイント追加してください。
バグ #9 (C#)
bool login(string username,
string password,
SqlConnection connection,
out string errorMessage) {
SqlCommand selectUserAndPassword = new SqlCommand(
"SELECT Password FROM UserAccount WHERE Username = @username",
connection);
selectUserAndPassword.Parameters.Add(
new SqlParameter("@username", username));
string validPassword =
(string)selectUserAndPassword.ExecuteScalar();
if (validPassword == null) {
// the user doesn't exist in the database
errorMessage = "Invalid user name";
return false;
}
else if (validPassword != password) {
// the given password doesn't match
errorMessage = "Incorrect password";
return false;
}
else {
// success
errorMessage = String.Empty;
return true;
}
}
解答 このコードの最大の問題は、ログインが失敗した場合に、アプリケーションからユーザーに返される情報が多すぎることです。たしかに、単純な入力ミスか、それともユーザー名を完全に忘れてしまったのかを、ユーザーが判断するには役立ちますが、この情報はアプリケーションに対してブルート フォース攻撃を試みる攻撃者にも役立つものです。直感には反するかもしれませんが、この状況では役に立たない方がよいのです。ログインが失敗した場合には、"ユーザー名が無効です" や "パスワードが無効です" ではなく、"ユーザー名またはパスワードが無効です" のようなメッセージを表示する必要があります。
このことに気付いたら、1 ポイントです。さらに、アプリケーションではパスワードをデータベースにプレーンテキストで保存してはならないことも覚えていたら、ボーナス ポイントを差し上げます。パスワードは、ソルト処理されたハッシュで保存および比較する必要があります。
バグ #10 (Silverlight CLR C#)
bool verifyCode(string discountCode) {
// We store the hash of the secret code instead of
// the plaintext of the secret code.
// Hash the incoming value and compare it against
// the stored hash.
SHA1Managed hashFunction = new SHA1Managed();
byte[] codeHash =
hashFunction.ComputeHash(
System.Text.Encoding.Unicode.GetBytes(discountCode));
byte[] secretCode = new byte[] {
116, 46, 130, 122, 36, 234, 158, 125, 163, 122,
157, 186, 64, 142, 51, 153, 113, 79, 1, 42 };
if (codeHash.Length != secretCode.Length) {
// The hash lengths don't match, so the strings don't
// match this should never happen, but we check anyway
return false;
}
// perform an element-by-element comparison of the arrays
for (int i = 0; i < codeHash.Length; i++) {
if (codeHash[i] != secretCode[i])
return false; // the hashes don't match
}
// all the elements match, so the strings match
// the discount code is valid, inform the server
WebServiceSoapClient client = new WebServiceSoapClient();
client.ApplyDiscountCode();
return true;
}
解答 開発者は賢明な判断を行って、シークレット コードをコード内にプレーンテキストで埋め込んでいません。ユーザーがシークレット (割引コードやパスワードなど) を知っているかどうかだけをテストする必要がある場合は、プレーンテキストを保存して文字列を直接比較するよりも、シークレットのハッシュを保存してハッシュを比較する方が常によい方法です。残念ながら、開発者は SHA-1 ハッシュ アルゴリズムを選択していますが、これは重大な脆弱性の兆候を示しており、後に SDL で禁止されています。もっとよい選択肢は、SDL が承認して推奨している SHA-256 ハッシュ アルゴリズムを実装する SHA256Managed クラスを使用することです。このことに気付いたら、1 ポイントです。
SHA-256 ではなく SHA-1 を選択したことよりもっとまずいのは、開発者がハッシュ値のソルト処理を疎かにしていることです。ソルト処理されていないハッシュは、事前に計算されたハッシュ テーブル (レインボー テーブルと呼ばれることがよくあります) によるクラックに対してはるかに脆弱です。おそらく、攻撃者は、ソルト処理されていないハッシュ値から元のプレーンテキストのシークレット コードを短時間で特定してしまうでしょう (著者は、プレーンテキストのシークレット コードで私たちに応答してくれた最初の人に対し、お祝いの言葉を
SDL ブログに投稿します)。ソルト処理されていないハッシュに気付いたなら、1 ポイント獲得です。
しかし、このコードの最大の問題は、そもそもコードがクライアント コンピュータで実行されるという事実です。最初にこれはユーザーのブラウザで実行される Silverlight CLR コードであると書かれていたことを思い出してください。クライアントで実行するすべてのコードは、攻撃者が操作できてしまいます。それだけのことです。Silverlight コードを実行するブラウザ インスタンスにデバッガをアタッチし、ブラウザを動かしてステップ実行しようと決意したユーザーを妨げるものは何もありません。
codeHash 変数を secretCode ハッシュと同じに設定し、比較ロジックが常に成功するようにするかもしれません。または、検証ロジックをそっくりスキップし、現在の命令から割引コードを適用する Web サービスの呼び出しにジャンプすることもできます。一番簡単なのは、デバッガを完全に回避して、単に Web サービスのメソッド ApplyDiscountCode を直接呼び出すことです。
C# または Visual Basic を使用して、ASP.NET Web フォームとまったく同じように Silverlight アプリケーションを作成しているかもしれませんが、Silverlight のコードはクライアント コンピュータで動作し、Web フォームはサーバーで動作するということを理解しておくことが重要です。クライアントで実行するコードは、攻撃者が見たり変更したりできます。クライアント側コードにシークレットを埋め込んだり、クライアント側コードが特権の必要な決定 (割引コードが有効かどうか、ユーザーが特定のアクションの実行を許可されているかどうか、など) を行うことができるようにしたりしないでください。このバグがわかったら、1 ポイント獲得です。