2304 文字
6 min read
AIでふっるいCVEの調査にあたりをつけてみる
2024-09-18

今回の試み

最近よくLLM周りのツールを色々触っているのですが、新規開発ばかりに利用して、既存改修などではあまり活用できていません。(Cursorの力は借りていますが)

そこで、今回はOSS絡みで調査が必要になっている古い脆弱性「CVE-2015-5237」について、生成AIを使用しながら調査してみたいと思います。

事前情報

CVE-2015-5237は https://github.com/protocolbuffers/protobuf/issues/760で2015年に報告されたバッファオーバーフローに関する古い脆弱性です。
大元のgoogle/protobufは当時様々なOSSが採用していたこともあり、影響としては大きなものであったようです。

ただ、2015年当時の脆弱性ということもあって、あまり詳しい情報はやり取りがありません。google/protobufの実装は色々あるのですが、C++の話はしていますが他のクライアントでどうなのか不明です。

脆弱性検知ツールでの扱い

snykにおいて、C++とPHPに脆弱性を認めていますが、Javaの脆弱性はかなり低い評価になっています。(Redhatのスコアが低いためと見られる)
この点についてなぜそうなのか記録が残っていません。

つまり、Web上にはたいした情報が転がっていないという状況です。

調査の試み

まず、検索AI Perplexityを試してみましたが、これは惨敗か、、と思いきや下記を見つけてくれました。

https://security.snyk.io/vuln/SNYK-JAVA-COMGOOGLEPROTOBUF-173761

最初、SnykのDBを見たときは前述の通り、javaには低いスコアしかないなあ、と思ったのですが、どうやらUIが悪く、全部表示されていない上にページングもできないようです。
Mavenで絞った場合に限り上記を確認できました。Javaでもハイスコアとなっています。

コードベースの調査

次に、v2.4.1のprotobufをダウンロードして、Cursorで質問しました。この際に使用したLLM modelはclaude3.5 sonnetと割と強めのAIを選定しています。
質問の際には Codebase Answers を使用しています。 この機能では、開いているファイルだけでなく、質問内容から内蔵のベクトルDBにベクトル化されているソースコードを検索し、必要そうなファイルをLLMに渡してくれます。

以下、全文を掲載します。

Q. 初期情報を与えて質問する

このプロジェクトはCVE-2015-5237: Integer overflow in serialization が報告されたprotobufです。
下記のIssueに書かれている脆弱性が埋め込まれているコードを特定してください

int is used to express the size of serialized messages. If the size exceeds 4 GiB, the application may allocate a buffer which is too small, or protobuf itself does this, in google::protobuf::MessageLite::SerializeToString. This lead to a heap buffer overflow, which may be exploitable for code execution in some cases.
It has been suggested that serialization of messages larger than 2 GiB is unsupported. But there is no good way for an application to ensure that the limit is not exceeded accidentally, without imposing rather draconian limits. To some degree, this is an gets-style interface.
Right now, this is more or less harmless because the message sizes involved are substantial. But this will change over time. My worry is that it will be difficult to fix this because some of the overflowing computations end up in generated *.pb.cc files, so the eventual fix will not be a simple library update.

A. 回答

この脆弱性は主にメッセージのシリアライズ処理に関連しています。特に重要な部分は以下のコードブロックにあります:

  GOOGLE_DCHECK(IsInitialized()) << InitializationErrorMessage("serialize", *this);
  return SerializePartialToCodedStream(output);
}

bool MessageLite::SerializePartialToCodedStream(
    io::CodedOutputStream* output) const {
  const int size = ByteSize();  // Force size to be cached.
  uint8* buffer = output->GetDirectBufferForNBytesAndAdvance(size);
  if (buffer != NULL) {
    uint8* end = SerializeWithCachedSizesToArray(buffer);
    if (end - buffer != size) {
      ByteSizeConsistencyError(size, ByteSize(), end - buffer);
    }
    return true;
  } else {
    int original_byte_count = output->ByteCount();
    SerializeWithCachedSizes(output);
    if (output->HadError()) {
      return false;
    }
    int final_byte_count = output->ByteCount();

    if (final_byte_count - original_byte_count != size) {
      ByteSizeConsistencyError(size, ByteSize(),
                               final_byte_count - original_byte_count);
    }

    return true;
  }
}

このコードブロックでは、SerializeToStringメソッドが実装されています。ここで、メッセージのサイズをint型で扱っているため、4GiBを超えるサイズのメッセージをシリアライズしようとすると整数オーバーフローが発生する可能性があります。

