吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 396|回复: 2
上一主题 下一主题
收起左侧

[学习记录] Rust hudhook in DyingLight

[复制链接]
跳转到指定楼层
楼主
Linkmanbo 发表于 2026-6-27 21:46 回帖奖励
本帖最后由 Linkmanbo 于 2026-6-28 15:13 编辑

Rust | HudHook_Internal -- 内部注入内存修改器框架

基于 Rust 的 DX11 内部注入内存辅助工具,以《消逝的光芒》为示例游戏
免责声明:本工具仅供学习和研究逆向工程技术使用,请勿用于任何形式的游戏作弊或违规行为。使用本工具所产生的任何后果由使用者自行承担。

Cargo.toml

[package]
name = "dlcheat"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[dependencies]
hudhook = { version = "0.9", default-features = false, features = ["dx11"] }

[profile.release]
panic = "abort"
lto = true
opt-level = 2

src/lib.rs -- DllMain 入口

#![cfg(target_os = "windows")]
#![windows_subsystem = "windows"]
#![allow(dead_code)]

mod core;
mod memory;
mod cheats;
mod ui;

use hudhook::windows::Win32::Foundation::HINSTANCE;
use hudhook::windows::Win32::System::SystemServices::DLL_PROCESS_ATTACH;

use crate::core::manager::CheatManager;
use crate::cheats::undead::UndeadCheat;
use crate::ui::overlay::DlCheatApp;

#[no_mangle]
pub unsafe extern "system" fn DllMain(
    hmodule: HINSTANCE,
    reason: u32,
    _reserved: *mut std::ffi::c_void,
) -> u32 {
    if reason == DLL_PROCESS_ATTACH {
        let module_raw = hmodule.0 as usize;
        std::thread::spawn(move || {
            init_cheats(HINSTANCE(module_raw as _));
        });
    }
    1
}

fn init_cheats(hmodule: HINSTANCE) {
    let mut manager = CheatManager::new();
    manager.register(Box::new(UndeadCheat::new()));

    let errors = manager.initialize_all();
    for (name, err) in &errors {
        hudhook::tracing::warn!("Cheat '{name}' failed to init: {err}");
    }

    let app = DlCheatApp::new(manager);
    match hudhook::Hudhook::builder()
        .with::<hudhook::hooks::dx11::ImguiDx11Hooks>(app)
        .with_hmodule(hmodule)
        .build()
        .apply()
    {
        Ok(()) => hudhook::tracing::info!("dlcheat loaded successfully"),
        Err(e) => {
            hudhook::tracing::error!("Hudhook apply failed: {e:?}");
            hudhook::eject();
        }
    }
}

src/core/manager.rs -- CheatManager

use crate::cheats::Cheat;
use hudhook::imgui::Ui;

pub struct CheatManager {
    cheats: Vec<Box<dyn Cheat>>,
}

impl CheatManager {
    pub fn new() -> Self {
        Self { cheats: Vec::new() }
    }

    pub fn register(&mut self, cheat: Box<dyn Cheat>) {
        self.cheats.push(cheat);
    }

    pub fn initialize_all(&mut self) -> Vec<(String, String)> {
        let mut errors = Vec::new();
        for cheat in self.cheats.iter_mut() {
            let name = cheat.name().to_string();
            if let Err(e) = cheat.initialize() {
                errors.push((name, e));
            }
        }
        errors
    }

    pub fn tick_all(&mut self) {
        for cheat in self.cheats.iter_mut() {
            if cheat.is_ready() && cheat.is_enabled() {
                cheat.tick();
            }
        }
    }

    pub fn draw_ui(&mut self, ui: &Ui) {
        for cheat in self.cheats.iter_mut() {
            if !cheat.is_ready() {
                ui.text_disabled(format!("{} [FAILED]", cheat.name()));
                continue;
            }
            cheat.draw_ui(ui);
        }
    }

    pub fn count(&self) -> usize {
        self.cheats.len()
    }

    pub fn ready_count(&self) -> usize {
        self.cheats.iter().filter(|c| c.is_ready()).count()
    }
}

