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.

Shaiya Core V9 encrypts every TCP packet payload after the first two bytes. The wire format is a plain-text wire_len prefix followed by a stream-ciphered body. Two cipher families are in play: a login XOR mode active during the initial handshake, and an AES-128 CTR-like stream cipher that takes over after the 0xA101 key exchange completes. This document maps every call site, global flag, context-struct offset, and handshake step confirmed by cross-binary disassembly of Game.exe (MD5 c1edd96639ad81835624b9c4516ac781) and ps_game.exe (MD5 91b212afbe6623382713772489dc82ce), both at ImageBase 0x00400000.

TCP Envelope

Every Shaiya TCP message is framed as a two-byte little-endian length followed by the encrypted body. The wire_len field is never encrypted — it is written and read in the clear so the receiver can allocate the right buffer before decryption begins.
FieldSizeRule
wire_len2 bytes (u16 LE)Not encrypted. Value = plaintext_size + 2 (includes its own 2 bytes).
ciphertextwire_len − 2 bytesStream cipher (AES-CTR game mode) or Login XOR, depending on cipher mode globals.

Client Send — NetworkSend @ 0x005EA9A0

  1. Build a local stack buffer: [u16 wire_len][plaintext opcode + body].
  2. Call PacketPayload_Encrypt @ 0x00401040 — encrypts the plaintext slice starting at buffer offset +2 in-place.
  3. Call send() to transmit wire_len + 2 bytes total over the socket.

Client Recv — NetworkRecv_SocketPump @ 0x005F438E

  1. Read wire_len from the socket; allocate wire_len − 2 bytes.
  2. Call PacketPayload_Decrypt @ 0x00401080 — decrypts the ciphertext buffer in-place.
  3. Hand the plaintext to PacketDispatcher, which reads the u16 opcode at plaintext offset +0.

Server Recv — Recv_Wrapper @ 0x004748E0

  1. If SConnection+0x57F0 != 0: call PacketStream_XOR @ 0x004E4180 on (payload+2, len−2) before dispatch. Decryption happens before CUser_DispatchPacket_Main sees the packet.
  2. Plaintext opcode is at packet+2; body begins at packet+4.

Server Send — Connection_EncryptOutbound @ 0x00464F20

Two mutually exclusive cipher paths per connection:
  1. AES path — if conn+0x231 != 0: call PacketStream_XOR in-place on the outbound buffer (plaintext → ciphertext).
  2. Login XOR path — if conn+0x232 != 0: XOR with the static login table at 0x005868C0 + index, mirroring the client login mode.

Cipher Modes

Client Globals

The client dispatcher reads five global bytes to decide which cipher to apply. All live in the .data segment:
GlobalAddressRole
DAT_023037e80x023037E8Login gate!= 0 → enter login XOR branch
DAT_023037e40x023037E4Login submode== 2 → use table XOR path
DAT_023037ea0x023037EAGame cipher on — enables AES-CTR after 0xA101 handshake
DAT_023037e90x023037E9Encrypt send — outbound encrypt gate
DAT_023037e00x023037E0Context selector0 → recv ctx @ 0x23037F0; != 0 → alt send ctx @ 0x2303908

PacketPayload_Decrypt @ 0x00401080 — Pseudocode

// Dispatcher for both login and game cipher modes
void PacketPayload_Decrypt(uint8_t *buf, size_t len) {
    if (DAT_023037e8 /* login_gate */ != 0) {
        if (DAT_023037e4 /* login_submode */ == 2) {
            Login_XORStream(buf, len);      // 0x00401000
        }
    } else if (DAT_023037ea /* game_cipher */ != 0) {
        if (DAT_023037e0 /* ctx_sel */ == 0) {
            StreamCrypt_XOR(0x23037F0, buf, len);   // 0x00404DF0 — main recv ctx
        } else {
            StreamCrypt_AltCtx(0x2303908, buf, len); // 0x00401640 — alt send ctx
        }
    }
}
PacketPayload_Encrypt @ 0x00401040 is a mirror of the above, gated on DAT_023037e9 instead.

