1
Fork 0
mirror of https://github.com/redstrate/Kawari.git synced 2025-07-09 15:37:45 +00:00

Begin correctly implementing packet obsfucation

I re-implemented Unscrambler, but in reverse! This currently only
affects names in the PlayerSpawn packet, it needs to be extended
into others to be considered complete.

See #9
This commit is contained in:
Joshua Goins 2025-07-03 16:12:19 -04:00
parent 8894e25da4
commit fb46a44e18
17 changed files with 271 additions and 53 deletions

View file

@ -78,6 +78,24 @@ fn main() {
output_str.push_str("}\n\n");
output_str.push_str("}\n\n");
// opcodes
output_str.push_str("/// Returns the integer opcode.\n");
output_str.push_str("pub fn get_opcode(&self) -> u16 {\n");
output_str.push_str("match self {\n");
for opcode in opcodes {
let opcode = opcode.as_object().unwrap();
let name = opcode.get("name").unwrap().as_str().unwrap();
let opcode = opcode.get("opcode").unwrap().as_number().unwrap();
output_str.push_str(&format!("{key}::{name} => {opcode},\n"));
}
output_str.push_str(&format!("{key}::Unknown(opcode) => *opcode,\n"));
output_str.push_str("}\n\n");
output_str.push_str("}\n\n");
// end impl
output_str.push_str("}\n\n");
}

View file

@ -20,7 +20,9 @@ use kawari::packet::oodle::OodleNetwork;
use kawari::packet::{
ConnectionType, PacketSegment, PacketState, SegmentData, SegmentType, send_keep_alive,
};
use kawari::world::{ChatHandler, ExtraLuaState, LuaZone, Zone, ZoneConnection, load_init_script};
use kawari::world::{
ChatHandler, ExtraLuaState, LuaZone, ObsfucationData, Zone, ZoneConnection, load_init_script,
};
use kawari::world::{
ClientHandle, Event, FromServer, LuaPlayer, PlayerData, ServerHandle, StatusEffects, ToServer,
WorldDatabase, handle_custom_ipc, server_main_loop,
@ -1099,6 +1101,7 @@ async fn main() {
last_keep_alive: Instant::now(),
gracefully_logged_out: false,
weather_id: 0,
obsfucation_data: ObsfucationData::default(),
});
}
Some((mut socket, _)) = handle_rcon(&rcon_listener) => {

View file

@ -2,20 +2,23 @@ use binrw::binrw;
use crate::{
opcodes::ServerChatIpcType,
packet::{IpcSegment, ReadWriteIpcSegment},
packet::{IPC_HEADER_SIZE, IpcSegment, ReadWriteIpcSegment},
};
pub type ServerChatIpcSegment = IpcSegment<ServerChatIpcType, ServerChatIpcData>;
impl ReadWriteIpcSegment for ServerChatIpcSegment {
fn calc_size(&self) -> u32 {
// 16 is the size of the IPC header
16 + self.op_code.calc_size()
IPC_HEADER_SIZE + self.op_code.calc_size()
}
fn get_name(&self) -> &'static str {
self.op_code.get_name()
}
fn get_opcode(&self) -> u16 {
self.op_code.get_opcode()
}
}
// TODO: make generic

View file

@ -3,34 +3,38 @@ use binrw::binrw;
use crate::{
common::{CHAR_NAME_MAX_LENGTH, read_bool_from, read_string, write_bool_as, write_string},
ipc::lobby::CharacterDetails,
packet::{IpcSegment, ReadWriteIpcSegment},
packet::{IPC_HEADER_SIZE, IpcSegment, ReadWriteIpcSegment},
};
pub type CustomIpcSegment = IpcSegment<CustomIpcType, CustomIpcData>;
impl ReadWriteIpcSegment for CustomIpcSegment {
fn calc_size(&self) -> u32 {
// 16 is the size of the IPC header
16 + match self.op_code {
CustomIpcType::RequestCreateCharacter => 1024 + CHAR_NAME_MAX_LENGTH as u32,
CustomIpcType::CharacterCreated => 12,
CustomIpcType::GetActorId => 8,
CustomIpcType::ActorIdFound => 4,
CustomIpcType::CheckNameIsAvailable => CHAR_NAME_MAX_LENGTH as u32,
CustomIpcType::NameIsAvailableResponse => 1,
CustomIpcType::RequestCharacterList => 4,
CustomIpcType::RequestCharacterListRepsonse => 1 + (1184 * 8),
CustomIpcType::DeleteCharacter => 8,
CustomIpcType::CharacterDeleted => 1,
CustomIpcType::ImportCharacter => 132,
CustomIpcType::RemakeCharacter => 1024 + 8,
CustomIpcType::CharacterRemade => 8,
}
IPC_HEADER_SIZE
+ match self.op_code {
CustomIpcType::RequestCreateCharacter => 1024 + CHAR_NAME_MAX_LENGTH as u32,
CustomIpcType::CharacterCreated => 12,
CustomIpcType::GetActorId => 8,
CustomIpcType::ActorIdFound => 4,
CustomIpcType::CheckNameIsAvailable => CHAR_NAME_MAX_LENGTH as u32,
CustomIpcType::NameIsAvailableResponse => 1,
CustomIpcType::RequestCharacterList => 4,
CustomIpcType::RequestCharacterListRepsonse => 1 + (1184 * 8),
CustomIpcType::DeleteCharacter => 8,
CustomIpcType::CharacterDeleted => 1,
CustomIpcType::ImportCharacter => 132,
CustomIpcType::RemakeCharacter => 1024 + 8,
CustomIpcType::CharacterRemade => 8,
}
}
fn get_name(&self) -> &'static str {
""
}
fn get_opcode(&self) -> u16 {
todo!()
}
}
impl Default for CustomIpcSegment {

View file

@ -15,20 +15,23 @@ pub use login_reply::{LoginReply, ServiceAccount};
use crate::{
common::{read_string, write_string},
opcodes::{ClientLobbyIpcType, ServerLobbyIpcType},
packet::{IpcSegment, ReadWriteIpcSegment},
packet::{IPC_HEADER_SIZE, IpcSegment, ReadWriteIpcSegment},
};
pub type ClientLobbyIpcSegment = IpcSegment<ClientLobbyIpcType, ClientLobbyIpcData>;
impl ReadWriteIpcSegment for ClientLobbyIpcSegment {
fn calc_size(&self) -> u32 {
// 16 is the size of the IPC header
16 + self.op_code.calc_size()
IPC_HEADER_SIZE + self.op_code.calc_size()
}
fn get_name(&self) -> &'static str {
self.op_code.get_name()
}
fn get_opcode(&self) -> u16 {
self.op_code.get_opcode()
}
}
// TODO: make generic
@ -53,13 +56,16 @@ pub type ServerLobbyIpcSegment = IpcSegment<ServerLobbyIpcType, ServerLobbyIpcDa
impl ReadWriteIpcSegment for ServerLobbyIpcSegment {
fn calc_size(&self) -> u32 {
// 16 is the size of the IPC header
16 + self.op_code.calc_size()
IPC_HEADER_SIZE + self.op_code.calc_size()
}
fn get_name(&self) -> &'static str {
self.op_code.get_name()
}
fn get_opcode(&self) -> u16 {
self.op_code.get_opcode()
}
}
// TODO: make generic

View file

@ -17,8 +17,12 @@ pub struct InitZone {
/// Zero means "no obsfucation" (not really, but functionally yes.)
/// To enable obsfucation, you need to set this to a constant that changes every patch. See lib.rs for the constant.
pub obsfucation_mode: u8,
pub unk1: u8,
pub unk2: u32,
/// First seed used in deobsfucation on the client side.
pub seed1: u8,
/// Second seed used in deobsfucation on the client side.
pub seed2: u8,
/// Third seed used in deobsfucation on the client size.
pub seed3: u32,
pub festival_id: u16,
pub additional_festival_id: u16,
pub unk3: u32,
@ -26,7 +30,7 @@ pub struct InitZone {
pub unk5: u32,
pub unk6: [u32; 4],
pub unk7: [u32; 3],
pub unk8_9: [u8; 9],
pub unk8_9: [u8; 8],
pub position: Position,
pub unk8: [u32; 4],
pub unk9: u32,

View file

@ -94,6 +94,7 @@ use crate::common::read_string;
use crate::common::write_string;
use crate::opcodes::ClientZoneIpcType;
use crate::opcodes::ServerZoneIpcType;
use crate::packet::IPC_HEADER_SIZE;
use crate::packet::IpcSegment;
use crate::packet::ReadWriteIpcSegment;
@ -101,13 +102,16 @@ pub type ClientZoneIpcSegment = IpcSegment<ClientZoneIpcType, ClientZoneIpcData>
impl ReadWriteIpcSegment for ClientZoneIpcSegment {
fn calc_size(&self) -> u32 {
// 16 is the size of the IPC header
16 + self.op_code.calc_size()
IPC_HEADER_SIZE + self.op_code.calc_size()
}
fn get_name(&self) -> &'static str {
self.op_code.get_name()
}
fn get_opcode(&self) -> u16 {
self.op_code.get_opcode()
}
}
// TODO: make generic
@ -128,13 +132,16 @@ pub type ServerZoneIpcSegment = IpcSegment<ServerZoneIpcType, ServerZoneIpcData>
impl ReadWriteIpcSegment for ServerZoneIpcSegment {
fn calc_size(&self) -> u32 {
// 16 is the size of the IPC header
16 + self.op_code.calc_size()
IPC_HEADER_SIZE + self.op_code.calc_size()
}
fn get_name(&self) -> &'static str {
self.op_code.get_name()
}
fn get_opcode(&self) -> u16 {
self.op_code.get_opcode()
}
}
// TODO: make generic

