Merge dev branch into main

This commit is contained in:
a2x
2024-03-28 22:19:20 +10:00
parent 755093fe06
commit 889ef7dcd8
315 changed files with 552043 additions and 333811 deletions

76
src/analysis/buttons.rs Normal file
View File

@@ -0,0 +1,76 @@
use std::env;
use log::debug;
use memflow::prelude::v1::*;
use serde::{Deserialize, Serialize};
use skidscan_macros::signature;
use crate::error::{Error, Result};
use crate::source_engine::KeyboardKey;
#[derive(Debug, Deserialize, Serialize)]
pub struct Button {
pub name: String,
pub value: u32,
}
pub fn buttons(process: &mut IntoProcessInstanceArcBox<'_>) -> Result<Vec<Button>> {
let (module_name, sig) = match env::consts::OS {
"linux" => (
"libclient.so",
signature!("48 8B 15 ? ? ? ? 48 89 83 ? ? ? ? 48 85 D2"),
),
"windows" => (
"client.dll",
signature!("48 8B 15 ? ? ? ? 48 85 D2 74 ? 0F 1F 40"),
),
_ => panic!("unsupported os"),
};
let module = process.module_by_name(&module_name)?;
let buf = process.read_raw(module.base, module.size as _)?;
let list_addr = sig
.scan(&buf)
.and_then(|ptr| process.read_addr64_rip(module.base + ptr).ok())
.ok_or_else(|| Error::Other("unable to read button list address"))?;
read_buttons(process, &module, list_addr)
}
fn read_buttons(
process: &mut IntoProcessInstanceArcBox<'_>,
module: &ModuleInfo,
list_addr: Address,
) -> Result<Vec<Button>> {
let mut buttons = Vec::new();
let mut key_ptr = Pointer64::<KeyboardKey>::from(process.read_addr64(list_addr)?);
while !key_ptr.is_null() {
let key = process.read_ptr(key_ptr)?;
let name = process.read_char_string(key.name.address())?;
let value =
((key_ptr.address() - module.base) + offset_of!(KeyboardKey.state) as i64) as u32;
debug!(
"found button: {} at {:#X} ({} + {:#X})",
name,
value as u64 + module.base.to_umem(),
module.name,
value
);
buttons.push(Button { name, value });
key_ptr = key.next;
}
buttons.sort_unstable_by(|a, b| a.name.cmp(&b.name));
Ok(buttons)
}

View File

@@ -0,0 +1,80 @@
use std::collections::BTreeMap;
use std::env;
use log::debug;
use memflow::prelude::v1::*;
use serde::{Deserialize, Serialize};
use skidscan_macros::signature;
use crate::error::Result;
use crate::source_engine::InterfaceReg;
pub type InterfaceMap = BTreeMap<String, Vec<Interface>>;
#[derive(Debug, Deserialize, Serialize)]
pub struct Interface {
pub name: String,
pub value: u32,
}
pub fn interfaces(process: &mut IntoProcessInstanceArcBox<'_>) -> Result<InterfaceMap> {
let sig = match env::consts::OS {
"linux" => signature!("48 8B 1D ? ? ? ? 48 85 DB 74 ? 49 89 FC"),
"windows" => signature!("4C 8B 0D ? ? ? ? 4C 8B D2 4C 8B D9"),
_ => panic!("unsupported os"),
};
process
.module_list()?
.iter()
.filter(|module| !module.name.starts_with("nvidia")) // Temporary workaround for upstream bug: https://github.com/memflow/memflow-native/blob/1b063fc573957498b88a13b6120120480bc65ea5/src/linux/mem.rs#L168
.filter_map(|module| {
let buf = process.read_raw(module.base, module.size as _).ok()?;
let list_addr = sig
.scan(&buf)
.and_then(|ptr| process.read_addr64_rip(module.base + ptr).ok())?;
read_interfaces(process, module, list_addr)
.ok()
.filter(|ifaces| !ifaces.is_empty())
.map(|ifaces| Ok((module.name.to_string(), ifaces)))
})
.collect()
}
fn read_interfaces(
process: &mut IntoProcessInstanceArcBox<'_>,
module: &ModuleInfo,
list_addr: Address,
) -> Result<Vec<Interface>> {
let mut ifaces = Vec::new();
let mut reg_ptr = Pointer64::<InterfaceReg>::from(process.read_addr64(list_addr)?);
while !reg_ptr.is_null() {
let reg = process.read_ptr(reg_ptr)?;
let name = process.read_char_string(reg.name.address())?;
let value = (reg.create_fn - module.base) as u32;
debug!(
"found interface: {} at {:#X} ({} + {:#X})",
name,
value as u64 + module.base.to_umem(),
module.name,
value
);
ifaces.push(Interface { name, value });
reg_ptr = reg.next;
}
ifaces.sort_unstable_by(|a, b| a.name.cmp(&b.name));
Ok(ifaces)
}

9
src/analysis/mod.rs Normal file
View File

@@ -0,0 +1,9 @@
pub use buttons::*;
pub use interfaces::*;
pub use offsets::*;
pub use schemas::*;
pub mod buttons;
pub mod interfaces;
pub mod offsets;
pub mod schemas;

95
src/analysis/offsets.rs Normal file
View File

@@ -0,0 +1,95 @@
use std::collections::BTreeMap;
use std::mem;
use std::str::FromStr;
use log::{debug, error};
use memflow::prelude::v1::*;
use serde::{Deserialize, Serialize};
use crate::config::{Operation, Signature, CONFIG};
use crate::error::{Error, Result};
pub type OffsetMap = BTreeMap<String, Vec<Offset>>;
#[derive(Debug, Deserialize, Serialize)]
pub struct Offset {
pub name: String,
pub value: u32,
}
pub fn offsets(process: &mut IntoProcessInstanceArcBox<'_>) -> Result<OffsetMap> {
let mut map = BTreeMap::new();
for (module_name, sigs) in CONFIG.signatures.iter().flatten() {
let module = process.module_by_name(module_name)?;
let mut offsets: Vec<_> = sigs
.iter()
.filter_map(|sig| match read_offset(process, &module, sig) {
Ok(offset) => Some(offset),
Err(err) => {
error!("{}", err);
None
}
})
.collect();
if !offsets.is_empty() {
offsets.sort_unstable_by(|a, b| a.name.cmp(&b.name));
map.insert(module_name.to_string(), offsets);
}
}
Ok(map)
}
fn read_offset(
process: &mut IntoProcessInstanceArcBox<'_>,
module: &ModuleInfo,
signature: &Signature,
) -> Result<Offset> {
let buf = process.read_raw(module.base, module.size as _)?;
let addr = skidscan::Signature::from_str(&signature.pattern)?
.scan(&buf)
.ok_or_else(|| Error::SignatureNotFound(signature.name.clone()))?;
let mut result = module.base + addr;
for op in &signature.operations {
result = match op {
Operation::Add { value } => result + *value,
Operation::Rip { offset, len } => {
let offset: i32 = process.read(result + offset.unwrap_or(3))?;
(result + offset) + len.unwrap_or(7)
}
Operation::Read => process.read_addr64(result)?,
Operation::Slice { start, end } => {
let buf = process.read_raw(result + *start, end - start)?;
let mut bytes = [0; mem::size_of::<usize>()];
bytes[..buf.len()].copy_from_slice(&buf);
usize::from_le_bytes(bytes).into()
}
Operation::Sub { value } => result - *value,
};
}
let value = (result - module.base)
.try_into()
.map_or_else(|_| result.to_umem() as u32, |v| v);
debug!("found offset: {} at {:#X}", signature.name, value);
Ok(Offset {
name: signature.name.clone(),
value,
})
}

337
src/analysis/schemas.rs Normal file
View File

