Skip to content

Commit 691e81e

Browse files
committed
refactor: 在 TLS 初始化前应用纯内存补丁
- 抽象共享 PatchMemory 后端,让 early 与 late 阶段复用同一组版本识别和 expected bytes 校验\n- 在 DLL_PROCESS_ATTACH 提前应用启动、计时器、曲数、120fps、网络、分辨率、AppUser 和音频字节补丁\n- 保留晚期 patch 调用,继续记录结果并为非 early 路径提供兜底\n- 将直接内存访问限制在主模块 SizeOfImage 范围内,并恢复页保护、刷新指令缓存\n- 删除 AppUser 专用重复扫描器\n- 移除可能读取泛型 padding 的 write_value<T>,改用整数 to_le_bytes\n- 添加 TLS/AppUser early 管线与通配符扫描回归测试
1 parent 4a92b27 commit 691e81e

18 files changed

Lines changed: 390 additions & 152 deletions

src/early_patch.rs

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
use std::ffi::c_void;
2+
3+
use windows_sys_loader::Win32::System::Diagnostics::Debug::FlushInstructionCache;
4+
use windows_sys_loader::Win32::System::Memory::{VirtualProtect, PAGE_EXECUTE_READWRITE};
5+
use windows_sys_loader::Win32::System::Threading::GetCurrentProcess;
6+
7+
use crate::config::Config;
8+
use crate::patches;
9+
use crate::util::memory::PatchMemory;
10+
11+
struct DirectMemory {
12+
game_base: usize,
13+
game_size: u32,
14+
}
15+
16+
pub unsafe fn apply(game_base: usize) {
17+
if game_base == 0 {
18+
return;
19+
}
20+
let game_size = crate::proxy::game_size(game_base);
21+
let Some(base_dir) = crate::proxy::base_dir() else {
22+
return;
23+
};
24+
if game_size == 0 {
25+
return;
26+
}
27+
28+
let memory = DirectMemory {
29+
game_base,
30+
game_size,
31+
};
32+
let config = Config::load(&base_dir);
33+
patches::apply_early(&memory, &config);
34+
}
35+
36+
impl DirectMemory {
37+
fn contains_range(&self, address: usize, len: usize) -> bool {
38+
let Some(offset) = address.checked_sub(self.game_base) else {
39+
return false;
40+
};
41+
offset
42+
.checked_add(len)
43+
.is_some_and(|end| end <= self.game_size as usize)
44+
}
45+
}
46+
47+
impl PatchMemory for DirectMemory {
48+
fn game_base(&self) -> usize {
49+
self.game_base
50+
}
51+
52+
fn game_size(&self) -> u32 {
53+
self.game_size
54+
}
55+
56+
fn aob_scan(&self, start: usize, size: u32, pattern: &[u8], mask: &str) -> usize {
57+
if !self.contains_range(start, size as usize) {
58+
return 0;
59+
}
60+
// SAFETY: Category 3/10(悬空与越界)。范围已限制在 Windows 已映射的游戏 PE 映像内,
61+
// 进程卸载主模块前该映像始终有效,且扫描期间不写入。
62+
let image = unsafe { std::slice::from_raw_parts(start as *const u8, size as usize) };
63+
find_pattern(image, pattern, mask).map_or(0, |offset| start + offset)
64+
}
65+
66+
fn mem_read(&self, addr: usize, buf: &mut [u8]) -> bool {
67+
if !self.contains_range(addr, buf.len()) {
68+
return false;
69+
}
70+
// SAFETY: Category 3/10(悬空与越界)。目标范围已验证属于仍映射的主模块,
71+
// 目标切片与调用方缓冲区互不重叠。
72+
unsafe {
73+
std::ptr::copy_nonoverlapping(addr as *const u8, buf.as_mut_ptr(), buf.len());
74+
}
75+
true
76+
}
77+
78+
fn mem_write(&self, addr: usize, data: &[u8]) -> bool {
79+
if data.is_empty() || !self.contains_range(addr, data.len()) {
80+
return false;
81+
}
82+
83+
let address = addr as *mut u8;
84+
let mut old_protect = 0;
85+
// SAFETY: Category 8/10(FFI 与越界)。地址范围已验证属于主模块;VirtualProtect
86+
// 只临时放宽该已提交映像页,写入长度与校验过的 data 完全一致。
87+
if unsafe {
88+
VirtualProtect(
89+
address.cast(),
90+
data.len(),
91+
PAGE_EXECUTE_READWRITE,
92+
&mut old_protect,
93+
)
94+
} == 0
95+
{
96+
return false;
97+
}
98+
99+
// SAFETY: Category 1/10(别名与越界)。补丁源位于 Rust slice,目标位于游戏映像,
100+
// 两者不重叠;目标长度已由 contains_range 证明。
101+
unsafe {
102+
std::ptr::copy_nonoverlapping(data.as_ptr(), address, data.len());
103+
}
104+
105+
let mut ignored = 0;
106+
// SAFETY: Category 8(FFI)。参数复用成功 VirtualProtect 返回的页保护值与同一范围。
107+
let _ = unsafe { VirtualProtect(address.cast(), data.len(), old_protect, &mut ignored) };
108+
// SAFETY: Category 8(FFI)。写入范围仍属于当前进程映像,刷新后 CPU 才能看到新指令。
109+
let _ = unsafe {
110+
FlushInstructionCache(GetCurrentProcess(), address.cast::<c_void>(), data.len())
111+
};
112+
true
113+
}
114+
115+
fn log_info(&self, _message: &str) {}
116+
117+
fn log_warn(&self, _message: &str) {}
118+
}
119+
120+
fn find_pattern(image: &[u8], pattern: &[u8], mask: &str) -> Option<usize> {
121+
if pattern.is_empty() || pattern.len() != mask.len() || pattern.len() > image.len() {
122+
return None;
123+
}
124+
let mask = mask.as_bytes();
125+
image.windows(pattern.len()).position(|window| {
126+
window
127+
.iter()
128+
.zip(pattern)
129+
.zip(mask)
130+
.all(|((&actual, &expected), &kind)| kind == b'?' || actual == expected)
131+
})
132+
}
133+
134+
#[cfg(test)]
135+
mod tests {
136+
use std::cell::RefCell;
137+
138+
use super::*;
139+
140+
struct FakeMemory {
141+
base: usize,
142+
image: RefCell<Vec<u8>>,
143+
}
144+
145+
impl PatchMemory for FakeMemory {
146+
fn game_base(&self) -> usize {
147+
self.base
148+
}
149+
150+
fn game_size(&self) -> u32 {
151+
self.image.borrow().len() as u32
152+
}
153+
154+
fn aob_scan(&self, start: usize, size: u32, pattern: &[u8], mask: &str) -> usize {
155+
let offset = start - self.base;
156+
let image = self.image.borrow();
157+
find_pattern(&image[offset..offset + size as usize], pattern, mask)
158+
.map_or(0, |found| start + found)
159+
}
160+
161+
fn mem_read(&self, addr: usize, buf: &mut [u8]) -> bool {
162+
let offset = addr - self.base;
163+
buf.copy_from_slice(&self.image.borrow()[offset..offset + buf.len()]);
164+
true
165+
}
166+
167+
fn mem_write(&self, addr: usize, data: &[u8]) -> bool {
168+
let offset = addr - self.base;
169+
self.image.borrow_mut()[offset..offset + data.len()].copy_from_slice(data);
170+
true
171+
}
172+
173+
fn log_info(&self, _message: &str) {}
174+
175+
fn log_warn(&self, _message: &str) {}
176+
}
177+
178+
#[test]
179+
fn early_scanner_matches_wildcards_before_tls() {
180+
// Given: 版本间变化的字节位于稳定指令签名中。
181+
let image = [0x85, 0xC0, 0x75, 0x07, 0xBE, 0x00, 0x12, 0x80];
182+
let pattern = [0x85, 0xC0, 0x75, 0x07, 0xBE, 0x00, 0x00, 0x80];
183+
184+
// When: TLS 前扫描器使用与晚期 patch 相同的掩码。
185+
let found = find_pattern(&image, &pattern, "xxxxxx?x");
186+
187+
// Then: 可变字节不会破坏版本无关识别。
188+
assert_eq!(found, Some(0));
189+
}
190+
191+
#[test]
192+
fn enabled_checks_are_patched_by_shared_early_pipeline() {
193+
// Given: AppUser 与 TLS 检测仍是原字节,且配置明确启用两项绕过。
194+
let mut image = vec![0x90; 64];
195+
image[8..14].copy_from_slice(&[0x83, 0x7C, 0x24, 0x04, 0x00, 0x75]);
196+
image[24..40].copy_from_slice(&[
197+
0x85, 0xC0, 0x75, 0x07, 0xBE, 0x00, 0x00, 0x80, 0x00, 0xEB, 0x02, 0x33, 0xF6, 0x8B,
198+
0x5B, 0x34,
199+
]);
200+
let memory = FakeMemory {
201+
base: 0x1000,
202+
image: RefCell::new(image),
203+
};
204+
let config = Config {
205+
base_dir: ".".to_owned(),
206+
sections: "[BypassAppUser]\n[DisableTLS]"
207+
.parse()
208+
.expect("测试配置必须有效"),
209+
};
210+
211+
// When: DLL_PROCESS_ATTACH 使用与晚期阶段共享的 patch 定义。
212+
patches::apply_early(&memory, &config);
213+
214+
// Then: 两个会在 TLS 初始化中被缓存的检测都已提前改写。
215+
let image = memory.image.borrow();
216+
assert_eq!(image[13], 0xEB);
217+
assert_eq!(image[31], 0x00);
218+
}
219+
}

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ mod autoplay;
1313
mod chuniio;
1414
mod config;
1515
mod d3d9;
16+
mod early_patch;
1617
mod gfx;
1718
mod io4;
1819
mod iohook;

