R·ex / Zeng


音遊狗、安全狗、攻城獅、業餘設計師、段子手、苦學日語的少年。

Redirect Attack - Shadowsocks 流密碼的不安全因素

Shadowsocks 是一款陪伴無數玩家多年的科學上網工具,但是近年來隨著牆的日益增高,一些 Shadowsocks 流量已經可以被很好的識別出來,然後就是——“同志,你梯子塌了”。

雖然“協議可以被識別”已經眾所周知,但我們依舊認為,Shadowsocks 的加密做的不錯,中間人應當破解不出明文資訊。

然而,晚上看到的一篇 文章,稍稍動搖了一下我對 Shadowsocks 資料安全的信心,更讓我重新審視了一下流密碼的安全性。之前我在做安全方面的科普的時候,我總會提到“ECB 是不安全的”,但現在發現,其它的一些流密碼也不一定安全,因為它們沒有保證資料的完整性,因此存在資料被篡改的可能性。

Shadowsocks 如果配置得當,還是比較安全的,或者至少可以讓目前的破解方法沒有用武之地。如果看完本文後依舊不放心,可以使用其它的科學上網工具。

本文的一些圖片引用自維基百科,如果你來自一些國家,可能需要科學地觀看。

一些基本概念

考慮到可能有些同學沒有密碼學的基礎,也對 Shadowsocks 的協議不是很瞭解,這裡就簡單的介紹一下。如果您已經有足夠的知識,可以 跳到下一節

流密碼

相信不少人都聽說過 AES 之類的對稱加密演算法,但並沒有瞭解的那麼細緻。例如什麼是 IV?什麼是基於資料塊的加密?

事實上,AES 演算法本體並不是為了處理無限長度的字串而設計的,它一次只能處理 16 個位元組(不管是 AES-128 還是 AES-256),我們管這 16 個位元組叫做一個“資料塊”,AES 就是一個基於資料塊的加密演算法。對於這一缺陷,可以用這樣一種思路:先將明文切割成若干個資料塊,對於每個塊用 AES 做加密,再把每個加密後的塊拼起來。

是不是很簡單?其實這就是 ECB 模式。使用這個思路的 AES-256 演算法被稱為 AES-256-ECB。由於 16 位元組對於一些資料來說實在是太小,很容易出現大量重複的塊。例如有這樣一張圖片:

原始的圖片

大家都知道這是一隻小企鵝,我們用 ECB 加密一下再看看:

使用 ECB 模式加密後

應該依舊能看出來圖中是剛才那隻小企鵝。ECB 模式無法隱藏原文的特徵,請大家儘量不要使用。

電子密碼本(ECB)的加密模式

有一些改良的方法,例如除了密碼以外,再提供一個初始向量 IV 作為第 0 個數據塊,在加密第 i 個塊之前,先將明文跟上一個塊的加密結果異或一下,即:

Cipher[0] = IV
Cipher[i] = Encrypt(Plain[i] ^ Cipher[i - 1])

密文分組連結(CBC)的加密模式

這種方式會比剛才的 ECB 安全得多。

還有其它的一些方式,所有的這些方式(包括 ECB)統稱為“流密碼”。一開始的那張小企鵝,用 ECB 以外的方式加密之後是這樣的,已經完全看不出是什麼了:

除 ECB 以外的加密模式,結果看起來像偽隨機

中間人攻擊

中間人攻擊是個比較好玩的東西,假設攻擊者利用一些方法(比如用一個釣魚熱點)讓受害者發出的資料包走到了自己這裡,那麼攻擊者就可以檢視或者修改資料包的內容。就好像小明在課上給小紅傳紙條,本來寫的是“我喜歡你”,但在經過小亮的時候,小亮用自己的紙條替換了他們的,上面寫著“滾犢子吧”,可見中間人攻擊對我們的影響有多大。

注:在課上傳紙條是不好的行為,小朋友們不要模仿。

大家在日常生活中最常經歷的中間人攻擊,可能是在訪問某些 HTTP 網站的時候,突然彈出一個“寬頻到期需要續費”的通知。這種中間人攻擊是一些無良運營商乾的,不過我們更習慣把它叫做“流量劫持”。

資料完整性

這一段就偷個懶,直接摘抄百度百科了(有刪改):

完整性是資訊保安的三個基本要點之一,指使用者、程序或者硬體元件具有能力,能夠驗證所傳送或傳送的東西的準確性,並且程序或硬體元件不會被以任何方式改變。

