Android 開發中的SSL pinning

xiangzhihong發表於2022-12-20

一、SSL

在日常的安全滲透過程中,我們經常會遇到瓶頸無處下手,這時候如果攻擊者從APP進行突破,往往會有很多驚喜。但是目前市場上的APP都會為防止別人惡意盜取和惡意篡改進行一些保護措施,比如模擬器檢測、root檢測、APK加固、程式碼混淆、程式碼反除錯、反脫殼、簽名校驗等等對抗機制。而測試人員對APP進行滲透的首步操作通常就是上burp或者Charles這類抓包工具進行抓包,檢視請求記錄裡的域名及連結地址是否可以進一步利用。

我們都知道http協議傳輸的是明文資訊,是可以直接捕獲的,從而造成了資料洩露。為了防止中間人的攔截,出現了HTTPS加密機制。在HTTPS中,使用了證書+數字簽名解決了抓包的問題,這裡用到了兩個概念:數字簽名和數字證書。

  • 數字簽名:是傳送方的明文經歷了兩次加密得到的兩個東西組成,一個是hash ,一個是經過私鑰加密。
  • 數字證書:其實就是明文+數字簽名。但是數字證書中的內容遠不止這倆,還包括了權威機構的資訊,伺服器的域名,最重要的是有簽名的計算方法,還有就是證書中還包括公鑰,公鑰用於發放給請求證書的客戶端。

SSL(安全套接字層)又被稱為TLS(資料層安全協議),是一種為網路通訊提供資料完整性的一種安全協議。它位於TCP/IP協議與各種應用協議之間。SSL協議主要分為兩個部分:Handshake Protocol和Record Protocol。

Handshake protocol是用來協商加密通訊資料的金鑰,Record Protocol定義傳輸內容的格式。事實上,HTTPS就是使用SSL/TLS協議進行加密傳輸,讓客戶端拿到伺服器的公鑰,然後客戶端隨機生成一個對稱加密的秘鑰,使用公鑰加密,傳輸給服務端,後續的所有資訊都透過該對稱秘鑰進行加密解密,完成整個HTTPS的流程。

二、SSL pinning方案

證書鎖定本質是對抗中間人攻擊,並非用於對抗抓包破解。但如果程式邏輯未被注入執行在"可信環境"中倒是有些作用。SSL證書鎖定之前,我們需要理解一些基本的概念:

  • 可信CA: CA(Certificate Authority)是數字證書認證中心的簡稱,是指發放、管理、廢除數字證書的機構。CA的作用是檢查證書持有者身份的合法性,並簽發證書,以防證書被偽造或篡改,以及對證書和金鑰進行管理。
  • 雙向鎖定:在客戶端鎖定服務端證書的基礎上,服務端對客戶端的證書也進行鎖定,需要客戶端再做一次證書預埋。多見於金融業務場景。
  • 證書鏈:證書鏈就是Root CA簽發二級Intermediate CA,二級Intermediate CA可以簽發三級Intermediate CA,也可以直接簽發使用者證書。從Root CA到使用者證書之間構成了一個信任鏈:信任Root CA,就應該信任它所信任的二級Intermediate CA,從而就應該信任三級Intermediate CA直至信任使用者證書。
  • 逐級驗證:客戶端對於收到的多級證書,需要從站點證書(leaf certificate)開始逐級驗證,直至出現作業系統或瀏覽器內建的受信任CA 根證書(root certificate)。

image.png

SSL Pinning技術指的是在應用程式中只信任固定證書或是公鑰。應用程式開發人員在程式中使用SSL pinning技術作為應用流量的附加安全層。實現SSL pinning的方法主要有兩種:證書固定和公鑰固定。

  • 證書固定:開發者將SSL證書的某些位元組碼硬編碼在用程式中。當應用程式與伺服器通訊時,它將檢查證書中是否存在相同的位元組碼。如果存在,則應用程式將請求傳送到伺服器;如果位元組碼不匹配,它將丟擲SSL證書錯誤。此技術可防止攻擊者使用自己的自簽名證書。
  • 公鑰固定:在客戶訪問網站時進行公鑰固定中,伺服器將其公鑰固定(透過注入)在客戶端(客戶)瀏覽器中。當客戶端重新訪問同一網站時,伺服器將標識其公共金鑰以檢查連線的完整性。此技術還可以防止攻擊者使用自簽名證書。

