Skip to main content

Documentation 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.

When 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:
OffsetSizeFieldDescriptionStatus
+0x00u8field_0Context selector: 0 → recv CounterLoad path; != 0 → send branchCONFIRMED @ 0x40116C
+0x01u8field_1block_b effective slice length for the ack vector appendCONFIRMED @ 0x404F96
+0x02u8field_2block_a effective slice length for ack vector; also the HMAC inner message length on block_bCONFIRMED @ 0x404FEA and 0x404569
+0x03u8[64]block_a64-byte HMAC/vector input blobCONFIRMED (handler read @ 0x005E3DAF)
+0x43u8[128]block_b128-byte HMAC message baseCONFIRMED (handler read @ 0x005E3DBE)
Wire total: 197 bytes (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 localProcessKeyPacket paramRole at 0x00401100
field_0 (+0x00)param_3Stored in DAT_023037E0; selects recv (0) vs send branch @ 0x40116C
field_1 (+0x01)param_6block_b slice lengthEBX in Crypto_KeyMaterialAppend @ 0x404F96
field_2 (+0x02)param_7HMAC inner length on block_b @ 0x404569; block_a append len @ 0x404FEA
block_a (+0x03)param_464-byte input blob (ack vector, not HMAC message ptr)
block_b (+0x43)param_5HMAC message base → ECX @ 0x40115C

Call Chain

1

PacketDispatcher @ 0x005F1E10

Compares the decrypted opcode against 0xA101. On match, dispatches to the key material handler.
2

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).
3

Connection_OnKeyMaterial @ 0x005A4D50

vtable +0x254 implementation. Re-orders arguments and calls Crypto_ProcessKeyPacket, then triggers the ack send and enables the game cipher.
4

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.
5

Crypto_CounterLoad @ 0x00404680

Receives a 16-byte counter source, applies the custom dword permute, performs AES-128 key expansion, and writes the result to ctx+0xF4.

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.
// ─────────────────────────────────────────────────────────────────────────────
// Phase A — PRNG fill + HMAC-SHA256  (common to both recv and send paths)
// ─────────────────────────────────────────────────────────────────────────────

uint8_t prng[128];   // stack esp+0x58 in 0x401100 frame
// CONFIRMED: Crypto_PRNGFill @ 0x00404610
// Uses a time-seeded LCPRNG (seed DAT_02303A8C); fills 128 bytes.
Crypto_PRNGFill(prng);

uint8_t digest[32];  // stack esp+0x38..0x57 (post-arg-pop alignment)
{
    // Pre-key: SHA256 of the 128-byte PRNG output
    // CONFIRMED @ 0x404434: data ptr [esp+0x11c], len 0x80
    uint8_t key0[32];
    SHA256(prng, 128, key0);   // SHA256 @ 0x404434–0x404496

    // HMAC-SHA256(key=key0, msg=block_b[0..field_2])
    // CONFIRMED @ 0x404569: [esp+0x134] with N=5 outstanding pushes = field_2
    HMAC_SHA256(
        key  = key0,           // 32 bytes
        msg  = block_b,        // wire body +0x43
        len  = (size_t)(uint8_t)field_2,   // CONFIRMED @ 0x404569
        out  = digest          // 32 bytes; EBX = stack+0x38 @ 0x404575–0x404579
    );
    // ipad: key XOR 0x36 on 64-byte block — CONFIRMED @ 0x4044F0–0x404504
    // opad: key XOR 0x5C on 64-byte block — CONFIRMED @ 0x40457E–0x4045E8
    // SHA-256 IV constants at 0x404456–0x40448E, 0x40451F–0x404557, 0x404593–0x4045CB
}

// ─────────────────────────────────────────────────────────────────────────────
// Phase B — Recv CounterLoad (field_0 == 0)
// Branch confirmed @ 0x40116C
// ─────────────────────────────────────────────────────────────────────────────

uint8_t counter_src[16];
memcpy(counter_src, digest + 0,  16);  // digest[0..15]
// lea ecx,[esp+0x38] @ 0x401179 (post-HMAC esp); no PRNG XOR applied

// Load counter into main recv ctx  (0x023037F0, counter VA = 0x023038E4)
Crypto_CounterLoad(counter_src, ctx_0x023037F0);   // eax=0x023037F0 @ 0x401174

// Load counter into secondary recv ctx  (0x02303940, counter VA = 0x02303A34)
Crypto_CounterLoad(counter_src, ctx_0x02303940);   // eax=0x02303940 @ 0x401182

// ─────────────────────────────────────────────────────────────────────────────
// key_seed — digest[16..31] distributed to globals @ 0x401190–0x4011EB
// CONFIRMED: stack esp+0x48..0x57 copied to 0x023027C0..0x023027CC + mirrors
// ─────────────────────────────────────────────────────────────────────────────