impl Default for CheatManager {
    fn default() -> Self { Self::new() }
}

unsafe impl Send for CheatManager {}
unsafe impl Sync for CheatManager {}

src/core/error.rs -- 可视化报错

use std::fmt;

pub type DlCheatResult<T> = Result<T, DlCheatError>;

#[derive(Debug)]
pub enum DlCheatError {
    ModuleNotFound(String),
    PatternNotFound(String),
    MemoryOperation(String),
    ProtectionError(String),
    SanityCheckFailed { address: usize, expected: Vec<u8>, actual: Vec<u8> },
}

impl fmt::Display for DlCheatError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::ModuleNotFound(name) => write!(f, "Module not found: {name}"),
            Self::PatternNotFound(pat) => write!(f, "AOB pattern not found: {pat}"),
            Self::MemoryOperation(detail) => write!(f, "Memory operation failed: {detail}"),
            Self::ProtectionError(detail) => write!(f, "VirtualProtect failed: {detail}"),
            Self::SanityCheckFailed { address, expected, actual } => {
                write!(
                    f, "Sanity check failed at {address:#x}: expected {expected:02x?}, got {actual:02x?}"
                )
            }
        }
    }
}

impl From<&str> for DlCheatError {
    fn from(s: &str) -> Self { Self::MemoryOperation(s.to_string()) }
}

impl From<String> for DlCheatError {
    fn from(s: String) -> Self { Self::MemoryOperation(s) }
}

src/cheats/cheat.rs -- trait 接口

use hudhook::imgui::Ui;

pub trait Cheat: Send + Sync {
    fn name(&self) -> &str;
    fn initialize(&mut self) -> Result<(), String>;
    fn enable(&mut self);
    fn disable(&mut self);
    fn toggle(&mut self) {
        if self.is_enabled() { self.disable(); }
        else { self.enable(); }
    }
    fn is_enabled(&self) -> bool;
    fn is_ready(&self) -> bool;
    fn tick(&mut self) {}
    fn draw_ui(&mut self, ui: &Ui);
}

src/cheats/undead.rs -- 示例功能锁血

use hudhook::imgui::Ui;

use crate::cheats::Cheat;
use crate::memory::module::get_module_info;
use crate::memory::patcher::Patch;
use crate::memory::scanner::read_bytes;

const AOB_PATTERN: &[u8] = &[0x77, 0x10, 0x41, 0xB1, 0x01];
const TARGET_MODULE: &str = "gamedll_x64_rwdi.dll";
const ORIGINAL_BYTES: &[u8] = &[0x77, 0x10];
const PATCH_BYTES: &[u8] = &[0xEB, 0x10];

pub struct UndeadCheat {
    name: String,
    enabled: bool,
    ready: bool,
    patch: Option<Patch>,
}

impl UndeadCheat {
    pub fn new() -> Self {
        Self {
            name: "不死族".to_string(),
            enabled: false,
            ready: false,
            patch: None,
        }
    }
}

impl Default for UndeadCheat {
    fn default() -> Self { Self::new() }
}

impl Cheat for UndeadCheat {
    fn name(&self) -> &str { &self.name }

    fn initialize(&mut self) -> Result<(), String> {
        let module = get_module_info(TARGET_MODULE)?;
        let addr = crate::memory::scanner::aob_scan(&module, AOB_PATTERN)
            .ok_or_else(|| format!("AOB pattern {AOB_PATTERN:02x?} not found"))?;

        let actual = unsafe { read_bytes(addr, 2)? };
        if actual != ORIGINAL_BYTES {
            return Err(format!(
                "Sanity check failed at {addr:#x}: expected {ORIGINAL_BYTES:02x?}, got {actual:02x?}"
            ));
        }
        self.patch = Some(Patch::new(addr, ORIGINAL_BYTES, PATCH_BYTES));
        self.ready = true;
        Ok(())
    }

    fn enable(&mut self) {
        if let Some(ref mut p) = self.patch {
            if p.apply() { self.enabled = true; }
        }
    }

