diff --git a/build.rs b/build.rs index 51d5e19..a3ec500 100644 --- a/build.rs +++ b/build.rs @@ -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"); } diff --git a/src/bin/kawari-world.rs b/src/bin/kawari-world.rs index 3939f48..8914fa3 100644 --- a/src/bin/kawari-world.rs +++ b/src/bin/kawari-world.rs @@ -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) => { diff --git a/src/ipc/chat/mod.rs b/src/ipc/chat/mod.rs index 6828e81..f4e4f3c 100644 --- a/src/ipc/chat/mod.rs +++ b/src/ipc/chat/mod.rs @@ -2,20 +2,23 @@ use binrw::binrw; use crate::{ opcodes::ServerChatIpcType, - packet::{IpcSegment, ReadWriteIpcSegment}, + packet::{IPC_HEADER_SIZE, IpcSegment, ReadWriteIpcSegment}, }; pub type ServerChatIpcSegment = IpcSegment; 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 diff --git a/src/ipc/kawari/mod.rs b/src/ipc/kawari/mod.rs index 7cdaf4c..aed5735 100644 --- a/src/ipc/kawari/mod.rs +++ b/src/ipc/kawari/mod.rs @@ -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; 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 { diff --git a/src/ipc/lobby/mod.rs b/src/ipc/lobby/mod.rs index bf16b7d..7a27bee 100644 --- a/src/ipc/lobby/mod.rs +++ b/src/ipc/lobby/mod.rs @@ -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; 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 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 diff --git a/src/ipc/zone/init_zone.rs b/src/ipc/zone/init_zone.rs index 0647958..3e85ba6 100644 --- a/src/ipc/zone/init_zone.rs +++ b/src/ipc/zone/init_zone.rs @@ -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, diff --git a/src/ipc/zone/mod.rs b/src/ipc/zone/mod.rs index b4857d4..3611577 100644 --- a/src/ipc/zone/mod.rs +++ b/src/ipc/zone/mod.rs @@ -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 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 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 diff --git a/src/lib.rs b/src/lib.rs index ed73c88..629e930 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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; diff --git a/src/lobby/connection.rs b/src/lobby/connection.rs index e1c2690..a9090fc 100644 --- a/src/lobby/connection.rs +++ b/src/lobby/connection.rs @@ -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( state: &mut PacketState, compression_type: &CompressionType, segments: &[PacketSegment], + keys: Option<&ScramblerKeys>, ) -> (Vec, 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 { diff --git a/src/packet/ipc.rs b/src/packet/ipc.rs index 2fb9efe..a4bab5b 100644 --- a/src/packet/ipc.rs +++ b/src/packet/ipc.rs @@ -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; diff --git a/src/packet/mod.rs b/src/packet/mod.rs index ba81a31..e887ac3 100644 --- a/src/packet/mod.rs +++ b/src/packet/mod.rs @@ -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; diff --git a/src/packet/send_helpers.rs b/src/packet/send_helpers.rs index 4082052..f8bcd28 100644 --- a/src/packet/send_helpers.rs +++ b/src/packet/send_helpers.rs @@ -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( connection_type: ConnectionType, compression_type: CompressionType, segments: &[PacketSegment], + 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::() + data.len(); let header = PacketHeader { @@ -61,6 +62,7 @@ pub async fn send_keep_alive( connection_type, CompressionType::Uncompressed, &[response_packet], + None, ) .await; } diff --git a/src/world/connection.rs b/src/world/connection.rs index dde95f1..b4aaa56 100644 --- a/src/world/connection.rs +++ b/src/world/connection.rs @@ -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, } -/// 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, + 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() diff --git a/src/world/custom_ipc_handler.rs b/src/world/custom_ipc_handler.rs index ef6ddf1..b5722fd 100644 --- a/src/world/custom_ipc_handler.rs +++ b/src/world/custom_ipc_handler.rs @@ -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; } diff --git a/src/world/mod.rs b/src/world/mod.rs index be805d2..da73c5f 100644 --- a/src/world/mod.rs +++ b/src/world/mod.rs @@ -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}; diff --git a/src/world/scrambler.rs b/src/world/scrambler.rs new file mode 100644 index 0000000..85ba355 --- /dev/null +++ b/src/world/scrambler.rs @@ -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] + } +}