ddcore_rs/memory/
mod.rs

1//
2// memory
3//
4
5use std::collections::HashMap;
6use std::mem::size_of;
7use std::cell::RefCell;
8use std::process::Child;
9use anyhow::bail;
10use sysinfo::{Pid, ProcessExt, System, SystemExt, PidExt};
11use crate::models::{StatsBlockWithFrames, StatsDataBlock, StatsFrame};
12
13use self::proc_mem_wrapper::Handle;
14
15pub mod proc_mem_wrapper;
16
17///////////////////////////////
18///////////////////////////////
19
20const DATA_BLOCK_SIZE: usize = size_of::<StatsDataBlock>();
21const STATS_FRAME_SIZE: usize = size_of::<StatsFrame>();
22
23thread_local! {
24    static BLOCK_BUF: RefCell<[u8; DATA_BLOCK_SIZE]> = RefCell::new([0_u8; DATA_BLOCK_SIZE]);
25    static FRAME_BUF: RefCell<[u8; STATS_FRAME_SIZE]> = RefCell::new([0_u8; STATS_FRAME_SIZE]);
26    static SYSTEM: RefCell<System> = RefCell::new(System::new_all());
27}
28
29/////////////////////////////// Structs
30///////////////////////////////
31
32pub struct OsInfo {
33    pub default_block_marker: usize,
34    pub default_process_name: String,
35    pub can_create_child: bool,
36    pub offsets: HashMap<String, Vec<usize>>
37}
38
39#[derive(Debug)]
40pub enum OperatingSystem {
41    Linux,
42    LinuxProton,
43    Windows
44}
45
46pub struct GameConnection {
47    pub pid: Pid,
48    pub path: String,
49    pub handle: Handle,
50    pub base_address: usize,
51    pub last_fetch: Option<StatsBlockWithFrames>,
52    pub child_handle: Option<Child>,
53    pub params: ConnectionParams,
54    pub(crate) pointers: Pointers
55}
56
57#[derive(Default)]
58pub struct Pointers {
59    pub ddstats_block: Option<usize>,
60    pub base_address: Option<usize>
61}
62
63#[derive(Default)]
64pub struct MemoryOverride {
65    pub block_marker: Option<usize>,
66    pub process_name: Option<String>,
67}
68
69pub struct ConnectionParams {
70    pub create_child: bool,
71    pub operating_system: OperatingSystem,
72    pub overrides: MemoryOverride,
73}
74
75#[repr(C)]
76#[derive(Debug)]
77pub struct Vec3 {
78    pub x: f32,
79    pub y: f32,
80    pub z: f32,
81}
82
83/////////////////////////////// Struct Impls
84///////////////////////////////
85
86impl OsInfo {
87    pub fn get_from_os(os: &OperatingSystem) -> Self {
88        match os {
89            OperatingSystem::Linux => Self {
90                can_create_child: true,
91                default_block_marker: 0x00521C98,
92                default_process_name: String::from("devildaggers"),
93                offsets: HashMap::new()
94            },
95            OperatingSystem::Windows => Self {
96                can_create_child: false,
97                default_block_marker: 0x250DC0,
98                default_process_name: String::from("dd.exe"),
99                offsets: HashMap::new()
100            },
101            OperatingSystem::LinuxProton => Self {
102                can_create_child: false,
103                default_block_marker: 0x250DC0,
104                default_process_name: String::from("wine-preloader"),
105                offsets: HashMap::new()
106            }
107        }
108    }
109}
110
111impl ConnectionParams {
112    pub fn empty() -> Self {
113        Self {
114            create_child: false,
115            operating_system: OperatingSystem::Linux,
116            overrides: MemoryOverride::default()
117        }
118    }
119}
120
121impl GameConnection {
122    #[cfg(target_os = "windows")]
123    pub fn try_create(params: ConnectionParams) -> anyhow::Result<Self> {
124        let os_info = OsInfo::get_from_os(&params.operating_system);
125        let proc_name = params.overrides.process_name.as_ref().unwrap_or(&os_info.default_process_name).clone();
126        let proc = get_proc(&proc_name);
127        if proc.is_none() { anyhow::bail!("Process not found") }
128        let pid = proc.as_ref().unwrap().1;
129        let handle = Handle::new(pid.as_u32() as usize)?;
130        let base_address = base_addr(&handle, &params);
131        if base_address.is_err() { anyhow::bail!("Couldn't get base address") }
132        let base_address = base_address.unwrap();
133        let ptrs = Pointers { base_address: Some(base_address), ..Default::default() };
134        Ok(Self {
135            pid,
136            handle,
137            base_address,
138            path: proc.as_ref().unwrap().0.clone(),
139            child_handle: None,
140            last_fetch: None,
141            params,
142            pointers: ptrs
143        })
144    }
145
146    #[cfg(target_os = "linux")]
147    pub fn try_create(params: ConnectionParams) -> anyhow::Result<Self> {
148        let os_info = OsInfo::get_from_os(&params.operating_system);
149        let proc_name = params.overrides.process_name.as_ref().unwrap_or(&os_info.default_process_name).clone();
150        let mut proc = get_proc(&proc_name);
151        if proc.is_none() { anyhow::bail!("Process not found") }
152        let mut pid = proc.as_ref().unwrap().1;
153        let mut handle = Handle::new(pid.as_u32() as usize)?;
154        let mut c = None;
155        if handle.copy_address(0, &mut [0u8]).is_err() && params.create_child{
156            c = create_as_child(pid);
157            proc = get_proc(&proc_name);
158            pid = proc.as_ref().unwrap().1;
159            handle = Handle::new(pid.as_u32() as usize)?;
160        }
161        let base_address = base_addr(&handle, &params);
162        if base_address.is_err() { anyhow::bail!("Couldn't get base address") }
163        let base_address = base_address.unwrap();
164        let ptrs = Pointers { base_address: Some(base_address), ..Default::default() };
165        Ok(Self {
166            pid,
167            handle,
168            base_address,
169            path: proc.as_ref().unwrap().0.clone(),
170            child_handle: c,
171            last_fetch: None,
172            params,
173            pointers: ptrs
174        })
175    }
176
177    pub fn dead_connection() -> Self {
178        Self {
179            pid: Pid::from_u32(0),
180            base_address: 0,
181            last_fetch: None,
182            path: String::new(),
183            handle: Handle::null_type(),
184            child_handle: None,
185            params: ConnectionParams::empty(),
186            pointers: Pointers::default()
187        }
188    }
189
190    pub fn is_alive(&mut self) -> bool {
191        match self.read_stats_block() {
192            Ok(_) => true,
193            Err(_e) => false
194        }
195    }
196
197    pub fn is_alive_res(&mut self) -> anyhow::Result<()> {
198        match self.read_stats_block() {
199            Ok(_) => Ok(()),
200            Err(e) => Err(e),
201        }
202    }
203
204    pub fn read_stats_block(&mut self) -> anyhow::Result<StatsDataBlock> {
205        read_stats_data_block(&self.handle, &self.params, &mut self.pointers)
206    }
207
208    pub fn read_mem(&self, addr: usize, buffer: &mut [u8]) -> anyhow::Result<()> {
209        self.handle.copy_address(addr, buffer)
210    }
211
212    #[cfg(target_os = "windows")]
213    pub fn maximize_dd(&self) {
214        use winapi::shared::windef::HWND;
215        use winapi::shared::minwindef::DWORD;
216
217        enumerate_windows(|hwnd: HWND| {
218            let mut pid: DWORD = DWORD::default();
219            unsafe { winapi::um::winuser::GetWindowThreadProcessId(hwnd, &mut pid); }
220            if pid as u32 != self.pid.as_u32() {
221                true
222            } else {
223                unsafe { winapi::um::winuser::ShowWindow(hwnd, 9); }
224                false
225            }
226        })
227    }
228
229    #[cfg(target_os = "linux")]
230    pub fn maximize_dd(&self) {
231        // pep
232    }
233
234    pub fn read_stats_block_with_frames(&mut self) -> anyhow::Result<StatsBlockWithFrames> {
235        match read_stats_data_block(&self.handle, &self.params, &mut self.pointers) {
236            Ok(data) => {
237                let res = StatsBlockWithFrames {
238                    frames: self.stat_frames_from_block(&data)?,
239                    block: data,
240                };
241                self.last_fetch = Some(res.clone());
242                Ok(res)
243            },
244            Err(e) => {
245                log::info!("[DDCORE] Failed to read stats block {e:?}");
246                Err(anyhow::anyhow!(e))
247            }
248        }
249    }
250
251    pub fn stat_frames_from_block(
252        &mut self,
253        block: &StatsDataBlock,
254    ) -> anyhow::Result<Vec<StatsFrame>> {
255        let (mut ptr, len) = (
256            block.get_stats_pointer(),
257            block.stats_frames_loaded as usize,
258        );
259        let mut res = Vec::with_capacity(len);
260        FRAME_BUF.with(|buf| {
261            let mut buf = buf.borrow_mut();
262            for _ in 0..len {
263                self.handle.copy_address(ptr, buf.as_mut())?;
264                let (_head, body, _tail) = unsafe { buf.align_to::<StatsFrame>() };
265                res.push(body[0]);
266                ptr += STATS_FRAME_SIZE;
267            }
268            Ok(res)
269        })
270    }
271
272    pub fn replay_bin(&mut self) -> anyhow::Result<Vec<u8>> {
273        if let Some(block) = &self.last_fetch {
274            let (ptr, len) = (
275                block.block.get_replay_pointer(),
276                block.block.replay_buffer_length as usize,
277            );
278            let mut res = vec![0u8; len];
279            self.handle.copy_address(ptr, &mut res)?;
280            Ok(res)
281        } else {
282            Err(anyhow::anyhow!(std::io::Error::new(
283                std::io::ErrorKind::NotFound,
284                "Stats not available",
285            )))
286        }
287    }
288
289    pub fn stat_frames(&self) -> anyhow::Result<Vec<StatsFrame>> {
290        if let Some(last_data) = &self.last_fetch {
291            let (mut ptr, len) = (
292                last_data.block.get_stats_pointer(),
293                last_data.block.stats_frames_loaded as usize,
294            );
295            let mut res = Vec::with_capacity(len);
296            FRAME_BUF.with(|buf| {
297                let mut buf = buf.borrow_mut();
298                for _ in 0..len {
299                    self.handle.copy_address(ptr, buf.as_mut())?;
300                    let (_head, body, _tail) = unsafe { buf.align_to::<StatsFrame>() };
301                    res.push(body[0]);
302                    ptr += STATS_FRAME_SIZE;
303                }
304                Ok(res)
305            })
306        } else {
307            Err(anyhow::anyhow!(std::io::Error::new(
308                std::io::ErrorKind::NotFound,
309                "Stats not available",
310            )))
311        }
312    }
313
314    pub fn last_stat_frame(&self) -> anyhow::Result<StatsFrame> {
315        if let Some(last_data) = &self.last_fetch {
316            let (mut ptr, len) = (
317                last_data.block.get_stats_pointer(),
318                last_data.block.stats_frames_loaded as usize,
319            );
320            ptr += STATS_FRAME_SIZE * (len - 1);
321            FRAME_BUF.with(|buf| {
322                let mut buf = buf.borrow_mut();
323                self.handle.copy_address(ptr, buf.as_mut())?;
324                let (_head, body, _tail) = unsafe { buf.align_to::<StatsFrame>() };
325                Ok(body[0])
326            })
327        } else {
328            Err(anyhow::anyhow!(std::io::Error::new(
329                std::io::ErrorKind::NotFound,
330                "Stats not available",
331            )))
332        }
333    }
334
335    pub fn play_replay(&self, replay: std::sync::Arc<Vec<u8>>) -> anyhow::Result<()> {
336        if let Some(last_data) = &self.last_fetch {
337            #[cfg(feature = "logger")]
338            log::info!("[DDCORE] Attempting to load replay");
339
340            let ddstats_addr = self.pointers.ddstats_block.expect("last data can't exist without this also being set");
341            let replay_buffer_addr = last_data.block.get_replay_pointer();
342            let flag_addr = ddstats_addr + 316;
343            let len_addr = ddstats_addr + 312;
344            let len = replay.len() as i32;
345
346            #[cfg(feature = "logger")]
347            log::info!("[DDCORE] Replay Flag debug: {ddstats_addr:X} {replay_buffer_addr:X} {flag_addr:X}");
348
349            self.handle.put_address(replay_buffer_addr, &replay)?;
350            self.handle.put_address(len_addr, &len.to_le_bytes())?;
351            self.handle.put_address(flag_addr, &[true as u8])?;
352
353            Ok(())
354        } else {
355            bail!("No data found");
356        }
357    }
358}
359
360/////////////////////////////// The Funcs
361///////////////////////////////
362
363pub fn get_proc(process_name: &str) -> Option<(String, Pid)> {
364    SYSTEM.with(|s| {
365        let mut s = s.borrow_mut();
366        s.refresh_processes();
367        if let Some(process) = s.processes_by_exact_name(process_name).next() {
368            return Some((String::from(process.exe().to_str().unwrap()), process.pid()));
369        }
370        None
371    })
372}
373
374#[cfg(target_os = "windows")]
375pub fn base_addr(handle: &Handle, params: &ConnectionParams) -> anyhow::Result<usize> {
376    let os_info = OsInfo::get_from_os(&params.operating_system);
377    let proc_name = params.overrides.process_name.as_ref().unwrap_or(&os_info.default_process_name).clone();
378    #[cfg(feature = "logger")]
379    log::info!("[DDCORE] reading base address: {} {proc_name}", handle.pid);
380    let addr = unsafe { get_base_address(Pid::from_u32(handle.pid as u32), proc_name) };
381    #[cfg(feature = "logger")]
382    log::info!("[DDCORE] base address: {addr:?}");
383    addr
384}
385
386#[cfg(target_os = "linux")]
387pub fn base_addr(handle: &Handle, params: &ConnectionParams) ->  anyhow::Result<usize> {
388    use std::io::Read;
389
390    use scan_fmt::scan_fmt;
391    let os_info = OsInfo::get_from_os(&params.operating_system);
392    let proc_name = params.overrides.process_name.as_ref().unwrap_or(&os_info.default_process_name).clone();
393    let pid = Pid::from_u32(handle.pid as u32);
394    
395    match &params.operating_system {
396        OperatingSystem::Linux => get_base_address(pid, proc_name),
397        OperatingSystem::Windows => get_base_address(pid, proc_name),
398        OperatingSystem::LinuxProton => {
399            use std::{
400                fs::File,
401                io::{BufRead, BufReader},
402            };
403
404            let mut stat = String::new();
405            BufReader::new(File::open(format!("/proc/{}/stat", pid))?).read_to_string(&mut stat)?;
406
407            if !stat.contains("dd.exe") {
408                return Err(anyhow::anyhow!(std::io::Error::new(std::io::ErrorKind::NotFound, "Not the correct process")));
409            }
410
411            let f = BufReader::new(File::open(format!("/proc/{}/maps", pid))?);
412            let mut magic_buf = [0u8; 2];
413
414            #[cfg(feature = "logger")]
415            log::info!("[DDCORE] Found LinuxProton dd.exe {:?}", pid);
416
417            for line in f.lines().flatten() {
418                if let Ok((start, _end, _perms, mod_path)) = scan_fmt!(&line, "{x}-{x} {} {*} {*} {*} {[^\t\n]}\n", [hex usize], [hex usize], String, String)
419                {
420                    let r = handle.copy_address(start, &mut magic_buf);
421                    if r.is_err() {
422                        #[cfg(feature = "logger")]
423                        log::info!("[DDCORE] Failed to read memory {:?} {} {:X}", r.err(), pid, start);
424                        continue;
425                    }
426
427                    if mod_path.contains("dd.exe") && is_windows_exe(&magic_buf) {
428                        return Ok(start);
429                    }
430                }
431            }
432
433            Err(anyhow::anyhow!(std::io::Error::new(
434                std::io::ErrorKind::NotFound,
435                "No base address",
436            )))
437        }
438    }
439}
440
441pub fn is_elf(start_bytes: &[u8; 4]) -> bool {
442    let elf_signature: [u8; 4] = [0x7f, 0x45, 0x4c, 0x46];
443    elf_signature == *start_bytes
444}
445
446pub fn is_windows_exe(start_bytes: &[u8; 2]) -> bool {
447    let elf_signature: [u8; 2] = [0x4D, 0x5A];
448    elf_signature == *start_bytes
449}
450
451#[cfg(target_os = "linux")]
452pub fn get_base_address(pid: Pid, proc_name: String) -> anyhow::Result<usize> {
453    use scan_fmt::scan_fmt;
454    use std::{
455        fs::File,
456        io::{BufRead, BufReader},
457    };
458
459    let f = BufReader::new(File::open(format!("/proc/{}/maps", pid))?);
460    let handle = Handle::new(pid.as_u32() as usize)?;
461    let mut magic_buf = [0u8; 4];
462
463    for line in f.lines().flatten() {
464        if let Ok((start, _end, perms, mod_path)) = scan_fmt!(&line, "{x}-{x} {} {*} {*} {*} {[^\t\n]}\n", [hex usize], [hex usize], String, String)
465        {
466            let r = handle.copy_address(start, &mut magic_buf);
467            if r.is_err() {
468                continue;
469            }
470
471            if is_elf(&magic_buf) && mod_path.contains(&proc_name) && perms.contains('x') {
472                return Ok(start);
473            }
474        }
475    }
476
477    Err(anyhow::anyhow!(std::io::Error::new(
478        std::io::ErrorKind::NotFound,
479        "No base address",
480    )))
481}
482
483#[cfg(target_os = "windows")]
484pub fn enumerate_windows<F>(mut callback: F)
485    where F: FnMut(winapi::shared::windef::HWND) -> bool
486{
487    use winapi::shared::windef::HWND;
488    use winapi::shared::minwindef::LPARAM;
489    use winapi::um::winuser::EnumWindows;
490    use std::mem;
491    use winapi::ctypes::c_void;
492    let mut trait_obj: &mut dyn FnMut(HWND) -> bool = &mut callback;
493    let closure_pointer_pointer: *mut c_void = unsafe { mem::transmute(&mut trait_obj) };
494
495    let lparam = closure_pointer_pointer as LPARAM;
496    unsafe { EnumWindows(Some(enumerate_callback), lparam) };
497}
498
499#[allow(clippy::transmute_ptr_to_ref)]
500#[cfg(target_os = "windows")]
501unsafe extern "system" fn enumerate_callback(hwnd: winapi::shared::windef::HWND, lparam: winapi::shared::minwindef::LPARAM) -> winapi::shared::minwindef::BOOL {
502    use std::mem;
503    use winapi::shared::windef::HWND;
504    use winapi::shared::minwindef::{TRUE, FALSE};
505    use winapi::ctypes::c_void;
506    let closure: &mut &mut dyn FnMut(HWND) -> bool = mem::transmute(lparam as *mut c_void);
507    if closure(hwnd) { TRUE } else { FALSE }
508}
509
510/// # Safety
511/// Winapi operation, self contained
512#[cfg(target_os = "windows")]
513pub unsafe fn get_base_address(pid: Pid, _proc_name: String) -> anyhow::Result<usize> {
514    // This is miserable
515    use winapi::um::handleapi::CloseHandle;
516    use std::{mem::size_of_val, os::raw::c_ulong};
517
518    let snapshot = winapi::um::tlhelp32::CreateToolhelp32Snapshot(
519        winapi::um::tlhelp32::TH32CS_SNAPMODULE | winapi::um::tlhelp32::TH32CS_SNAPMODULE32,
520        pid.as_u32() as winapi::shared::minwindef::DWORD,
521    );
522
523    let mut me: winapi::um::tlhelp32::MODULEENTRY32 = std::mem::zeroed();
524    me.dwSize = size_of_val(&me) as c_ulong as winapi::shared::minwindef::DWORD;
525    winapi::um::tlhelp32::Module32First(snapshot, &mut me);
526
527    let res = me.modBaseAddr as usize;
528    CloseHandle(snapshot);
529    Ok(res)
530}
531
532#[cfg(target_os = "windows")]
533fn _create_as_child(_pid: Pid) -> Option<Child> {
534    None
535}
536
537#[cfg(target_os = "linux")]
538fn create_as_child(pid: Pid) -> Option<Child> {
539    use std::{
540        fs::File,
541        io::{BufReader, Read},
542        path::Path,
543        process::Command,
544    };
545
546    let mut exe = String::new();
547    BufReader::new(File::open(format!("/proc/{}/cmdline", pid)).expect("Coudln't read cmdline"))
548        .read_to_string(&mut exe)
549        .unwrap();
550    let cwd = Path::new(&format!("/proc/{}/cwd", pid)).to_owned();
551    let mut exe = exe.chars();
552    exe.next_back();
553    let exe = exe.as_str();
554    Command::new("kill")
555        .arg(format!("{}", pid))
556        .spawn()
557        .expect("Couldn't kill current DD process");
558    let old_cwd = std::env::current_dir().expect("Couldn't save cwd");
559    std::env::set_current_dir(&cwd).expect("Coudln't set cwd");
560    Command::new("sh")
561        .arg("-c")
562        .arg("echo")
563        .arg("422970 > steam_appid.txt")
564        .spawn()
565        .expect("Coudln't write steam appid");
566    Command::new("nohup")
567        .arg(exe)
568        .spawn()
569        .expect("Couldn't create DD child process");
570    std::env::set_current_dir(&old_cwd).expect("Couldn't set cwd");
571    None
572}
573
574pub fn mem_search(handle: &Handle, to_find: &[u8]) -> anyhow::Result<usize> {
575    let mut big_ass_buffer = [0_u8; 1024 * 100]; // 100kb buffer
576    let mut offset = 0x00010000;
577    loop {
578        handle.copy_address(offset, &mut big_ass_buffer)?;
579        for (i, w) in big_ass_buffer.windows(to_find.len()).enumerate() {
580            if w == to_find {
581                return Ok(offset + i);
582            }
583        }
584        offset += big_ass_buffer.len();
585    }
586}
587
588fn calc_pointer_ddstats_block(handle: &Handle, params: &ConnectionParams, base_address: usize) -> anyhow::Result<usize> {
589    let os_info = OsInfo::get_from_os(&params.operating_system);
590    let block_start = params.overrides.block_marker.unwrap_or(os_info.default_block_marker);
591    log::info!("[DDCORE] block start {block_start}");
592    match &params.operating_system {
593        OperatingSystem::Linux => {
594            handle.get_offset(&[base_address + block_start, 0])
595        },
596        OperatingSystem::Windows => {
597            handle.get_offset(&[base_address + block_start, 0])
598        },
599        OperatingSystem::LinuxProton => {
600            mem_search(handle, b"__ddstats__")
601        }
602    }
603}
604
605pub fn read_stats_data_block(handle: &Handle, params: &ConnectionParams, pointers: &mut Pointers) -> anyhow::Result<StatsDataBlock> {
606    let base = if pointers.base_address.is_none() { base_addr(handle, params)? } else { *pointers.base_address.as_ref().unwrap() };
607    pointers.base_address = Some(base);
608    BLOCK_BUF.with(|buf| {
609        let pointer;
610        if let Some(ddstats_ptr) = pointers.ddstats_block {
611            pointer = ddstats_ptr;
612        } else {
613            pointers.ddstats_block = Some(calc_pointer_ddstats_block(handle, params, base)?);
614            pointer = *pointers.ddstats_block.as_ref().unwrap();
615        }
616        let mut buf = buf.borrow_mut();
617        handle.copy_address(pointer, buf.as_mut())?;
618        if !buf.starts_with(b"__ddstats__") {
619            return Err(anyhow::anyhow!(std::io::Error::new(std::io::ErrorKind::InvalidData, "No ddstats block found at address")));
620        }
621        let (_head, body, _tail) = unsafe { buf.as_mut().align_to::<StatsDataBlock>() };
622        Ok(body[0].clone())
623    })
624}
625
626#[cfg(target_os = "windows")]
627pub fn start_dd() -> anyhow::Result<()> {
628    use std::process::Command;
629    Command::new("cmd").arg("/c start steam://run/422970").output()?;
630    Ok(())
631}
632
633#[cfg(target_os = "linux")]
634pub fn start_dd() -> anyhow::Result<()> {
635    use std::process::Command;
636    Command::new("steam").arg("steam://run/422970").output()?;
637    Ok(())
638}
639