Apache HTTPD 2.2.21 における変更点 (CVE-2011-3192 Range header DoS 対策)

やっと出たので動作差分です。主に2.2.20でエンバグして修正された点が主になります。
現時点ではまだリリースノートは更新されていませんが、開発者によるGA投票通過して
ミラーもされてるので引っ込む事は無いでしょう。
ともあれ今回の件はこれで最後になるといいなぁ。


アドバイザリはこちら。(2011/09/14 ドラフトから正式版に張り替え)
大きな変更としては1.3系が影響対象外になった旨が記載されています。


Rangeリクエスト関連の処理で2.2.19から動作が変わった点は以下です。
2番目と4番目は本質的には同じでしたが、実際にバグを踏んだ例があったので
分離しています。
リグレッションした箇所だけ直して終わりかと思ったら結構変わったようです。

  • 1. 32区間を超える応答にてContent-Lengthヘッダが使用されなくなった
    • 2.2.19 全区間Content-Lengthヘッダでの応答
    • 2.2.20 32区間を境界値としてchunkedでの応答へ変わるようになった
    • 2.2.21 2.2.20同様
  • 2. 合計値がコンテンツサイズを超える複数区間要求を処理しなくなった
    • 2.2.19 要求した通りに返す
    • 2.2.20 コンテンツサイズを超える場合はRangeヘッダを無視
    • 2.2.21 2.2.20同様
  • 3. 負値のみを指定した場合の動作が変更された
    • 2.2.19 負値として扱う末尾からのデータが返る (RFC的に正しい動作)
    • 2.2.20 始点区間無しとして扱い先頭のデータが返る (リグレッション)
    • 2.2.21 2.2.19同様
  • 4. Rangeリクエストで全区間要求した場合にレスポンスコードが異なる
    • 2.2.19 206のレスポンスとして応答 (訂正 こっちがRFC的に推奨される動作)
    • 2.2.20 200でAccept-Rangesヘッダ付きレスポンスとして応答 (206でSHOULDなので一応違反ではない)
    • 2.2.21 2.2.20同様
  • 5. 不正な区間を指定した場合の動作が変更
    • 2.2.19 416としてエラー
    • 2.2.20 200として全区間を返す
    • 2.2.21 2.2.19同様
  • 6. MaxRangesディレクティブが追加
    • 2.2.19 存在しない
    • 2.2.20 存在しない
    • 2.2.21 指定した値以上の区間数はRangeヘッダを無視 (未指定の場合は200区間)

1と2は2.2.20と変わっていないので詳細は省略します。


3. 負値のみを指定した場合の動作が変更された
普通にバグっていたというオチですね。
踏んでる人見かけなかったのでこんな使い方してる人は殆ど居ないのかもしれません。

## リクエスト
GET /xxxx HTTP/1.1
Host: xxxx
Range: bytes=-10
## 2.2.20 のレスポンス (終点のみ指定された物として扱われる)
HTTP/1.1 206 Partial Content
Content-Length: 11
Content-Range: bytes 0-10/1000
## 2.2.19, 2.2.21のレスポンス (負の値として扱われて後ろの部分が返ってくる)
HTTP/1.1 206 Partial Content
Content-Length: 10
Content-Range: bytes 990-999/1000
...


4. Rangeリクエストで全区間要求した場合にレスポンスコードが異なる
trunkのコードでは2.2.19の動作へ戻されてましたが、最終的には2.2.20の動作になりました。
報告者のケースは206の応答をRangeサポートの判別に使っていた様ですが、結論としては
レスポンスのAccept-Rangesヘッダで判別せよという事になったみたいです。
RFC的には2.2.20以降の200で応答する動作が正しいようです。
(訂正 RFC的には206でSHOULDでした。該当箇所はRFC 2616の14.35.1 Byte Ranges)

## リクエスト
GET /xxxx HTTP/1.1
Host: xxxx
Range: bytes=0-
## 2.2.19 のレスポンス 206で全区間が返る
HTTP/1.1 206 Partial Content
Accept-Ranges: bytes
Content-Length: 1000
Content-Range: bytes 0-999/1000
...
## 2.2.20, 2.2.21 のレスポンス 200で全区間が返る
HTTP/1.1 200 OK
Accept-Ranges: bytes
Content-Length: 1000
...


5. 不正な区間を指定した場合の動作が変更
コンテンツサイズが要求した値を超えている場合にエラーが返るようになりました。
2.2.20で動作が変わって元に戻ったパターンですね。
# 前回に2.2.20の変更点挙げたときには漏れてました

## リクエスト (対象コンテンツは1000bytes)
GET /xxxx HTTP/1.1
Host: xxxx
Range: bytes=10000-
## 2.2.19, 2.2.21 のレスポンス 416でエラー
HTTP/1.1 416 Requested Range Not Satisfiable
...
## 2.2.20 のレスポンス 200で全区間が返る
HTTP/1.1 200 OK
Accept-Ranges: bytes
Content-Length: 1000
...


