這 10 行比較字符串相等的代碼給我整懵了,不信你也來看看

csdn 發佈 2020-06-26T17:03:16+00:00

例如 JDK 1.6.0_17 中的Release Notes中就提到了中的bug的修復,如下圖所示:大家可以看看這次變更的的詳細信息openjdk中的 bug fix diff為:那麼,Timing Attack 真的可行嗎?本文通過實驗證明,通過這種計時攻擊方式能夠攻破一個基於 OpenSSL 的 web 伺服器的私鑰。

作者 | 碼農唐磊

責編 | Aholiab

封圖 | CSDN 付費下載自視覺中國

出品 | CSDN(ID:CSDNnews)

抱歉用這種標題吸引你點進來了,不過你不妨看完,看看能否讓你有所收穫。

先直接上代碼:

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可能知道些眉目,與安全有關。

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

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

public static boolean isEqual(byte[] digesta, byte[] digestb) {
if (digesta == digestb) return true;
if (digesta == || digestb == ) {
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)。(也有人翻譯成時序攻擊)

計時攻擊是邊信道攻擊(或稱"側信道攻擊", 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中就提到了MessageDigest.isEqual中的bug的修復,如下圖所示:

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

那麼,Timing Attack 真的可行嗎?

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

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

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

關鍵字: