加密未必是死路#

網路協定上的加密會讓協定分析與「重新實作以測試安全問題」變得困難。幸運的是,多數應用程式不會自己造密碼學輪子,而是採用某個版本的 TLS(見第 7 章末)。既然 TLS 是已知量,我們通常可以把它從協定中剝掉,或用標準工具與函式庫重新實作它。

先搞清楚對方用了什麼加密#

SuperFunkyChat 支援 TLS 端點,但需要傳入伺服器憑證的路徑來啟用;它的二進位發行版附了一個 server.pfx 供此用途。

ChatServer --server_cert ChatServer/server.pfx

輸出中有兩個訊號可以確認 TLS 已啟用:伺服器憑證的 subject 名稱被印出來,以及多了一個監聽中的 TLS 埠 12346

用戶端這邊只要加上 --tls 參數就好,不必指定埠號——用戶端會自動把埠號加一來對應 TLS 服務。連上後它會把連線的基本資訊印在主控台,包括協商出的 TLS 版本、金鑰交換(key exchange)、加密演算法與雜湊演算法,以及憑證的 Cert Subject 與 Cert Issuer。

Cert Subject 通常代表憑證的擁有者,Cert Issuer 則是簽發該伺服器憑證的權威機構,也就是憑證鏈上的下一張憑證。當兩者相同時,通常表示這是一張自簽(self-signed)憑證

伺服器與用戶端的完整輸出

啟動帶 TLS 憑證的伺服器:

$ ChatServer --server_cert ChatServer/server.pfx
ChatServer (c) 2017 James Forshaw
WARNING: Don't use this for a real chat system!!!
Loaded certificate, Subject=CN=ExampleChatServer
Running server on port 12345 Global Bind False
Running TLS server on port 12346 Global Bind False

正常的用戶端 TLS 連線:

$ ChatClient --tls user1 127.0.0.1
Connecting to 127.0.0.1:12346
TLS Protocol: TLS v1.2
TLS KeyEx   : RsaKeyX
TLS Cipher  : Aes256
TLS Hash    : Sha384
Cert Subject: CN=ExampleChatServer
Cert Issuer : CN=ExampleChatServer

這裡協商出的是 TLS 1.2,金鑰交換為 RsaKeyX,加密演算法 Aes256,雜湊 Sha384;Subject 與 Issuer 相同,代表憑證是自簽的。

用中間人攻擊解密 TLS 流量#

解密 TLS 流量最常見的手法,是對網路流量主動發動中間人攻擊(man-in-the-middle, MITM):代理程式解開用戶端送來的 TLS,在中間任意觀察與竄改流量,再重新加密後轉送給真正的伺服器。

圖表 8-1:MITM TLS 代理範例

「中間人攻擊不正是 TLS 要防的東西嗎?」是的。但只要你對用戶端應用程式有足夠的控制權(例如能決定它信任哪些根憑證),在測試情境下這個攻擊通常仍然做得到——這正是後面「換上自己的憑證」那一節要處理的事。

好消息是,替代理程式加上 TLS 支援往往只要一兩行程式碼:在 template 上多掛一個 TLS 網路層。整體結構就是「用戶端 → TLS 解密 → 解析/竄改 → TLS 重新加密 → 真實伺服器」。

有兩個地方要留意:

  • 埠號要各加一:用戶端連 TLS 時會自動把埠號加 1,所以代理程式要監聽 4445、轉送到 12346。
  • TLS 層必須加在 parser 層之前,否則 parser 會試圖去解析 TLS 網路流量,結果自然不會好。

代理之後再連一次用戶端,會看到幾個明顯變化:TLS 版本從 v1.2 掉成 v1.0,Cipher 與 Hash 演算法改變,金鑰交換換成具備前向保密(forward secrecy)的 ECDH(Elliptic Curve Diffie–Hellman),而 Cert Issuer 變成了代理函式庫的 CA。代理函式庫會依照伺服器原本的憑證自動產生一張有效憑證,但簽發者是函式庫自己的 CA 憑證;若沒有設定 CA 憑證,它會在第一次使用時自行產生一張。

協商結果被改變這件事本身可能造成麻煩:有些應用程式會檢查協商出的 TLS 版本。如果用戶端只肯連 TLS 1.2 的服務,加上這行強制指定版本即可。

tls.Config.ServerProtocol = System.Security.Authentication.SslProtocols.Tls12;
為代理程式加上 TLS 層,以及代理後的用戶端輸出

把擷取用代理程式的 template 初始化改成:

var template = new FixedProxyTemplate();
// Local port of 4445, destination 127.0.0.1:12346
template.LocalPort = 4445;
template.Host = "127.0.0.1";
template.Port = 12346;

var tls = new TlsNetworkLayerFactory();
template.AddLayer(tls);
template.AddLayer<Parser>();

透過這個代理程式連線的用戶端輸出:

C:\> ChatClient user1 127.0.0.1 --port 4444 -l
Connecting to 127.0.0.1:4445
TLS Protocol: TLS v1.0
TLS KeyEx   : ECDH
TLS Cipher  : Aes256
TLS Hash    : Sha1
Cert Subject: CN=ExampleChatServer
Cert Issuer : CN=BrokenCA_PleaseFix