    fn disable(&mut self) {
        if let Some(ref mut p) = self.patch {
            if p.restore() { self.enabled = false; }
        }
    }

    fn is_enabled(&self) -> bool { self.enabled }
    fn is_ready(&self) -> bool { self.ready }
    fn tick(&mut self) {}

    fn draw_ui(&mut self, ui: &Ui) {
        if !self.ready {
            ui.text_disabled("不死族 [FAILED]");
            return;
        }
        let mut state = self.enabled;
        if ui.checkbox("不死族", &mut state) {
            self.toggle();
        }
    }
}

unsafe impl Send for UndeadCheat {}
unsafe impl Sync for UndeadCheat {}

src/memory/scanner.rs -- AOB 扫描

use crate::memory::module::ModuleInfo;

pub fn aob_scan(module: &ModuleInfo, pattern: &[u8]) -> Option<usize> {
    if pattern.is_empty() || module.size < pattern.len() {
        return None;
    }
    let base = module.base;
    let size = module.size;
    let end = base + size;
    let mut current = base;
    while current + pattern.len() <= end {
        let slice = unsafe {
            std::slice::from_raw_parts(current as *const u8, pattern.len())
        };
        if slice == pattern { return Some(current); }
        current += 1;
    }
    None
}

pub fn aob_scan_checked(module: &ModuleInfo, pattern: &[u8]) -> Result<usize, String> {
    let addr = aob_scan(module, pattern)
        .ok_or_else(|| format!("AOB pattern {pattern:02x?} not found in {}", module.name))?;
    let found: Vec<u8> = unsafe {
        std::slice::from_raw_parts(addr as *const u8, pattern.len())
    }.to_vec();
    if found != pattern {
        return Err(format!(
            "AOB pattern verification failed at {addr:#x}: expected {pattern:02x?}, got {found:02x?}"
        ));
    }
    Ok(addr)
}

pub unsafe fn read_bytes(address: usize, len: usize) -> Result<Vec<u8>, String> {
    if len == 0 { return Ok(Vec::new()); }
    let ptr = address as *const u8;
    if ptr.is_null() { return Err("read_bytes: null pointer".to_string()); }
    let slice = std::slice::from_raw_parts(ptr, len);
    Ok(slice.to_vec())
}

src/memory/patcher.rs -- 内存补丁

use hudhook::windows::Win32::System::Memory::{
    VirtualProtect, PAGE_EXECUTE_READWRITE, PAGE_PROTECTION_FLAGS,
};

pub struct Patch {
    pub address: usize,
    pub original_bytes: Vec<u8>,
    pub patch_bytes: Vec<u8>,
    pub enabled: bool,
}

impl Patch {
    pub fn new(address: usize, original: &[u8], patch: &[u8]) -> Self {
        assert_eq!(original.len(), patch.len(), "original and patch must be same length");
        Self {
            address,
            original_bytes: original.to_vec(),
            patch_bytes: patch.to_vec(),
            enabled: false,
        }
    }

    pub fn apply(&mut self) -> bool {
        if self.enabled { return true; }
        if self.patch_bytes.is_empty() || self.address == 0 {
            return false;
        }
        unsafe {
            let size = self.patch_bytes.len();
            let mut old_protect = PAGE_PROTECTION_FLAGS(0);
            if VirtualProtect(
                self.address as *const std::ffi::c_void,
                size,
                PAGE_EXECUTE_READWRITE,
                &mut old_protect,
            )
            .is_err()
            {
                return false;
            }
            std::ptr::copy_nonoverlapping(
                self.patch_bytes.as_ptr(),
                self.address as *mut u8,
                size,
            );
            let _ = VirtualProtect(
                self.address as *const std::ffi::c_void,
                size,
                old_protect,
                &mut PAGE_PROTECTION_FLAGS(0),
            );
        }
        self.enabled = true;
        true
    }

