diff --git a/Cargo.toml b/Cargo.toml index 0fe6df3..8f3229b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,5 +30,8 @@ features = [ "Win32_System_Threading", ] +[target.'cfg(target_os = "linux")'.dependencies] +procfs = "0.16.0" + [profile.release] strip = true diff --git a/config_linux.json b/config_linux.json new file mode 100644 index 0000000..d700ac7 --- /dev/null +++ b/config_linux.json @@ -0,0 +1,132 @@ +{ + "signatures": [ + { + "name": "dwBuildNumber", + "module": "libengine2.so", + "pattern": "89 15 ? ? ? ? 48 83 ? ? 48 8D 3D ? ? ? ?", + "operations": [ + { + "type": "rip", + "offset": 2, + "length": 6 + } + ] + }, + { + "name": "dwNetworkGameClient_deltaTick", + "module": "libengine2.so", + "pattern": "89 83 ? ? ? ? B8 01", + "operations": [ + { + "type": "slice", + "start": 2, + "end": 4 + } + ] + }, + { + "name": "dwEntityList", + "module": "libclient.so", + "pattern": "48 8B 3D ? ? ? ? 44 89 E3 45 89 E6 C1 ? 0E", + "operations": [ + { + "type": "rip" + } + ] + }, + { + "name": "dwForceAttack", + "module": "libclient.so", + "pattern": "4c 8d ? ? ? ? ? 4d 89 ? 48 89 ? ? ? ? ? eb", + "operations": [ + { + "type": "rip" + }, + { + "type": "add", + "value": 48 + } + ] + }, + { + "name": "dwForceAttack2", + "module": "libclient.so", + "pattern": "4c 8d ? ? ? ? ? 48 8d ? ? ? ? ? 48 89 ? ? ? ? ? e8 ? ? ? ? 49 ? ? ? 31 d2 48 c7 ? ? ? ? ? ? 08", + "operations": [ + { + "type": "rip" + }, + { + "type": "add", + "value": 48 + } + ] + }, + { + "name": "dwGameEntitySystem", + "module": "libclient.so", + "pattern": "48 89 3d ? ? ? ? e9 ? ? ? ? 55", + "operations": [ + { + "type": "rip" + } + ] + }, + { + "name": "dwGameEntitySystem_getHighestEntityIndex", + "module": "libclient.so", + "pattern": "8b 87 ? ? ? ? c3 66 ? ? ? ? ? ? ? ? 8b 97", + "operations": [ + { + "type": "slice", + "start": 2, + "end": 4 + } + ] + }, + { + "name": "dwGameRules", + "module": "libclient.so", + "pattern": "48 89 3d ? ? ? ? 8b", + "operations": [ + { + "type": "rip" + } + ] + }, + { + "name": "dwLocalPlayerController", + "module": "libclient.so", + "pattern": "48 8B 15 ? ? ? ? 31 C0 48 85 D2 74 5C", + "operations": [ + { + "type": "rip" + } + ] + }, + { + "name": "dwLocalPlayerPawn", + "module": "libclient.so", + "pattern": "48 8D 05 ? ? ? ? C3 ? ? ? 00 00 00 00 00 C7 47 40", + "operations": [ + { + "type": "rip" + }, + { + "type": "add", + "value": 56 + } + ] + }, + { + "name": "dwViewMatrix", + "module": "libclient.so", + "pattern": "4c 8d 05 ? ? ? ? 48 ? ? 48 8d ? ? ? ? ? e8 ? ? ? ? 8b", + "operations": [ + { + "type": "rip" + } + ] + } + ] +} diff --git a/src/config.rs b/src/config.rs index 78d12e7..875e390 100644 --- a/src/config.rs +++ b/src/config.rs @@ -39,3 +39,42 @@ pub struct Signature { pub pattern: String, pub operations: Vec, } + +#[derive(Debug)] +pub struct SchemaSystemConfig { + pub module_name: &'static str, + pub pattern: &'static str, + pub type_scope_size_offset: usize, + pub type_scope_data_offset: usize, + pub declared_classes_offset: usize, +} + +#[cfg(target_os = "windows")] +pub const SCHEMA_CONF: SchemaSystemConfig = SchemaSystemConfig { + module_name: "schemasystem.dll", + pattern: "48 8D 0D ? ? ? ? E9 ? ? ? ? CC CC CC CC 48 8D 0D ? ? ? ? E9 ? ? ? ? CC CC CC CC 48 83 EC 28", + type_scope_size_offset: 0x190, + type_scope_data_offset: 0x198, + declared_classes_offset: 0x5B8, +}; + +#[cfg(target_os = "linux")] +pub const SCHEMA_CONF: SchemaSystemConfig = SchemaSystemConfig { + module_name: "libschemasystem.so", + pattern: "48 8D 05 ? ? ? ? c3 ? ? ? 00 00 00 00 00 48 8d 05 ? ? ? ? c3 ? ? ? 00 00 00 00 00 48 ? ? ? c3", + type_scope_size_offset: 0x1f8, + type_scope_data_offset: 0x200, + declared_classes_offset: 0x620, +}; + +#[cfg(target_os = "windows")] +pub const PROC_NAME: &str = "cs2.exe"; + +#[cfg(target_os = "linux")] +pub const PROC_NAME: &str = "cs2"; + +#[cfg(target_os = "windows")] +pub const OFFSETS_CONF: &str = "config.json"; + +#[cfg(target_os = "linux")] +pub const OFFSETS_CONF: &str = "config_linux.json"; diff --git a/src/dumper/interfaces.rs b/src/dumper/interfaces.rs index b921b49..20b299f 100644 --- a/src/dumper/interfaces.rs +++ b/src/dumper/interfaces.rs @@ -58,8 +58,21 @@ pub fn dump_interfaces( if let Some(create_interface_export) = module.export_by_name("CreateInterface") { info!("Dumping interfaces in {}...", module.name); - let create_interface_address = - process.resolve_rip(create_interface_export, None, None)?; + let create_interface_address; + + #[cfg(target_os = "windows")] + { + create_interface_address = + process.resolve_rip(create_interface_export, None, None)?; + } + + #[cfg(target_os = "linux")] + { + let create_interface_fn = + process.resolve_jmp(create_interface_export, None, None)?; + create_interface_address = + process.resolve_rip(create_interface_fn + 0x10, None, None)?; + } let mut node = process.read_memory::<*mut InterfaceNode>(create_interface_address)?; diff --git a/src/dumper/offsets.rs b/src/dumper/offsets.rs index 70e0545..f88510e 100644 --- a/src/dumper/offsets.rs +++ b/src/dumper/offsets.rs @@ -7,8 +7,8 @@ use simplelog::{debug, error, info}; use super::{generate_files, Entries, Entry}; use crate::builder::FileBuilderEnum; -use crate::config::Config; use crate::config::Operation::*; +use crate::config::{self, Config}; use crate::os::Process; pub fn dump_offsets( @@ -17,7 +17,7 @@ pub fn dump_offsets( file_path: &str, indent: usize, ) -> Result<()> { - let file = File::open("config.json")?; + let file = File::open(config::OFFSETS_CONF)?; let config: Config = serde_json::from_reader(file)?; @@ -149,8 +149,6 @@ mod tests { fn setup() -> Result { let mut process = Process::new("cs2.exe")?; - process.initialize()?; - Ok(process) } diff --git a/src/main.rs b/src/main.rs index a3c63f6..0982a23 100644 --- a/src/main.rs +++ b/src/main.rs @@ -86,14 +86,14 @@ fn main() -> Result<()> { TermLogger::init(log_level, config, TerminalMode::Mixed, ColorChoice::Auto)?; - if !Path::new("config.json").exists() { - bail!("Missing config.json file"); + if !Path::new(config::OFFSETS_CONF).exists() { + bail!("Missing {} file", config::OFFSETS_CONF); } // Create the output directory if it doesn't exist. fs::create_dir_all(&output)?; - let mut process = Process::new("cs2.exe")?; + let mut process = Process::new(config::PROC_NAME)?; let now = Instant::now(); diff --git a/src/os/mod.rs b/src/os/mod.rs index fa3b87b..266a781 100644 --- a/src/os/mod.rs +++ b/src/os/mod.rs @@ -1,3 +1,6 @@ +#[cfg(target_os = "linux")] +pub use module::ModuleEntry; + pub use module::Module; pub use process::Process; diff --git a/src/os/module.rs b/src/os/module.rs index 1819d54..5c1767f 100644 --- a/src/os/module.rs +++ b/src/os/module.rs @@ -1,18 +1,43 @@ use anyhow::Result; -use goblin::pe::export::Export; -use goblin::pe::import::Import; -use goblin::pe::options::ParseOptions; -use goblin::pe::section_table::SectionTable; -use goblin::pe::PE; +#[cfg(target_os = "windows")] +use goblin::pe::{ + export::Export, import::Import, options::ParseOptions, section_table::SectionTable, PE, +}; +#[cfg(target_os = "linux")] +use goblin::elf::{sym, Elf, SectionHeader}; +#[cfg(target_os = "linux")] +use std::path::PathBuf; + +#[cfg(target_os = "linux")] +#[derive(Debug)] +pub struct ModuleEntry { + pub path: PathBuf, + pub start_addr: usize, + pub data: Vec, +} +/// Represents a module loaded in a Windows process. pub struct Module<'a> { + /// The name of the module. pub name: &'a str, + + /// A reference to a slice of bytes containing the module data. + #[cfg(target_os = "windows")] pub data: &'a [u8], + #[cfg(target_os = "linux")] + pub module_info: &'a ModuleEntry, + + #[cfg(target_os = "windows")] + /// The PE file format representation of the module. pub pe: PE<'a>, + + #[cfg(target_os = "linux")] + pub elf: Elf<'a>, } impl<'a> Module<'a> { + #[cfg(target_os = "windows")] pub fn parse(name: &'a str, data: &'a [u8]) -> Result { let pe = PE::parse_with_opts( data, @@ -25,22 +50,67 @@ impl<'a> Module<'a> { Ok(Self { name, data, pe }) } + // parse the elf + #[cfg(target_os = "linux")] + pub fn parse(name: &'a str, module_entry: &'a ModuleEntry) -> Result { + let elf = Elf::parse(&module_entry.data)?; + Ok(Self { + name, + module_info: module_entry, + elf, + }) + } + #[inline] + #[cfg(target_os = "windows")] pub fn base(&self) -> usize { self.pe.image_base } #[inline] + #[cfg(target_os = "linux")] + pub fn base(&self) -> usize { + self.module_info.start_addr + } + + #[inline] + #[cfg(target_os = "windows")] pub fn exports(&self) -> &[Export] { &self.pe.exports } #[inline] + #[cfg(target_os = "linux")] + pub fn exports(&self) -> Vec { + let exports: Vec = self + .elf + .dynsyms + .iter() + .filter(|sym| sym.st_bind() == sym::STB_GLOBAL || sym.st_bind() == sym::STB_WEAK) + .collect(); + exports + } + + #[inline] + #[cfg(target_os = "windows")] pub fn imports(&self) -> &[Import] { &self.pe.imports } #[inline] + #[cfg(target_os = "linux")] + pub fn imports(&self) -> Vec { + let imports: Vec = self + .elf + .dynsyms + .iter() + .filter(|sym| sym.is_import()) + .collect(); + imports + } + + #[inline] + #[cfg(target_os = "windows")] pub fn export_by_name(&self, name: &str) -> Option { self.pe .exports @@ -50,6 +120,21 @@ impl<'a> Module<'a> { } #[inline] + #[cfg(target_os = "linux")] + pub fn export_by_name(&self, name: &str) -> Option { + let base_addr: usize = self.base(); + self.elf + .dynsyms + .iter() + .find(|sym| { + (sym.st_bind() == sym::STB_GLOBAL || sym.st_bind() == sym::STB_WEAK) + && self.elf.dynstrtab.get_at(sym.st_name) == Some(name) + }) + .map(|sym| (base_addr as u64 + sym.st_value) as usize) + } + + #[inline] + #[cfg(target_os = "windows")] pub fn import_by_name(&self, name: &str) -> Option { self.pe .imports @@ -59,11 +144,30 @@ impl<'a> Module<'a> { } #[inline] + #[cfg(target_os = "linux")] + pub fn get_import_by_name(&self, name: &str) -> Option { + let base_addr: usize = self.base().into(); + self.elf + .dynsyms + .iter() + .find(|sym| sym.is_import() && self.elf.dynstrtab.get_at(sym.st_name) == Some(name)) + .map(|sym| (base_addr as u64 + sym.st_value) as usize) + } + + #[inline] + #[cfg(target_os = "windows")] pub fn sections(&self) -> &[SectionTable] { &self.pe.sections } #[inline] + #[cfg(target_os = "linux")] + pub fn sections(&self) -> &[SectionHeader] { + self.elf.section_headers.as_slice() + } + + #[inline] + #[cfg(target_os = "windows")] pub fn size(&self) -> u32 { self.pe .header diff --git a/src/os/process.rs b/src/os/process.rs index 8a7f138..ee813b8 100644 --- a/src/os/process.rs +++ b/src/os/process.rs @@ -1,25 +1,54 @@ -use std::collections::HashMap; -use std::ffi::{c_void, CStr}; -use std::mem; -use std::ptr; +use super::Module; +#[cfg(target_os = "linux")] +use super::ModuleEntry; use anyhow::{bail, Result}; -use windows::Win32::Foundation::{CloseHandle, HANDLE}; -use windows::Win32::System::Diagnostics::Debug::ReadProcessMemory; -use windows::Win32::System::Diagnostics::ToolHelp::*; -use windows::Win32::System::Threading::{OpenProcess, PROCESS_ALL_ACCESS}; +use std::collections::HashMap; +use std::ffi::c_void; +use std::mem; -use super::Module; +#[cfg(target_os = "windows")] +use std::ffi::CStr; +#[cfg(target_os = "windows")] +use std::ptr; +#[cfg(target_os = "windows")] +use windows::Win32::{ + Foundation::{CloseHandle, HANDLE}, + System::Diagnostics::Debug::ReadProcessMemory, + System::Diagnostics::ToolHelp::*, + System::Threading::{OpenProcess, PROCESS_ALL_ACCESS}, +}; +#[cfg(target_os = "linux")] +use procfs::process::{self, all_processes}; +#[cfg(target_os = "linux")] +use std::fs::File; +#[cfg(target_os = "linux")] +use std::io::{Read, Seek, SeekFrom}; +#[cfg(target_os = "linux")] +use std::path::{Path, PathBuf}; + +/// Represents a Windows process. #[derive(Debug)] pub struct Process { + /// ID of the process. id: u32, + + #[cfg(target_os = "windows")] + /// Handle to the process. handle: HANDLE, + + /// A HashMap containing the name of each module and its corresponding raw data. + #[cfg(target_os = "windows")] modules: HashMap>, + + #[cfg(target_os = "linux")] + modules: HashMap, } impl Process { + #[cfg(target_os = "windows")] pub fn new(name: &str) -> Result { let id = Self::get_process_id_by_name(name)?; @@ -30,12 +59,21 @@ impl Process { handle, modules: HashMap::new(), }; - process.parse_loaded_modules()?; - Ok(process) } + #[cfg(target_os = "linux")] + pub fn new(name: &str) -> Result { + let id = Self::get_process_id_by_name(name)?; + let mut process = Self { + id, + modules: HashMap::new(), + }; + process.parse_loaded_modules()?; + Ok(process) + } + #[cfg(target_os = "windows")] pub fn find_pattern(&self, module_name: &str, pattern: &str) -> Option { let module = self.get_module_by_name(module_name)?; @@ -54,6 +92,30 @@ impl Process { None } + #[cfg(target_os = "linux")] + pub fn find_pattern(&self, module_name: &str, pattern: &str) -> Option { + let module = self.get_module_by_name(module_name)?; + + let pattern_bytes = Self::pattern_to_bytes(pattern); + + for (i, window) in module + .module_info + .data + .windows(pattern_bytes.len()) + .enumerate() + { + if window + .iter() + .zip(&pattern_bytes) + .all(|(&x, &y)| x == y as u8 || y == -1) + { + return Some(module.base() + i); + } + } + + None + } + pub fn get_module_by_name<'a>(&'a self, name: &'a str) -> Option> { self.modules .get(name) @@ -82,6 +144,7 @@ impl Process { Ok(buffer) } + #[cfg(target_os = "windows")] pub fn read_memory_raw(&self, address: usize, buffer: *mut c_void, size: usize) -> Result<()> { unsafe { ReadProcessMemory( @@ -95,6 +158,21 @@ impl Process { .map_err(|e| e.into()) } + #[cfg(target_os = "linux")] + pub fn read_memory_raw(&self, address: usize, buffer: *mut c_void, size: usize) -> Result<()> { + let proc_mem_path = format!("/proc/{}/mem", self.id); + let mut mem_file = File::open(proc_mem_path)?; + + // Go to the start address + mem_file.seek(SeekFrom::Start(address as u64))?; + + let buffer_slice = unsafe { std::slice::from_raw_parts_mut(buffer as *mut u8, size) }; + + // Try to read the data + mem_file.read_exact(buffer_slice)?; + Ok(()) + } + pub fn read_string(&self, address: usize) -> Result { let mut buffer = Vec::new(); @@ -129,7 +207,12 @@ impl Process { // The displacement value can be negative. let displacement = self.read_memory::(address + offset.unwrap_or(0x1))?; - Ok((address + displacement as usize) + length.unwrap_or(0x5)) + let final_address = if displacement.is_negative() { + address - displacement.wrapping_abs() as usize + } else { + address + displacement as usize + } + length.unwrap_or(0x5); + Ok(final_address) } pub fn resolve_rip( @@ -141,9 +224,15 @@ impl Process { // The displacement value can be negative. let displacement = self.read_memory::(address + offset.unwrap_or(0x3))?; - Ok((address + displacement as usize) + length.unwrap_or(0x7)) + let final_address = if displacement.is_negative() { + address - displacement.wrapping_abs() as usize + } else { + address + displacement as usize + } + length.unwrap_or(0x7); + Ok(final_address) } + #[cfg(target_os = "windows")] fn get_process_id_by_name(process_name: &str) -> Result { let snapshot = unsafe { CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0) }?; @@ -167,6 +256,27 @@ impl Process { bail!("Process not found: {}", process_name) } + #[cfg(target_os = "linux")] + fn get_process_id_by_name(process_name: &str) -> Result { + use std::io::{BufRead, BufReader}; + + for process_iter in all_processes()? { + let Ok(process) = process_iter else { continue }; + let comm_path = format!("/proc/{}/comm", process.pid()); + if let Ok(comm_file) = File::open(Path::new(&comm_path)) { + let mut comm = String::new(); + if BufReader::new(comm_file).read_line(&mut comm).is_ok() { + comm.pop(); + if comm == process_name && process.pid() > 0 { + return Ok(process.pid() as u32); + } + } + } + } + bail!("Process not found: {}", process_name); + } + + #[cfg(target_os = "windows")] fn parse_loaded_modules(&mut self) -> Result<()> { let snapshot = unsafe { CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, self.id) }?; @@ -196,6 +306,58 @@ impl Process { Ok(()) } + #[cfg(target_os = "linux")] + fn parse_loaded_modules(&mut self) -> Result<()> { + let process = process::Process::new(self.id as i32)?; + + let mut modules_info: HashMap = HashMap::new(); + + for mmap in process.maps()? { + let module_path = match mmap.pathname { + process::MMapPath::Path(path) => path, + _ => continue, + }; + let get_module_name = |path: &PathBuf| -> Option { + path.file_name() + .and_then(|name| name.to_str()) + .filter(|name| name.starts_with("lib") && name.ends_with(".so")) + .map(|name| name.to_string()) + }; + if let Some(module_name) = get_module_name(&module_path) { + let module_entry = modules_info + .entry(module_name) + .or_insert_with(|| (mmap.address, module_path)); + module_entry.0 = ( + std::cmp::min(mmap.address.0, module_entry.0 .0), + std::cmp::max(mmap.address.1, module_entry.0 .1), + ); + } + } + + for (module_name, (address_space, path)) in modules_info.into_iter() { + let (start, end) = address_space; + let read_elf_file = |path: &PathBuf| -> Result> { + let mut file = File::open(path)?; + let mut data = Vec::new(); + file.read_to_end(&mut data)?; + + Ok(data) + }; + if let Ok(data) = read_elf_file(&path) { + self.modules.insert( + module_name, + ModuleEntry { + path: path.clone(), + start_addr: start as usize, + data: data, + }, + ); + } + } + + Ok(()) + } + fn pattern_to_bytes(pattern: &str) -> Vec { pattern .split_whitespace() @@ -210,6 +372,7 @@ impl Process { } } +#[cfg(target_os = "windows")] impl Drop for Process { fn drop(&mut self) { if !self.handle.is_invalid() { diff --git a/src/sdk/schema_system.rs b/src/sdk/schema_system.rs index 07dc8b2..52816f9 100644 --- a/src/sdk/schema_system.rs +++ b/src/sdk/schema_system.rs @@ -6,6 +6,8 @@ use super::SchemaSystemTypeScope; use crate::os::Process; +use crate::config::SCHEMA_CONF; + pub struct SchemaSystem<'a> { process: &'a Process, address: usize, @@ -13,10 +15,9 @@ pub struct SchemaSystem<'a> { impl<'a> SchemaSystem<'a> { pub fn new(process: &'a Process) -> Result { - let mut address = process.find_pattern( - "schemasystem.dll", - "48 8D 0D ? ? ? ? E9 ? ? ? ? CC CC CC CC 48 8D 0D ? ? ? ? E9 ? ? ? ? CC CC CC CC 48 83 EC 28" - ).expect("unable to find schema system pattern"); + let mut address = process + .find_pattern(SCHEMA_CONF.module_name, SCHEMA_CONF.pattern) + .expect("unable to find schema system pattern"); address = process.resolve_rip(address, None, None)?; @@ -24,13 +25,17 @@ impl<'a> SchemaSystem<'a> { } pub fn type_scopes(&self) -> Result> { - let size = self.process.read_memory::(self.address + 0x190)?; + let size = self + .process + .read_memory::(self.address + SCHEMA_CONF.type_scope_size_offset)?; if size == 0 { bail!("no type scopes found"); } - let data = self.process.read_memory::(self.address + 0x198)?; + let data = self + .process + .read_memory::(self.address + SCHEMA_CONF.type_scope_data_offset)?; let mut addresses = vec![0; size as usize]; diff --git a/src/sdk/schema_system_type_scope.rs b/src/sdk/schema_system_type_scope.rs index 1db085e..c576408 100644 --- a/src/sdk/schema_system_type_scope.rs +++ b/src/sdk/schema_system_type_scope.rs @@ -4,6 +4,8 @@ use super::{SchemaClassInfo, SchemaTypeDeclaredClass, UtlTsHash}; use crate::os::Process; +use crate::config::SCHEMA_CONF; + pub struct SchemaSystemTypeScope<'a> { process: &'a Process, address: usize, @@ -17,7 +19,9 @@ impl<'a> SchemaSystemTypeScope<'a> { pub fn classes(&self) -> Result> { let declared_classes = self .process - .read_memory::>(self.address + 0x5B8)?; + .read_memory::>( + self.address + SCHEMA_CONF.declared_classes_offset, + )?; let classes: Vec = declared_classes .elements(self.process)?