OpenSSLの「重大な」脆弱性を徹底解説

本記事は2022年11月4日（米国時間）に公開した弊社の英語ブログ[Breaking down the ‘critical’ OpenSSL vulnerability](https://snyk.io/blog/breaking-down-openssl-vulnerability/)を日本語化した内容です。

なお、この脆弱性に関しては下記のブログもご参照ください。

https://qiita.com/SnykSec/items/131024996d4e20f208b7

https://qiita.com/SnykSec/items/ddf78b9ec8ead1d4fa60

[![logo-solid-background.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2216557/165b3952-4446-777f-bb3e-8982e6864164.png)](https://snyk.io/jp)


2022年11月1日、OpenSSLチームは、深刻度 (Severity) が「高 (High)」の脆弱性2つ（CVE-2022-3602とCVE-2022-3786）の詳細を示す[アドバイザリ](https://www.openssl.org/news/secadv/20221101.txt)を公表しました。これは深刻度「クリティカル (Critical)」の脆弱性として予告されていましたが、実際のアドバイザリでは「高 (High)」に格下げされました。しかしながら、OpenSSLは主要な暗号化ライブラリの1つであり、インターネットのTLS暗号化通信の大部分を支えているため、これはまだ問題といえそうです。

この記事では、これら2つの脆弱性を特にCVE-2022-3602の技術的な詳細に重点を置いて解説し、これが注目されるレベルに値するかどうか、これが本当に「Heartbleed (ハートブリード) 2.0」なのか、そして実際にどの程度懸念すべきかについて考えていきたいと思います。


# アドバイザリ

このアドバイザリでは、X.509証明書の検証、特に[名前の制約の](https://www.rfc-editor.org/rfc/rfc5280#section-4.2.1.10)検証におけるバッファオーバーフロー/オーバーランの脆弱性2つについて説明されています。これらの問題は、OpenSSL 3.0.0 から 3.0.6のバージョン に影響を与え、OpenSSL 3 のユーザーは 3.0.7 にアップデートする必要があります。ただし、落ち着いて対応することが重要です。 [Censys](https://censys.io/critical-vulnerability-in-openssl/)によると、2022年10月現在、約180万台のホストのうち、3.0.0以上のバージョンを実行しているのは7000台強（0.4%）です。脆弱である内で最も利用されているOpenSSLバージョンは3.0.1で、これらホストの50%にあたります。また、SSL/TLSの実装はOpenSSLだけでなく、NSS、BoringSSL、SChannel、LibreSSLなどの選択肢もあります。さらに、この脆弱性は特定の条件下で発生するため、影響を受けるシステムの数は、脆弱性のあるバージョンのOpenSSL 3を実行しているシステムよりもさらに少なくなっています。

では、この状況を理解するためにどのように考えていけばよいでしょうか。まずは、RCE (リモートコード実行) につながる可能性のある、より深刻な問題 (CVE-2022-3602) を修正した[コミット](https://github.com/openssl/openssl/commit/3b421ebc64c7b52f1b9feb3812bdc7781c784332)から、どんなことが分かるか見ていきましょう。

## [CVE-2022-3602](https://security.snyk.io/vuln/SNYK-UNMANAGED-OPENSSL-3090874)

興味深いことに、このコミットはコードベース内の1行を変更するだけです!
![blog-openssl-breakdown-single-line-change.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2216557/8260197f-9db4-ed41-1749-c5837e30cc26.jpeg)

上記の変更を踏まえると、これは `ossl_punycode_decode` の古典的な "off by one" バグのように見えます。コミットメッセージでそのとおりであることが明記されています。「punycode decoder の off by one エラーにより、バッファに対するsingle unsigned int オーバーライトが可能で、その結果としてクラッシュやコード実行を引き起こす可能性がある」

## [CVE-2022-3786](https://security.snyk.io/vuln/SNYK-UNMANAGED-OPENSSL-3092519)

もうひとつのバッファオーバーフローは `ossl_a2ulabel` 関数で見つかり、深刻度「高 (High)」とされ、この[コミット](https://github.com/openssl/openssl/commit/680e65b94c916af259bfdc2e25f1ab6e0c7a97d6)で修正されました。CVE-2022-3602 とは異なり、この問題ははじめから深刻度高と評価され、ダウングレードはされていません。攻撃者はオーバーフローを利用して大きなメモリ領域に書き込むことができる一方で、書き込まれるデータの内容を制御することはできません。この脆弱性では「.」文字 (10進数 46) のみが書き込み可能であるため、攻撃は指定された文字を使用したメモリ破壊に限定され、Denial of Service (サービス拒否) を引き起こします。

この2つの問題を比較すると、リモートでコードを実行される可能性がある CVE-2022-3602 の方が技術的により興味深い脆弱性であることは明らかです。そこで、ここからは、この脆弱性に焦点を当てます。

# Off by one

off by one脆弱性とは具体的にどのようなもので、どのように悪用するのかについては、すでに公開済みの記事 ([Fortify](https://vulncat.fortify.com/en/detail?id=desc.internal.cpp.buffer_overflow_off_by_one)、[Daniel Slater](https://www.youtube.com/watch?v=wUPujx-f_Js)) でよく取り上げられているため、ここでは詳しく説明しません。しかし、問題の OpenSSL バグを理解するには、いくつかの基本知識が必要です。Off by one 脆弱性は、長さのチェック条件が正しく実装されていない場合に発生します。これを理解する簡単な方法は、固定サイズのバッファと、ゼロ・インデックスを考慮に入れなかった for ループを考えることです。

![blog-openssl-breakdown-off-by-one-600x501.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2216557/9dc59603-877e-3185-864f-c8340da84e05.jpeg)

上の例では、2つのことがうまくいきません。まず、文字列のサイズが32を超えるとプログラムの引数からその文字列を終了させるための NULL バイトが得られなくなります。これにより、次の `printf` が `buffer` の境界を越えて読み込んでしまいます。しかし、もっと重要なことは、`buffer` の境界の外側に任意の1バイトを書き込むということです。一般的に、1バイトのずれは次のようなシナリオになります：

- 前の関数で保存されたフレームポインタを上書きすることで、関数が戻ったときにアプリケーションフローをリダイレクトすることができる
- デスティネーションバッファの隣にあるスタック上の変数を上書き

# OpenSSL CVE-2022-3206 について

CVE-2022-3206 のパッチによれば、上記の教科書的な例とかなり似かよった off by one 脆弱性だったことがわかります。その結果、ループの終了条件が正しい条件で戻らず、割り当てられた領域の外側にさらに符号なし整数を書き込んでしまいます。

では、この脆弱な関数はいつ呼び出されるのでしょうか？`ossl_punycode_decode` はpunycode デコード機能を提供し、ドメイン名でサポートされている限られた ASCII 文字サブセットでユニコード文字を表現できるようにします。例えば、`ŠÑÝĶ.io`は`xn--iday5n4f.io`にエンコードされます。信頼できない入力がこの関数によってデコードされるためには、以下のことが必要です。

- 悪意のあるCAや仲介者の証明書は、punycode を持つ名前の制約フィールドを含んでいなければならない。
- 末端の証明書は `SubjectAlternateName (SAN) otherName` フィールドを含んでいなければならない。このフィールドは `SmtpUTF8Mailbox` 文字列を指定する。

これらの条件が満たされると、名前の制約フィールドからのpunycode は脆弱な `ossl_punycode_decode` 関数によってパースされます。


## サーバーサイドの攻撃

OpenSSL 3 を実行しているサーバーに影響を与えるには、サーバーがクライアント証明書を受領するように設定されている必要があります。クライアント証明書による認証は、通常のシナリオでは使用されていないため、ほとんどのサーバーは影響を受けません (ほとんどのサービスがフォームベースのアプローチのみを使用しているため)。クライアント証明書を受け入れている場合、攻撃経路は実行可能である可能性があります。しかし、punycode のデコードは証明書の検証後に行われるため、有効な証明書チェーンや署名エラーを無視する構成が攻撃の成功には必要です。

プライベートCAや仲介者が信頼されない当事者であり、サーバーに信頼される有効な証明書に署名するように位置づけられることは十分にあり得ます。例えば、これは外部のウェブサービスからのAPIリクエストを認証する際にかなり一般的なことです。しかし、クライアント証明書が信頼できる当事者によって発行されている場合、現在利用可能な情報に基づいて悪用することはできないはずです。

## クライアントサイドの攻撃

OpenSSL 3 を使用する TLS クライアントが、悪意のあるサーバーに接続する際に影響を受ける可能性があります。有効な証明書チェーンや証明書エラーを無視するための同じ制約が、ここでも適用されます。TLS クライアントで証明書エラーを無視するのはよくあることですが、これは一般にデフォルトではありません。


# 攻撃要件を満たしている場合、悪用される可能性はどのくらいあるか？

脆弱性があるため、OpenSSL ライブラリに 3.0.7 のパッチを適用して、この問題を修正する必要があります。ほとんどの場合、結果は Denial of Service (DoS: サービス拒否) になるでしょう。RCE (リモートコード実行) の悪用は、容易でない可能性が高いです。これらの条件では、たった4バイトの任意の書き込みが可能であり、攻撃者が成功する可能性は高くありません。Datadogのセキュリティラボチームは、すでにこの問題の悪用について[詳細に](https://securitylabs.datadoghq.com/articles/openssl-november-1-vulnerabilities/#notes-on-exploitability-leading-to-rce)調査しており、重要なメモリ領域を破壊して DoS を引き起こすことさえ、Linuxベースのシステムでは困難です。PIE、パディング、スタックカナリア/クッキーなどの最新のメモリ保護メカニズムにより、RCE の攻撃が成功する可能性は大幅に減少します。


# メモリ破壊のバグを検出

C/C++ のようなアンマネージドな言語を扱う場合、微妙な欠陥が有害な形で顕在化しやすいのですが、このバグは大きな問題ではなさそうで、動的ファズテストで検出できた可能性があります。

これらの OpenSSL のバグをファズテストで検出するには、2つのアプローチがあります。
1. crypto/punycode.c 内の脆弱な関数 `ossl_punycode_decode` と `ossl_a2ulabel` に対してテストハーネスを作成し、直接的にファジングを行ってください。この方法は、何を探しているのかが分かっている場合には良い方法です。
2. 細工した証明書でクライアント-サーバをセットアップして、OpenSSL 証明書のパースと検証をすべて通過させます。これは一般的な方法ですが、最初の方法と比較してセットアップのオーバーヘッドが大きくなります。

 
この方が圧倒的に簡単なので、私たちは一番目の選択肢を選びました。以下は、カバレッジガイド付きインプロセスファジングエンジンである [libFuzzer](https://llvm.org/docs/LibFuzzer.html) で CVE-2022-3602 を捕捉するために作成されたテストファイルの例です。


```
#include <stdio.h>
#include <openssl/x509.h>
#include <openssl/x509v3.h>
#include <openssl/err.h>
#include "fuzzer.h"
#include "internal/nelem.h"
#include <crypto/punycode.h>
 
#define MAX_OUTPUT_BUFF 1024
 
static BIO *bio_out;
 
int FuzzerInitialize(int *argc, char ***argv)
{
   OPENSSL_init_crypto(OPENSSL_INIT_LOAD_CRYPTO_STRINGS, NULL);
   ERR_clear_error();
   CRYPTO_free_ex_index(0, -1);
   return 1;
}
 
int FuzzerTestOneInput(const uint8_t *buf, size_t len)
{  
   unsigned int output_buf[MAX_OUTPUT_BUFF];
   unsigned int bsize = OSSL_NELEM(output_buf);
   (void)ossl_punycode_decode(buf, len, output_buf, &bsize);
   ERR_clear_error();
   return 0;
}
 
void FuzzerCleanup(void)
{
    BIO_free(bio_out);
}
```

バージョン 3.0.6 の `ossl_punycode_decode` で少し実行したところ、脆弱性のある関数でクラッシュを発見しました。

```
==9024==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7ffeb247ede0 at pc 0x5615d1499782 bp 0x7ffeb247da50 sp 0x7ffeb247d220
WRITE of size 264 at 0x7ffeb247ede0 thread T0
    #0 0x5615d1499781 in __asan_memmove (/home/user/openssl/fuzz/punycode+0xd7d781) (BuildId: 546d1cbf454c87a04c514633876bd5307eab38b2)
    #1 0x5615d155c023 in ossl_punycode_decode /home/supriza/openssl/crypto/punycode.c:188:9
    #2 0x5615d14d76de in FuzzerTestOneInput /home/supriza/openssl/fuzz/punycode.c:42:11
```

この問題を修正したパッチを適用すると、ファズテストがクラッシュしなくなり、想定どおりの検出であったことが分かります。

もし自分のネイティブコードを保護するために libfuzzer に慣れたい、あるいは単に OpenSSL のファジングを試してみたいなら、[GitHub](https://github.com/supriza/openssl-v3.0.7-cve-fuzzing) で私たちのファジング設定をごらんください。このバグが実際のクライアントサーバー SSL 証明書検証シナリオでどのように捉えられるか、より完全なファジング設定により実証できれば興味深いと感じられるでしょう。

# 最も重要なポイント

OpenSSL 3 の脆弱性が事前に発表された際には大きな懸念をもたらしました。しかし詳細が明らかになった今、多くの人が予想したような「Heartbleed 2.0」スタイルの脆弱性ではなく、一安心と言えるでしょう。深刻度「高」の脆弱性が2つ存在することに変わりはありませんが、これらはインターネットを破壊するようなバグではなく、広範囲に悪用されることはないと思われます。企業内における通常のパッチおよび脆弱性の管理手順に従って、OpenSSL 3.0.7 以降に適時にアップデートすることが可能です。さらに、[Snyk](https://snyk.io/jp) などのツールを使用すると、OpenSSL の脆弱バージョンの存在 (およびその他の多くのセキュリティ問題) を特定し、パッチを適用することができます。[まずは無料でSnykをお試しください](https://app.snyk.io/login)。