@@ -257,6 +257,8 @@ std::optional<std::string> IntegrityCheckBypass::on_initialize() {
257257}
258258
259259void 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+
21052212void IntegrityCheckBypass::remove_stack_destroyer () {
21062213 spdlog::info (" [IntegrityCheckBypass]: Searching for stack destroyer..." );
21072214
0 commit comments