對照未經代理的連線可以看出三處差異:協定版本降為 TLS v1.0、Cipher 與 Hash 不同、Cert Issuer 變成代理函式庫自動產生的 CA(CN=BrokenCA_PleaseFix)。Cert Subject 則沿用原伺服器憑證的名稱。

換上自己的憑證#

要讓中間人攻擊真正成立,得讓用戶端把你產生的憑證當成有效的根 CA 來接受。步驟是:產生一張自己的 CA 憑證 → 讓代理程式用它當根憑證 → 把公開憑證匯入用戶端信任的根存放區。

匯入的方法取決於很多因素:用戶端跑在什麼裝置上(行動裝置通常更難搞),以及應用程式的信任根存放在哪裡——是系統存放區,還是內嵌在應用程式二進位檔裡?由於 Windows 應用程式常直接參考系統的信任根存放區,把憑證匯入該存放區,SuperFunkyChat 就會信任它。

匯入任意的根 CA 憑證要非常謹慎。一旦有人取得對應的私鑰,即使你原本只打算測試單一應用程式,對方也能對你所有的 TLS 連線發動中間人攻擊。絕對不要在你日常使用或在意的裝置上安裝來路不明的憑證。

憑證名稱不符的問題#

把 CA 加進信任根之後,用 ChatClient 的 --verify 參數啟用伺服器憑證驗證(預設關閉,以便讓伺服器使用自簽憑證),連線仍會失敗:

SSL Policy Errors: RemoteCertificateNameMismatch
Error: The remote certificate is invalid according to the validation procedure.

問題在於:我們雖然讓 CA 被信任了,但憑證上的伺服器名稱(多數情況下就是憑證的 subject)對目標而言是錯的——因為連線經過代理,用戶端連的主機是 127.0.0.1,而產生的憑證卻是照著原伺服器的憑證做出來的。加上這兩行指定產生憑證的 subject 名稱即可解決:

tls.Config.SpecifyServerCert = true;
tls.Config.ServerCertificateSubject = "CN=127.0.0.1";

再試一次,用戶端就能成功連上代理程式、再連到真正的伺服器,而所有流量在代理程式內部都是未加密的。

同樣的修改也可以套用到前一節的網路用戶端與伺服器程式碼上,框架會負責確保只建立指定的 TLS 連線。你甚至可以在設定裡指定 TLS 用戶端憑證來做雙向認證(mutual authentication),不過那屬於超出本書範圍的進階題目。

產生自己的 root CA 憑證

在 CANAPE.Cli 執行以下腳本(generate_ca_cert.csx),產生新的 CA 憑證,把憑證與金鑰輸出成 PFX 檔,並把公開憑證輸出成 PEM 格式:

using System.IO;

// Generate a 4096 bit RSA key with SHA512 hash
var ca = CertificateUtils.GenerateCACert("CN=MyTestCA",
    4096, CertificateHashAlgorithm.Sha512);

// Export to PFX with no password
File.WriteAllBytes("ca.pfx", ca.ExportToPFX());

// Export public certificate to a PEM file
File.WriteAllText("ca.crt", ca.ExportToPEM());

執行後磁碟上會多出 ca.pfxca.crt。把 ca.pfx 複製到代理程式腳本所在的目錄,並在初始化 TLS 層之前加上這一行:

CertificateManager.SetRootCert("ca.pfx");

之後所有自動產生的憑證都會以你的 CA 憑證作為根憑證。

在 Windows 匯入 ca.crt 的步驟
  1. 從「執行」對話框或命令提示字元執行 certmgr.msc,開啟 Windows 憑證管理員。

  2. 選擇「受信任的根憑證授權單位(Trusted Root Certification Authorities)」▸「憑證(Certificates)」。

    圖表 8-2:Windows 憑證管理員

  3. 選擇「動作(Action)」▸「所有工作(All Tasks)」▸「匯入(Import)」,啟動匯入精靈。

  4. 按「下一步」,輸入或瀏覽到 ca.crt 的路徑,再按「下一步」。

    圖表 8-3:以憑證匯入精靈匯入檔案

  5. 確認「憑證存放區」欄位顯示的是「受信任的根憑證授權單位」,按「下一步」。

    圖表 8-4:憑證存放區的位置

  6. 最後一頁按「完成」。系統會跳出一個警告對話框——請認真看待它的警告內容,然後才按「是」。

    圖表 8-5:匯入根 CA 憑證的警告

只要你的應用程式使用系統根存放區,TLS 代理連線就會被信任。

結語#

本章示範了幾種途徑,讓你能根據「線上流量檢視」或「逆向工程實作」的分析結果,把目標應用協定重新實作出來——從最省力的流量重放、改造既有分析代理程式,到直接重用二進位執行碼,再到把 TLS 從協定中剝離。

這些技巧能讓你解密與加密許多應用程式的流量,以進行分析與安全測試。而這仍然只是這個複雜主題的皮毛:在研究網路協定的安全問題時,還有許多有趣的挑戰等著你。