6. MaxRangesディレクティブが追加
許可する区間数を設定で決められるようになりました。
2.2.21で設定できる値との対応は以下の通りです。
設定値の扱いをどうするか最後まで検討していたようで、httpd.confのコメント
と一致しない部分がありますがそのうち直るでしょう。(0がunlimitedと書かれているが違う)

設定値 意味
指定無し デフォルト(200区間まで許可)
default デフォルト(200区間まで許可)
none Rangeを許可しない(ヘッダ削除と同じ)
unlimited 制限無し(今まで通り)
1以上の数値 指定した区間数まで許可

指定した値前後の応答は以下の通りです。

## リクエスト
GET /xxxx HTTP/1.1
Host: xxxx
Range: bytes=0-1,1-2,3-4,...
## 制限を超過した場合のレスポンス
HTTP/1.1 200 OK
Accept-Ranges: bytes
Content-Length: xxxxx
...
## 制限に収まってる場合のレスポンス
HTTP/1.1 206 Partial Content
Accept-Ranges: bytes
Content-Length: xxxxx
Content-Type: multipart/byteranges; boundary=xxxxxxxxxx
...


2.2.20の変更点は前回も書いたとおり、このバージョンは急かされたからなのか結構なバグ
が混入していました。2.2.21はその後暫くバグ報告が無いか待ってからタグが打たれた訳ですが
アドバイザリの更新等も無かったため、2.2.20(各ディストリビューションのバックポートパッチ含む)が
そういう状態にあるという事を知らない人も多かったのではないでしょうか。


欲を言えば、2.2.20リリース後の数日で既にいくつかの問題に気づいていた訳ですし、以下を
アドバイザリのアップデートで公開してから、追加のバグ報告待ちに入った方が良かったと思いました。

  • 1.3.xは脆弱性の対象にならない
  • 2.2.20には問題があり修正した2.2.21を準備中

されていれば、問題を知らずに2.2.20へバージョンアップして、すぐに2.2.21へ
再度バージョンアップする羽目になる人も今よりは減ったんじゃないですかね。
結果として対象外になった1.3系の対応しようと頑張ってた人とかも同様で。

Apache HTTPD 2.2.20 における変更点 (CVE-2011-3192 Range header DoS 対策)

既にヘッダ削除設定で対応を終えた人も多いと思いますが、新しいバージョンが出た様なので。
まだ未対策の人は、どちらの対応がリスクが低いか判断して対策されると良いかと思います。

新バージョンのアナウンスは以下です。


他に変わってる箇所もありますが、今回の脆弱性(CVE-2011-3192)対応で修正された
箇所で、外部からアクセスした際にも変更がみられる点がメインになります。
当然プロセスが肥大化する部分は直ってますが、内部動作なので詳細は省略します。
バージョンは2.2.19と2.2.20の比較になります。


Rangeリクエスト関連の処理で変更があった、無かった箇所は以下の通りです。

  • 動作が変わった点
    • 32区間を超える応答にてContent-Lengthヘッダが使用されなくなった
    • 合計値がコンテンツサイズを超える複数区間要求を処理しなくなった
    • 負値のみを指定した場合の動作が変更された(多分リグレッション)
    • Rangeリクエストで全区間要求した場合にレスポンスコードが異なる(2011/09/01追記)
  • 動作が変わらなかった点
    • Rangeヘッダでの区間数制限(無しのまま)
      • 以前同様リクエストヘッダの最大長依存
      • ヘッダ削除設定で対応した場合はこの部分に制限が掛かっている状態
    • Rangeヘッダで重複する区間指定をした場合のレスポンス(マージしない、要求されたまま返す)
      • trunkでは途中まで重複区間をマージする動作に変更されていた。
      • が、2.2.xでは以前と変わらない動作になった。現在のtrunk版はどうなっているか未確認


まず、一つめの動作が変わった点です。
32区間を超える要求した場合以下の通りレスポンスが変わります。
chunkedはHTTP/1.1の機能となるので、HTTP/1.0の場合は使用できません。
そのためHTTP/1.0かつ指定区間以上要求した場合はKeep-Aliveが使用不可になります。

## リクエスト
GET /xxxx HTTP/1.1
Host: xxxx
Range: bytes=100-200,200-300,...
## 2.2.19 と 2.2.20の32区間以下 の場合のレスポンス
HTTP/1.1 206 Partial Content
Content-Length: xxxxx
Content-Type: multipart/byteranges; boundary=xxxxxxxxxx
...
## 2.2.20の32区間を超えるレスポンス
HTTP/1.1 206 Partial Content
Transfer-Encoding: chunked
Content-Type: multipart/byteranges; boundary=xxxxxxxxxx
...

