這 10 行比較字串相等的程式碼給我整懵逼了,不信你也來看看|原創版

程式猿石頭發表於2020-06-25

抱歉用這種標題吸引你點進來了,不過你不妨看完,看看能否讓你有所收穫。​(有收穫,請評論區留個言,沒收穫,下週末我直播吃**,哈哈,這你也信)

補充說明:微信公眾號改版,對各個號主影響還挺大的。目前從後臺資料來看,對我影響不大,因為我這反正都是小號,?閱讀量本身就少的可憐,真相了,?狗頭(剛從交流群學會的表情)。

先直接上程式碼:

boolean safeEqual(String a, String b) {
   if (a.length() != b.length()) {
       return false;
   }
   int equal = 0;
   for (int i = 0; i < a.length(); i++) {
       equal |= a.charAt(i) ^ b.charAt(i);
   }
   return equal == 0;
}

上面的程式碼是我根據原版(Scala)翻譯成 Java的,Scala 版本(最開始吸引程式猿石頭注意力的程式碼)如下:

def safeEqual(a: String, b: String) = {
  if (a.length != b.length) {
    false
  } else {
    var equal = 0
    for (i <- Array.range(0, a.length)) {
      equal |= a(i) ^ b(i)
    }
    equal == 0
  }
}

剛開始看到這段原始碼感覺挺奇怪的,這個函式的功能是比較兩個字串是否相等,首先“長度不等結果肯定不等,立即返回”這個很好理解。

再看看後面的,稍微動下腦筋,轉彎下也能明白這其中的門道:通過異或操作1^1=0, 1^0=1, 0^0=0,來比較每一位,如果每一位都相等的話,兩個字串肯定相等,最後儲存累計異或值的變數equal必定為 0,否則為 1。

再細想一下呢?

for (i <- Array.range(0, a.length)) {
  if (a(i) ^ b(i) != 0) // or a(i) != b[i]
    return false
}

我們常常講效能優化,從效率角度上講,難道不是應該只要中途發現某一位的結果不同了(即為1)就可以立即返回兩個字串不相等了嗎?(如上所示)

這其中肯定有……

再再細想一下呢?

結合方法名稱 safeEquals 可能知道些眉目,與安全有關。

本文開篇的程式碼來自playframewok 裡用來驗證cookie(session)中的資料是否合法(包含簽名的驗證),也是石頭寫這篇文章的由來。

以前知道通過延遲計算等手段來提高效率的手段,但這種已經算出結果卻延遲返回的,還是頭一回!

我們來看看,JDK 中也有類似的方法,如下程式碼摘自 java.security.MessageDigest

public static boolean isEqual(byte[] digesta, byte[] digestb) {
   if (digesta == digestb) return true;
   if (digesta == null || digestb == null) {
       return false;
   }
   if (digesta.length != digestb.length) {
       return false;
   }

   int result = 0;
   // time-constant comparison
   for (int i = 0; i < digesta.length; i++) {
       result |= digesta[i] ^ digestb[i];
   }
   return result == 0;
}

看註釋知道了,目的是為了用常量時間複雜度進行比較。

但這個計算過程耗費的時間不是常量有啥風險? (腦海裡響起了背景音樂:“小朋友,你是否有很多問號?”)

真相大白

再深入探索和了解了一下,原來這麼做是為了防止計時攻擊(Timing Attack)。(也有人翻譯成時序攻擊​)​

計時攻擊(Timing Attack)

計時攻擊是邊通道攻擊(或稱"側通道攻擊", Side Channel Attack, 簡稱SCA) 的一種,邊通道攻擊是一種針對軟體或硬體設計缺陷,走“歪門邪道”的一種攻擊方式。

這種攻擊方式是通過功耗、時序、電磁洩漏等方式達到破解目的。在很多物理隔絕的環境中,往往也能出奇制勝,這類新型攻擊的有效性遠高於傳統的密碼分析的數學方法(某百科上說的)。

這種手段可以讓呼叫 safeEquals("abcdefghijklmn", "xbcdefghijklmn") (只有首位不相同)和呼叫 safeEquals("abcdefghijklmn", "abcdefghijklmn") (兩個完全相同的字串)的所耗費的時間一樣。防止通過大量的改變輸入並通過統計執行時間來暴力破解出要比較的字串。

