VoIPのオープンソースライブラリPJSIPにおけるバッファオーバーフロー

こんにちは。セキュリティ室(アプリケーションセキュリティチーム)で主にLINEサービスのセキュリティ診断を担当しているYoungsung Kim(Facebookアカウント/Twitterアカウント)です。

これはLINE Advent Calendar 2017の24日目の記事です。

今日はVoIPのオープンソースライブラリであるPJSIPの脆弱性(CVE-2017-16872およびAST-2017-009)について書かせていただきます。PJSIPは、標準プロトコル(SIP、SDP、RTP、STUN、TURN、ICE)を実装したオープンソースのマルチメディア通信ライブラリです。たとえばIP PBXやVoIPゲートウェイなどで広く使用されているAsteriskフレームワークは、PJSIPを使用してSIPスタックを実装しています。

はじめに

今回発見した脆弱性は、64ビット環境においてクライアントから受け取ったデータを処理する際にsigned intからunsigned longへの暗黙的な型変換(型キャスト)が行われており、そこで整数型の符号拡張を考慮していないことが原因でバッファオーバーフローが発生するというものでした。この脆弱性についての詳細は、「(Security) Function in PJSIP 2.7 miscalculates the length of an unsigned long variable in 64bit machines」を参照してください。

私は最初にこの脆弱性を発見したとき、まずPJSIPの開発チームに問い合わせましたが、そのチームにはセキュリティの担当者がいなかったためAsteriskセキュリティチームとコンタクトをとることになりました。その後、Asteriskの開発者George Joseph氏とパッチについて話し合い、無事PJSIPとAsteriskにそのパッチが適用されました(PJSIPおよびAsterisk)。一緒に修正に取り組んでくれた開発者のJoseph氏に、この場を借りてお礼を申し上げたいと思います。

脆弱性

C言語では関数を呼び出す際に引数の型が異なっていた場合、暗黙的に型変換が行われます。たとえばfunction(int a)という関数を呼び出すときにchar型の引数を指定すると、引数はint型に変換されて関数内で処理されます。厳密に書くなら、符号付きの状態を維持したままsigned char型がsigned int型に拡張されます。符号付きの場合、最上位ビットは値の正負を意味しますから、最上位ビットの値が0なら0、1なら1のまま拡張が行われます。以下の画像の例においては、最上位ビットの値に応じて00もしくはFFが3バイト分追加される、と言い換えてもよいでしょう。

sign_extension_1

同じ符号付きの型同士なら型変換は分かりやすいです。ではsigned char型からunsigned int型へ型変換するとどうなるでしょうか。次の画像を見てください。

sign_extension_2

signed char型における-5は、unsigned int型では4294967291となります。符号なしの場合は負の値を扱いませんから当然といえば当然ですが、このような変化は境界チェックのバイパスやバッファオーバーフローを引き起こす原因となります。

PJSIPは、create_tsx_key_2543関数内でpj_utoa関数を呼び出しますが、渡される引数rdata->msg_info.cseq->cseqsigned int型です。

