現在地

緊急コラム: glibc 脆弱性( CVE-2015-0235 )の影響範囲の調査方法について


先週公開された glibc の脆弱性について、管理されているサーバーへの影響の有無を気にされている方が多いと思います。そこで緊急コラムとして、脆弱性の見つかった関数が使われているかどうかを調査する方法について紹介します。

この脆弱性について調査するにあたり、

ステップ1:
脆弱性の見つかった関数を呼び出している可能性があるかどうか?
ステップ2:
脆弱性の見つかった関数を呼び出している場合、脆弱性を攻撃するために任意の値を攻撃者が渡すことができるかどうか?
ステップ3:
任意の値を攻撃者が渡すことができる場合、どのような被害が生じうるか?

という3つのステップを考える必要があります。

ステップ1に関しては、

  1. ライブラリを静的にリンクしたプログラムファイルは、この記事で紹介している方法では調査できない。
  2. glibc 以外のライブラリを経由して呼び出している場合は調査手順が複雑になる。
  3. .tar.gz などのアーカイブファイルに含まれているファイルは、内容を抽出しないと調査できない。

などの制約がありますが、ある程度は机上で調査することが可能です。なお、 2.の制約については、実測に基づく調査と組み合わせることで緩和することが可能です。

ステップ2に関しては、第10回「ソースコード閲覧ノススメ」( http://www.intellilink.co.jp/article/column/oss10.html )で紹介した手順に沿って、プログラムのソースパッケージをダウンロードして展開して調査することになるかと思います。ただし、対象となるプログラムが多数あり、複雑さは千差万別であるため、何百時間もの稼働と何百万円とかいう費用をかけて、気が遠くなるような調査を行う覚悟が必要かもしれません。

glibc は Linux 上で動作するほぼ全てのプログラムから使用されているライブラリであるため、脆弱性の見つかった関数が呼び出されているかどうかを簡単かつ確実に判断できる万能な方法は存在しません。脆弱性の影響の有無の調査結果を待っている間に侵入されてしまっては意味がありません。第9回「アップデートノススメ」( http://www.intellilink.co.jp/article/column/oss09.html )で紹介した通り、RHEL のメジャーバージョンが同じである間はアプリケーションの互換性が維持されています。 Red Hat 社の記事 GHOST: glibc 脆弱性 (CVE-2015-0235) ( https://access.redhat.com/ja/articles/1333303 )を確認の上、素直にアップデートするほうが安らかな夜を迎えられる可能性が高いと思います。

調査方法1:机上で調査する方法

今回発見された脆弱性は gethostbyname という名前で始まる関数の中に存在していることから、プログラムやライブラリ内で定義されている/呼び出しているシンボルを列挙する nm コマンドを用いてある程度の調査を行うことができます。

まず、 glibc パッケージの中から対象となるシンボルの一覧を列挙してみると、gethostbyname() gethostbyname_r() gethostbyname2() gethostbyname2_r() の4つが該当していることを確認 (*1) できます。

次に、システム上に存在している、実行可能ビットが付与されている全ての通常ファイル(プログラムやライブラリなど)を対象に、 gethostbyname という名前で始まるシンボルを列挙 (*2) (*3) してみます。

   # /usr/bin/find / -type f \( -perm /100 -o -perm /10 -o -perm /1 \) -exec /usr/bin/nm -A -D -- \{\} \; 2> /dev/null | /bin/grep -F " gethostbyname"

上記の実行結果から、様々なプログラムやライブラリが対象となることを確認できます。例えば、 RHEL 6 以降の /bin/tar に対して nm コマンドを実行すると、 tar コマンドは gethostbyname というシンボルを参照していること (*4) が判ります。

ライブラリ内の関数呼び出しを追跡する ltrace コマンドを用いると、その関数が実際に呼ばれているかどうかを確認することができます。 ltrace コマンドを経由してtar コマンドを実行すると、実際に gethostbyname() 関数を呼び出していることを確認 (*5) できます。

さらに、メモリ関連のエラーを検出する valgrind というコマンドを経由してtar コマンドに対して非常に長いIPアドレスを渡してやると、この脆弱性が修正される前の glibc を使用している場合には gethostbyname() 関数から呼ばれる__nss_hostname_digits_dots() 関数内でメモリ破壊が発生していることも確認 (*6)できます。

注意が必要なのは、 glibc 以外のライブラリを経由して間接的に glibc のライブラリを呼び出している場合です。例えば、 RHEL 6 以降の /bin/rpm に対して nm コマンドを実行すると、 rpm コマンドは gethostbyname というシンボルを参照していないという結果になります (*7) 。しかし、プログラムやライブラリの情報を出力する objdump コマンドを用いて依存関係のあるライブラリを抽出してみる (*8) と、 RHEL 7 における /bin/rpm というプログラムは librpm.so.3 というライブラリを必要とすることが確認 (*9) できます。
そして、 librpm.so.3 に対して nm コマンドを実行した結果 (*10) より、 rpmコマンドも間接的に gethostbyname というシンボルを参照していることが判ります。ltrace コマンドを経由して rpm コマンドを実行すると、 rpm コマンドが実際にgethostbyname() 関数を呼び出していることを確認 (*11) できます。

以上より、以下のように ltrace コマンドを用いることで、今回脆弱性の見つかった関数を実際に呼び出しているかどうかを調査できる (*12) 筈です。

  $ /usr/bin/ltrace -tt -T -f -s 4096 -e gethostbyname -e gethostbyname2 -e gethostbyname_r -e gethostbyname2_r 調査対象のプログラムのコマンドライン

しかし、机上で調査する方法では、インストールされているプログラムやライブラリが実際に使用されているかどうかについては考慮できません。また、攻撃者が攻撃できるかどうか(ステップ2やステップ3)を判断するために、対象のプログラムがどこからどのように起動されているのかも含めて追跡する目的では力不足です。そこで、RHEL 7 の場合には、次に紹介する実測に基づき調査する方法を併用することができます。

調査方法2:実測に基づき調査する方法( RHEL 7 用)

実測に基づき調査する方法は緊急コラム: bash 脆弱性( CVE-2014-6271 )の影響範囲の調査方法について( http://www.intellilink.co.jp/article/column/oss-ex20140930.html )で説明した方法と似ているのですが、状況が異なっています。

インストールされているほぼ全てのプログラムが、今回脆弱性の見つかった関数を提供している /lib/libc.so.6 あるいは /lib64/libc.so.6 を使用しているため、第12回「 System Call Auditing ノススメ」( http://www.intellilink.co.jp/article/column/oss12.html )で紹介した手順に沿って libc.so.6 ファイルのオープンを捕捉しても意味がありません。

Linux 3.5 で追加された UPROBES 機能が利用可能なカーネルの場合、第14回「 SystemTap ノススメ」( http://www.intellilink.co.jp/article/column/oss14.html )に登場したSystemTap を用いてユーザ空間の関数呼び出しも捕捉できるようになる (*13) ため、System Call Auditing や TOMOYO Linux / AKARI のようにカーネル内部で捕捉する方法では対処できない今回のようなケースにも対応できるようになります。以下に手順を示します。

1. kernel の debuginfo パッケージと systemtap パッケージをインストールします。

  # debuginfo-install kernel
  # yum install systemtap

2. gethostbyname.stp (*14) をコンパイルして stap_gethostbyname.ko を作成します。

  # stap -p4 -g -DMAXSTRINGLEN=4096 -m stap_gethostbyname gethostbyname.stp 

3. stap_gethostbyname.ko をロードした状態でシステムを稼働させることにより、 /lib/libc.so.6 あるいは /lib64/libc.so.6 が提供している gethostbyname で始まる関数の呼び出し履歴を取得する (*15) ことができます。

  # staprun stap_gethostbyname.ko

Linux にはパフォーマンス計測やデバッグなどのために使える様々なツールが存在しています。是非、これらのツールの存在を知って、問題解決に役立てていただきたいと思います。

(*1) 以下に示します。出力される内容はバージョンにより異なります。

---------- 実行結果例 ここから ----------
$ for i in `/bin/rpm -ql glibc nscd`; do /usr/bin/nm -A -D -- $i ; done 2> /dev/null | /bin/grep " gethostbyname"
/lib64/libc-2.17.so:000000000010e8a0 T gethostbyname
/lib64/libc-2.17.so:000000000010eaa0 T gethostbyname2
/lib64/libc-2.17.so:000000000010ecb0 T gethostbyname2_r
/lib64/libc-2.17.so:000000000010f070 T gethostbyname_r
/lib64/libc.so.6:000000000010e8a0 T gethostbyname
/lib64/libc.so.6:000000000010eaa0 T gethostbyname2
/lib64/libc.so.6:000000000010ecb0 T gethostbyname2_r
/lib64/libc.so.6:000000000010f070 T gethostbyname_r
---------- 実行結果例 ここまで ----------

(*2) -perm /numeric という構文をサポートしていない古い find コマンドの場合、 /numeric の代わりに +numeric を使用します。 RHEL 7 で使われている findコマンドでは、以下の報告にあるように +numeric を指定した場合の挙動がRHEL 6 まで使われていた find コマンドとは異なっていますので、注意してください。

     Bug 1116237 - find -perm +numeric does not work as expected
     https://bugzilla.redhat.com/show_bug.cgi?id=1116237

(*3) nm コマンドには複数のパス名を指定することができるので、本来であれば find コマンドの -print0 と xargs コマンドの -0 とを組み合わせることで効率的に処理できる筈です。しかし、以下の報告にあるように期待通りに動作しないため、find コマンドの -exec を用いて1個ずつ処理するようにしています。

     Bug 1022845 - binutils: nm -D does not process subsequent files after "No symbols".
     https://bugzilla.redhat.com/show_bug.cgi?id=1022845

(*4) 以下に示します。

---------- 実行結果例 ここから ----------
$ /usr/bin/nm -A -D /bin/tar | /bin/grep -F " gethostbyname"
/bin/tar:                 U gethostbyname
---------- 実行結果例 ここまで ----------

(*5) 以下に示します。

---------- 実行結果例 ここから ----------
$ /usr/bin/ltrace -tt -T -e gethostbyname /bin/tar -tf 127.0.0.1:
09:56:30.325517 tar->gethostbyname("127.0.0.1")  = 0x7f7c98f34e20 <0.002021>
bash: /etc/rmt: No such file or directory
09:56:30.541222 --- SIGCHLD (Child exited) ---
/bin/tar: 127.0.0.1\:: Cannot open: Input/output error
/bin/tar: Error is not recoverable: exiting now
09:56:30.541839 +++ exited (status 2) +++
---------- 実行結果例 ここまで ----------

(*6) 以下に示します。

---------- 実行結果例 ここから ----------
$ /usr/bin/valgrind /bin/tar -tf 127.0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000.0.1:
==3856== Memcheck, a memory error detector
==3856== Copyright (C) 2002-2013, and GNU GPL'd, by Julian Seward et al.
==3856== Using Valgrind-3.9.0 and LibVEX; rerun with -h for copyright info
==3856== Command: /bin/tar -tf 127.0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000.0.1:
==3856==
==3856== Invalid write of size 1
==3856==    at 0x4C2B430: __GI_strcpy (in /usr/lib64/valgrind/vgpreload_memcheck-amd64-linux.so)
==3856==    by 0x5369520: __nss_hostname_digits_dots (in /usr/lib64/libc-2.17.so)
==3856==    by 0x536E92F: gethostbyname (in /usr/lib64/libc-2.17.so)
==3856==    by 0x4263FC: ??? (in /usr/bin/tar)
==3856==    by 0x4088D9: ??? (in /usr/bin/tar)
==3856==    by 0x41922D: ??? (in /usr/bin/tar)
==3856==    by 0x404F76: ??? (in /usr/bin/tar)
==3856==    by 0x5281AF4: (below main) (in /usr/lib64/libc-2.17.so)
==3856==  Address 0x60d4ef1 is 0 bytes after a block of size 1,041 alloc'd
==3856==    at 0x4C2A3AA: realloc (in /usr/lib64/valgrind/vgpreload_memcheck-amd64-linux.so)
==3856==    by 0x536946B: __nss_hostname_digits_dots (in /usr/lib64/libc-2.17.so)
==3856==    by 0x536E92F: gethostbyname (in /usr/lib64/libc-2.17.so)
==3856==    by 0x4263FC: ??? (in /usr/bin/tar)
==3856==    by 0x4088D9: ??? (in /usr/bin/tar)
==3856==    by 0x41922D: ??? (in /usr/bin/tar)
==3856==    by 0x404F76: ??? (in /usr/bin/tar)
==3856==    by 0x5281AF4: (below main) (in /usr/lib64/libc-2.17.so)
==3856==
==3856== Invalid write of size 1
==3856==    at 0x4C2B443: __GI_strcpy (in /usr/lib64/valgrind/vgpreload_memcheck-amd64-linux.so)
==3856==    by 0x5369520: __nss_hostname_digits_dots (in /usr/lib64/libc-2.17.so)
==3856==    by 0x536E92F: gethostbyname (in /usr/lib64/libc-2.17.so)
==3856==    by 0x4263FC: ??? (in /usr/bin/tar)
==3856==    by 0x4088D9: ??? (in /usr/bin/tar)
==3856==    by 0x41922D: ??? (in /usr/bin/tar)
==3856==    by 0x404F76: ??? (in /usr/bin/tar)
==3856==    by 0x5281AF4: (below main) (in /usr/lib64/libc-2.17.so)
==3856==  Address 0x60d4ef8 is 7 bytes after a block of size 1,041 alloc'd
==3856==    at 0x4C2A3AA: realloc (in /usr/lib64/valgrind/vgpreload_memcheck-amd64-linux.so)
==3856==    by 0x536946B: __nss_hostname_digits_dots (in /usr/lib64/libc-2.17.so)
==3856==    by 0x536E92F: gethostbyname (in /usr/lib64/libc-2.17.so)
==3856==    by 0x4263FC: ??? (in /usr/bin/tar)
==3856==    by 0x4088D9: ??? (in /usr/bin/tar)
==3856==    by 0x41922D: ??? (in /usr/bin/tar)
==3856==    by 0x404F76: ??? (in /usr/bin/tar)
==3856==    by 0x5281AF4: (below main) (in /usr/lib64/libc-2.17.so)
==3856==
bash: /etc/rmt: No such file or directory
==3856== Warning: invalid file descriptor -1 in syscall close()
==3856== Warning: invalid file descriptor -1 in syscall close()
/bin/tar: 127.0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000.0.1\:: Cannot open: Input/output error
/bin/tar: Error is not recoverable: exiting now
==3856==
==3856== HEAP SUMMARY:
==3856==     in use at exit: 19,609 bytes in 6 blocks
==3856==   total heap usage: 109 allocs, 103 frees, 43,968 bytes allocated
==3856==
==3856== LEAK SUMMARY:
==3856==    definitely lost: 0 bytes in 0 blocks
==3856==    indirectly lost: 0 bytes in 0 blocks
==3856==      possibly lost: 0 bytes in 0 blocks
==3856==    still reachable: 19,609 bytes in 6 blocks
==3856==         suppressed: 0 bytes in 0 blocks
==3856== Rerun with --leak-check=full to see details of leaked memory
==3856==
==3856== For counts of detected and suppressed errors, rerun with: -v
==3856== ERROR SUMMARY: 8 errors from 2 contexts (suppressed: 3 from 3)
---------- 実行結果例 ここまで ----------

(*7) 以下に示します。

---------- 実行結果例 ここから ----------
$ /usr/bin/nm -A -D /bin/rpm | /bin/grep -F " gethostbyname"
---------- 実行結果例 ここまで ----------

(*8) 以下のページにあるように、セキュリティ上の理由から ldd コマンドではなく objdump コマンドを使用しています。

     Man page of LDD
     http://linuxjm.sourceforge.jp/html/LDP_man-pages/man1/ldd.1.html

(*9) 以下に示します。

---------- 実行結果例 ここから ----------
$ /usr/bin/objdump -p /bin/rpm | /bin/grep NEEDED
  NEEDED               librpm.so.3
  NEEDED               librpmio.so.3
  NEEDED               libselinux.so.1
  NEEDED               libcap.so.2
  NEEDED               libacl.so.1
  NEEDED               libdb-5.3.so
  NEEDED               libbz2.so.1
  NEEDED               libelf.so.1
  NEEDED               liblzma.so.5
  NEEDED               liblua-5.1.so
  NEEDED               libm.so.6
  NEEDED               libnss3.so
  NEEDED               libpopt.so.0
  NEEDED               libz.so.1
  NEEDED               libdl.so.2
  NEEDED               libpthread.so.0
  NEEDED               libc.so.6
---------- 実行結果例 ここまで ----------

(*10) 以下に示します。

---------- 実行結果例 ここから ----------
$ /usr/bin/nm -A -D /usr/lib64/librpm.so.3 | /bin/grep -F " gethostbyname"
/usr/lib64/librpm.so.3:                 U gethostbyname
---------- 実行結果例 ここまで ----------

(*11) 以下に示します。

---------- 実行結果例 ここから ----------
$ /usr/bin/ltrace -tt -T -e gethostbyname /bin/rpm -qlp http://127.0.0.1/
10:00:00.293391 librpm.so.3->gethostbyname("localhost") = 0x7fa42db26e20 <0.015916>
curl: (7) Failed connect to 127.0.0.1:80; Connection refused
10:00:00.358327 --- SIGCHLD (Child exited) ---
error: open of http://127.0.0.1/ failed: No such file or directory
10:00:00.359835 +++ exited (status 1) +++
---------- 実行結果例 ここまで ----------

(*12) 以下の報告にあるように、古い ltrace コマンドではマルチスレッドのプログラムを扱えませんので、該当する方はアップデートしてください。

Bug 742340 - ltrace cannot properly handle multi-threaded processes
https://bugzilla.redhat.com/show_bug.cgi?id=742340

Bug 526007 - ltrace cannot properly handle multi-threaded processes
https://bugzilla.redhat.com/show_bug.cgi?id=526007

(*13) 以下の連載では、 RHEL 6 上で SystemTap を用いて Java をデバッグする事例が紹介されています。

     Java on Linuxを鬼凄ネイティブデバッグ!
     http://www.atmarkit.co.jp/ait/kw/java_on_linux_wo_onisugo.html

(*14) 以下に示します。

---------- gethostbyname.stp ここから ----------
global task_domain%[32768];
function get_current:long() {
  return task_current() & %{ ULONG_MAX %};
}
function is_success:long(ret:long) {
  return ret <= -4096 || ret >= 0;
}
function make_domain:string() {
  task = get_current();
  if (task_domain[task] == "")
    task_domain[task] = sprintf("%s(%d) ", execname(), pid());
  return task_domain[task];
}
probe kernel.function("copy_process").return {
  if (is_success($return))
    task_domain[$return] = make_domain();
}
probe kernel.function("do_execve") {
  make_domain();
}
probe kernel.function("install_exec_creds") {
  task_domain[get_current()] .= sprintf("%s(%d) ", execname(), pid());
}
probe kernel.function("free_task") {
  delete task_domain[$tsk];
}
probe end {
  delete task_domain;
}
probe process("/lib*/libc.so.6").function("gethostbyname*") {
  printf("[%s] Called by uid=%d from %s\n", ctime(gettimeofday_s()),
         uid(), make_domain());
}
---------- gethostbyname.stp ここまで ----------

(*15) 以下に示します。

---------- 実行結果例 ここから ----------
[Mon Feb  2 10:08:53 2015] Called by uid=0 from sshd(1592) sshd(5423) bash(5425) tar(5476)
[Mon Feb  2 10:08:53 2015] Called by uid=0 from sshd(1592) sshd(5478)
[Mon Feb  2 10:09:29 2015] Called by uid=0 from sshd(1592) sshd(5423) bash(5425) rpm(5519)
[Mon Feb  2 10:09:29 2015] Called by uid=0 from sshd(1592) sshd(5423) bash(5425) rpm(5519)
[Mon Feb  2 10:09:58 2015] Called by uid=0 from sshd(1592) sshd(5423) bash(5425) yum(5548)
[Mon Feb  2 10:09:58 2015] Called by uid=0 from sshd(1592) sshd(5423) bash(5425) yum(5548)
---------- 実行結果例 ここまで ----------