使用Burp攔截Flutter App與其后端的通信

Flutter是谷歌的移動UI框架,可以快速在iOS和Android上構建高質量的原生用戶界面。Flutter應用程序是用Dart編寫的,這是一種由Google在7年多前創建的語言。

通常情況下我們會通過添加Burp作為攔截代理,來攔截移動應用程序與其后端之間的通信流量(以用于安全評估等)。雖然Flutter應用代理起來可能會有些困難,但這絕對是可能的。

TL;DR

Flutter使用Dart編寫,因此它不會使用系統CA存儲

Dart使用編譯到應用程序中的CA列表

Dart在Android上不支持代理,因此請使用帶有iptables的ProxyDroid

Hook x509.cc中的session_verify_cert_chain函數以禁用鏈驗證(chain validation)

你可以直接使用本文底部的腳本,或者按照下面的步驟獲取正確的字節或偏移量。

測試設置

為了執行我的測試,我安裝了flutter插件并創建了一個flutter應用程序,該應用程序附帶了一個默認的交互式按鈕,用于遞增計數器。我將其修改為通過HttpClient類獲取URL:

該應用程序可以使用flutter build aot進行編譯,并通過adb install推送到設備。

每次按此按鈕時,都會向http://www.nviso.eu發送一個調用,如果成功,則會將其打印到設備日志中。

在我的設備上,我通過Magisk-Frida-Server安裝了Frida,我的Burp證書通過MagiskTrustUserCerts模塊添加到系統CA存儲中。但不幸的是,Burp上并沒有看到有任何流量通過,即使應用程序日志顯示請求成功。

通過 ProxyDroid/iptables 向代理發送流量

HttpClient有一個findProxy方法,其文檔寫的非常清楚:默認情況下,所有流量都直接發送到目標服務器,而不考慮任何代理設置:

設置用于解析代理服務器的功能,該代理服務器用于打開指定URL的HTTP連接。如果未設置此功能,則將始終使用直接連接。

– findProxy文檔

應用程序可以將此屬性設置為HttpClient.findProxyFromEnvironment,它會搜索特定的環境變量,例如http_proxy和https_proxy。即使應用程序是用這個實現編譯的,但在Android上它也將毫無用處,因為所有應用程序都是初始zygote進程的子進程,因此沒有這些環境變量。

也可以定義一個返回首選代理的自定義findProxy實現。對我的測試應用程序進行的快速修改確實表明,此配置將所有HTTP數據發送到了我的代理服務器:

當然,我們無法在黑盒評估期間修改應用程序,因此需要另一種方法。幸運的是,我們總是有iptables fallback來將所有流量從設備路由到我們的代理。在已root的設備上,ProxyDroid可以很好地處理這個問題,我們可以看到所有HTTP流量都流經了Burp。

攔截 HTTPS 流量

這是個更加棘手的問題。如果我將URL更改為HTTPS,會導致Burp SSL握手失敗。這很奇怪,因為我的設備被設置為將我的Burp證書包含為受信任的根證書。

經過一些研究,最終我在一個GitHub issue中找到了有關Windows上問題的解釋,但它同樣也適用于Android: Dart使用Mozilla的NSS庫生成并編譯自己的Keystore。

這意味著我們不能通過將代理CA添加到系統CA存儲來繞過SSL驗證。為了解決這個問題,我們必須深入研究libflutter.so,并找出我們需要修補或hook的,以驗證我們的證書。Dart使用Google的BoringSSL來處理與SSL相關的所有內容,幸運的是Dart和BoringSSL都是開源的。

當向Burp發送HTTPS流量時,Flutter應用程序實際上會拋出一個錯誤,我們可以將其作為起點:

我們需要做的第一件事是在BoringSSL庫中找到這個錯誤。該錯誤實際上已向我們顯示了觸發錯誤的位置:handshake.cc:352。Handshake.cc確實是BoringSSL庫的一部分,并且包含了執行證書驗證的邏輯。第352行的代碼如下所示,這很可能就是我們看到的錯誤。行數并不完全匹配,但這很可能是版本差異的結果。