@@ -0,0 +1,337 @@
use std::collections::BTreeMap;
use std::ffi::CStr;
use std::ops::Add;
use std::{env, mem};
use log::debug;
use memflow::prelude::v1::*;
use serde::{Deserialize, Serialize};
use skidscan_macros::signature;
use crate::error::{Error, Result};
use crate::source_engine::*;
pub type SchemaMap = BTreeMap<String, (Vec<Class>, Vec<Enum>)>;
#[derive(Debug, Eq, Ord, PartialEq, PartialOrd, Deserialize, Serialize)]
pub enum ClassMetadata {
Unknown { name: String },
NetworkChangeCallback { name: String },
NetworkVarNames { name: String, ty: String },
}
#[derive(Debug, Deserialize, Serialize)]
pub struct Class {
pub name: String,
pub module_name: String,
pub parent: Option<Box<Class>>,
pub metadata: Vec<ClassMetadata>,
pub fields: Vec<ClassField>,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct ClassField {
pub name: String,
pub ty: String,
pub offset: u32,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct Enum {
pub name: String,
pub ty: String,
pub alignment: u8,
pub size: u16,
pub members: Vec<EnumMember>,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct EnumMember {
pub name: String,
pub value: i64,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct TypeScope {
pub name: String,
pub classes: Vec<Class>,
pub enums: Vec<Enum>,
}
pub fn schemas(process: &mut IntoProcessInstanceArcBox<'_>) -> Result<SchemaMap> {
let schema_system = read_schema_system(process)?;
let type_scopes = read_type_scopes(process, &schema_system)?;
let map: BTreeMap<_, _> = type_scopes
.into_iter()
.map(|type_scope| (type_scope.name, (type_scope.classes, type_scope.enums)))
.collect();
Ok(map)
}
fn read_class_binding(
process: &mut IntoProcessInstanceArcBox<'_>,
binding_ptr: Pointer64<SchemaClassBinding>,
) -> Result<Class> {
let binding = process.read_ptr(binding_ptr)?;
let module_name = process
.read_char_string(binding.module_name.address())
.map(|s| {
format!(
"{}.{}",
s,
match env::consts::OS {
"linux" => "so",
"windows" => ".dll",
_ => panic!("unsupported os"),
}
)
})?;
let name = process.read_char_string(binding.name.address())?;
let parent = binding.base_classes.non_null().and_then(|ptr| {
let base_class = process.read_ptr(ptr).ok()?;
read_class_binding(process, base_class.prev)
.ok()
.map(Box::new)
});
let metadata = read_class_binding_metadata(process, &binding)?;
let fields = read_class_binding_fields(process, &binding)?;
debug!(
"found class: {} at {:#X} (module name: {}) (parent name: {:?}) (metadata count: {}) (fields count: {})",
name,
binding_ptr.to_umem(),
module_name,
parent.as_ref().map(|parent| parent.name.clone()),
metadata.len(),
fields.len()
);
Ok(Class {
name,
module_name,
parent,
metadata,
fields,
})
}
fn read_class_binding_fields(
process: &mut IntoProcessInstanceArcBox<'_>,
binding: &SchemaClassBinding,
) -> Result<Vec<ClassField>> {
(0..binding.fields_count)
.map(|i| {
let field_ptr: Pointer64<SchemaClassFieldData> = binding
.fields
.address()
.add(i * mem::size_of::<SchemaClassFieldData>() as u16)
.into();
let field = process.read_ptr(field_ptr)?;
if field.schema_type.is_null() {
return Err(Error::Other("field schema type is null"));
}
let name = process.read_char_string(field.name.address())?;
let schema_type = process.read_ptr(field.schema_type)?;
// TODO: Parse this properly.
let ty = process.read_char_string(schema_type.name.address())?;
Ok(ClassField {
name,
ty,
offset: field.offset,
})
})
.collect()
}
fn read_class_binding_metadata(
process: &mut IntoProcessInstanceArcBox<'_>,
binding: &SchemaClassBinding,
) -> Result<Vec<ClassMetadata>> {
if binding.static_metadata.is_null() {
return Ok(Vec::new());
}
(0..binding.static_metadata_count)
.map(|i| {
let metadata_ptr: Pointer64<SchemaMetadataEntryData> =
binding.static_metadata.offset(i as _).into();
let metadata = process.read_ptr(metadata_ptr)?;
if metadata.network_value.is_null() {
return Err(Error::Other("class metadata network value is null"));
}
let name = process.read_char_string(metadata.name.address())?;
let network_value = process.read_ptr(metadata.network_value)?;
let metadata = match name.as_str() {
"MNetworkChangeCallback" => unsafe {
let name =
process.read_char_string(network_value.union_data.name_ptr.address())?;
ClassMetadata::NetworkChangeCallback { name }
},
"MNetworkVarNames" => unsafe {
let var_value = network_value.union_data.var_value;
let name = process.read_char_string(var_value.name.address())?;
let ty = process.read_char_string(var_value.ty.address())?;
ClassMetadata::NetworkVarNames { name, ty }
},
_ => ClassMetadata::Unknown { name },
};
Ok(metadata)
})
.collect()
}
fn read_enum_binding(
process: &mut IntoProcessInstanceArcBox<'_>,
binding_ptr: Pointer64<SchemaEnumBinding>,
) -> Result<Enum> {
let binding = process.read_ptr(binding_ptr)?;
let name = process.read_char_string(binding.name.address())?;
let members = read_enum_binding_members(process, &binding)?;
debug!(
"found enum: {} at {:#X} (type name: {}) (alignment: {}) (members count: {})",
name,
binding_ptr.to_umem(),
binding.type_name(),
binding.alignment,
binding.size,
);
Ok(Enum {
name,
ty: binding.type_name().to_string(),
alignment: binding.alignment,
size: binding.size,
members,
})
}
fn read_enum_binding_members(
process: &mut IntoProcessInstanceArcBox<'_>,
binding: &SchemaEnumBinding,
) -> Result<Vec<EnumMember>> {
(0..binding.size)
.map(|i| {
let enumerator_info_ptr: Pointer64<SchemaEnumeratorInfoData> = binding
.enum_info
.address()
.add(i * mem::size_of::<SchemaEnumeratorInfoData>() as u16)
.into();
let enumerator_info = process.read_ptr(enumerator_info_ptr)?;
let name = process.read_char_string(enumerator_info.name.address())?;
let value = {
let value = unsafe { enumerator_info.union_data.ulong } as i64;
if value == i64::MAX {
-1
} else {
value
}
};
Ok(EnumMember { name, value })
})
.collect()
}
fn read_schema_system(process: &mut IntoProcessInstanceArcBox<'_>) -> Result<SchemaSystem> {
let (module_name, sig) = match env::consts::OS {
"linux" => (
"libschemasystem.so",
signature!("48 8D 35 ? ? ? ? 48 8D 3D ? ? ? ? E8 ? ? ? ? 48 8D 15 ? ? ? ? 48 8D 35 ? ? ? ? 48 8D 3D"),
),
"windows" => (
"schemasystem.dll",
signature!("48 89 05 ? ? ? ? 4C 8D 45"),
),
_ => panic!("unsupported os"),
};
let module = process.module_by_name(&module_name)?;
let buf = process.read_raw(module.base, module.size as _)?;
let addr = sig
.scan(&buf)
.and_then(|ptr| process.read_addr64_rip(module.base + ptr).ok())
.ok_or_else(|| Error::Other("unable to read schema system address"))?;
let schema_system: SchemaSystem = process.read(addr)?;
if schema_system.num_registrations == 0 {
return Err(Error::Other("no schema system registrations found"));
}
Ok(schema_system)
}
fn read_type_scopes(
process: &mut IntoProcessInstanceArcBox<'_>,
schema_system: &SchemaSystem,
) -> Result<Vec<TypeScope>> {
let type_scopes = &schema_system.type_scopes;
(0..type_scopes.size)
.map(|i| {
let type_scope_ptr = type_scopes.get(process, i as _)?;
let type_scope = process.read_ptr(type_scope_ptr)?;
let name = unsafe { CStr::from_ptr(type_scope.name.as_ptr()) }
.to_string_lossy()
.to_string();
let classes: Vec<_> = type_scope
.class_bindings
.elements(process)?
.iter()
.filter_map(|ptr| read_class_binding(process, *ptr).ok())
.collect();
let enums: Vec<_> = type_scope
.enum_bindings
.elements(process)?
.iter()
.filter_map(|ptr| read_enum_binding(process, *ptr).ok())
.collect();
debug!(
"found type scope: {} at {:#X} (classes count: {}) (enums count: {})",
name,
type_scope_ptr.to_umem(),
classes.len(),
enums.len()
);
Ok(TypeScope {
name,
classes,
enums,
})
})
.collect()
}

View File

@@ -1,53 +0,0 @@
use std::io::{Result, Write};
use super::FileBuilder;
#[derive(Clone, Debug, PartialEq)]
pub struct CppFileBuilder;
impl FileBuilder for CppFileBuilder {
fn extension(&mut self) -> &str {
"hpp"
}
fn write_top_level(&mut self, output: &mut dyn Write) -> Result<()> {
writeln!(output, "#pragma once\n")?;
writeln!(output, "#include <cstddef>\n")?;
Ok(())
}
fn write_namespace(
&mut self,
output: &mut dyn Write,
name: &str,
comment: Option<&str>,
) -> Result<()> {
let comment = comment.map_or(String::new(), |c| format!(" // {}", c));
write!(output, "namespace {} {{{}\n", name, comment)
}
fn write_variable(
&mut self,
output: &mut dyn Write,
name: &str,
value: usize,
comment: Option<&str>,
indentation: Option<usize>,
) -> Result<()> {
let indentation = " ".repeat(indentation.unwrap_or(4));
let comment = comment.map_or(String::new(), |c| format!(" // {}", c));
write!(
output,
"{}constexpr std::ptrdiff_t {} = {:#X};{}\n",
indentation, name, value, comment
)
}
fn write_closure(&mut self, output: &mut dyn Write, eof: bool) -> Result<()> {
write!(output, "{}", if eof { "}" } else { "}\n\n" })
}
}

View File

@@ -1,50 +0,0 @@
use std::io::{Result, Write};
use super::FileBuilder;
#[derive(Clone, Debug, PartialEq)]
pub struct CSharpFileBuilder;
impl FileBuilder for CSharpFileBuilder {
fn extension(&mut self) -> &str {
"cs"
}
fn write_top_level(&mut self, _output: &mut dyn Write) -> Result<()> {
Ok(())
}
fn write_namespace(
&mut self,
output: &mut dyn Write,
name: &str,
comment: Option<&str>,
) -> Result<()> {
let comment = comment.map_or(String::new(), |c| format!(" // {}", c));
write!(output, "public static class {} {{{}\n", name, comment)
}
fn write_variable(
&mut self,
output: &mut dyn Write,
name: &str,
value: usize,
comment: Option<&str>,
indentation: Option<usize>,
) -> Result<()> {
let indentation = " ".repeat(indentation.unwrap_or(4));
let comment = comment.map_or(String::new(), |c| format!(" // {}", c));
write!(
output,
"{}public const nint {} = {:#X};{}\n",
indentation, name, value, comment
)
}
fn write_closure(&mut self, output: &mut dyn Write, eof: bool) -> Result<()> {
write!(output, "{}", if eof { "}" } else { "}\n\n" })
}
}

View File

@@ -1,25 +0,0 @@
use std::io::{Result, Write};
pub trait FileBuilder {
fn extension(&mut self) -> &str;
fn write_top_level(&mut self, output: &mut dyn Write) -> Result<()>;
fn write_namespace(
&mut self,
output: &mut dyn Write,
name: &str,
comment: Option<&str>,
) -> Result<()>;
fn write_variable(
&mut self,
output: &mut dyn Write,
name: &str,
value: usize,
comment: Option<&str>,
indentation: Option<usize>,
) -> Result<()>;
fn write_closure(&mut self, output: &mut dyn Write, eof: bool) -> Result<()>;
}

View File

@@ -1,79 +0,0 @@
use std::collections::BTreeMap;
use std::io::{Result, Write};
use serde::Serialize;
use super::FileBuilder;
#[derive(Clone, Debug, Default, PartialEq, Serialize)]
struct JsonOffsetValue {
value: usize,
comment: Option<String>,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize)]
struct JsonModule {
data: BTreeMap<String, JsonOffsetValue>,
comment: Option<String>,
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct JsonFileBuilder {
data: BTreeMap<String, JsonModule>,
current_namespace: String,
}
impl FileBuilder for JsonFileBuilder {
fn extension(&mut self) -> &str {
"json"
}
fn write_top_level(&mut self, _output: &mut dyn Write) -> Result<()> {
Ok(())
}
fn write_namespace(
&mut self,
_output: &mut dyn Write,
name: &str,
comment: Option<&str>,
) -> Result<()> {
self.current_namespace = name.to_string();
self.data.entry(name.to_string()).or_default().comment = comment.map(str::to_string);
Ok(())
}
fn write_variable(
&mut self,
_output: &mut dyn Write,
name: &str,
value: usize,
comment: Option<&str>,
_indentation: Option<usize>,
) -> Result<()> {
self.data
.entry(self.current_namespace.clone())
.or_default()
.data
.insert(
name.to_string(),
JsonOffsetValue {
value,
comment: comment.map(str::to_string),
},
);
Ok(())
}
fn write_closure(&mut self, output: &mut dyn Write, eof: bool) -> Result<()> {
if eof {
write!(output, "{}", serde_json::to_string_pretty(&self.data)?)?;
self.data.clear();
}
Ok(())
}
}

View File

@@ -1,75 +0,0 @@
pub use cpp_file_builder::CppFileBuilder;
pub use csharp_file_builder::CSharpFileBuilder;
pub use file_builder::FileBuilder;
pub use json_file_builder::JsonFileBuilder;
pub use python_file_builder::PythonFileBuilder;
pub use rust_file_builder::RustFileBuilder;
pub use yaml_file_builder::YamlFileBuilder;
use std::io::{Result, Write};
pub mod cpp_file_builder;
pub mod csharp_file_builder;
pub mod file_builder;
pub mod json_file_builder;
pub mod python_file_builder;
pub mod rust_file_builder;
pub mod yaml_file_builder;
#[derive(Clone, Debug, PartialEq)]
pub enum FileBuilderEnum {
CppFileBuilder(CppFileBuilder),
CSharpFileBuilder(CSharpFileBuilder),
JsonFileBuilder(JsonFileBuilder),
PythonFileBuilder(PythonFileBuilder),
RustFileBuilder(RustFileBuilder),
YamlFileBuilder(YamlFileBuilder),
}
impl FileBuilder for FileBuilderEnum {
fn extension(&mut self) -> &str {
self.as_mut().extension()
}
fn write_top_level(&mut self, output: &mut dyn Write) -> Result<()> {
self.as_mut().write_top_level(output)
}
fn write_namespace(
&mut self,
output: &mut dyn Write,
name: &str,
comment: Option<&str>,
) -> Result<()> {
self.as_mut().write_namespace(output, name, comment)
}
fn write_variable(
&mut self,
output: &mut dyn Write,
name: &str,
value: usize,
comment: Option<&str>,
indentation: Option<usize>,
) -> Result<()> {
self.as_mut()
.write_variable(output, name, value, comment, indentation)
}
fn write_closure(&mut self, output: &mut dyn Write, eof: bool) -> Result<()> {
self.as_mut().write_closure(output, eof)
}
}
impl FileBuilderEnum {
fn as_mut(&mut self) -> &mut dyn FileBuilder {
match self {
FileBuilderEnum::CppFileBuilder(builder) => builder,
FileBuilderEnum::CSharpFileBuilder(builder) => builder,
FileBuilderEnum::JsonFileBuilder(builder) => builder,
FileBuilderEnum::PythonFileBuilder(builder) => builder,
FileBuilderEnum::RustFileBuilder(builder) => builder,
FileBuilderEnum::YamlFileBuilder(builder) => builder,
}
}
}

View File

@@ -1,54 +0,0 @@
use std::io::{Result, Write};
use super::FileBuilder;
#[derive(Clone, Debug, PartialEq)]
pub struct PythonFileBuilder;
impl FileBuilder for PythonFileBuilder {
fn extension(&mut self) -> &str {
"py"
}
fn write_top_level(&mut self, _output: &mut dyn Write) -> Result<()> {
Ok(())
}
fn write_namespace(
&mut self,
output: &mut dyn Write,
name: &str,
comment: Option<&str>,
) -> Result<()> {
let comment = comment.map_or(String::new(), |c| format!(" # {}", c));
write!(output, "class {}:{}\n", name, comment)
}
fn write_variable(
&mut self,
output: &mut dyn Write,
name: &str,
value: usize,
comment: Option<&str>,
indentation: Option<usize>,
) -> Result<()> {
let indentation = " ".repeat(indentation.unwrap_or(4));
let comment = comment.map_or(String::new(), |c| format!(" # {}", c));
write!(
output,
"{}{} = {:#X}{}\n",
indentation, name, value, comment
)
}
fn write_closure(&mut self, output: &mut dyn Write, eof: bool) -> Result<()> {
if !eof {
write!(output, "\n")?;
}
Ok(())
}
}

View File

@@ -1,53 +0,0 @@
use super::FileBuilder;
use std::io::{Result, Write};
#[derive(Clone, Debug, Default, PartialEq)]
pub struct RustFileBuilder;
impl FileBuilder for RustFileBuilder {
fn extension(&mut self) -> &str {
"rs"
}
fn write_top_level(&mut self, output: &mut dyn Write) -> Result<()> {
write!(
output,
"#![allow(non_snake_case, non_upper_case_globals)]\n\n"
)
}
fn write_namespace(
&mut self,
output: &mut dyn Write,
name: &str,
comment: Option<&str>,
) -> Result<()> {
let comment = comment.map_or(String::new(), |c| format!(" // {}", c));
write!(output, "pub mod {} {{{}\n", name, comment)
}
fn write_variable(
&mut self,
output: &mut dyn Write,
name: &str,
value: usize,
comment: Option<&str>,
indentation: Option<usize>,
) -> Result<()> {
let indentation = " ".repeat(indentation.unwrap_or(4));
let comment = comment.map_or(String::new(), |c| format!(" // {}", c));
write!(
output,
"{}pub const {}: usize = {:#X};{}\n",
indentation, name, value, comment
)
}
fn write_closure(&mut self, output: &mut dyn Write, eof: bool) -> Result<()> {
write!(output, "{}", if eof { "}" } else { "}\n\n" })
}
}

View File

@@ -1,46 +0,0 @@
use std::io::{Result, Write};
use super::FileBuilder;
#[derive(Clone, Debug, Default, PartialEq)]
pub struct YamlFileBuilder;
impl FileBuilder for YamlFileBuilder {
fn extension(&mut self) -> &str {
"yaml"
}
fn write_top_level(&mut self, output: &mut dyn Write) -> Result<()> {
write!(output, "---\n")
}
fn write_namespace(
&mut self,
output: &mut dyn Write,
name: &str,
comment: Option<&str>,
) -> Result<()> {
let comment = comment.map_or(String::new(), |c| format!(" # {}", c));
write!(output, "{}:{}\n", name, comment)
}
fn write_variable(
&mut self,
output: &mut dyn Write,
name: &str,
value: usize,
comment: Option<&str>,
indentation: Option<usize>,
) -> Result<()> {
let indentation = " ".repeat(indentation.unwrap_or(4));
let comment = comment.map_or(String::new(), |c| format!(" # {}", c));
write!(output, "{}{}: {}{}\n", indentation, name, value, comment)
}
fn write_closure(&mut self, _output: &mut dyn Write, _eof: bool) -> Result<()> {
Ok(())
}
}

View File

@@ -1,86 +1,61 @@
use std::collections::HashMap;
use std::sync::LazyLock;
use std::{env, fs};
use serde::{Deserialize, Serialize};
pub static CONFIG: LazyLock<Config> = LazyLock::new(|| {
let file_name = match env::consts::OS {
"linux" => "config_linux.json",
"windows" => "config_win.json",
_ => panic!("unsupported os"),
};
let content = fs::read_to_string(file_name).expect("unable to read config file");
let config: Config = serde_json::from_str(&content).expect("unable to parse config file");
config
});
#[derive(Debug, Deserialize, Serialize)]
#[serde(tag = "type", rename_all = "camelCase")]
#[serde(rename_all = "snake_case", tag = "type")]
pub enum Operation {
Add {
value: usize,
},
Deref {
times: Option<usize>,
size: Option<usize>,
},
Jmp {
offset: Option<usize>,
length: Option<usize>,
},
/// Adds the specified value to the current address.
Add { value: usize },
/// Resolves the absolute address of a RIP-relative address.
Rip {
offset: Option<usize>,
length: Option<usize>,
},
Slice {
start: usize,
end: usize,
},
Sub {
value: usize,
len: Option<usize>,
},
/// Reads the value at the current address, treating it as a pointer.
Read,
/// Extracts a range of bytes from the current address and interprets them as a value.
Slice { start: usize, end: usize },
/// Subtracts the specified value from the current address.
Sub { value: usize },
}
#[derive(Debug, Deserialize, Serialize)]
pub struct Config {
pub signatures: Vec<Signature>,
/// Name of the process.
pub executable: String,
/// List of signatures to search for.
pub signatures: Vec<HashMap<String, Vec<Signature>>>,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct Signature {
/// Name of the signature.
pub name: String,
pub module: String,
/// An IDA-style pattern containing the bytes to search for.
pub pattern: String,
/// List of operations to perform on the matched address.
pub operations: Vec<Operation>,
}
#[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";
#[cfg(target_os = "windows")]
pub const DEFAULT_OUT_DIR: &str = "generated";
#[cfg(target_os = "linux")]
pub const DEFAULT_OUT_DIR: &str = "generated_linux";

View File

@@ -1,110 +0,0 @@
use std::ffi::c_char;
use std::mem::offset_of;
use anyhow::Result;
use simplelog::{debug, info};
use super::{generate_files, Entries, Entry};
use crate::builder::FileBuilderEnum;
use crate::os::Process;
#[derive(Debug)]
#[repr(C)]
struct InterfaceNode {
pub create_fn: *const (),
pub name: *const c_char,
pub next: *mut InterfaceNode,
}
impl InterfaceNode {
fn instance(&self, process: &Process) -> Result<usize> {
process
.read_memory::<usize>(
(self as *const _ as usize + offset_of!(InterfaceNode, create_fn)).into(),
)
.map(|ptr| ptr.into())
}
fn name(&self, process: &Process) -> Result<String> {
let name_ptr = process.read_memory::<usize>(
(self as *const _ as usize + offset_of!(InterfaceNode, name)).into(),
)?;
process.read_string(name_ptr.into())
}
fn next(&self, process: &Process) -> Result<*mut InterfaceNode> {
process.read_memory::<*mut InterfaceNode>(
(self as *const _ as usize + offset_of!(InterfaceNode, next)).into(),
)
}
}
pub fn dump_interfaces(
process: &Process,
builders: &mut Vec<FileBuilderEnum>,
file_path: &str,
indent: usize,
) -> Result<()> {
let mut entries = Entries::new();
for module in process
.modules()?
.iter()
.filter(|m| m.name != "crashhandler64.dll")
{
if let Some(create_interface_export) = module.export_by_name("CreateInterface") {
info!("Dumping interfaces in <blue>{}</>...", module.name);
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)?;
while !node.is_null() {
let instance = unsafe { (*node).instance(process) }?;
let name = unsafe { (*node).name(process) }?;
debug!(
"Found <bright-yellow>{}</> @ <bright-magenta>{:#X}</> (<blue>{}</> + <bright-blue>{:#X}</>)",
name,
instance,
module.name,
instance - module.base()
);
let container = entries.entry(module.name.replace(".", "_")).or_default();
container.comment = Some(module.name.to_string());
container.data.push(Entry {
name,
value: (instance - module.base()).into(),
comment: None,
indent: Some(indent),
});
node = unsafe { (*node).next(process) }?;
}
}
}
generate_files(builders, &entries, file_path, "interfaces")?;
Ok(())
}

View File

@@ -1,108 +0,0 @@
pub use interfaces::dump_interfaces;
pub use offsets::dump_offsets;
pub use schemas::dump_schemas;
use std::collections::BTreeMap;
use std::fs::File;
use std::io::Write;
use anyhow::Result;
use chrono::Utc;
use crate::builder::{FileBuilder, FileBuilderEnum};
pub mod interfaces;
pub mod offsets;
pub mod schemas;
#[derive(Debug, PartialEq)]
pub struct Entry {
pub name: String,
pub value: usize,
pub comment: Option<String>,
pub indent: Option<usize>,
}
#[derive(Default)]
pub struct EntriesContainer {
pub data: Vec<Entry>,
pub comment: Option<String>,
}
pub type Entries = BTreeMap<String, EntriesContainer>;
pub fn generate_file(
builder: &mut FileBuilderEnum,
entries: &Entries,
file_path: &str,
file_name: &str,
) -> Result<()> {
if entries.is_empty() {
return Ok(());
}
let file_path = format!("{}/{}.{}", file_path, file_name, builder.extension());
let mut file = File::create(file_path)?;
write_banner_to_file(&mut file, builder.extension())?;
builder.write_top_level(&mut file)?;
let len = entries.len();
for (i, pair) in entries.iter().enumerate() {
builder.write_namespace(&mut file, pair.0, pair.1.comment.as_deref())?;
pair.1.data.iter().try_for_each(|entry| {
builder.write_variable(
&mut file,
&entry.name,
entry.value,
entry.comment.as_deref(),
entry.indent,
)
})?;
builder.write_closure(&mut file, i == len - 1)?;
}
Ok(())
}
pub fn generate_files(
builders: &mut [FileBuilderEnum],
entries: &Entries,
file_path: &str,
file_name: &str,
) -> Result<()> {
builders
.iter_mut()
.try_for_each(|builder| generate_file(builder, entries, file_path, file_name))
}
fn write_banner_to_file(file: &mut File, file_extension: &str) -> Result<()> {
const REPO_URL: &str = "https://github.com/a2x/cs2-dumper";
let time_now = Utc::now().to_rfc2822();
let banner = match file_extension {
"json" => None,
"py" => Some(format!(
"'''\nGenerated using {}\n{}\n'''\n\n",
REPO_URL, time_now
)),
"yaml" => None,
_ => Some(format!(
"/*\n * Generated using {}\n * {}\n */\n\n",
REPO_URL, time_now
)),
};
if let Some(banner) = banner {
write!(file, "{}", banner)?;
}
Ok(())
}

View File

@@ -1,420 +0,0 @@
use std::fs::File;
use anyhow::Result;
use simplelog::{debug, error, info};
use super::{generate_files, Entries, Entry};
use crate::builder::FileBuilderEnum;
use crate::config::Operation::*;
use crate::config::{self, Config};
use crate::os::Process;
pub fn dump_offsets(
process: &Process,
builders: &mut Vec<FileBuilderEnum>,
file_path: &str,
indent: usize,
) -> Result<()> {
let file = File::open(config::OFFSETS_CONF)?;
let config: Config = serde_json::from_reader(file)?;
info!("Dumping offsets...");
let mut entries = Entries::new();
for signature in config.signatures {
debug!("Searching for <bright-yellow>{}</>...", signature.name);
let module = process
.get_module_by_name(&signature.module)
.expect(&format!("Failed to find module {}.", signature.module));
let mut address = match process.find_pattern(&signature.module, &signature.pattern) {
Some(a) => a,
None => {
error!(
"Failed to find pattern for <bright-yellow>{}</>.",
signature.name
);
continue;
}
};
for operation in signature.operations {
match operation {
Add { value } => address += value,
Deref { times, size } => {
let times = times.unwrap_or(1);
let size = size.unwrap_or(8);
for _ in 0..times {
process.read_memory_raw(address, &mut address as *mut _ as *mut _, size)?;
}
}
Jmp { offset, length } => {
address = process.resolve_jmp(address, offset, length)?.into();
}
Rip { offset, length } => {
address = process.resolve_rip(address, offset, length)?.into()
}
Slice { start, end } => {
let mut result: usize = 0;
process.read_memory_raw(
address + start,
&mut result as *mut _ as *mut _,
end - start,
)?;
address = result.into();
}
Sub { value } => address -= value,
}
}
let (name, value) = if address < module.base() {
debug!(
"Found <bright-yellow>{}</> @ <bright-blue>{:#X}</>",
signature.name, address
);
(signature.name, address)
} else {
debug!(
"Found <bright-yellow>{}</> @ <bright-magenta>{:#X}</> (<blue>{}</> + <bright-blue>{:#X}</>)",
signature.name,
address,
signature.module,
address - module.base()
);
(signature.name, address - module.base())
};
if name == "dwBuildNumber" {
let build_number = process.read_memory::<u32>(module.base() + value)?;
debug!("Game build number: <bright-yellow>{}</>", build_number);
let container = entries.entry("game_info".to_string()).or_default();
container.comment =
Some("Some additional information about the game at dump time".to_string());
container.data.push(Entry {
name: "buildNumber".to_string(),
value: build_number as usize,
comment: Some("Game build number".to_string()),
indent: Some(indent),
});
}
let container = entries
.entry(signature.module.replace(".", "_"))
.or_default();
container.comment = Some(signature.module);
container.data.push(Entry {
name,
value,
comment: None,
indent: Some(indent),
});
}
generate_files(builders, &entries, file_path, "offsets")?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::ffi::{c_char, c_void};
use std::fs;
use std::mem::offset_of;
use std::thread;
use std::time::Duration;
use core::arch::x86_64::_bittest;
use anyhow::anyhow;
use serde_json::Value;
fn read_json_value(file_path: &str) -> Result<Value> {
let content = fs::read_to_string(file_path)?;
serde_json::from_str(&content).map_err(Into::into)
}
fn get_class_field(module_name: &str, class_name: &str, class_key: &str) -> Result<u64> {
let value = read_json_value(&format!("generated/{}.json", module_name))
.expect("unable to read json file");
value[class_name]["data"][class_key]["value"]
.as_u64()
.ok_or_else(|| {
anyhow!(
"unable to find class field {} in class {}",
class_key,
class_name
)
})
}
fn get_offset_value(module_name: &str, offset_name: &str) -> Result<u64> {
let value = read_json_value("generated/offsets.json").expect("unable to read offsets.json");
value[module_name.replace(".", "_")]["data"][offset_name]["value"]
.as_u64()
.ok_or_else(|| anyhow!("unable to find offset"))
}
#[test]
fn build_number() -> Result<()> {
let process = Process::new("cs2.exe")?;
let engine_base = process
.get_module_by_name("engine2.dll")
.expect("unable to find engine2.dll")
.base();
let build_number_offset = get_offset_value("engine2.dll", "dwBuildNumber")?;
let build_number =
process.read_memory::<u32>(engine_base + build_number_offset as usize)?;
println!("build number: {}", build_number);
Ok(())
}
#[test]
fn key_buttons() -> Result<()> {
let process = Process::new("cs2.exe")?;
let client_base = process
.get_module_by_name("client.dll")
.expect("unable to find client.dll")
.base();
const KEY_BUTTONS: [&str; 8] = [
"dwForceAttack",
"dwForceAttack2",
"dwForceBackward",
"dwForceCrouch",
"dwForceForward",
"dwForceJump",
"dwForceLeft",
"dwForceRight",
];
let get_key_state = |value: u32| match value {
256 => "key up",
65537 => "key down",
_ => "unknown",
};
// Sleep for a second, so we're able to test.
thread::sleep(Duration::from_secs(1));
for button in &KEY_BUTTONS {
let offset = get_offset_value("client.dll", button).expect("unable to find client.dll");
let value = process.read_memory::<u32>(client_base + offset as usize)?;
println!("key button: {} (state: {})", button, get_key_state(value));
}
Ok(())
}
#[test]
fn global_vars() -> Result<()> {
#[derive(Debug)]
#[repr(C)]
struct GlobalVarsBase {
real_time: f32, // 0x0000
frame_count: i32, // 0x0004
frame_time: f32, // 0x0008
absolute_frame_time: f32, // 0x000C
max_clients: i32, // 0x0010
pad_0: [u8; 0x14], // 0x0014
frame_time_2: f32, // 0x0028
current_time: f32, // 0x002C
current_time_2: f32, // 0x0030
pad_1: [u8; 0xC], // 0x0034
tick_count: f32, // 0x0040
pad_2: [u8; 0x4], // 0x0044
network_channel: *const c_void, // 0x0048
pad_3: [u8; 0x130], // 0x0050
current_map: *const c_char, // 0x0180
current_map_name: *const c_char, // 0x0188
}
impl GlobalVarsBase {
fn current_map(&self, process: &Process) -> Result<String> {
let name_ptr = process.read_memory::<usize>(
(self as *const _ as usize + offset_of!(Self, current_map)).into(),
)?;
process.read_string(name_ptr.into())
}
fn current_map_name(&self, process: &Process) -> Result<String> {
let name_ptr = process.read_memory::<usize>(
(self as *const _ as usize + offset_of!(Self, current_map_name)).into(),
)?;
process.read_string(name_ptr.into())
}
}
let process = Process::new("cs2.exe")?;
let client_base = process
.get_module_by_name("client.dll")
.expect("unable to find client.dll")
.base();
let global_vars_offset = get_offset_value("client.dll", "dwGlobalVars")?;
let global_vars = process
.read_memory::<*const GlobalVarsBase>(client_base + global_vars_offset as usize)?;
let current_map_name = unsafe {
(*global_vars)
.current_map_name(&process)
.unwrap_or_default()
};
println!("current map name: {}", current_map_name);
Ok(())
}
#[test]
fn is_key_down() -> Result<()> {
let process = Process::new("cs2.exe")?;
let input_system_base = process
.get_module_by_name("inputsystem.dll")
.expect("unable to find inputsystem.dll")
.base();
let input_system_offset = get_offset_value("inputsystem.dll", "dwInputSystem")?;
let input_system = input_system_base + input_system_offset as usize;
let is_key_down = |key_code: i32| -> bool {
let element = process
.read_memory::<i32>((input_system + 0x4 * (key_code as usize / 32) + 0x12A0).into())
.unwrap_or_default();
unsafe { _bittest(&element, key_code & 0x1F) != 0 }
};
// Sleep for a second, so we're able to test.
thread::sleep(Duration::from_secs(1));
// See https://www.unknowncheats.me/forum/3855779-post889.html for button codes.
println!("insert key down: {}", is_key_down(73));
Ok(())
}
#[test]
fn local_player_controller() -> Result<()> {
let process = Process::new("cs2.exe")?;
let client_base = process
.get_module_by_name("client.dll")
.expect("unable to find client.dll")
.base();
let local_player_controller_offset =
get_offset_value("client.dll", "dwLocalPlayerController")?;
let player_name_offset =
get_class_field("client.dll", "CBasePlayerController", "m_iszPlayerName")?;
let local_player_controller =
process.read_memory::<usize>(client_base + local_player_controller_offset as usize)?;
let player_name =
process.read_string((local_player_controller + player_name_offset as usize).into())?;
println!("local player name: {}", player_name);
Ok(())
}
#[test]
fn local_player_pawn() -> Result<()> {
#[derive(Debug)]
#[repr(C)]
struct Vector3D {
x: f32,
y: f32,
z: f32,
}
let process = Process::new("cs2.exe")?;
let client_base = process
.get_module_by_name("client.dll")
.expect("unable to find client.dll")
.base();
let local_player_pawn_offset = get_offset_value("client.dll", "dwLocalPlayerPawn")?;
let game_scene_node_offset =
get_class_field("client.dll", "C_BaseEntity", "m_pGameSceneNode")?;
let absolute_origin_offset =
get_class_field("client.dll", "CGameSceneNode", "m_vecAbsOrigin")?;
let local_player_pawn =
process.read_memory::<usize>(client_base + local_player_pawn_offset as usize)?;
let game_scene_node = process
.read_memory::<usize>((local_player_pawn + game_scene_node_offset as usize).into())?;
let absolute_origin = process
.read_memory::<Vector3D>((game_scene_node + absolute_origin_offset as usize).into())?;
println!("local player origin: {:?}", absolute_origin);
Ok(())
}
#[test]
fn window_size() -> Result<()> {
let process = Process::new("cs2.exe")?;
let engine_base = process
.get_module_by_name("engine2.dll")
.expect("unable to find engine2.dll")
.base();
let window_width_offset = get_offset_value("engine2.dll", "dwWindowWidth")?;
let window_height_offset = get_offset_value("engine2.dll", "dwWindowHeight")?;
let window_width =
process.read_memory::<u32>(engine_base + window_width_offset as usize)?;
let window_height =
process.read_memory::<u32>(engine_base + window_height_offset as usize)?;
println!("window size: {}x{}", window_width, window_height);
Ok(())
}
}

View File

@@ -1,65 +0,0 @@
use anyhow::Result;
use simplelog::{debug, info};
use super::{generate_files, Entries, Entry};
use crate::builder::FileBuilderEnum;
use crate::os::Process;
use crate::sdk::SchemaSystem;
pub fn dump_schemas(
process: &Process,
builders: &mut Vec<FileBuilderEnum>,
file_path: &str,
indent: usize,
) -> Result<()> {
let schema_system = SchemaSystem::new(&process)?;
for type_scope in schema_system.type_scopes()? {
let module_name = type_scope.module_name()?;
info!("Generating files for <blue>{}</>...", module_name);
let mut entries = Entries::new();
for class in type_scope.classes()? {
let parent_name = class.parent()?.map(|p| p.name().to_string());
debug!(
"<u><bright-yellow>{}</></> : <u><yellow>{}</></>",
class.name(),
parent_name.clone().unwrap_or_default()
);
let container = entries.entry(class.name().replace("::", "_")).or_default();
container.comment = parent_name;
for field in class.fields()? {
let name = field.name()?;
let offset = field.offset()?;
let type_name = field.r#type()?.name()?;
debug!(
"{}<bright-yellow>{}</> = <bright-blue>{:#X}</> // <b><cyan>{}</></>",
" ".repeat(indent),
name,
offset,
type_name
);
container.data.push(Entry {
name,
value: offset as usize,
comment: Some(type_name),
indent: Some(indent),
});
}
}
generate_files(builders, &entries, file_path, &module_name)?;
}
Ok(())
}

44
src/error.rs Normal file
View File

@@ -0,0 +1,44 @@
use thiserror::Error;
#[derive(Debug, Error)]
pub enum Error {
#[error(transparent)]
Fmt(#[from] std::fmt::Error),
#[error("index {idx} is out of bounds for array with length {len}")]
IndexOutOfBounds { idx: usize, len: usize },
#[error(transparent)]
Io(#[from] std::io::Error),
#[error(transparent)]
Memflow(#[from] memflow::error::Error),
#[error(transparent)]
Serde(#[from] serde_json::Error),
#[error("unable to parse signature")]
SignatureInvalid,
#[error("unable to find signature for: {0}")]
SignatureNotFound(String),
#[error("{0}")]
Other(&'static str),
}
impl<T> From<memflow::error::PartialError<T>> for Error {
#[inline]
fn from(err: memflow::error::PartialError<T>) -> Self {
Error::Memflow(err.into())
}
}
impl From<skidscan::SignatureParseError> for Error {
#[inline]
fn from(_err: skidscan::SignatureParseError) -> Self {
Error::SignatureInvalid
}
}
pub type Result<T> = std::result::Result<T, Error>;

View File

@@ -1,132 +1,148 @@
#![allow(dead_code)]
#![feature(lazy_cell)]
use std::fs;
use std::path::Path;
use std::path::PathBuf;
use std::time::Instant;
use std::{env, fs};
use anyhow::{bail, Result};
use clap::*;
use clap::Parser;
use log::{info, Level};
use log::LevelFilter;
use memflow::prelude::v1::*;
use simplelog::{info, ColorChoice, ConfigBuilder, TermLogger, TerminalMode};
use simplelog::{ColorChoice, TermLogger};
use builder::*;
use dumper::{dump_interfaces, dump_offsets, dump_schemas};
use os::Process;
use config::CONFIG;
use error::Result;
use output::Results;
mod builder;
mod analysis;
mod config;
mod dumper;
mod os;
mod sdk;
#[derive(Debug, Parser)]
#[command(name = "cs2-dumper")]
#[command(author = "a2x")]
#[command(version = "1.1.5")]
struct Args {
/// Whether to dump interfaces.
#[arg(short, long)]
interfaces: bool,
/// Whether to dump offsets.
#[arg(short, long)]
offsets: bool,
/// Whether to dump schema classes.
#[arg(short, long)]
schemas: bool,
/// List of file builders to use.
/// Valid values: `.cs`, `.hpp`, `.json`, `.py`, `.rs`, `.yaml`.
#[arg(
short,
long,
value_parser = map_file_extension_to_builder,
value_delimiter = ',',
default_values = [".cs", ".hpp", ".json", ".py", ".rs", ".yaml"],
)]
builders: Vec<FileBuilderEnum>,
/// Indentation level for generated files.
/// Defaults to 4 spaces.
#[arg(long, default_value_t = 4)]
indent: usize,
/// Output directory for generated files.
/// Defaults to `generated`.
#[arg(long, default_value = config::DEFAULT_OUT_DIR)]
output: String,
/// Enable verbose output.
#[arg(short, long)]
verbose: bool,
}
mod error;
mod output;
mod source_engine;
fn main() -> Result<()> {
let Args {
interfaces,
offsets,
schemas,
mut builders,
indent,
output,
verbose,
} = Args::parse();
let start_time = Instant::now();
let log_level = if verbose {
LevelFilter::Debug
} else {
LevelFilter::Info
};
let config = ConfigBuilder::new().add_filter_ignore_str("goblin").build();
TermLogger::init(log_level, config, TerminalMode::Mixed, ColorChoice::Auto)?;
if !Path::new(config::OFFSETS_CONF).exists() {
bail!("Missing {} file", config::OFFSETS_CONF);
}
let matches = parse_args();
let (conn_name, conn_args, indent_size, out_dir) = extract_args(&matches)?;
// Create the output directory if it doesn't exist.
fs::create_dir_all(&output)?;
fs::create_dir_all(&out_dir)?;
let mut process = Process::new(config::PROC_NAME)?;
let os = if let Some(conn_name) = conn_name {
let inventory = Inventory::scan();
let now = Instant::now();
let os_name = match env::consts::OS {
"linux" => "linux",
"windows" => "win32",
_ => panic!("unsupported os"),
};
let all = !(interfaces || offsets || schemas);
inventory
.builder()
.connector(&conn_name)
.args(conn_args)
.os(os_name)
.build()?
} else {
// Fallback to the native OS layer if no connector name was provided.
memflow_native::create_os(&Default::default(), Default::default())?
};
if schemas || all {
dump_schemas(&mut process, &mut builders, &output, indent)?;
}
let mut process = os.into_process_by_name(&CONFIG.executable)?;
if interfaces || all {
dump_interfaces(&mut process, &mut builders, &output, indent)?;
}
let buttons = analysis::buttons(&mut process)?;
let interfaces = analysis::interfaces(&mut process)?;
let offsets = analysis::offsets(&mut process)?;
let schemas = analysis::schemas(&mut process)?;
if offsets || all {
dump_offsets(&mut process, &mut builders, &output, indent)?;
}
let results = Results::new(buttons, interfaces, offsets, schemas);
info!(
"<on-green>Done!</> <green>Time elapsed: <b>{:?}</></>",
now.elapsed()
);
results.dump_all(&out_dir, indent_size)?;
info!("finished in {:?}", start_time.elapsed());
Ok(())
}
fn map_file_extension_to_builder(extension: &str) -> Result<FileBuilderEnum, &'static str> {
match extension {
".cs" => Ok(FileBuilderEnum::CSharpFileBuilder(CSharpFileBuilder)),
".hpp" => Ok(FileBuilderEnum::CppFileBuilder(CppFileBuilder)),
".json" => Ok(FileBuilderEnum::JsonFileBuilder(JsonFileBuilder::default())),
".py" => Ok(FileBuilderEnum::PythonFileBuilder(PythonFileBuilder)),
".rs" => Ok(FileBuilderEnum::RustFileBuilder(RustFileBuilder)),
".yaml" => Ok(FileBuilderEnum::YamlFileBuilder(YamlFileBuilder)),
_ => Err("Invalid file extension"),
}
fn parse_args() -> ArgMatches {
Command::new("cs2-dumper")
.version(crate_version!())
.author(crate_authors!())
.arg(
Arg::new("verbose")
.help("Increase logging verbosity. Can be specified multiple times.")
.short('v')
.action(ArgAction::Count),
)
.arg(
Arg::new("connector")
.help("The name of the memflow connector to use.")
.long("connector")
.short('c')
.required(false),
)
.arg(
Arg::new("connector-args")
.help("Additional arguments to supply to the connector.")
.long("connector-args")
.short('a')
.required(false),
)
.arg(
Arg::new("output")
.help("The output directory to write the generated files to.")
.long("output")
.short('o')
.default_value("output")
.value_parser(value_parser!(PathBuf))
.required(false),
)
.arg(
Arg::new("indent-size")
.help("The number of spaces to use per indentation level.")
.long("indent-size")
.short('i')
.default_value("4")
.value_parser(value_parser!(usize))
.required(false),
)
.get_matches()
}
fn extract_args(matches: &ArgMatches) -> Result<(Option<String>, ConnectorArgs, usize, &PathBuf)> {
use std::str::FromStr;
let log_level = match matches.get_count("verbose") {
0 => Level::Error,
1 => Level::Warn,
2 => Level::Info,
3 => Level::Debug,
4 => Level::Trace,
_ => Level::Trace,
};
TermLogger::init(
log_level.to_level_filter(),
Default::default(),
Default::default(),
ColorChoice::Auto,
)
.unwrap();
let conn_name = matches
.get_one::<String>("connector")
.map(|s| s.to_string());
let conn_args = matches
.get_one::<String>("connector-args")
.map(|s| ConnectorArgs::from_str(&s).expect("unable to parse connector arguments"))
.unwrap_or_default();
let indent_size = *matches.get_one::<usize>("indent-size").unwrap();
let out_dir = matches.get_one::<PathBuf>("output").unwrap();
Ok((conn_name, conn_args, indent_size, out_dir))
}

View File

@@ -1,8 +0,0 @@
#[cfg(target_os = "linux")]
pub use module::ModuleEntry;
pub use module::Module;
pub use process::Process;
pub mod module;
pub mod process;

View File

@@ -1,188 +0,0 @@
use anyhow::Result;
#[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;
/// Represents the data associated with a specific module on Linux.
#[cfg(target_os = "linux")]
#[derive(Debug)]
pub struct ModuleEntry {
pub path: PathBuf,
pub start_addr: usize,
pub data: Vec<u8>,
}
/// Represents a module loaded in a Windows process.
#[cfg(target_os = "windows")]
pub struct Module<'a> {
/// The name of the module.
pub name: &'a str,
/// A reference to a slice of bytes containing the module data.
pub data: &'a [u8],
/// The PE file format representation of the module.
pub pe: PE<'a>,
}
/// Represents a module loaded in a Linux process.
#[cfg(target_os = "linux")]
pub struct Module<'a> {
/// The name of the module.
pub name: &'a str,
/// A reference to a slice of bytes containing the module info.
pub module_info: &'a ModuleEntry,
/// The Elf file format representation of the module.
pub elf: Elf<'a>,
}
impl<'a> Module<'a> {
#[cfg(target_os = "windows")]
pub fn parse(name: &'a str, data: &'a [u8]) -> Result<Self> {
let pe = PE::parse_with_opts(
data,
&ParseOptions {
parse_attribute_certificates: false,
resolve_rva: false,
},
)?;
Ok(Self { name, data, pe })
}
// parse the elf
#[cfg(target_os = "linux")]
pub fn parse(name: &'a str, module_entry: &'a ModuleEntry) -> Result<Self> {
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<sym::Sym> {
let exports: Vec<sym::Sym> = 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<sym::Sym> {
let imports: Vec<sym::Sym> = 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<usize> {
self.pe
.exports
.iter()
.find(|e| e.name.unwrap() == name)
.map(|e| self.pe.image_base + e.rva)
}
#[inline]
#[cfg(target_os = "linux")]
pub fn export_by_name(&self, name: &str) -> Option<usize> {
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<usize> {
self.pe
.imports
.iter()
.find(|i| i.name.to_string() == name)
.map(|i| self.pe.image_base + i.rva)
}
#[inline]
#[cfg(target_os = "linux")]
pub fn get_import_by_name(&self, name: &str) -> Option<usize> {
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
.optional_header
.expect("optional header not found")
.windows_fields
.size_of_image
}
}

View File

@@ -1,389 +0,0 @@
use super::Module;
#[cfg(target_os = "linux")]
use super::ModuleEntry;
use anyhow::{bail, Result};
use std::collections::HashMap;
use std::ffi::c_void;
use std::mem;
#[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.
#[cfg(target_os = "windows")]
#[derive(Debug)]
pub struct Process {
/// ID of the process.
id: u32,
/// Handle to the process.
handle: HANDLE,
/// A HashMap containing the name of each module and its corresponding raw data.
modules: HashMap<String, Vec<u8>>,
}
/// Represents a Linux process.
#[cfg(target_os = "linux")]
#[derive(Debug)]
pub struct Process {
/// PID of the process.
pid: u32,
/// A HashMap containing the name of each module and its corresponding data.
modules: HashMap<String, ModuleEntry>,
}
impl Process {
#[cfg(target_os = "windows")]
pub fn new(name: &str) -> Result<Self> {
let id = Self::get_process_id_by_name(name)?;
let handle = unsafe { OpenProcess(PROCESS_ALL_ACCESS, false, id) }?;
let mut process = Self {
id,
handle,
modules: HashMap::new(),
};
process.parse_loaded_modules()?;
Ok(process)
}
#[cfg(target_os = "linux")]
pub fn new(name: &str) -> Result<Self> {
let pid = Self::get_process_pid_by_name(name)?;
let mut process = Self {
pid,
modules: HashMap::new(),
};
process.parse_loaded_modules()?;
Ok(process)
}
#[cfg(target_os = "windows")]
pub fn find_pattern(&self, module_name: &str, pattern: &str) -> Option<usize> {
let module = self.get_module_by_name(module_name)?;
let pattern_bytes = Self::pattern_to_bytes(pattern);
for (i, window) in module.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
}
#[cfg(target_os = "linux")]
pub fn find_pattern(&self, module_name: &str, pattern: &str) -> Option<usize> {
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<Module<'a>> {
self.modules
.get(name)
.map(|data| Module::parse(name, data).unwrap())
}
pub fn modules(&self) -> Result<Vec<Module>> {
let mut modules = Vec::new();
for (name, data) in &self.modules {
modules.push(Module::parse(name, data)?);
}
Ok(modules)
}
pub fn read_memory<T>(&self, address: usize) -> Result<T> {
let mut buffer: T = unsafe { mem::zeroed() };
self.read_memory_raw(
address,
&mut buffer as *const _ as *mut _,
mem::size_of::<T>(),
)?;
Ok(buffer)
}
#[cfg(target_os = "windows")]
pub fn read_memory_raw(&self, address: usize, buffer: *mut c_void, size: usize) -> Result<()> {
unsafe {
ReadProcessMemory(
self.handle,
address as *mut _,
buffer,
size,
Some(ptr::null_mut()),
)
}
.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.pid);
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<String> {
let mut buffer = Vec::new();
for i in 0.. {
match self.read_memory::<u8>(address + i) {
Ok(byte) if byte != 0 => buffer.push(byte),
_ => break,
}
}
Ok(String::from_utf8(buffer)?)
}
pub fn read_string_length(&self, address: usize, length: usize) -> Result<String> {
let mut buffer = vec![0; length];
self.read_memory_raw(address, buffer.as_mut_ptr() as *mut _, length)?;
if let Some(end) = buffer.iter().position(|&x| x == 0) {
buffer.truncate(end);
}
Ok(String::from_utf8(buffer)?)
}
pub fn resolve_jmp(
&self,
address: usize,
offset: Option<usize>,
length: Option<usize>,
) -> Result<usize> {
// The displacement value can be negative.
let displacement = self.read_memory::<i32>(address + offset.unwrap_or(0x1))?;
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(
&self,
address: usize,
offset: Option<usize>,
length: Option<usize>,
) -> Result<usize> {
// The displacement value can be negative.
let displacement = self.read_memory::<i32>(address + offset.unwrap_or(0x3))?;
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<u32> {
let snapshot = unsafe { CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0) }?;
let mut entry = PROCESSENTRY32 {
dwSize: mem::size_of::<PROCESSENTRY32>() as u32,
..Default::default()
};
unsafe {
Process32First(snapshot, &mut entry)?;
while Process32Next(snapshot, &mut entry).is_ok() {
let name = CStr::from_ptr(&entry.szExeFile as *const _ as *const _).to_str()?;
if name == process_name {
return Ok(entry.th32ProcessID);
}
}
}
bail!("Process not found: {}", process_name)
}
#[cfg(target_os = "linux")]
fn get_process_pid_by_name(process_name: &str) -> Result<u32> {
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) }?;
let mut entry = MODULEENTRY32 {
dwSize: mem::size_of::<MODULEENTRY32>() as u32,
..Default::default()
};
unsafe {
Module32First(snapshot, &mut entry)?;
while Module32Next(snapshot, &mut entry).is_ok() {
let name = CStr::from_ptr(&entry.szModule as *const _ as *const _).to_str()?;
let mut data = vec![0; entry.modBaseSize as usize];
if let Ok(_) = self.read_memory_raw(
entry.modBaseAddr as _,
data.as_mut_ptr() as *mut _,
data.len(),
) {
self.modules.insert(name.to_string(), data);
}
}
}
Ok(())
}
#[cfg(target_os = "linux")]
fn parse_loaded_modules(&mut self) -> Result<()> {
let process = process::Process::new(self.pid as i32)?;
let mut modules_info: HashMap<String, ((u64, u64), PathBuf)> = 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<String> {
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<Vec<u8>> {
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<i32> {
pattern
.split_whitespace()
.map(|s| {
if s == "?" {
-1
} else {
i32::from_str_radix(s, 16).unwrap_or(0)
}
})
.collect()
}
}
#[cfg(target_os = "windows")]
impl Drop for Process {
fn drop(&mut self) {
if !self.handle.is_invalid() {
unsafe { CloseHandle(self.handle).unwrap() }
}
}
}

91
src/output/buttons.rs Normal file
View File

@@ -0,0 +1,91 @@
use std::env;
use std::fmt::Write;
use heck::{AsPascalCase, AsShoutySnakeCase, AsSnakeCase};
use super::{Button, CodeGen, Results};
use crate::error::Result;
impl CodeGen for Vec<Button> {
fn to_cs(&self, results: &Results, indent_size: usize) -> Result<String> {
self.write_content(results, indent_size, |fmt| {
fmt.block("namespace CS2Dumper", |fmt| {
writeln!(fmt, "// Module: {}", get_module_name())?;
fmt.block("public static class Buttons", |fmt| {
for button in self {
writeln!(
fmt,
"public const nint {} = {:#X};",
AsPascalCase(&button.name),
button.value
)?;
}
Ok(())
})
})?;
Ok(())
})
}
fn to_hpp(&self, results: &Results, indent_size: usize) -> Result<String> {
self.write_content(results, indent_size, |fmt| {
writeln!(fmt, "#pragma once\n")?;
writeln!(fmt, "#include <cstddef>\n")?;
fmt.block("namespace cs2_dumper", |fmt| {
writeln!(fmt, "// Module: {}", get_module_name())?;
fmt.block("namespace buttons", |fmt| {
for button in self {
writeln!(
fmt,
"constexpr std::ptrdiff_t {} = {:#X};",
AsSnakeCase(&button.name),
button.value
)?;
}
Ok(())
})
})?;
Ok(())
})
}
fn to_rs(&self, results: &Results, indent_size: usize) -> Result<String> {
self.write_content(results, indent_size, |fmt| {
fmt.block("pub mod cs2_dumper", |fmt| {
writeln!(fmt, "// Module: {}", get_module_name())?;
fmt.block("pub mod buttons", |fmt| {
for button in self {
writeln!(
fmt,
"pub const {}: usize = {:#X};",
AsShoutySnakeCase(&button.name),
button.value
)?;
}
Ok(())
})
})?;
Ok(())
})
}
}
#[inline]
fn get_module_name() -> &'static str {
match env::consts::OS {
"linux" => "libclient.so",
"windows" => "client.dll",
_ => panic!("unsupported os"),
}
}

78
src/output/formatter.rs Normal file
View File

@@ -0,0 +1,78 @@
use std::fmt::{self, Write};
#[derive(Debug)]
pub struct Formatter<'a> {
/// Write destination.
pub out: &'a mut String,
/// Number of spaces per indentation level.
pub indent_size: usize,
/// Current indentation level.
pub indent_level: usize,
}
impl<'a> Formatter<'a> {
pub fn new(out: &'a mut String, indent_size: usize) -> Self {
Self {
out,
indent_size,
indent_level: 0,
}
}
pub fn block<F>(&mut self, heading: &str, f: F) -> fmt::Result
where
F: FnOnce(&mut Self) -> fmt::Result,
{
write!(self, "{} {{\n", heading)?;
self.indent(f)?;
write!(self, "}}\n")?;
Ok(())
}
pub fn indent<F, R>(&mut self, f: F) -> R
where
F: FnOnce(&mut Self) -> R,
{
self.indent_level += 1;
let ret = f(self);
self.indent_level -= 1;
ret
}
#[inline]
#[rustfmt::skip]
fn push_indentation(&mut self) {
if self.indent_level > 0 {
self.out.push_str(&" ".repeat(self.indent_level * self.indent_size));
}
}
}
impl<'a> Write for Formatter<'a> {
fn write_str(&mut self, s: &str) -> fmt::Result {
let mut lines = s.lines().peekable();
while let Some(line) = lines.next() {
// Add indentation before the line if necessary.
if self.out.ends_with('\n') && !line.is_empty() {
self.push_indentation();
}
self.out.push_str(line);
if lines.peek().is_some() || s.ends_with('\n') {
self.out.push('\n');
}
}
Ok(())
}
}

109
src/output/interfaces.rs Normal file
View File

@@ -0,0 +1,109 @@
use std::fmt::Write;
use heck::{AsPascalCase, AsShoutySnakeCase, AsSnakeCase};
use super::{format_module_name, CodeGen, InterfaceMap, Results};
use crate::error::Result;
impl CodeGen for InterfaceMap {
fn to_cs(&self, results: &Results, indent_size: usize) -> Result<String> {
self.write_content(results, indent_size, |fmt| {
fmt.block("namespace CS2Dumper.Interfaces", |fmt| {
for (module_name, ifaces) in self {
writeln!(fmt, "// Module: {}", module_name)?;
fmt.block(
&format!(
"public static class {}",
AsPascalCase(format_module_name(module_name))
),
|fmt| {
for iface in ifaces {
writeln!(
fmt,
"public const nint {} = {:#X};",
AsPascalCase(&iface.name),
iface.value
)?;
}
Ok(())
},
)?;
}
Ok(())
})?;
Ok(())
})
}
fn to_hpp(&self, results: &Results, indent_size: usize) -> Result<String> {
self.write_content(results, indent_size, |fmt| {
writeln!(fmt, "#pragma once\n")?;
writeln!(fmt, "#include <cstddef>\n")?;
fmt.block("namespace cs2_dumper", |fmt| {
fmt.block("namespace interfaces", |fmt| {
for (module_name, ifaces) in self {
writeln!(fmt, "// Module: {}", module_name)?;
fmt.block(
&format!("namespace {}", AsSnakeCase(format_module_name(module_name))),
|fmt| {
for iface in ifaces {
writeln!(
fmt,
"constexpr std::ptrdiff_t {} = {:#X};",
AsSnakeCase(&iface.name),
iface.value
)?;
}
Ok(())
},
)?;
}
Ok(())
})
})?;
Ok(())
})
}
fn to_rs(&self, results: &Results, indent_size: usize) -> Result<String> {
self.write_content(results, indent_size, |fmt| {
fmt.block("pub mod cs2_dumper", |fmt| {
fmt.block("pub mod interfaces", |fmt| {
for (module_name, ifaces) in self {
writeln!(fmt, "// Module: {}", module_name)?;
fmt.block(
&format!("pub mod {}", AsSnakeCase(format_module_name(module_name))),
|fmt| {
for iface in ifaces {
writeln!(
fmt,
"pub const {}: usize = {:#X};",
AsShoutySnakeCase(&iface.name),
iface.value
)?;
}
Ok(())
},
)?;
}
Ok(())
})
})?;
Ok(())
})
}
}

178
src/output/mod.rs Normal file
View File

@@ -0,0 +1,178 @@
use std::fmt::Write;
use std::path::Path;
use std::{env, fs};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use formatter::Formatter;
use crate::analysis::*;
use crate::error::Result;
mod buttons;
mod formatter;
mod interfaces;
mod offsets;
mod schemas;
#[derive(Serialize)]
#[serde(rename_all = "snake_case")]
enum Item<'a> {
Buttons(&'a Vec<Button>),
Interfaces(&'a InterfaceMap),
Offsets(&'a OffsetMap),
Schemas(&'a SchemaMap),
}
impl<'a> Item<'a> {
fn generate(&self, results: &Results, indent_size: usize, file_type: &str) -> Result<String> {
match file_type {
"cs" => self.to_cs(results, indent_size),
"hpp" => self.to_hpp(results, indent_size),
"rs" => self.to_rs(results, indent_size),
"json" => serde_json::to_string_pretty(self).map_err(Into::into),
_ => unreachable!(),
}
}
}
trait CodeGen {
fn to_cs(&self, results: &Results, indent_size: usize) -> Result<String>;
fn to_hpp(&self, results: &Results, indent_size: usize) -> Result<String>;
fn to_rs(&self, results: &Results, indent_size: usize) -> Result<String>;
fn write_content<F>(&self, results: &Results, indent_size: usize, callback: F) -> Result<String>
where
F: FnOnce(&mut Formatter<'_>) -> Result<()>,
{
let mut buf = String::new();
let mut fmt = Formatter::new(&mut buf, indent_size);
results.write_banner(&mut fmt)?;
callback(&mut fmt)?;
Ok(buf)
}
}
impl<'a> CodeGen for Item<'a> {
fn to_cs(&self, results: &Results, indent_size: usize) -> Result<String> {
match self {
Item::Buttons(buttons) => buttons.to_cs(results, indent_size),
Item::Interfaces(interfaces) => interfaces.to_cs(results, indent_size),
Item::Offsets(offsets) => offsets.to_cs(results, indent_size),
Item::Schemas(schemas) => schemas.to_cs(results, indent_size),
}
}
fn to_hpp(&self, results: &Results, indent_size: usize) -> Result<String> {
match self {
Item::Buttons(buttons) => buttons.to_hpp(results, indent_size),
Item::Interfaces(interfaces) => interfaces.to_hpp(results, indent_size),
Item::Offsets(offsets) => offsets.to_hpp(results, indent_size),
Item::Schemas(schemas) => schemas.to_hpp(results, indent_size),
}
}
fn to_rs(&self, results: &Results, indent_size: usize) -> Result<String> {
match self {
Item::Buttons(buttons) => buttons.to_rs(results, indent_size),
Item::Interfaces(interfaces) => interfaces.to_rs(results, indent_size),
Item::Offsets(offsets) => offsets.to_rs(results, indent_size),
Item::Schemas(schemas) => schemas.to_rs(results, indent_size),
}
}
}
#[derive(Deserialize, Serialize)]
pub struct Results {
/// Timestamp of the dump.
pub timestamp: DateTime<Utc>,
/// List of buttons to dump.
pub buttons: Vec<Button>,
/// Map of interfaces to dump.
pub interfaces: InterfaceMap,
/// Map of offsets to dump.
pub offsets: OffsetMap,
/// Map of schema classes and enums to dump.
pub schemas: SchemaMap,
}
impl Results {
pub fn new(
buttons: Vec<Button>,
interfaces: InterfaceMap,
offsets: OffsetMap,
schemas: SchemaMap,
) -> Self {
Self {
timestamp: Utc::now(),
buttons,
interfaces,
offsets,
schemas,
}
}
pub fn dump_all<P: AsRef<Path>>(&self, out_dir: P, indent_size: usize) -> Result<()> {
let items = [
("buttons", Item::Buttons(&self.buttons)),
("interfaces", Item::Interfaces(&self.interfaces)),
("offsets", Item::Offsets(&self.offsets)),
("schemas", Item::Schemas(&self.schemas)),
];
// TODO: Make this user-configurable.
let file_types = ["cs", "hpp", "json", "rs"];
for (file_name, item) in &items {
for &file_type in &file_types {
let content = item.generate(self, indent_size, file_type)?;
self.dump_file(out_dir.as_ref(), file_name, file_type, &content)?;
}
}
Ok(())
}
fn dump_file<P: AsRef<Path>>(
&self,
out_dir: P,
file_name: &str,
file_type: &str,
content: &str,
) -> Result<()> {
let file_path = out_dir
.as_ref()
.join(format!("{}.{}", file_name, file_type));
fs::write(file_path, content)?;
Ok(())
}
fn write_banner(&self, fmt: &mut Formatter<'_>) -> Result<()> {
writeln!(fmt, "// Generated using https://github.com/a2x/cs2-dumper")?;
writeln!(fmt, "// {}\n", self.timestamp)?;
Ok(())
}
}
pub fn format_module_name(module_name: &String) -> String {
let extension = match env::consts::OS {
"linux" => ".so",
"windows" => ".dll",
_ => panic!("unsupported os"),
};
module_name.strip_suffix(extension).unwrap().to_string()
}

109
src/output/offsets.rs Normal file
View File

@@ -0,0 +1,109 @@
use std::fmt::Write;
use heck::{AsPascalCase, AsShoutySnakeCase, AsSnakeCase};
use super::{format_module_name, CodeGen, OffsetMap, Results};
use crate::error::Result;
impl CodeGen for OffsetMap {
fn to_cs(&self, results: &Results, indent_size: usize) -> Result<String> {
self.write_content(results, indent_size, |fmt| {
fmt.block("namespace CS2Dumper.Offsets", |fmt| {
for (module_name, offsets) in self {
writeln!(fmt, "// Module: {}", module_name)?;
fmt.block(
&format!(
"public static class {}",
AsPascalCase(format_module_name(module_name))
),
|fmt| {
for offset in offsets {
writeln!(
fmt,
"public const nint {} = {:#X};",
AsPascalCase(&offset.name),
offset.value
)?;
}
Ok(())
},
)?;
}
Ok(())
})?;
Ok(())
})
}
fn to_hpp(&self, results: &Results, indent_size: usize) -> Result<String> {
self.write_content(results, indent_size, |fmt| {
writeln!(fmt, "#pragma once\n")?;
writeln!(fmt, "#include <cstddef>\n")?;
fmt.block("namespace cs2_dumper", |fmt| {
fmt.block("namespace offsets", |fmt| {
for (module_name, offsets) in self {
writeln!(fmt, "// Module: {}", module_name)?;
fmt.block(
&format!("namespace {}", AsSnakeCase(format_module_name(module_name))),
|fmt| {
for offset in offsets {
writeln!(
fmt,
"constexpr std::ptrdiff_t {} = {:#X};",
AsSnakeCase(&offset.name),
offset.value
)?;
}
Ok(())
},
)?;
}
Ok(())
})
})?;
Ok(())
})
}
fn to_rs(&self, results: &Results, indent_size: usize) -> Result<String> {
self.write_content(results, indent_size, |fmt| {
fmt.block("pub mod cs2_dumper", |fmt| {
fmt.block("pub mod offsets", |fmt| {
for (module_name, offsets) in self {
writeln!(fmt, "// Module: {}", module_name)?;
fmt.block(
&format!("pub mod {}", AsSnakeCase(format_module_name(module_name))),
|fmt| {
for offset in offsets {
writeln!(
fmt,
"pub const {}: usize = {:#X};",
AsShoutySnakeCase(&offset.name),
offset.value
)?;
}
Ok(())
},
)?;
}
Ok(())
})
})?;
Ok(())
})
}
}

313
src/output/schemas.rs Normal file
View File

@@ -0,0 +1,313 @@
use std::fmt::{self, Write};
use heck::{AsPascalCase, AsShoutySnakeCase, AsSnakeCase};
use super::{format_module_name, CodeGen, Formatter, Results, SchemaMap};
use crate::analysis::ClassMetadata;
use crate::error::Result;
impl CodeGen for SchemaMap {
fn to_cs(&self, results: &Results, indent_size: usize) -> Result<String> {
self.write_content(results, indent_size, |fmt| {
fmt.block("namespace CS2Dumper.Schemas", |fmt| {
for (module_name, (classes, enums)) in self {
writeln!(fmt, "// Module: {}", module_name)?;
writeln!(fmt, "// Classes count: {}", classes.len())?;
writeln!(fmt, "// Enums count: {}", enums.len())?;
fmt.block(
&format!(
"public static class {}",
AsPascalCase(format_module_name(module_name))
),
|fmt| {
for enum_ in enums {
let ty = match enum_.ty.as_str() {
"int8" => "sbyte",
"int16" => "short",
"int32" => "int",
"int64" => "long",
_ => continue,
};
writeln!(fmt, "// Alignment: {}", enum_.alignment)?;
writeln!(fmt, "// Members count: {}", enum_.size)?;
fmt.block(
&format!("public enum {} : {}", AsPascalCase(&enum_.name), ty),
|fmt| {
let members = enum_
.members
.iter()
.map(|member| {
format!(
"{} = {}",
AsPascalCase(&member.name),
member.value
)
})
.collect::<Vec<_>>()
.join(",\n");
writeln!(fmt, "{}", members)
},
)?;
}
for class in classes {
let parent_name = class
.parent
.as_ref()
.map(|parent| format!("{}", AsPascalCase(&parent.name)))
.unwrap_or_else(|| "None".to_string());
writeln!(fmt, "// Parent: {}", parent_name)?;
writeln!(fmt, "// Fields count: {}", class.fields.len())?;
write_metadata(fmt, &class.metadata)?;
fmt.block(
&format!("public static class {}", AsPascalCase(&class.name)),
|fmt| {
for field in &class.fields {
writeln!(
fmt,
"public const nint {} = {:#X}; // {}",
AsPascalCase(&field.name),
field.offset,
field.ty
)?;
}
Ok(())
},
)?;
}
Ok(())
},
)?;
}
Ok(())
})?;
Ok(())
})
}
fn to_hpp(&self, results: &Results, indent_size: usize) -> Result<String> {
self.write_content(results, indent_size, |fmt| {
writeln!(fmt, "#pragma once\n")?;
writeln!(fmt, "#include <cstddef>\n")?;
fmt.block("namespace cs2_dumper", |fmt| {
fmt.block("namespace schemas", |fmt| {
for (module_name, (classes, enums)) in self {
writeln!(fmt, "// Module: {}", module_name)?;
writeln!(fmt, "// Classes count: {}", classes.len())?;
writeln!(fmt, "// Enums count: {}", enums.len())?;
fmt.block(
&format!("namespace {}", AsSnakeCase(format_module_name(module_name))),
|fmt| {
for enum_ in enums {
let ty = match enum_.ty.as_str() {
"int8" => "int8_t",
"int16" => "int16_t",
"int32" => "int32_t",
"int64" => "int64_t",
_ => continue,
};
writeln!(fmt, "// Alignment: {}", enum_.alignment)?;
writeln!(fmt, "// Members count: {}", enum_.size)?;
fmt.block(
&format!(
"enum class {} : {}",
AsSnakeCase(&enum_.name),
ty
),
|fmt| {
let members = enum_
.members
.iter()
.map(|member| {
format!(
"{} = {}",
AsSnakeCase(&member.name),
member.value
)
})
.collect::<Vec<_>>()
.join(",\n");
writeln!(fmt, "{}", members)
},
)?;
}
for class in classes {
let parent_name = class
.parent
.as_ref()
.map(|parent| format!("{}", AsSnakeCase(&parent.name)))
.unwrap_or_else(|| "None".to_string());
writeln!(fmt, "// Parent: {}", parent_name)?;
writeln!(fmt, "// Fields count: {}", class.fields.len())?;
write_metadata(fmt, &class.metadata)?;
fmt.block(
&format!("namespace {}", AsSnakeCase(&class.name)),
|fmt| {
for field in &class.fields {
writeln!(
fmt,
"constexpr std::ptrdiff_t {} = {:#X}; // {}",
AsSnakeCase(&field.name),
field.offset,
field.ty
)?;
}
Ok(())
},
)?;
}
Ok(())
},
)?;
}
Ok(())
})
})?;
Ok(())
})
}
fn to_rs(&self, results: &Results, indent_size: usize) -> Result<String> {
self.write_content(results, indent_size, |fmt| {
fmt.block("pub mod cs2_dumper", |fmt| {
fmt.block("pub mod schemas", |fmt| {
for (module_name, (classes, enums)) in self {
writeln!(fmt, "// Module: {}", module_name)?;
writeln!(fmt, "// Classes count: {}", classes.len())?;
writeln!(fmt, "// Enums count: {}", enums.len())?;
fmt.block(
&format!("pub mod {}", AsSnakeCase(format_module_name(module_name))),
|fmt| {
for enum_ in enums {
let ty = match enum_.ty.as_str() {
"int8" => "i8",
"int16" => "i16",
"int32" => "i32",
"int64" => "i64",
_ => continue,
};
writeln!(fmt, "// Alignment: {}", enum_.alignment)?;
writeln!(fmt, "// Members count: {}", enum_.size)?;
fmt.block(
&format!(
"#[repr({})]\npub enum {}",
ty,
AsPascalCase(&enum_.name),
),
|fmt| {
// TODO: Handle the case where multiple members share
// the same value.
let members = enum_
.members
.iter()
.map(|member| {
format!(
"{} = {}",
AsPascalCase(&member.name),
member.value
)
})
.collect::<Vec<_>>()
.join(",\n");
writeln!(fmt, "{}", members)
},
)?;
}
for class in classes {
let parent_name = class
.parent
.as_ref()
.map(|parent| format!("{}", AsSnakeCase(&parent.name)))
.unwrap_or_else(|| "None".to_string());
writeln!(fmt, "// Parent: {}", parent_name)?;
writeln!(fmt, "// Fields count: {}", class.fields.len())?;
write_metadata(fmt, &class.metadata)?;
fmt.block(
&format!("pub mod {}", AsSnakeCase(&class.name)),
|fmt| {
for field in &class.fields {
writeln!(
fmt,
"pub const {}: usize = {:#X}; // {}",
AsShoutySnakeCase(&field.name),
field.offset,
field.ty
)?;
}
Ok(())
},
)?;
}
Ok(())
},
)?;
}
Ok(())
})
})?;
Ok(())
})
}
}
fn write_metadata(fmt: &mut Formatter<'_>, metadata: &[ClassMetadata]) -> fmt::Result {
if metadata.is_empty() {
return Ok(());
}
writeln!(fmt, "//")?;
writeln!(fmt, "// Metadata:")?;
for metadata in metadata {
match metadata {
ClassMetadata::NetworkChangeCallback { name } => {
writeln!(fmt, "// NetworkChangeCallback: {}", name)?;
}
ClassMetadata::NetworkVarNames { name, ty } => {
writeln!(fmt, "// NetworkVarNames: {} ({})", name, ty)?;
}
ClassMetadata::Unknown { name } => {
writeln!(fmt, "// {}", name)?;
}
}
}
Ok(())
}

View File

@@ -1,15 +0,0 @@
pub use schema_class_field_data::SchemaClassFieldData;
pub use schema_class_info::SchemaClassInfo;
pub use schema_system::SchemaSystem;
pub use schema_system_type_scope::SchemaSystemTypeScope;
pub use schema_type::SchemaType;
pub use schema_type_declared_class::SchemaTypeDeclaredClass;
pub use utl_ts_hash::UtlTsHash;
pub mod schema_class_field_data;
pub mod schema_class_info;
pub mod schema_system;
pub mod schema_system_type_scope;
pub mod schema_type;
pub mod schema_type_declared_class;
pub mod utl_ts_hash;

View File

@@ -1,32 +0,0 @@
use anyhow::Result;
use super::SchemaType;
use crate::os::Process;
pub struct SchemaClassFieldData<'a> {
process: &'a Process,
address: usize,
}
impl<'a> SchemaClassFieldData<'a> {
pub fn new(process: &'a Process, address: usize) -> Self {
Self { process, address }
}
pub fn name(&self) -> Result<String> {
let name_ptr = self.process.read_memory::<usize>(self.address)?;
self.process.read_string_length(name_ptr.into(), 64)
}
pub fn r#type(&self) -> Result<SchemaType> {
let address = self.process.read_memory::<usize>(self.address + 0x8)?;
Ok(SchemaType::new(self.process, address))
}
pub fn offset(&self) -> Result<u16> {
self.process.read_memory::<u16>(self.address + 0x10)
}
}

View File

@@ -1,65 +0,0 @@
use anyhow::Result;
use super::SchemaClassFieldData;
use crate::os::Process;
pub struct SchemaClassInfo<'a> {
process: &'a Process,
address: usize,
name: String,
}
impl<'a> SchemaClassInfo<'a> {
pub fn new(process: &'a Process, address: usize, name: &str) -> Self {
Self {
process,
address,
name: name.to_string(),
}
}
#[inline]
pub fn name(&self) -> &str {
&self.name
}
pub fn fields(&self) -> Result<Vec<SchemaClassFieldData>> {
let address = self.process.read_memory::<usize>(self.address + 0x28)?;
if address == 0 {
return Ok(Vec::new());
}
let count = self.fields_count()?;
let fields: Vec<SchemaClassFieldData> = (address..address + count as usize * 0x20)
.step_by(0x20)
.map(|address| SchemaClassFieldData::new(self.process, address.into()))
.collect();
Ok(fields)
}
pub fn fields_count(&self) -> Result<u16> {
self.process.read_memory::<u16>(self.address + 0x1C)
}
pub fn parent(&self) -> Result<Option<SchemaClassInfo>> {
let address = self.process.read_memory::<usize>(self.address + 0x38)?;
if address == 0 {
return Ok(None);
}
let parent = self.process.read_memory::<usize>(address + 0x8)?;
let name_ptr = self.process.read_memory::<usize>(parent + 0x8)?;
let name = self.process.read_string(name_ptr.into())?;
Ok(Some(SchemaClassInfo::new(
self.process,
parent.into(),
&name,
)))
}
}

View File

@@ -1,55 +0,0 @@
use std::mem;
use anyhow::{bail, Result};
use super::SchemaSystemTypeScope;
use crate::os::Process;
use crate::config::SCHEMA_CONF;
pub struct SchemaSystem<'a> {
process: &'a Process,
address: usize,
}
impl<'a> SchemaSystem<'a> {
pub fn new(process: &'a Process) -> Result<Self> {
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)?;
Ok(Self { process, address })
}
pub fn type_scopes(&self) -> Result<Vec<SchemaSystemTypeScope>> {
let size = self
.process
.read_memory::<u32>(self.address + SCHEMA_CONF.type_scope_size_offset)?;
if size == 0 {
bail!("no type scopes found");
}
let data = self
.process
.read_memory::<usize>(self.address + SCHEMA_CONF.type_scope_data_offset)?;
let mut addresses = vec![0; size as usize];
self.process.read_memory_raw(
data.into(),
addresses.as_mut_ptr() as *mut _,
addresses.len() * mem::size_of::<usize>(),
)?;
let type_scopes: Vec<SchemaSystemTypeScope> = addresses
.iter()
.map(|&address| SchemaSystemTypeScope::new(self.process, address))
.collect();
Ok(type_scopes)
}
}

View File

@@ -1,47 +0,0 @@
use anyhow::Result;
use super::{SchemaClassInfo, SchemaTypeDeclaredClass, UtlTsHash};
use crate::os::Process;
use crate::config::SCHEMA_CONF;
pub struct SchemaSystemTypeScope<'a> {
process: &'a Process,
address: usize,
}
impl<'a> SchemaSystemTypeScope<'a> {
pub fn new(process: &'a Process, address: usize) -> Self {
Self { process, address }
}
pub fn classes(&self) -> Result<Vec<SchemaClassInfo>> {
let declared_classes = self
.process
.read_memory::<UtlTsHash<*mut SchemaTypeDeclaredClass>>(
self.address + SCHEMA_CONF.declared_classes_offset,
)?;
let classes: Vec<SchemaClassInfo> = declared_classes
.elements(self.process)?
.iter()
.filter_map(|&class_ptr| {
let address = class_ptr as usize;
let declared_class = SchemaTypeDeclaredClass::new(self.process, address);
declared_class
.name()
.ok()
.map(|name| SchemaClassInfo::new(self.process, address, &name))
})
.collect();
Ok(classes)
}
pub fn module_name(&self) -> Result<String> {
self.process.read_string_length(self.address + 0x8, 256)
}
}

View File

@@ -1,69 +0,0 @@
use std::collections::HashMap;
use anyhow::Result;
use lazy_static::lazy_static;
use regex::Regex;
use crate::os::Process;
const TYPE_MAP: &[(&'static str, &'static str)] = &[
("uint8", "uint8_t"),
("uint16", "uint16_t"),
("uint32", "uint32_t"),
("uint64", "uint64_t"),
("int8", "int8_t"),
("int16", "int16_t"),
("int32", "int32_t"),
("int64", "int64_t"),
("float32", "float"),
("float64", "double"),
];
lazy_static! {
static ref REGEX_MAP: HashMap<&'static str, Regex> = {
let mut map = HashMap::with_capacity(TYPE_MAP.len());
for (k, _v) in TYPE_MAP.iter() {
map.insert(*k, Regex::new(&format!(r"\b{}\b", k)).unwrap());
}
map
};
}
pub struct SchemaType<'a> {
process: &'a Process,
address: usize,
}
impl<'a> SchemaType<'a> {
pub fn new(process: &'a Process, address: usize) -> Self {
Self { process, address }
}
pub fn name(&self) -> Result<String> {
let name_ptr = self.process.read_memory::<usize>(self.address + 0x8)?;
let name = self
.process
.read_string(name_ptr.into())?
.replace(" ", "")
.to_string();
Ok(Self::convert_type_name(&name))
}
fn convert_type_name(type_name: &str) -> String {
let mut result = type_name.to_string();
for (k, v) in TYPE_MAP.iter() {
let re = REGEX_MAP.get(*k).unwrap();
result = re.replace_all(&result, &v.to_string()).to_string();
}
result
}
}

View File

@@ -1,20 +0,0 @@
use anyhow::Result;
use crate::os::Process;
pub struct SchemaTypeDeclaredClass<'a> {
process: &'a Process,
address: usize,
}
impl<'a> SchemaTypeDeclaredClass<'a> {
pub fn new(process: &'a Process, address: usize) -> Self {
Self { process, address }
}
pub fn name(&self) -> Result<String> {
let name_ptr = self.process.read_memory::<usize>(self.address + 0x8)?;
self.process.read_string_length(name_ptr, 64)
}
}

View File

@@ -1,159 +0,0 @@
use std::mem::offset_of;
use anyhow::Result;
use crate::os::Process;
#[derive(Debug)]
#[repr(C)]
struct HashFixedDataInternal<T, K> {
ui_key: K, // 0x0010
next: *mut HashFixedDataInternal<T, K>, // 0x0010
data: T, // 0x0010
}
impl<T, K> HashFixedDataInternal<T, K> {
fn next(&self, process: &Process) -> Result<*mut HashFixedDataInternal<T, K>> {
process.read_memory::<*mut HashFixedDataInternal<T, K>>(
(self as *const _ as usize + offset_of!(HashFixedDataInternal<T, K>, next)) as _,
)
}
}
#[derive(Debug)]
#[repr(C)]
struct HashBucketDataInternal<T, K> {
data: T, // 0x0000
next: *mut HashFixedDataInternal<T, K>, // 0x0008
ui_key: K, // 0x0010
}
impl<T, K> HashBucketDataInternal<T, K> {
fn next(&self, process: &Process) -> Result<*mut HashFixedDataInternal<T, K>> {
process.read_memory::<*mut HashFixedDataInternal<T, K>>(
(self as *const _ as usize + offset_of!(HashBucketDataInternal<T, K>, next)) as _,
)
}
}
#[derive(Debug)]
#[repr(C)]
pub struct HashAllocatedData<T, K> {
pad_0: [u8; 0x18], // 0x0000
list: [HashFixedDataInternal<T, K>; 128], // 0x0018
}
impl<T, K> HashAllocatedData<T, K> {
fn list(&self, process: &Process) -> Result<[HashFixedDataInternal<T, K>; 128]> {
process.read_memory::<[HashFixedDataInternal<T, K>; 128]>(
(self as *const _ as usize + offset_of!(HashAllocatedData<T, K>, list)) as _,
)
}
}
#[derive(Debug)]
#[repr(C)]
struct HashUnallocatedData<T, K> {
next: *mut HashUnallocatedData<T, K>, // 0x0000
unknown_1: K, // 0x0008
ui_key: K, // 0x0010
unknown_2: K, // 0x0018
block_list: [HashBucketDataInternal<T, K>; 256], // 0x0020
}
impl<T, K> HashUnallocatedData<T, K> {
fn next(&self, process: &Process) -> Result<*mut HashUnallocatedData<T, K>> {
process.read_memory::<*mut HashUnallocatedData<T, K>>(
(self as *const _ as usize + offset_of!(HashUnallocatedData<T, K>, next)) as _,
)
}
fn ui_key(&self, process: &Process) -> Result<K> {
process.read_memory::<K>(
(self as *const _ as usize + offset_of!(HashUnallocatedData<T, K>, ui_key)) as _,
)
}
fn block_list(&self, process: &Process) -> Result<[HashBucketDataInternal<T, K>; 256]> {
process.read_memory::<[HashBucketDataInternal<T, K>; 256]>(
(self as *const _ as usize + offset_of!(HashUnallocatedData<T, K>, block_list)) as _,
)
}
}
#[derive(Debug)]
#[repr(C)]
struct HashBucket<T, K> {
pad_0: [u8; 0x10], // 0x0000
allocated_data: *const HashAllocatedData<T, K>, // 0x0010
unallocated_data: *const HashUnallocatedData<T, K>, // 0x0018
}
#[derive(Debug)]
#[repr(C)]
struct UtlMemoryPool {
block_size: i32, // 0x0000
blocks_per_blob: i32, // 0x0004
grow_mode: i32, // 0x0008
blocks_allocated: i32, // 0x000C
block_allocated_size: i32, // 0x0010
peak_alloc: i32, // 0x0014
}
impl UtlMemoryPool {
#[inline]
fn block_size(&self) -> i32 {
self.blocks_per_blob
}
#[inline]
fn count(&self) -> i32 {
self.block_allocated_size
}
}
#[derive(Debug)]
#[repr(C)]
pub struct UtlTsHash<T, K = u64> {
entry_memory: UtlMemoryPool, // 0x0000
buckets: HashBucket<T, K>, // 0x0018
}
impl<T, K> UtlTsHash<T, K>
where
T: Copy,
{
#[inline]
pub fn block_size(&self) -> i32 {
self.entry_memory.block_size()
}
#[inline]
pub fn count(&self) -> i32 {
self.entry_memory.count()
}
pub fn elements(&self, process: &Process) -> Result<Vec<T>> {
let mut address = self.buckets.unallocated_data;
let min_size = (self.block_size() as usize).min(self.count() as usize);
let mut list = Vec::with_capacity(min_size);
while !address.is_null() {
let block_list = unsafe { (*address).block_list(process) }?;
for i in 0..min_size {
list.push(block_list[i].data);
if list.len() >= self.count() as usize {
return Ok(list);
}
}
address = unsafe { (*address).next(process) }?;
}
Ok(list)
}
}

View File

@@ -0,0 +1,13 @@
use memflow::prelude::v1::*;
#[repr(C)]
pub struct KeyboardKey {
pad_0000: [u8; 0x8],
pub name: Pointer64<ReprCString>,
pad_0010: [u8; 0x20],
pub state: u32,
pad_0034: [u8; 0x50],
pub next: Pointer64<KeyboardKey>,
}
unsafe impl Pod for KeyboardKey {}

View File

@@ -0,0 +1,3 @@
pub use input::KeyboardKey;
pub mod input;

7
src/source_engine/mod.rs Normal file
View File

@@ -0,0 +1,7 @@
pub use client::*;
pub use schema_system::*;
pub use tier1::*;
pub mod client;
pub mod schema_system;
pub mod tier1;

View File

@@ -0,0 +1,21 @@
pub use schema_base_class_info_data::*;
pub use schema_class_field_data::*;
pub use schema_class_info_data::*;
pub use schema_enum_info_data::*;
pub use schema_enumerator_info_data::*;
pub use schema_metadata_entry_data::*;
pub use schema_static_field_data::*;
pub use schema_system::*;
pub use schema_system_type_scope::*;
pub use schema_type::*;
pub mod schema_base_class_info_data;
pub mod schema_class_field_data;
pub mod schema_class_info_data;
pub mod schema_enum_info_data;
pub mod schema_enumerator_info_data;
pub mod schema_metadata_entry_data;
pub mod schema_static_field_data;
pub mod schema_system;
pub mod schema_system_type_scope;
pub mod schema_type;

View File

@@ -0,0 +1,11 @@
use memflow::prelude::v1::*;
use super::SchemaClassInfoData;
#[repr(C)]
pub struct SchemaBaseClassInfoData {
pub offset: u32,
pub prev: Pointer64<SchemaClassInfoData>,
}
unsafe impl Pod for SchemaBaseClassInfoData {}

View File

@@ -0,0 +1,14 @@
use memflow::prelude::v1::*;
use super::{SchemaMetadataEntryData, SchemaType};
#[repr(C)]
pub struct SchemaClassFieldData {
pub name: Pointer64<ReprCString>,
pub schema_type: Pointer64<SchemaType>,
pub offset: u32,
pub metadata_count: u32,
pub metadata: Pointer64<SchemaMetadataEntryData>,
}
unsafe impl Pod for SchemaClassFieldData {}

View File

@@ -0,0 +1,33 @@
use memflow::prelude::v1::*;
use super::{
SchemaBaseClassInfoData, SchemaClassFieldData, SchemaMetadataEntryData, SchemaStaticFieldData,
SchemaSystemTypeScope, SchemaType,
};
pub type SchemaClassBinding = SchemaClassInfoData;
#[repr(C)]
pub struct SchemaClassInfoData {
pub base: Pointer64<SchemaClassInfoData>,
pub name: Pointer64<ReprCString>,
pub module_name: Pointer64<ReprCString>,
pub size: u32,
pub fields_count: u16,
pub static_fields_count: u16,
pub static_metadata_count: u16,
pub alignment: u8,
pub has_base_class: bool,
pub total_class_size: u16,
pub derived_class_size: u16,
pub fields: Pointer64<SchemaClassFieldData>,
pub static_fields: Pointer64<SchemaStaticFieldData>,
pub base_classes: Pointer64<SchemaBaseClassInfoData>,
pad_0040: [u8; 0x8],
pub static_metadata: Pointer64<SchemaMetadataEntryData>,
pub type_scope: Pointer64<SchemaSystemTypeScope>,
pub schema_type: Pointer64<SchemaType>,
pad_0060: [u8; 0x10],
}
unsafe impl Pod for SchemaClassInfoData {}

View File

@@ -0,0 +1,35 @@
use memflow::prelude::v1::*;
use super::{SchemaEnumeratorInfoData, SchemaMetadataEntryData, SchemaSystemTypeScope};
pub type SchemaEnumBinding = SchemaEnumInfoData;
#[repr(C)]
pub struct SchemaEnumInfoData {
pub base: Pointer64<SchemaEnumInfoData>,
pub name: Pointer64<ReprCString>,
pub module_name: Pointer64<ReprCString>,
pub alignment: u8,
pad_0019: [u8; 0x3],
pub size: u16,
pub static_metadata_count: u16,
pub enum_info: Pointer64<SchemaEnumeratorInfoData>,
pub static_metadata: Pointer64<SchemaMetadataEntryData>,
pub type_scope: Pointer64<SchemaSystemTypeScope>,
pad_0038: [u8; 0xC],
}
impl SchemaEnumInfoData {
#[inline]
pub fn type_name(&self) -> &str {
match self.alignment {
1 => "int8",
2 => "int16",
4 => "int32",
8 => "int64",
_ => "unknown",
}
}
}
unsafe impl Pod for SchemaEnumInfoData {}

View File

@@ -0,0 +1,21 @@
use memflow::prelude::v1::*;
use super::SchemaMetadataEntryData;
#[repr(C)]
pub struct SchemaEnumeratorInfoData {
pub name: Pointer64<ReprCString>,
pub union_data: SchemaEnumeratorInfoDataUnion,
pub metadata_count: u32,
pub metadata: Pointer64<SchemaMetadataEntryData>,
}
unsafe impl Pod for SchemaEnumeratorInfoData {}
#[repr(C)]
pub union SchemaEnumeratorInfoDataUnion {
pub uchar: u8,
pub ushort: u16,
pub uint: u32,
pub ulong: u64,
}

View File

@@ -0,0 +1,35 @@
use std::ffi::c_char;
use memflow::prelude::v1::*;
#[repr(C)]
pub struct SchemaMetadataEntryData {
pub name: Pointer64<ReprCString>,
pub network_value: Pointer64<SchemaNetworkValue>,
}
unsafe impl Pod for SchemaMetadataEntryData {}
#[repr(C)]
pub struct SchemaNetworkValue {
pub union_data: SchemaNetworkValueUnion,
}
unsafe impl Pod for SchemaNetworkValue {}
#[repr(C)]
pub union SchemaNetworkValueUnion {
pub name_ptr: Pointer64<ReprCString>,
pub int_value: i32,
pub float_value: f32,
pub ptr: Pointer64<()>,
pub var_value: SchemaVarName,
pub name_value: [c_char; 32],
}
#[derive(Clone, Copy)]
#[repr(C)]
pub struct SchemaVarName {
pub name: Pointer64<ReprCString>,
pub ty: Pointer64<ReprCString>,
}

View File

@@ -0,0 +1,13 @@
use memflow::prelude::v1::*;
use super::SchemaType;
#[repr(C)]
pub struct SchemaStaticFieldData {
pub name: Pointer64<ReprCString>,
pub type_: Pointer64<SchemaType>,
pub instance: Address,
pad_0018: [u8; 0x10],
}
unsafe impl Pod for SchemaStaticFieldData {}

View File

@@ -0,0 +1,25 @@
use memflow::prelude::v1::*;
use super::SchemaSystemTypeScope;
use crate::source_engine::UtlVector;
#[cfg(target_os = "linux")]
#[repr(C)]
pub struct SchemaSystem {
pad_0000: [u8; 0x1F8],
pub type_scopes: UtlVector<Pointer64<SchemaSystemTypeScope>>,
pad_01a0: [u8; 0x120],
pub num_registrations: u32,
}
#[cfg(target_os = "windows")]
#[repr(C)]
pub struct SchemaSystem {
pad_0000: [u8; 0x190],
pub type_scopes: UtlVector<Pointer64<SchemaSystemTypeScope>>,
pad_01a0: [u8; 0x120],
pub num_registrations: u32,
}
unsafe impl Pod for SchemaSystem {}

View File

@@ -0,0 +1,31 @@
use std::ffi::c_char;
use memflow::prelude::v1::*;
use super::{SchemaClassBinding, SchemaEnumBinding};
use crate::source_engine::UtlTsHash;
#[cfg(target_os = "linux")]
#[repr(C)]
pub struct SchemaSystemTypeScope {
pad_0000: [u8; 0x8],
pub name: [c_char; 256],
pad_0108: [u8; 0x518],
pub class_bindings: UtlTsHash<Pointer64<SchemaClassBinding>>,
pad_05f0: [u8; 0x2810],
pub enum_bindings: UtlTsHash<Pointer64<SchemaEnumBinding>>,
}
#[cfg(target_os = "windows")]
#[repr(C)]
pub struct SchemaSystemTypeScope {
pad_0000: [u8; 0x8],
pub name: [c_char; 256],
pad_0108: [u8; 0x4B0],
pub class_bindings: UtlTsHash<Pointer64<SchemaClassBinding>>,
pad_05f0: [u8; 0x2810],
pub enum_bindings: UtlTsHash<Pointer64<SchemaEnumBinding>>,
}
unsafe impl Pod for SchemaSystemTypeScope {}

View File

@@ -0,0 +1,99 @@
use memflow::prelude::v1::*;
use super::{SchemaClassInfoData, SchemaEnumBinding, SchemaSystemTypeScope};
#[derive(Debug, Eq, Ord, PartialEq, PartialOrd)]
#[repr(u8)]
pub enum SchemaAtomicCategory {
Basic = 0,
T,
CollectionOfT,
TF,
TT,
TTF,
I,
None,
}
#[derive(Debug, Eq, Ord, PartialEq, PartialOrd)]
#[repr(u8)]
pub enum SchemaTypeCategory {
BuiltIn = 0,
Ptr,
Bitfield,
FixedArray,
Atomic,
DeclaredClass,
DeclaredEnum,
None,
}
#[derive(Clone, Copy)]
#[repr(C)]
pub struct SchemaArray {
pub array_size: u32,
pad_0004: [u8; 0x4],
pub element_type: Pointer64<SchemaType>,
}
#[derive(Clone, Copy)]
#[repr(C)]
pub struct SchemaAtomic {
pub element_type: Pointer64<SchemaType>,
pad_0008: [u8; 0x8],
pub template_ty: Pointer64<SchemaType>,
}
#[derive(Clone, Copy)]
#[repr(C)]
pub struct SchemaAtomicI {
pad_0000: [u8; 0x10],
pub value: u64,
}
#[derive(Clone, Copy)]
#[repr(C)]
pub struct SchemaAtomicTF {
pad_0000: [u8; 0x10],
pub template_ty: Pointer64<SchemaType>,
pub size: u32,
}
#[derive(Clone, Copy)]
#[repr(C)]
pub struct SchemaAtomicTT {
pad_0000: [u8; 0x10],
pub templates: [Pointer64<SchemaType>; 2],
}
#[derive(Clone, Copy)]
#[repr(C)]
pub struct SchemaAtomicTTF {
pad_0000: [u8; 0x10],
pub templates: [Pointer64<SchemaType>; 2],
pub size: u32,
}
#[repr(C)]
pub struct SchemaType {
pad_0000: [u8; 0x8],
pub name: Pointer64<ReprCString>,
pub type_scope: Pointer64<SchemaSystemTypeScope>,
pub type_category: SchemaTypeCategory,
pub atomic_category: SchemaAtomicCategory,
}
unsafe impl Pod for SchemaType {}
#[repr(C)]
pub union SchemaTypeUnion {
pub schema_type: Pointer64<SchemaType>,
pub class_info: Pointer64<SchemaClassInfoData>,
pub enum_binding: Pointer64<SchemaEnumBinding>,
pub array: SchemaArray,
pub atomic: SchemaAtomic,
pub atomic_tt: SchemaAtomicTT,
pub atomic_tf: SchemaAtomicTF,
pub atomic_ttf: SchemaAtomicTTF,
pub atomic_i: SchemaAtomicI,
}

View File

@@ -0,0 +1,44 @@
use memflow::prelude::v1::*;
use super::UtlMemory;
use crate::error::Result;
#[repr(C)]
pub struct ConCommandBase {
pub name: Pointer64<ReprCString>,
pub description: Pointer64<ReprCString>,
pub flags: u64,
pad_0018: [u8; 0x20],
}
unsafe impl Pod for ConCommandBase {}
#[repr(C)]
pub struct CVar {
pad_0000: [u8; 0xD8],
pub cmds: UtlMemory<ConCommandBase>,
}
impl CVar {
pub fn iter(
&self,
process: &mut IntoProcessInstanceArcBox<'_>,
) -> Result<impl Iterator<Item = ConCommandBase>> {
let mut cmds = Vec::new();
for i in 0..self.cmds.alloc_count as usize {
let cmd = self.cmds.get(process, i)?;
if cmd.name.is_null() {
continue;
}
cmds.push(cmd);
}
Ok(cmds.into_iter())
}
}
unsafe impl Pod for CVar {}

View File

@@ -0,0 +1,10 @@
use memflow::prelude::v1::*;
#[repr(C)]
pub struct InterfaceReg {
pub create_fn: Address,
pub name: Pointer64<ReprCString>,
pub next: Pointer64<InterfaceReg>,
}
unsafe impl Pod for InterfaceReg {}

View File

@@ -0,0 +1,11 @@
pub use convar::*;
pub use interface::*;
pub use utl_memory::*;
pub use utl_ts_hash::*;
pub use utl_vector::*;
pub mod convar;
pub mod interface;
pub mod utl_memory;
pub mod utl_ts_hash;
pub mod utl_vector;

View File

@@ -0,0 +1,30 @@
use std::mem;
use memflow::prelude::v1::*;
use crate::error::{Error, Result};
#[repr(C)]
pub struct UtlMemory<T: Sized + Pod> {
pub mem: Pointer64<T>,
pub alloc_count: u32,
pub grow_size: u32,
}
impl<T: Sized + Pod> UtlMemory<T> {
#[inline]
pub fn get(&self, process: &mut IntoProcessInstanceArcBox<'_>, idx: usize) -> Result<T> {
if idx >= self.alloc_count as usize {
return Err(Error::IndexOutOfBounds {
idx,
len: self.alloc_count as usize,
});
}
let ptr = Pointer64::from(self.mem.address() + (idx * mem::size_of::<T>()));
Ok(process.read_ptr(ptr)?)
}
}
unsafe impl<T: Sized + Pod> Pod for UtlMemory<T> {}

View File

@@ -0,0 +1,102 @@
use memflow::prelude::v1::*;
use crate::error::Result;
pub trait HashData: Copy + Sized + Pod {}
impl<T: Copy + Sized + Pod> HashData for T {}
pub trait HashKey: Copy + Sized + Pod {}
impl<K: Copy + Sized + Pod> HashKey for K {}
#[repr(C)]
struct HashFixedDataInternal<T: HashData, K: HashKey> {
ui_key: K,
next: Pointer64<HashFixedDataInternal<T, K>>,
data: T,
}
unsafe impl<T: HashData, K: HashKey> Pod for HashFixedDataInternal<T, K> {}
#[repr(C)]
struct HashBucketDataInternal<T: HashData, K: HashKey> {
data: T,
next: Pointer64<HashFixedDataInternal<T, K>>,
ui_key: K,
}
unsafe impl<T: HashData, K: HashKey> Pod for HashBucketDataInternal<T, K> {}
#[repr(C)]
struct HashAllocatedData<T: HashData, K: HashKey> {
pad_0000: [u8; 0x18],
list: [HashFixedDataInternal<T, K>; 128],
}
unsafe impl<T: HashData, K: HashKey> Pod for HashAllocatedData<T, K> {}
#[repr(C)]
struct HashUnallocatedData<T: HashData, K: HashKey> {
next: Pointer64<HashUnallocatedData<T, K>>,
unk_1: K,
ui_key: K,
unk_2: K,
block_list: [HashBucketDataInternal<T, K>; 256],
}
unsafe impl<T: HashData, K: HashKey> Pod for HashUnallocatedData<T, K> {}
#[repr(C)]
struct HashBucket<T: HashData, K: HashKey> {
pad_0000: [u8; 0x10],
allocated_data: Pointer64<HashAllocatedData<T, K>>,
unallocated_data: Pointer64<HashUnallocatedData<T, K>>,
}
unsafe impl<T: HashData, K: HashKey> Pod for HashBucket<T, K> {}
#[repr(C)]
struct UtlMemoryPool {
block_size: u32,
blocks_per_blob: u32,
grow_mode: u32,
blocks_alloc: u32,
block_alloc_size: u32,
peak_alloc: u32,
}
#[repr(C)]
pub struct UtlTsHash<T: HashData, K: HashKey = u64> {
entry: UtlMemoryPool,
buckets: HashBucket<T, K>,
}
impl<T: HashData, K: HashKey> UtlTsHash<T, K> {
pub fn elements(&self, process: &mut IntoProcessInstanceArcBox<'_>) -> Result<Vec<T>> {
let block_size = self.entry.blocks_per_blob as usize;
let num_blocks = self.entry.block_alloc_size as usize;
let mut element_ptr = self.buckets.unallocated_data;
let mut list = Vec::with_capacity(num_blocks);
while !element_ptr.is_null() {
let element = process.read_ptr(element_ptr)?;
for i in 0..num_blocks {
if i >= block_size || list.len() >= block_size {
break;
}
list.push(element.block_list[i].data);
}
element_ptr = element.next;
}
Ok(list)
}
}
unsafe impl<T: HashData, K: HashKey> Pod for UtlTsHash<T, K> {}

View File

@@ -0,0 +1,29 @@
use std::mem;
use memflow::prelude::v1::*;
use crate::error::{Error, Result};
#[repr(C)]
pub struct UtlVector<T: Sized + Pod> {
pub size: u32,
pub mem: Pointer64<T>,
}
impl<T: Sized + Pod> UtlVector<T> {
#[inline]
pub fn get(&self, process: &mut IntoProcessInstanceArcBox<'_>, idx: usize) -> Result<T> {
if idx >= self.size as usize {
return Err(Error::IndexOutOfBounds {
idx,
len: self.size as usize,
});
}
let ptr = Pointer64::from(self.mem.address() + (idx * mem::size_of::<T>()));
Ok(process.read_ptr(ptr)?)
}
}
unsafe impl<T: Sized + Pod> Pod for UtlVector<T> {}