View file

@ -66,9 +66,6 @@ pub fn get_supported_expac_versions() -> HashMap<&'static str, Version<'static>>
HashMap::from(SUPPORTED_EXPAC_VERSIONS)
}
/// Constant to enable packet obsfucation. Changes every patch.
pub const OBFUSCATION_ENABLED_MODE: u8 = 41;
/// The size of the unlock bitmask.
pub const UNLOCK_BITMASK_SIZE: usize = 92;

View file

@ -54,6 +54,7 @@ impl LobbyConnection {
ConnectionType::Lobby,
CompressionType::Uncompressed,
&[segment],
None,
)
.await;
}
@ -166,6 +167,7 @@ impl LobbyConnection {
ConnectionType::Lobby,
CompressionType::Uncompressed,
&packets,
None,
)
.await;
@ -560,6 +562,7 @@ pub async fn send_custom_world_packet(segment: CustomIpcSegment) -> Option<Custo
ConnectionType::None,
CompressionType::Uncompressed,
&[segment],
None,
)
.await;

View file

@ -6,9 +6,10 @@ use binrw::{BinRead, BinResult};
use crate::{
config::get_config,
packet::{PacketHeader, PacketSegment},
world::ScramblerKeys,
};
use super::{PacketState, ReadWriteIpcSegment, oodle::OodleNetwork};
use super::{IPC_HEADER_SIZE, PacketState, ReadWriteIpcSegment, SegmentData, oodle::OodleNetwork};
#[binrw]
#[brw(repr = u8)]
@ -77,18 +78,44 @@ pub(crate) fn compress<T: ReadWriteIpcSegment>(
state: &mut PacketState,
compression_type: &CompressionType,
segments: &[PacketSegment<T>],
keys: Option<&ScramblerKeys>,
) -> (Vec<u8>, usize) {
let mut segments_buffer = Cursor::new(Vec::new());
let mut segments_buffer = Vec::new();
for segment in segments {
segment
.write_le_args(
&mut segments_buffer,
(state.client_key.as_ref().map(|s: &[u8; 16]| s.as_slice()),),
)
.unwrap();
let mut buffer = Vec::new();
// write to buffer
{
let mut cursor = Cursor::new(&mut buffer);
segment
.write_le_args(
&mut cursor,
(state.client_key.as_ref().map(|s: &[u8; 16]| s.as_slice()),),
)
.unwrap();
}
// obsfucate if needed
if let Some(keys) = keys {
if let SegmentData::Ipc { data } = &segment.data {
let opcode = data.get_opcode();
let base_key = keys.get_base_key(opcode);
if data.get_name() == "PlayerSpawn" {
let name_offset = 610;
for i in 0..32 {
buffer[(IPC_HEADER_SIZE + name_offset + i) as usize] = buffer
[(IPC_HEADER_SIZE + name_offset + i) as usize]
.wrapping_add(base_key);
}
}
}
}
segments_buffer.append(&mut buffer);
}
let segments_buffer = segments_buffer.into_inner();
let segments_buffer_len = segments_buffer.len();
match compression_type {

View file

@ -10,6 +10,9 @@ pub trait ReadWriteIpcSegment:
/// Returns a human-readable name of the opcode.
fn get_name(&self) -> &'static str;
/// Returns the integer opcode.
fn get_opcode(&self) -> u16;
}
/// An IPC packet segment.
@ -60,3 +63,5 @@ where
#[br(args(&op_code, size))]
pub data: Data,
}
pub const IPC_HEADER_SIZE: u32 = 16;

View file

@ -11,7 +11,7 @@ mod encryption;
pub use encryption::generate_encryption_key;
mod ipc;
pub use ipc::{IpcSegment, ReadWriteIpcSegment};
pub use ipc::{IPC_HEADER_SIZE, IpcSegment, ReadWriteIpcSegment};
/// Bindings for Oodle network compression.
pub mod oodle;

View file

