こんにちは。セキュリティ室(アプリケーションセキュリティチーム)で主に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バイト分追加される、と言い換えてもよいでしょう。

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

signed char
型における-5
は、unsigned int
型では4294967291
となります。符号なしの場合は負の値を扱いませんから当然といえば当然ですが、このような変化は境界チェックのバイパスやバッファオーバーフローを引き起こす原因となります。
PJSIPは、create_tsx_key_2543
関数内でpj_utoa
関数を呼び出しますが、渡される引数rdata->msg_info.cseq->cseq
はsigned 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引数val
はunsigned long
型です。よって、もし負の値をval
に入れてこの関数を呼ぶと、関数内でval
はとても巨大な数値(8バイトなら18446744073709551615
)になってしまいます。

// 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 number
やVia 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 number
とVia 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->seq
とrdata->msg_info.via->sent_by.port
はpj_utoa
の引数ですが、どちらもint
型であるため負の値を入れることができます。

実証
ヒープ領域でオーバーフローすることは分かりました。しかし、PJSIPにはパフォーマンスのために(既存のメモリ管理機構をラップする形で)独自のメモリ管理モジュールが実装されています。これにより、たとえばAddressSanitizerのようなツールを用いてもオーバーフローを検出できません。
またPJSIPは最初にサイズの大きいブロックを1つ確保し、allocateされるたびにそのブロックからメモリ領域を割り当てます。つまり仮にオーバーフローが起こってもブロック内の次の領域(下図のcurからendの領域)が上書きされるだけであるため、簡単には攻撃が成功しないのです。

今回の脆弱性を利用して実際に攻撃を成功させるためには、入力サイズを工夫し、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.c
とstring.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をサービスとして提供する」です。お楽しみに。