Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Sequence and XOR Transformation

Most binary packets carry a sequence byte and use three XOR passes over the payload plus a trailing zero. The message indexes call this Encrypted: Yes for protocol familiarity. It is obfuscation rather than authenticated encryption: there is no integrity tag, message authentication code, or cryptographic security boundary.

Wire placement

The transformation applies inside the standard 0xAA frame:

Encrypted:     AA [uint16 body_length, big-endian] [action] [sequence] [XOR(payload || 00)]
Not encrypted: AA [uint16 body_length, big-endian] [action] [payload] [00]

body_length counts the action and every subsequent body byte. It excludes the marker and the two-byte length. The action remains visible in both forms. Only the payload and trailing zero enter the XOR passes.

These actions bypass the sequence and XOR code:

DirectionEncrypted: No
Client to server0x00, 0x10, 0x48
Server to client0x00, 0x03, 0x40

All other fixed actions in the current indexes use the encrypted branch.

Runtime state and defaults

StateDefaultBehavior
Repeating keyNine ASCII bytes NexonInc.Repeats across the transformed data.
Table function0Produces the identity table, where the generated byte equals input_value.
Client send sequence0x00Inserted after the action and incremented modulo 256 after each encrypted client packet.

Clear client packets do not consume the client send sequence. On receive, the decoder uses the sequence carried by the server packet. No independent receive counter or monotonic-sequence check appears in the decoded server path.

Server-controlled key and table

The server can replace both transformation inputs during the clear SVersionCheck action 0x00. Subtype 0x00 has this payload prefix:

Payload offsetWidthFieldBehavior
0x001subtypeConstant 0x00 for transformation negotiation.
0x011server_versionStored and compared with the active server version.
0x021table_functionQueues regeneration with function index 0 through 9.
0x031key_lengthThe main-menu handler requires exactly 9.
0x04key_lengthkeyCopied and queued as the new repeating key.

ui_main_menu_handle_server_packet queues the table update through net_queue_xor_table_function and the key update through net_queue_xor_key. Socket work codes 11 and 10 apply them in net_process_work_item. The general setter accepts a key length no greater than nine, but this server-negotiation path asserts that the supplied length equals nine.

Because action 0x00 is not encrypted, these values can establish new state without depending on the previous key or table.

Client encoding

The encoder produces:

[action] [packet_sequence] [encoded payload and trailing zero]

It copies packet_sequence to the output, increments the stored counter modulo 256, and transforms payload_and_zero in this order:

void encode_packet_data(
    const uint8_t *payload_and_zero,
    size_t data_length,
    const uint8_t *repeating_key,
    size_t key_length,
    const uint8_t *xor_table,
    uint8_t packet_sequence,
    uint8_t *encoded_data)
{
    size_t byte_index;
    size_t block_index;
    size_t key_index;
    size_t data_index;
    uint8_t block_number;

    for (byte_index = 0; byte_index < data_length; ++byte_index) {
        encoded_data[byte_index] =
            payload_and_zero[byte_index] ^
            repeating_key[byte_index % key_length];
    }

    for (block_index = 0;
         block_index * key_length < data_length;
         ++block_index) {
        block_number = (uint8_t)block_index;
        if (block_number == packet_sequence)
            continue;

        for (key_index = 0; key_index < key_length; ++key_index) {
            data_index = block_index * key_length + key_index;
            if (data_index >= data_length)
                break;

            encoded_data[data_index] ^=
                xor_table[block_number + key_index];
        }
    }

    for (byte_index = 0; byte_index < data_length; ++byte_index) {
        encoded_data[byte_index] ^=
            xor_table[packet_sequence + byte_index];
    }
}

The first pass uses the repeating nine-byte key. The second applies table bytes by key-sized block, except when the block number equals the packet sequence. The third applies table bytes indexed from the sequence.

Server decoding

net_s_decode_packet_body leaves the action in place, reads the following sequence byte, and reverses the XOR passes in the opposite order:

  1. XOR with table bytes indexed from the received sequence.
  2. XOR each key-sized block with its table bytes, skipping the block whose byte index equals the received sequence.
  3. XOR with the repeating key.
  4. Remove the sequence byte from the application-facing packet.

The decoder returns one fewer byte than the wire body because the sequence is removed. It also writes an additional zero immediately after the returned bytes.

Trailing zero behavior

net_c_queue_send appends a zero before client encoding, so the trailing zero is transformed with the payload. The server is expected to provide the corresponding byte on server packets.

The 4.21 decoder does not verify that this byte becomes zero. It neither rejects the packet nor reports a transformation failure when the decoded value is nonzero. It keeps that byte in the returned logical packet and writes another local zero guard after it. A proxy or compatible server can use the decoded value as a diagnostic, but treating it as a required integrity check would be stricter than this client.

Table generators

net_build_xor_table accepts function indexes 0 through 9. For input_value from 0 through 255, it generates the following byte values. Function 9 first calculates centered_value as ((int32_t)input_value - 128) / 8, using signed C integer division toward zero.

FunctionC expression for the generated byte
0input_value
1128 + (((input_value & 1) == 0) ? ((input_value + 1) / 2) : -((input_value + 1) / 2))
2255 - input_value
3128 + (((input_value & 1) == 0) ? ((255 - input_value) / 2) : -((255 - input_value) / 2))
4(input_value / 16) * (input_value / 16)
5(2 * input_value) & 0xFF
6255 - ((2 * input_value) & 0xFF)
7(input_value <= 127) ? (255 - 2 * input_value) : (2 * input_value - 256)
8(input_value <= 127) ? (2 * input_value) : (511 - 2 * input_value)
9255 - ((centered_value * centered_value) & 0xFF)

The image starts with function 0. Each generated byte is stored in all four bytes of one dword, allowing the optimized paths to XOR four data bytes at once.

The additive table indexes in the encoder and decoder do not have an explicit modulo operation or bounds check after addition. Compatible implementations should reproduce the observed indexing before adding stricter validation.

Function reference

AddressCurrent IDA namePrototypePurpose
Darkages.exe:0x0045F780ui_main_menu_handle_server_packetint __thiscall(void *this, void *event)Parse clear action 0x00, including the table selector and negotiated key.
Darkages.exe:0x004A36C0net_queue_xor_keyvoid __thiscall(void *this, unsigned int key_length, const uint8_t *key)Copy and queue a key update as Socket work code 10.
Darkages.exe:0x004A3700net_queue_xor_table_functionvoid __thiscall(void *this, unsigned int function_index)Queue table regeneration as Socket work code 11.
Darkages.exe:0x004A44D4net_s_decode_packet_bodyint __stdcall(const uint8_t *src, int src_size, uint8_t *dest)Decode a sequence-bearing server body.
Darkages.exe:0x004A54D0net_process_work_itemvoid __thiscall(void *this, int code, void *data, int value)Apply queued key and table updates on work codes 10 and 11.
Darkages.exe:0x004A79E4net_c_encode_packet_bodyint __stdcall(const uint8_t *src, int src_size, uint8_t *dest)Insert the client sequence and encode the payload and trailing zero.
Darkages.exe:0x004A8414net_set_xor_keyvoid __stdcall(int length, const uint8_t *key)Install and repeat the active key.
Darkages.exe:0x004A8590net_build_xor_tablevoid __stdcall(int function_index)Generate the table with one of ten functions.