// pjsip/src/pjsip/sip_transaction.c
static pj_status_t create_tsx_key_2543( ... )
{
...
    /* Add CSeq (only the number). */
    len = pj_utoa(rdata->msg_info.cseq->cseq, p); // rdata->msg_info.cseq->cseq : pj_int32_t 
                                                  // (defined: pjsip/include/pjsip/sip_msg.h)

しかしpj_utoaの第1引数valunsigned long型です。よって、もし負の値をvalに入れてこの関数を呼ぶと、関数内でvalはとても巨大な数値(8バイトなら18446744073709551615)になってしまいます。

sign_extension_utoa
// pjlib/src/pj/string.c
PJ_DEF(int) pj_utoa(unsigned long val, char *buf)
{
    // Note that passing a negative `val` can result in a large unsigned long value.
    // In 64bit machines, the size of unsigned long is 8 bytes.
    // Max unsigned long = 0xFFFFFFFFFFFFFFFF = 18446744073709551615

    return pj_utoa_pad(val, buf, 0, 0);
}

pj_utoa関数内に処理が入った時点で、val(=Cseq number)int型からunsigned long型に(暗黙的に)型変換されています。そしてpj_utoa_pad関数は第1引数valを文字列に変えて第2引数のbufに格納します。したがって、bufのサイズは最大でlength("18446744073709551615" + NULL)、すなわち21バイト必要になります。

しかし、create_tsx_key_2543関数ではCSeq numberVia portのためのサイズを9バイトしか確保していません。

// pjsip/src/pjsip/sip_transaction.c
/*
 * Create key from the incoming data, to be used to search the transaction
 * in the transaction hash table.
 */
static pj_status_t create_tsx_key_2543( ... )
{
...
    /* Calculate length required. */
    // Note that Cseq, Via port are calculated as 9bytes each.
    len_required = method->name.slen +      /* Method */ 
                   9 +                      /* CSeq number */
                   ...
                   9 +                      /* Via port. */
                   16;                      /* Separator+Allowance. */
    key = p = (char*) pj_pool_alloc(pool, len_required);

...
    /* Add CSeq (only the number). */
    len = pj_utoa(rdata->msg_info.cseq->cseq, p);    // rdata->msg_info.cseq->cseq : pj_int32_t
                                                     // Implicit type casting from int to unsigned long
    p += len;
    *p++ = SEPARATOR;

...
    len = pj_utoa(rdata->msg_info.via->sent_by.port, p); // rdata->msg_info.via->sent_by.port : int
                                                         // Implicit type casting from int to unsigned long
    p += len;
    *p++ = SEPARATOR;

    *p++ = '\0';
...
    return PJ_SUCCESS;
}

まずpj_pool_allocを用いてヒープから必要なサイズを取得し、それをlen_requiredへ格納する処理です。len_required変数に必要なサイズを入れていますが、Cseq numberVia portのためにはたった9バイトずつしか確保されていません。しかし、実際は21バイト必要なため、pj_pool_alloc関数で確保する領域を超えてしまいます。ちなみに、仮に符号拡張がなくともint型は4バイトで負の値も扱えるため文字列としてはlength("-2147483648" + NULL)で12バイト必要ですから、そもそも9バイトでは足りていません。この時点で既に正しくない実装なのですが、Separator+Allowanceとして、実際に使用されるサイズよりも余分に(不足分を許容できる)16バイトが確保されているため問題にはなりませんでした。

しかし21バイトとなると話は別で、これにより確保した領域を超えてしまいcreate_tsx_key_2543関数内でヒープバッファオーバーフローが発生することになります。rdata->msg_info.cseq->seqrdata->msg_info.via->sent_by.portpj_utoaの引数ですが、どちらもint型であるため負の値を入れることができます。

sign_extension_utoa

実証

ヒープ領域でオーバーフローすることは分かりました。しかし、PJSIPにはパフォーマンスのために(既存のメモリ管理機構をラップする形で)独自のメモリ管理モジュールが実装されています。これにより、たとえばAddressSanitizerのようなツールを用いてもオーバーフローを検出できません。

またPJSIPは最初にサイズの大きいブロックを1つ確保し、allocateされるたびにそのブロックからメモリ領域を割り当てます。つまり仮にオーバーフローが起こってもブロック内の次の領域(下図のcurからendの領域)が上書きされるだけであるため、簡単には攻撃が成功しないのです。

pool

今回の脆弱性を利用して実際に攻撃を成功させるためには、入力サイズを工夫し、curからendの領域のサイズを0に調整する必要がありました。そしてmethod->name.slenは、SIPメッセージ(クライアントから送信するデータ)のCSeqフィールドのメソッド文字列を通じて操作できるため、create_tsx_key_2543内で、method->name.slenのサイズを調整することで、割り当て済みブロックの残りサイズ(curからendの領域)を0にできました。実証コードについては、前述の「(Security) Function in PJSIP 2.7 miscalculates the length of an unsigned long variable in 64bit machines」を参照してください。

そして下記sip_parser.cstring.cが主な修正箇所になります。hdr->cseqが負の値にならないようにする対策コードが追加されました。またcreate_tsx_key_2543以外の場所でもいくつかこの問題は見つかっていますが、すべて修正されています。

// pjsip/src/pjsip/sip_parser.c
/* Parse CSeq header. */
static pjsip_hdr* parse_hdr_cseq( pjsip_parse_ctx *ctx )
{
    pj_str_t cseq, method;
    pjsip_cseq_hdr *hdr;
...
    // Parses only the numeric value of the CSeq header field and store it in cseq variable.
    pj_scan_get( ctx->scanner, &pconst.pjsip_DIGIT_SPEC, &cseq); // 

    // Converts a cseq value, which is a string in decimal form, to an integer.
    // Type casting from 'unsigned long' to 'pj_int32_t'
    // 'hdr->cseq' can be set to a negative integer.
    hdr->cseq = pj_strtoul(&cseq); // hdr->cseq : pj_int32_t 
                                   // (defined: pjsip/include/pjsip/sip_msg.h)
...
}
// pjlib/src/pj/string.c
PJ_DEF(unsigned long) pj_strtoul(const pj_str_t *str)
{   
    unsigned long value;
    unsigned i;

    PJ_CHECK_STACK();
        
    value = 0;
    for (i=0; i<(unsigned)str->slen; ++i) {
        if (!pj_isdigit(str->ptr[i]))
            break;
        value = value * 10 + (str->ptr[i] - '0');
    }
    return value;
} 

まとめ

今回紹介したのはほんの一例で、分析の結果、他にもさまざまな場所で同じようなオーバーフローの問題があることが分かりました。それらの根本的な解決のため、文字列を数値に変換する関数に対して負の値を設定できないように修正を加えました。

このような整数型にまつわるバグは今回のライブラリに限らず割とさまざまな場所で見つかっており、いわゆる典型的な脆弱性のひとつです(そして特に開発者にとっては発見しにくいバグでもあります)。C言語で発生しやすい整数型のバグについては、「SEI CERT C Coding Standard」の「Rule 04. Integers (INT)」を参考にしてください。

さて、明日はいよいよAdvent Calendar最終日、Paul Traylorさんの「Prometheusをサービスとして提供する」です。お楽しみに。

Related Post