CVE-2011-3192 Range header DoS vulnerability Apache 1.3/2.x の更に続き
気になって調べてたら細かくなってしまったのでエントリ分離。
コンテンツサイズ依存やPoCの独特な区間指定の謎が気になった人向けです。
対策や概要だけで良い方は前々回のエントリをご覧ください。
- CVE-2011-3192 Range header DoS vulnerability Apache HTTPD 1.3/2.x
まず最も誤解されそうな点として、今回のプロセス肥大化はコンテンツサイズ
やレスポンスサイズに比例する訳ではありません。
元となるコンテンツに要求した区間を満たす程度のサイズは必要ですが、
一定以上は大きくても変わらない結果になります。
コンテンツサイズとプロセスサイズの関係についてはこちらにまとまっています。
1300bytes付近に壁があるのがよく分かると思います。
これは公開されているPoCが約1300区間を生成して投げている事に起因します。
この部分増やせば更に肥大化すると思うかもしれませんが、リクエストヘッダの
サイズ制限もあるので、デフォルトでは大体この程度が限度になります。
次に区間指定ですが、コンテンツが十分大きい場合にプロセスサイズ肥大化有無は
リクエストした区間のパターンによって以下の結果となります。
## 太る Range: 0-1,0-2,0-3,0-4,... ## 太らない Range: 0-,0-,0-,... Range: 1-2,2-3,4-5,...
何故こうなるかというとメモリ上で肥大化しているのは、レスポンスで返す実データ
そのものではなく、管理データ部分(apr_bucket)な為です。
実際に消費メモリが増えていくのは以下の箇所です。
apr_bucketは実データへのポインタとオフセットやサイズ等を保持しています(実データその物ではない)
apr_bucket_copyで対象のapr_bucketのコピーを作り、APR_BRIGADE_INSERT_TAILで
返すべきデータのリストに追加しています。
ecとe2はそれぞれ要求した区間の始点と終点のapr_bucketで、これは連結リストなので
辿っていけば要求した区間のデータが取れることになります。(全部辿り終わると ec==e2)
@@httpd-2.2.19/modules/http/byterange_filter.c do { apr_bucket *foo; const char *str; apr_size_t len; if (apr_bucket_copy(ec, &foo) != APR_SUCCESS) { /* As above; this should not fail since the bucket has * a known length, but just to be sure, this takes * care of uncopyable buckets that do somehow manage * to slip through. */ /* XXX: check for failure? */ apr_bucket_read(ec, &str, &len, APR_BLOCK_READ); apr_bucket_copy(ec, &foo); } APR_BRIGADE_INSERT_TAIL(bsend, foo); ec = APR_BUCKET_NEXT(ec); } while (ec != e2);
つまり、この部分のループ回数が増えれば増えるほどメモリ消費が増えていくことになります。
ecとe2に値を格納しているのは以下の箇所です。apr_brigade_partitionが該当関数です。
bbはapr_bucket_brigadeでapr_bucketの集合を持っている様な物だと思ってください。
ここでは要求した区間それぞれの始点、終点位置に該当するapr_bucketを取得します。
@@httpd-2.2.19/modules/http/byterange_filter.c /* These calls to apr_brigage_partition should only fail in * pathological cases, e.g. a file being truncated whilst * being served. */ if ((rv = apr_brigade_partition(bb, range_start, &ec)) != APR_SUCCESS) { ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r, PARTITION_ERR_FMT, range_start, clength); continue; } if ((rv = apr_brigade_partition(bb, range_end+1, &e2)) != APR_SUCCESS) { ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r, PARTITION_ERR_FMT, range_end+1, clength); continue; }
取得するだけなら特に害は無いと思うかもしれませんが、要求した位置によって
apr_bucket_brigadeの断片化(apr_bucketの分割)が発生します。例えば100bytesのコンテンツに
Range: bytes=0-,0-0,0-1,0-2,0-3,0-4,... のリクエストをした場合以下の様に分割されます。
(区間の終点は内部的に+1して要求しています。range_end+1の箇所がそれになります。)
## 0- (1 apr_bucket 取得) 0 ... 100 byte | ... | 1 apr_bucket 存在 ## 0-0 (1 apr_bucket 取得) 0 1 ... 100 byte | | ... | 2 apr_bucket 存在 ## 0-1 (2 apr_bucket 取得) 0 1 2 ... 100 byte | | | ... | 3 apr_bucket 存在 ## 0-2 (3 apr_bucket 取得) 0 1 2 3 ... 100 byte | | | | ... | 4 apr_bucket 存在 ## 0-3 (4 apr_bucket 取得) 0 1 2 3 4 ... 100 byte | | | | | ... | 5 apr_bucket 存在 ## 0-4 (5 apr_bucket 取得) 0 1 2 3 4 5 ... 100 byte | | | | | | ... | 6 apr_bucket 存在 ...
最初は100bytesの1つのapr_bucketだった物が、細かい区間要求によりどんどん
分割されていきます。PoCのプロセスが肥大化するケースでは始点固定、終点が
インクリメントしていく要求をしているので、1回の取得で辿らなければならない
apr_bucketはリニアに増えていきます。(サイズが1のapr_bucketをひたすら辿っている状態)
各条件において1リクエスト処理した後のプロセスサイズと1回での最大&合計ループ回数等を
表にすると以下のようになります。
条件としてはバージョン2.2.19、32bitバイナリ、コンテンツ10Kbytes、deflate無しです。
区間数も条件を揃えるためにすべて1000区間に統一してあります。
要求区間 | 最大ループ回数 | 合計ループ回数 | レスポンスサイズ | プロセスサイズ |
---|---|---|---|---|
0-,0-1,...,0-999 | 999 | 499501 | 594,658 bytes | 48,029,696 bytes |
0-,0-,...,0- | 1 | 1000 | 10,326,025 bytes | 3,514,368 bytes |
0-1,1-2,...,999-1000 | 2 | 1999 | 87,808 bytes | 3,694,592 bytes |
この通りレスポンスサイズではなく合計ループ回数に比例したプロセスサイズとなります。
リクエスト内容と現象だけみると単純な感じですが、実際に内部まで掘り下げてみると
結構複雑になっている事が分かると思います。
apr_bucket_brigade/apr_bucket部分は初めて読みましたがなかなか面白い構造でした。