KDX Protocol
Protocol Overview
Haxial KDX was a non-compatible Hotline clone developed between 2001 and 2005 by Australian-based company Haxial Software Pty. Ltd. It provides everything the Hotline protocol does along with its own array of features such as remote desktop administration, detailed file access, user account classes, custom user icons, multiple public chat rooms, IRC bridging, server categories and accounts on the tracker, and voice chat. Most notable is its extensive use of mixing known standard cryptographic hashes and encryption algorithms of the time (MD5, CRC32, Twofish, etc.) as well as some of its own proprietary algorithms. KDX's news system is a hybrid of Hotline's threaded news system and it's classic bulletin board-style news feed: a threaded news system where each category is its own bulletin board-style feed.
Algorithms
The following is a list of algorithms discovered in KDX. While I do my best to document and reimplement the algorithms, their purposes, until KDX is fully reimplemented, are only speculation and are subject to clarification as I progress in development. - Schala
Multipurpose
I found this recurring LCG-based XOR encryption algorithm used in various areas of the network code with varying constants passed to it.
use bytemuck::{cast_slice, cast_slice_mut};
pub enum CryptError {
Align(u8, usize), // expected, got
Length(usize, usize), // expected, got
}
pub fn data_crypt(input: &[u8], seed: u32, mul: u32, add: u32) -> Result<Vec<u8>, CryptError> {
if input.len() & 3 != 0 {
return Err(CryptError::Align(4, input.len() & 3));
}
let mut output = vec![0u8; input.len()];
let out_blocks: &mut [u32] = cast_slice_mut(&mut output[..]);
let in_blocks: &[u32] = cast_slice(input);
let mut seed = seed;
in_blocks
.iter()
.zip(out_blocks.iter_mut())
.for_each(|(&input, out)| {
*out = input ^ seed.to_be();
seed = seed.wrapping_mul(mul).wrapping_add(add);
});
Ok(output)
}
TCP (client/server based) packets
A C implementation of this can be found here while a Rust implementation can be found below.
- The function takes a 32-bit key, a data buffer, and its length in bytes
- The data length is converted from bytes to 32-bit words (dividing by 4)
- The data buffer is treated as an array of 32-bit integers
For each 32-bit word:
- The key is updated by left-shifting and adding a constant (0x4878)
- The data word is XORed with the key (after converting byte order if needed)
The final key value is returned
pub fn tcp_packet_crypt(key: u32, data: &[u8]) -> Result<Vec<u8>, CryptError> {
if data.len() & 3 != 0 {
return Err(CryptError::Align(4, data.len() & 3));
}
let mut output = vec![0u8; data.len()];
let out_blocks: &mut [u32] = cast_slice_mut(&mut output[..]);
let mut key = key;
let in_blocks: &[u32] = cast_slice(data);
in_blocks
.iter()
.zip(out_blocks.iter_mut())
.for_each(|(&input, out)| {
key = key.wrapping_shl(1).wrapping_add(0x4878); // 'Hx'
*out = input ^ key.to_be();
});
Ok(output)
}
UDP (tracker-based) packets
Tracker packet encryption has been observed to be encrypted with the aforementioned multipurpose LCG-XOR algorithm, but with these constants fed to it:
pub fn udp_packet_crypt(input: &[u8]) -> Result<Vec<u8>, CryptError> {
data_crypt(input, 0xA5A16C4A, 0x41D28485, 12843)
}