    pub fn restore(&mut self) -> bool {
        if !self.enabled { return true; }
        if self.original_bytes.is_empty() || self.address == 0 {
            return false;
        }
        unsafe {
            let size = self.original_bytes.len();
            let mut old_protect = PAGE_PROTECTION_FLAGS(0);
            if VirtualProtect(
                self.address as *const std::ffi::c_void,
                size,
                PAGE_EXECUTE_READWRITE,
                &mut old_protect,
            )
            .is_err()
            {
                return false;
            }
            std::ptr::copy_nonoverlapping(
                self.original_bytes.as_ptr(),
                self.address as *mut u8,
                size,
            );
            let _ = VirtualProtect(
                self.address as *const std::ffi::c_void,
                size,
                old_protect,
                &mut PAGE_PROTECTION_FLAGS(0),
            );
        }
        self.enabled = false;
        true
    }
}

impl Drop for Patch {
    fn drop(&mut self) {
        if self.enabled { self.restore(); }
    }
}

unsafe impl Send for Patch {}
unsafe impl Sync for Patch {}

src/memory/module.rs -- 获取模块基地址

use hudhook::windows::Win32::System::Diagnostics::ToolHelp::{
    CreateToolhelp32Snapshot, Module32FirstW, Module32NextW, MODULEENTRY32W,
    TH32CS_SNAPMODULE, TH32CS_SNAPMODULE32,
};
use hudhook::windows::Win32::Foundation::HANDLE;

#[derive(Clone, Debug)]
pub struct ModuleInfo {
    pub base: usize,
    pub size: usize,
    pub name: String,
}

pub fn get_module_info(name: &str) -> Result<ModuleInfo, String> {
    let snapshot: HANDLE = unsafe {
        CreateToolhelp32Snapshot(TH32CS_SNAPMODULE | TH32CS_SNAPMODULE32, 0)
            .map_err(|e| format!("Failed to create module snapshot: {e:?}"))?
    };
    let mut me = MODULEENTRY32W::default();
    me.dwSize = std::mem::size_of::<MODULEENTRY32W>() as u32;
    if unsafe { Module32FirstW(snapshot, &mut me) }.is_err() {
        return Err(format!("Module snapshot is empty"));
    }
    loop {
        let module_name: String = {
            let slice = &me.szModule;
            let len = slice.iter().position(|&c| c == 0).unwrap_or(slice.len());
            String::from_utf16_lossy(&slice[..len])
        };
        if module_name.eq_ignore_ascii_case(name) {
            let info = ModuleInfo {
                base: me.modBaseAddr as usize,
                size: me.modBaseSize as usize,
                name: module_name,
            };
            return Ok(info);
        }
        if unsafe { Module32NextW(snapshot, &mut me) }.is_err() { break; }
    }
    Err(format!("Module '{name}' not found in process"))
}

pub fn get_module_base(name: &str) -> Result<usize, String> {
    get_module_info(name).map(|m| m.base)
}

pub fn get_module_size(name: &str) -> Result<usize, String> {
    get_module_info(name).map(|m| m.size)
}

src/ui/overlay.rs -- ImGui 覆盖层 UI

use std::fs;
use std::ptr;

use hudhook::imgui::{Condition, Context, FontConfig, FontGlyphRanges, FontSource, StyleColor, Ui};
use hudhook::windows::Win32::Foundation::{HWND, POINT};
use hudhook::windows::Win32::Graphics::Gdi::ScreenToClient;
use hudhook::windows::Win32::UI::Input::KeyboardAndMouse::GetAsyncKeyState;
use hudhook::windows::Win32::UI::WindowsAndMessaging::{
    FindWindowA, GetCursorPos, ShowCursor,
};
use hudhook::{ImguiRenderLoop, RenderContext};

use crate::core::manager::CheatManager;

const VK_TILDE: i32 = 0xC0;
const VK_LBUTTON: i32 = 0x01;

pub struct DlCheatApp {
    manager: CheatManager,
    visible: bool,
    prev_toggle: bool,
    prev_left: bool,
    game_window: HWND,
}