ソースコードの該当箇所はこの辺です。
何故32なのかまでは追っていませんがフィルタやブリッジの制約なのかもしれません。
# 0x1F=31だけどiは0から始まるので32区間まではOK
更にこの部分の処理で、プロセス肥大化要因となっていたapr_bucketのコピー処理が
置き換わっているので大量の区間を処理する場合においてもプロセス肥大化しなくなっています。

@@httpd-2.2.20/modules/http/byterange_filter.c
        APR_BRIGADE_CONCAT(bsend, tmpbb);
        if (i && !(i & 0x1F)) {
            /*
             * Every now and then, pass what we have down the filter chain.
             * In this case, the content-length filter cannot calculate and
             * set the content length and we must remove any Content-Length
             * header already present.
             */
            apr_table_unset(r->headers_out, "Content-Length");
            if ((rv = ap_pass_brigade(f->next, bsend)) != APR_SUCCESS)
                return rv;
            apr_brigade_cleanup(bsend);
        }

動作は変わってはいますが制限が追加された訳ではないので、HTTP/1.1準拠の
クライアント実装であれば特に問題にはならないと思われます。chunkedの処理で問題が
起きる様なクライアント実装であれば既に別の問題を踏んでいるでしょう。


次に二つめの変更点です。
合計値がコンテンツサイズを超える複数区間要求を処理しなくなりました。
と、書くとなんだか解りにくいですが要は Range: bytes=0-,0-,0-,... 等への負荷対策だと思われます。
例を挙げるとこんな感じです。リクエスト対象のコンテンツは1000bytesだと思ってください。

## リクエスト
GET /xxxx HTTP/1.1
Host: xxxx
Range: bytes=0-,0-,0-
## 2.2.19のレスポンス (元コンテンツ*3 + マルチパートバウンダリ等 のサイズになる)
HTTP/1.1 206 Partial Content
Content-Length: 3500
Content-Type: multipart/byteranges; boundary=xxxxxxxxxx
...
## 2.2.20のレスポンス (無視してRangeヘッダが無かったのと同じ扱いになる)
HTTP/1.1 200 OK
Content-Length: 1000
...

ソースコードの該当箇所はこの辺です。
sum_lengthsは要求した複数区間の合計値、clengthは対象のコンテンツのサイズが入ります。

@@httpd-2.2.20/modules/http/byterange_filter.c
    if (sum_lengths >= clength) {
        ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r,
                      "Sum of ranges not smaller than file, ignoring.");
        return 0;
    }

2.2.20においても合計値が超えない場合は2.2.19と同じ動作になります。
こちらも行儀の悪い使い方してない限りはまず問題にならない修正だと思います。


最後に三つめです。
区間指定で負値を指定した場合の動作が変更されました。
ソース見た感じ負値を考慮しようとしてるので、意図した変更では無い感じですね。
これは単一、複数区間のリクエスト問わず両方に影響する変更となります。
以下は1000bytesコンテンツに対してリクエストした場合の動作です。

## リクエスト
GET /xxxx HTTP/1.1
Host: xxxx
Range: bytes=-10
## 2.2.19のレスポンス (負の値として扱われて後ろの部分が返ってくる)
HTTP/1.1 206 Partial Content
Content-Length: 10
Content-Range: bytes 990-999/1000
...
## 2.2.20のレスポンス (終点のみ指定された物として扱われる)
HTTP/1.1 206 Partial Content
Content-Length: 11
Content-Range: bytes 0-10/1000
...

異なる解釈になっている事が分かると思います。

ソースコードは以下の箇所になります。