@ -3,7 +3,7 @@ use std::io::Cursor;
use binrw::BinWrite;
use tokio::{io::AsyncWriteExt, net::TcpStream};
use crate::common::timestamp_msecs;
use crate::{common::timestamp_msecs, world::ScramblerKeys};
use super::{
CompressionType, ConnectionType, PacketHeader, PacketSegment, PacketState, ReadWriteIpcSegment,
@ -16,8 +16,9 @@ pub async fn send_packet<T: ReadWriteIpcSegment>(
connection_type: ConnectionType,
compression_type: CompressionType,
segments: &[PacketSegment<T>],
keys: Option<&ScramblerKeys>,
) {
let (data, uncompressed_size) = compress(state, &compression_type, segments);
let (data, uncompressed_size) = compress(state, &compression_type, segments, keys);
let size = std::mem::size_of::<PacketHeader>() + data.len();
let header = PacketHeader {
@ -61,6 +62,7 @@ pub async fn send_keep_alive<T: ReadWriteIpcSegment>(
connection_type,
CompressionType::Uncompressed,
&[response_packet],
None,
)
.await;
}

View file

@ -9,7 +9,7 @@ use mlua::Function;
use tokio::net::TcpStream;
use crate::{
COMPLETED_QUEST_BITMASK_SIZE, OBFUSCATION_ENABLED_MODE,
COMPLETED_QUEST_BITMASK_SIZE,
common::{
GameData, ObjectId, ObjectTypeId, Position, timestamp_secs, value_to_flag_byte_index_value,
},
@ -34,8 +34,8 @@ use crate::{
};
use super::{
Actor, CharacterData, EffectsBuilder, Event, LuaPlayer, StatusEffects, ToServer, WorldDatabase,
Zone,
Actor, CharacterData, EffectsBuilder, Event, LuaPlayer, OBFUSCATION_ENABLED_MODE,
ScramblerKeyGenerator, ScramblerKeys, StatusEffects, ToServer, WorldDatabase, Zone,
common::{ClientId, ServerHandle},
load_init_script,
lua::Task,
@ -85,7 +85,16 @@ pub struct PlayerData {
pub completed_quests: Vec<u8>,
}
/// Represents a single connection between an instance of the client and the world server
/// Various obsfucation-related bits like the seeds and keys for this connection.
#[derive(Debug, Default, Clone)]
pub struct ObsfucationData {
pub keys: Option<ScramblerKeys>,
pub seed1: u8,
pub seed2: u8,
pub seed3: u32,
}
/// Represents a single connection between an instance of the client and the world server.
pub struct ZoneConnection {
pub config: WorldConfig,
pub socket: TcpStream,
@ -119,6 +128,8 @@ pub struct ZoneConnection {
// TODO: really needs to be moved somewhere else
pub weather_id: u16,
pub obsfucation_data: ObsfucationData,
}
impl ZoneConnection {
@ -140,6 +151,7 @@ impl ZoneConnection {
CompressionType::Uncompressed
},
&[segment],
self.obsfucation_data.keys.as_ref(),
)
.await;
}
@ -155,6 +167,7 @@ impl ZoneConnection {
CompressionType::Uncompressed
},
&[segment],
self.obsfucation_data.keys.as_ref(),
)
.await;
}
@ -350,6 +363,27 @@ impl ZoneConnection {
// Player Class Info
self.update_class_info().await;
// Generate obsfucation-related keys if needed.
if self.config.enable_packet_obsfucation {
let seed1 = fastrand::u8(..);
let seed2 = fastrand::u8(..);
let seed3 = fastrand::u32(..);
let generator = ScramblerKeyGenerator::new();
self.obsfucation_data = ObsfucationData {
keys: Some(generator.generate(seed1, seed2, seed3)),
seed1,
seed2,
seed3,
};
tracing::info!(
"You enabled packet obsfucation in your World config, things will break! {:?}",
self.obsfucation_data
);
}
// Init Zone
{
let config = get_config();
@ -372,6 +406,9 @@ impl ZoneConnection {
} else {
0
},
seed1: !self.obsfucation_data.seed1,
seed2: !self.obsfucation_data.seed2,
seed3: !self.obsfucation_data.seed3,
..Default::default()
}),
..Default::default()

View file

@ -161,6 +161,7 @@ pub async fn handle_custom_ipc(connection: &mut ZoneConnection, data: &CustomIpc
},
..Default::default()
}],
None,
)
.await;
}
@ -186,6 +187,7 @@ pub async fn handle_custom_ipc(connection: &mut ZoneConnection, data: &CustomIpc
},
..Default::default()
}],
None,
)
.await;
}
@ -234,6 +236,7 @@ pub async fn handle_custom_ipc(connection: &mut ZoneConnection, data: &CustomIpc
},
..Default::default()
}],
None,
)
.await;
}

