RSA 演算法一種常見的非對稱加密演算法, 常用來對一些在網路上傳輸的敏感資訊進行加密, 本文將概述RSA演算法的流程以及一種意想不到的"旁門左道"的攻擊方式.
RSA
RSA 演算法流程如下,
- 找到互質的兩個數,
p
和q
, 計算N = p*q
- 確定一個數
e
, 使得e
與(p-1)(q-1)
互質, 此時公鑰為(N, e)
, 告訴給對方 - 確定私鑰
d
, 使得e*d-1
能夠被(p-1)(q-1)
整除 - 訊息傳輸方傳輸訊息
M
, 加密密文C
為: $C = M^e \mod N$ - 訊息接受方通過收到密文訊息
C
, 解密訊息M
: $M= C^d \mod N$
RSA演算法依賴於尤拉定理, 一個簡化版本為大致為 a
和 p
互質, 那麼有,
$ a^{p-1} \equiv 1 \mod p$, a
的 p-1
次方 對 p
取餘為1
, (a
的 p-1
次方減去1
可以整除 p
).
尤拉定理的證明比較複雜,本來有一個絕妙的證明方式的, 但由於微信公眾號字數有限, 這裡就省略了(什麼? 這跟費馬有什麼關係? 實在要看的可以看文末參考資料)
舉個例子
N = pq
, 取倆素數 p=11, q = 3, N = p * q = 33
, 取 e
與 (p-1)(q-1) = 20
互質的數 e = 3
, 然後通過 $ed \equiv 1 \mod (p-1)(q-1)$ 確定私鑰,
即取一個 d
使得 3*d -1
能 20 被整除, 假設取 d=7
或者d=67
. (3*7-1=20
當然能被20整除, 3*67-1=200
也能被20整除)
因此 public key 為 (N=33, e=3)
, private key 為 d=7
或者d=67
,
假設加密訊息M=8
,
通過加密演算法 $C = M^e \mod N$
得到密文 C=8^3 % 33 = 17
再來看解密, 由$M= C^d \mod N$, 得到明文 M = 17^7 % 33 = 8
或者 M=17^67 % 33=8
, 是不是很神奇? (這裡^
表示多少次方, 後文中的有的表示異或)
(來, 安利一個計算器的工具, bc
命令, 支援任意精度的計算, 其實 Mac簡單的計算就可以通過前面介紹的 Alfred 可以方便得完成)
RSA 破解
如果需要破解 RSA 的話, 就是需要找到 p
和 q
, 使得 pq=33
, 如果知道了 p
和 q
就能通過公鑰 N
和 e
反推出私鑰 d
了. 然而大數分解在歷史以來就一直是數學上的難題.
當然上面所述的案例較簡單, 當 N 很大時, 就特別困難了. 曾經有人花了五個月時間分解了這個數39505874583265144526419767800614481996020776460304936454139376051579355626529450683609727842468219535093544305870490251995655335710209799226484977949442955603
(159位數), RSA-155 (512 bits) [from wikipedia].
這條路走不通, 就有人走了"旁門左道"了, Stanford 的幾個研究者用了兩個小時破解了 OpenSSL 0.9.7 的 1024-bit 的 RSA 私鑰 (感興趣的同學可以看他們的論文Remote Timing Attacks are Practical),
用到的方法就是後面提到的時序攻擊(或譯為"計時攻擊"), 主要思想是因為在進行加密時所進行的模指數運算是一個bit一個bit進行的, 而bit為1所花的運算比bit為0的運算要多很多(耗時久),
因此可以通過得到大量訊息與其加密時間, 然後基於統計的方法就可以大致反推出私鑰的內容.
計時攻擊(Timing Attack)
計時攻擊是邊通道攻擊(或稱"側通道攻擊", Side Channel Attack, 簡稱SCA) 的一種, 主要是一種利用不同的輸入會有不同的執行時間這個特點.
舉個具體的例子, 這個來自playframewok 裡用來驗證cookie(session)中的資料是否合法(包含簽名的驗證), 也是我寫這篇文章的由來.
def safeEquals(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
, 如果每一 bit 都相等的話, 兩個字串肯定相等, 最後的equal
肯定為0, 否則為1.
但從效率角度上講, 不是應該只要中途發現某一位的結果為1了就可以立即返回 false 了嗎? (如下所示)
for (i <- Array.range(0, a.length)) {
if (a(i) ^ b(i) != 0) // or a(i) != b[i]
return false
}複製程式碼
結合方法名稱 safeEquals
可能知道些眉目, 與安全有關, 延遲計算等提高效率的手段見過不少, 但這種延遲返回的還是很少見.
這種手段可以讓呼叫 safeEquals("abcdefghijklmn", "xbcdefghijklmn")
和呼叫 safeEquals("abcdefghijklmn", "abcdefghijklmn")
的所耗費的時間一樣,
防止通過大量的改變輸入並通過統計執行時間來暴力破解出要比較的字串, 這裡其實都忽略了對比較字串長度的attack問題.
舉個例子, 假設某個使用者設定了密碼為 password
, 通過從a到z(實際範圍可能更廣)不斷列舉第一位, 最終統計發現 p0000000
的執行時間比其他從任意a~z
的都短,
這樣就能猜測出使用者密碼的第一位很可能是p
, 然後再不斷一位一位迭代下去最終破解出使用者的密碼. 如果密碼通過hash加密後也能通過這種攻擊方式得到hash後的密文.
當然, 從理論角度上講這個確實容易理解, 如上文所提到的學術界已經有論文發表指出用這種計時攻擊的方法破解了 OpenSSL 0.9.7 的RSA加密演算法了.
然而在實際中是否存在這樣的攻擊問題呢?
因為好像通過統計執行時間總感覺不太靠譜, 這個執行時間對環境太敏感了, 比如網路, 記憶體, CPU負載等等都會影響.
儘管如此, 各個軟體的實現也都採用了這種 safeEquals
的方法.
JDK 1.6.0_17
中的Release Notes中就提到了MessageDigest.isEqual
中的bug的修復
BugId | Category | Subcategory | Description |
---|---|---|---|
6863503 | java | classes_security | SECURITY: MessageDigest.isEqual introduces timing attack vulnerabilities |
這次變更的diff詳細資訊來源為:
為了防止(特別是與簽名/密碼驗證等相關的操作)被 timing attack, 目前各大語言都提供了響應的安全比較函式, 例如 "世界上最好的程式語言" -- 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來確定兩個字串是否相等.
參考資料:
- Timing Attacks on RSA: Revealing Your Secrets through the Fourth Dimension
- Remote Timing Attacks are Practical
- RSA演算法原理
- 費馬小定理
p.s 如果你覺得這文章對你有那麼一點點收穫, 請不要猶豫掃描下面二維碼關注我的公眾號, 如果你再能幫忙轉發一下就更好了. 麼麼噠.