また、以下のコードブロックも関連しています:

  const void* void_buffer;
  int buffer_size;
  if (NextNonEmpty(input_, &void_buffer, &buffer_size)) {
    buffer_ = reinterpret_cast<const uint8*>(void_buffer);
    buffer_end_ = buffer_ + buffer_size;
    GOOGLE_CHECK_GE(buffer_size, 0);

    if (total_bytes_read_ <= INT_MAX - buffer_size) {
      total_bytes_read_ += buffer_size;
    } else {
      // Overflow.  Reset buffer_end_ to not include the bytes beyond INT_MAX.
      // We can't get that far anyway, because total_bytes_limit_ is guaranteed
      // to be less than it.  We need to keep track of the number of bytes
      // we discarded, though, so that we can call input_->BackUp() to back
      // up over them on destruction.

      // The following line is equivalent to:
      //   overflow_bytes_ = total_bytes_read_ + buffer_size - INT_MAX;
      // except that it avoids overflows.  Signed integer overflow has
      // undefined results according to the C standard.
      overflow_bytes_ = total_bytes_read_ - (INT_MAX - buffer_size);
      buffer_end_ -= overflow_bytes_;
      total_bytes_read_ = INT_MAX;
    }

このコードでは、バッファサイズの計算が行われていますが、int型を使用しているため、同様に整数オーバーフローの問題が発生する可能性があります。

これらのコードブロックは、大きなメッセージをシリアライズする際に整数オーバーフローを引き起こし、結果としてバッファオーバーフローを引き起こす可能性があります。これは、メモリ破壊や潜在的なコード実行の脆弱性につながる可能性があります。

修正するには、これらの部分で64ビット整数型を使用するか、メッセージサイズに上限を設けるなどの対策が必要です。また、生成された*.pb.ccファイルにも影響を与える可能性があるため、単純なライブラリの更新だけでは解決できない可能性があります。

Q. 他のパッケージへの影響を聞く

javaディレクトリ以下にあるprotobuf-javaにこの影響がどう関わるか調べてください

A. 追加の回答

protobuf-javaにおけるCVE-2015-5237の影響を調査しました。この脆弱性は主にメッセージのシリアライズ処理に関連しています。javaディレクトリ以下の関連するコードを確認したところ、以下の点が重要です:

  1. MessageLite.javaファイル:
    このインターフェースはシリアライズメソッドを定義しています。特に toByteString() メソッドが関連しています。
  ByteString toByteString();

  /**
   * Serializes the message to a {@code byte} array and returns it.  This is
   * just a trivial wrapper around
   * {@link #writeTo(CodedOutputStream)}.
   */
  byte[] toByteArray();
  1. AbstractMessageLite.java:
    このクラスは toByteString() メソッドを実装しています。ここでメッセージサイズの計算と実際のシリアライズが行われます。
  public ByteString toByteString() {
    try {
      final ByteString.CodedBuilder out =
        ByteString.newCodedBuilder(getSerializedSize());
      writeTo(out.getCodedOutput());
      return out.build();
    } catch (IOException e) {
      throw new RuntimeException(
        "Serializing to a ByteString threw an IOException (should " +
        "never happen).", e);
    }
  }
  1. CodedOutputStream.java:
    このクラスはバイトストリームへの書き込みを担当します。特に computeMessageSizeNoTag() メソッドがメッセージサイズの計算に関与しています。
  public static int computeMessageSizeNoTag(final MessageLite value) {
    final int size = value.getSerializedSize();
    return computeRawVarint32Size(size) + size;
  }
  1. CodedInputStream.java:
    このクラスはサイズ制限を設定するメソッドを持っています。これは大きすぎるメッセージの処理を防ぐために使用されます。
  public int setSizeLimit(final int limit) {
    if (limit < 0) {
      throw new IllegalArgumentException(
        "Size limit cannot be negative: " + limit);
    }
    final int oldLimit = sizeLimit;
    sizeLimit = limit;
    return oldLimit;
  }

これらのコードは、メッセージのシリアライズとデシリアライズ、およびサイズの計算と制限に関連しています。CVE-2015-5237の脆弱性は、4GiBを超えるサイズのメッセージを処理する際に整数オーバーフローが発生する可能性があるというものです。

Java実装では、int型(32ビット)を使用してメッセージサイズを扱っているため、この問題が発生する可能性があります。特に、CodedOutputStream.computeMessageSizeNoTag()メソッドやAbstractMessageLite.toByteString()メソッドでのサイズ計算において、大きなメッセージを扱う際にオーバーフローが起こる可能性があります。

この脆弱性を修正するためには、メッセージサイズの計算と制限において64ビット整数を使用するか、明示的にサイズチェックを行うなどの対策が必要です。また、CodedInputStream.setSizeLimit()メソッドを適切に使用して、処理するメッセージサイズに上限を設けることも重要です。

評価

まず、最初の質問で返されたC++のコードはまさしく対象のコードでした。これはまだ、Issueの中に示唆があるので見つけられるのもわかりますが、解説付きで探してくれるのは大変ありがたいです。

次の質問は、javaパッケージに与える影響を水平に質問しただけですが、こちらも的確にシリアライズを行っている関数と、int型で定義された計算フローを見つけてくれました。
この実装からして、C++だけでなくjavaでも間違いなく4GBを超えるメッセージを取り扱うとバッファオーバーフローを引き起こせることがわかります。

なぜJVMで発生するバッファオーバーフローがハイスコアに評価されているのか疑問は残りますが、下記2点がわかるまで30分たらずでした。ニンゲンの手だけではこうは行かないでしょう。

  • セキュリティベンダーでC++もjavaもハイリスクに分類されている
  • コードベースでみて、C++、Javaどちらでも発生しうる
AIでふっるいCVEの調査にあたりをつけてみる
https://www.chalkboard.me/posts/ai-cve-challenge/
作者
ange-k
公開日
2024-09-18
ライセンス
CC BY-NC-SA 4.0