CVE-2015-7547 glibc における getaddrinfo の脆弱性について

この脆弱性は2016年2月17日に対策済みバージョンと共に公開されました。内容としては getaddrinfo の呼び出しにおいてスタックバッファオーバーフローが発生する物です。公開時点で以下の通り、PoC を含めた技術的な詳細が公開されています。本記事では、脆弱性が発生するまでの処理と、回避策について解説したいと思います。
CVE-2015-7547 — glibc getaddrinfo() stack-based buffer overflow
Google Online Security Blog: CVE-2015-7547: glibc getaddrinfo stack-based buffer overflow

脆弱性に至るまで

今回の脆弱性は getaddrinfo の呼び出しに起因しています。脆弱性のある箇所までの関数呼び出しは以下の様になっています。

getaddrinfo gaih_inet _nss_dns_gethostbyname4_r __libc_res_nsearch __libc_res_nquerydomain __libc_res_nquery __libc_res_nsend send_dg send_vc
Code language: plaintext (plaintext)

まず、問題となっている2048バイトのスタックは _nss_dns_gethostbyname4_r 関数にて確保しています。攻撃が成功してバッファオーバーフローが発生した場合は、このスタックが上書きされます。

== glibc/resolv/nss_dns/dns-host.c:_nss_dns_gethostbyname4_r _nss_dns_gethostbyname4_r (const char *name, struct gaih_addrtuple **pat, char *buffer, size_t buflen, int *errnop, int *herrnop, int32_t *ttlp) ... // スタック上に2048バイト確保 host_buffer.buf = orig_host_buffer = (querybuf *) alloca (2048); ... // 確保したバッファのポインタとバッファ長を引数に呼び出し int n = __libc_res_nsearch (&_res, name, C_IN, T_UNSPEC, host_buffer.buf->buf, 2048, &host_buffer.ptr, &ans2p, &nans2p, &resplen2);
Code language: C++ (cpp)

渡されたポインタとサイズはそのまま引き継がれて呼び出されて __libc_res_nsend に至ります。この関数は複数のネームサーバへのクエリ、リトライの処理を受け持っています。resolv.conf における nameserver や options attempts の設定が該当します。また、今回の脆弱性の該当箇所である send_dgsend_vc はここから呼び出されます。

== glibc/resolv/res_send.c:__libc_res_nsend // ans=確保したバッファのポインタ(スタック) anssiz=バッファのサイズ(2048) __libc_res_nsend(res_state statp, const u_char *buf, int buflen, const u_char *buf2, int buflen2, u_char *ans, int anssiz, u_char **ansp, u_char **ansp2, int *nansp2, int *resplen2) ... // options attemptsのリトライループ for (try = 0; try < statp->retry; try++) { // nameserverが複数指定されている場合のループ for (ns = 0; ns < MAXNS; ns++) ... same_ns: ... if (__builtin_expect (v_circuit, 0)) { // TCPの名前解決処理 // TCPにフォールバックした場合はattemptsのパラメータは無視される try = statp->retry; n = send_vc(statp, buf, buflen, buf2, buflen2, &ans, &anssiz, &terrno, ns, ansp, ansp2, nansp2, resplen2); if (n < 0) return (-1); // 応答が無い場合は次のnameserverを使用 if (n == 0 && (buf2 == NULL || *resplen2 == 0)) goto next_ns; } else { // UDPの名前解決処理 n = send_dg(statp, buf, buflen, buf2, buflen2, &ans, &anssiz, &terrno, ns, &v_circuit, &gotsomewhere, ansp, ansp2, nansp2, resplen2); if (n < 0) return (-1); // 応答が無い場合は次のnameserverを使用 if (n == 0 && (buf2 == NULL || *resplen2 == 0)) goto next_ns; // TCPへのフォールバック(TCビットが立っている場合) if (v_circuit) goto same_ns; }
Code language: C++ (cpp)

send_dg は UDP のクエリ処理を受け持っています。最初はこちらが呼ばれます。