Login XOR — Login_XORStream @ 0x00401000

A simple byte-at-a-time XOR stream using a 4096-byte static table:
  • Table base: 0x0023027E0 — 4096 bytes, pre-populated at startup.
  • Index wrap: index &= 0xFFF after each byte (keeps index in [0, 4095]).
  • Operation: cipher[i] = plain[i] ^ table[index++].
  • Active only while DAT_023037e8 != 0 and DAT_023037e4 == 2.

Server Per-Connection Flags (SConnection / CUser blob)

OffsetRole
+0x230Recv stream on → Connection_DecryptInbound @ 0x00464FA0 called on inbound data
+0x231Send AES stream → Connection_EncryptOutbound uses PacketStream_XOR path
+0x232Send login XOR → static table 0x005868C0 + index (mirror of client login mode)
+0x118Crypto context pointer/inline — layout mirrors client 0x23037F0 (see Context Struct below)

AES-128 CTR Cipher

The game-mode stream cipher is an AES-128 ECB on a counter block to produce a keystream, which is then XORed with the data. Because XOR is self-inverse, the same routine handles both encrypt and decrypt — PacketStream_XOR is called for both directions.
CONFIRMED — layout offsets +0xF4 / +0x104 / +0x108 are verified by decompiled source plus disassembly. AES-128 identity is high confidence from T-table constant matching. Exact CTR vs CFB mode is medium-high based on observed symmetric behavior.

AES Core Function Table

RoleClient VAServer VAT-table Base
Keystream blockAES_BlockKeystream 0x00404830AES_BlockKeystream 0x004E34500x00567D00 (client)
Key schedule(via SHA256 → expand in Crypto_CounterLoad)AES_KeyExpand 0x004E4350
Counter load/initCrypto_CounterLoad 0x00404680Crypto_CounterInit 0x004E4000copies 16 B → ctx+0xF4

Context Struct Offsets

Offsets are relative to the context pointer (esi / this). The client main recv context lives at 0x023037F0; the server context is at conn+0x118.
OffsetFieldDescription
+0xF4counter[0..3]128-bit LE nonce/counter; incremented by 1 (uint128) after each 16-byte block
+0x104partialBytes already consumed from current keystream block (0..15)
+0x108keystream[16]Output of last AES_BlockKeystream(counter) call
+0x??round_keys[]AES-128 key schedule — 44 uint32_t words written by Crypto_CounterLoad
The server context additionally stores ctx+0xF0 = 10 (AES-128 round count), confirmed at 0x00404693.

PacketStream_XOR Loop @ 0x004E4180

The client implementation at 0x00404DF0 is structurally identical. Both iterate the keystream in 16-byte blocks, handling a partial leading block if ctx+0x104 > 0, then full 16-byte blocks, then a final partial tail.
// PacketStream_XOR — symmetric encrypt/decrypt via AES-CTR keystream
// Server: 0x004E4180 | Client: 0x00404DF0
void PacketStream_XOR(CryptoCtx *ctx, uint8_t *buf, size_t len) {
    uint32_t partial = ctx->partial;  // ctx+0x104: bytes used from current block

    // 1. Drain any leftover keystream from a previous partial block
    if (partial > 0) {
        while (partial < 16 && len > 0) {
            *buf++ ^= ctx->keystream[partial++];  // ctx+0x108
            len--;
        }
        if (partial == 16) partial = 0;
        ctx->partial = partial;
        if (len == 0) return;
    }

    // 2. Full 16-byte blocks
    while (len >= 16) {
        AES_BlockKeystream(ctx->keystream, ctx);  // ECB-encrypt counter → keystream
        // Increment 128-bit LE counter at ctx+0xF4
        if (++ctx->counter[0] == 0)
            if (++ctx->counter[1] == 0)
                if (++ctx->counter[2] == 0)
                    ++ctx->counter[3];
        for (int i = 0; i < 16; i++) buf[i] ^= ctx->keystream[i];
        buf += 16;
        len -= 16;
    }

    // 3. Partial trailing block
    if (len > 0) {
        AES_BlockKeystream(ctx->keystream, ctx);
        if (++ctx->counter[0] == 0)
            if (++ctx->counter[1] == 0)
                if (++ctx->counter[2] == 0)
                    ++ctx->counter[3];
        for (size_t i = 0; i < len; i++) buf[i] ^= ctx->keystream[i];
        ctx->partial = (uint32_t)len;
    }
}

