Files
ts3java/ts3-client/docs/myteamspeak/CRYPTO_RE.md
ericek111 a7aa122728 Sign in to myTeamSpeak and import what the account synchronises
The myTeamSpeak client protocol and its end-to-end encryption are
reverse-engineered (docs/myteamspeak): an HTTP POST of a framed protobuf,
a PBKDF2-HMAC-SHA512 login token and account key, an AES-GCM wrapped
item key and AES-CTR items with a SHA-512 trailer.

Options on desktop and Settings on Android gain a myTeamSpeak page: sign
in and out, and the account's bookmarks and identities, each picked for
import and marked when already here. The client stays signed in the way
the official one does, keeping the derived token and key rather than the
password; each read signs in afresh, and a sign-in the server no longer
takes signs out. Nothing is written to the account.

Items are the same Item_Data as the local TeamSpeak store, so they go
through the existing decoder and importer; a bookmark chosen alone brings
the identity it connects with, found by UUID or, as some name it, by
the identity's name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 01:07:17 +00:00

5.3 KiB

myTeamSpeak crypto — reverse-engineering progress & resume notes

Status: password-login and item crypto cracked (upload framing too); decryption implemented. The item is IV[16] || little-endian AES-CTR ciphertext || SHA512(plaintext); the old fixture was a capture truncated 37 bytes into its digest. See CRYPTO_RE_SALT.md section 0 for formulas, addresses, captured outputs, and the Java implementation. The notes below are retained as historical RE context.

Confirmed facts

  • TeamSpeak's teamcrypto provides ONLY: AES-256-GCM, AES-CTR, SHA-256, SHA-512, CTR-DRBG. No PBKDF2/scrypt/Argon2/HKDF of its own (those strings in the binary are from statically-linked OpenSSL, unused by the sync code). => everything is buildable from SHA-2 + AES-GCM, so BouncyCastle/JCA can replicate it once the recipe is known. Nothing exotic to port.
  • Sync is end-to-end encrypted. Local settings.db stores items DECRYPTED; only the wire blobs are encrypted.
  • cloud_sync_client/src/lib/Encryption.cpp asserts plain_hash.size() == teamcrypto::sha2::sha512_size (64) => a SHA-512 "plain_hash" is central. Item_Manager.cpp asserts !salt.empty() => items use a salt.
  • Login token is DETERMINISTIC (verified: same email+password → identical 48-byte base64 token twice). So it is a reproducible KDF, not randomized. 48 bytes out.

Ground-truth samples (test account; its credentials are not committed)

Kept in the session scratchpad; re-capture via the harness if needed. Two login pairs (email, password, 48-byte token) — determinism confirmed. One LoginSession with: user_public_key 32B, encrypted_user_private_key 112B, my_teamspeak_id 01 20 <32B>. One encrypted bookmark item_blob (274B, leading 0x11) whose DECRYPTED plaintext is known (from local settings.db): server.lixko.eu, nick "niger", uid +Tyg2JtxE8vRNZp+JiUBnmBh0MY=. Recovery key = 32B.

Ruled out (brute-forced against real samples)

  • Login token: NOT plain SHA-256/384/512, HMAC-*, PBKDF2, scrypt, Argon2 over any obvious email/password combo with common salts/peppers; NOT ~35 SHA-512 concat/HMAC/xor constructions tested against BOTH samples. => baked-in salt/pepper or non-obvious structure; must be read from code.
  • Item blob: NOT AES-256-GCM under {recovery key, sha256(rk), sha512(rk) halves} with nonce at offset 0/1, len 12/16, tag-last, and AAD ∈ {none, item_uuid, item_version, both, binary UUIDs, 0x11}. => item key is a derivation of the account data key (which is wrapped by the password-derived key and/or the recovery key), not the recovery key directly.

Located in the binary (arm64 re-android/.../libteamspeak_client.so; file offset == vaddr in .rodata/.text)

  • Encryption.cpp SHA-512 assert string: vaddr 0x1baed6; referenced by code at 0xa8c280; enclosing function starts at 0xa8c198 (stack 0x1e0). That function dispatches through a vtable (ldr x9,[x8,#0x18]/[x8,#0x48]; blr x9) — the SHA-512/AES-GCM are behind an interface object.
  • SHA-512 K-table @ vaddr 0x29f4d8; SHA-256 K-table @ 0x29f318; SHA-512 IV0 @ 0x241580.
  • Desktop x86-64 ts3client_linux_amd64: same assert string @ vaddr 0x2dcac1 (.rodata).
  • Login path entry: Java_..._AccountManager_setupSyncAccount @ arm64 0x8931a8 -> converts the 3 String args (email, password, device) -> calls Account_Manager_Impl vtable slot at [vtable+0x18].

Tools built (in session scratchpad)

  • disasm.py <lib> <vaddr> <len> — capstone arm64 disassembler, annotates adrp+add string loads and bl targets with .symtab/.dynsym names.
  • xref.py <lib> <string> / xrefaddr.py <lib> <vaddr> [window] — find code adrp(+add/ldr) xrefs. NOTE: capstone adrp op_str includes #; strip it when parsing (already fixed). ldr literal-pool (ldr xN, #imm PC-relative) xrefs are NOT yet handled — add this to find mbedtls SHA callers.

Next steps (pick one, both bounded)

  1. Static: from the SHA-512 K-table (0x29f4d8) find the compression fn (add literal-pool ldr xref handling), then its caller (sha512 one-shot wrapper), then THAT wrapper's callers — one is the login KDF (expected linear: reads password + a constant salt -> SHA-512 -> 48-byte token), another is Encryption. Read the login KDF to get the salt + truncation. Then read Encryption's item path (key derivation + GCM nonce/tag/AAD layout). Verify each against the ground-truth samples.
  2. Dynamic (faster to interpret): gdb the running desktop client (harness in headless-ui-testing memory). Anchor: find the Encryption fn in the x86-64 binary via lea-xref to the assert string @0x2dcac1, break there; trigger a login (client decrypts the bookmark) and read the AES key/nonce/ AAD + the SHA-512 inputs from registers/stack. ptrace needs sudo gdb (yama scope=1 here) or ptrace_scope=0. libcrypto.so.1.1 EVP_PBE_scrypt/PKCS5_PBKDF2_HMAC are breakable but NOT used by the login KDF (it's teamcrypto), so break on the located Encryption fn, not on OpenSSL.

Capture harness (working)

DNS/connect LD_PRELOAD shim redirecting *.myteamspeak.com -> local TLS relay (client trusts an added CA via SSL_CERT_FILE/SSL_CERT_DIR); relay must speak http/1.1 upstream. Drive the official client under Xvfb+xdotool (licence: scroll to end then accept; dismiss the promo webview; fresh profile = clear AccountData+ProtobufItems in settings.db). See headless-ui-testing.