点击蓝字 / 关注我们
前置知识
MAC
在密码学中,Message Authentication Code(消息认证码,有时也被称为tag)是用来认证消息的比较短的信息。换言之,MAC用来保证消息的数据完整性和消息的数据源认证。
MAC由消息本身和一个密钥经过一系列计算产生,用于生成MAC的算法,称为MAC算法。MAC算法应能满足如下几个条件:
-
在仅有消息本身没有密钥的情况下,无法得到该消息的MAC -
同一个消息在使用不同密钥的情况下,生成的MAC应当无关联 -
在已有一系列消息以及其MAC时,给定一个新的消息,无法得到该消息的MAC。
下图摘自维基百科,可以很好的描述MAC的使用原理:
HMAC
HMAC (有时扩展为 英语:keyed-hash message authentication code, 密钥散列消息认证码, 或 英语:hash-based message authentication code,散列消息认证码),是一种通过特别计算方式之后产生的消息认证码(MAC),使用密码散列函数,同时结合一个加密密钥。它可以用来保证资料的完整性,同时可以用来作某个消息的身份验证。
HMAC是MAC算法中的一种,其基于加密HASH算法实现。任何加密HASH, 比如MD5、SHA256等,都可以用来实现HMAC算法,其相应的算法称为HMAC-MD5、HMAC-SHA256等。
function hmac (key, message)
if (length(key) > blocksize) then
key = hash(key) // keys longer than blocksize are shortened
end if
if (length(key) < blocksize) then
key = key + [0x00 * (blocksize - length(key))] // keys shorter than blocksize are zero-padded (where ∥ is concatenation)
end if
o_key_pad = [0x5c * blocksize] ⊕ key // Where blocksize is that of the underlying hash function
i_key_pad = [0x36 * blocksize] ⊕ key // Where ⊕ is exclusive or (XOR)
return hash(o_key_pad + hash(i_key_pad + message))
end function
AEAD
认证加密(英语:Authenticated encryption,AE)和带有关联数据的认证加密(authenticated encryption with associated data,AEAD,AE的变种)是一种能够同时保证数据的保密性、 完整性和真实性的一种加密模式。这些属性都是在一个易于使用的编程接口下提供的。
具体的实现方法
-
Encrypt-then-MAC (EtM)
EtM方法 首先对明文进行加密,然后根据得到的密文生成消息认证码(MAC)。密文和它的MAC一起发送。例如IPsec。EtM是ISO/IEC 19772:2009规定的六种认证加密方法中的一种。[5]这是唯一可以达到认证加密安全性最高定义的方法,但这只有在使用的MAC“强不可伪造”时才能实现。[8]2014年11月,EtM的传输层安全性协议(TLS)和资料包传输层安全(DTLS)扩展已经作为RFC 7366发布。各种EtM密码包也存在于SSHv2中(例如[email protected])。
-
Encrypt-and-MAC (E&M)
基于明文生成MAC,并且明文在没有MAC的情况下被加密。明文的MAC和密文一起发送。用于例如SSH。E&M方法本身并未被证明是“强不可伪造”的。
-
MAC-then-Encrypt (MtE)
基于明文生成MAC,然后将明文和MAC一起加密以基于两者生成密文。密文(包含加密的MAC)被发送。MtE方法本身并未被证明是“强不可伪造”的。用于例如SSL/TLS。[8]尽管有理论上的安全性,但对SSL/TLS进行更深入的分析将保护模型化为MAC-then-pad-then-encrypt,即明文先填充到加密函数的块大小。填充错误通常会导致接收方发现可检测到的错误,从而导致密文填塞攻击(Padding oracle attack),如Lucky Thirteen attack。
Vmess协议的Client(基于v2ray-core)
ClientSession.isAEAD
这里首先有一个是否采用AEAD的属性
在outbound中可以看到
isAEAD默认是false,只有在没有明确关闭AEAD模式,同时在client配置文件中没有指定了AlterID属性时才会启用
ClientSession.idHash
这个属性protocol.idHash其实是一个hmac函数,通过这个函数可以计算mac值。前面前置知识写了mac算法有很多种,hmac便是一种。这个属性一般都是传默认值,protocol.DefaultIDHash
ClientSession.requestBodyKey和ClientSession.requestBodyIV和ClientSession.responseHeader
这个比较关键,是request加密的关键参数。
可以看到,首先生成了33位随机的byte,然后把前16位作为BodyKey,后16位作为BodyIV。同时最后一位留作校验responseHeader。
如果client没有启用AEAD模式,则返回的BodyKey应该为requestBodyKey的md5,返回的BodyIV也为requestBodyIV的md5。
如果client启用了AEAD模式,则返回的BodyKey为requestBodyKey进行sha256后得到的前16位,返回的BodyIV同理。
protocol.MemoryUser结构体
protocol.Account结构体
Account结构体是MemoryUser的核心属性
Account结构体中对应config.json中的设置
我们也可以通过
这种方式动态生成protocol.Account
header包中的RequestHeader
ClientSession.EncodeRequestHeader
客户端请求认证信息部分
查看outbound中客户端发请求的部分,
其中的reuqest头部分的Version为硬编码的1,User。
再回到client的Encode函数中,可以看到从拿到的header中拿出了用户信息,同时判断client是否被设置成AEAD模式,如果没设置AEAD模式那就好说,拿到一个以用户的ValidID作为key的HMAC生成器。此处的ValidID是怎么来的呢,我们跟进看
当MemoryAccount中的中的AlterID个数为0时返回MemoryAccound中的ID,其实也就是config.json中指定的id(如果配置json中没指定,那就会随机从protocol包中的server_spec进行随机抽取)。
也就是说根据配置文件中的id作为hmac算法的key拿到一个hmac生成器,同时把当前的时间戳放进去,算出来一个MAC,将其写入请求流
也就是官方文档中的客户端请求中的16字节认证信息。
客户端请求指令部分
指令部分此处不做详细分析。
ClientSession.EncodeRequestBody
客户端请求数据部分
查看outbound中客户端发请求的剩下部分
首先调用Client.EncodeRequestBody拿到一个关联了请求输出writer的加密bodyWrite,任何写进bodyWriter的数据都会直接被自动加密,接着拿着这个封装好的bodyWriter直接进行一个buf.Copy,把客户需要代理的数据直接给原封不动送进去。
接着看比较核心的ClientSession.EncodeRequestBody核心逻辑,直接先参考官方文档
可以看到整个的实现完全符合文档规范
至此,我们完成了客户端请求的发送部分。
Vmess协议的Server(基于v2ray-core)
ServerSession.userValidator
这个从名字就能知道肯定是验证用户的,它的类型是TimedUserValidator,从类型名也能知道这个是和时间有关系。
这个对应user数组也就是
然后再看一下这个TimedUserValidator的构造函数
注意这里的baseTime,取得是本地时间的时间戳,hasher基本都是和client一样的defaultIdHash,同时设置定时更新IDhash和客户端保持一致
间隔为10s一次。
ServerSession.DecodeReuqestHeader
服务端请求认证部分
看Inbound.go中的处理函数
主要就是根据Handler中的clients也就是TimedUserValidator和sessionHistory来创建ServerSession
然后使用创建好的ServerSession调用DecodeRequestHeader
这里先会读取16字节(protocol.IDBytesLen),然后尝试解密
这里服务端遍历TimedUserValidator中所有user的hash,看看有没有能和HMAC匹配的hash,匹配到了则返回这个MemoryUser。
同时既然在GetAEAD这个操作中取到了对应User,那自然说明client是AEAD形式认证模式。返回true。
服务端请求指令部分
和客户端那边一样,对应是否存在AEAD的模式采取不同处理。
ServerSession.DecodeRequestBody
服务端请求数据部分
继续看inbound中处理的RequestBody,也就是请求的数据部分
这也是和client一样直接返回解密流writer,我们直接读取就行。
ServerSession.EncodeResponseHeader
服务端响应头部分
ServerSession.EncodeResponseBody
返回包裹好的加密流writer
强网杯2022-谍影重重解密
题目给了一个pcap包,使用wireshark打开并追踪流发现
捕获数据是一个socket连接传输的一个完整数据。
伪造时间戳
根据前面介绍和官方文档,IDHash的生成依赖于当前系统对应UTC时间的时间戳
因此我们在wireshark中观察frame时间
更改NewTimedUserValidator中的baseTime
注意 尽管我们更改了baseTime,但是每隔10s仍然会更近时间为本地时间,但是因为程序处理时间较短用不了一个updateInterval的时间,因此此处为了做题不需要考虑task.Periodic的更新
伪造vmess ServerSession
因为根据题目给的流量,客户端先发起请求,所以我们先要解密客户请求的内容,进而先构造ServserSession
结合题目给的配置文件
我们可以构造一个这样的ServerSession
伪造vmess ClientSession
这里我改了一下NewClientSession直接把ServerSession传入
目的是获取server端解密后的Request和Response加密参数,
对应的我也再ServerSession中添加了对应方法。
注意,这里我经过测试得出了请求流量的AEAD模式并未开启值为false,所以响应的BodyKey和BodyIV是md5,而非sha256
读入数据包
观察wireshark流量发现存在两段客户端发给服务端的流量
我们手动导入
然后遍历pcap包提取服务端发给客户端的数据
解密
解密得出一个html文件,html文件打开后会执行js代码下载含有恶意宏的word文件。把word上传至VT分析即可拿到c2 api解密压缩包。
解压后得到一个gob文件使用degob进行反序列化。
cat flag|base64|go run main.go -b64|tee dump.txt
包含了日期 再使用golang伪随机数unshuffle,并对图片处理得到最终flag。
完整流量解密脚本构造
直接用v2ray现成的库(https://github.com/v2ray/v2ray-core)
Route.pcapng
package encoding_test
import (
“bytes”
“context”
“fmt”
“github.com/google/gopacket”
“github.com/google/gopacket/layers”
“github.com/google/gopacket/pcap”
“io”
“os”
“testing”
“v2ray.com/core/common”
“v2ray.com/core/common/buf”
“v2ray.com/core/common/protocol”
“v2ray.com/core/proxy/vmess”
. “v2ray.com/core/proxy/vmess/encoding”
)
func toAccount(a *vmess.Account) protocol.Account {
account, err := a.AsAccount()
common.Must(err)
return account
}
func TestEncryption(t *testing.T) {
user := &protocol.MemoryUser{}
account := &vmess.Account{
Id: “b831381d-6324-4d53-ad4f-8cda48b30811”}
user.Account = toAccount(account)
sessionHistory := NewSessionHistory()
defer common.Close(sessionHistory)
userValidator := vmess.NewTimedUserValidator(protocol.DefaultIDHash)
userValidator.Add(user)
defer common.Close(userValidator)
server := NewServerSession(userValidator, sessionHistory)
var data []byte
data = []byte{
0x4d, 0xd1, 0x1f, 0x9b, 0x04, 0xf2, 0xb5, 0x62,
0xb9, 0xdb, 0x53, 0x9d, 0x93, 0x9f, 0x1d, 0x52,
0xb4, 0x8b, 0x35, 0xbf, 0x59, 0x2c, 0x09, 0xb2,
0x15, 0x45, 0x39, 0x2f, 0x73, 0xf6, 0xce, 0xf9,
0x11, 0x43, 0x78, 0x64, 0x64, 0x57, 0x8c, 0x1c,
0x36, 0x1a, 0xa7, 0x2f, 0x63, 0x8c, 0xd0, 0x13,
0x5f, 0x25, 0x34, 0x35, 0x55, 0xf5, 0x09, 0xae,
0xf6, 0xc7, 0x4c, 0xd2, 0xa2, 0xb8, 0x6e, 0xe0,
0xa9, 0xeb, 0x3b, 0x93, 0xa8, 0x1a, 0x54, 0x1d,
0xef, 0x47, 0x63, 0xcc, 0x54, 0xf9, 0x1b, 0xa0,
0x26, 0x81, 0xad, 0xd1, 0xb8, 0x15, 0xe8, 0xc5,
0x0e, 0x02, 0x8c, 0x76, 0xbd, 0xe0, 0xee, 0x8a,
0x95, 0x93, 0xdb, 0x88, 0xd9, 0x01, 0x06, 0x63,
0x05, 0xa5, 0x1a, 0x95, 0x86, 0xa9, 0xe3, 0x77,
0xee, 0x10, 0x0e, 0x7d, 0x4d, 0x33, 0xfc, 0xfc,
0x04, 0x53, 0xc8, 0x6b, 0x19, 0x98, 0xa9, 0x52,
0x75, 0xcd, 0x93, 0x68, 0xa6, 0x88, 0x20, 0xc2,
0xa6, 0xa5, 0x40, 0xb6, 0x38, 0x6c, 0x14, 0x6e,
0xa7, 0x57, 0x9c, 0xfe, 0x87, 0xb2, 0xe4, 0x59,
0x85, 0x67, 0x72, 0xef, 0xdc, 0xf0, 0xe4, 0xc6,
0xab, 0x0f, 0x11, 0xd0, 0x18, 0xa1, 0x55, 0x61,
0xcf, 0x40, 0x9c, 0xbc, 0x00, 0x49, 0x1d, 0x7f,
0x4d, 0x22, 0xb7, 0xc4, 0x86, 0xa7, 0x6a, 0x5f,
0x2f, 0x25, 0xfb, 0xef, 0x50, 0x35, 0x51, 0xa0,
0xae, 0xb9, 0x0a, 0xd9, 0xdd, 0x24, 0x6a, 0x9c,
0xc5, 0xe0, 0xd0, 0xc0, 0xb7, 0x51, 0xeb, 0x7b,
0x54, 0xb0, 0xab, 0xbf, 0xef, 0x19, 0x8b, 0x1c,
0x4e, 0x5e, 0x75, 0x50, 0x77, 0x46, 0x9c, 0x31,
0x8f, 0x20, 0xf3, 0xe4, 0x18, 0xaf, 0x03, 0x54,
0x08, 0x11, 0xab, 0x5c, 0x1e, 0xa7, 0x80, 0xc8,
0x86, 0xea, 0x2c, 0x90, 0x3b, 0x45, 0x8a, 0x26} //client header
var data1 []byte
data1 = []byte{ /* Packet 92 */
0x04, 0xf0, 0x65, 0xe0, 0xbb, 0xc3, 0x52, 0x6b,
0xcf, 0x41, 0x14, 0xad, 0x95, 0xf8, 0x36, 0x71,
0x9b, 0x5c, 0xf3, 0x42, 0x11, 0x12, 0xc8, 0x03,
0x36, 0xc6, 0xc3, 0x52, 0xad, 0x5d, 0x26, 0xa5,
0xda, 0xa9, 0xa2, 0xf5, 0x86, 0x90, 0x26, 0xe6,
0xa1, 0x20, 0x7c, 0xd2, 0x10, 0x85, 0x1c, 0x29,
0x34, 0x2f, 0xd5, 0x27, 0xd9, 0x4f, 0x85, 0x70,
0x35, 0x8e, 0xc1, 0x5a, 0x79, 0xc9, 0x5d, 0xb3,
0x1a, 0x68, 0x8b, 0xc6, 0x09, 0xa6, 0x46, 0xab,
0x74, 0xa9, 0xd9, 0xcd, 0xce, 0xa1, 0xe8, 0x28} //client body
var data2 []byte
if handle, err := pcap.OpenOffline(“./Route.pcapng”); err != nil {
panic(err)
} else {
packetSource := gopacket.NewPacketSource(handle, handle.LinkType())
for packet := range packetSource.Packets() {
if packet != nil {
if tcpLayer := packet.Layer(layers.LayerTypeTCP); tcpLayer != nil && len(tcpLayer.LayerPayload()) > 0 {
if bytes.Compare(tcpLayer.LayerPayload(), data) != 0 && bytes.Compare(tcpLayer.LayerPayload(), data1) != 0 {
data2 = append(data2, tcpLayer.LayerPayload()…)
} else {
fmt.Println(“Found client package”)
}
}
}
}
}
OutboundResponseConn := data2 //from server
InboudRequestConn := append((data)[:], data1…) //from client
reader := &buf.BufferedReader{Reader: buf.NewReader(bytes.NewReader(InboudRequestConn))}
reader1 := &buf.BufferedReader{Reader: buf.NewReader(bytes.NewReader(OutboundResponseConn))}
request, err := server.DecodeRequestHeader(reader)
if err != nil {
panic(err)
}
fmt.Println(“Received request for “, request.Destination())
bodyReader := server.DecodeRequestBody(request, reader)
fmt.Println(bodyReader.ReadMultiBuffer())
client := NewClientSession(false, protocol.DefaultIDHash, context.TODO(), server)
_, err = client.DecodeResponseHeader(reader1)
bodyReader1 := client.DecodeResponseBody(request, reader1)
stringData, err := bodyReader1.ReadMultiBuffer()
f, err := os.Create(“./evil.html”)
for err == nil {
f.Write([]byte(stringData.String()))
stringData, err = bodyReader1.ReadMultiBuffer()
}
if err != io.EOF {
t.Error(“nil error”)
}
}
然后改动一下server和client
-
client.go
package encoding
import (
“bytes”
“context”
“crypto/aes”
“crypto/cipher”
“crypto/md5”
“crypto/rand”
“encoding/binary”
“hash”
“hash/fnv”
“io”
“golang.org/x/crypto/chacha20poly1305”
“v2ray.com/core/common”
“v2ray.com/core/common/bitmask”
“v2ray.com/core/common/buf”
“v2ray.com/core/common/crypto”
“v2ray.com/core/common/dice”
“v2ray.com/core/common/protocol”
“v2ray.com/core/common/serial”
“v2ray.com/core/proxy/vmess”
vmessaead “v2ray.com/core/proxy/vmess/aead”
)
func hashTimestamp(h hash.Hash, t protocol.Timestamp) []byte {
common.Must2(serial.WriteUint64(h, uint64(t)))
common.Must2(serial.WriteUint64(h, uint64(t)))
common.Must2(serial.WriteUint64(h, uint64(t)))
common.Must2(serial.WriteUint64(h, uint64(t)))
return h.Sum(nil)
}
// ClientSession stores connection session info for VMess client.
type ClientSession struct {
isAEAD bool
idHash protocol.IDHash
requestBodyKey [16]byte
requestBodyIV [16]byte
responseBodyKey [16]byte
responseBodyIV [16]byte
responseReader io.Reader
responseHeader byte
}
// NewClientSession creates a new ClientSession.
func NewClientSession(isAEAD bool, idHash protocol.IDHash, ctx context.Context, sessionServer *ServerSession) *ClientSession {
session := &ClientSession{
isAEAD: sessionServer.isAEADRequest,
idHash: idHash,
}
//randomBytes := make([]byte, 33) // 16 + 16 + 1
//common.Must2(rand.Read(randomBytes))
//copy(session.requestBodyKey[:], randomBytes[:16])
//session.requestBodyIV = []uint8{}
//copy(session.requestBodyIV[:], randomBytes[16:32])
//session.responseHeader = randomBytes[32]
session.requestBodyIV = sessionServer.getRequestBodyIV()
session.responseBodyIV = sessionServer.getResponseBodyIV()
session.requestBodyKey = sessionServer.getRequestBodyKey()
session.responseBodyKey = sessionServer.getResponseBodyKey()
//if !session.isAEAD {
// session.responseBodyKey = md5.Sum(session.requestBodyKey[:])
// session.responseBodyIV = md5.Sum(session.requestBodyIV[:])
//} else {
// BodyKey := sha256.Sum256(session.requestBodyKey[:])
// copy(session.responseBodyKey[:], BodyKey[:16])
// BodyIV := sha256.Sum256(session.requestBodyIV[:])
// copy(session.responseBodyIV[:], BodyIV[:16])
//}
return session
}
func (c *ClientSession) EncodeRequestHeader(header *protocol.RequestHeader, writer io.Writer) error {
timestamp := protocol.NewTimestampGenerator(protocol.NowTime(), 30)()
account := header.User.Account.(*vmess.MemoryAccount)
if !c.isAEAD {
idHash := c.idHash(account.AnyValidID().Bytes())
common.Must2(serial.WriteUint64(idHash, uint64(timestamp)))
common.Must2(writer.Write(idHash.Sum(nil)))
}
buffer := buf.New()
defer buffer.Release()
common.Must(buffer.WriteByte(Version))
common.Must2(buffer.Write(c.requestBodyIV[:]))
common.Must2(buffer.Write(c.requestBodyKey[:]))
common.Must(buffer.WriteByte(c.responseHeader))
common.Must(buffer.WriteByte(byte(header.Option)))
padingLen := dice.Roll(16)
security := byte(padingLen<<4) | byte(header.Security)
common.Must2(buffer.Write([]byte{security, byte(0), byte(header.Command)}))
if header.Command != protocol.RequestCommandMux {
if err := addrParser.WriteAddressPort(buffer, header.Address, header.Port); err != nil {
return newError(“failed to writer address and port”).Base(err)
}
}
if padingLen > 0 {
common.Must2(buffer.ReadFullFrom(rand.Reader, int32(padingLen)))
}
{
fnv1a := fnv.New32a()
common.Must2(fnv1a.Write(buffer.Bytes()))
hashBytes := buffer.Extend(int32(fnv1a.Size()))
fnv1a.Sum(hashBytes[:0])
}
if !c.isAEAD {
iv := hashTimestamp(md5.New(), timestamp)
aesStream := crypto.NewAesEncryptionStream(account.ID.CmdKey(), iv[:])
aesStream.XORKeyStream(buffer.Bytes(), buffer.Bytes())
common.Must2(writer.Write(buffer.Bytes()))
} else {
var fixedLengthCmdKey [16]byte
copy(fixedLengthCmdKey[:], account.ID.CmdKey())
vmessout := vmessaead.SealVMessAEADHeader(fixedLengthCmdKey, buffer.Bytes())
common.Must2(io.Copy(writer, bytes.NewReader(vmessout)))
}
return nil
}
func (c *ClientSession) EncodeRequestBody(request *protocol.RequestHeader, writer io.Writer) buf.Writer {
var sizeParser crypto.ChunkSizeEncoder = crypto.PlainChunkSizeParser{}
if request.Option.Has(protocol.RequestOptionChunkMasking) {
sizeParser = NewShakeSizeParser(c.requestBodyIV[:])
}
var padding crypto.PaddingLengthGenerator
if request.Option.Has(protocol.RequestOptionGlobalPadding) {
padding = sizeParser.(crypto.PaddingLengthGenerator)
}
switch request.Security {
case protocol.SecurityType_NONE:
if request.Option.Has(protocol.RequestOptionChunkStream) {
if request.Command.TransferType() == protocol.TransferTypeStream {
return crypto.NewChunkStreamWriter(sizeParser, writer)
}
auth := &crypto.AEADAuthenticator{
AEAD: new(NoOpAuthenticator),
NonceGenerator: crypto.GenerateEmptyBytes(),
AdditionalDataGenerator: crypto.GenerateEmptyBytes(),
}
return crypto.NewAuthenticationWriter(auth, sizeParser, writer, protocol.TransferTypePacket, padding)
}
return buf.NewWriter(writer)
case protocol.SecurityType_LEGACY:
aesStream := crypto.NewAesEncryptionStream(c.requestBodyKey[:], c.requestBodyIV[:])
cryptionWriter := crypto.NewCryptionWriter(aesStream, writer)
if request.Option.Has(protocol.RequestOptionChunkStream) {
auth := &crypto.AEADAuthenticator{
AEAD: new(FnvAuthenticator),
NonceGenerator: crypto.GenerateEmptyBytes(),
AdditionalDataGenerator: crypto.GenerateEmptyBytes(),
}
return crypto.NewAuthenticationWriter(auth, sizeParser, cryptionWriter, request.Command.TransferType(), padding)
}
return &buf.SequentialWriter{Writer: cryptionWriter}
case protocol.SecurityType_AES128_GCM:
aead := crypto.NewAesGcm(c.requestBodyKey[:])
auth := &crypto.AEADAuthenticator{
AEAD: aead,
NonceGenerator: GenerateChunkNonce(c.requestBodyIV[:], uint32(aead.NonceSize())),
AdditionalDataGenerator: crypto.GenerateEmptyBytes(),
}
return crypto.NewAuthenticationWriter(auth, sizeParser, writer, request.Command.TransferType(), padding)
case protocol.SecurityType_CHACHA20_POLY1305:
aead, err := chacha20poly1305.New(GenerateChacha20Poly1305Key(c.requestBodyKey[:]))
common.Must(err)
auth := &crypto.AEADAuthenticator{
AEAD: aead,
NonceGenerator: GenerateChunkNonce(c.requestBodyIV[:], uint32(aead.NonceSize())),
AdditionalDataGenerator: crypto.GenerateEmptyBytes(),
}
return crypto.NewAuthenticationWriter(auth, sizeParser, writer, request.Command.TransferType(), padding)
default:
panic(“Unknown security type.”)
}
}
func (c *ClientSession) DecodeResponseHeader(reader io.Reader) (*protocol.ResponseHeader, error) {
if !c.isAEAD {
aesStream := crypto.NewAesDecryptionStream(c.responseBodyKey[:], c.responseBodyIV[:])
c.responseReader = crypto.NewCryptionReader(aesStream, reader)
} else {
aeadResponseHeaderLengthEncryptionKey := vmessaead.KDF16(c.responseBodyKey[:], vmessaead.KDFSaltConst_AEADRespHeaderLenKey)
aeadResponseHeaderLengthEncryptionIV := vmessaead.KDF(c.responseBodyIV[:], vmessaead.KDFSaltConst_AEADRespHeaderLenIV)[:12]
aeadResponseHeaderLengthEncryptionKeyAESBlock := common.Must2(aes.NewCipher(aeadResponseHeaderLengthEncryptionKey)).(cipher.Block)
aeadResponseHeaderLengthEncryptionAEAD := common.Must2(cipher.NewGCM(aeadResponseHeaderLengthEncryptionKeyAESBlock)).(cipher.AEAD)
var aeadEncryptedResponseHeaderLength [18]byte
var decryptedResponseHeaderLength int
var decryptedResponseHeaderLengthBinaryDeserializeBuffer uint16
if _, err := io.ReadFull(reader, aeadEncryptedResponseHeaderLength[:]); err != nil {
return nil, newError(“Unable to Read Header Len”).Base(err)
}
if decryptedResponseHeaderLengthBinaryBuffer, err := aeadResponseHeaderLengthEncryptionAEAD.Open(nil, aeadResponseHeaderLengthEncryptionIV, aeadEncryptedResponseHeaderLength[:], nil); err != nil {
return nil, newError(“Failed To Decrypt Length”).Base(err)
} else {
common.Must(binary.Read(bytes.NewReader(decryptedResponseHeaderLengthBinaryBuffer), binary.BigEndian, &decryptedResponseHeaderLengthBinaryDeserializeBuffer))
decryptedResponseHeaderLength = int(decryptedResponseHeaderLengthBinaryDeserializeBuffer)
}
aeadResponseHeaderPayloadEncryptionKey := vmessaead.KDF16(c.responseBodyKey[:], vmessaead.KDFSaltConst_AEADRespHeaderPayloadKey)
aeadResponseHeaderPayloadEncryptionIV := vmessaead.KDF(c.responseBodyIV[:], vmessaead.KDFSaltConst_AEADRespHeaderPayloadIV)[:12]
aeadResponseHeaderPayloadEncryptionKeyAESBlock := common.Must2(aes.NewCipher(aeadResponseHeaderPayloadEncryptionKey)).(cipher.Block)
aeadResponseHeaderPayloadEncryptionAEAD := common.Must2(cipher.NewGCM(aeadResponseHeaderPayloadEncryptionKeyAESBlock)).(cipher.AEAD)
encryptedResponseHeaderBuffer := make([]byte, decryptedResponseHeaderLength+16)
if _, err := io.ReadFull(reader, encryptedResponseHeaderBuffer); err != nil {
return nil, newError(“Unable to Read Header Data”).Base(err)
}
if decryptedResponseHeaderBuffer, err := aeadResponseHeaderPayloadEncryptionAEAD.Open(nil, aeadResponseHeaderPayloadEncryptionIV, encryptedResponseHeaderBuffer, nil); err != nil {
return nil, newError(“Failed To Decrypt Payload”).Base(err)
} else {
c.responseReader = bytes.NewReader(decryptedResponseHeaderBuffer)
}
}
buffer := buf.StackNew()
defer buffer.Release()
if _, err := buffer.ReadFullFrom(c.responseReader, 4); err != nil {
return nil, newError(“failed to read response header”).Base(err).AtWarning()
}
if buffer.Byte(0) != c.responseHeader {
return nil, newError(“unexpected response header. Expecting “, int(c.responseHeader), ” but actually “, int(buffer.Byte(0)))
}
header := &protocol.ResponseHeader{
Option: bitmask.Byte(buffer.Byte(1)),
}
if buffer.Byte(2) != 0 {
cmdID := buffer.Byte(2)
dataLen := int32(buffer.Byte(3))
buffer.Clear()
if _, err := buffer.ReadFullFrom(c.responseReader, dataLen); err != nil {
return nil, newError(“failed to read response command”).Base(err)
}
command, err := UnmarshalCommand(cmdID, buffer.Bytes())
if err == nil {
header.Command = command
}
}
if c.isAEAD {
aesStream := crypto.NewAesDecryptionStream(c.responseBodyKey[:], c.responseBodyIV[:])
c.responseReader = crypto.NewCryptionReader(aesStream, reader)
}
return header, nil
}
func (c *ClientSession) DecodeResponseBody(request *protocol.RequestHeader, reader io.Reader) buf.Reader {
var sizeParser crypto.ChunkSizeDecoder = crypto.PlainChunkSizeParser{}
if request.Option.Has(protocol.RequestOptionChunkMasking) {
sizeParser = NewShakeSizeParser(c.responseBodyIV[:])
}
var padding crypto.PaddingLengthGenerator
if request.Option.Has(protocol.RequestOptionGlobalPadding) {
padding = sizeParser.(crypto.PaddingLengthGenerator)
}
switch request.Security {
case protocol.SecurityType_NONE:
if request.Option.Has(protocol.RequestOptionChunkStream) {
if request.Command.TransferType() == protocol.TransferTypeStream {
return crypto.NewChunkStreamReader(sizeParser, reader)
}
auth := &crypto.AEADAuthenticator{
AEAD: new(NoOpAuthenticator),
NonceGenerator: crypto.GenerateEmptyBytes(),
AdditionalDataGenerator: crypto.GenerateEmptyBytes(),
}
return crypto.NewAuthenticationReader(auth, sizeParser, reader, protocol.TransferTypePacket, padding)
}
return buf.NewReader(reader)
case protocol.SecurityType_LEGACY:
if request.Option.Has(protocol.RequestOptionChunkStream) {
auth := &crypto.AEADAuthenticator{
AEAD: new(FnvAuthenticator),
NonceGenerator: crypto.GenerateEmptyBytes(),
AdditionalDataGenerator: crypto.GenerateEmptyBytes(),
}
return crypto.NewAuthenticationReader(auth, sizeParser, c.responseReader, request.Command.TransferType(), padding)
}
return buf.NewReader(c.responseReader)
case protocol.SecurityType_AES128_GCM:
aead := crypto.NewAesGcm(c.responseBodyKey[:])
auth := &crypto.AEADAuthenticator{
AEAD: aead,
NonceGenerator: GenerateChunkNonce(c.responseBodyIV[:], uint32(aead.NonceSize())),
AdditionalDataGenerator: crypto.GenerateEmptyBytes(),
}
return crypto.NewAuthenticationReader(auth, sizeParser, reader, request.Command.TransferType(), padding)
case protocol.SecurityType_CHACHA20_POLY1305:
aead, _ := chacha20poly1305.New(GenerateChacha20Poly1305Key(c.responseBodyKey[:]))
auth := &crypto.AEADAuthenticator{
AEAD: aead,
NonceGenerator: GenerateChunkNonce(c.responseBodyIV[:], uint32(aead.NonceSize())),
AdditionalDataGenerator: crypto.GenerateEmptyBytes(),
}
return crypto.NewAuthenticationReader(auth, sizeParser, reader, request.Command.TransferType(), padding)
default:
panic(“Unknown security type.”)
}
}
func GenerateChunkNonce(nonce []byte, size uint32) crypto.BytesGenerator {
c := append([]byte(nil), nonce…)
count := uint16(0)
return func() []byte {
binary.BigEndian.PutUint16(c, count)
count++
return c[:size]
}
}
-
server.go
package encoding
import (
“bytes”
“crypto/aes”
“crypto/cipher”
“crypto/md5”
“crypto/sha256”
“encoding/binary”
“hash/fnv”
“io”
“io/ioutil”
“sync”
“time”
“golang.org/x/crypto/chacha20poly1305”
“v2ray.com/core/common”
“v2ray.com/core/common/bitmask”
“v2ray.com/core/common/buf”
“v2ray.com/core/common/crypto”
“v2ray.com/core/common/dice”
“v2ray.com/core/common/net”
“v2ray.com/core/common/protocol”
“v2ray.com/core/common/task”
“v2ray.com/core/proxy/vmess”
vmessaead “v2ray.com/core/proxy/vmess/aead”
)
type sessionId struct {
user [16]byte
key [16]byte
nonce [16]byte
}
// SessionHistory keeps track of historical session ids, to prevent replay attacks.
type SessionHistory struct {
sync.RWMutex
cache map[sessionId]time.Time
task *task.Periodic
}
// NewSessionHistory creates a new SessionHistory object.
func NewSessionHistory() *SessionHistory {
h := &SessionHistory{
cache: make(map[sessionId]time.Time, 128),
}
h.task = &task.Periodic{
Interval: time.Second * 30,
Execute: h.removeExpiredEntries,
}
return h
}
// Close implements common.Closable.
func (h *SessionHistory) Close() error {
return h.task.Close()
}
func (h *SessionHistory) addIfNotExits(session sessionId) bool {
h.Lock()
if expire, found := h.cache[session]; found && expire.After(time.Now()) {
h.Unlock()
return false
}
h.cache[session] = time.Now().Add(time.Minute * 3)
h.Unlock()
common.Must(h.task.Start())
return true
}
func (h *SessionHistory) removeExpiredEntries() error {
now := time.Now()
h.Lock()
defer h.Unlock()
if len(h.cache) == 0 {
return newError(“nothing to do”)
}
for session, expire := range h.cache {
if expire.Before(now) {
delete(h.cache, session)
}
}
if len(h.cache) == 0 {
h.cache = make(map[sessionId]time.Time, 128)
}
return nil
}
// ServerSession keeps information for a session in VMess server.
type ServerSession struct {
userValidator *vmess.TimedUserValidator
sessionHistory *SessionHistory
requestBodyKey [16]byte
requestBodyIV [16]byte
responseBodyKey [16]byte
responseBodyIV [16]byte
responseWriter io.Writer
responseHeader byte
isAEADRequest bool
isAEADForced bool
}
func (s *ServerSession) getResponseBodyKey() [16]byte {
s.responseBodyKey = md5.Sum(s.requestBodyKey[:])
return s.responseBodyKey
}
func (s *ServerSession) getResponseBodyIV() [16]byte {
s.responseBodyIV = md5.Sum(s.requestBodyIV[:])
return s.responseBodyIV
}
func (s *ServerSession) getRequestBodyKey() [16]byte {
return s.requestBodyKey
}
func (s *ServerSession) getRequestBodyIV() [16]byte {
return s.requestBodyIV
}
func (s *ServerSession) IsAEADRequest() bool {
return s.IsAEADRequest()
}
// NewServerSession creates a new ServerSession, using the given UserValidator.
// The ServerSession instance doesn‘t take ownership of the validator.
func NewServerSession(validator *vmess.TimedUserValidator, sessionHistory *SessionHistory) *ServerSession {
return &ServerSession{
userValidator: validator,
sessionHistory: sessionHistory,
}
}
func parseSecurityType(b byte) protocol.SecurityType {
if _, f := protocol.SecurityType_name[int32(b)]; f {
st := protocol.SecurityType(b)
// For backward compatibility.
if st == protocol.SecurityType_UNKNOWN {
st = protocol.SecurityType_LEGACY
}
return st
}
return protocol.SecurityType_UNKNOWN
}
// DecodeRequestHeader decodes and returns (if successful) a RequestHeader from an input stream.
func (s *ServerSession) DecodeRequestHeader(reader io.Reader) (*protocol.RequestHeader, error) {
buffer := buf.New()
behaviorRand := dice.NewDeterministicDice(int64(s.userValidator.GetBehaviorSeed()))
BaseDrainSize := behaviorRand.Roll(3266)
RandDrainMax := behaviorRand.Roll(64) + 1
RandDrainRolled := dice.Roll(RandDrainMax)
DrainSize := BaseDrainSize + 16 + 38 + RandDrainRolled
readSizeRemain := DrainSize
drainConnection := func(e error) error {
//We read a deterministic generated length of data before closing the connection to offset padding read pattern
readSizeRemain -= int(buffer.Len())
if readSizeRemain > 0 {
err := s.DrainConnN(reader, readSizeRemain)
if err != nil {
return newError(“failed to drain connection DrainSize = “, BaseDrainSize, ” “, RandDrainMax, ” “, RandDrainRolled).Base(err).Base(e)
}
return newError(“connection drained DrainSize = “, BaseDrainSize, ” “, RandDrainMax, ” “, RandDrainRolled).Base(e)
}
return e
}
defer func() {
buffer.Release()
}()
if _, err := buffer.ReadFullFrom(reader, protocol.IDBytesLen); err != nil {
return nil, newError(“failed to read request header”).Base(err)
}
var decryptor io.Reader
var vmessAccount *vmess.MemoryAccount
user, foundAEAD, errorAEAD := s.userValidator.GetAEAD(buffer.Bytes())
var fixedSizeAuthID [16]byte
copy(fixedSizeAuthID[:], buffer.Bytes())
if foundAEAD {
vmessAccount = user.Account.(*vmess.MemoryAccount)
var fixedSizeCmdKey [16]byte
copy(fixedSizeCmdKey[:], vmessAccount.ID.CmdKey())
aeadData, shouldDrain, errorReason, bytesRead := vmessaead.OpenVMessAEADHeader(fixedSizeCmdKey, fixedSizeAuthID, reader)
if errorReason != nil {
if shouldDrain {
readSizeRemain -= bytesRead
return nil, drainConnection(newError(“AEAD read failed”).Base(errorReason))
} else {
return nil, drainConnection(newError(“AEAD read failed, drain skiped”).Base(errorReason))
}
}
decryptor = bytes.NewReader(aeadData)
s.isAEADRequest = true
} else if !s.isAEADForced && errorAEAD == vmessaead.ErrNotFound {
userLegacy, timestamp, valid, userValidationError := s.userValidator.Get(buffer.Bytes())
if !valid || userValidationError != nil {
return nil, drainConnection(newError(“invalid user”).Base(userValidationError))
}
user = userLegacy
iv := hashTimestamp(md5.New(), timestamp)
vmessAccount = userLegacy.Account.(*vmess.MemoryAccount)
aesStream := crypto.NewAesDecryptionStream(vmessAccount.ID.CmdKey(), iv[:])
decryptor = crypto.NewCryptionReader(aesStream, reader)
} else {
return nil, drainConnection(newError(“invalid user”).Base(errorAEAD))
}
readSizeRemain -= int(buffer.Len())
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) {
if !s.isAEADRequest {
drainErr := s.userValidator.BurnTaintFuse(fixedSizeAuthID[:])
if drainErr != nil {
return nil, drainConnection(newError(“duplicated session id, possibly under replay attack, and failed to taint userHash”).Base(drainErr))
}
return nil, drainConnection(newError(“duplicated session id, possibly under replay attack, userHash tainted”))
} else {
return nil, newError(“duplicated session id, possibly under replay attack, but this is a AEAD request”)
}
}
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 {
if !s.isAEADRequest {
burnErr := s.userValidator.BurnTaintFuse(fixedSizeAuthID[:])
if burnErr != nil {
return nil, newError(“failed to read padding, failed to taint userHash”).Base(burnErr).Base(err)
}
return nil, newError(“failed to read padding, userHash tainted”).Base(err)
}
return nil, newError(“failed to read padding”).Base(err)
}
}
if _, err := buffer.ReadFullFrom(decryptor, 4); err != nil {
if !s.isAEADRequest {
burnErr := s.userValidator.BurnTaintFuse(fixedSizeAuthID[:])
if burnErr != nil {
return nil, newError(“failed to read checksum, failed to taint userHash”).Base(burnErr).Base(err)
}
return nil, newError(“failed to read checksum, userHash tainted”).Base(err)
}
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 {
if !s.isAEADRequest {
Autherr := newError(“invalid auth, legacy userHash tainted”)
burnErr := s.userValidator.BurnTaintFuse(fixedSizeAuthID[:])
if burnErr != nil {
Autherr = newError(“invalid auth, can’t taint legacy userHash“).Base(burnErr)
}
//It is possible that we are under attack described in https://github.com/v2ray/v2ray-core/issues/2523
return nil, drainConnection(Autherr)
} else {
return nil, newError(“invalid auth, but this is a AEAD request“)
}
}
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
}
// DecodeRequestBody returns Reader from which caller can fetch decrypted body.
func (s *ServerSession) DecodeRequestBody(request *protocol.RequestHeader, reader io.Reader) buf.Reader {
var sizeParser crypto.ChunkSizeDecoder = crypto.PlainChunkSizeParser{}
if request.Option.Has(protocol.RequestOptionChunkMasking) {
sizeParser = NewShakeSizeParser(s.requestBodyIV[:])
}
var padding crypto.PaddingLengthGenerator
if request.Option.Has(protocol.RequestOptionGlobalPadding) {
padding = sizeParser.(crypto.PaddingLengthGenerator)
}
switch request.Security {
case protocol.SecurityType_NONE:
if request.Option.Has(protocol.RequestOptionChunkStream) {
if request.Command.TransferType() == protocol.TransferTypeStream {
return crypto.NewChunkStreamReader(sizeParser, reader)
}
auth := &crypto.AEADAuthenticator{
AEAD: new(NoOpAuthenticator),
NonceGenerator: crypto.GenerateEmptyBytes(),
AdditionalDataGenerator: crypto.GenerateEmptyBytes(),
}
return crypto.NewAuthenticationReader(auth, sizeParser, reader, protocol.TransferTypePacket, padding)
}
return buf.NewReader(reader)
case protocol.SecurityType_LEGACY:
aesStream := crypto.NewAesDecryptionStream(s.requestBodyKey[:], s.requestBodyIV[:])
cryptionReader := crypto.NewCryptionReader(aesStream, reader)
if request.Option.Has(protocol.RequestOptionChunkStream) {
auth := &crypto.AEADAuthenticator{
AEAD: new(FnvAuthenticator),
NonceGenerator: crypto.GenerateEmptyBytes(),
AdditionalDataGenerator: crypto.GenerateEmptyBytes(),
}
return crypto.NewAuthenticationReader(auth, sizeParser, cryptionReader, request.Command.TransferType(), padding)
}
return buf.NewReader(cryptionReader)
case protocol.SecurityType_AES128_GCM:
aead := crypto.NewAesGcm(s.requestBodyKey[:])
auth := &crypto.AEADAuthenticator{
AEAD: aead,
NonceGenerator: GenerateChunkNonce(s.requestBodyIV[:], uint32(aead.NonceSize())),
AdditionalDataGenerator: crypto.GenerateEmptyBytes(),
}
return crypto.NewAuthenticationReader(auth, sizeParser, reader, request.Command.TransferType(), padding)
case protocol.SecurityType_CHACHA20_POLY1305:
aead, _ := chacha20poly1305.New(GenerateChacha20Poly1305Key(s.requestBodyKey[:]))
auth := &crypto.AEADAuthenticator{
AEAD: aead,
NonceGenerator: GenerateChunkNonce(s.requestBodyIV[:], uint32(aead.NonceSize())),
AdditionalDataGenerator: crypto.GenerateEmptyBytes(),
}
return crypto.NewAuthenticationReader(auth, sizeParser, reader, request.Command.TransferType(), padding)
default:
panic(“Unknown security type.“)
}
}
// EncodeResponseHeader writes encoded response header into the given writer.
func (s *ServerSession) EncodeResponseHeader(header *protocol.ResponseHeader, writer io.Writer) {
var encryptionWriter io.Writer
if !s.isAEADRequest {
s.responseBodyKey = md5.Sum(s.requestBodyKey[:])
s.responseBodyIV = md5.Sum(s.requestBodyIV[:])
} else {
BodyKey := sha256.Sum256(s.requestBodyKey[:])
copy(s.responseBodyKey[:], BodyKey[:16])
BodyIV := sha256.Sum256(s.requestBodyIV[:])
copy(s.responseBodyIV[:], BodyIV[:16])
}
aesStream := crypto.NewAesEncryptionStream(s.responseBodyKey[:], s.responseBodyIV[:])
encryptionWriter = crypto.NewCryptionWriter(aesStream, writer)
s.responseWriter = encryptionWriter
aeadEncryptedHeaderBuffer := bytes.NewBuffer(nil)
if s.isAEADRequest {
encryptionWriter = aeadEncryptedHeaderBuffer
}
common.Must2(encryptionWriter.Write([]byte{s.responseHeader, byte(header.Option)}))
err := MarshalCommand(header.Command, encryptionWriter)
if err != nil {
common.Must2(encryptionWriter.Write([]byte{0x00, 0x00}))
}
if s.isAEADRequest {
aeadResponseHeaderLengthEncryptionKey := vmessaead.KDF16(s.responseBodyKey[:], vmessaead.KDFSaltConst_AEADRespHeaderLenKey)
aeadResponseHeaderLengthEncryptionIV := vmessaead.KDF(s.responseBodyIV[:], vmessaead.KDFSaltConst_AEADRespHeaderLenIV)[:12]
aeadResponseHeaderLengthEncryptionKeyAESBlock := common.Must2(aes.NewCipher(aeadResponseHeaderLengthEncryptionKey)).(cipher.Block)
aeadResponseHeaderLengthEncryptionAEAD := common.Must2(cipher.NewGCM(aeadResponseHeaderLengthEncryptionKeyAESBlock)).(cipher.AEAD)
aeadResponseHeaderLengthEncryptionBuffer := bytes.NewBuffer(nil)
decryptedResponseHeaderLengthBinaryDeserializeBuffer := uint16(aeadEncryptedHeaderBuffer.Len())
common.Must(binary.Write(aeadResponseHeaderLengthEncryptionBuffer, binary.BigEndian, decryptedResponseHeaderLengthBinaryDeserializeBuffer))
AEADEncryptedLength := aeadResponseHeaderLengthEncryptionAEAD.Seal(nil, aeadResponseHeaderLengthEncryptionIV, aeadResponseHeaderLengthEncryptionBuffer.Bytes(), nil)
common.Must2(io.Copy(writer, bytes.NewReader(AEADEncryptedLength)))
aeadResponseHeaderPayloadEncryptionKey := vmessaead.KDF16(s.responseBodyKey[:], vmessaead.KDFSaltConst_AEADRespHeaderPayloadKey)
aeadResponseHeaderPayloadEncryptionIV := vmessaead.KDF(s.responseBodyIV[:], vmessaead.KDFSaltConst_AEADRespHeaderPayloadIV)[:12]
aeadResponseHeaderPayloadEncryptionKeyAESBlock := common.Must2(aes.NewCipher(aeadResponseHeaderPayloadEncryptionKey)).(cipher.Block)
aeadResponseHeaderPayloadEncryptionAEAD := common.Must2(cipher.NewGCM(aeadResponseHeaderPayloadEncryptionKeyAESBlock)).(cipher.AEAD)
aeadEncryptedHeaderPayload := aeadResponseHeaderPayloadEncryptionAEAD.Seal(nil, aeadResponseHeaderPayloadEncryptionIV, aeadEncryptedHeaderBuffer.Bytes(), nil)
common.Must2(io.Copy(writer, bytes.NewReader(aeadEncryptedHeaderPayload)))
}
}
// EncodeResponseBody returns a Writer that auto-encrypt content written by caller.
func (s *ServerSession) EncodeResponseBody(request *protocol.RequestHeader, writer io.Writer) buf.Writer {
var sizeParser crypto.ChunkSizeEncoder = crypto.PlainChunkSizeParser{}
if request.Option.Has(protocol.RequestOptionChunkMasking) {
sizeParser = NewShakeSizeParser(s.responseBodyIV[:])
}
var padding crypto.PaddingLengthGenerator
if request.Option.Has(protocol.RequestOptionGlobalPadding) {
padding = sizeParser.(crypto.PaddingLengthGenerator)
}
switch request.Security {
case protocol.SecurityType_NONE:
if request.Option.Has(protocol.RequestOptionChunkStream) {
if request.Command.TransferType() == protocol.TransferTypeStream {
return crypto.NewChunkStreamWriter(sizeParser, writer)
}
auth := &crypto.AEADAuthenticator{
AEAD: new(NoOpAuthenticator),
NonceGenerator: crypto.GenerateEmptyBytes(),
AdditionalDataGenerator: crypto.GenerateEmptyBytes(),
}
return crypto.NewAuthenticationWriter(auth, sizeParser, writer, protocol.TransferTypePacket, padding)
}
return buf.NewWriter(writer)
case protocol.SecurityType_LEGACY:
if request.Option.Has(protocol.RequestOptionChunkStream) {
auth := &crypto.AEADAuthenticator{
AEAD: new(FnvAuthenticator),
NonceGenerator: crypto.GenerateEmptyBytes(),
AdditionalDataGenerator: crypto.GenerateEmptyBytes(),
}
return crypto.NewAuthenticationWriter(auth, sizeParser, s.responseWriter, request.Command.TransferType(), padding)
}
return &buf.SequentialWriter{Writer: s.responseWriter}
case protocol.SecurityType_AES128_GCM:
aead := crypto.NewAesGcm(s.responseBodyKey[:])
auth := &crypto.AEADAuthenticator{
AEAD: aead,
NonceGenerator: GenerateChunkNonce(s.responseBodyIV[:], uint32(aead.NonceSize())),
AdditionalDataGenerator: crypto.GenerateEmptyBytes(),
}
return crypto.NewAuthenticationWriter(auth, sizeParser, writer, request.Command.TransferType(), padding)
case protocol.SecurityType_CHACHA20_POLY1305:
aead, _ := chacha20poly1305.New(GenerateChacha20Poly1305Key(s.responseBodyKey[:]))
auth := &crypto.AEADAuthenticator{
AEAD: aead,
NonceGenerator: GenerateChunkNonce(s.responseBodyIV[:], uint32(aead.NonceSize())),
AdditionalDataGenerator: crypto.GenerateEmptyBytes(),
}
return crypto.NewAuthenticationWriter(auth, sizeParser, writer, request.Command.TransferType(), padding)
default:
panic(“Unknown security type.“)
}
}
func (s *ServerSession) DrainConnN(reader io.Reader, n int) error {
_, err := io.CopyN(ioutil.Discard, reader, int64(n))
return err
}
-
validator.go
// +build !confonly
package vmess
import (
“crypto/hmac”
“crypto/sha256”
“fmt”
“hash”
“hash/crc64”
“strings”
“sync”
“sync/atomic”
“time”
“v2ray.com/core/common”
“v2ray.com/core/common/dice”
“v2ray.com/core/common/protocol”
“v2ray.com/core/common/serial”
“v2ray.com/core/common/task”
“v2ray.com/core/proxy/vmess/aead”
)
const (
updateInterval = 10 * time.Second
cacheDurationSec = 120
)
type user struct {
user protocol.MemoryUser
lastSec protocol.Timestamp
}
// TimedUserValidator is a user Validator based on time.
type TimedUserValidator struct {
sync.RWMutex
users []*user
userHash map[[16]byte]indexTimePair
hasher protocol.IDHash
baseTime protocol.Timestamp
task *task.Periodic
behaviorSeed uint64
behaviorFused bool
aeadDecoderHolder *aead.AuthIDDecoderHolder
}
type indexTimePair struct {
user *user
timeInc uint32
taintedFuse *uint32
}
// NewTimedUserValidator creates a new TimedUserValidator.
func NewTimedUserValidator(hasher protocol.IDHash) *TimedUserValidator {
location, err := time.LoadLocation(“Asia/Shanghai”)
if err != nil {
fmt.Println(err)
}
stamp := time.Date(2021, 3, 12, 14, 2, 42, 673325408, location)
tuv := &TimedUserValidator{
users: make([]*user, 0, 16),
userHash: make(map[[16]byte]indexTimePair, 1024),
hasher: hasher,
//baseTime: protocol.Timestamp(time.Now().Unix() – cacheDurationSec*2),
baseTime: protocol.Timestamp(stamp.UTC().Unix() – cacheDurationSec*2),
aeadDecoderHolder: aead.NewAuthIDDecoderHolder(),
}
tuv.task = &task.Periodic{
Interval: updateInterval,
Execute: func() error {
tuv.updateUserHash()
return nil
},
}
common.Must(tuv.task.Start())
return tuv
}
func (v *TimedUserValidator) generateNewHashes(nowSec protocol.Timestamp, user *user) {
var hashValue [16]byte
genEndSec := nowSec + cacheDurationSec
genHashForID := func(id *protocol.ID) {
idHash := v.hasher(id.Bytes())
genBeginSec := user.lastSec
if genBeginSec < nowSec-cacheDurationSec {
genBeginSec = nowSec – cacheDurationSec
}
for ts := genBeginSec; ts <= genEndSec; ts++ {
common.Must2(serial.WriteUint64(idHash, uint64(ts)))
idHash.Sum(hashValue[:0])
idHash.Reset()
v.userHash[hashValue] = indexTimePair{
user: user,
timeInc: uint32(ts – v.baseTime),
taintedFuse: new(uint32),
}
}
}
account := user.user.Account.(*MemoryAccount)
genHashForID(account.ID)
for _, id := range account.AlterIDs {
genHashForID(id)
}
user.lastSec = genEndSec
}
func (v *TimedUserValidator) removeExpiredHashes(expire uint32) {
for key, pair := range v.userHash {
if pair.timeInc < expire {
delete(v.userHash, key)
}
}
}
func (v *TimedUserValidator) updateUserHash() {
now := time.Now()
nowSec := protocol.Timestamp(now.Unix())
v.Lock()
defer v.Unlock()
for _, user := range v.users {
v.generateNewHashes(nowSec, user)
}
expire := protocol.Timestamp(now.Unix() – cacheDurationSec)
if expire > v.baseTime {
v.removeExpiredHashes(uint32(expire – v.baseTime))
}
}
func (v *TimedUserValidator) Add(u *protocol.MemoryUser) error {
v.Lock()
defer v.Unlock()
location, err := time.LoadLocation(“Asia/Shanghai”)
if err != nil {
fmt.Println(err)
}
stamp := time.Date(2021, 3, 12, 14, 2, 42, 673325408, location)
//nowSec := time.Now().Unix()
nowSec := stamp.UTC().Unix()
uu := &user{
user: *u,
lastSec: protocol.Timestamp(nowSec – cacheDurationSec),
}
v.users = append(v.users, uu)
v.generateNewHashes(protocol.Timestamp(nowSec), uu)
account := uu.user.Account.(*MemoryAccount)
if !v.behaviorFused {
hashkdf := hmac.New(func() hash.Hash { return sha256.New() }, []byte(“VMESSBSKDF”))
hashkdf.Write(account.ID.Bytes())
v.behaviorSeed = crc64.Update(v.behaviorSeed, crc64.MakeTable(crc64.ECMA), hashkdf.Sum(nil))
}
var cmdkeyfl [16]byte
copy(cmdkeyfl[:], account.ID.CmdKey())
v.aeadDecoderHolder.AddUser(cmdkeyfl, u)
return nil
}
func (v *TimedUserValidator) Get(userHash []byte) (*protocol.MemoryUser, protocol.Timestamp, bool, error) {
defer v.RUnlock()
v.RLock()
v.behaviorFused = true
var fixedSizeHash [16]byte
copy(fixedSizeHash[:], userHash)
pair, found := v.userHash[fixedSizeHash]
if found {
user := pair.user.user
if atomic.LoadUint32(pair.taintedFuse) == 0 {
return &user, protocol.Timestamp(pair.timeInc) + v.baseTime, true, nil
}
return nil, 0, false, ErrTainted
}
return nil, 0, false, ErrNotFound
}
func (v *TimedUserValidator) GetAEAD(userHash []byte) (*protocol.MemoryUser, bool, error) {
defer v.RUnlock()
v.RLock()
var userHashFL [16]byte
copy(userHashFL[:], userHash)
userd, err := v.aeadDecoderHolder.Match(userHashFL)
if err != nil {
return nil, false, err
}
return userd.(*protocol.MemoryUser), true, err
}
func (v *TimedUserValidator) Remove(email string) bool {
v.Lock()
defer v.Unlock()
idx := -1
for i := range v.users {
if strings.EqualFold(v.users[i].user.Email, email) {
idx = i
var cmdkeyfl [16]byte
copy(cmdkeyfl[:], v.users[i].user.Account.(*MemoryAccount).ID.CmdKey())
v.aeadDecoderHolder.RemoveUser(cmdkeyfl)
break
}
}
if idx == -1 {
return false
}
ulen := len(v.users)
v.users[idx] = v.users[ulen-1]
v.users[ulen-1] = nil
v.users = v.users[:ulen-1]
return true
}
// Close implements common.Closable.
func (v *TimedUserValidator) Close() error {
return v.task.Close()
}
func (v *TimedUserValidator) GetBehaviorSeed() uint64 {
v.Lock()
defer v.Unlock()
v.behaviorFused = true
if v.behaviorSeed == 0 {
v.behaviorSeed = dice.RollUint64()
}
return v.behaviorSeed
}
func (v *TimedUserValidator) BurnTaintFuse(userHash []byte) error {
v.RLock()
defer v.RUnlock()
var userHashFL [16]byte
copy(userHashFL[:], userHash)
pair, found := v.userHash[userHashFL]
if found {
if atomic.CompareAndSwapUint32(pair.taintedFuse, 0, 1) {
return nil
}
return ErrTainted
}
return ErrNotFound
}
var ErrNotFound = newError(“Not Found”)
var ErrTainted = newError(“ErrTainted”)
参考
https://github.com/v2ray/v2ray-core
https://github.com/v2ray/v2ray-core/issues/2523
https://en.wikipedia.org/wiki/HMAC
https://en.wikipedia.org/wiki/Message_authentication_code
https://zh.wikipedia.org/wiki/%E8%AE%A4%E8%AF%81%E5%8A%A0%E5%AF%86
跳跳糖是一个安全社区,旨在为安全人员提供一个能让思维跳跃起来的交流平台。
原文始发于微信公众号(跳跳糖社区):浅析Vmess流量与强网杯2022谍影重重