2011 年の年末に HashDoS というのが話題になった.
要するにハッシュテーブルのキーのハッシュ値を意図的に衝突させ, 非効率な挿入を行わせることで, 効率的にサービスを妨害する, というものだ.
ということをドヤ顔で書いてはいるが, 年末の時点ではこの HashDoS の原理については理解しておらず, 「データ構造を偏らせて計算量を増やすんだろう」ぐらいの漠然としたイメージしか無かった.
その後, PHP の HashTable 構造体や, 一般的なハッシュテーブルの実装について調べることで, HashDoS の原理がわかってきた.
ハッシュテーブルについては, いくつかの解説ページを見ながら, サンプルコードを Ruby に翻訳することで学習した.
PHP の HashTable 構造体は双方向リストを用いたハッシュテーブルとなっている.
キーのハッシュ値を元に要素を格納するスロットが決まり, そのスロットに既に要素が存在する場合は, そのスロット内の双方向リストの先頭に値を挿入する.
…という説明では上手く伝えきれないので, これを可視化する PHP 拡張を書いた.
hashtable_dump() 関数は引数として array 型の値を受け取り, HashTable 構造体レベルで内部の情報を出力する.
いずれも HashTable 構造体のメンバに対応するものだ. 出力している内容は以下の通り.
- nTableSize: ハッシュテーブルのスロット数. 最少で 8, 必要に応じて 2 倍ずつ拡張される.
- nTableMask: 値を格納するスロットを格納するためのビット演算に使用する値. 常に nTableSize より 1 小さい値.
- nNumOfElements: ハッシュテーブル内に存在する要素の数.
- nNextFreeElement: $hash[] = ‘foo’; としたときに, 暗黙的に指定されるキー.
- pListHead: ハッシュテーブル内の先頭要素のキー.
- pListTail: ハッシュテーブル内の末尾要素のキー.
- **arBuckets: Bucket 構造体へを指すポインタの配列. スロットの一覧と, その中に存在するキーを出力している.
配列が空のときは, 以下のようになる.
空なので, リストの先頭も末尾も NULL を指し, いずれのスロットにも値が無い. (NULL しか無い)
値の格納先のスロットは以下のようなビット演算で算出される.
これは 0 以上 nTableMask 以下になる.
最初の例のように, 連番をキーに順番に値を挿入した場合は, 各スロットに値が均等に振り分けられる.
しかし, 以下のような値の場合, ひとつのスロットに値が偏る.
ハッシュテーブルのスロット数が 8 であれば, キーとして 8 の倍数の要素だけを挿入することで, いずれもハッシュ値が 0 となり, ひとつのスロットに値が集中してしまう.
ここからキーが 0 の要素を探索する場合, 全ての要素を操作して 8 番目にならないと辿り着けない.
線形検索をリッチに実装しただけのものになってしまっている.
ここにもうひとつ要素を追加すると次のようになる.
要素数が 9 のときは, テーブルの大きさが 8 の 2 倍の 16 に拡張されるため, 偏りが少し解消される.
以下の関数を使うと, テーブルの拡張も考慮しつつ非効率な HashTable を構築することができる.
これを応用すると, HashDoS により効率よく Web サーバを落とすことができてしまう可能性がある.
なぐり書きブログなので特にまとめとかは無い.