uint8_t key_seed[16];
memcpy(key_seed, digest + 16, 16);  // digest[16..31]
// Distributed to:
//   DAT_023027C0, DAT_023027C4, DAT_023027C8, DAT_023027CC  (key seed buffer)
//   and counter shadow copies @ 0x401190–0x4011EB
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 0x4011560x40115B. 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 @ 0x00404680
// Calling convention: __fastcall  (ECX = src uint32[4], EAX = dst ctx*)
// CONFIRMED @ 0x404693: ctx+0xF0 = 10  (AES-128 round count)
uint32_t __fastcall Crypto_CounterLoad(uint32_t *src, CryptoCtx *ctx) {
    if (src == NULL || ctx == NULL) return 0xFFFFFFFF;

    ctx->rounds = 10;  // ctx+0xF0 — AES-128

    // Custom dword permute (confirmed @ 0x40468D–0x40470C):
    // Each 32-bit word has its bytes rotated so that:
    //   byte[3] → byte[0], byte[2] → byte[1], byte[1] → byte[2], byte[0] → byte[3]
    // Equivalent to: t = rotr(x,8) & 0xFF00FF00 | rotl(x,8) & 0x00FF00FF
    for (int i = 0; i < 4; i++) {
        uint32_t x = src[i];
        uint32_t t = (x >> 8 & 0xFF00FF00u) | (x << 24)
                   | ((x & 0xFF00u) << 8)   | (x >> 24);
        ctx->counter_u32[i] = t;  // → ctx+0xF4
    }

    // AES-128 key expansion into ctx+0x100..
    // T-tables at 0x00790760; round constants at 0x00791F64
    AES_KeyExpand_inplace(ctx);  // writes round_keys[] into ctx
    return 0;
}
After 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 0x4011900x4011EB. These are later consumed by Crypto_DeriveSessionKeys @ 0x00401320:
Global AddressSizeSource
0x023027C0u32digest[16..19]
0x023027C4u32digest[20..23]
0x023027C8u32digest[24..27]
0x023027CCu32digest[28..31]
These globals are zeroed by 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:
// Crypto_KeyMaterialAppend @ 0x00404F90
// Uses std::vector-like blob
void Crypto_KeyMaterialAppend(uint8_t *block_b, uint8_t field_1,
                               uint8_t *block_a, uint8_t field_2,
                               uint8_t *prng,   uint8_t *out_vec) {
    // CONFIRMED @ 0x404F96: memcpy(block_b, field_1 bytes)
    memcpy(out_vec, block_b, field_1);
    // CONFIRMED @ 0x404FEA: memcpy(block_a, field_2 bytes)
    memcpy(out_vec + field_1, block_a, field_2);
}
// Crypto_OutputPack @ 0x00405050 packs the vector + prng[128] → 128-byte client output
// NetworkSendKeyFollowUp @ 0x005EC5A0 sends opcode 0x00A101 / 131 bytes
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.
The PRNG state at the exact moment Crypto_ProcessKeyPacket @ 0x00401100 executes depends on time() seed plus all prior PRNG calls in the session. To pin the exact initial counter at ctx+0xF4 without a live debugger, break at 0x401162 and capture the 128-byte PRNG buffer at [esp+0x58] before the HMAC call.
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

  1. Break @ 0x005E3D60 — log field_0, field_1, field_2, block_a[0..3], block_b[0..3].
  2. Break @ 0x401162 (pre-HMAC) — confirm ECX = block_b; [esp+4] = PRNG ptr; [esp+8] = (u32)field_2.
  3. Break @ 0x404569 — confirm EAX == (uint8_t)field_2 (inner HMAC update length).
  4. Break @ 0x40117D — dump 16 bytes at ECX; must equal digest[0..15] at EBX/stack+0x38 (pre-permute).
  5. Break @ 0x401740 after CounterLoad — dump 0x23038E4..0x23038F3 (16 bytes): initial CTR after permute.
  6. Compare against server Connection_InitStreamCrypto @ ps_game.exe 0x00464E60 counter copy.

Confidence Summary

ClaimStatus
195-byte body layout + handler → 0x401100 chainCONFIRMED
field_0 == 0CounterLoad0x23037F0+0xF4CONFIRMED (0x4011740x40118B)
field_1 / field_2 as independent slice lengthsCONFIRMED (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 (0x4045640x404570)
CounterLoad input = HMAC digest [0..15], no PRNG XORCONFIRMED (0x40115E / 0x401179)
Key-seed globals = HMAC digest [16..31]CONFIRMED (0x4011900x4011EB)
Dword permute formula inside Crypto_CounterLoadCONFIRMED (0x40468D0x40470C)
Server outbound 0xA101 counter end-to-endNot mapped — requires runtime capture

Cross-References

DocumentContents
Wire CryptoTCP envelope, cipher modes, AES-CTR context struct
0xA101 Body MapByte-accurate server build ↔ client parse map
Server Key Blobps_login.exe key table slot initialization

Build docs developers (and LLMs) love