舉個?,如果用之前說的“高效”的方式來實現的話。假設某個使用者設定了密碼為 password,通過從a到z(實際範圍可能更廣)不斷列舉第一位,最終統計發現 p0000000 的執行時間比其他從任意a~z的都長(因為要到第二位才能發現不同,其他非 p 開頭的字串第一位不同就直接返回了),這樣就能猜測出使用者密碼的第一位很可能是p了,然後再不斷一位一位迭代下去最終破解出使用者的密碼。

當然,以上是從理論角度分析,確實容易理解。但實際上好像通過統計執行時間總感覺不太靠譜,這個執行時間對環境太敏感了,比如網路,記憶體,CPU負載等等都會影響。

但安全問題感覺更像是 “寧可信其有,不可信其無”。為了防止(特別是與簽名/密碼驗證等相關的操作)被 timing attack,目前各大語言都提供了相應的安全比較函式。各種軟體系統(例如 OpenSSL)、框架(例如 Play)的實現也都採用了這種方式。

例如 “世界上最好的程式語言”(粉絲較少,評論區應該打不起架來)—— php中的:

// Compares two strings using the same time whether they're equal or not.
// This function should be used to mitigate timing attacks; 
// for instance, when testing crypt() password hashes.
bool hash_equals ( string $known_string , string $user_string )

//This function is safe against timing attacks.
boolean password_verify ( string $password , string $hash )

其實各種語言版本的實現方式都與上面的版本差不多,將兩個字串每一位取出來異或(^)並用或(|)儲存,最後通過判斷結果是否為 0 來確定兩個字串是否相等。

如果剛開始沒有用 safeEquals 去實現,後續的版本還會通過打補丁的方式去修復這樣的安全隱患。

例如 JDK 1.6.0_17 中的Release Notes[1]中就提到了MessageDigest.isEqual 中的bug的修復,如下圖所示:

MessageDigest timing attack vulnerabilities

MessageDigest timing attack vulnerabilities

大家可以看看這次變更的的詳細資訊openjdk中的 bug fix diff[2]為:

MessageDigest.isEqual計時攻擊

MessageDigest.isEqual計時攻擊

Timing Attack 真的可行嗎?

我覺得各大語言的 API 都用這種實現,肯定還是有道理的,理論上應該可以被利用的。 這不,學術界的這篇論文就宣稱用這種計時攻擊的方法破解了 OpenSSL 0.9.7 的RSA加密演算法了。關於 RSA 演算法的介紹可以看看之前本人寫的這篇文章。

這篇Remote Timing Attacks are Practical[3] 論文中指出(我大致翻譯下摘要,感興趣的同學可以通過文末連結去看原文):

計時攻擊往往用於攻擊一些效能較弱的計算裝置,例如一些智慧卡。我們通過實驗發現,也能用於攻擊普通的軟體系統。本文通過實驗證明,通過這種計時攻擊方式能夠攻破一個基於 OpenSSL 的 web 伺服器的私鑰。結果證明計時攻擊用於進行網路攻擊在實踐中可行的,因此各大安全系統需要抵禦這種風險。

最後,本人畢竟不是專研完全方向,以上描述是基於本人的理解,如果有不對的地方,還請大家留言指出來。感謝。

補充說明2:感謝正在閱讀文章的你,讓我還有動力繼續堅持更新原創。

本人發文不多,但希望寫的文章能達到的目的是:佔用你的閱讀時間,就儘量能夠讓你有所收穫。

如果你覺得我的文章有所幫助,還請你幫忙轉發分享,另外請別忘了點選公眾號右上角加個星標,好讓你別錯過後續的精彩文章(微信改版了,或許我發的文章都不能推送到你那了)。

​原創真心不易,希望你能幫我個小忙唄,如果本文內容你覺得有所啟發,有所收穫,請幫忙點個“在看”唄,或者轉發分享讓更多的小夥伴看到。 ​ 參考資料:

參考資料

[1] Release Notes: http://www.oracle.com/technetwork/java/javase/6u17-141447.html

[2] openjdk中的 bug fix diff: http://hg.openjdk.java.net/jdk6/jdk6/jdk/rev/562da0baf70b

[3] Remote Timing Attacks are Practical: http://crypto.stanford.edu/~dabo/papers/ssl-timing.pdf

相關文章