@@ -1653,6 +1653,7 @@ void IntegrityCheckBypass::immediate_patch_re9() {
16531653 std::optional<uintptr_t > result{};
16541654 size_t nop_size{};
16551655
1656+ #ifdef RE9
16561657 for (auto ref = utility::scan (game, function_epilogue_sig);
16571658 ref.has_value ();
16581659 ref = utility::scan (*ref + 1 , (game_end - (*ref + 1 )) - 0x1000 , function_epilogue_sig))
@@ -1743,9 +1744,98 @@ void IntegrityCheckBypass::immediate_patch_re9() {
17431744 break ;
17441745 }
17451746 }
1747+ #endif
1748+
1749+ // Fallback: UD2 writer anchor approach (works for MHSTORIES3 and other games where the
1750+ // epilogue signature above doesn't match). The UD2 writer instruction 'mov [rax+rcx+8], rdx'
1751+ // (48 89 ? 08 08) is unique or near-unique in the anti-tamper section. Searching backwards from it
1752+ // for the SETcc + dispatch table load pattern finds the discriminator reliably.
1753+ if (!result) {
1754+ spdlog::info (" [IntegrityCheckBypass]: Epilogue scan failed, trying UD2 writer anchor approach..." );
1755+
1756+ for (auto ud2_ref = utility::scan (game, " 48 89 ? 08 08" );
1757+ ud2_ref.has_value () && !result;
1758+ ud2_ref = utility::scan (*ud2_ref + 1 , (game_end - (*ud2_ref + 1 )) - 0x1000 , " 48 89 ? 08 08" ))
1759+ {
1760+ // Filter: the real UD2 writer uses SIB addressing: mov [base+index+disp8], reg.
1761+ // ModR/M byte (offset +2) must have rm=100 (SIB follows) and mod=01 (8-bit disp).
1762+ // False positives like mov [rdi+0x808],rax have rm=111 and mod=10 (32-bit disp).
1763+ const uint8_t modrm = *reinterpret_cast <const uint8_t *>(*ud2_ref + 2 );
1764+ if ((modrm & 0xC7 ) != 0x44 ) { // mod=01, rm=100 -> SIB + disp8
1765+ continue ;
1766+ }
1767+
1768+ // Search backwards from the UD2 writer for the dispatch pattern:
1769+ // [REX?] 0F 9x {ModR/M mod=11} [REX.W] 8B {ModR/M rm=100(SIB)} {SIB scale=8}
1770+ // The SETcc sets an index (0 or 1), the MOV loads from a 2-entry dispatch table.
1771+ const auto search_start = (*ud2_ref > 0x2000 ) ? (*ud2_ref - 0x2000 ) : (uintptr_t )game;
1772+ const uint8_t * base = reinterpret_cast <const uint8_t *>(search_start);
1773+ const size_t search_len = *ud2_ref - search_start;
1774+
1775+ for (size_t i = 0 ; i < search_len; i++) {
1776+ // Check for 0F 9x with the byte after having mod=11 (>= 0xC0)
1777+ size_t setcc_off = 0 ;
1778+ size_t setcc_len = 0 ;
1779+
1780+ // Pattern A: no REX prefix on SETcc -> 0F 9? {mod=11}
1781+ if (base[i] == 0x0F && (base[i+1 ] & 0xF0 ) == 0x90 && (base[i+2 ] & 0xC0 ) == 0xC0 ) {
1782+ setcc_off = i;
1783+ setcc_len = 3 ;
1784+ }
1785+ // Pattern B: REX prefix (40-4F) before SETcc -> 4? 0F 9? {mod=11}
1786+ else if ((base[i] & 0xF0 ) == 0x40 && base[i+1 ] == 0x0F && (base[i+2 ] & 0xF0 ) == 0x90 && (base[i+3 ] & 0xC0 ) == 0xC0 ) {
1787+ setcc_off = i;
1788+ setcc_len = 4 ;
1789+ }
1790+ else {
1791+ continue ;
1792+ }
1793+
1794+ // Now check if a MOV with SIB scale=8 follows within the next few bytes
1795+ // (there may be 0-2 intervening bytes between the SETcc and the MOV).
1796+ size_t dispatch_mov_end = 0 ;
1797+ bool found_dispatch = false ;
1798+ for (size_t j = setcc_off + setcc_len; j < setcc_off + setcc_len + 4 && j + 3 < search_len; j++) {
1799+ // REX.W prefix (48-4F) followed by 8B (MOV), ModR/M with rm=100 (SIB), SIB with scale=8
1800+ if ((base[j] & 0xF0 ) == 0x40 && base[j+1 ] == 0x8B && (base[j+2 ] & 0x07 ) == 0x04 && (base[j+3 ] & 0xC0 ) == 0xC0 ) {
1801+ found_dispatch = true ;
1802+ dispatch_mov_end = j + 4 ; // byte after the 4-byte MOV+SIB
1803+ break ;
1804+ }
1805+ }
1806+
1807+ if (!found_dispatch) {
1808+ continue ;
1809+ }
1810+
1811+ // Final verification: this must be a anti-tamper dispatch block, not normal game code.
1812+ // anti-tamper dispatches always end with 'xchg [rsp], rXX; ret' (obfuscated indirect jmp).
1813+ // Compilers never emit this pattern. Search forward from the dispatch MOV for:
1814+ // [REX?] 87 {ModR/M: mod=00, rm=100(SIB)} 24(SIB=[rsp]) C3(ret)
1815+ bool has_xchg_ret = false ;
1816+ for (size_t k = dispatch_mov_end; k + 4 < search_len && k < dispatch_mov_end + 30 ; k++) {
1817+ size_t xo = k;
1818+ if ((base[xo] & 0xF0 ) == 0x40 ) xo++; // skip optional REX
1819+ if (xo + 3 < search_len &&
1820+ base[xo] == 0x87 && (base[xo+1 ] & 0xC7 ) == 0x04 && base[xo+2 ] == 0x24 && base[xo+3 ] == 0xC3 ) {
1821+ has_xchg_ret = true ;
1822+ break ;
1823+ }
1824+ }
1825+
1826+ if (has_xchg_ret) {
1827+ result = search_start + setcc_off;
1828+ nop_size = setcc_len;
1829+ spdlog::info (" [IntegrityCheckBypass]: Found SETcc dispatch via UD2 writer anchor @ 0x{:X} ({}B), UD2 writer @ 0x{:X}" ,
1830+ *result, nop_size, *ud2_ref);
1831+ break ;
1832+ }
1833+ }
1834+ }
1835+ }
17461836
17471837 if (result) {
1748- spdlog::info (" [IntegrityCheckBypass]: Found slow path discriminator in RE9 @ 0x{:X} ({}B), patching..." , *result, nop_size);
1838+ spdlog::info (" [IntegrityCheckBypass]: Found slow path discriminator @ 0x{:X} ({}B), patching..." , *result, nop_size);
17491839 // NOP the conditional. This forces the dispatch index to its default (clean) value:
17501840 // - For SETcc: the target register keeps its restored value (0) from the surrounding obfuscation,
17511841 // so the dispatch table always selects index 0 (the clean path).
@@ -1754,7 +1844,7 @@ void IntegrityCheckBypass::immediate_patch_re9() {
17541844 std::vector<int16_t > nops{};
17551845 nops.resize (nop_size, 0x90 );
17561846 static auto patch = Patch::create (*result, nops, true );
1757- spdlog::info (" [IntegrityCheckBypass]: Patched slow path discriminator in RE9 !" );
1847+ spdlog::info (" [IntegrityCheckBypass]: Patched slow path discriminator!" );
17581848 }
17591849
17601850 // Hook this anyways as a backup plan.
0 commit comments