WhenDocumentation Index
Fetch the complete documentation index at: https://mintlify.com/ThalissonTMora/shaiya-chat-native-re/llms.txt
Use this file to discover all available pages before exploring further.
Game.exe receives the 0xA101 key material packet from ps_login.exe, it does not directly copy any wire bytes into the AES counter. Instead, a three-phase pipeline — PRNG fill → SHA256 → HMAC-SHA256 — produces a 32-byte digest. The first 16 bytes become the counter seed for Crypto_CounterLoad @ 0x00404680, which applies a custom dword permute and an AES-128 key expansion before writing the result to ctx+0xF4. This page documents the full closed formula for the recv path (field_0 == 0), the Ghidra-confirmed call chain, and the items that still require a live runtime capture to pin exactly.
All evidence is from Game.exe (MD5 c1edd96639ad81835624b9c4516ac781, ImageBase 0x00400000).
0xA101 Body Layout
The 195-byte body begins immediately after the u16 opcode. Five PacketRead calls in Handler_Packet_A101_KeyMaterial @ 0x005E3D60 parse it sequentially:
| Offset | Size | Field | Description | Status |
|---|---|---|---|---|
+0x00 | u8 | field_0 | Context selector: 0 → recv CounterLoad path; != 0 → send branch | CONFIRMED @ 0x40116C |
+0x01 | u8 | field_1 | block_b effective slice length for the ack vector append | CONFIRMED @ 0x404F96 |
+0x02 | u8 | field_2 | block_a effective slice length for ack vector; also the HMAC inner message length on block_b | CONFIRMED @ 0x404FEA and 0x404569 |
+0x03 | u8[64] | block_a | 64-byte HMAC/vector input blob | CONFIRMED (handler read @ 0x005E3DAF) |
+0x43 | u8[128] | block_b | 128-byte HMAC message base | CONFIRMED (handler read @ 0x005E3DBE) |
u16 opcode + 195-byte body).
field_1 and field_2 are independent slice controls. HMAC uses field_2 as the inner update length on block_b. The ack vector uses field_1 on block_b and field_2 on block_a. They are not aliases of the same length value.Handler Argument Mapping
Connection_OnKeyMaterial @ 0x005A4D50 re-orders the handler locals before calling Crypto_ProcessKeyPacket @ 0x00401100. The shuffle is confirmed by disassembly of the vtable call site at 0x00747798:
| Handler local | ProcessKeyPacket param | Role at 0x00401100 |
|---|---|---|
field_0 (+0x00) | param_3 | Stored in DAT_023037E0; selects recv (0) vs send branch @ 0x40116C |
field_1 (+0x01) | param_6 | block_b slice length — EBX in Crypto_KeyMaterialAppend @ 0x404F96 |
field_2 (+0x02) | param_7 | HMAC inner length on block_b @ 0x404569; block_a append len @ 0x404FEA |
block_a (+0x03) | param_4 | 64-byte input blob (ack vector, not HMAC message ptr) |
block_b (+0x43) | param_5 | HMAC message base → ECX @ 0x40115C |
Call Chain
PacketDispatcher @ 0x005F1E10
Compares the decrypted opcode against
0xA101. On match, dispatches to the key material handler.Handler_Packet_A101_KeyMaterial @ 0x005E3D60
Reads the 195-byte body with five sequential
PacketRead calls. Passes field_0, field_1, field_2, block_a, block_b to the vtable slot at conn+0x254 (VA 0x00747798).Connection_OnKeyMaterial @ 0x005A4D50
vtable
+0x254 implementation. Re-orders arguments and calls Crypto_ProcessKeyPacket, then triggers the ack send and enables the game cipher.Crypto_ProcessKeyPacket @ 0x00401100
Full HMAC/expand pipeline. Performs PRNG fill, SHA256, HMAC-SHA256, then branches on
field_0 to either the recv CounterLoad path or the send path.Closed Formula — Recv Path (field_0 == 0)
The derivation runs entirely inside Crypto_ProcessKeyPacket @ 0x00401100 calling through Crypto_HMAC_Derive @ 0x00404420. The stack layout comments reference the actual Ghidra-identified esp-relative addresses.
CounterLoad input is digest[0..15] only — no PRNG XOR. The apparent
stack+0x40 vs stack+0x38 discrepancy seen in some Ghidra frames is caused by the two push instructions before call 0x00404420 at 0x401156–0x40115B. After the add esp, 8 at 0x401169, lea ecx,[esp+0x38] at 0x401179 correctly addresses the start of the digest.Crypto_CounterLoad @ 0x00404680 Internals
Crypto_CounterLoad takes a 16-byte counter source (ECX) and a context pointer (EAX). It applies a custom dword byte-swap permute before writing the counter, then runs a full AES-128 key expansion into the context’s round-key storage.
Crypto_CounterLoad, the initial counter at ctx+0xF4 is the permuted digest bytes, ready to be consumed by PacketStream_XOR @ 0x00404DF0.
Key-Seed Global Assignments
After the HMAC finalize,digest[16..31] is copied to the key-seed globals at 0x401190–0x4011EB. These are later consumed by Crypto_DeriveSessionKeys @ 0x00401320:
| Global Address | Size | Source |
|---|---|---|
0x023027C0 | u32 | digest[16..19] |
0x023027C4 | u32 | digest[20..23] |
0x023027C8 | u32 | digest[24..27] |
0x023027CC | u32 | digest[28..31] |
Crypto_DeriveSessionKeys after the session key expansion completes, preventing key material from persisting in memory.
Send Path (field_0 != 0)
When field_0 != 0, Crypto_ProcessKeyPacket skips the direct CounterLoad calls. Instead, the full 32-byte digest block is copied to the alt-context addresses 0x02303908 and 0x02303A58, and Crypto_CounterAdvance @ 0x00401500 (LCG mutation on ctx+0xC) is called to advance the counter state. This path is triggered by NetworkSendKeyBlob @ 0x005EC610 during the client → server follow-up send.
Ack Vector — Crypto_KeyMaterialAppend @ 0x00404F90
The 131-byte follow-up 0xA101 sent back to the server is assembled by appending slices of the received blocks:
field_1 controls how many bytes of block_b are echoed back; field_2 controls how many bytes of block_a are appended. These are independent of each other and independent of the HMAC inner length also indexed by field_2.
PRNG Seeding Notes
Crypto_PRNGFill @ 0x00404610 uses a time-seeded linear congruential PRNG whose state is stored at DAT_02303A8C. Because the PRNG state evolves continuously during the game session, the exact 128-byte PRNG output — and therefore the exact initial counter — cannot be pre-computed offline without knowing the PRNG state at the moment 0x00401100 is called.
Offline verification tool: tools/crypto/validate_a101_counter.py --prng-hex <hex> — accepts a captured PRNG buffer (via --prng-hex), block_b, and field_2 value, re-runs the HMAC-SHA256 pipeline, applies the Crypto_CounterLoad permute, and compares against a dumped ctx+0xF4 snapshot.
Reproduction Checklist
- Break
@ 0x005E3D60— logfield_0,field_1,field_2,block_a[0..3],block_b[0..3]. - Break
@ 0x401162(pre-HMAC) — confirmECX=block_b;[esp+4]= PRNG ptr;[esp+8]=(u32)field_2. - Break
@ 0x404569— confirmEAX == (uint8_t)field_2(inner HMAC update length). - Break
@ 0x40117D— dump 16 bytes atECX; must equaldigest[0..15]atEBX/stack+0x38(pre-permute). - Break
@ 0x401740afterCounterLoad— dump0x23038E4..0x23038F3(16 bytes): initial CTR after permute. - Compare against server
Connection_InitStreamCrypto@ps_game.exe0x00464E60counter copy.
Confidence Summary
| Claim | Status |
|---|---|
195-byte body layout + handler → 0x401100 chain | CONFIRMED |
field_0 == 0 → CounterLoad → 0x23037F0+0xF4 | CONFIRMED (0x401174–0x40118B) |
field_1 / field_2 as independent slice lengths | CONFIRMED (0x404F96, 0x404FEA) |
HMAC pre-key = SHA256(PRNG[128]), not SHA256(block_b) | CONFIRMED (0x404434, len 0x80) |
HMAC inner message = block_b[0..field_2] | CONFIRMED (0x404564–0x404570) |
CounterLoad input = HMAC digest [0..15], no PRNG XOR | CONFIRMED (0x40115E / 0x401179) |
Key-seed globals = HMAC digest [16..31] | CONFIRMED (0x401190–0x4011EB) |
Dword permute formula inside Crypto_CounterLoad | CONFIRMED (0x40468D–0x40470C) |
Server outbound 0xA101 counter end-to-end | Not mapped — requires runtime capture |
Cross-References
| Document | Contents |
|---|---|
| Wire Crypto | TCP envelope, cipher modes, AES-CTR context struct |
| 0xA101 Body Map | Byte-accurate server build ↔ client parse map |
| Server Key Blob | ps_login.exe key table slot initialization |