翻譯成白話就是:能保證資料在傳輸過程中不被篡改,小紅收到的紙條內容跟小明傳出去的一模一樣。

  • 大家聽說過的 ECC 記憶體糾錯演算法就能保證資料完整性,它可以發現記憶體資料被篡改(例如遭受了高能粒子衝擊或硬體的部分損壞),並透過一些演算法儘量恢復被損壞的部分;
  • HTTPS 中的數字簽名也是一種保證資料完整性的方法,它可以發現傳輸的資料被篡改(例如中間人攻擊),並且立即停止資料傳輸,以防止使用者或伺服器因為接收到虛假資料而遭受損失。

Shadowsocks 協議基礎

雖然 Shadowsocks 使用的底層協議是 SOCKS5,但對於本文而言,底層的 SOCKS5 並不是重點,我們只需要關注 Shadowsocks 的客戶端與伺服器之間是如何傳輸資料的。

根據官方文件所說,客戶端向伺服器傳送的資料,一開始是流密碼的 IV(也就是說,IV 由客戶端生成,並直接扔進資料包中),之後就是一段加密資料,它的明文格式是這樣的:

[目標地址][資料]

其中資料可以是任意長度;至於目標地址,Shadowsocks 用的是 SOCKS5 的表示法:

[1 位元組型別][主機名][2 位元組埠]

其中,型別是 1 位元組的列舉值:

  • 0x01:主機名是 IPv4 地址;
  • 0x03:主機名是變長字串,首位元組表示長度(最大 255),後面是資料;
  • 0x04:主機名是 IPv6 地址。

一次代理的過程如下:

  1. 客戶端將這些資料加密後發到伺服器;
  2. 伺服器收到後將其解密,會得到 [1 位元組型別][主機名][2 位元組埠][資料]
  3. 伺服器會將資料部分直接傳送給 主機名:埠
  4. 伺服器將主機返回的資料直接使用同樣的演算法加密(如果加密演算法用了流密碼,則會生成並使用一個新的 IV,並將其放在包的最前面),傳送給客戶端;
  5. 客戶端解密後即可得到主機返回的資料。

Shadowsocks + 流密碼的不安全因素

回到一開始說的那篇文章上,作者的發現是:如果攻擊者抓到了一個 Shadowsocks 伺服器返回的包,並且已知資料部分的開頭七個位元組,那麼有可能在不知道密碼的情況下,利用那個 Shadowsocks 伺服器來解出包的絕大部分內容(最多損失 16 位元組)。

作者的思路是這樣的:

假設有一臺 Shadowsocks 伺服器,攻擊者透過嗅探或其它方式抓到了這個 Shadowsocks 伺服器返回的一個包。

為了知道明文內容,攻擊者要麼暴力破解密碼(隨著大家安全意識的提升,這已經幾乎不可行了),要麼想辦法利用這臺 Shadowsocks 伺服器幫忙解密。

作者選擇了後者,即想辦法把這個包變成客戶端發的包,讓伺服器解密後代理到自己指定的伺服器,這被稱為 Redirect attack

上一節說到,Shadowsocks 客戶端發的包格式(明文狀態下)是 [1 位元組型別][主機名][2 位元組埠][資料]。如果攻擊者可以利用加密演算法的缺陷來篡改明文資料,就可以把主機名改成攻擊者的伺服器地址,Shadowsocks 伺服器就會以為客戶端想訪問攻擊者的伺服器,於是就把解密後的包中的資料部分發了過去。

先考慮如何篡改資料。假設這臺 Shadowsocks 伺服器的加密演算法使用的是 AES-256-CFB,那麼解密的方式如維基百科所述是這樣的:

密文反饋(CFB)的解密模式

其中 IV、每一塊 Ciphertext 和 Plaintext 長度都是 16 位元組。

作者發現,key 是不變的,IV 也可以重用伺服器返回包中的那個,那麼如果只修改第一塊 Ciphertext,那麼只有前兩塊 Plaintext 會改變,更重要的是,由於第一塊 Plaintext 就是第一塊 Ciphertext 跟某個串 A 的異或值,那麼攻擊者完全可以透過修改第一塊 Ciphertext 的值來控制第一塊 Plaintext!具體方法如下:

假設當前的第一塊 Ciphertext 是 c1,第一塊 Plaintext 是 p1IV 做了一系列 whatever 的運算得到的結果是 a,那麼:

  • 已知 a ^ c1 == p1
  • 那麼 a ^ c1 ^ X == p1 ^ X
  • 根據異或的結合律
  • 可得 a ^ (c1 ^ X) == (p1 ^ X)
  • 也就是說,攻擊者對 c1 做的異或操作,會完完全全反映在 p1

攻擊方法也不復雜:

  • 假設最終讓 p1 變成了 q1,我們可以認為 p1 ^ X == q1
  • 將兩邊同時異或 p1,可得 p1 ^ X ^ p1 == q1 ^ p1
  • 由於異或的性質,左邊的兩個 p1 抵消
  • 可得 X == q1 ^ p1

