Skip to content

Commit 2c58f73

Browse files
committed
IntegrityCheckBypass (MHSTORIES3): Fix crashes using robust method
This should keep up with newer games/updates (probably!)
1 parent e7b8ce9 commit 2c58f73

2 files changed

Lines changed: 103 additions & 1 deletion

File tree

src/mods/IntegrityCheckBypass.cpp

Lines changed: 102 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,8 @@ std::optional<std::string> IntegrityCheckBypass::on_initialize() {
257257
}
258258

259259
void IntegrityCheckBypass::on_frame() {
260+
re9_heartbeat_bypass();
261+
260262
#ifdef RE3
261263
if (m_bypass_integrity_checks != nullptr) {
262264
*m_bypass_integrity_checks = true;
@@ -1649,6 +1651,7 @@ void IntegrityCheckBypass::immediate_patch_re9() {
16491651
// Invariant that works through obfuscation. They don't obfuscate the epilogue of the block above the slow path conditional.
16501652
// The xor rcx,rsp + call __security_check_cookie + vmovaps xmm6 sequence is compiler-generated and stable.
16511653
// In new builds there was a sub rbp, rbp randomly inserted after the vmovaps, so we added a wildcard functionality to the signature scan to allow some instructions in between.
1654+
16521655
const auto function_epilogue_sig = "48 31 E1 E8 ? ? ? ? *[5] C5 F8 28 B4 24 D0 01 00 00 *[5] 48 81 C4 E8 01 00 00";
16531656
std::optional<uintptr_t> result{};
16541657
size_t nop_size{};
@@ -1750,6 +1753,7 @@ void IntegrityCheckBypass::immediate_patch_re9() {
17501753
// epilogue signature above doesn't match). The UD2 writer instruction 'mov [rax+rcx+8], rdx'
17511754
// (48 89 ? 08 08) is unique or near-unique in the anti-tamper section. Searching backwards from it
17521755
// for the SETcc + dispatch table load pattern finds the discriminator reliably.
1756+
#if 0
17531757
if (!result) {
17541758
spdlog::info("[IntegrityCheckBypass]: Epilogue scan failed, trying UD2 writer anchor approach...");
17551759

@@ -1833,6 +1837,7 @@ void IntegrityCheckBypass::immediate_patch_re9() {
18331837
}
18341838
}
18351839
}
1840+
#endif
18361841

18371842
if (result) {
18381843
spdlog::info("[IntegrityCheckBypass]: Found slow path discriminator @ 0x{:X} ({}B), patching...", *result, nop_size);
@@ -1845,7 +1850,7 @@ void IntegrityCheckBypass::immediate_patch_re9() {
18451850
nops.resize(nop_size, 0x90);
18461851
static auto patch = Patch::create(*result, nops, true);
18471852
spdlog::info("[IntegrityCheckBypass]: Patched slow path discriminator!");
1848-
}
1853+
}
18491854

18501855
// Hook this anyways as a backup plan.
18511856
{
@@ -2108,6 +2113,102 @@ void IntegrityCheckBypass::immediate_patch_re9() {
21082113
}
21092114
}
21102115

2116+
void IntegrityCheckBypass::re9_heartbeat_bypass() {
2117+
// let me explain what's happening here.
2118+
// because the obfuscation has been randomized around the areas we've been patching so far (immediate_patch_re9, see commented out code)
2119+
// I had become a bit fed up with manually fixing broken anti-tamper bypasses every update.
2120+
// So I wrote an emulator that executed the RenderTaskEnd path (which contains anti-tamper code, especially the penalty code)
2121+
// During my analysis of the trace, I found the conditional that decided between the penalty or the clean path.
2122+
// So instead of patching that, I wanted to figure out WHAT caused that conditional to evaluate to "tampered" in the first place.
2123+
// I found that, inside of a bunch of horrible obfuscated code, it was evaluating some value inside the renderer.
2124+
// In this case it almost looked like the frame count.
2125+
// I analyzed the memory region near this frame count and noticed 6 other values very close in value to the frame count, and they were all being updated
2126+
// every 500ms or so to the actual frame count.
2127+
// I noticed that when any of these frame counts were set to 0, the penalty path triggered and the game lagged to hell or crashed.
2128+
// I then noticed that making these values equal to the frame count always made the clean path trigger, even if the integrity checks were triggered.
2129+
// No patching necessary!
2130+
#if TDB_VER >= 82
2131+
static auto renderer_t = sdk::find_type_definition("via.render.Renderer");
2132+
static auto get_RenderFrame = renderer_t != nullptr ? renderer_t->get_method("get_RenderFrame") : nullptr;
2133+
auto renderer = sdk::get_native_singleton("via.render.Renderer");
2134+
2135+
if (renderer != nullptr && renderer_t != nullptr && get_RenderFrame != nullptr) {
2136+
static uint32_t* heartbeat_offset_start{nullptr};
2137+
static std::vector<uintptr_t> candidates{};
2138+
static uint32_t last_scan_frame = 0;
2139+
static int confirmation_count = 0;
2140+
static constexpr int CONFIRMATIONS_NEEDED = 5;
2141+
static constexpr int32_t MAX_DISTANCE = 1000;
2142+
static constexpr size_t HEARTBEAT_COUNT = 6;
2143+
2144+
const auto frame_count = get_RenderFrame->call<uint32_t>(); // static func
2145+
const auto renderer_addr = (uintptr_t)renderer;
2146+
2147+
if (heartbeat_offset_start != nullptr) {
2148+
// Confirmed, sync heartbeats to frame counter every frame
2149+
for (size_t i = 0; i < HEARTBEAT_COUNT; i++) {
2150+
heartbeat_offset_start[i] = frame_count;
2151+
}
2152+
} else if (frame_count > 100 && frame_count != last_scan_frame) {
2153+
last_scan_frame = frame_count;
2154+
2155+
// Scan renderer struct for runs of 6 consecutive DWORDs all within
2156+
// MAX_DISTANCE of frame_count (and <= frame_count).
2157+
std::vector<uintptr_t> this_frame{};
2158+
for (size_t i = 0x2000; i + HEARTBEAT_COUNT * 4 <= 0x4000; i += sizeof(uint32_t)) {
2159+
try {
2160+
auto* ints = reinterpret_cast<uint32_t*>(renderer_addr + i);
2161+
if (ints[-1] != 1) {
2162+
continue; // the DWORD immediately preceding the 6 we care about should be 1, it's used as a sentinel for the start of the heartbeat cluster.
2163+
}
2164+
if (ints[HEARTBEAT_COUNT] != 0) {
2165+
continue; // the DWORD immediately following the 6 we care about should be 0, it's used as a sentinel for the end of the heartbeat cluster.
2166+
}
2167+
bool ok = true;
2168+
for (size_t j = 0; j < HEARTBEAT_COUNT; j++) {
2169+
auto val = ints[j];
2170+
if (val < 100 || val > frame_count || (frame_count - val) >= MAX_DISTANCE) {
2171+
ok = false;
2172+
break;
2173+
}
2174+
}
2175+
if (ok) {
2176+
this_frame.push_back(renderer_addr + i);
2177+
}
2178+
} catch (...) {}
2179+
}
2180+
2181+
if (candidates.empty()) {
2182+
// First scan, seed candidates
2183+
candidates = std::move(this_frame);
2184+
confirmation_count = 1;
2185+
} else {
2186+
// Intersect with previous candidates, only keep offsets
2187+
// that match across multiple frames
2188+
std::vector<uintptr_t> intersection{};
2189+
for (auto addr : candidates) {
2190+
if (std::find(this_frame.begin(), this_frame.end(), addr) != this_frame.end()) {
2191+
intersection.push_back(addr);
2192+
}
2193+
}
2194+
candidates = std::move(intersection);
2195+
confirmation_count++;
2196+
2197+
if (candidates.size() == 1 && confirmation_count >= CONFIRMATIONS_NEEDED) {
2198+
heartbeat_offset_start = (uint32_t*)candidates[0];
2199+
spdlog::info("[IntegrityCheckBypass] Found heartbeat cluster at renderer+0x{:X} after {} confirmations at frame count {}, syncing it to frame count every frame now",
2200+
(uintptr_t)heartbeat_offset_start - renderer_addr, confirmation_count, frame_count);
2201+
} else if (candidates.empty()) {
2202+
// Lost all candidates, restart
2203+
confirmation_count = 0;
2204+
spdlog::warn("[IntegrityCheckBypass] Heartbeat candidates lost, restarting scan");
2205+
}
2206+
}
2207+
}
2208+
}
2209+
#endif
2210+
}
2211+
21112212
void IntegrityCheckBypass::remove_stack_destroyer() {
21122213
spdlog::info("[IntegrityCheckBypass]: Searching for stack destroyer...");
21132214

src/mods/IntegrityCheckBypass.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ class IntegrityCheckBypass : public Mod {
2929
static void immediate_patch_re4();
3030
static void immediate_patch_dd2();
3131
static void immediate_patch_re9();
32+
static void re9_heartbeat_bypass();
3233
static void remove_stack_destroyer();
3334

3435
static void setup_pristine_syscall();

0 commit comments

Comments
 (0)