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

69 lines
5.3 KiB
Markdown

# 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]].