這是why的第 103 篇原創
你好呀,我是why。
如圖,重放攻擊,這題我真的在面試的時候遇到過,兩次。
印象比較深的是第一次遇到這個面試題的時候,也是第一次聽到“重放攻擊”這個詞的時候,一臉矇蔽,於是我就連蒙帶猜的,朝著介面冪等性的方向去答了。
結果就涼了。
要回答怎麼防止重放攻擊,那麼我們得知道啥是重放攻擊。
學術上的解釋是這樣的:
重放攻擊(英語:replay attack,或稱為回放攻擊)是一種惡意或欺詐的重複或延遲有效資料的網路攻擊形式。 這可以由發起者或由攔截資料並重新傳輸資料的對手來執行,這可能是通過IP資料包替換進行的欺騙攻擊的一部分。 這是“中間人攻擊”的一個較低階別版本。
這種攻擊的另一種描述是: “從不同上下文將訊息重播到安全協議的預期(或原始和預期)上下文,從而欺騙其他參與者,致使他們誤以為已經成功完成了協議執行。”
舉個簡單的例子:
我們程式設計師日夜操勞的,在按摩店裡面辦個卡,偶爾去洗個腳放鬆一下不過分吧。
有一天,我去洗腳的時候對著店員說:給我安排一個 168 價位的,要小夥子啊,按著比較帶勁兒,我的卡號是 88888888。
然後我在前臺簽上了自己的名字,店員就安排了一個精壯的小夥子給我按摩。
沒想到我們的對話被其他人聽到了,於是他也給店員說:給我安排一個 168 價位的,要小夥子啊,按著比較帶勁兒,我的卡號是 88888888。
還模仿了我的簽名,在前臺簽字。
把之前的、正常的請求再次傳送,這就是重放攻擊。
有的朋友就會說了:我的介面是加簽的,應該沒問題吧?
你加簽咋了?
我沒有動你的報文,所以你也可以正常驗籤呀。
我不僅抄你報文裡面的正常欄位,報文裡面的簽名我也抄全乎了。
所以,接收方接到報文之後能正常驗籤。
沒有任何毛病。
有的朋友還會說了:我的介面是有加密的,應該沒問題吧?
看來還是不懂重放攻擊的基本原理。
你加密咋了?
反正我擷取到了你的報文,雖然你報文加密了,我看起來是一段亂碼,但是我也不需要知道你報文的具體內容呀,直接重發就完事了。
還是前面的例子。
假設我去洗腳的時候對著店員說:天王蓋地虎。
被旁邊的人聽到了,他根本就不知道“天王蓋地虎”是啥。
但是他看到了我說了這句話之後,就被安排了一個 168 元的技術服務。
於是他也對店員說:天王蓋地虎。
也能被安排。
所以,別人根本就不需要知道你報文的具體含義。
只要我再次發給你,你進行解密操作,發現能解密。
能解密說明暗號對上了。
所以,雖然報文是加密、加簽傳輸的,對於防止請求重放,並沒有什麼卵用。
加密加簽
來,說解決方案之前,我們先明確兩個概念:加密和加簽。
字面意思不解釋了,大家都知道,說說目的。
加密的目的:為了保證傳輸資訊的隱私性,不被別人看到傳輸的具體內容,只能讓接收方看到正確的資訊。
加簽的目的:訊息接收方驗證資訊是否是合法的傳送方傳送的,確認資訊是否被其他人篡改過。
不管是加密還是加簽,都涉及到公私鑰。
記住了:公鑰加密、私鑰加簽。
簡單的說一下原理。
傳送方有這樣三樣東西:自己的私鑰、自己的公鑰、接收方的公鑰。
接收方有這樣三樣東西:自己的私鑰、自己的公鑰、傳送方的公鑰。
中間人有這樣兩樣東西:接收方的公鑰、傳送方的公鑰。
為什麼是公鑰加密呢?
來個反證法嘛。
假設訊息傳送方用自己的私鑰加密。然後訊息被中間人攔截到了,因為他有傳送方的公鑰,那麼中間人就可以用公鑰對訊息進行解密,獲取明文報文,這樣達不到加密的目的。
所以,正確的操作應該是用接收方的公鑰加密,這樣就算訊息被中間人攔截到了,他也沒有接收方的私鑰呀,解不了密,看不到明文。
為什麼是私鑰加簽呢?
同樣,反證法。
假設訊息傳送方,用接收方的公鑰加簽。如果訊息被中間人攔截到了,巧了,我也有接收方的公鑰。咔一下,直接把訊息一改,然後也拿著接收方的公鑰加簽,發過去了。
這樣的加簽是沒有意義的。
因此,要用自己的私鑰加簽,就算被攔截,中間人沒有私鑰,修改報文之後,搞不了簽名,也就沒啥卵用。
前面說了,對於重放攻擊,擷取到的內容是不是加密都無所謂。因為我根本不需要你們在說什麼,我只需要把攔截下來的請求一遍遍的重發就行了。
所以,重要的是加簽和驗籤。
如果你能修改報文,並且重新加簽,那就不叫重放攻擊了,那就叫做中間人攻擊了。
其實重放攻擊也是“中間人攻擊”的一個較低階別版本。
啥是中間人攻擊呢?
我去洗腳的時候對著店員說:給我安排一個 168 價位的,要小夥子啊,按著比較帶勁兒,我的卡號是 88888888。
對話被偷聽到了,中間人對店員說:給我安排一個 1999 價位的,要小姑涼啊,按摩手法好一點的,我的卡號是 88888888。
篡改報文,這是中間人攻擊。
本文主要聚焦於重放攻擊的解決方案。
經過前面的分析,我們知道要解決重放攻擊,就是想著怎麼在參與簽名的欄位裡面搞事情。
能想到這裡,就比較好回答這個問題了。
如果是從資料加密角度回答這個問題的同學,可以回去等通知了。
另外,說到加密了,大家都會想到 HTTPS 資料加密。
所以,當面試官問你:HTTPS資料加密是否可以防止重放攻擊?
答:否,加密可以有效防止明文資料被監聽,但是卻防止不了重放攻擊。
接下來,我們看看解決方案。
解決方案
加時間戳
首先,常見的解決方案就是在請求報文裡面加上時間戳,並參與加簽。
當接收方收到報文,經過驗籤之後。
首先第一個事兒就是拿著請求中的時間戳欄位和本地時間做個對比。
如果時間誤差在指定時間,比如 60 秒內,那麼認為這個請求是合理的,程式可以繼續處理。
為什麼要有一個時間容錯範圍,能理解吧?
因為報文的傳輸、解密、驗籤是需要時間,不能假設我這一秒發出去,下一秒服務端就收到了。
所以,得有時間容錯範圍。
但是這個容錯範圍又帶來了另外一個問題。
不能完全避免重放攻擊。
至少時間容錯範圍內,比如 60 秒,重發過來的請求,服務端認為是有效的。
那麼怎麼辦呢?
加隨機串
換個思路,我們在請求報文裡面加個隨機串,然後讓它參與加簽。
接受方收到報文,驗籤之後,把隨機串拿出來,來判斷一下這個隨機串是否已經處理過了。比如判斷一下是否存在於 Redis 裡面。
當請求再次重放過來的時候,一看:嚯,好傢伙,這個隨機串已經被用過了呀,不處理了。
在這個情況下,隨機串就得保證唯一性了,還得歷史全域性唯一。
因為你指不定哪天就收到一個幾天前的被重放過來的請求。
確實是解決了請求重放的問題,但是弊端也很明顯:歷史全域性唯一。
我還得儲存下來,而且儲存的資料量還會越來越大,是不是有點麻煩了?
確實麻煩了。
這個思想就和用全域性唯一流水號去保證介面冪等性很像了。
所以,我第一次遇到這個面試題的時候,我朝著介面冪等的角度去回答了,也不能說回答的不對。
只能說回答的不是面試官想要的標準答案。
那麼什麼是面試官想要聽到的回答呢?
時間戳+隨機串
時間戳的問題是有一定的時間容錯視窗,這個時間視窗內的重放攻擊是防不住的。
隨機串的問題是要保證歷史全域性唯一,儲存隨機串成了一個麻煩的事情。
那麼當我們把這兩個方案揉在一起的時候,神奇的事情就發生了:
我只需要保證時間視窗內的生成的隨機串不重複就行。
而且假設時間視窗為 60 秒,我們用 Redis 來記錄出現過的隨機串,那麼這個串在後臺的超時時間設定為 60 秒就行。
一般來說這個時間視窗都不會太長了,我對接過這麼多各種各樣的渠道,見過最長的也就 5 分鐘。
保證 5 分鐘內生成的兩個隨機串不重複,這個需求比保證實現一個歷史全域性唯一的流水號容易實現多了吧?
另外,最關鍵的一句話一定要說:時間戳和隨機串得參與到加簽邏輯中去。
這個很好理解吧?
接受方看報文是否被篡改,看的就是簽名是否能匹配上。
而簽名的結果是和參與簽名的欄位的值有直接關係的。
要是你時間戳和隨機串不參與加簽,那麼任意修改時間戳或者隨機串,都不會引起簽名的變化,那不白忙活一場嗎?
中間人咔一下攔截到請求,發現有時間戳和隨機串,正準備放棄的時候,想著死馬當做活馬醫,把隨機串一改,又扔給接收方了。
結果收到正確的響應了。
我要是這個中間人,我都會笑出來聲來:寫這個程式碼的程式設計師也太可愛了吧?
微信支付
其實說到時間戳加隨機串的時候,我就想起了微信支付。
剛剛入行的時候,可是被這個微信支付搞的服服帖帖的。
但是需要說明的是,雖然它的介面文件裡面也有時間戳加隨機串,但是目的不是為了防止重放攻擊的。
寫出來呢只是為了讓對於加簽這個東西不太熟悉的朋友有一個具體的認知。
來,我們看一下微信支付的介面文件:
https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=7_7&index=6
可以看到請求引數裡面確實有時間戳(timeStamp)和隨機字串(nonceStr),且人家還專門加粗了:
參與簽名的引數為:appId、timeStamp、nonceStr、package、signType,引數區分大小寫。
那麼是怎麼簽名的呢?
官方也是給了詳盡的說明的:
https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=4_3
首先就是按照字典序,對所有需要參與簽名的、非空的欄位進行排序。並使用 URL 鍵值對的格式(即key1=value1&key2=value2…)拼接成字串 stringA。
然後在 stringA 最後拼接上 key(商戶金鑰) 得到 stringSignTemp 字串,並對 stringSignTemp 進行 MD5 運算,再將得到的字串所有字元轉換為大寫,得到 sign 值 signValue。
官方給了一個實際的案例,如下:
再說一次:微信支付的介面裡面雖然有時間戳加隨機串,但是目的不是為了防止重放攻擊的。寫在這裡只是讓大家對於加簽這個過程有一個具體的認知。
別整茬了。
那麼它在介面裡面加入隨機串的目的是什麼呢?
官方自己都說了:
微信支付API介面協議中包含欄位nonce_str,主要保證簽名不可預測。我們推薦生成隨機數演算法如下:呼叫隨機數函式生成,將得到的值轉換為字串。
阿里API閘道器
看完微信支付,再看看阿里的 API 閘道器是怎麼防止重放攻擊的。
https://help.aliyun.com/knowledge_detail/50041.html
阿里的 API 閘道器,就是在 HEADER 裡面加了兩個引數:X-Ca-Timestamp、X-Ca-Nonce。
這個解決方案就是我們前面說的時間戳加隨機串。
接著看看它的簽名生成過程。
首先是客戶端生成簽名,三步:
1.從原始請求中提取關鍵資料,得到一個用來簽名的字串 2.使用加密演算法加APP Secret對關鍵資料簽名串進行加密處理,得到簽名 3.將簽名所相關的所有頭加入到原始HTTP請求中,得到最終HTTP請求
一圖勝千言:
然後是服務端驗證簽名,四步:
1.從接收到的請求中提取關鍵資料,得到一個用來簽名的字串 2.從接收到的請求中讀取APP Key,通過APP Key查詢到對應的APP Secret 3.使用加密演算法和APP Secret對關鍵資料簽名串進行加密處理,得到簽名 4.從接收到的請求中讀取客戶端簽名,對比伺服器端簽名和客戶端簽名的一致性。
而具體的簽名演算法其實和微信支付,大同小異,主要也是對於參與簽名的欄位按照字典序排序。
箇中差異就不進行對比說明了,有興趣的朋友可以自己看一下。
最後說一句
好了,看到了這裡點個贊吧,周更很累的,需要一點正反饋。
才疏學淺,難免會有紕漏,如果你發現了錯誤的地方,可以在留言區提出來,我對其加以修改。
感謝您的閱讀,我堅持原創,十分歡迎並感謝您的關注。