diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..53e7dcc --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/.vscode +/tgp diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..37292a0 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Heorhi Valakhanovich + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..4914785 --- /dev/null +++ b/go.mod @@ -0,0 +1,7 @@ +module tgp + +go 1.19 + +require ( + github.com/BurntSushi/toml v1.2.1 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..a3b7af3 --- /dev/null +++ b/go.sum @@ -0,0 +1,5 @@ +github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak= +github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go new file mode 100644 index 0000000..9b5e419 --- /dev/null +++ b/main.go @@ -0,0 +1,77 @@ +package main + +import ( + "fmt" + "net" + + "github.com/BurntSushi/toml" +) + +var defaultConfig = ` +listen_url = "0.0.0.0:6666" +secret = "dd000102030405060708090a0b0c0d0e0f" +#secret = "00000000000000000000000000000000" +` + +type Config struct { + Listen_Url string + Secret string +} + +func handleConnection(conn net.Conn, secret *Secret) { + // var initialPacket [initialHeaderSize]byte + // _, err := io.ReadFull(conn, initialPacket[:]) + // if err != nil { + // conn.Close() + // return + // } + // // dump header + // println("header: " + hex.EncodeToString(initialPacket[:])) + // c, err := obfuscatedCryptoFromHeader(initialPacket, secret) + // if err != nil { + // println(err.Error()) + // conn.Close() + // return + // } + // if true { + // buf := make([]byte, 4) + // err = c.ReadExact(buf, conn) + + // if err != nil { + // println(err.Error()) + // conn.Close() + // return + // } + // fmt.Printf("first packet bytes: %x %x %x %x\n", buf[0], buf[1], buf[2], buf[3]) + // } + obfuscatedRoutine, err := obfuscatedRouterFromStream(conn, secret) + if err != nil { + println(err.Error()) + return + } + obfuscatedRoutine.Wait() +} + +func main() { + c := &Config{} + _, err := toml.Decode(defaultConfig, &c) + if err != nil { + panic(err) + } + fmt.Printf("listen: %s, secret: %s\n", c.Listen_Url, c.Secret) + secret, err := NewSecretHex(c.Secret) + if err != nil { + panic(err) + } + listener, err := net.Listen("tcp", c.Listen_Url) + if err != nil { + panic(err) + } + for { + conn, err := listener.Accept() + if err != nil { + return + } + go handleConnection(conn, secret) + } +} diff --git a/obfuscated.go b/obfuscated.go new file mode 100644 index 0000000..db41dc9 --- /dev/null +++ b/obfuscated.go @@ -0,0 +1,147 @@ +package main + +import ( + "encoding/hex" + "fmt" + "io" + "net" +) + +const dc_port = "443" + +var dc_ip4 = [...]string{ + "149.154.175.50", + "149.154.167.51", + "149.154.175.100", + "149.154.167.91", + "149.154.171.5", +} + +var dc_ip6 = [...]string{ + "2001:b28:f23d:f001::a", + "2001:67c:04e8:f002::a", + "2001:b28:f23d:f003::a", + "2001:67c:04e8:f004::a", + "2001:b28:f23f:f005::a", +} + +const initialHeaderSize = 64 + +const ( + abridged = 0xef + intermediate = 0xee //0xeeeeeeee + padded = 0xdd //0xdddddddd + full = 0 +) + +type obfuscatedRouter struct { + cryptClient *obfuscatedClientCtx + cryptDc *dcCtx + stream io.ReadWriteCloser + readerJoinChannel chan error + writerJoinChannel chan error +} + +func obfuscatedRouterFromStream(stream io.ReadWriteCloser, secret *Secret) (r *obfuscatedRouter, err error) { + var initialPacket [initialHeaderSize]byte + _, err = io.ReadFull(stream, initialPacket[:]) + if err != nil { + return nil, err + } + cryptClient, err := obfuscatedClientCtxFromHeader(initialPacket, secret) + if err != nil { + return nil, err + } + // basic afterchecks + switch cryptClient.protocol { + case abridged, intermediate, padded: + break + default: + return nil, fmt.Errorf("invalid protocol %d", cryptClient.protocol) + } + if int(cryptClient.dc) > len(dc_ip4) || int(cryptClient.dc) < -len(dc_ip4) { + return nil, fmt.Errorf("invalid dc %d", cryptClient.dc) + } + //connect to dc + dcConnection, err := connectDC(int(cryptClient.dc)) + if err != nil { + return nil, err + } + cryptDc, err := dcCtxFromClient(int(cryptClient.dc), cryptClient.protocol) + if err != nil { + return nil, err + } + readerJoinChannel := make(chan error, 1) + go func() { + _, err = dcConnection.Write(cryptDc.nonce[:]) + if err != nil { + readerJoinChannel <- err + return + } + buf := make([]byte, 2048) + for { + size, err := stream.Read(buf) + if err != nil { + fmt.Printf("reader broken, size: %d\n", size) + readerJoinChannel <- err + return + } + cryptClient.decryptNext(buf[:size]) + fmt.Printf("cl dec: %s\n", hex.EncodeToString(buf[:size])) + cryptDc.encryptNext(buf[:size]) + _, err = dcConnection.Write(buf[:size]) + if err != nil { + readerJoinChannel <- err + return + } + } + }() + writerJoinChannel := make(chan error, 1) + go func() { + buf := make([]byte, 2048) + for { + size, err := dcConnection.Read(buf) + if err != nil { + fmt.Printf("writer broken, size: %d\n", size) + writerJoinChannel <- err + return + } + cryptDc.decryptNext(buf[:size]) + fmt.Printf("dc dec: %s\n", hex.EncodeToString(buf[:size])) + cryptClient.encryptNext(buf[:size]) + _, err = stream.Write(buf[:size]) + if err != nil { + writerJoinChannel <- err + return + } + } + }() + r = &obfuscatedRouter{ + cryptClient: cryptClient, + cryptDc: cryptDc, + stream: stream, + readerJoinChannel: readerJoinChannel, + writerJoinChannel: writerJoinChannel, + } + return r, nil +} + +func (r obfuscatedRouter) Wait() { + <-r.readerJoinChannel + <-r.writerJoinChannel +} + +func connectDC(dc int) (c net.Conn, err error) { + if dc < 0 { + dc = -dc + } + if dc < 1 || dc > len(dc_ip4) { + return nil, fmt.Errorf("invalid dc %d", dc) + } + dc_addr := dc_ip4[dc-1] + ":" + dc_port + c, err = net.Dial("tcp", dc_addr) + if err != nil { + return nil, err + } + return c, nil +} diff --git a/obfuscated_test.go b/obfuscated_test.go new file mode 100644 index 0000000..eb333cb --- /dev/null +++ b/obfuscated_test.go @@ -0,0 +1,25 @@ +package main + +import "testing" + +func TestGenInits(t *testing.T) { + //lint:ignore SA4006 this is a test + init, err := genHeader() + if err != nil { + t.Fatal(err) + } + if len(init) != initialHeaderSize { + t.Fatal("wrong init length") + } +} + +func TestDecryptInit(t *testing.T) { + var init [initialHeaderSize]byte + for i := 0; i < len(init); i++ { + init[i] = byte(i) + } + dec := decryptInit(init) + if dec[0] != 55 || dec[47] != 8 { + t.Errorf("dec[0]=%d, dec[47]=%d", dec[0], dec[47]) + } +} diff --git a/secret.go b/secret.go new file mode 100644 index 0000000..d72bfac --- /dev/null +++ b/secret.go @@ -0,0 +1,58 @@ +package main + +import ( + "encoding/hex" + "fmt" +) + +type SecretType int + +const simpleSecretLen = 16 + +const ( + Simple SecretType = 1 + Secured = 2 + FakeTLS = 3 +) + +type Secret struct { + RawSecret []byte + Type SecretType + Tag byte + Fakehost string +} + +// Generate secret from hex string +func NewSecretHex(secret string) (*Secret, error) { + secretBytes, err := hex.DecodeString(secret) + if err != nil { + return nil, err + } + return NewSecret(secretBytes) +} + +// Generate secret from byte array +func NewSecret(secret []byte) (*Secret, error) { + switch { + case len(secret) == simpleSecretLen: + return &Secret{ + RawSecret: secret, + Type: Simple, + }, nil + case len(secret) == simpleSecretLen+1: + return &Secret{ + RawSecret: secret[1 : simpleSecretLen+1], + Type: Secured, + Tag: secret[0], + }, nil + case len(secret) > simpleSecretLen+1: + return &Secret{ + RawSecret: secret[1 : simpleSecretLen+1], + Type: FakeTLS, + Tag: secret[0], + Fakehost: string(secret[simpleSecretLen+1:]), + }, nil + default: + return nil, fmt.Errorf("Incorrect secret length: %d", len(secret)) + } +} diff --git a/secret_test.go b/secret_test.go new file mode 100644 index 0000000..34087cc --- /dev/null +++ b/secret_test.go @@ -0,0 +1,50 @@ +package main + +import ( + "encoding/hex" + "testing" +) + +func TestSecretSimple(t *testing.T) { + secretBytes, err := hex.DecodeString("000102030405060708090a0b0c0d0e0f") + secret, err := NewSecret(secretBytes) + if err != nil { + t.Fatal(err) + } + if secret.Type != Simple { + t.Errorf("Wrong secret type expected %d found %d", Simple, secret.Type) + } +} + +func TestSecretSecured(t *testing.T) { + secretBytes, err := hex.DecodeString("dd000102030405060708090a0b0c0d0e0f") + secret, err := NewSecret(secretBytes) + if err != nil { + t.Fatal(err) + } + if secret.Type != Secured { + t.Errorf("Wrong secret type expected %d found %d", Secured, secret.Type) + } +} + +func TestSecretFakeTls(t *testing.T) { + secretBytes, err := hex.DecodeString("ee000102030405060708090a0b0c0d0e0f676f6f676c652e636f6d") + secret, err := NewSecret(secretBytes) + if err != nil { + t.Fatal(err) + } + if secret.Type != FakeTLS { + t.Errorf("Wrong secret type expected %d found %d", FakeTLS, secret.Type) + } + if secret.Fakehost != "google.com" { + t.Errorf("Wrong fakehost expected %s found %s", "google.com", secret.Fakehost) + } +} + +func TeestSecretError(t *testing.T) { + secretBytes, err := hex.DecodeString("dd") + _, err = NewSecret(secretBytes) + if err == nil { + t.Errorf("Wrong secret length passed %d", len(secretBytes)) + } +} diff --git a/tgcrypt.go b/tgcrypt.go new file mode 100644 index 0000000..c16ad46 --- /dev/null +++ b/tgcrypt.go @@ -0,0 +1,196 @@ +// Generate init packet +package main + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "crypto/sha256" + "encoding/hex" + "fmt" + "io" + "runtime" +) + +func decryptInit(packet [initialHeaderSize]byte) (decrypt [48]byte) { + k := 0 + for i := 55; i >= 8; i-- { + decrypt[k] = packet[i] + k++ + } + return +} + +// struct that handles encryption +type obfuscatedClientCtx struct { + header [initialHeaderSize]byte + secret *Secret + protocol uint8 + dc int16 + random [2]byte + fromClient cipher.Stream + toClient cipher.Stream +} + +func obfuscatedClientCtxFromHeader(header [initialHeaderSize]byte, secret *Secret) (c *obfuscatedClientCtx, err error) { + encKey := header[8:40] + encIV := header[40:56] + decReversed := decryptInit(header) + decKey := decReversed[:32] + decIV := decReversed[32:48] + secretData := secret.RawSecret[0:16] + hasher := sha256.New() + hasher.Write(encKey) + hasher.Write(secretData) + encKey = hasher.Sum(nil) + hasher.Reset() + hasher.Write(decKey) + hasher.Write(secretData) + decKey = hasher.Sum(nil) + hasher.Reset() + // encKey is used for receiving data bbecause abbreviations was taken from client specs + fromClientCipher, err := aes.NewCipher(encKey) + if err != nil { + return nil, err + } + fromClientStream := cipher.NewCTR(fromClientCipher, encIV) + toClientCipher, err := aes.NewCipher(decKey) + if err != nil { + return nil, err + } + toClientStream := cipher.NewCTR(toClientCipher, decIV) + // decrypt encrypted part of innitial packet + // basicaly you need to appy decrypt to all incoming packet and take last 8 bytes + buf := make([]byte, 64) + fromClientStream.XORKeyStream(buf, header[:]) + fmt.Printf("Decrypted tail %s\n", hex.EncodeToString(buf[56:])) + protocol := buf[56] + dc := int16(buf[60]) + (int16(buf[61]) << 8) + var random [2]byte + copy(random[:], buf[62:64]) + fmt.Printf("protocol: %x. DC %x\n", protocol, dc) + c = &obfuscatedClientCtx{ + header: header, + secret: secret, + fromClient: fromClientStream, + toClient: toClientStream, + protocol: protocol, + dc: dc, + random: random, + } + return +} + +func (c *obfuscatedClientCtx) decryptNext(buf []byte) { + c.fromClient.XORKeyStream(buf, buf) +} + +func (c *obfuscatedClientCtx) encryptNext(buf []byte) { + c.toClient.XORKeyStream(buf, buf) +} + +var wrongNonceStarters = [...][]byte{ + {0xef}, // abridged header + {0x44, 0x41, 0x45, 0x48}, //HEAD + {0x54, 0x53, 0x4f, 0x50}, //POST + {0x20, 0x54, 0x45, 0x47}, //GET + {0x49, 0x54, 0x50, 0x4f}, //OPTI + {0x02, 0x01, 0x03, 0x16}, // -----? + {0xdd, 0xdd, 0xdd, 0xdd}, // padded intermediate header + {0xee, 0xee, 0xee, 0xee}, // intermediate header +} + +type dcCtx struct { + nonce [initialHeaderSize]byte + toDc cipher.Stream + fromDc cipher.Stream +} + +func genHeader() (packet [initialHeaderSize]byte, err error) { + // init := (56 random bytes) + protocol + dc + (2 random bytes) + for { + _, err = rand.Read(packet[:]) + if err != nil { + return + } + runtime.Gosched() + for _, s := range wrongNonceStarters { + if bytes.Equal(packet[:len(s)], s) { + continue + } + } + if bytes.Equal(packet[4:8], []byte{0, 0, 0, 0}) { + continue + } + return + } +} + +func dcCtxFromClient(dc int, protocol byte) (c *dcCtx, err error) { + header, err := genHeader() + if err != nil { + return + } + header[56] = protocol + header[57] = protocol + header[58] = protocol + header[59] = protocol + encKey := header[8:40] + encIV := header[40:56] + decReversed := decryptInit(header) + decKey := decReversed[:32] + decIV := decReversed[32:48] + toDcCipher, err := aes.NewCipher(encKey) + if err != nil { + return + } + toDcStream := cipher.NewCTR(toDcCipher, encIV) + fromDcCipher, err := aes.NewCipher(decKey) + if err != nil { + return + } + fromDcStream := cipher.NewCTR(fromDcCipher, decIV) + var nonce [initialHeaderSize]byte + toDcStream.XORKeyStream(nonce[:], header[:]) + copy(nonce[:56], header[:56]) + c = &dcCtx{ + nonce: nonce, + toDc: toDcStream, + fromDc: fromDcStream, + } + return +} + +func (c *dcCtx) decryptNext(buf []byte) { + c.fromDc.XORKeyStream(buf, buf) +} + +func (c *dcCtx) encryptNext(buf []byte) { + c.toDc.XORKeyStream(buf, buf) +} + +type encDecCtx interface { + decryptNext(buf []byte) + encryptNext(buf []byte) +} + +type encDecStream struct { + encDec encDecCtx + stream io.ReadWriter +} + +func (ed *encDecStream) Read(buf []byte) (size int, err error) { + size, err = ed.stream.Read(buf) + ed.encDec.decryptNext(buf[:size]) + return +} + +func (ed *encDecStream) Write(data []byte) (size int, err error) { + //write should not transform data + buf := make([]byte, len(data)) + copy(buf, data) + size, err = ed.stream.Write(buf) + ed.encDec.encryptNext(buf[:size]) + return +}