This commit is contained in:
Jose Luis Montañes Ojados
2026-01-15 16:49:16 +01:00
commit 47b8173045
23 changed files with 2864 additions and 0 deletions

BIN
cmd/client/client.exe Normal file

Binary file not shown.

41
cmd/client/main.go Normal file
View File

@@ -0,0 +1,41 @@
package main
import (
"flag"
"log"
"os"
"os/signal"
"syscall"
"go-ts/internal/client"
)
func main() {
serverAddr := flag.String("server", "127.0.0.1:9987", "TeamSpeak 3 Server Address")
nickname := flag.String("nickname", "GoCient", "Nickname")
flag.Parse()
log.Printf("Starting TS3 Client...")
log.Printf("Server: %s", *serverAddr)
log.Printf("Nickname: %s", *nickname)
c := client.NewClient(*nickname)
errChan := make(chan error)
go func() {
if err := c.Connect(*serverAddr); err != nil {
errChan <- err
}
}()
// Wait for signals
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
select {
case err := <-errChan:
log.Fatalf("Client Error: %v", err)
case <-sigChan:
log.Println("Shutting down...")
}
}

332
cmd/fakeserver/main.go Normal file
View File

@@ -0,0 +1,332 @@
package main
import (
"bytes"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/sha1"
"crypto/sha256"
"crypto/sha512"
"encoding/asn1"
"encoding/base64"
"encoding/binary"
"encoding/hex"
"fmt"
"log"
"math/big"
"net"
"time"
"go-ts/pkg/protocol"
"filippo.io/edwards25519"
)
type ServerState struct {
Step int
A1 [16]byte
A2 [100]byte
Identity *ecdsa.PrivateKey
LicensePriv *edwards25519.Scalar
LicensePub *edwards25519.Point
LicenseBlock []byte
Alpha []byte
Beta []byte
SharedSecret []byte
SharedIV []byte
SharedMac [8]byte
}
func main() {
addr, _ := net.ResolveUDPAddr("udp", ":9988")
conn, _ := net.ListenUDP("udp", addr)
log.Println("FakeServer listening on :9988")
// 1. Generate Server Identity (P-256)
privKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
// 2. Generate Transport/License Key (Ed25519)
var seed [32]byte
rand.Read(seed[:])
lPriv, _ := new(edwards25519.Scalar).SetBytesWithClamping(seed[:])
lPub := new(edwards25519.Point).ScalarBaseMult(lPriv)
// 3. Create 'l' (License Block) - 32 random bytes for now
lData := make([]byte, 32)
rand.Read(lData)
state := &ServerState{
Step: 0,
Identity: privKey,
LicensePriv: lPriv,
LicensePub: lPub,
LicenseBlock: lData,
Alpha: make([]byte, 10),
Beta: make([]byte, 54),
}
rand.Read(state.Alpha)
rand.Read(state.Beta)
rand.Read(state.A2[:])
buf := make([]byte, 4096)
for {
n, rAddr, err := conn.ReadFromUDP(buf)
if err != nil {
continue
}
data := make([]byte, n)
copy(data, buf[:n])
handlePacket(conn, rAddr, data, state)
}
}
func handlePacket(conn *net.UDPConn, addr *net.UDPAddr, data []byte, s *ServerState) {
pkt, err := protocol.Decode(data, true)
if err != nil {
return
}
if pkt.Header.PacketType() == protocol.PacketTypeInit1 {
if len(pkt.Data) > 5 && pkt.Data[4] == 0x00 { // Step 0
log.Println("Recv Step 0, Sending Step 1")
sendStep1(conn, addr, s)
} else if len(pkt.Data) > 5 && pkt.Data[4] == 0x02 { // Step 2
log.Println("Recv Step 2, Sending Step 3")
sendStep3(conn, addr, s)
}
} else if pkt.Header.PacketType() == protocol.PacketTypeCommand {
decrypted, err := decryptHandshake(pkt)
if err == nil {
sStr := string(decrypted)
if len(sStr) > 8 && sStr[0:8] == "clientek" {
log.Printf("Recv clientek (Decrypted): %s", sStr)
if err := s.processClientEk(decrypted); err != nil {
log.Printf("Error processing clientek: %v", err)
} else {
log.Println("Shared Secret Derived. Waiting for clientinit.")
}
sendAck(conn, addr, pkt.Header.PacketID)
return
} else if len(sStr) > 12 && sStr[0:12] == "clientinitiv" {
log.Println("Recv clientinitiv. Sending initivexpand2...")
sendInitivexpand2(conn, addr, s)
return
}
}
if len(s.SharedSecret) > 0 {
s.decryptClientInit(pkt)
} else if err != nil && pkt.Header.FlagUnencrypted() == false {
// log.Printf("Decrypt failed: %v", err)
}
}
}
func decryptHandshake(pkt *protocol.Packet) ([]byte, error) {
key := protocol.HandshakeKey
nonce := protocol.HandshakeNonce
meta := make([]byte, 5)
binary.BigEndian.PutUint16(meta[0:2], pkt.Header.PacketID)
binary.BigEndian.PutUint16(meta[2:4], pkt.Header.ClientID)
meta[4] = pkt.Header.Type
return protocol.DecryptEAX(key, nonce, meta, pkt.Data, pkt.Header.MAC[:])
}
func sendStep1(conn *net.UDPConn, addr *net.UDPAddr, s *ServerState) {
buf := new(bytes.Buffer)
binary.Write(buf, binary.BigEndian, int32(time.Now().Unix()))
buf.WriteByte(0x01)
rand.Read(s.A1[:])
buf.Write(s.A1[:])
pkt := protocol.NewPacket(protocol.PacketTypeInit1, buf.Bytes())
pkt.Header.PacketID = 1
copy(pkt.Header.MAC[:], []byte("TS3INIT1"))
encoded, _ := pkt.Encode(false)
conn.WriteToUDP(encoded, addr)
}
func sendStep3(conn *net.UDPConn, addr *net.UDPAddr, s *ServerState) {
buf := new(bytes.Buffer)
binary.Write(buf, binary.BigEndian, int32(time.Now().Unix()))
buf.WriteByte(0x03)
x := make([]byte, 64)
rand.Read(x)
n := make([]byte, 64)
rand.Read(n)
buf.Write(x)
buf.Write(n)
binary.Write(buf, binary.BigEndian, uint32(0))
buf.Write(s.A2[:])
pkt := protocol.NewPacket(protocol.PacketTypeInit1, buf.Bytes())
pkt.Header.PacketID = 2
copy(pkt.Header.MAC[:], []byte("TS3INIT1"))
encoded, _ := pkt.Encode(false)
conn.WriteToUDP(encoded, addr)
}
func sendInitivexpand2(conn *net.UDPConn, addr *net.UDPAddr, s *ServerState) {
lStr := base64.StdEncoding.EncodeToString(s.LicenseBlock)
base64.StdEncoding.EncodeToString(s.Beta) // unused?
betaStr := base64.StdEncoding.EncodeToString(s.Beta)
// Encode Omega (P-256 Public Key)
pub := s.Identity.PublicKey
omegaBytes := elliptic.Marshal(pub.Curve, pub.X, pub.Y)
omegaStr := base64.StdEncoding.EncodeToString(omegaBytes)
// Create Proof: Sign(SHA256(lBytes)) with Identity
hash := sha256.Sum256(s.LicenseBlock)
r, sb, err := ecdsa.Sign(rand.Reader, s.Identity, hash[:])
if err != nil {
log.Printf("Signing failed: %v", err)
}
type ECDSASignature struct {
R, S *big.Int
}
sig := ECDSASignature{R: r, S: sb}
proofBytes, _ := asn1.Marshal(sig)
proofStr := base64.StdEncoding.EncodeToString(proofBytes)
// Send command
cmd := fmt.Sprintf("initivexpand2 l=%s beta=%s omega=%s ot=1 proof=%s tvd=C",
lStr, betaStr, omegaStr, proofStr)
pkt := protocol.NewPacket(protocol.PacketTypeCommand, []byte(cmd))
pkt.Header.PacketID = 3
// Encrypt with HandshakeKey
key := protocol.HandshakeKey
nonce := protocol.HandshakeNonce
// Meta S->C (3 bytes: PID(2)+Type(1))
meta := make([]byte, 3)
binary.BigEndian.PutUint16(meta[0:2], pkt.Header.PacketID)
meta[2] = pkt.Header.Type
encData, mac, _ := protocol.EncryptEAX(key, nonce, meta, pkt.Data)
pkt.Data = encData
copy(pkt.Header.MAC[:], mac)
encoded, _ := pkt.Encode(false)
conn.WriteToUDP(encoded, addr)
}
func sendAck(conn *net.UDPConn, addr *net.UDPAddr, pid uint16) {
pkt := protocol.NewPacket(protocol.PacketTypeAck, nil)
pkt.Header.PacketID = pid
// Encrypt ACK
key := protocol.HandshakeKey
nonce := protocol.HandshakeNonce
meta := make([]byte, 3)
binary.BigEndian.PutUint16(meta[0:2], pid)
meta[2] = pkt.Header.Type
encData, mac, _ := protocol.EncryptEAX(key, nonce, meta, pkt.Data)
pkt.Data = encData
copy(pkt.Header.MAC[:], mac)
encoded, _ := pkt.Encode(false)
conn.WriteToUDP(encoded, addr)
}
func (s *ServerState) deriveSharedSecret(clientEkPub []byte) error {
clientPoint, err := new(edwards25519.Point).SetBytes(clientEkPub)
if err != nil {
return fmt.Errorf("invalid client point: %v", err)
}
// Server negates client point
clientPoint.Negate(clientPoint)
sharedPoint := new(edwards25519.Point).ScalarMult(s.LicensePriv, clientPoint)
sharedBytes := sharedPoint.Bytes()
sharedBytes[31] ^= 0x80
hash := sha512.Sum512(sharedBytes)
s.SharedSecret = hash[:]
s.SharedIV = make([]byte, 64)
copy(s.SharedIV, s.SharedSecret)
for i := 0; i < 10; i++ {
s.SharedIV[i] ^= s.Alpha[i]
}
if len(s.Beta) >= 54 {
for i := 0; i < 54; i++ {
s.SharedIV[10+i] ^= s.Beta[i]
}
}
macHash := sha1.Sum(s.SharedIV)
copy(s.SharedMac[:], macHash[0:8])
log.Printf("Shared Secret Derived! IV: %s", hex.EncodeToString(s.SharedIV))
return nil
}
func (s *ServerState) processClientEk(data []byte) error {
str := string(data)
start := "ek="
idx := 0
for i := 0; i < len(str)-len(start); i++ {
if str[i:i+3] == start {
idx = i + 3
break
}
}
if idx == 0 {
return fmt.Errorf("no ek found")
}
end := idx
for i := idx; i < len(str); i++ {
if str[i] == ' ' {
end = i
break
}
}
ekStr := str[idx:end]
ekBytes, err := base64.StdEncoding.DecodeString(ekStr)
if err != nil {
return err
}
return s.deriveSharedSecret(ekBytes)
}
func (s *ServerState) decryptClientInit(pkt *protocol.Packet) {
crypto := &protocol.CryptoState{
SharedIV: s.SharedIV,
SharedMac: s.SharedMac[:],
GenerationID: 0,
}
key, nonce := crypto.GenerateKeyNonce(&pkt.Header, true) // isClientToServer
meta := make([]byte, 5)
binary.BigEndian.PutUint16(meta[0:2], pkt.Header.PacketID)
binary.BigEndian.PutUint16(meta[2:4], pkt.Header.ClientID)
meta[4] = pkt.Header.Type
dec, err := protocol.DecryptEAX(key, nonce, meta, pkt.Data, pkt.Header.MAC[:])
if err != nil {
log.Printf("Decryption Failed: %v", err)
return
}
log.Printf(">>> DECRYPTED CLIENTINIT <<<\n%s\n", string(dec))
}