通常,我們在測試一個應用的時候會設定代理,從而獲取應用在執行過程中的流量。我們使用代理工具獲取流量的話,需要在裝置中安裝我們自己的證書到可信任的根證書中,這樣應用程式才會將我們的證書視為可信任的有效證書,允許代理工具攔截應用的流量。但是使用的SSL pinning技術的應用程式,只信任指定的證書,那麼我們就算把我們的證書安裝到裝置中,應用程式也不會信任我們的證書,這樣的話我們就不能透過代理的方式來攔截應用的流量。

三、SSL pinning方案對比

根據安全級別的不同,證書鎖定方案大體可以分為如下幾種:

安全等級策略信任範圍破解方法
0完全相容策略信任所有證書包括自簽發證書無需特殊操作
1系統/瀏覽器預設策略信任系統或瀏覽器內建CA證書、使用者安裝的證書裝置安裝代理證書(使用者)
2System CA pinning只信任系統根證書,不信任使用者安裝的證書(android7支援配置network-security-config)注入或者root後將使用者證書複製到系統證書目錄
3CA pinningRoot(intermediate)certificate pinning信任指定CA辦法的證書Hook注入等方式篡改鎖定邏輯
4Leaf Certificate pinning信任指定站點證書Hook注入等方式篡改鎖定邏輯,如遇雙向鎖定需將app自帶證書匯入代理軟體

3.1 站點證書鎖定

鎖定站點證書是一種比較安全的做法,但是它有個缺陷就是需要維護預埋證書。如果你沒考慮過更新預埋證書的話就會出現SSL握手失敗的提示。現在的站點證書一般有效期都是在1到2年,所以做站點證書鎖定還要保證服務可用性的話就得必須實現客戶端鎖定證書指紋的更新。更新站點證書的網路請求通常有如下策略:

  • 指紋更新請求被劫持到的機率比較低,不鎖定更新指紋請求直接使用https完成,缺點是安全性稍弱。
  • 自簽名證書的有效期非常長,用自簽名證書鎖定指紋更新請求,缺點是相容性稍弱。

通常,鎖定站點證書時,服務端需要實現證書指紋下發介面。還有每到證書即將過期的時候需要人為的將證書指紋配置到客戶端中。提取指紋配置可以由程式碼實現,但是簽發證書是由第三方CA完成的,所以此種方式並不是很智慧。並且,"人"為因素的引入會給業務穩定性帶來極大風險。

3.2 中間證書鎖定

鎖定中間證書或根證書的優勢是安全性接近鎖定站點證書,且這兩證書的有效期一般很長,可以達到10年到30年。所以,在不考慮熱更新證書指紋的情況下,可以使用此種方案。

除了證書有效期時間長的優勢外,鎖定間證書或根證書還可以更好的兼顧業複雜的業務場景。因為企業子域名很多情況下都是自己業務的站點證書,但是一個企業通常站點證書都是由一箇中間證書(根證書)衍生下來的。所以,鎖定間證書或根證書不用特別對每個業務線做調整,一套策略或者方案基本可以適用企業整個業務線。

比如,我們有一箇中間證書,到期時間為2029年,對於這麼長的時間視窗,我們完全可以讓指紋隨著應用更新完成迭代。

image.png

但是,鎖定中間證書的方案會遇到一個問題,那就是更換證書CA(數字證書頒發機構)。這就需要透過備份一些可能會用的到CA指紋,中間證書的量級相對於根證書要高出很多,而且也不好預測將來可能會更換到哪些中間證書。

3.3 根證書鎖定

參考作業系統更新預埋CA根證書的機制,我們可以透過自升級完成鎖定CA的指紋更新。在Android N系統版本中,內建了150多個系統根證書。而實際作為一個應用是不需要信任這麼多CA的根證書的。可靠賣證書的CA就那麼十來家,業務的安全需求決定了需要哪種型別的證書,這樣備份證書的範圍就收窄了,且根證書的數量級相對小,所以就沒中間證書備份難的問題。

image.png

目前主流的SSL證書主要分為 DV < OV < EV,安全性和價格是遞增的。DV和OV型證書最大的差別是:DV型證書不包含企業名稱資訊;而OV型證書包含企業名稱資訊。

image.png

綜合看來,根鎖定策略的安全性實施難度比較適合賬號業務。接下來就是備份證書的選擇,備份鎖定證書的主要的考量因素:

  • 有效期
  • 安全性
  • 相容性

例如,下面是Mac系統內建的一些根證書。有效期較長CA分別是HARICA、DigiCert等。其中,對於那些將在未來某個時間才會生效的根證書,我們可以將這類根作為備份,因為這些有效期長且在較早的系統版本中預埋,說明相容性也過關,如果對安全性有更改追求可以預埋些EV證書。

