Skip to content

Commit f8061df

Browse files
committed
Merge branch 'master' into pd-upscaler
2 parents f968de2 + 2c58f73 commit f8061df

File tree

2 files changed

+178
-70
lines changed

2 files changed

+178
-70
lines changed

src/mods/IntegrityCheckBypass.cpp

Lines changed: 177 additions & 70 deletions
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
{
@@ -1874,78 +1879,84 @@ void IntegrityCheckBypass::immediate_patch_re9() {
18741879
}*/
18751880

18761881
static std::vector<SafetyHookMid> callsites{};
1882+
const auto candidate_pats = std::vector<std::string>{
1883+
"? 8b ? 08 ? 8b ? 10 ? 8b ? 18 48 85 c9 0f 84 ? ? ? ? ff d0", // observed in RE9 PC, MHSTORIES 3
1884+
"? 8b ? 08 ? 8b ? 10 ? 8b ? 18 48 85 c9 74 ? ff d0", // Rare path sometimes taken. seen in both.
1885+
};
18771886

1878-
for (auto ref = utility::scan(utility::get_executable(), "? 8b ? 08 ? 8b ? 10 ? 8b ? 18 48 85 c9 0f 84 ? ? ? ? ff d0");
1879-
ref;
1880-
ref = utility::scan((*ref + 1), game_end - (*ref + 1), "? 8b ? 08 ? 8b ? 10 ? 8b ? 18 48 85 c9 0f 84 ? ? ? ? ff d0"))
1881-
{
1882-
const auto dec = utility::decode_one((uint8_t*)(*ref));
1883-
int reg = NDR_RDX; // default to rdx, which is the most common register used for the job descriptor pointer in observed patterns
1884-
if (dec && dec->OperandsCount >= 2 && dec->Operands[1].Type == ND_OP_MEM) {
1885-
// determine register being used in right hand side (mem)
1886-
reg = dec->Operands[1].Info.Memory.Base;
1887-
spdlog::info("[IntegrityCheckBypass]: Found candidate call site for job submission with integrity check in RE9 @ 0x{:X}, using register {} for descriptor", *ref, reg);
1888-
} else {
1889-
spdlog::warn("[IntegrityCheckBypass]: Found candidate call site for job submission with integrity check in RE9 @ 0x{:X}, but failed to decode register used for descriptor, defaulting to rdx", *ref);
1890-
}
1891-
1892-
switch (reg)
1887+
for (const auto& pat : candidate_pats) {
1888+
for (auto ref = utility::scan(utility::get_executable(), pat);
1889+
ref;
1890+
ref = utility::scan((*ref + 1), game_end - (*ref + 1), pat))
18931891
{
1894-
case NDR_RAX:
1895-
callsites.emplace_back(safetyhook::create_mid((void*)(*ref + 4), &validate_job_func<NDR_RAX>));
1896-
break;
1897-
case NDR_RCX:
1898-
callsites.emplace_back(safetyhook::create_mid((void*)(*ref + 4), &validate_job_func<NDR_RCX>));
1899-
break;
1900-
case NDR_RDX:
1901-
callsites.emplace_back(safetyhook::create_mid((void*)(*ref + 4), &validate_job_func<NDR_RDX>));
1902-
break;
1903-
case NDR_RBX:
1904-
callsites.emplace_back(safetyhook::create_mid((void*)(*ref + 4), &validate_job_func<NDR_RBX>));
1905-
break;
1906-
case NDR_RSP:
1907-
callsites.emplace_back(safetyhook::create_mid((void*)(*ref + 4), &validate_job_func<NDR_RSP>));
1908-
break;
1909-
case NDR_RBP:
1910-
callsites.emplace_back(safetyhook::create_mid((void*)(*ref + 4), &validate_job_func<NDR_RBP>));
1911-
break;
1912-
case NDR_RSI:
1913-
callsites.emplace_back(safetyhook::create_mid((void*)(*ref + 4), &validate_job_func<NDR_RSI>));
1914-
break;
1915-
case NDR_RDI:
1916-
callsites.emplace_back(safetyhook::create_mid((void*)(*ref + 4), &validate_job_func<NDR_RDI>));
1917-
break;
1918-
case NDR_R8:
1919-
callsites.emplace_back(safetyhook::create_mid((void*)(*ref + 4), &validate_job_func<NDR_R8>));
1920-
break;
1921-
case NDR_R9:
1922-
callsites.emplace_back(safetyhook::create_mid((void*)(*ref + 4), &validate_job_func<NDR_R9>));
1923-
break;
1924-
case NDR_R10:
1925-
callsites.emplace_back(safetyhook::create_mid((void*)(*ref + 4), &validate_job_func<NDR_R10>));
1926-
break;
1927-
case NDR_R11:
1928-
callsites.emplace_back(safetyhook::create_mid((void*)(*ref + 4), &validate_job_func<NDR_R11>));
1929-
break;
1930-
case NDR_R12:
1931-
callsites.emplace_back(safetyhook::create_mid((void*)(*ref + 4), &validate_job_func<NDR_R12>));
1932-
break;
1933-
case NDR_R13:
1934-
callsites.emplace_back(safetyhook::create_mid((void*)(*ref + 4), &validate_job_func<NDR_R13>));
1935-
break;
1936-
case NDR_R14:
1937-
callsites.emplace_back(safetyhook::create_mid((void*)(*ref + 4), &validate_job_func<NDR_R14>));
1938-
break;
1939-
case NDR_R15:
1940-
callsites.emplace_back(safetyhook::create_mid((void*)(*ref + 4), &validate_job_func<NDR_R15>));
1941-
break;
1942-
default:
1943-
callsites.emplace_back(safetyhook::create_mid((void*)(*ref + 4), &validate_job_func<NDR_RDX>));
1944-
break;
1945-
};
1892+
const auto dec = utility::decode_one((uint8_t*)(*ref));
1893+
int reg = NDR_RDX; // default to rdx, which is the most common register used for the job descriptor pointer in observed patterns
1894+
if (dec && dec->OperandsCount >= 2 && dec->Operands[1].Type == ND_OP_MEM) {
1895+
// determine register being used in right hand side (mem)
1896+
reg = dec->Operands[1].Info.Memory.Base;
1897+
spdlog::info("[IntegrityCheckBypass]: Found candidate call site for job submission with integrity check in RE9 @ 0x{:X}, using register {} for descriptor", *ref, reg);
1898+
} else {
1899+
spdlog::warn("[IntegrityCheckBypass]: Found candidate call site for job submission with integrity check in RE9 @ 0x{:X}, but failed to decode register used for descriptor, defaulting to rdx", *ref);
1900+
}
1901+
1902+
switch (reg)
1903+
{
1904+
case NDR_RAX:
1905+
callsites.emplace_back(safetyhook::create_mid((void*)(*ref + 4), &validate_job_func<NDR_RAX>));
1906+
break;
1907+
case NDR_RCX:
1908+
callsites.emplace_back(safetyhook::create_mid((void*)(*ref + 4), &validate_job_func<NDR_RCX>));
1909+
break;
1910+
case NDR_RDX:
1911+
callsites.emplace_back(safetyhook::create_mid((void*)(*ref + 4), &validate_job_func<NDR_RDX>));
1912+
break;
1913+
case NDR_RBX:
1914+
callsites.emplace_back(safetyhook::create_mid((void*)(*ref + 4), &validate_job_func<NDR_RBX>));
1915+
break;
1916+
case NDR_RSP:
1917+
callsites.emplace_back(safetyhook::create_mid((void*)(*ref + 4), &validate_job_func<NDR_RSP>));
1918+
break;
1919+
case NDR_RBP:
1920+
callsites.emplace_back(safetyhook::create_mid((void*)(*ref + 4), &validate_job_func<NDR_RBP>));
1921+
break;
1922+
case NDR_RSI:
1923+
callsites.emplace_back(safetyhook::create_mid((void*)(*ref + 4), &validate_job_func<NDR_RSI>));
1924+
break;
1925+
case NDR_RDI:
1926+
callsites.emplace_back(safetyhook::create_mid((void*)(*ref + 4), &validate_job_func<NDR_RDI>));
1927+
break;
1928+
case NDR_R8:
1929+
callsites.emplace_back(safetyhook::create_mid((void*)(*ref + 4), &validate_job_func<NDR_R8>));
1930+
break;
1931+
case NDR_R9:
1932+
callsites.emplace_back(safetyhook::create_mid((void*)(*ref + 4), &validate_job_func<NDR_R9>));
1933+
break;
1934+
case NDR_R10:
1935+
callsites.emplace_back(safetyhook::create_mid((void*)(*ref + 4), &validate_job_func<NDR_R10>));
1936+
break;
1937+
case NDR_R11:
1938+
callsites.emplace_back(safetyhook::create_mid((void*)(*ref + 4), &validate_job_func<NDR_R11>));
1939+
break;
1940+
case NDR_R12:
1941+
callsites.emplace_back(safetyhook::create_mid((void*)(*ref + 4), &validate_job_func<NDR_R12>));
1942+
break;
1943+
case NDR_R13:
1944+
callsites.emplace_back(safetyhook::create_mid((void*)(*ref + 4), &validate_job_func<NDR_R13>));
1945+
break;
1946+
case NDR_R14:
1947+
callsites.emplace_back(safetyhook::create_mid((void*)(*ref + 4), &validate_job_func<NDR_R14>));
1948+
break;
1949+
case NDR_R15:
1950+
callsites.emplace_back(safetyhook::create_mid((void*)(*ref + 4), &validate_job_func<NDR_R15>));
1951+
break;
1952+
default:
1953+
callsites.emplace_back(safetyhook::create_mid((void*)(*ref + 4), &validate_job_func<NDR_RDX>));
1954+
break;
1955+
};
19461956

1947-
//callsites.emplace_back(safetyhook::create_mid((void*)(*ref + 4), validate_job_func
1948-
spdlog::info("[IntegrityCheckBypass]: Hooked call site at 0x{:X}", *ref);
1957+
//callsites.emplace_back(safetyhook::create_mid((void*)(*ref + 4), validate_job_func
1958+
spdlog::info("[IntegrityCheckBypass]: Hooked call site at 0x{:X}", *ref);
1959+
}
19491960
}
19501961
}
19511962

@@ -2102,6 +2113,102 @@ void IntegrityCheckBypass::immediate_patch_re9() {
21022113
}
21032114
}
21042115

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+
21052212
void IntegrityCheckBypass::remove_stack_destroyer() {
21062213
spdlog::info("[IntegrityCheckBypass]: Searching for stack destroyer...");
21072214

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)