== glibc/resolv/res_send.c:send_dg // *ansps=確保したバッファのポインタ(スタック) *anssizp=バッファのサイズ(2048) send_dg(res_state statp, const u_char *buf, int buflen, const u_char *buf2, int buflen2, u_char **ansp, int *anssizp, int *terrno, int ns, int *v_circuit, int *gotsomewhere, u_char **anscp, u_char **ansp2, int *anssizp2, int *resplen2) ... if (*thisanssizp < MAXPACKET && anscp #ifdef FIONREAD // ソケットから読み取れる受信データサイズを取得 // 総パケットサイズのみで判定 // レスポンスのデータ構造やトランザクションID等のチェックはこの時点ではしない && (ioctl (pfd[0].fd, FIONREAD, thisresplenp) < 0 // 現在のバッファのサイズに収まるか判定 // *thisanssizpには*anssizpの2048がセットされている || *thisanssizp < *thisresplenp) #endif ) { // 足りないのでヒープに65536バイトの領域を確保 u_char *newp = malloc (MAXPACKET); if (newp != NULL) { // バッファサイズを更新 // ここで不整合が起きる *anssizp = MAXPACKET; *thisansp = ans = newp;
Code language: C++ (cpp)

send_dg 関数の呼び出し時にはレスポンス格納領域として、バッファポインタ ans (スタック上の2048バイト)、バッファサイズ anssiz (2048)が引数に渡されます。しかし、2048バイトを越えるデータを受信した場合は、内部でヒープが確保されて引数として渡した値が更新されます。その後関数から戻ると、バッファポインタ ans (スタック上の2048バイト)、バッファサイズ anssiz (65536)となり不整合が発生します。この状態でもう一度 send_dg または send_vc を呼び出すと、バッファサイズのみ拡張された引数が渡されるためスタックバッファオーバーフローが発生します。

また、TCP で SERVFAIL した場合などは一度 __libc_res_nsend から抜けた後、再度同様の呼び出しが発生します。しかしながら __libc_res_nsearch にて以下の通りバッファポインタの是正が行われる為、攻撃を成立させるためには一回の __libc_res_nsend 呼び出し内部の処理で完結させる必要があります。