攻擊者需要做的就是將 c1 ^= (q1 ^ p1)。但這裡有一個問題,我們並不能知道具體的 p1 是什麼!不過還好,它是明文資料的一部分,在上網的過程中,總有些協議的頭幾個位元組是固定的,例如 HTTP 協議。

在 21 世紀的第三個十年,大家應該早就切換成 HTTP 1.1 了,因此返回的資料包一開始一定是 8 個位元組 HTTP/1.1。攻擊者能否將 TA 的主機地址壓縮到這麼小呢?畢竟除掉 1 位元組型別和 2 位元組埠以外,可用空間只有 5 位元組了。

對於絕大部分攻擊者來說,不可能拿到不超過 5 位元組的域名,因此只能考慮 IPv4 了,而且如果用 IPv4 的話,甚至只需要總共 7 個位元組!舉個例子:

01 c0 a8 01 03 12 12
-- ----------- -----

劃線的三部分分別代表了:使用 IPv4 協議、地址是 192.168.1.3,埠是 4626

那麼我們完全可以令:

p1 = 'HTTP/1.'
q1 = '\x01\xc0\xa8\x01\x03\x12\x12'
new_c_part = c1[0:7] ^ p1 ^ q1

用這個 7 位元組的 new_c_part 替換掉之前 c1 的前 7 個位元組,然後直接將替換後的整個包傳送到剛才的 Shadowsocks 伺服器。

Shadowsocks 伺服器嘗試解密,解密後發現明文是這樣的:

01 c0 a8 01 03 12 12 XX XX XX XX XX XX XX XX XX

伺服器會認為這是一個合法的客戶端請求,因此將後面的一串 XX(明文資料)按照前 7 個位元組的要求,轉發到了 192.168.1.3:4626

攻擊者早就在這兒坐等了,方法非常簡單,只需要用 nc 啟動一個埠監聽即可:

$ nc -l -p 4626

由於攻擊者修改了 c1,而 c1 在 CFB 模式中又用來解密 p2,因此收到的 p2 這 16 個位元組應該是亂碼。攻擊者最終可以還原出 p2 以外的所有資料。論文中的命令列截圖也說明了這點,獲取到的資料的第一個位元組是之前包的明文的第 8 個位元組(前 7 個是 HTTP/1.),然後有 9 個位元組是正確的,之後 16 個位元組是亂碼,再之後是完全正確的:

1 304 Not???????????????? Sat, 26 Jan 2019 07:15:21 GMT
Connection: close
Via: 1.1 varnish
Cache-Control: max-age=600
ETag: W/"5c45d22a-127"
Expires: Sat, 26 Jan 2019 06:59:41 GMT
Age: 0
....

作者給出的防禦措施是:

  • 停用 shadowsocks-pyshadowsocks-gogo-shadowsocks2shadowsocks-nodejs
  • 只用 shadowsocks-libev,並且只使用 AEAD 加密

原因如下:shadowsocks-libev 的實現很久之前就已經禁止了 IV 重用,可以在一定程度上防止這種攻擊;只要加密演算法帶有 AEAD 特性,那麼資料就無法被篡改,本文的攻擊方式也是無效的。

對大眾的影響

雖然文章中只列舉了 HTTP 協議和 CFB 模式的例子,但理論上來說,所有頭部 7 個位元組已知的協議和所有類似流密碼的組合,都可以被這種方法攻擊。你不能保證你科學上的網總是 HTTPS,即使是 HTTPS,如果是國內網站,當某些不可抗力獲取了其證書之後,你的 TLS 流量總是會被解密的。

不過有一些值得欣慰的地方:

由於牆的逐漸升高,大家已經逐漸意識到“只有加密是不行的”了,因此紛紛改用帶有混淆功能的科學上網工具。由於攻擊者無法得知混淆的引數(甚至不知道哪個流量是梯子的流量),因此這個方法不再起作用了。

目前大部分科學上網工具已經停用了舊的加密演算法,甚至強制只讓使用帶有 GCM 或者 Poly1305 的加密演算法,這些演算法有嚴格的 AEAD 特性,可以極大保證資料安全。TLS 1.3 強制使用 AEAD 也在某些程度上為它的安全性做了擔保。

如果你還在用 Shadowsocks 或其衍生工具,並且依舊使用普通的流密碼來加密,那麼請立即聽從作者給出的防禦措施,為了你的伺服器,也為了你自己。

參考資料

Update 2020-02-15

添加了對 IV 重用相關的補充。

Disqus 載入中……如未能載入,請將 disqus.com 和 disquscdn.com 加入白名單。

這是我們共同度過的

第 3849 天