src/patch_engine.rs

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
use crate::config::Config;
2-
use crate::util::api::Api;
3-
use crate::util::memory::{file_offset_to_va, patch_bytes, PatchResult};
2+
use crate::util::memory::{file_offset_to_va, patch_bytes, PatchMemory, PatchResult};
43
use crate::util::pattern;
54

65
pub struct VersionedPatch {
@@ -17,7 +16,7 @@ pub struct PatchVariant {
1716
pub patch: &'static [u8],
1817
}
1918

20-
pub fn apply_patch(api: &Api, config: &Config, def: &VersionedPatch) -> PatchResult {
19+
pub fn apply_patch<M: PatchMemory>(api: &M, config: &Config, def: &VersionedPatch) -> PatchResult {
2120
if !config.is_enabled(def.section) {
2221
return PatchResult::AlreadyPatched;
2322
}
@@ -47,7 +46,7 @@ pub fn apply_patch(api: &Api, config: &Config, def: &VersionedPatch) -> PatchRes
4746
log_result(api, def, fallback)
4847
}
4948

50-
fn find_by_pattern(api: &Api, variant: &PatchVariant) -> Option<usize> {
49+
fn find_by_pattern<M: PatchMemory>(api: &M, variant: &PatchVariant) -> Option<usize> {
5150
let pattern = variant.pattern?;
5251
let found = pattern::scan(api, pattern);
5352
if found == 0 {
@@ -71,7 +70,7 @@ fn classify_known_offset_result(
7170
}
7271
}
7372

74-
fn log_result(api: &Api, def: &VersionedPatch, result: PatchResult) -> PatchResult {
73+
fn log_result<M: PatchMemory>(api: &M, def: &VersionedPatch, result: PatchResult) -> PatchResult {
7574
match result {
7675
PatchResult::Applied => api.log_info(&format!("patch applied: {}", def.name)),
7776
PatchResult::AlreadyPatched => {

src/patches/audio.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,18 @@
11
use crate::config::Config;
2-
use crate::patch_engine::{PatchVariant, VersionedPatch, apply_patch};
2+
use crate::patch_engine::{apply_patch, PatchVariant, VersionedPatch};
33
use crate::util::api::Api;
4+
use crate::util::memory::PatchMemory;
45

56
pub fn apply(api: &Api, config: &Config) {
67
apply_shared_audio(api, config);
8+
apply_force_2ch(api, config);
9+
}
10+
11+
pub(crate) fn apply_early<M: PatchMemory>(api: &M, config: &Config) {
12+
apply_force_2ch(api, config);
13+
}
14+
15+
fn apply_force_2ch<M: PatchMemory>(api: &M, config: &Config) {
716
apply_patch(
817
api,
918
config,

src/patches/bypass.rs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use crate::config::Config;
2-
use crate::patch_engine::{PatchVariant, VersionedPatch, apply_patch};
2+
use crate::patch_engine::{apply_patch, PatchVariant, VersionedPatch};
33
use crate::util::api::Api;
4+
use crate::util::memory::PatchMemory;
45

56
const BYPASS_1080P_EXPECTED: &[u8] = &[
67
0x81, 0xBC, 0x24, 0x34, 0x02, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x75, 0x1F, 0x81, 0xBC, 0x24,
@@ -13,6 +14,10 @@ const BYPASS_1080P_PATCH: &[u8] = &[
1314
];
1415

1516
pub fn apply(api: &Api, config: &Config) {
17+
apply_early(api, config);
18+
}
19+
20+
pub(crate) fn apply_early<M: PatchMemory>(api: &M, config: &Config) {
1621
apply_bypass_1080p(api, config);
1722
apply_bypass_120hz(api, config);
1823
apply_patch(
@@ -32,7 +37,7 @@ pub fn apply(api: &Api, config: &Config) {
3237
);
3338
}
3439

35-
pub fn apply_bypass_120hz(api: &Api, config: &Config) {
40+
fn apply_bypass_120hz<M: PatchMemory>(api: &M, config: &Config) {
3641
apply_patch(
3742
api,
3843
config,
@@ -52,7 +57,7 @@ pub fn apply_bypass_120hz(api: &Api, config: &Config) {
5257
);
5358
}
5459

55-
fn apply_bypass_1080p(api: &Api, config: &Config) {
60+
fn apply_bypass_1080p<M: PatchMemory>(api: &M, config: &Config) {
5661
apply_patch(
5762
api,
5863
config,

src/patches/custom_freeplay.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use crate::config::Config;
22
use crate::util::api::Api;
3-
use crate::util::memory::{PatchResult, patch_bytes, write_value};
3+
use crate::util::memory::{patch_bytes, PatchResult};
44
use crate::util::pattern;
55

66
const FREE_PLAY_TEXT_EXPECTED: &[u8] = b"FREE PLAY";
@@ -29,7 +29,7 @@ pub fn apply(api: &Api, config: &Config) {
2929
}
3030

3131
let length_addr = find_length_addr(api, text_addr);
32-
if length_addr == 0 || !write_value(api, length_addr, text_bytes.len() as u8) {
32+
if length_addr == 0 || !api.mem_write(length_addr, &(text_bytes.len() as u8).to_le_bytes()) {
3333
api.log_warn("patch write failed: custom FREE PLAY text length");
3434
return;
3535
}
@@ -46,7 +46,7 @@ fn find_length_addr(api: &Api, text_addr: usize) -> usize {
4646
let addr_bytes = (text_addr as u32).to_le_bytes();
4747
let mut sig = [0u8; 7];
4848
sig[0] = 0x6A; // PUSH imm8
49-
// sig[1] = length (wildcard)
49+
// sig[1] = length (wildcard)
5050
sig[2] = 0x68; // PUSH imm32
5151
sig[3..7].copy_from_slice(&addr_bytes);
5252
let mask = "x?xxxxx";

src/patches/free_play.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
use crate::config::Config;
2-
use crate::patch_engine::{PatchVariant, VersionedPatch, apply_patch};
2+
use crate::patch_engine::{apply_patch, PatchVariant, VersionedPatch};
33
use crate::util::api::Api;
4+
use crate::util::memory::PatchMemory;
45

56
pub fn apply(api: &Api, config: &Config) {
7+
apply_early(api, config);
8+
}
9+
10+
pub(crate) fn apply_early<M: PatchMemory>(api: &M, config: &Config) {
611
apply_patch(
712
api,
813
config,

src/patches/mod.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,19 @@ pub mod unlock_tracks;
1414

1515
use crate::config::Config;
1616
use crate::util::api::Api;
17+
use crate::util::memory::PatchMemory;
18+
19+
pub fn apply_early<M: PatchMemory>(memory: &M, config: &Config) {
20+
skip_startup::apply_early(memory, config);
21+
free_play::apply_early(memory, config);
22+
timers::apply_early(memory, config);
23+
skip_map_anim::apply_early(memory, config);
24+
unlock_tracks::apply_early(memory, config);
25+
unlock_120fps::apply_early(memory, config);
26+
network::apply_early(memory, config);
27+
bypass::apply_early(memory, config);
28+
audio::apply_early(memory, config);
29+
}
1730

1831
pub fn apply_all(api: &Api, config: &Config) {
1932
custom_version::apply(api, config);

0 commit comments

Comments
 (0)