== glibc/resolv/res_query.c:__libc_res_nsearch __libc_res_nsearch(res_state statp, const char *name, /* domain name */ int class, int type, /* class and type of query */ u_char *answer, /* buffer to put answer */ int anslen, /* size of answer */ u_char **answerp, u_char **answerp2, int *nanswerp2, int *resplen2) ... ret = __libc_res_nquerydomain(statp, name, NULL, class, type, answer, anslen, answerp, answerp2, nanswerp2, resplen2); if (ret > 0 || trailing_dot) return (ret); saved_herrno = h_errno; tried_as_is++; if (answerp && *answerp != answer) { // 初回のsend_dg/send_vcにてヒープ確保した場合はポインタを更新する // 不整合が起きたとしてもここで是正される // 以降の__libc_res_nquerydomain呼び出しでは攻撃は成立しない answer = *answerp; anslen = MAXPACKET; }
Code language: C++ (cpp)

PoC にて用いられている手法

脆弱性に至るまでの流れは先に示した通りです。この状態を作り出すためには幾つか方法がありますが、公開されている PoC にて用いている手法は以下です。TC ビットによる TCP フォールバックを利用することにより、攻撃が成立する条件を作り出しています。

  • クライアントが getaddrinfo 経由で A/AAAA クエリを UDP で送信する (send_dg の処理)
  • サーバは UDP クエリに対して TC ビットを立てた2048バイトを越える UDP レスポンスを返す
  • クライアントは TC ビットを認識し TCP で再度クエリを送信する (send_vc の処理)
  • サーバは TCP 接続にて2048バイトを越えるデータを送出
  • クライアントにてスタックバッファオーバーフローが発生

意図した物かは解りませんが、この PoC は DNS のパケットとしては不正な形式になっているため直接 glibc のリゾルバから通信した場合には受け入れられますが、BIND 等による DNS キャッシュサーバを経由した場合は整合性チェックでエラーになります。この不正な形式は攻撃に必要な構造では無く、DNS のパケットとして正しい形式であっても成立する事を確認しています。そのため、この PoC の実行結果のみをもって DNS キャッシュサーバによる防御が成立すると云えませんので注意が必要です。

信頼できる DNS キャッシュサーバを経由した場合の影響

PoC による攻撃が成立した状況においては、リゾルバが直接参照している DNS キャッシュサーバから攻撃を受けたというシナリオになっています。このサーバは resolv.conf における nameserver が該当しますが、通常の用途においては正規の DNS キャッシュサーバを参照しているはずです。その状態でも、スプーフィングされたパケットを受け入れる可能性があったり、MITM が可能な通信路を使用している場合は、DNS キャッシュサーバから攻撃された場合と同様の状態になるため、攻撃が成立する可能性があります。

よってそれ以外のケースである、信頼できる通信路信頼できる DNS キャッシュサーバ (BIND 等) の環境下における影響を説明します。具体例としては、127.0.0.1 で DNS キャッシュサーバを動作させ、そこをリゾルバの参照先としている様な場合です。

UDP クエリの扱い

リゾルバと DNS キャッシュサーバ間が信頼できる通信路の場合はスプーフィングされたパケットや MITM による攻撃の可能性を排除出来ます。これより、信頼できる DNS キャッシュサーバを通過してきたレスポンスにて、脆弱性が起きる状態が作り出せるか否かが焦点になります。正規の DNS キャッシュサーバのソフトウェアは仕様に沿った動作をしますので以下が成立します。

  • 512バイトを越えるレスポンスは送出されない (resolv.conf の options にて edns0 を設定しない場合)
  • TC ビットの付いたレスポンスはデータを含まない (BIND にて確認)

この条件により A/AAAA のレスポンスを合わせても、2048バイトを越えることは無く不整合の起きるヒープの確保は行われません。したがって UDP クエリについては、この条件において攻撃の防御が可能です。

TCP クエリの扱い

TCP においてもスプーフィング、MITM における攻撃の可能性は同様であるため、DNS キャッシュサーバを通過したレスポンスが焦点です。TCP の場合は UDP と異なり、512バイトのサイズの制限はありません。

攻撃が成立するためには、一度ヒープが確保される状態を作り出した後に、再度 send_dg (UDP) または send_vc (TCP) の処理を通る必要があります。

PoC の条件と異なり send_dg (UDP) をヒープ拡張のトリガーとして使用できないため、以下の様なシナリオが必要になります。

  • resolv.conf にて複数のネームサーバを指定 (TCP フォールバック後はリトライが抑制されるため)
  • UDP の A/AAAA レスポンスにて TC ビットを立てたデータを返す (TCP へのフォールバックを誘発)
  • TCP の A レスポンスにて2048を越えるデータを返す (ヒープの確保を誘発)
  • TCP の AAAA レスポンス処理にて以下の何れかの状態にする
    • 条件1. 不完全なヘッダを送出
    • 条件2. TCP 接続を切断する
  • send_vc が戻り値0で抜けてくるため、2台目のネームサーバへフォールバック
  • 2台目のネームサーバへ TCP 問い合わせが発生 (不整合の起きた状態で send_vc の呼び出し)
  • TCP の A/AAAA レスポンスにて2048を越えるデータを返す
  • クライアントにてスタックバッファオーバーフローが発生

条件1または条件2を満たす場合に攻撃が成立します。条件1ですが、正規の DNS キャッシュサーバのソフトウェアにおいては不完全なヘッダを送出する事はまずありません。条件2の TCP 切断はサーバのプロセス停止等により起きる可能性はありますが、攻撃者の DNS レスポンスをトリガーとして起こすことは事実上不可能です。

この理由により、TCP の状況下においてはヒープの確保を誘発させるような状態を作り出したとしても攻撃成立は困難です。また、TCP の回避策として1024バイトのサイズ制限が挙げられているのは、A/AAAA の両レスポンスを受信してもヒープの確保を誘発しないサイズであるためです。

TCP の処理を受け持つ send_vc の該当箇所は以下の通りです。send_dg と異なり *thisanssizp へヒープサイズを書き込んでいますが、結果としては同じ事になります。

== glibc/resolv/res_send.c:send_vc send_vc(res_state statp, const u_char *buf, int buflen, const u_char *buf2, int buflen2, u_char **ansp, int *anssizp, ... int *thisanssizp; u_char **thisansp; if ((recvresp1 | recvresp2) == 0 || buf2 == NULL) { // A のレスポンス処理 thisanssizp = anssizp; thisansp = anscp ?: ansp; ... } else { // AAAA のレスポンス処理 // ヒープ確保でバッファが広がってる場合は MAXPACKET になる // send_dg でヒープ確保に成功した場合は不整合有り if (*anssizp != MAXPACKET) { #ifdef _STRING_ARCH_unaligned *anssizp2 = orig_anssizp - resplen; *ansp2 = *ansp + resplen; #else int aligned_resplen = ((resplen + __alignof__ (HEADER) - 1) & ~(__alignof__ (HEADER) - 1)); *anssizp2 = orig_anssizp - aligned_resplen; *ansp2 = *ansp + aligned_resplen; #endif // A はスタックに収まったので AAAA も納めようとする // レスポンスサイズ1023の回避策は必ずここの分岐に落ちる為の条件 } else { *anssizp2 = orig_anssizp; *ansp2 = *ansp; // A は malloc したので、AAAA 用にスタックはまだ空いてると想定 } // カレントポインタを AAAA 用のバッファに切り替え thisanssizp = anssizp2; thisansp = ansp2; thisresplenp = resplen2; } ... // 残バッファより大きかったら新規確保 if (rlen > *thisanssizp) { if (__builtin_expect (anscp != NULL, 1)) { u_char *newp = malloc (MAXPACKET); if (newp == NULL) { *terrno = ENOMEM; __res_iclose(statp, false); return (0); } // ここでサイズ不整合が起きる *thisanssizp = MAXPACKET; *thisansp = newp; ... if (__builtin_expect (len < HFIXEDSZ, 0)) { ... // TCP のみで攻撃する場合は malloc 後にここで抜けてリトライ誘発が必要(どちらか) // ヘッダサイズ不正チェック。信頼できるサーバの場合はまず起きない return (0); } ... if (__builtin_expect (n <= 0, 0)) { ... // TCP のみで攻撃する場合は malloc 後にここで抜けてリトライ誘発が必要(どちらか) // 接続断。信頼できるサーバでも起きる可能性はあるが狙って起こせる物ではない return (0); }
Code language: C++ (cpp)

まとめ

上記の分析より 信頼できる通信路信頼できる DNS キャッシュサーバ の組み合わせは今回の攻撃に対する回避策になると判断できます。

例示では 信頼できる通信路 は IPv4 ループバックとして挙げていますが、組織内ネットワークなどの第三者から直接到達不可能なプライベートネットワークにおいては、同様にリスクとしては限定されると思われます。

一方、インターネットなどの公開ネットワークでは、スプーフィングや MITM などの脅威があるため、信頼できる DNS キャッシュサーバを使用していたとしても、回避策にはなりません。

今回の脆弱性は、公開時点で発見者による技術解説と PoC が公開されており、影響調査の観点からもかなりの助けになっています。PoC がある場合に注意すべき点としては、使われている攻撃手法は成立する条件の一つであり、それがすべてとは限らない点です。

そのため、上に示したように使われている手法や攻撃成立の条件を理解し、回避策として成立しうるか判断する事が必要になります。

シェアする