138
cmd/proxy/main_design.go Normal file
View File

@@ -0,0 +1,138 @@
package main
import (
"flag"
"log"
"net"
"strings"
)
// TS3 Proxy to capture clientinit parameters
func main() {
var localAddr string
var serverAddr string
flag.StringVar(&localAddr, "listen", ":9988", "Local listen address")
flag.StringVar(&serverAddr, "server", "localhost:9987", "Real TeamSpeak server address")
flag.Parse()
// Resolve addresses
localUDP, err := net.ResolveUDPAddr("udp", localAddr)
if err != nil {
log.Fatalf("Invalid local address: %v", err)
}
serverUDP, err := net.ResolveUDPAddr("udp", serverAddr)
if err != nil {
log.Fatalf("Invalid server address: %v", err)
}
// Listen on local port
conn, err := net.ListenUDP("udp", localUDP)
if err != nil {
log.Fatalf("Failed to listen: %v", err)
}
defer conn.Close()
log.Printf("Proxy listening on %s, forwarding to %s", localAddr, serverAddr)
log.Println("Connect your Real TeamSpeak Client to 'localhost:9988' to capture credentials.")
// Map client addresses to server connections (for multiple clients support, simplifying for 1)
// Simple forwarder: We only expect one client for this task.
var clientAddr *net.UDPAddr
buf := make([]byte, 2048)
for {
n, addr, err := conn.ReadFromUDP(buf)
if err != nil {
log.Printf("Read error: %v", err)
continue
}
// Check if packet is from client or server
if clientAddr == nil || addr.String() != clientAddr.String() {
// New client or client packet
// If it's from our known server (unlikely as we don't bind to serverAddr), it's a client.
// But wait, we are listening on localUDP.
// We need to dial the server to forward.
// We'll use a separate socket to talk to the server to keep track of sessions?
// Simplest UDP proxy:
// 1. Receive from Client -> Send to Server (using a dialer)
// 2. Receive from Server -> Send to Client
// Let's reset for new clients to keep it simple
clientAddr = addr
handleClientPacket(conn, serverUDP, buf[:n], addr)
} else {
// Known client
handleClientPacket(conn, serverUDP, buf[:n], addr)
}
}
}
// We need a persistent connection to the server to receive responses
// In a simple loop, we can't easily run two blocking reads (from client and from server) without goroutines.
// Let's structure:
// Main loop reads from Client (ListenUDP).
// On first packet, start a Goroutine that Dials the Server.
// That goroutine reads from Server and writes to Client.
// Main loop writes to Server using that connection.
var serverConn *net.UDPConn
func handleClientPacket(clientConn *net.UDPConn, serverAddr *net.UDPAddr, data []byte, clientAddr *net.UDPAddr) {
// Inspect Packet for clientinit strings (Naive text search)
// clientinit is encrypted in standard flow?
// WAIT. If we proxy, the client and server negotiate encryption.
// We CANNOT decrypt the traffic unless we perform a MitM attack on the Diffie-Hellman handshake.
// TS3 uses ECDH. We cannot derive the shared secret just by passively observing.
// We would need to:
// 1. Intercept `initivexpand2` (Step 2/3/4?)
// 2. Replace the Server's Public Key with OUR Public Key.
// 3. Negotiate SharedSecret1 with Client.
// 4. Negotiate SharedSecret2 with Server (using our Private Key).
// 5. Decrypt Client packet -> Re-encrypt for Server.
// This is significantly complex.
// BUT, `clientinit` might be sent *plaintext* in some legacy modes?
// No, the user's issue is specifically about the encrypted handshake.
// However, `client_version` is sometimes sent in cleartext in the INIT1 packet?
// No, INIT1 is just 4 timestamps + random bytes.
// WAIT. The very first packet is valid?
// `clientinit` is sent AFTER the handshake (Step 6). It is definitely encrypted.
// If the user connects with a real client, the client calculates the SharedSecret using the REAL server's key.
// We verify the packets pass through.
// WE CANNOT READ THE PAYLOAD without a full MitM (replacing keys).
// IS THERE ANOTHER WAY?
// Does the client send version in clear text anywhere?
// Maybe in the UserAgent? (UDP doesn't have headers like HTTP)
// Proposal: Implement Full MitM.
// We already have the ECDH logic implemented in Go!
// We can act as the "Server" to the Client, and "Client" to the Server.
// 1. Client connects to Proxy. Proxy acts as Server.
// 2. Proxy sends its OWN Identity/Key to Client.
// 3. Proxy connects to Real Server.
// 4. Proxy completes handshake with Real Server.
// 5. Client completes handshake with Proxy.
// 6. Proxy decrypts Client's `clientinit`, LOGS IT, then re-encrypts for Server.
// This is the chosen path. It reuses our `handshake.go` and `license.go` logic.
inspectPacket(data)
// Forwarding logic placeholder (Passive won't work for decryption)
}
func inspectPacket(data []byte) {
// Check for "clientinit" ASCII pattern
s := string(data)
if strings.Contains(s, "clientinit") {
log.Printf("[!] FOUND POSSIBLE CLIENTINIT (Plaintext?): %s", s)
}
}