## 2.2.19 始点が無ければ負値として扱っている
@@httpd-2.2.19/modules/http/byterange_filter.c
static int parse_byterange(char *range, apr_off_t clength,
                           apr_off_t *start, apr_off_t *end)
{
    char *dash = strchr(range, '-');
    char *errp;
    apr_off_t number;

    if (!dash) {
        return 0;
    }

    if ((dash == range)) {
        /* In the form "-5" */
        if (apr_strtoff(&number, dash+1, &errp, 10) || *errp) {
            return 0;
        }
        *start = clength - number;
        *end = clength - 1;
    }
    else {
...
## 2.2.20 同じ様にしたつもりが間違えた?(dash == range ではなく dash == cur)
@@httpd-2.2.20/modules/http/byterange_filter.c
static int ap_set_byterange(request_rec *r, apr_off_t clength,
                            apr_array_header_t **indexes)
...
    *indexes = apr_array_make(r->pool, ranges, sizeof(indexes_t));
    while ((cur = ap_getword(r->pool, &range, ','))) {
        char *dash;
        char *errp;
        apr_off_t number, start, end;

        if (!(dash = strchr(cur, '-'))) {
            break;
        }

        if (dash == range) {
            /* In the form "-5" */
            if (apr_strtoff(&number, dash+1, &errp, 10) || *errp) {
                break;
            }
            start = clength - number;
            end = clength - 1;
        }
        else {
...

と、結果として解釈が変わっているので返ってくるバイト数が変わってしまっています。
問題になる可能性が一番高いのはこの動作変更ですね。(というよりバグ)
以下の様に修正したら以前同様の動作になりましたが、他にも問題があるかも知れないので
公式修正を待つのが無難かと思います。

@@httpd-2.2.20/modules/http/byterange_filter.c
        if (!(dash = strchr(cur, '-'))) {
            break;
        }

-       if (dash == range) {
+       if (dash == cur) {
            /* In the form "-5" */
            if (apr_strtoff(&number, dash+1, &errp, 10) || *errp) {
                break;
            }

この値の扱いについては2.2.19の動作で正しい様です。
わざわざRFC違反するように直したりはしないと思うのでリグレッションでしょうね。
以下は RFC 2616 の 14.35.1 Byte Ranges から抜粋。

   Examples of byte-ranges-specifier values (assuming an entity-body of
   length 10000):
      - The first 500 bytes (byte offsets 0-499, inclusive):  bytes=0-
        499
      - The second 500 bytes (byte offsets 500-999, inclusive):
        bytes=500-999
      - The final 500 bytes (byte offsets 9500-9999, inclusive):
        bytes=-500
      - Or bytes=9500-
      - The first and last bytes only (bytes 0 and 9999):  bytes=0-0,-1
      - Several legal but not canonical specifications of the second 500
        bytes (byte offsets 500-999, inclusive):
         bytes=500-600,601-999
         bytes=500-700,601-999


上記の通り、新しいバージョンにて変更された動作はいくつかあります。
変わった使い方をしていない限りは気付かない変更だとは思いますが、
全く問題なしというわけでもなく、既に踏んでしまった人も居るようです。


報告されているのはDebianが先にバックポートしたバージョンなので、2.2.20と
全く同じではありませんが、内容的には今回の修正に関連する所だと思われます。
(現時点ではリクエストヘッダ等が出てないので詳細は分かりませんが)


======
他にも変わってる点があったので追記(2011/09/01)
まだありそうな感じもしますが。

Rangeリクエストで全区間要求した場合にレスポンスコードが異なります。

## リクエスト
GET /xxxx HTTP/1.1
Host: xxxx
Range: bytes=0-
## 2.2.19 のレスポンス 206で全区間が返る
HTTP/1.1 206 Partial Content
Content-Length: 1000
Content-Range: bytes 0-999/1000
...
## 2.2.20 のレスポンス 200で全区間が返る
HTTP/1.1 200 OK
Accept-Ranges: bytes
Content-Length: 1000
...

ソースコード的には二つめの変更点の箇所ですね。
trunkのコードでは判定条件が >= から > に変わっているので
次のリリースでは直るんじゃないかと思います。
Debianで報告されてたバグはこれの様な感じがしますね。


Redhatでもbugzilla見ると指摘はされていますがそのままリリースされてる感じが。(良いのかこれ?)
Debianの方は最初は同じでしたが、報告あった後に修正したバージョンを作ろうとしてるようですね。

CVE-2011-3192 Range header DoS vulnerability Apache 1.3/2.x の更に続き

気になって調べてたら細かくなってしまったのでエントリ分離。
コンテンツサイズ依存やPoCの独特な区間指定の謎が気になった人向けです。
対策や概要だけで良い方は前々回のエントリをご覧ください。


まず最も誤解されそうな点として、今回のプロセス肥大化はコンテンツサイズ
やレスポンスサイズに比例する訳ではありません。
元となるコンテンツに要求した区間を満たす程度のサイズは必要ですが、
一定以上は大きくても変わらない結果になります。


コンテンツサイズとプロセスサイズの関係についてはこちらにまとまっています。
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部分は初めて読みましたがなかなか面白い構造でした。

CVE-2011-3192 Range header DoS vulnerability Apache HTTPD 1.3/2.x の続き

元のエントリに現時点の修正状況を追記しました。
動作的に互換性のある直し方をしてくると思ったらそんなことは無かった様で。
http://d.hatena.ne.jp/nice20/20110825/p1

CVE-2011-3192 Range header DoS vulnerability Apache HTTPD 1.3/2.x

数日前からFull Disclosureで騒がれてたけどやっとCVE採番されたので。
以前のISC BINDの脆弱性(CVE-2011-1910)とかに比べるとzero-day状態に
なったにも関わらずApache側の動きが遅い気もします。(表に見えてなかっただけ?)


アドバイザリは以下


DoSだけといってしまえばそれまでですが、Apache HTTPDでは久しぶりに

  • 現時点で出ている全バージョンが対象 (2011/09/03追記 1.3系は対象外)
  • 特に変わった設定していなくても影響を受ける (静的コンテンツ置いてるだけでもNG)
  • 攻撃コスト << 効果 (HEADリクエストで可能なのでトラフィックも殆ど必要ない)
  • HTTPDだけでは済まない可能性が高い
  • とても簡単

と、される側からすると嫌な感じ満載の脆弱性になっています。


HTTPへのDoSといえばかなり前に見つかっても、まだ対策してない所が大量に見られる
Slowlorisとかもあります。これはApache HTTPDのChildプロセス(スレッド)占有するだけなので、
MaxClientsまで達してHTTPでのアクセスが出来なくなるだけです。(だけというとアレですが)


一方今回のはプロセスの肥大化を伴うので、実メモリ消費して更にスワップ
使い尽くしてOS毎激重になったあげくLinuxとかの場合はOOM Killer発動と、他の
プロセスや場合によってはOSを巻き込んで逝ってしまいます。
そうでなくても限界までスワップアウトした場合は、処理が戻ってくるまで待ちきれなくて
サーバ毎リセット掛ける事が多いかもしれませんが。


メモリリークしてるわけではないので、1プロセスあたりは際限なく増えていく
のでは無く一定サイズで止まります。が、増えたプロセスサイズは減らず、
更にリクエストの度に毎回メモリアクセスもされるのでスワップアウトして
放置するわけにも行かずと行った感じで、サーバに対する効率的な攻撃に
なってしまっています。


該当箇所は多分この辺のループです。Rangeヘッダ処理はフィルタとして実装されて
おり複数の区間要求があった場合は multipart/byteranges として返されます。

@@httpd-2.2.19/modules/http/byterange_filter.c
AP_CORE_DECLARE_NONSTD(apr_status_t) ap_byterange_filter(ap_filter_t *f,
                                                         apr_bucket_brigade *bb)
...
    /* this brigade holds what we will be sending */
    bsend = apr_brigade_create(r->pool, c->bucket_alloc);

    while ((current = ap_getword(r->pool, &r->range, ','))
           && (rv = parse_byterange(current, clength, &range_start,
                                    &range_end))) {
        apr_bucket *e2;
        apr_bucket *ec;
...
        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);
    }


で、結局の所対策どうすれば良いというのは、今のところ1リクエストで複数の区間
要求するリクエストを拒否or無視するしかありません。
正式版でどう直してくるのか気になるところではあります。

## これはOKだし、普通のブラウザも発行する
GET /xxxx HTTP/1.1
Host: xxxx
Range: bytes=1000-2000
## こういうのは無視したい
GET /xxxx HTTP/1.1
Host: xxxx
Range: bytes=1000-2000,1000-2001,1000-2002,...

アドバイザリにも何種類か対策方法が載っており以下はそのうちの一つですが
一定以上の区間要求が来た場合のみ無かったことにしています。

## 6区間以上は無視してヘッダ削除(無かったことに)
SetEnvIf Range (,.*?){5,} bad-range=1
RequestHeader unset Range env=bad-range

複数の区間要求するRangeヘッダとか誰がこんなの使ってるのと云うと、Acrobat(Adobe) Reader Plugin
がこういうリクエストを送ってくる様です。様ですというのは手元のAdobe Reader Xでは、
前者の単一区間のリクエストしか観測出来ませんでした。
出すのを止めたのか何か条件が足りないのかは不明ですが。




===
抜けがあったので補足。
削る場合はRangeヘッダだけでは不十分でした。

## Rangeヘッダ用
SetEnvIf Range (,.*?){5,} bad-range=1
RequestHeader unset Range env=bad-range
## Request-Rangeヘッダ用
SetEnvIf Request-Range (,.*?){5,} bad-range=1
RequestHeader unset Request-Range env=bad-range

該当箇所はこの辺。互換性のために残ってる部分も対応が必要です。

@@httpd-2.2.19/modules/http/byterange_filter.c
    /* Check for Range request-header (HTTP/1.1) or Request-Range for
     * backwards-compatibility with second-draft Luotonen/Franks
     * byte-ranges (e.g. Netscape Navigator 2-3).
     *
     * We support this form, with Request-Range, and (farther down) we
     * send multipart/x-byteranges instead of multipart/byteranges for
     * Request-Range based requests to work around a bug in Netscape
     * Navigator 2-3 and MSIE 3.
     */

    if (!(range = apr_table_get(r->headers_in, "Range"))) {
        range = apr_table_get(r->headers_in, "Request-Range");
    }


更に2.0ではenvを用いたRequestHeaderディレクティブによるヘッダ削除が効かないようです。
header unset takes two arguments とか出た場合はダメです。
諦めてRequestHeader unset Rangeで丸ごと消してしまうか、Deny from env=xxxで
エラー返すなりになるんじゃ無いかと思います。
ソースの該当箇所は以下。

@@httpd-2.2.19/modules/metadata/mod_headers.c
    if (new->action == hdr_unset) {
        if (value) {
            if (envclause) {
                return "header unset takes two arguments";
            }
            envclause = value;
            value = NULL;
        }
    }
@@httpd-2.0.64/modules/metadata/mod_headers.c
    if (new->action == hdr_unset) {
        if (value)
            return "header unset takes two arguments";
    }


===
更に追記(2011/08/27)
アドバイザリが更新されてRequest-Rangeヘッダの考慮が追加されてます。


ソースコードリポジトリの修正を見る限り結構大幅に変えていますね。
まだ正式リリースされていないので変わるかもしれませんが、重複する区間
マージするようになっているみたいです。
たとえば以下のような区間要求の場合、修正の前後でレスポンスに差異が出ます。

## リクエスト
GET /xxxx HTTP/1.1
Host: xxxx
Range: bytes=100-200,150-300,500-600
## 2.2.19のレスポンス
##  被っていようが要求されたとおりの区間で返す
HTTP/1.1 206 Partial Content
Content-Type: multipart/byteranges; boundary=xxxxxxxxxx
...
--xxxxxxxxxx
Content-range: bytes 100-200/100000
...
--xxxxxxxxxx
Content-range: bytes 150-300/100000
...
--xxxxxxxxxx
Content-range: bytes 500-600/100000
...
--xxxxxxxxxx--
## trunk revision 1162297時点のコードでのレスポンス
##  連続または被っている区間はまとめられる (注意:この修正は2.2.20には採用されませんでした)
HTTP/1.1 206 Partial Content
Content-Type: multipart/byteranges; boundary=xxxxxxxxxx
...
--xxxxxxxxxx
Content-range: bytes 100-300/100000
...
--xxxxxxxxxx
Content-range: bytes 500-600/100000
...
--xxxxxxxxxx--

被っている区間も別々に返ってくることを想定して作られているアプリの場合は
破綻してしまいそうな気もします。そんな使い方するなってことかもしれませんが。


ともあれ当面はヘッダ削除対応で凌いでおいた方がトラブルが少ない感じがします。
内部だけではなく外部から見た動作も変わっているので、バージョンアップで対応する場合は
しっかりテストをした方が良いと思います。




====
2.2.20が出たので追記(2011/08/31)
Rangeヘッダ例にbytes=が抜けてた部分を修正。
trunkバージョンの動作説明に注意書きを追加。


====
1.3系対象外の旨を追記(2011/09/03)
アドバイザリUPDATE 3のドラフトより。
正式版出たらファイル消えそうなのでこのへんのコメント欄参照で。


====
2.2.21が出たので追記(2011/09/13)

OllyDbg 2 Plugin インターフェイス変更点 その1 (2.01a4 暫定版)

暫く前になりますが 8月3日にリリースされた OllyDbg 2.01 alpha 4
でついにプラグインサポートが追加されました。
やはりプラグインあってこその OllyDbg ですよね。


ただ、現時点ではビルド済みのBookmarkプラグインのDLLとソースコード
ヘッダが置いてあるだけで、まだ完全なPDKでは無いようです。(.defファイルも無し)
ドキュメント無しでプラグイン書くのはマゾ的な記述が有ったりするので、
普通に開発する人は正式版PDKのリリースを待った方が無難だと思います。
取りあえず、付属のBookmarkプラグインを再コンパイルしてみるだけでも大変でした。


物はこの辺から拾えます。


残念な事に OllyDbg 1.10 のプラグインとの互換性は無くなっています。
似たような構造にはなっていますが、OllyDbg 1.10とImmunity Debuggerの様な
近さではないのでコンバータで変換するのは、事実上無理だと思います。
(不可能では無いかもしれませんが、そこまでがんばる人いるかなぁ)


弄っていて目に付いた変更点は以下の通りです。
2.01a4を元にしているので、次のバージョンでは変わってるかもしれませんがあしからず。

  • UNICODEビルド必須
    • デバッガとやりとりする文字関係はすべてワイドキャラクタに変更
  • エクスポートされている関数の追加、削除
    • 消えた関数多数、更に物によってstdcallとcdecl混在 (なぜ混ぜた...)
    • 同じ名前で残っていてもフラグやデフォルトの動作が変わってる物がある感じ
    • デバックビルドだからなのか見えてはいけないシンボルも見えてるような気も
  • プラグイン側でエクスポートするコールバック関数の変更
    • 中身も変わってる関数多数(メニューや初期化周りなど)
  • defineや構造体の変更
    • t_memory.sect => t_memory.sectname 等
  • Plugingetvalue廃止
    • 関数経由の取得から直接値がエクスポートされるように変更


と、こんな感じでソースコードがあったとしてもそのまま再コンパイルして動く
程度の変更では無くなっています。きっちりTCHARとか使って書いてた人は
インターフェイス変わった部分だけ追従すれば良いですが、ほとんどの人は
まずマルチバイト/ワイドキャラクタ対応部分から必要になるんじゃないかと思います。

MinGW+GCCでImmunity Debugger Plugin開発

今度はImmunity Debuggerです。
OllyDbgを元にしているのでPlugin用のインターフェイスなどは
シンボル名を除いて大体同じです。


が、1.7系までと1.8系以降でエクスポートされている関数名などが変わっている
ため、対象バージョンによりプラグインを分ける必要があります。
更にPDKは1.7系の物までしか配布されていないので、1.8系は適当に書き換えて
使用します。一応問題無く動いているようですが何か変な可能性もあります。
(自分で使用してるAPIだけしか動作確認してないので)
なお、今回の動作や仕様確認はバージョン1.73と1.83で行っています。


PDKは以下の場所から取得できます。
もうメンテナンスされてないようなのでいつまで置かれているかは分かりませんが。


まずは差分を確認しましょう。
以下は、各バージョンのImmunity DebuggerとOllyDbgのプラグインでエクスポートする
必要のあるコールバック関数です(一部抜粋)。呼び出し規約はすべてcdeclです。

OllyDbg 1.10 Immunity Debugger 1.7X Immunity Debugger 1.8X
_ODBG_Pluginaction _IMMDBG_Pluginaction IMMDBG_Pluginaction
_ODBG_Pluginclose _IMMDBG_Pluginclose IMMDBG_Pluginclose
_ODBG_Plugindata _IMMDBG_Plugindata IMMDBG_Plugindata
_ODBG_Plugindestroy _IMMDBG_Plugindestroy IMMDBG_Plugindestroy
_ODBG_Plugininit _IMMDBG_Plugininit IMMDBG_Plugininit
_ODBG_Pluginmainloop _IMMDBG_Pluginmainloop IMMDBG_Pluginmainloop
_ODBG_Pluginmenu _IMMDBG_Pluginmenu IMMDBG_Pluginmenu
_ODBG_Pluginreset _IMMDBG_Pluginreset IMMDBG_Pluginreset

この通り1.7Xと1.8Xで関数先頭の_が除去されています。


更に以下がデバッガのexeがエクスポートしている関数です(一部抜粋)。
プラグインからはインポートする対象になります。
呼び出し規約は同様にすべてcdeclです。
Immunity DebuggerではPython拡張で用いたと思われるPy*のシンボル等が追加されていますが、
OllyDbg互換で書く分には特に気にする必要はありません。

OllyDbg 1.10 Immunity Debugger 1.7X Immunity Debugger 1.8X
_Addsorteddata _Addsorteddata Addsorteddata
_Addtolist _Addtolist Addtolist
_Analysecode _Analysecode Analysecode
_Assemble _Assemble Assemble
_Broadcast _Broadcast Broadcast
_Browsefilename _Browsefilename Browsefilename

こちらも同様で関数先頭の_が除去されています。


この辺の差違を吸収するため、1.7Xまで使えていたPDKのplugin.hを以下の通り修正します。
OllyDbgのPDKを元に書き換えても動くと思いますが、折角あるのでこちらを元にします。
コンパイル時に渡すバージョンでどちらの名前でエクスポートするか振り分けます。
Immunity Debugger専用にプラグインを書いている人はあまりいないと思うので
基本的にOllyDbgのシンボルに合わせてあります。

  // If you like Microsoft compiler, this will force byte alignment and verify
  // that character is set to unsigned.
  #ifdef _MSC_VER
  ...
  #endif
+ #ifdef __GNUC__
+   #pragma pack(1)
+   #ifndef cdecl
+     #define cdecl __cdecl
+   #endif
+   #undef _export
+   #if IMMDBGVER >= 180
+     #define ODBG_Plugindata      IMMDBG_Plugindata
+     #define ODBG_Plugininit      IMMDBG_Plugininit
+     #define ODBG_Pluginmainloop  IMMDBG_Pluginmainloop
+     #define ODBG_Pluginsaveudd   IMMDBG_Pluginsaveudd
+     #define ODBG_Pluginuddrecord IMMDBG_Pluginuddrecord
+     #define ODBG_Pluginmenu      IMMDBG_Pluginmenu
+     #define ODBG_Pluginaction    IMMDBG_Pluginaction
+     #define ODBG_Pluginshortcut  IMMDBG_Pluginshortcut
+     #define ODBG_Pluginreset     IMMDBG_Pluginreset
+     #define ODBG_Pluginclose     IMMDBG_Pluginclose
+     #define ODBG_Plugindestroy   IMMDBG_Plugindestroy
+     #define ODBG_Paused          IMMDBG_Paused
+     #define ODBG_Pausedex        IMMDBG_Pausedex
+     #define ODBG_Plugincmd       IMMDBG_Plugincmd
+   #else
+     #define IMMDBG_Plugindata      _IMMDBG_Plugindata
+     #define IMMDBG_Plugininit      _IMMDBG_Plugininit
+     #define IMMDBG_Pluginmainloop  _IMMDBG_Pluginmainloop
+     #define IMMDBG_Pluginsaveudd   _IMMDBG_Pluginsaveudd
+     #define IMMDBG_Pluginuddrecord _IMMDBG_Pluginuddrecord
+     #define IMMDBG_Pluginmenu      _IMMDBG_Pluginmenu
+     #define IMMDBG_Pluginaction    _IMMDBG_Pluginaction
+     #define IMMDBG_Pluginshortcut  _IMMDBG_Pluginshortcut
+     #define IMMDBG_Pluginreset     _IMMDBG_Pluginreset
+     #define IMMDBG_Pluginclose     _IMMDBG_Pluginclose
+     #define IMMDBG_Plugindestroy   _IMMDBG_Plugindestroy
+     #define IMMDBG_Paused          _IMMDBG_Paused
+     #define IMMDBG_Pausedex        _IMMDBG_Pausedex
+     #define IMMDBG_Plugincmd       _IMMDBG_Plugincmd
+
+     #define ODBG_Plugindata      _IMMDBG_Plugindata
+     #define ODBG_Plugininit      _IMMDBG_Plugininit
+     #define ODBG_Pluginmainloop  _IMMDBG_Pluginmainloop
+     #define ODBG_Pluginsaveudd   _IMMDBG_Pluginsaveudd
+     #define ODBG_Pluginuddrecord _IMMDBG_Pluginuddrecord
+     #define ODBG_Pluginmenu      _IMMDBG_Pluginmenu
+     #define ODBG_Pluginaction    _IMMDBG_Pluginaction
+     #define ODBG_Pluginshortcut  _IMMDBG_Pluginshortcut
+     #define ODBG_Pluginreset     _IMMDBG_Pluginreset
+     #define ODBG_Pluginclose     _IMMDBG_Pluginclose
+     #define ODBG_Plugindestroy   _IMMDBG_Plugindestroy
+     #define ODBG_Paused          _IMMDBG_Paused
+     #define ODBG_Pausedex        _IMMDBG_Pausedex
+     #define ODBG_Plugincmd       _IMMDBG_Plugincmd
+   #endif
+ #endif


ImmunityDebugger.defをOllyDbgの時と同様に変換します。
1.7Xと1.8X用のインポートテーブル分を別々に生成します。(差分は-Uオプション)

# dlltoolが処理できない不要な宣言削除、 関数名の_を外す
$ sed -e 's/^CODE.*//' -e 's/^EXETYPE.*//' -e 's/^DATA.*//' -e '/^$/d' -e 's/_//' ImmunityDebugger.def \
   > ImmunityDebugger.def.mingw
# gccで扱える形式に変換 (1.7X用)
$ i686-pc-mingw32-dlltool -U --dllname ImmunityDebugger.exe --input-def ImmunityDebugger.def.mingw --output-lib immdbg17.a
# gccで扱える形式に変換 (1.8X用)
$ i686-pc-mingw32-dlltool --dllname ImmunityDebugger.exe --input-def ImmunityDebugger.def.mingw --output-lib immdbg18.a


プラグインのソースをコンパイルする際にバーションを引数で渡して振り分けます。

# 1.7X用オブジェクトファイル生成
$ i686-pc-mingw32-gcc -funsigned-char -mwindows -c Bookmark.c -DIMMDBGVER=173 -o Bookmark_17.o
# 1.8X用オブジェクトファイル生成
$ i686-pc-mingw32-gcc -funsigned-char -mwindows -c Bookmark.c -DIMMDBGVER=183 -o Bookmark_18.o


リンク時にそれぞれのバージョンのライブラリにリンクします。

# 1.7X用DLL生成
$ i686-pc-mingw32-gcc -funsigned-char -mwindows Bookmark_17.o immdbg17.a \
    -o Bookmark_17.dll -W -shared -Wl,--dll
# 1.8X用DLL生成
$ i686-pc-mingw32-gcc -funsigned-char -mwindows Bookmark_18.o immdbg18.a \
    -o Bookmark_18.dll -W -shared -Wl,--dll


インクルードファイルとリンクするライブラリを振り分けてあげれば
一つのソースコードでOllyDbg 1.10も含む各バージョンで動くプラグイン
が生成できます。


既にバイナリになっている物はImmunity Debugger Plugin Fixer Tool等で
書き換えるしか無いですが、折角プラグインを作るなら最初からそれぞれで
動くバージョンを用意した方が良いんじゃ無いでしょうか。
ほとんど差が無いのでそれほど手間も掛かりませんし。