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");
});
}
}