Skip to content

Commit 1904b55

Browse files
committed
IntegrityCheckBypass: Fix for RE9 1.1.1.0
1 parent bf2dd2d commit 1904b55

1 file changed

Lines changed: 66 additions & 19 deletions

File tree

src/mods/IntegrityCheckBypass.cpp

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

18311878
void IntegrityCheckBypass::remove_stack_destroyer() {

0 commit comments

Comments
 (0)