image.png

解決根證書的鎖定問題後,接下來就是解決到期的問題。比如鎖的根證書2031年到期,2031年之後應該如何處理。對於這種場景,我們可以有如下選項:

  • 拒絕連線,安全優先;
  • 允許連線,可用優先;
  • 提示風險讓使用者選擇,折中策略;

3.4 客戶端系統證書鎖定

這個鎖定方案相對前三個要保守許多,安全性提升也相對有限,不過可以作為一種加強的方案。我們需要做的僅僅是將通用作業系統中使用者安裝的第三方證書移除APP的證書信任列表。並且從Android7.0版本開始預設支援此特性,操作的方法也很簡單,只需要透過network-security-config更改配置即可。

在 Android 7.0 及其以後的版本中,我們可以透過使用“網路安全性配置”來自定義其安全(HTTPS、TLS)連線的行為,無需修改任何程式碼,下面是官方關於網路安全配置的說明

四、網路安全性配置

下面我們簡單介紹一下,網路安全性配置network-security-config,如果想要了解更多細節,可以點選檢視:網路安全配置的說明

4.1 系統證書鎖定

通常,Android的應用包只有在release模式下只能信任系統證書,移除使用者安裝的證書的信任。

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <base-config>
        <trust-anchors>
            <certificates src="system"/>
        </trust-anchors>
    </base-config>
</network-security-config>

debug模式下可以加入對信任使用者安裝的證書。

<?xml version="1.0" encoding="utf-8"?>
<network-security-config> 
  <base-config> 
    <trust-anchors> 
      <certificates src="system"/> 
    </trust-anchors> 
  </base-config>  
  <debug-overrides> 
    <trust-anchors> 
      <certificates src="system"/>  
      <certificates src="user"/> 
    </trust-anchors> 
  </debug-overrides> 
</network-security-config>

4.2 根證書鎖定

在使用根證書鎖定時,強制鎖定了兩個根證書GoDaddy Class 2 Certification Authority Root Certificate和DigiCert。debug模式下加入對信任使用者安裝的證書,超過根證書的時間時解除鎖定。

<?xml version="1.0" encoding="utf-8"?>
 
<network-security-config> 
  <!-- Android N 7.0 以上版本 -->  
  <domain-config> 
    <domain includeSubdomains="true">www.mi.com</domain>  
    <pin-set expiration="2034-06-30"> 
      <!--GoDaddy Class 2 Certification Authority Root Certificate-->  
      <pin digest="SHA-256">VjLZe/p3W/PJnd6lL8JVNBCGQBZynFLdZSTIqcO0SJ8=</pin>  
      <!--備份pin-->  
      <pin digest="SHA-256">VjLZe/p3W/PJnd6lL8JVNBCGQBZynFLdZSTIqcO0SJ8=</pin> 
    </pin-set>  
    <!-- TrustKit Android API -->  
    <trustkit-config enforcePinning="true"> 
      <!-- Add a reporting URL for pin validation reports -->  
      <report-uri>http://report.m.com/log_report</report-uri> 
    </trustkit-config> 
  </domain-config>
  <!-- Debug模式 -->  
  <debug-overrides> 
    <trust-anchors> 
      <!-- For debugging purposes, add a debug CA and override pins -->  
      <!--<certificates overridePins="true" src="@raw/debugca" />-->  
      <certificates overridePins="true" src="user"/> 
    </trust-anchors> 
  </debug-overrides> 
</network-security-config>

五、裝置證書

裝置ROM發版相對app發版要複雜許多,所以裝置的證書鎖定場景複雜性更高。為了方便說明,我們先將裝置抽象成兩大類:系統自帶的證書,比如AndroidTV作業系統自帶的;另一種是系統沒有預製的證書,比如實時作業系統(RTOS)。如果裝置是第一類通用作業系統,則比較好處理。

  • 如果證書是CA簽發的,只需信任系統證書即可,最好同時開啟系統分割槽保護。
  • 如果證書是自簽發的,除了信任系統證書以外還需要額外只信任此自簽發證書。需要注意的是,切勿為了跑通業務盲目信任所有證書。

如果,一些業務剛開發的時候可能還沒買證書,所以初期程式碼是信任所有證書,後來買正式證書後忘記修復證書信任程式碼。例如沒買證書之前curl使用了-k引數,買完證書後忘記統一除去此引數。

 -k, --insecure Allow connections to SSL sites without certs (H)