View file

@ -5,7 +5,7 @@ mod chat_handler;
pub use chat_handler::ChatHandler;
mod connection;
pub use connection::{ExtraLuaState, PlayerData, ZoneConnection};
pub use connection::{ExtraLuaState, ObsfucationData, PlayerData, ZoneConnection};
mod database;
pub use database::{CharacterData, WorldDatabase};
@ -30,3 +30,6 @@ pub use custom_ipc_handler::handle_custom_ipc;
mod common;
pub use common::{ClientHandle, ClientId, FromServer, ServerHandle, ToServer};
mod scrambler;
pub use scrambler::{OBFUSCATION_ENABLED_MODE, ScramblerKeyGenerator, ScramblerKeys};

96
src/world/scrambler.rs Normal file
View file

@ -0,0 +1,96 @@
//! Obfuscation-related structures and procedures. This is based on the ever fantastic work of Perchbird and his Unscrambler: https://github.com/perchbirdd/Unscrambler
//! This is simply a Rust-reimplementation of Unscrambler.
/// Constant to enable packet obfuscation. Changes every patch.
pub const OBFUSCATION_ENABLED_MODE: u8 = 118;
/// Generates the necessary keys from three seeds.
pub struct ScramblerKeyGenerator {
table0: &'static [i32],
table1: &'static [i32],
table2: &'static [i32],
mid_table: &'static [u8],
day_table: &'static [u8],
table_radixes: &'static [i32],
table_max: &'static [i32],
}
impl ScramblerKeyGenerator {
pub fn new() -> Self {
// Technically unsafe, but Unscrambler's tables should be correct anyway
unsafe {
Self {
table0: std::mem::transmute::<&[u8], &[i32]>(include_bytes!(
"../../resources/table0.bin"
)),
table1: std::mem::transmute::<&[u8], &[i32]>(include_bytes!(
"../../resources/table1.bin"
)),
table2: std::mem::transmute::<&[u8], &[i32]>(include_bytes!(
"../../resources/table2.bin"
)),
mid_table: include_bytes!("../../resources/midtable.bin"),
day_table: include_bytes!("../../resources/daytable.bin"),
// TODO: is it possible to calculate these automatically?
table_radixes: &[93, 94, 113],
table_max: &[219, 187, 113],
}
}
}
fn derive(&self, set: u8, n_seed_1: u8, n_seed_2: u8, epoch: u32) -> u8 {
// FIXME: so many probably unnecessary casts here
let mid_index = 8 * (n_seed_1 as usize % (self.mid_table.len() / 8));
let mid_table_value = self.mid_table[4 + mid_index];
let mut mid_bytes = [0u8; 4];
mid_bytes.copy_from_slice(&self.mid_table[mid_index..mid_index + 4]);
let mid_value = u32::from_le_bytes(mid_bytes);
let epoch_days = 3 * (epoch as usize / 60 / 60 / 24);
let day_table_index = 4 * (epoch_days % (self.day_table.len() / 4));
let day_table_value = self.day_table[day_table_index];
let set_radix = self.table_radixes[set as usize];
let set_max = self.table_max[set as usize];
let table_index = (set_radix as i32 * (n_seed_2 as i32 % set_max as i32)) as usize
+ mid_value as usize * n_seed_1 as usize % set_radix as usize;
let set_result = match set {
0 => self.table0[table_index],
1 => self.table1[table_index],
2 => self.table2[table_index],
_ => 0,
};
(n_seed_1 as i32 + mid_table_value as i32 + day_table_value as i32 + set_result) as u8
}
/// Generates keys for scrambling or unscrambling packets. The callee must keep track of their seeds, we only generate the keys.
pub fn generate(&self, seed1: u8, seed2: u8, seed3: u32) -> ScramblerKeys {
let neg_seed_1 = seed1;
let neg_seed_2 = seed2;
let neg_seed_3 = seed3;
ScramblerKeys {
keys: [
self.derive(0, neg_seed_1, neg_seed_2, neg_seed_3),
self.derive(1, neg_seed_1, neg_seed_2, neg_seed_3),
self.derive(2, neg_seed_1, neg_seed_2, neg_seed_3),
],
}
}
}
/// Holds the keys generated by `ScramblerKeyGenerator`.
#[derive(Debug, Clone)]
pub struct ScramblerKeys {
keys: [u8; 3],
}
impl ScramblerKeys {
/// Fetches the required base key for the given opcode.
pub fn get_base_key(&self, opcode: u16) -> u8 {
self.keys[(opcode % 3) as usize]
}
}