IIJ-SECT

IIJ

 

Security Diary

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

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


まず、問題となっている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);


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


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;


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;
    }


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);
     }


まとめ

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

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

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

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

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

-
 
カテゴリー :
脆弱性  
タグ :
Vulnerability   Linux  
この記事のURL :
https://sect.iij.ad.jp/d/2016/02/197129.html