ksnctf: HTTPS is secure, Writeup (TLS 通信解読)
ksnctf: HTTPS is secure, Writeup (TLS 通信解読)
ksnctf(https://ksnctf.sweetduet.info/ ) の No.33 である HTTPS is secure の Writeup をメモします。 こちらは、2つの類似した証明書を利用して、TLS 通信を破ることでフラグを取り出します。パケットファイルは、http://ksnctf.sweetduet.info/q/33/q33.pcap から落としてきます。以下、Wireshark に取り込んでパケットキャプチャを行います。
TLS 通信の復習と共に、回答の手順が含まれますので、ネタバレしたくない場合は、読まないようお願いします。
TLS 通信のハンドシェイクについて
TLSのコネクションを確立するためには、以下のハンドシェイクを通して行われます。最初の ClientHello でクライアントが利用可能な暗号スイートを提示し、それに対してサーバーは自身が利用可能な暗号スイートと比較し、使用する暗号スイートを返します。それと同時 Certificate メッセージでサーバの証明書も返します。
クライアント サーバー
ClientHello -------->
ServerHello
Certificate*
ServerKeyExchange*
CertificateRequest*
<-------- ServerHelloDone
Certificate*
ClientKeyExchange
CertificateVerify*
[ChangeCipherSpec]
Finished -------->
[ChangeCipherSpec]
<-------- Finished
Application Data <-------> Application Data公開鍵の取得
q33.pcapファイル中では、SSL通信のハンドシェイクが No.4 ~ No.12のパケットで192.168.66.129のクライアントから192.168.0.39のサーバへ, No.73 ~ No.81のパケットで192.168.66.129のクライアントから192.168.0.40のサーバへと2回行われていることが確認されます。 与えられたヒントが、2つの類似した証明書を利用するとのことなので、このそれぞれ2つのSSL通信中の証明書を利用してときます。
192.168.66.129のクライアントから192.168.0.39のサーバへのハンドシェイク
192.168.66.129のクライアントから192.168.0.40のサーバへのハンドシェイク
証明書1の公開鍵
No.4のパケットで最初に ClientHello が投げられています。
具体的にパケットの中身を見てみると、TLS の層で11種類の暗号スイートをサーバへ提示しています。RSAとDHEの暗号化方式を提示していることがわかります。
ClientHelloを受け取ったサーバは次にServerHelloを返しています。
具体的なパケットの中身を見てみるとクライアントから提示された暗号スイートに対して、TLS_RSA_WITH_RC4_128_MD5 の暗号スイートを選択したことが分かります。つまり、RSA の公開鍵暗号でRC4の共有鍵暗号方式でMD5のハッシュ関数を使用していることがわかります。
その後、サーバは証明書をクライアントに返しています。
具体的にパケットの中身を見ていくと、subjectPublicKey の modulus というところに公開鍵の情報が含まれています。こちらの modulus が最初に必要な情報となります。
こちらの値を右クリックで 「コピー > 値」と選択すると、値が取り出されます。
上記は16進数の数字ですが、10進数に直すと以下のようになります。変換は、どのような手段でもいいのですが、「2進数、8進数、10進数、16進数相互変換ツール( https://hogehoge.tk/tool/number.html )」こちらのサイトのツールを利用しました。こちらの値をu1とします。
証明書2の公開鍵
No.73 ~ No.81のパケットで192.168.66.129のサーバと TLS 通信のハンドシェイクが行われています。 証明書1と同様の手順でNo.76のCertificateのメッセージを送っているパケットからsubjectPublicKeyのmodulusの値を取り出してきます。
10進数に直すと以下のようになります。こちらの値をu2とします。
メッセージの復号
先程の公開鍵を取得する中で、ハンドシェイク中で TLS_RSA_WITH_RC4_128_MD5 の暗号スイートが使用されることを決めていました。つまり、公開鍵暗号方式として、RSA が使用することになりました。
こちらの RSA ですが、暗号化されたメッセージを解くことが困難な根拠は、巨大な素数同士の積は素因数分解をすることが困難なことが大変難しいことを根拠としています。
証明書1の先程の取り出した値をu1で積を構成するそれぞれの素数をp1,q1とします。証明書2についても同様にu2で、積を構成するそれぞれの素数をp2,q2とします。
すると、以下のような式となります。
1024ビットの素因数分解を総当りで行うとなると億レベルのお金をかけて何年というほどの計算を要するものになるため、普通にしては解けません。 しかし、u1 と u2 がもし仮に、共通の因数を持つと仮定してみると、それを最大公約数を求める要領で同じ手順で求められることになります。 つまり、q1 と q2 が同じ値として、q = q1 = q2 とすると、以下の式になります。
ここで、最大公約数を求めるために u1 と u2 に対して、ユークリッドの互除法を適用します。すると、q の値が求められます。実際に Python で実行すると、以下のようなコードとなります。
すると、以下の値が得られます。こちらがqの値となります。
すると、u1,u2,qの値が分かっていることから、p1,p2の値はそれぞれ以下のようにして求められます。
実際に計算すると、p1は以下のようになります。
p2は以下のようになります。
以上で RSA の復号が困難な根拠とされた素因数分解ができました。 2つの証明書の類似性とは、2つの数字に共通の因数を持つことのようだったようです。 あとは、これを元に秘密鍵を構築します。
秘密鍵の構築
pem 形式の秘密鍵について
pem 形式の秘密鍵は、ASN.1のバイナリデータをBase64でエンコードされたテキストファイルとなります。こちらのファイルの中身は、-----BEGIN RSA PRIVATE KEY-----で始まり、-----END RSA PRIVATE KEY-----で終わるファイルとなります。この間に挟まれたデータは、ASN.1をDERという形式でエンコードされたバイナリファイルとなります。
RSAにおけるASN.1の定義は、以下のように定められています。versionは0か1の値で、otherPrimeInfosはversionが0のときは存在しません。
(引用: RSA 秘密鍵/公開鍵ファイルのフォーマット http://bearmini.hatenablog.com/entry/2014/02/05/143510 )
publicExponentであるeの値は、公開指数として655337がよく用いられるそうです。以下のような理由があるようです。
(引用: RSA暗号体験入門 http://post1.s105.xrea.com/)
証明書1の秘密鍵の取得
秘密鍵を求めるに当って、prime1 である p , prime2 である q , publicExponent である e の3つをパラメータとして pem形式のファイルを作成することができます。
最初に証明書1から pem形式のファイルを求めるために、p1,q,e(=65537)の値から以下のようにして、pem形式のデータを出力します。こちらのデータをファイルに保存します。
ただし、こちらは Python の Crypto モジュールを使用しますので、pip install pycrypto のようにモジュールをインストールする必要があります。
上記を genpem1.py という名前で保存したとします。すると、以下のようにして秘密鍵を出力します。
こちらの秘密鍵はパスがかかった状態で、このままでは利用できないので、以下のコマンドを実行してパスを解除します。
上を実行しないと、Wireshark に読み込ませると、下記のエラーが出力されます。 ssl_load_key: can't import pem data: Base64 unexpected header error.
(参考: https://serverfault.com/questions/224589/error-while-decrypting-https-traffic-in-wireshark )
証明書2の秘密鍵の取得
こちらも証明書1の秘密鍵の取得の手順と同様にして、以下の gempem2.py というファイル名で Python のコードを準備します。
以下のコマンドを実行して秘密鍵を生成します。
TLS 通信の復号
最後に暗号化された TLS 通信を作成した秘密鍵で復号します。
Wireshark で実際に Certificate メッセージを送っていたフロー上(パケット No.7)で、右クリックします。「プロトコル設定 > RSA keys list...」を選択します。
SSL Decryptというウィンドウで、「IP addres, Port, Protocol, Key File, Password」の列名の表が現れますので、以下のように入力します。Protocolとしてhttp,Portとして443を指定します。httpsと指定するわけではないことに注意して下さい。
すると、先程まで、SSL通信として中身が見えていなかった通信(No.12 と No.81)が以下のようにHTTPプロトコルとして通信が見えるようになりました。
なお、こちらのデバッグとして、「プロトコル設定 > SSL debug file...」という項目でデバッグログの出力設定ができるので、こちらに出力されるログを見ながらデバッグすることが可能です。
最後の上記通信中でやりとりされていたjpgの画像ファイルを取り出します。 「ファイル(F) > オブジェクトをエクスポート > HTTP...(H)」を選択します。 最後にパケット49と125のファイルを選択してSaveします。
すると、2枚の画像ファイルが取り出されます。これらを組み合わせると、"FLAG_"で始まる文字列が取り出されます。画像のイメージはページ最後に載せておきます(答えは伏せています)。
補足
クライアントとして、ブラウザが提示する暗号スイートについて
SSL Labsの以下のサイトの実行結果により、ブラウザが提示する暗号スイートについて、確認してみました。
参照: SSL/TLS Capabilities of Your Browser
Chrome(バージョン: 63.0.3207.0(Official Build)canary (64 ビット))を使用したところ、クライアントから提示される暗号スイートは以下のようになっていました。
Firefox(55.0.3 (64-bit))を使用したところ、クライアントから提示される暗号スイートは以下のようになっていました。
プロフェッショナル SSL/TLSには、以下のような記述がありますが、上記の通り、必ずしも現時点では、ブラウザ等の実装でそうではなさそうです。
(引用: プロフェッショナル SSL/TLS https://www.lambdanote.com/products/tls )
作成したpem形式の秘密鍵のフォーマットの形式確認方法
作成したpem形式の秘密鍵のフォーマットが正しいかを確認する方法があります。
private1.pem.unencryptedについては、以下のコマンドを実行します。
以下の結果が出力されます。
同様にprivate2.pem.unencryptedについては、以下のコマンドを実行します。
以下の結果が出力されます。
上記について、先程のRSAのASN.1での定義を表現したものに対応しています。
private1.pem.unencryptedの場合、以下のように対応することが分かります。
q の値で素数であることの確認
以下のPythonのファイルを用意します。
上記ファイルを実行すると、以下の結果が出力されます。
取得した画像
正常に完了した場合、以下のような画像を取り出すことができます。

Last updated