@@ -1626,7 +1626,9 @@ void IntegrityCheckBypass::immediate_patch_re9() {
16261626 // auto thread_scheduler_corruptor = utility::scan(game, "48 89 74 08 08 48 89 F0");
16271627
16281628 // Invariant that works through obfuscation. They don't obfuscate the epilogue of the block above the slow path conditional.
1629- const auto function_epilogue_sig = " 48 31 e1 e8 ? ? ? ? C5 F8 28 B4 24 D0 01 00 00 48 81 c4 e8 01" ;
1629+ // The xor rcx,rsp + call __security_check_cookie + vmovaps xmm6 sequence is compiler-generated and stable.
1630+ // 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.
1631+ 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" ;
16301632 std::optional<uintptr_t > result{};
16311633 size_t nop_size{};
16321634
@@ -1655,43 +1657,82 @@ void IntegrityCheckBypass::immediate_patch_re9() {
16551657
16561658 spdlog::info (" Checking candidate at 0x{:X}, pop_count: {}" , *ref, pop_count);
16571659
1658- // Now check if we have a cmov conditional nearby.
1659- bool prev_was_ret = false ;
1660- utility::linear_decode ((uint8_t *)*ref, 0x150 , [&](utility::ExhaustionContext& ctx) -> bool {
1661- #if 0
1662- char buf[256]{};
1663- NdToText(&ctx.instrux, ctx.addr, sizeof(buf), buf);
1664- spdlog::info(" 0x{:X}: {}", ctx.addr, buf);
1665- #endif
1660+ // Linear decode past rets into the dispatch block. Look for the slow path discriminator:
1661+ // a SETcc or CMOVcc instruction followed by a dispatch table access [reg + reg*8].
1662+ // Old obfuscation used CMOVcc as the dispatch table load itself.
1663+ // New obfuscation uses SETcc to set an index, then a separate MOV rXX, [rYY + rZZ*8].
1664+ // The semantic proof that this is an obfuscated dispatch block is the presence of BOTH:
1665+ // 1) a flag-dependent instruction (CMOVcc or SETcc)
1666+ // 2) a SIB-indexed memory load with scale=8 (the 2-entry dispatch table)
1667+ std::optional<uintptr_t > cond_addr{};
1668+ size_t cond_nop_size{};
1669+ bool has_dispatch_table = false ;
1670+ bool already_in_dispatch_block = false ;
16661671
1672+ utility::linear_decode ((uint8_t *)*ref, 0x150 , [&](utility::ExhaustionContext& ctx) -> bool {
1673+ // CMOVcc pattern (old + variant): The cmov IS the dispatch selector.
1674+ // Variant A: cmovcc rcx, [rip+disp32] (memory source, dispatch table load)
1675+ // Variant B: cmovcc rcx, rbx (register source, both paths loaded via LEA)
1676+ // In all observed variants the CMOVcc conditionally overwrites the default (clean)
1677+ // path with the penalty path. NOPing it keeps the clean path unconditionally.
1678+ // The structural context (epilogue match + pop_count <= 2 + ret-skip) is sufficient
1679+ // to confirm this is an obfuscated dispatch, no operand-type check needed.
16671680 if (ctx.instrux .Instruction == ND_INS_CMOVcc) {
1668- result = ctx.addr ;
1669- nop_size = ctx.instrux .Length ;
1681+ cond_addr = ctx.addr ;
1682+ cond_nop_size = ctx.instrux .Length ;
1683+ has_dispatch_table = true ;
16701684 return false ;
16711685 }
16721686
1673- // Stop at ret/int3/jmp
1687+ // New pattern: SETcc sets an index register, then a separate MOV rXX, [rYY + rZZ*8]
1688+ // reads from the 2-entry dispatch table using that index.
1689+ if (ctx.instrux .Instruction == ND_INS_SETcc) {
1690+ cond_addr = ctx.addr ;
1691+ cond_nop_size = ctx.instrux .Length ;
1692+ }
1693+
1694+ // After finding a SETcc, look for the dispatch table access: MOV with [base + index*8]
1695+ if (cond_addr.has_value () && ctx.instrux .Instruction == ND_INS_MOV) {
1696+ for (uint8_t i = 0 ; i < ctx.instrux .OperandsCount ; i++) {
1697+ const auto & op = ctx.instrux .Operands [i];
1698+ if (op.Type == ND_OP_MEM && op.Info .Memory .HasIndex && op.Info .Memory .HasBase && op.Info .Memory .Scale == 8 ) {
1699+ has_dispatch_table = true ;
1700+ return false ;
1701+ }
1702+ }
1703+ }
1704+
1705+ // Skip past ret + garbage byte to continue into the next obfuscated block.
16741706 if (ctx.instrux .Category == ND_CAT_RET) {
1675- // advance ip by 1 to get to the other basic block.
1676- // this is part of their obfuscation where they insert a random byte after a ret
1677- // to break linear decoders, it really messes with IDA for example.
1707+ if (already_in_dispatch_block) {
1708+ // We've already passed through one RET, the next RET would be the end of the dispatch block, so stop.
1709+ return false ;
1710+ }
1711+
1712+ already_in_dispatch_block = true ;
16781713 ctx.addr += 1 ;
16791714 }
16801715 return true ;
16811716 });
16821717
1683- if (result) {
1718+ if (cond_addr.has_value () && has_dispatch_table) {
1719+ result = cond_addr;
1720+ nop_size = cond_nop_size;
16841721 break ;
16851722 }
16861723 }
16871724
16881725 if (result) {
1689- spdlog::info (" [IntegrityCheckBypass]: Found conditional move instruction for thread scheduler corruptor in RE9 @ 0x{:X}, patching..." , *result);
1690- // Patch the cmov to to do nothing, the correct path is already in rcx.
1726+ spdlog::info (" [IntegrityCheckBypass]: Found slow path discriminator in RE9 @ 0x{:X} ({}B), patching..." , *result, nop_size);
1727+ // NOP the conditional. This forces the dispatch index to its default (clean) value:
1728+ // - For SETcc: the target register keeps its restored value (0) from the surrounding obfuscation,
1729+ // so the dispatch table always selects index 0 (the clean path).
1730+ // - For CMOVcc: the destination register keeps the value from the preceding MOV (the default path),
1731+ // preventing the conditional overwrite to the penalty path.
16911732 std::vector<int16_t > nops{};
16921733 nops.resize (nop_size, 0x90 );
16931734 static auto patch = Patch::create (*result, nops, true );
1694- spdlog::info (" [IntegrityCheckBypass]: Patched thread scheduler corruptor in RE9!" );
1735+ spdlog::info (" [IntegrityCheckBypass]: Patched slow path discriminator in RE9!" );
16951736 } else {
16961737 spdlog::error (" [IntegrityCheckBypass]: Could not find conditional move instruction for thread scheduler corruptor in RE9!" );
16971738
@@ -1726,6 +1767,7 @@ void IntegrityCheckBypass::immediate_patch_re9() {
17261767
17271768 // Scan for PE header integrity check (thanks to SunBeam for pointing out this exists in RE9 and showing me where it is!)
17281769 auto before_sig = " 4C 89 ? 24 40 00 00 00 41 ?" ;
1770+ bool patched_pe_header_check = false ;
17291771
17301772 for (auto ref = utility::scan (game, before_sig);
17311773 ref.has_value ();
@@ -1823,9 +1865,14 @@ void IntegrityCheckBypass::immediate_patch_re9() {
18231865 std::vector<int16_t > patch_bytes (raw.begin (), raw.end ());
18241866 static auto pe_header_patch = Patch::create (patch_addr, patch_bytes, true );
18251867 spdlog::info (" [IntegrityCheckBypass]: Patched PE header integrity check with movabs to 0x{:X} (reg: {})" , (uintptr_t )allocated_memory, reg);
1868+ patched_pe_header_check = true ;
18261869 break ;
18271870 }
18281871 }
1872+
1873+ if (!patched_pe_header_check) {
1874+ spdlog::error (" [IntegrityCheckBypass]: Could not find PE header integrity check!" );
1875+ }
18291876}
18301877
18311878void IntegrityCheckBypass::remove_stack_destroyer () {
0 commit comments