AES_BlockKeystream Internals @ 0x00404830

The block function applies full AES-128 ECB encryption of the current counter block using four T-tables (Te0Te3) rooted at 0x00567D00 in the client binary. The counter dwords are byte-swapped before entry (rotr/rotl permute), matching the custom endianness applied by Crypto_CounterLoad.

Handshake Flow — 0xA101 / 0xA102

The crypto handshake is initiated by the login server and completes with the client activating AES-CTR mode. All steps below are CONFIRMED by disassembly of Game.exe.

Who Sends What

StepActorDirectionPacket
1ps_login.exe→ Client0xA101 (197 bytes): key blob with block_a[64] + block_b[128]
2Game.exe→ Server0xA101 follow-up (131 bytes): ack vector built by NetworkSendKeyFollowUp @ 0x5EC5A0
3ps_login.exe→ Client0xA102 login ack
The outbound 0xA101 from ps_login.exe is built by SendKeyBlob_A101 @ 0x00404DA0. It is not present in ps_game.exe — zone-side stream crypto initialization uses a separate path through Connection_InitStreamCrypto @ 0x00464E60. See Server Key Blob for the full ps_login.exe send chain.

Client Recv Dispatch Chain

NetworkRecv_SocketPump @ 0x005F438E
  └─ PacketPayload_Decrypt @ 0x00401080
  └─ PacketDispatcher @ 0x005F1E10      (cmp opcode == 0xA101)
       └─ Handler_Packet_A101_KeyMaterial @ 0x005E3D60
            reads body: 3× u8 scalar + u8[64] block_a + u8[128] block_b
            └─ [conn vtable + 0x254] → VA 0x00747798
                 └─ Connection_OnKeyMaterial @ 0x005A4D50
                      ├─ Crypto_ProcessKeyPacket @ 0x00401100   (HMAC/expand pipeline)
                      ├─ NetworkSendKeyFollowUp @ 0x005EC5A0   (send 131-byte ack)
                      └─ Crypto_EnableGameCipher @ 0x00401310  (flip AES-CTR globals)

Crypto_EnableGameCipher @ 0x00401310

This single function activates the AES-CTR cipher for both directions:
// Crypto_EnableGameCipher @ 0x00401310
void Crypto_EnableGameCipher(void) {
    DAT_023037e9 = 1;  // enable outbound encrypt
    DAT_023037ea = 1;  // enable inbound decrypt (game cipher on)
}
After this call, all subsequent packets use the AES-CTR stream seeded by the 0xA101 key material. The login XOR gate (DAT_023037e8) remains in whatever state it was left — the game cipher check short-circuits the login branch.

Crypto_DeriveSessionKeys @ 0x00401320

Called from the login tick at 0x50C767 after conn+0x224 is set. Runs SHA256 @ 0x00404390 over the key seed buffer at 0x023027C0, then distributes the digest to counter addresses and clears the seed buffer:
// Crypto_DeriveSessionKeys @ 0x00401320 (abbreviated from decompiled source)
void Crypto_DeriveSessionKeys(void) {
    uint32_t digest[4];
    SHA256(&DAT_023027C0, digest);  // 0x00404390 over 0x23027C0

    if (DAT_023037e0 == 0) {
        // recv path: write directly to counter slots
        DAT_023038E4 = digest[0];  DAT_023038E8 = digest[1];
        DAT_023038EC = digest[2];  DAT_023038F0 = digest[3];
        DAT_023038F4 = 0;          // partial = 0
        DAT_02303A34 = digest[0];  DAT_02303A38 = digest[1];
        DAT_02303A3C = digest[2];  DAT_02303A40 = digest[3];
        DAT_02303A44 = 0;
    } else {
        // send path: distribute to alt ctx + advance counter
        memcpy((void*)0x2303908, digest, 32);
        Crypto_CounterAdvance();   // 0x00401500 — LCG mutate
        memcpy((void*)0x2303A58, digest, 32);
        DAT_02303938 = 0;
        Crypto_CounterAdvance();
        DAT_02303A88 = 0;
    }

    // Always clear key seed buffer after derive
    DAT_023027C0 = DAT_023027C4 = DAT_023027C8 = DAT_023027CC = 0;
    DAT_023027D0 = DAT_023027D4 = DAT_023027D8 = DAT_023027DC = 0;
}