這是ssl_verify_peer_cert函數的一部分,該函數返回ssl_verify_result_t枚舉,它在第2290行的ssl.h中被定義:

如果我們可以將ssl_verify_peer_cert的返回值更改為ssl_verify_ok (=0),那么我們就可以繼續了。然而,在這個方法中有很多事情正在發生,Frida只能更改函數的返回值。如果我們更改這個值,它仍會因為上面的ssl_send_alert()函數調用而失敗(相信我,我試過)。

讓我們找一個更好的hook的方法。handshake.cc的代碼段正上方是以下代碼,這是驗證鏈的方法的實際部分:

session_verify_cert_chain函數在第362行的ssl_x509.cc中被定義。此函數還返回原始數據類型(布爾值),并且是一個更好的hook選項。如果此函數中的檢查失敗,則它僅通過OPENSSL_PUT_ERROR報告問題,但它沒有像ssl_verify_peer_cert函數那樣的問題。OPENSSL_PUT_ERROR是err.h中第418行被定義的宏,其中包含源文件名。這與用于Flutter應用程序的錯誤的宏相同。

既然我們知道要hook哪個函數了,現在我們要做的就是在libflutter.so中找到它。在session_verify_cert_chain函數中多次調用OPENSSL_PUT_ERROR宏,這樣可以使用Ghidra輕松的找到正確的方法。因此,將庫導入Ghidra,使用Search -> Find Strings并搜索x509.cc。

這里只有4個XREF,因此很容易找到一個看起來像session_verify_cert_chain的函數:

其中一個函數取2個整數,1個“undefined未定義”,并且包含一個對OPENSSL_PUT_ERROR(FUN_00316500)的單獨調用。在我的libflutter.so版本中為FUN_0034b330。現在你要做的是從一個導出函數計算該函數的偏移量并將其hook。我通常會采用一種懶惰的方法,復制函數的前10個字節,并檢查該模式出現的頻率。如果它只出現一次,我就知道我找到了這個函數,并且我可以hook它。這很有用,因為我經常可以為庫的不同版本使用相同的腳本。使用基于偏移的方法,這更加困難。這很有用,因為我可以經常對不同版本的庫使用相同的腳本。對于基于偏移量的方法,更加困難。

所以,現在我們讓Frida在libflutter.so庫中搜索這個模式:

在我的Flutter應用程序上運行此腳本的結果如下:

現在,我們只需使用Interceptor將返回值更改為1 (true):

設置proxydroid并使用此腳本啟動應用程序后,現在我們可以看到HTTP流量了:

我已經在一些Flutter應用程序上對此進行了測試,這種方法適用于所有應用程序。由于BoringSSL庫較為穩定,因此這種方法可能會在未來很長一段時間內都有效。

禁用 SSL Pinning(SecurityContext)

最后,讓我們看看如何繞過SSL Pinning。一種方法是定義一個包含特定證書的新SecurityContext。

對于我的應用程序,我添加了以下代碼讓它只接受我的Burp證書。SecurityContext構造函數接受一個參數withTrustedRoots,默認為false。

應用程序現在將自動接受我們的Burp代理作為任意網站的證書。如果我們現在將其切換到nviso.eu證書,我們將無法攔截連接(請求和響應)。

幸運的是,上面列出的Frida腳本已經繞過了這種root-ca-pinning實現,因為底層邏輯仍然依賴于BoringSSL庫的相同方法。

禁用 SSL Pinning(ssl_pinning_plugin)

Flutter開發人員執行ssl pinning的方法之一是通過 ssl_pinning_plugin flutter插件。此插件實際上是發送一個HTTPS連接并驗證證書,之后開發人員將信任該通信并執行non-pinned HTTPS請求:

該插件是Java實現的,我們可以使用Frida輕松的hook:

結論

這是一個非常有趣的過程,因為Dart和BoringSSL都是開源的,所以進行的非常順利。由于字符串的數量并不多,因此即使沒有任何符號,也能很容易的找到禁用ssl驗證邏輯的正確位置。我掃描函數序言(function prologue)的方法可能并不總是有效,但由于BoringSSL非常穩定,因此在未來的一段時間內它應該都會有效。

【via@FreeBuf.COM