Skip to content

Commit f59350b

Browse files
committed
IntegrityCheckBypass: Update for MHSTORIES3 (probably fragile)
1 parent 98a3b8e commit f59350b

1 file changed

Lines changed: 92 additions & 2 deletions

File tree

src/mods/IntegrityCheckBypass.cpp

Lines changed: 92 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)