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.
| Field | Size | Rule |
|---|
wire_len | 2 bytes (u16 LE) | Not encrypted. Value = plaintext_size + 2 (includes its own 2 bytes). |
ciphertext | wire_len − 2 bytes | Stream cipher (AES-CTR game mode) or Login XOR, depending on cipher mode globals. |
Client Send — NetworkSend @ 0x005EA9A0
- Build a local stack buffer:
[u16 wire_len][plaintext opcode + body].
- Call
PacketPayload_Encrypt @ 0x00401040 — encrypts the plaintext slice starting at buffer offset +2 in-place.
- Call
send() to transmit wire_len + 2 bytes total over the socket.
Client Recv — NetworkRecv_SocketPump @ 0x005F438E
- Read
wire_len from the socket; allocate wire_len − 2 bytes.
- Call
PacketPayload_Decrypt @ 0x00401080 — decrypts the ciphertext buffer in-place.
- Hand the plaintext to
PacketDispatcher, which reads the u16 opcode at plaintext offset +0.
Server Recv — Recv_Wrapper @ 0x004748E0
- If
SConnection+0x57F0 != 0: call PacketStream_XOR @ 0x004E4180 on (payload+2, len−2) before dispatch. Decryption happens before CUser_DispatchPacket_Main sees the packet.
- Plaintext opcode is at
packet+2; body begins at packet+4.
Server Send — Connection_EncryptOutbound @ 0x00464F20
Two mutually exclusive cipher paths per connection:
- AES path — if
conn+0x231 != 0: call PacketStream_XOR in-place on the outbound buffer (plaintext → ciphertext).
- 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:
| Global | Address | Role |
|---|
DAT_023037e8 | 0x023037E8 | Login gate — != 0 → enter login XOR branch |
DAT_023037e4 | 0x023037E4 | Login submode — == 2 → use table XOR path |
DAT_023037ea | 0x023037EA | Game cipher on — enables AES-CTR after 0xA101 handshake |
DAT_023037e9 | 0x023037E9 | Encrypt send — outbound encrypt gate |
DAT_023037e0 | 0x023037E0 | Context selector — 0 → 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)
| Offset | Role |
|---|
+0x230 | Recv stream on → Connection_DecryptInbound @ 0x00464FA0 called on inbound data |
+0x231 | Send AES stream → Connection_EncryptOutbound uses PacketStream_XOR path |
+0x232 | Send login XOR → static table 0x005868C0 + index (mirror of client login mode) |
+0x118 | Crypto 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
| Role | Client VA | Server VA | T-table Base |
|---|
| Keystream block | AES_BlockKeystream 0x00404830 | AES_BlockKeystream 0x004E3450 | 0x00567D00 (client) |
| Key schedule | (via SHA256 → expand in Crypto_CounterLoad) | AES_KeyExpand 0x004E4350 | — |
| Counter load/init | Crypto_CounterLoad 0x00404680 | Crypto_CounterInit 0x004E4000 | copies 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.
| Offset | Field | Description |
|---|
+0xF4 | counter[0..3] | 128-bit LE nonce/counter; incremented by 1 (uint128) after each 16-byte block |
+0x104 | partial | Bytes already consumed from current keystream block (0..15) |
+0x108 | keystream[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 (Te0–Te3) 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
| Step | Actor | Direction | Packet |
|---|
| 1 | ps_login.exe | → Client | 0xA101 (197 bytes): key blob with block_a[64] + block_b[128] |
| 2 | Game.exe | → Server | 0xA101 follow-up (131 bytes): ack vector built by NetworkSendKeyFollowUp @ 0x5EC5A0 |
| 3 | ps_login.exe | → Client | 0xA102 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)
| VA | Name | Action |
|---|
0x005E3D60 | Handler_Packet_A101_KeyMaterial | Recv handler; reads 195 B body; dispatches via vtable +0x254 |
0x005A4D50 | Connection_OnKeyMaterial | vtable +0x254 impl; orchestrates crypto setup + ack send |
0x00401100 | Crypto_ProcessKeyPacket | HMAC/expand pipeline; populates 0x23027C0 and AES context |
0x00401320 | Crypto_DeriveSessionKeys | SHA256 over 0x23027C0; distributes to counter globals |
0x00401310 | Crypto_EnableGameCipher | Sets DAT_023037e9 = 1, DAT_023037ea = 1 |
0x00404680 | Crypto_CounterLoad | Loads 16 B counter into ctx with dword permute + AES-128 key expansion |
0x005A4D30 | Connection_SendKeyAck | Sends 0xA102 ack packet |
Client → Server Key Ack (0xA102)
Built by NetworkSendKeyBlob @ 0x005EC610 and sent still under login XOR encryption:
| Offset | Size | Field |
|---|
+0 | 2 | opcode = 0x00A102 (LE) |
+2 | 2 | 0x0032 (50) — sub-field / length marker |
+4 | 34 | Payload: conn+0x27C (18 B) + conn+0x29F (16 B) |
Client Global Context Map
| Address | Use |
|---|
0x023037F0 | Main recv context (StreamCrypt_XOR) |
0x02303908 | Alt send context (StreamCrypt_AltCtx) |
0x02303A58 | Secondary recv context |
0x023027C0 | Key seed buffer (cleared after Crypto_DeriveSessionKeys) |
0x023027E0 | Login XOR table (4096 bytes) |
Server Mirror — ps_game.exe Zone Crypto
Zone-side stream crypto initialization is separate from the 0xA101 login path:
| VA | Name | Action |
|---|
0x00464E60 | Connection_InitStreamCrypto | AES_KeyExpand on 16 B key; copy 16 B → conn+0x118 ctx +0xF4; set conn+0x230=1, conn+0x231=1 |
0x00464F00 | Connection_EnableSendCipher | conn+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
| Evidence | Detail | Confidence |
|---|
Server copies [struct+0x10..0x1F] → ctx+0xF4..0x100 | Connection_InitStreamCrypto direct copy | High |
Client counter at 0x23037F0+0xF4 comes from 0x401100 HMAC/PRNG path | Not a fixed offset in the 0xA101 wire body | High |
| Exact byte-for-byte formula without runtime capture | End-to-end mapping of PRNG state to initial counter | Not 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
| Document | Contents |
|---|
| Crypto Counter | 0xA101 → ctx+0xF4, HMAC-SHA256 derivation |
| 0xA101 Body Map | Byte-accurate server build ↔ client parse map |
| Server Key Blob | ps_login.exe SendKeyBlob_A101 outbound chain |