From a064f72104cfa5385d34f75d9dfc6eb112e4f58c Mon Sep 17 00:00:00 2001 From: wenyifan Date: Fri, 9 Sep 2022 15:29:41 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E9=98=B2=E6=8E=A2=E6=B5=8B?= =?UTF-8?q?=E5=8A=9F=E8=83=BD,=E5=A2=9E=E5=8A=A0=E6=B5=81=E9=87=8F?= =?UTF-8?q?=E5=8A=A0=E5=AF=86=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 19 +++++++++---- cmd/client.go | 5 ++++ cmd/root.go | 11 ++++++++ cmd/server.go | 5 ++++ main.go | 10 +++++-- shadow/client.go | 10 +++++-- shadow/client_test.go | 50 +++++++++++++++++++++++++------- shadow/global.go | 8 ++++++ shadow/packer.go | 27 ++++++++++++++++-- shadow/rand.go | 49 ++++++++++++++++++++++++++++++++ shadow/server.go | 66 ++++++++----------------------------------- shadow/tls_util.go | 4 +++ 12 files changed, 185 insertions(+), 79 deletions(-) create mode 100644 shadow/global.go create mode 100644 shadow/rand.go diff --git a/README.md b/README.md index a4d5b56..d070e33 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ # Shadow-TLS ### TLS伪装代理 -- 包装任意TCP连接为真正合法域名的TLS连接 ## 基本原理 -与服务端连接后,服务端会请求指定合法的HTTPS域名(例如www.apple.com)并转发TLS握手流量(给防火墙/审计设备表演一个真正的TLS握手),与客户端TLS握手成功后后续将转发实际的TCP流量,对审计设备(防火墙/上网行为管理软件/零信任网关)而言你是访问一个合法且是真实证书的HTTPS网站. +给审计设备表演一个访问指定网站的TLS握手,在后续的加密流量中传输自定义的数据,由于握手的证书是真实的证书,对审计设备(防火墙/上网行为分析/零信任网关)而言本次TCP连接是访问指定网站的HTTPS流量 ## 使用场景 - 在有域名白名单的情况下需要将流量转发出去 -- 对抗网络审计设备的审查 +- 审计设备会验证TLS证书合法性,自签证书无法通过审计设备的场景 ## 使用方法 - 服务端示例: @@ -18,6 +18,17 @@ ```shell ./shadowtls client -l 0.0.0.0:11222 -s 145.142.63.32:443 -d www.apple.com ``` + +## 探测防御 +- 设置参数密码-p 可开启探测防御功能,服务端检测到非被客户端发起的请求后将会作为标准的SNI代理服务器,转发用于伪装源站的所有流量,对主动探测者而言这台服务器是指定网站的官方服务器 +- 若被大量请求可能造成服务器产生大量流量,注意风控 + +## 流量加密 +- 设置加密密钥参数-k 可启用流量加密,密钥长度必须为16,24或32个字符 + +## 已知限制 +- 若审计软件会对HTTPS证书进行替换使用审计软件自签的证书,本工具将不再适用 + ## 使用Nginx将本服务和其他443端口服务并存 - 编译带有stream和stream_ssl_preread模块的nginx - 参考以下配置 @@ -69,10 +80,6 @@ http { ``` - -## 安全性特别说明 -- 包装的TCP流量没有加密,因由上层处理,推荐配合gost使用 - ## 特别说明 - 感谢v2ex网友ihciah的思路灵感. - 仅供技术研究,请勿用于非法用途. \ No newline at end of file diff --git a/cmd/client.go b/cmd/client.go index 0cf1b1a..3da5040 100644 --- a/cmd/client.go +++ b/cmd/client.go @@ -1,6 +1,7 @@ package cmd import ( + "fmt" "github.com/spf13/cobra" "shadowTLS/shadow" ) @@ -17,6 +18,10 @@ var ( Use: "client", Short: "Client mode", Run: func(cmd *cobra.Command, args []string) { + if !verifyAndParseEncryptionKey() { + fmt.Println("[Client] Invalid encryption key length") + return + } client := shadow.NewClient(clientParams.ListenAddr, clientParams.ServerAddr, clientParams.SNI) client.Start() }, diff --git a/cmd/root.go b/cmd/root.go index 6a31c13..1a952f3 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/spf13/cobra" "os" + "shadowTLS/shadow" ) var ( @@ -15,6 +16,8 @@ var ( func init() { rootCmd.CompletionOptions.DisableDefaultCmd = true + rootCmd.PersistentFlags().StringVarP(&shadow.HandshakePassword, "password", "p", "", "Password for probe resist.Probe resist will be disabled if password is empty.") + rootCmd.PersistentFlags().StringVarP(&shadow.Key, "key", "k", "", "Encryption key for AES-CBC.Encryption will be enabled if key is set.Length of key must be 16,24 or 32.") } func Execute() { @@ -23,3 +26,11 @@ func Execute() { os.Exit(1) } } + +func verifyAndParseEncryptionKey() bool { + if shadow.Key != "" && len(shadow.Key) != 16 && len(shadow.Key) != 24 && len(shadow.Key) != 32 { + return false + } + shadow.EncryptKey = []byte(shadow.Key) + return true +} diff --git a/cmd/server.go b/cmd/server.go index a3d0afb..da38cef 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -1,6 +1,7 @@ package cmd import ( + "fmt" "github.com/spf13/cobra" "shadowTLS/shadow" ) @@ -17,6 +18,10 @@ var ( Use: "server", Short: "Server mode", Run: func(cmd *cobra.Command, args []string) { + if !verifyAndParseEncryptionKey() { + fmt.Println("[Server] Invalid encryption key length") + return + } server := shadow.NewServer(serverParams.ListenAddr, serverParams.TargetAddr, serverParams.FakeAddr) server.Start() }, diff --git a/main.go b/main.go index d0a3d4b..187ad18 100644 --- a/main.go +++ b/main.go @@ -1,8 +1,14 @@ package main -import "shadowTLS/cmd" +import ( + "math/rand" + "shadowTLS/cmd" + "time" +) +func init() { + rand.Seed(time.Now().UnixNano()) +} func main() { - cmd.Execute() } diff --git a/shadow/client.go b/shadow/client.go index f0d85e7..b8c9648 100644 --- a/shadow/client.go +++ b/shadow/client.go @@ -42,11 +42,15 @@ func (c *Client) Start() { } func handlerClient(conn net.Conn, serverAddress string, fakeAddressSNI string) { + config := &tls.Config{ + ServerName: fakeAddressSNI, + } + if HandshakePassword != "" { + config.Rand = RandReaderObj + } dial, err := tls.DialWithDialer(&net.Dialer{ Timeout: time.Second * 5, - }, "tcp", serverAddress, &tls.Config{ - ServerName: fakeAddressSNI, - }) + }, "tcp", serverAddress, config) if err != nil { fmt.Printf("[Client] Dial server error: %v\n", err) return diff --git a/shadow/client_test.go b/shadow/client_test.go index 45c68cb..e02390e 100644 --- a/shadow/client_test.go +++ b/shadow/client_test.go @@ -1,6 +1,8 @@ package shadow import ( + "bytes" + "crypto/md5" "crypto/tls" "fmt" "net" @@ -8,7 +10,7 @@ import ( "time" ) -func TestName(t *testing.T) { +func TestHandshake(t *testing.T) { dial, err := tls.DialWithDialer(&net.Dialer{ Timeout: time.Second * 5, }, "tcp", "evan.run:443", &tls.Config{ @@ -22,15 +24,43 @@ func TestName(t *testing.T) { time.Sleep(time.Minute) } -func TestName2(t *testing.T) { - b := []byte("ABC") - encrypt, err := AesEncrypt(b, []byte("1234567812345678")) - if err != nil { - fmt.Println(err) +func TestMd5(t *testing.T) { + key := "Passwd" + passwd := []byte(key) + + buf := make([]byte, 32) + srcCode := md5.Sum(RandomByte(16)) + copy(buf[0:], srcCode[0:]) + buffer := bytes.NewBuffer(srcCode[:]) + + sum := md5.Sum(passwd) + buffer.Write(sum[:]) + + hash := md5.Sum(buffer.Bytes()) + copy(buf[16:], hash[0:]) + fmt.Println(buf) + + vBuf := make([]byte, 32) + copy(vBuf, buf[0:16]) + verifyBuf := bytes.NewBuffer(vBuf) + verifyBuf.Write(sum[:]) + + verifyHash := md5.Sum(buffer.Bytes()) + if bytes.Equal(verifyHash[:], buf[16:32]) { + fmt.Println("GOOD") } - decrypt, err := AesDecrypt(encrypt, []byte("1234567812345678")) - if err != nil { - fmt.Println(err) + if VerifyKey(buf, key) { + fmt.Println("VerifyKey GOOD") } - fmt.Println(string(decrypt)) + +} + +func TestAes(t *testing.T) { + key := []byte("1234567812345678") + data := []byte("AVC") + + e := AesEncryptCBC(data, key) + d := AesDecryptCBC(e, key) + + fmt.Println(string(d)) } diff --git a/shadow/global.go b/shadow/global.go new file mode 100644 index 0000000..402753c --- /dev/null +++ b/shadow/global.go @@ -0,0 +1,8 @@ +package shadow + +var ( + HandshakePassword = "" + RandReaderObj = &RandReader{} + EncryptKey = []byte{} + Key = "" +) diff --git a/shadow/packer.go b/shadow/packer.go index 9af56d7..81eefd3 100644 --- a/shadow/packer.go +++ b/shadow/packer.go @@ -47,6 +47,12 @@ func (m PackAppData) Read(p []byte) (n int, err error) { copy(p[sum:], buf[HeaderLength+2+sum:HeaderLength+2+sum+r]) sum += r } + if len(EncryptKey) > 0 { + encryptedData := p[0:sum] + decrypted := AesDecryptCBC(encryptedData, EncryptKey) + copy(p[0:], decrypted) + sum = len(decrypted) + } return sum, err } else { fmt.Printf("Invalid header") @@ -55,15 +61,30 @@ func (m PackAppData) Read(p []byte) (n int, err error) { } func (m PackAppData) Write(p []byte) (n int, err error) { + var sendData []byte + if len(EncryptKey) > 0 { + sendData = AesEncryptCBC(p, EncryptKey) + } else { + sendData = p + } + lenNum := make([]byte, 2) - binary.BigEndian.PutUint16(lenNum, uint16(len(p))) + binary.BigEndian.PutUint16(lenNum, uint16(len(sendData))) packetBuf := bytes.NewBuffer(AppDataHeader) packetBuf.Write(lenNum) - packetBuf.Write(p) + packetBuf.Write(sendData) write, err := m.Conn.Write(packetBuf.Bytes()) - write = write - HeaderLength - 2 + if len(EncryptKey) > 0 { + if write != packetBuf.Len() { + write = 0 + } else { + write = len(p) + } + } else { + write = write - HeaderLength - 2 + } return write, err } diff --git a/shadow/rand.go b/shadow/rand.go new file mode 100644 index 0000000..d23d92e --- /dev/null +++ b/shadow/rand.go @@ -0,0 +1,49 @@ +package shadow + +import ( + "bytes" + "crypto/md5" + "io" + "math/rand" +) + +type RandReader struct { + io.Reader +} + +func (r RandReader) Read(p []byte) (n int, err error) { + buf := make([]byte, 32) + randBytes := md5.Sum(RandomByte(16)) + copy(buf[0:], randBytes[:]) + + preHashData := bytes.NewBuffer(randBytes[:]) + sum := md5.Sum([]byte(HandshakePassword)) + preHashData.Write(sum[:]) + hash := md5.Sum(preHashData.Bytes()) + copy(buf[16:], hash[:]) + copy(p, buf) + return 32, nil +} + +func RandomByte(size int) []byte { + buf := make([]byte, size) + for i := 0; i < size; i++ { + buf[i] = byte(rand.Intn(255)) + } + return buf +} + +func VerifyKey(p []byte, key string) bool { + if len(p) != 32 { + return false + } + buf := make([]byte, 16) + copy(buf, p[0:16]) + buffer := bytes.NewBuffer(buf) + sum := md5.Sum([]byte(key)) + buffer.Write(sum[:]) + + hash := md5.Sum(buffer.Bytes()) + + return bytes.Equal(hash[:], p[16:]) +} diff --git a/shadow/server.go b/shadow/server.go index e589575..39c4b90 100644 --- a/shadow/server.go +++ b/shadow/server.go @@ -79,62 +79,11 @@ func handler(conn net.Conn, targetAddress string, fakeAddress string) { go MyCopy(p, realConnection, exit) go MyCopy(realConnection, p, exit) <-exit - - //go func() { - // buf := make([]byte, 64*1024) - // for { - // nr, er := realConnection.Read(buf) - // if er != nil { - // if er == io.EOF { - // continue - // } else { - // fmt.Println("read err:", er) - // break - // } - // } else { - // lenNum := make([]byte, 2) - // binary.BigEndian.PutUint16(lenNum, uint16(nr)) - // - // packetBuf := bytes.NewBuffer(AppDataHeader) - // packetBuf.Write(lenNum) - // packetBuf.Write(buf[0:nr]) - // - // _, ew := conn.Write(packetBuf.Bytes()) - // if ew != nil { - // fmt.Printf("err2:%v\n", ew) - // break - // } - // } - // } - //}() - // - //go func() { - // result := bytes.NewBuffer(nil) - // var buf [65542]byte // 由于 标识数据包长度 的只有两个字节 故数据包最大为 2^16+4(魔数)+2(长度标识) - // for { - // n, er := conn.Read(buf[0:]) - // result.Write(buf[0:n]) - // if er != nil { - // if er == io.EOF { - // continue - // } else { - // fmt.Println("read err:", er) - // break - // } - // } else { - // scanner := bufio.NewScanner(result) - // scanner.Split(packetSlitFunc) - // for scanner.Scan() { - // realConnection.Write(scanner.Bytes()[HeaderLength+2:]) - // } - // } - // result.Reset() - // } - //}() } func processHandshake(src net.Conn, dst net.Conn, waitCh chan int, srcType string) { buf := make([]byte, 32*1024) + verifyPass := false for { nr, er := src.Read(buf) if nr > 0 { @@ -142,10 +91,17 @@ func processHandshake(src net.Conn, dst net.Conn, waitCh chan int, srcType strin if srcType == "client" { header := ParseAndVerifyTLSHeader(buf[0:nr]) if header != nil { + if header != nil && header.Type == Handshake && header.HandshakeType == ClientHello && !verifyPass { + verifyPass = VerifyKey(header.Rand, HandshakePassword) + } if header.Type == ChangeCipherSpec { - fmt.Println("[Server] handshake complete") - waitCh <- 1 - break + if HandshakePassword != "" && !verifyPass { + fmt.Println("[Server] Probe detected,pass through all traffic.") + } else { + fmt.Println("[Server] handshake complete") + waitCh <- 1 + break + } } //fmt.Println(header.toString()) } diff --git a/shadow/tls_util.go b/shadow/tls_util.go index dd68416..84008a7 100644 --- a/shadow/tls_util.go +++ b/shadow/tls_util.go @@ -31,6 +31,7 @@ type TLSHeader struct { Length int HandshakeType uint8 ChangeCipherSpecNext uint8 + Rand []byte } func (t *TLSHeader) toString() string { @@ -95,6 +96,9 @@ func ParseAndVerifyTLSHeader(data []byte) *TLSHeader { if header.HandshakeType != ServerHello && header.HandshakeType != ClientHello && header.HandshakeType != Certificate && header.HandshakeType != ServerKeyExchange && header.HandshakeType != ServerHelloDone { header.HandshakeType = EncryptedHandshake } + if header.HandshakeType == ClientHello { + header.Rand = data[11:43] + } } if header.Type == ChangeCipherSpec { if len(data) > 6 {