glibcのGHOST脆弱性の内容と検出

Security 1月 29, 2015
(Last Updated On: 2018年8月4日)

glibcのgethostbynameのバッファーオーバーフローバグであるGHOSTがどのようなバグだったのか気になったので調べてみました。Twitterで「GHOSTは4バイトだけオーバーフローする」といったツイートを見かけたことが調べはじめた切っ掛けです。

追記だらけになって解りづらいので別エントリでまとめています。

http://blog.ohgaki.net/glibc-ghost-revisited

参考:より新しいglibcのgetaddrinfoの問題はこちら

バグの内容

これだけ話題になっていると既に調べている方が直ぐに見つかりました。

これによると1.1.1.1などIPアドレス形式の場合に必要なバッファ計算に誤りがあり、そのバッファの最後に任意文字列を書き込める為に問題が発生しているようです。

問題の発生箇所と修正は以下としています。

       size_needed = (sizeof (*host_addr)
-                    + sizeof (*h_addr_ptrs) + strlen (name) + 1);
+                    + sizeof (*h_addr_ptrs)
+                    + sizeof (*h_alias_ptr) + strlen (name) + 1);

 

sizeof (*h_alias_ptr)が抜けているのでsize_neededが少くなっています。実際に何バイト少く見積もっているのかはコードを見ないと分かりません。sizeof()の中身とこの計算が何度も呼ばれるのか見る必要があります。複数回呼ばれていればさらに少く見積る事になります。

 

バグの検出

システムが利用しているglibcが脆弱であるか検出/テストするプログラムを作っている方が居ないか調べてみると、こちらもすぐ見つかりました。

ここに掲載されているコードを見ると、どのように攻撃するのか凡そ解ります。

#include <netdb.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>

#define CANARY "in_the_coal_mine"

struct {
 char buffer[1024];
 char canary[sizeof(CANARY)];
} temp = { "buffer", CANARY };

int main(void) {
 struct hostent resbuf;
 struct hostent *result;
 int herrno;
 int retval;

/*** strlen (name) = size_needed - sizeof (*host_addr) - sizeof (*h_addr_ptrs) - 1; ***/
 size_t len = sizeof(temp.buffer) - 16*sizeof(unsigned char) - 2*sizeof(char *) - 1;
 char name[sizeof(temp.buffer)];
 memset(name, '0', len);
 name[len] = '\0';
retval = gethostbyname_r(name, &resbuf, temp.buffer, sizeof(temp.buffer), &result, &herrno);

if (strcmp(temp.canary, CANARY) != 0) {
 puts("vulnerable");
 exit(EXIT_SUCCESS);
 }
 if (retval == ERANGE) {
 puts("not vulnerable");
 exit(EXIT_SUCCESS);
 }
 puts("should not happen");
 exit(EXIT_FAILURE);
}

短いコードなので読めば直ぐに解りますが、struct tempにbufferとcanary(オーバーフロー検出用)のメモリを割り当て、bufferには間違ったサイズ計算を使って’0’(文字のゼロ)を割り当てています。

/*** strlen (name) = size_needed - sizeof (*host_addr) - sizeof (*h_addr_ptrs) - 1; ***/
 size_t len = sizeof(temp.buffer) - 16*sizeof(unsigned char) - 2*sizeof(char *) - 1;
 char name[sizeof(temp.buffer)];
 memset(name, '0', len);

準備したバッファをgethostbyname_r()に渡した後に

if (strcmp(temp.canary, CANARY) != 0) {
 puts("vulnerable");
 exit(EXIT_SUCCESS);
 }

としてglibcが脆弱であるかどうか判別しています。

に、「ワーキングバッファの後方にはnameがコピーされます」とあるので、実際にはアクセスできてはならないハズのtemp.canaryが、’0’文字で埋められたtemp.bufferの内容で上書きされてしまいstrcmpで比較すると、脆弱性があるglibcの場合に異ることになります。

Twitterで「GHOSTは4バイトだけオーバーフローする」といった内容のツイートを見かけたのですが、これらの情報が正しいとするとオーバーフローするバイト数はもっと大きな値になります。(追記参照)

 

攻撃方法

見つけた検出プログラムを見ると、割と多くのバイト数オーバーフローするようなので通常のヒープオーバーフロー(GHOSTはヒープオーバーフローと聞いています)の攻撃手法が利用できると考えられます。ヒープオーバーフローの攻撃方法を知りたい方は検索してみてください。(条件が厳しいので簡単に攻撃できるようには見えませんが、少なくともEximへの攻撃は成功するようです)

GHOST脆弱性検出プログラムのコードを見ると、アプリケーションで普通に入力バリデーションしていれば攻撃を防げるように見えます。

 

まとめ

パッチだけでなく、実際のコードを見てみないと本当の所はよく分かりません。参照したブログが誤っている可能性もありますが、見たところ正しい情報のように思います。

ディストリビューションが対応しているので、あれこれ考えず素直に脆弱性対策済みのglibcに入れ替えると良いでしょう。

