Follow these steps to reverse the login packet aka hwid/anticheat. I had ChatGPT summarize it for me because I'm not the best at writing:
Reversing the HWID System: A Step-by-Step Guide
A walkthrough of how I reversed the hardware fingerprinting in a Themida-packed game client and built a spoofer from scratch.
Step 1: Find the Hook Point
The client is Themida-packed, so static analysis of the login flow is mostly a dead end. Instead, work backwards from the network layer. Trace from
ws2_32.send up the call
stack until you find the function that receives
plaintext packets before the game's encryption layer. In this case, that's
CNetClient::SendMsg — a method that
takes a client pointer, a data buffer, and a size.
This is the ideal hook point. Hooking lower (at the socket layer) means you'd have to reverse the Themida stateful cipher to decrypt, modify, and re-encrypt. Hooking here, you get clean plaintext
with a simple
[u16 size][u16 type][payload] header.
Step 2: Log Everything During Login
Hook SendMsg with Detours, log every outgoing packet's type and hex dump. During a login attempt you'll see two packets fire in sequence:
- A small ~34-byte packet (type 0x0421)
- A larger ~169-byte packet (type 0x041B)
These only appear at login time. Capture them from two different machines — one clean, one banned — and diff them.
Step 3: Decode the Wire Format
Both packets use protobuf encoding after the 4-byte header. You don't need
.proto files — just write a minimal varint + tag parser. The wire format is simple: each field
is a tag byte (field number + wire type) followed by either a varint or a length-delimited blob.
Packet 0x0421: Five varint fields. All differ between machines. These are hardware identifiers computed inside the Themida VM via direct syscalls (bypassing any user-mode hooks). You can't
intercept their collection, only replace the finished packet.
Packet 0x041B: Six fields — a string (account name), a 64-byte blob (password hash), a string (server name), a varint (timestamp), a varint (timestamp XOR'd with a constant), and a 64-byte
blob (field 6 — purpose unknown at this point).
Step 4: Experiment with Replacement
Try different spoofing strategies and observe the server's response:
Results:
Code:
What you replace | Server response
----------------------------------------------------|---------------------------------------------
Only the 0x0421 packet | Rejected — cross-check fails
Only field 6 of 0x041B with random bytes | ~75% "VM detected", ~25% passes
Both 0x0421 + field 6 with donor data | Accepted
Random values in 0x0421 varints | "Invalid client version"
The intermittent VM detection from random field 6 is the critical clue. It means field 6 is
not opaque — the server decrypts and inspects it. The ~75% failure rate suggests a flags field
where random bits trigger detection.
Step 5: Reverse the Field 6 Encryption
You know field 6 is exactly 64 bytes (block-aligned), and the server can decrypt it, so it must use a key derivable from other packet data. The obvious candidates are the timestamp (field 4) and
password hash (field 2) — the only other substantial data in the packet.
Search the binary for crypto constants. AES S-box values, MD5 init constants (
0x67452301, etc.), or known block cipher structures. Even inside Themida-virtualized code,
the crypto primitives often live in non-virtualized helper functions.
Through a combination of tracing the code path that builds 0x041B and examining cross-references to crypto functions, you can recover the key derivation:
Code:
key material = timestamp(4 bytes) + password(64 bytes) + timestamp(4 bytes) + constant(4 bytes)
key = IV = MD5(key material)
cipher = AES-128-CBC, no padding
The constant is a magic value found in the key derivation function.
Step 6: Decrypt and Map the Struct
Now decrypt field 6 from your captured packets using the derived key. You get 64 bytes of plaintext. Compare the decrypted blobs from your two machines side by side:
- Bytes 0-3 differ (some machine ID)
- Bytes 4-7 are identical (don't touch these — server validates them)
- Bytes 8-11 differ (your IP as a uint32)
- Bytes 12-15 are zero on both (the flags field — this is what triggers VM detection when randomized)
- Bytes 16-31 look like ASCII hex strings (MAC address)
- Bytes 32-47 are ASCII, null-padded (Windows username)
- Bytes 48-63 are ASCII, null-padded (computer name)
You can verify each field by changing one thing at a time on your test machine (rename the PC, change the MAC) and re-capturing.
Step 7: Implement the Spoofer
With the full picture, the implementation is straightforward:
- Hook SendMsg via Detours
- For 0x0421: replace the entire packet with captured bytes from the donor machine
- For 0x041B: parse the protobuf, keep fields 1-3 (your credentials), decrypt field 6, modify the hardware-identifying fields while keeping the flags field zeroed and the app hash
untouched, re-encrypt with the same key, splice the 64 bytes back in at the same offset
Because the struct is exactly 64 bytes (4 AES blocks), the ciphertext is the same size as the plaintext — the packet length never changes, so no protobuf length fields need updating.
Key Takeaways
- Hook high, not low. Intercepting plaintext at SendMsg eliminated the entire Themida cipher problem.
- The ~75% failure rate was the Rosetta Stone. It proved field 6 was structured, not opaque, and that a specific bit region controlled VM detection.
- Diff two known machines. Comparing clean vs. banned packet captures, field by field, maps the struct faster than static analysis.
- The server doesn't check timestamp freshness. Full replay of donor packets works, meaning you can capture once and reuse indefinitely.