Key Material Enable Path (VA Table)

VANameAction
0x005E3D60Handler_Packet_A101_KeyMaterialRecv handler; reads 195 B body; dispatches via vtable +0x254
0x005A4D50Connection_OnKeyMaterialvtable +0x254 impl; orchestrates crypto setup + ack send
0x00401100Crypto_ProcessKeyPacketHMAC/expand pipeline; populates 0x23027C0 and AES context
0x00401320Crypto_DeriveSessionKeysSHA256 over 0x23027C0; distributes to counter globals
0x00401310Crypto_EnableGameCipherSets DAT_023037e9 = 1, DAT_023037ea = 1
0x00404680Crypto_CounterLoadLoads 16 B counter into ctx with dword permute + AES-128 key expansion
0x005A4D30Connection_SendKeyAckSends 0xA102 ack packet

Client → Server Key Ack (0xA102)

Built by NetworkSendKeyBlob @ 0x005EC610 and sent still under login XOR encryption:
OffsetSizeField
+02opcode = 0x00A102 (LE)
+220x0032 (50) — sub-field / length marker
+434Payload: conn+0x27C (18 B) + conn+0x29F (16 B)

Client Global Context Map

AddressUse
0x023037F0Main recv context (StreamCrypt_XOR)
0x02303908Alt send context (StreamCrypt_AltCtx)
0x02303A58Secondary recv context
0x023027C0Key seed buffer (cleared after Crypto_DeriveSessionKeys)
0x023027E0Login XOR table (4096 bytes)

Server Mirror — ps_game.exe Zone Crypto

Zone-side stream crypto initialization is separate from the 0xA101 login path:
VANameAction
0x00464E60Connection_InitStreamCryptoAES_KeyExpand on 16 B key; copy 16 B → conn+0x118 ctx +0xF4; set conn+0x230=1, conn+0x231=1
0x00464F00Connection_EnableSendCipherconn+0x231 = 1; clears login XOR send flag conn+0x232
0x00413CB5(inside ZoneCryptoInit @ 0x413B30)Calls 0x464E60 after copying 8×u32 key material; parent called from 0x406C1B / 0x406C52
Connection_InitStreamCrypto at 0x00464E60 is called from ZoneCryptoInit @ 0x413B30 only. It is not called from Handler_Packet1201 @ 0x47FCC0 — that handler only invokes 0x453CB0 for zone-name processing.

Initial Counter (IV) — Confidence Summary

EvidenceDetailConfidence
Server copies [struct+0x10..0x1F]ctx+0xF4..0x100Connection_InitStreamCrypto direct copyHigh
Client counter at 0x23037F0+0xF4 comes from 0x401100 HMAC/PRNG pathNot a fixed offset in the 0xA101 wire bodyHigh
Exact byte-for-byte formula without runtime captureEnd-to-end mapping of PRNG state to initial counterNot mapped
The precise mapping from the 0xA101 wire body to the initial counter value at ctx+0xF4 requires a live PRNG capture — see Crypto Counter for the derivation steps and known uncertainties.

Cross-References

DocumentContents
Crypto Counter0xA101ctx+0xF4, HMAC-SHA256 derivation
0xA101 Body MapByte-accurate server build ↔ client parse map
Server Key Blobps_login.exe SendKeyBlob_A101 outbound chain

Build docs developers (and LLMs) love