現物のコードを見ずに書いています。誤りなどがありましたら、指摘頂けると助かります。

追記

検出プログラムはリエントラントなgethostbyname_r()を使いリエントラントなAPIが必要なバッファ引数が不正に上書きされることを利用して脆弱かどうか判断しています。このコードだけ見ると4バイトより大きくオーバーフローできるように思えました。(当初、ヒープオーバーフローだと聞いていまいたがヒープオーバーフローをするのはgethostbyname()で、get_hostbyname_r()の場合はスタックオーバーフローも起こりえます)

取り敢えずテストプログラムを自分で試してみました。為すには、上のテストプログラムをghost.cに保存して

[yohgaki@localhost ~]$ cc ghost.c -o ghost
[yohgaki@localhost ~]$ ./ghost
vulnerable

などとします。脆弱性があるシステムで実行したのでvulnerableと出力されています。

gdbで変数の中身を確認してみます。(gdbを使う場合、gcc -O0 -g ghost.c -o ghost などとしてください)

Breakpoint 1, main () at t.c:21
21	size_t len = sizeof(temp.buffer) - 16*sizeof(unsigned char) - 2*sizeof(char *) - 1;
(gdb) n
23	memset(name, '0', len);
(gdb) n
n24	name[len] = '\0';
(gdb) n
25	retval = gethostbyname_r(name, &resbuf, temp.buffer, sizeof(temp.buffer), &result, &herrno);
(gdb) n
27	if (strcmp(temp.canary, CANARY) != 0) {
(gdb) p temp.canary
$1 = "000\000he_coal_mine"

この脆弱性テストプログラムでは4バイトオーバーフローしています。 バッファーオーバーフローがあるのは確かですが、これを使って成功する攻撃対象のコードと攻撃方法がどんな物かますます興味がでてきました。実際に最大のオーバーフローが4バイト/8バイトで書き込み可能なデータが限定されているとすると、glibc開発者/ディストリビューターが軽視したのも理解できます。

安直にtemp.bufferのサイズを小く指定すれば大きなバッファーオーバーフローが起きるのでは?とテストプログラムのコードを改変して試してみましたが、十分なサイズがない場合はオーバーフローしないようなコードになっているようです。

ちなみにPHPの場合、ビッグエンディアン環境でバイトスタックアンダーフローする場合に任意コード実行が可能になる脆弱性があったことがあります。書き換え可能なバイト数は1バイトで’\0’のみ書き込み可能でした。メモリレイアウトの関係でアンダーフローで戻り先のメモリアドレスを有意に変えることが可能で、その攻撃者が戻り先のメモリ領域に任意のデータを書き込める為、シェルコードを配置できました。もちろん、この脆弱性は今はありません。PHP4の頃の話です。

オーバーフローするメモリが4バイトで内容が制限されていたとすると、今回のC/C++プログラマの教訓は

  • たとえ限定的なヒープオーバーフローでも致命的な脆弱性として扱う

でしょう。全てのアプリケーションプログラマの教訓は

  • アプリで入力バリデーションを行うと未知の脆弱性にも対応できる

でしょう。これは私が何時も伝えている事です。ISO 27000/ISMSでも入力データのバリデーションを要求しています。セキュリティ対策として当たり前のことです。全く行わないアプリなら瑕疵担保責任を超える賠償のリスクも背負うことになると考えられます。

時間があったらglibcのコードを読むのですが。。コードを読まないと確定的なことは言えませんが、次に紹介するURLにOpenWall MLのリンクがあり、そこに詳しく書いてあったので読む必要はないようです。

攻撃手法を分かり易く解説したページがあったので紹介します。

なるほど、です。単独の4バイト(または8バイト)だけのオーバーフローだと攻撃できないですが、インタラクティブな動作をするアプリケーションの場合、やり取りを行うことでコード実行を可能にしています。

GHOSTでオーバーフローするのは4バイト(32ビット環境)または8バイト(64ビット環境)で攻撃を実行するには、ある程度のインタラクティブ性が必要ということです。例えば、ログ解析するだけのツールがIPアドレスを逆引きしているだけの場合、任意コード実行攻撃はほぼ無理と言えます。Webアプリのようにインタラクティブ性がある物はある程度リスクがあると言えますが、ユーザーが入力したIPアドレスを処理することはほとんど無いはずです。

ユーザーがIPアドレスを入力するサービス(IPアドレス逆引きサービスやDNSサーバーの管理アプリなど)の場合などではある程度リスクがあると考えて良いです。しかし、簡単に攻撃できるようなモノではありません。

最初に考えていた通り、攻撃可能だったEximはユーザーが提供したIPアドレス情報をバリデーションせずに利用していた為、この脆弱性の影響を受けることになりました。思いもよらない脆弱性の影響を受けないようにするためにも入力バリデーションは確実に行うべきです。これには変わりありません。今までに同類のケースは山程見てきています。

 

投稿者: yohgaki