impl DlCheatApp {
    pub fn new(manager: CheatManager) -> Self {
        Self {
            manager,
            visible: true,
            prev_toggle: false,
            prev_left: false,
            game_window: HWND(ptr::null_mut()),
        }
    }
}

unsafe impl Send for DlCheatApp {}
unsafe impl Sync for DlCheatApp {}

impl ImguiRenderLoop for DlCheatApp {
    fn initialize<{bt}a{bt}>(&{bt}a{bt} mut self, ctx: &mut Context, _render_context: &{bt}a{bt} mut dyn RenderContext) {
        self.game_window = unsafe {
            FindWindowA(hudhook::windows::core::s!("techland_game_class"), None)
            .unwrap_or(HWND(ptr::null_mut()))
        };

        if let Ok(font_data) = fs::read("C:\Windows\Fonts\msyh.ttc") {
            let fonts = ctx.fonts();
            fonts.clear();
            fonts.add_font(&[FontSource::TtfData {
                data: &font_data,
                size_pixels: 15.0,
                config: Some(FontConfig {
                    glyph_ranges: FontGlyphRanges::chinese_simplified_common(),
                    rasterizer_multiply: 1.2,
                    ..Default::default()
                }),
            }]);
        }

        let style = ctx.style_mut();
        style.use_dark_colors();

        style[StyleColor::WindowBg] = [0.07, 0.07, 0.09, 0.94];
        style[StyleColor::TitleBg] = [0.04, 0.04, 0.05, 1.0];
        style[StyleColor::TitleBgActive] = [0.04, 0.04, 0.05, 1.0];
        style[StyleColor::FrameBg] = [0.12, 0.12, 0.14, 0.94];
        style[StyleColor::FrameBgHovered] = [0.20, 0.20, 0.22, 0.94];
        style[StyleColor::FrameBgActive] = [0.28, 0.28, 0.30, 0.94];
        style[StyleColor::Button] = [0.15, 0.15, 0.17, 0.94];
        style[StyleColor::ButtonHovered] = [0.25, 0.25, 0.27, 0.94];
        style[StyleColor::CheckMark] = [0.35, 0.75, 0.35, 1.0];
        style[StyleColor::Header] = [0.18, 0.18, 0.20, 0.94];
        style[StyleColor::Separator] = [0.18, 0.18, 0.20, 0.50];
    }

    fn render(&mut self, ui: &mut Ui) {
        unsafe {
            let toggle_down = GetAsyncKeyState(VK_TILDE) < 0;
            if toggle_down && !self.prev_toggle {
                self.visible = !self.visible;
                ShowCursor(self.visible);
            }
            self.prev_toggle = toggle_down;
        }
        self.manager.tick_all();
        if !self.visible { return; }

        ui.window("dlcheat v0.1")
            .size([320.0, 140.0], Condition::FirstUseEver)
            .position([20.0, 20.0], Condition::FirstUseEver)
            .movable(true)
            .resizable(true)
            .build(|| {
                ui.columns(2, "status", false);
                ui.text_colored([0.25, 0.70, 0.25, 1.0], format!("cheat loaded"));
                ui.next_column();
                ui.text_colored([0.50, 0.50, 0.50, 1.0], format!("{} / {} ready", self.manager.ready_count(), self.manager.count()));
                ui.columns(1, "", false);
                ui.separator();
                self.manager.draw_ui(ui);
                ui.separator();
                ui.text_colored([0.35, 0.35, 0.35, 1.0], "[~] show/hide");
            });
    }
}

屏幕截图 2026-06-27 213212.png (146.92 KB, 下载次数: 1)

注入

注入

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

沙发
 楼主| Linkmanbo 发表于 2026-6-27 21:48 |楼主
本帖最后由 Linkmanbo 于 2026-6-27 22:01 编辑

不直接分享打包源码文件(介于该游戏社区环境)
3#
BayMax2911 发表于 2026-6-29 01:22
看着确实很方便,但是想换另一个单机游戏的话,小白就完全抓瞎了&#128514;
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - 52pojie.cn ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2026-7-17 07:44

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表