Windows CryptoAPIの脆弱性によるECC証明書の偽造(CVE-2020-0601)
1. はじめに
つい先日のWindowsのセキュリティアップデートでWindowsのCryptoAPIの楕円曲線暗号処理に関連した脆弱性の修正が行われました。
「CVE-2020-0601 | Windows CryptoAPI Spoofing Vulnerability」
これがまぁ世界の暗号専門家を中心にセキュリティ業界を驚かせ、いろいろ騒がしています。
その驚きの一つは、この脆弱性の報告者がNSA(米国家安全保障局)だったことです。NSAはMicrosoftのアナウンスとは別により詳しい内容でこの脆弱性を警告するアナウンスを出しています。
「Patch Critical Cryptographic Vulnerability in Microsoft Windows Clients and Servers」
これまで数々の諜報活動をインターネット上で行ってきたNSAが、この脆弱性を自分たちの諜報活動に利用しないというのだろうか?
楕円曲線暗号にバックドアを仕込んだのではないかと疑われているNSAが、自ら発見した楕円曲線暗号処理の脆弱性をわざわざ開発元に報告し、修正を求めるなんてことが本当にありうるのだろうか?
他にも、NSAは既にもっと巧妙な攻撃方法を使っているのでもうこの脆弱性はいらなくなったとか、他国がこの脆弱性を狙った攻撃を使い始めたからとか、いろんなことを想像しちゃいます。
まぁこれが本当なら世の中かなり変わったものです。
こういった騒ぎの中で、Hacker Newsではこの脆弱性の原因について解説をするコメントが寄せられていました。
「CVE-2020-0601の攻撃方法について述べたHacker Newsのコメント」
これを読んでみると、この脆弱性なかなかヤバそうです。早速世界中でこの脆弱性をついて exploit するPoCが作成され、既にいくつか公表されています。
その中で Kudelski Security が公表した、
「CVE-2020-0601: THE CHAINOFFOOLS/CURVEBALL ATTACK EXPLAINED WITH POC」
の内容は非常に具体的です。私も試してみると本当にHTTPSサーバの証明書を偽造することができました。このデモは、時雨堂さんのサイトを偽造してします。Vさん、ごめんなさい。
スクリーンショットにあるよう、IE11のURL表示バーには何もエラーが出ていません。ただし証明書のパスのチェックしてみると、ルート証明書にバッテンがついています。なんか不思議な状態です。通常のユーザはこれが異常であることに気づかないでしょう。
ちなみにWindows Updateで脆弱性修正のパッチをあててみると、まずDefenderがブロックしてサイトにアクセスできません。警告を受け入れてアクセスしてみると、見事URLバーが赤色になり証明書エラーの表示が出ています。
これだとひと目で攻撃サイトにアクセスしていることがわかります。まだWindows Updateしていない皆さん、直ちにパッチをあてましょう。
今回は、この脆弱性のしくみと上記の試験で検証した Kudelski Security が公表したHTTPSサーバを偽造する攻撃手法に限定にして解説したいと思います。コード署名やVPNなどでは他の攻撃ベクターが存在している可能性があります。ご注意ください。
2. 超ざっくりしたECDSAの解説
今回の脆弱性は、ECDSA(Elliptic Curve Digital Signature Algorithm)の処理に関連したものです。 ここでECDSAのしっかりした解説をするのはかなり荷が重いです。なので、この記事が分かる程度に超ざっくりとECDSAがどういうものか書いてみます。
これでなんとなく雰囲気を掴んでいただければと思います。ただかなり雑に書いているので、正確に理解したいならちゃんとした本や資料で勉強してください。
楕円曲線暗号の特徴は、ECパラーメータという数値セットで決定されます。そのECパラメータには、曲線の係数やベースポイント(G)と呼ばれる計算の開始点、剰余算を行う素数の値、その他いくつかの数値が含まれます。もちろん楕円曲線暗号の種類によってその中身も変わります。
この楕円曲線上の点で、数学的に決まった形で剰余演算を行います。楕円曲線暗号方式のキモは、ある楕円曲線上の点Gを何倍かして新しい点の位置を計算することはできるが、逆にある点を見てGを何倍してたどり着いた点なのか、その倍数を見つけるのが非常に難しいという性質です。さらに楕円曲線上の点Gを2倍3倍…と進んでいくと、同じ点Gに戻ってくるという性質もあります。
ECDSAは、この楕円曲線暗号を使った署名方式です。
上図の様にG、P(公開鍵)を署名者、署名検証者の間で交換しておきます。署名者は、署名作成時に乱数rから生成される点Rとメッセージのハッシュ値(Hash(M))と合わせて署名値SとRのx座標(Rx)を計算し、メッセージと共に署名検証者に渡します。
署名検証者は、受け取った署名値(Rx, S)とメッセージM、あらかじめ知っているGとPを使って別にRを計算します。この計算したRのX座標が、受け取ったRxと一致していれば署名検証成功です。
Pを生成する秘密鍵dを知っている者でないと、この計算を一致させる署名値を作ることができないからです。
こういった特徴を持つECDSA署名のしくみによってメッセージの認証や完全性が保証され、なりすましや改ざんといった脅威から守ることが可能となります。
3. CVE-2020-0601の脆弱性
今回のWindowsの脆弱性 CVE-2020-0601 は、CryptoAPI のバグをついてECパラメータを偽造し、署名検証をバイパスできてしまう脆弱性です。
ベースポイントGを含むECパラメータは、通常セットで名前付けられており、P-256, P-384などの名前で呼んで識別します(namedCurve)。ただし同じECパラメータでも名付け団体によって名前が異なるのでややこしいし、混乱します。
仕様的には名前付けされたパラメータセットではなく、ベタでECパラメータを書いている方式も認められています(specifiedCurve)。
修正される以前のWindows CryptoAPIでは、このECパラメータの処理する際にベースポイントGをチェックする処理が見落とされるバグを持っていました。
通常署名で利用する公開鍵は、証明書に記載されています。公開鍵を変えてしまうと別の証明書であると見なされます。そこで攻撃者は、ベースポイントGのチェックが甘いCryptoAPIのバグをついて、Gを秘密鍵がわかる位置まで移動させてしまえば、公開鍵Pに対応した秘密鍵を攻撃者が自由に設定できることを見つけました。
秘密鍵がわかる位置、そうですG'を公開鍵Pの倍数の位置まで動かせばいいのです。
公開鍵Pの座標を変えられないから計算の起点であるベースポイントGを動かす、逆転の発想です。
署名検証者が利用するベースポイントGをなんらかの方法で変更することが可能であるならば、公開鍵の値はそのままで攻撃者が自由に秘密鍵の値を決めることができてしまうのです。なんてことでしょう。
署名検証者が利用するCryptoAPIではベースポイントのチェックが漏れています。何も知らずに偽造されたベースポイントG'を利用して署名検証をしてしまうと偽造署名の検証が通ってしまうことになります。
もう少し細かくこれを数式で示したのが以下の図です。
自分も実際に紙上で手計算するまでは信じられませんでしたが、お見事です。これ見つけた人はすごい。
当初 twitter で公開した図の計算式に関しては、 xagawa さん と super duper blooper さん から貴重なコメントを頂きました。深く感謝いたします。また頂いたコメントに従い一部表現を修正しました。
4. ECC証明書の形式
ではどうやって偽造したベースポイントG'を署名検証者に伝えるのでしょうか? 証明書を使えばいけそうです。
ECDSA署名で利用するECC証明書には、namedCurve形式とspecifiedCurve形式の2通りが存在します。仕様的には3つ目のimplictCurve形式も存在しますが、今回は対象外とします。
各形式の証明書を実際に見てみましょう。
4.1 namedCurve形式のECC証明書
namedCurve形式の証明書は以下の形です。証明書のなかほどに、ASN1 OID: prime256v1, NIST CURVE: P-256 と書いてあります。これがECパラメータを表す名前です。
この名前(prime256v1)で規定されているECパラメータの中身は、opensslのコマンドで以下の様に簡単に確認することができます。
$ openssl ecparam -name prime256v1 -text -param_enc explicit -noout Field Type: prime-field Prime: 00:ff:ff:ff:ff:00:00:00:01:00:00:00:00:00:00: 00:00:00:00:00:00:ff:ff:ff:ff:ff:ff:ff:ff:ff: ff:ff:ff A: 00:ff:ff:ff:ff:00:00:00:01:00:00:00:00:00:00: 00:00:00:00:00:00:ff:ff:ff:ff:ff:ff:ff:ff:ff: ff:ff:fc B: 5a:c6:35:d8:aa:3a:93:e7:b3:eb:bd:55:76:98:86: bc:65:1d:06:b0:cc:53:b0:f6:3b:ce:3c:3e:27:d2: 60:4b Generator (uncompressed): 04:6b:17:d1:f2:e1:2c:42:47:f8:bc:e6:e5:63:a4: 40:f2:77:03:7d:81:2d:eb:33:a0:f4:a1:39:45:d8: 98:c2:96:4f:e3:42:e2:fe:1a:7f:9b:8e:e7:eb:4a: 7c:0f:9e:16:2b:ce:33:57:6b:31:5e:ce:cb:b6:40: 68:37:bf:51:f5 Order: 00:ff:ff:ff:ff:00:00:00:00:ff:ff:ff:ff:ff:ff: ff:ff:bc:e6:fa:ad:a7:17:9e:84:f3:b9:ca:c2:fc: 63:25:51 Cofactor: 1 (0x1) Seed: c4:9d:36:08:86:e7:04:93:6a:66:78:e1:13:9d:26: b7:81:9f:7e:90
今回注目のベースポイントGは、Generator (uncompressed) の項目に該当します。uncompressedは、ベースポイントの座標がX,Yの座標でエンコードされていることを示しています。
namedCurve形式のECC証明書は、opensslのデフォルトでECC証明書の秘密鍵を生成すると使えます。
$ openssl ecparam -out namedCurvePrivate.key -name prime256v1 -genkey
後述しますが、インターネットのPKIシステムで使われる証明書はこのnamedCurve形式になります。
4.2 specifiedCurve形式のECC証明書
specifiedCurve形式のECC証明書は、ECパラメータを証明書内にベタに書き込む形式です。
以下の証明書は、先のnamedCurve形式と全く同じECパラメータ(P-256)の証明書です。しかし名前ではなくECパラーメータの値がベタに記載されています。
先程のopensslで出力したECパラメータと全く同じ値が証明書内に明示的に記載されていることがわかるでしょう。
今回のCVE-2020-0601は、ここに書かれているベースポイントを偽造した値に入れ替えた証明書を使い攻撃を行います。
specifiedCurve形式のECC証明書は、opensslでECC証明書の秘密鍵を生成する際に、-param_enc explicit
を付けると使えます。
$ openssl ecparam -out specifiedCurvePrivate.key -name prime256v1 -genkey -param_enc explicit
5. Kudelski Security が実証した攻撃手法
では実際にKudelski Security が実証した攻撃手法を試してみます。
時雨堂ドメインのサーバ証明書を偽装し、IE11のブラウザーからエラーを出さずに偽のWebサーバにアクセスさせる攻撃です。
具体的な攻撃コードは、
にのっています。
5.1 偽造ベースポイントのルート証明書を作る
まずは適当な本物のルートECC証明書を探します。Kudelski Securityでは、現Sectigo社が管理するUSERTRUST NetworkのルートECC証明書を利用しました。
このルートECC証明書は、namedCurve形式でP-384を利用していることがわかります。
次に攻撃用のルート証明書を作成します。本物のルート証明書から、シリアルと公開鍵データをパクります。
次に、P-384のECパラメータを記載したspecifiedCurve形式の証明書から偽造ベースポイントを公開鍵を2倍にした座標に変更します。
G'=2Pなので秘密鍵は1/2となります。1/2といっても小数ではなく整数で表します。2倍して1になる数なので、1周して元に戻るちょうど中間折返し地点までの数を示します*1。
5.2 時雨堂の偽造サーバ証明書を作る
時雨堂になりすましを行うHTTPSサーバ用の証明書を作り、この偽造ルートECC証明書を使って署名します。subjectAltName に shiguredo.jp を入れ込んでいます。
5.2 Windows CryptoAPIのキャッシュを上書き
脆弱性をついて攻撃できるルート証明書を作成できたとしても、どうやってこれをターゲットの端末に認識させられるのでしょうか? ルート証明書は通常端末の中で管理されており、外部の攻撃者がルート証明書を入れ替えたりすることは容易ではありません。
ここでもう一つの Windows CryptoAPI のバグ(仕様?)をつきます。
どうもCryptoAPIは、署名検証で利用した証明書をキャッシュしているようです。
ここからはWindows内部の挙動になるので想定になりますが、どうもシリアル番号・公開鍵・Subjectの情報など証明書の一部の情報が一致していると、ルート証明書といえどもキャッシュ更新を行う感じです。
2020年1月22日追記: McAfeeから調査ブログが出てました。思ったよりひどそう。 blogs.mcafee.jp
まず最初に攻撃対象となっている本物のルート証明書から発行されたサイトにアクセスしてルート証明書のキャッシュを作成させます。
次に攻撃者は、偽造ルート証明書と偽造サーバ証明書をクライアントに送り込みます。通常はルート証明書をサーバからクライアントに送ることはありませんが、しったこっちゃありません。
これを受け取ったクライアントのCrypto APIでは、なんと偽造ルート証明書にキャッシュを更新します。
本来ならECパラメータなど完全に同一のものか厳密にチェックしないといけないのですが、なんとベースポイントのチェックを忘れるバグがありました。楕円曲線の係数とかの情報はちゃんとチェックしているようで完全な見落としだと思います。
偽造ルート証明書は、CryptoAPI内では正常なルート証明書として扱われ偽造サーバ証明書の署名検証が成功することになります。これで最初に示した攻撃が完了です。 最初に示した攻撃に成功した画面では証明書パスのチェックでルート証明書にバッテンがついていました。これはキャッシュ上のルート証明書で署名検証は成功しているけどクライアントで管理している本当のルート証明書でないのでバッテンをつけているのだと思われます。
証明書パスがエラーならURLバーの表示でもエラーを出してもらいたいものです。
6. WindowsはRFC5480違反?
実はインターネットのPKIシステムでspecifiedCurve形式の証明書を利用することはRFC違反なんです。 https://tools.ietf.org/html/rfc5480#section-2.1.1 では明確に、
implicitCurve and specifiedCurve MUST NOT be used in PKIX. (implicitCurveとspecifiedCurveはPKIXでは利用しては**いけない**。)
規定されています。おそらく企業ユーザなどで過去TLS1.0/1.1の古い時代にspecifiedCurve形式を使った証明書を扱うようなケースがあり、互換性を維持するためにこうなってしまったのではないかと思われますが、本当のことはわかりません。
OpenSSLでも以下の issue でspecifiedCurve形式のクライアント証明書が使えなくなったというissueがあがってました。
Connection error when using EC client certificate with explicit parameters and TLS1.2
結局RFC5480に従うということでWon't Fix扱いでクローズされました。実際にspecifiedCurve形式のサーバ証明書を受け取るとopenssl-1.1.1のクライアントはエラーで接続できないです。
どういう理由かはわかりませんが、RFC違反で残っていた機能がこんなところで大きな脆弱性として出てきてしまったことは皮肉なもんです。
歴史の長いIEだからこそ起きてしまっことなのかも知れません。新しいMicrosoft EDGEでは、ChromiumのネットワークスタックやTLSライブラリ(BoringSSL)を利用しているようなので同じようなことを心配しなくても良くなるでしょう。
Google Chromeについて(2020年1月20日追記)
IE11は脆弱性の影響を受け、Google Chromeや新しいMicrosoft EDGEでは問題ないように捉えられる記述をしていましたが、間違いでした。
Chrome も Windows CryptoAPIの CertGetCertificateChain を利用しており、下記コードで修正が入っています(修正版のChrome 79がリリース済み)
41853ce2057201bdd225aee96be4e6cd51b2458b - chromium/src.git - Git at Google
ただし、Google Chromeの場合はサーバ証明書に Certificate Transparency (SCT)が入っていないといけないので、SCT必須となる以前の2018年5月1日以前に発行された証明書に偽造する必要があります。
*1:実際には P-384 の order を法として2の逆数を求めます