// patches.cpp — apply v3b, v5, v11, v12, v14 inline to our own process #include "patches.h" #include "logging.h" #include "thunks.h" #include "ac_addrs.h" #include #include #include #include using namespace leakfix; namespace { // Copy `data` to absolute address `addr`, flipping page protection. bool write_memory(uintptr_t addr, const void* data, size_t len) { DWORD old = 0; if (!VirtualProtect((void*)addr, len, PAGE_EXECUTE_READWRITE, &old)) { logf(" VirtualProtect(0x%08x, %u) failed err=%lu", addr, (unsigned)len, GetLastError()); return false; } std::memcpy((void*)addr, data, len); DWORD restored = 0; VirtualProtect((void*)addr, len, old, &restored); FlushInstructionCache(GetCurrentProcess(), (void*)addr, len); return true; } bool bytes_equal(uintptr_t addr, const void* expected, size_t len) { return std::memcmp((void*)addr, expected, len) == 0; } void hexdump_short(uintptr_t addr, size_t n, char* out, size_t out_sz) { const uint8_t* p = (const uint8_t*)addr; size_t used = 0; for (size_t i = 0; i < n && used + 3 < out_sz; ++i) { used += (size_t)std::snprintf(out + used, out_sz - used, "%02x", p[i]); } } // Write a 5-byte JMP rel32 at `at` targeting `target`. Pad remaining bytes // up to `total_replace` with 0x90 NOPs. bool write_jmp_rel32(uintptr_t at, uintptr_t target, size_t total_replace) { uint8_t buf[64]; if (total_replace > sizeof(buf)) return false; int32_t rel = (int32_t)(target - (at + 5)); buf[0] = 0xE9; std::memcpy(buf + 1, &rel, 4); std::memset(buf + 5, 0x90, total_replace - 5); return write_memory(at, buf, total_replace); } } // anon namespace namespace leakfix { // ===== v3b ===== bool apply_v3b() { const uint8_t nops[3] = { 0x90, 0x90, 0x90 }; const uint8_t orig1[3] = { 0xff, 0x40, 0x24 }; // inc dword [eax+0x24] const uint8_t orig2[3] = { 0xff, 0x46, 0x24 }; // inc dword [esi+0x24] if (bytes_equal(ac::V3B_SITE_1, nops, 3) && bytes_equal(ac::V3B_SITE_2, nops, 3)) { logf("v3b: already applied"); return true; } if (!bytes_equal(ac::V3B_SITE_1, orig1, 3)) { char h[16]; hexdump_short(ac::V3B_SITE_1, 3, h, sizeof(h)); logf("v3b: site1 unexpected bytes %s — refusing", h); return false; } if (!bytes_equal(ac::V3B_SITE_2, orig2, 3)) { char h[16]; hexdump_short(ac::V3B_SITE_2, 3, h, sizeof(h)); logf("v3b: site2 unexpected bytes %s — refusing", h); return false; } write_memory(ac::V3B_SITE_1, nops, 3); write_memory(ac::V3B_SITE_2, nops, 3); logf("v3b: applied (NOPs at 0x%08x + 0x%08x)", ac::V3B_SITE_1, ac::V3B_SITE_2); return true; } // ===== v5 ===== bool apply_v5() { uintptr_t rs_cur = *(uintptr_t*)ac::V5_RS_VTABLE_SLOT_2; uintptr_t rt_cur = *(uintptr_t*)ac::V5_RT_VTABLE_SLOT_2; uintptr_t rs_new = (uintptr_t)&purge_rendersurface_thunk; uintptr_t rt_new = (uintptr_t)&purge_rendertexture_thunk; bool rs_done = (rs_cur != ac::V5_NOOP_STUB_VA); bool rt_done = (rt_cur != ac::V5_NOOP_STUB_VA); if (!rs_done) { if (rs_cur != ac::V5_NOOP_STUB_VA) { logf("v5: RS slot already redirected (0x%08x); not overwriting", rs_cur); } else { write_memory(ac::V5_RS_VTABLE_SLOT_2, &rs_new, 4); logf("v5: RS vtable slot -> 0x%08x", rs_new); } } else { logf("v5: RS slot already non-default (0x%08x) — skipping", rs_cur); } if (!rt_done) { write_memory(ac::V5_RT_VTABLE_SLOT_2, &rt_new, 4); logf("v5: RT vtable slot -> 0x%08x", rt_new); } else { logf("v5: RT slot already non-default (0x%08x) — skipping", rt_cur); } return true; } // ===== v11 ===== bool apply_v11() { // Site 1: 2-byte rewrite of a JMP target const uint8_t s1_orig[2] = { 0xEB, 0x07 }; const uint8_t s1_patched[2] = { 0xEB, 0x42 }; // Site 2: 9-byte rewrite for ~GXTri3Mesh NULL-check const uint8_t s2_orig[9] = { 0x8B, 0x08, 0x50, 0xFF, 0x51, 0x08, 0x89, 0x5E, 0x08 }; const uint8_t s2_patched[9] = { 0x89, 0x5E, 0x08, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90 }; if (bytes_equal(ac::V11_SITE_1_VA, s1_patched, 2)) { logf("v11: site1 already patched"); } else if (bytes_equal(ac::V11_SITE_1_VA, s1_orig, 2)) { write_memory(ac::V11_SITE_1_VA, s1_patched, 2); logf("v11: site1 patched"); } else { char h[8]; hexdump_short(ac::V11_SITE_1_VA, 2, h, sizeof(h)); logf("v11: site1 unexpected %s — skipping", h); } if (bytes_equal(ac::V11_SITE_2_VA, s2_patched, 9)) { logf("v11: site2 already patched"); } else if (bytes_equal(ac::V11_SITE_2_VA, s2_orig, 9)) { write_memory(ac::V11_SITE_2_VA, s2_patched, 9); logf("v11: site2 patched"); } else { char h[24]; hexdump_short(ac::V11_SITE_2_VA, 9, h, sizeof(h)); logf("v11: site2 unexpected %s — skipping", h); } return true; } // ===== v12 RETIRED ===== // v12 was designed against post-Decal in-memory bytes that don't match // the on-disk binary. When the leakfix.dll loads at PE-import time (before // Decal init), it sees the truly-original bytes and v12 would refuse. // When the Python patcher ran later against a running PID, it saw // Decal-modified bytes that happened to match its expected pattern and // applied a duplicate range check — adding no protection beyond what // Decal already provides. Neither variant prevented the Shadow/Frank // stale-heap-pointer crashes. v12 removed. // ===== v14 ===== bool apply_v14() { static const uint8_t ORIG[18] = { 0x8B, 0x86, 0xDC, 0x00, 0x00, 0x00, // mov eax, [esi+0xDC] 0x3B, 0xC3, // cmp eax, ebx 0x74, 0x08, // jz +8 0x8B, 0x00, // mov eax, [eax] 0x3B, 0xC3, // cmp eax, ebx 0x74, 0x02, // jz +2 0x89, 0x18, // mov [eax], ebx <- the broken "fix" }; // If already patched, the first byte is 0xE9 (our JMP). uint8_t cur = *(uint8_t*)ac::V14_PATCH_SITE_VA; if (cur == 0xE9) { logf("v14: already applied"); return true; } if (!bytes_equal(ac::V14_PATCH_SITE_VA, ORIG, 18)) { char h[40]; hexdump_short(ac::V14_PATCH_SITE_VA, 18, h, sizeof(h)); logf("v14: site unexpected bytes %s — refusing", h); return false; } uintptr_t thunk_va = (uintptr_t)&v14_clipplane_cleanup_thunk; if (!write_jmp_rel32(ac::V14_PATCH_SITE_VA, thunk_va, 18)) return false; logf("v14: applied (JMP rel32 -> 0x%08x, thunk in leakfix.dll)", thunk_va); return true; } bool apply_all_patches() { bool ok = true; ok &= apply_v3b(); ok &= apply_v11(); ok &= apply_v5(); ok &= apply_v14(); logf("all-patches result: %s", ok ? "OK" : "PARTIAL"); return ok; } } // namespace leakfix