如果裝置是第二類RTOS,首先得需要確認其是否支援SSL。其上執行的業務是否需要SSL,如果需要且支援,則可以透過自行預製根再參考前文完成鎖定。

六、TrustKit

對於Android N及更高版本,我們可以透過新增了證書方式來保證安全,但是對於Android N一以下的版本,我們需要怎麼做呢?此處給大家介紹一款開源TrustKit,它可以在network_security_config.xml檔案中使用相同的格式來新增對Android N下版本的支援。

下面我們就來說說TrustKit的使用。首先,在專案的app/build.gradle檔案新增如下依賴。

implementation 'com.datatheorem.android.trustkit:trustkit:<last_version>'

然後,開啟network_security_config.xml檔案,新增如下內容:

<!-- res/xml/network_security_config.xml -->
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
  <!-- Pin the domain www.datatheorem.com -->
  <!-- Official Android N API -->
  <domain-config>
    <domain>www.datatheorem.com</domain>
    <pin-set>
      <pin digest="SHA-256">k3XnEYQCK79AtL9GYnT/nyhsabas03V+bhRQYHQbpXU=</pin>
      <pin digest="SHA-256">2kOi4HdYYsvTR1sTIR7RHwlf2SescTrpza9ZrWy7poQ=</pin>
    </pin-set>
    <!-- TrustKit Android API -->
    <!-- Do not enforce pinning validation -->
    <trustkit-config enforcePinning="false">
      <!-- Add a reporting URL for pin validation reports -->
      <report-uri>http://report.datatheorem.com/log_report</report-uri>
    </trustkit-config>
  </domain-config>
  <debug-overrides>
    <trust-anchors>
      <!-- For debugging purposes, add a debug CA and override pins -->
      <certificates overridePins="true" src="@raw/debugca" />
    </trust-anchors>
  </debug-overrides>
</network-security-config>

完成上述配置之後,在AndroidManifest.xml檔案中引入上面的配置檔案,如下。

<?xml version="1.0" encoding="utf-8"?>
<manifest ... >
    <application 
         android:networkSecurityConfig="@xml/network_security_config"
        ... >
        ...
    </application>
</manifest>

當然,我們也可以透過程式碼的方式來引入TrustKit並進行相關的配置。

@Override
protected void onCreate(Bundle savedInstanceState) {
  super.OnCreate(savedInstanceState);


  // Using the default path - res/xml/network_security_config.xml
  TrustKit.initializeWithNetworkSecurityConfiguration(this);


  // OR using a custom resource (TrustKit can't be initialized twice)
  TrustKit.initializeWithNetworkSecurityConfiguration(this, R.xml.my_custom_network_security_config);


  URL url = new URL("https://www.datatheorem.com");
  String serverHostname = url.getHost();
  
  //Optionally add a local broadcast receiver to receive PinningFailureReports
  PinningValidationReportTestBroadcastReceiver receiver = new PinningValidationReportTestBroadcastReceiver();
          LocalBroadcastManager.getInstance(context)
                  .registerReceiver(receiver, new IntentFilter(BackgroundReporter.REPORT_VALIDATION_EVENT));


  // HttpsUrlConnection
  HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
  connection.setSSLSocketFactory(TrustKit.getInstance().getSSLSocketFactory(serverHostname));


  // OkHttp 2.x
  OkHttpClient client =
    new OkHttpClient()
        .setSslSocketFactory(OkHttp2Helper.getSSLSocketFactory());
  client.interceptors().add(OkHttp2Helper.getPinningInterceptor());
  client.setFollowRedirects(false);


  // OkHttp 3.0.x, 3.1.x and 3.2.x
  OkHttpClient client =
    new OkHttpClient.Builder()
        .sslSocketFactory(OkHttp3Helper.getSSLSocketFactory())
        .addInterceptor(OkHttp3Helper.getPinningInterceptor())
        .followRedirects(false)
        .followSslRedirects(false)


  // OkHttp 3.3.x and higher
  OkHttpClient client =
    new OkHttpClient.Builder()
        .sslSocketFactory(OkHttp3Helper.getSSLSocketFactory(), OkHttp3Helper.getTrustManager())
        .addInterceptor(OkHttp3Helper.getPinningInterceptor())
        .followRedirects(false)
        .followSslRedirects(false)
    .build();
}


class PinningFailureReportBroadcastReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        PinningFailureReport report = (PinningFailureReport) intent.getSerializableExtra(BackgroundReporter.EXTRA_REPORT);
    }
}

關於SSL pinning的知識就介紹這麼多,有疑問歡迎留言。

相關文章