Skip to content

Commit

Permalink
GamePass (RE2): Enigma workaround + fixes for new build
Browse files Browse the repository at this point in the history
  • Loading branch information
praydog committed Jan 16, 2024
1 parent 0e70316 commit 1ed9f31
Show file tree
Hide file tree
Showing 12 changed files with 247 additions and 7 deletions.
18 changes: 18 additions & 0 deletions shared/sdk/REContext.cpp
Expand Up @@ -168,8 +168,26 @@ namespace sdk {
std::vector<std::string> invoke_patterns {
"40 53 48 83 ec 20 48 8b 41 30 4c 8b d2 48 8b 51 40 48 8b d9 4c 8b 00 48 8b 41 10", // RE2 - MHRise v1
"40 53 48 83 ec 20 48 8b 41 10 48 8b da 8b 48 08", // MHRise Sunbreak/newer games?
"40 53 48 83 EC ? 48 8B 41 30 4C 8B D2 4C 8B 49 10 48 8B D9 48 8B 51 40 49 8B CA 4C 8B 00 41 FF" // seen in game pass RE2
};

// ok so if these patterns above are failing, we can find the invoke table by looking for these set of instructions:
// 8D 56 FF lea edx, [rsi-1]
// 48 8B CF mov rcx, rdi
// E8 67 FD 6C 01 call sub_[removed]

// Then, scroll up from here, and you'll see something that looks like this:
/*
E8 D4 D6 6C 01 call sub_[removed]
41 B8 53 15 00 00 mov r8d, 1553h ; dead giveaway is a number like this that is the size of the invoke table
48 8D 15 37 C7 6B 06 lea rdx, g_invokeTbl ; this is what we want
48 8B 08 mov rcx, [rax]
48 8B 05 2D 47 86 06 mov rax, cs:off_[removed]
48 89 08 mov [rax], rcx
48 8B CF mov rcx, rdi
*/


std::optional<uintptr_t> method_inside_invoke_tbl{std::nullopt};

for (const auto& pat : invoke_patterns) {
Expand Down
3 changes: 2 additions & 1 deletion shared/sdk/REGlobals.cpp
Expand Up @@ -70,7 +70,8 @@ REGlobals::REGlobals() {

if (name.find(game_namespace("SingletonBehavior`1")) != std::string::npos ||
name.find(game_namespace("SingletonBehaviorRoot`1")) != std::string::npos ||
name.find(game_namespace("SnowSingletonBehaviorRoot`1")) != std::string::npos)
name.find(game_namespace("SnowSingletonBehaviorRoot`1")) != std::string::npos ||
name.find(game_namespace("RopewaySingletonBehaviorRoot`1")) != std::string::npos)
{
const auto type_definition = utility::re_type::get_type_definition(t);

Expand Down
1 change: 1 addition & 0 deletions src/Mod.hpp
Expand Up @@ -362,6 +362,7 @@ class Mod {
// Called when REFramework::initialize finishes in the first render frame
// Returns an error string if it fails
virtual std::optional<std::string> on_initialize() { return std::nullopt; };
virtual std::optional<std::string> on_initialize_d3d_thread() { return std::nullopt; };
virtual void on_lua_state_created(sol::state& lua) {};
virtual void on_lua_state_destroyed(sol::state& lua) {};

Expand Down
29 changes: 29 additions & 0 deletions src/Mods.cpp
Expand Up @@ -90,6 +90,35 @@ std::optional<std::string> Mods::on_initialize() const {
return std::nullopt;
}


std::optional<std::string> Mods::on_initialize_d3d_thread() const {
std::scoped_lock _{g_framework->get_hook_monitor_mutex()};

utility::Config cfg{ (REFramework::get_persistent_dir() / "re2_fw_config.txt").string() };

// once here to at least setup the values
for (auto& mod : m_mods) {
spdlog::info("{:s}::on_config_load()", mod->get_name().data());
mod->on_config_load(cfg);
}

for (auto& mod : m_mods) {
spdlog::info("{:s}::on_initialize_d3d_thread()", mod->get_name().data());

if (auto e = mod->on_initialize_d3d_thread(); e != std::nullopt) {
spdlog::info("{:s}::on_initialize_d3d_thread() has failed: {:s}", mod->get_name().data(), *e);
return e;
}
}

for (auto& mod : m_mods) {
spdlog::info("{:s}::on_config_load()", mod->get_name().data());
mod->on_config_load(cfg);
}

return std::nullopt;
}

void Mods::on_pre_imgui_frame() const {
for (auto& mod : m_mods) {
mod->on_pre_imgui_frame();
Expand Down
1 change: 1 addition & 0 deletions src/Mods.hpp
Expand Up @@ -8,6 +8,7 @@ class Mods {
virtual ~Mods() {}

std::optional<std::string> on_initialize() const;
std::optional<std::string> on_initialize_d3d_thread() const;

void on_pre_imgui_frame() const;
void on_frame() const;
Expand Down
47 changes: 45 additions & 2 deletions src/REFramework.cpp
Expand Up @@ -168,6 +168,8 @@ REFramework::REFramework(HMODULE reframework_module)
const auto pre_allocated_buffer = (uintptr_t)AllocateBuffer((LPVOID)halfway_module); // minhook function
spdlog::info("Preallocated buffer: {:x}", pre_allocated_buffer);

IntegrityCheckBypass::fix_virtual_protect();

IMGUI_CHECKVERSION();
ImGui::CreateContext();

Expand Down Expand Up @@ -503,7 +505,7 @@ void REFramework::on_frame_d3d11() {
return;
}

const bool is_init_ok = m_error.empty() && m_game_data_initialized;
bool is_init_ok = m_error.empty() && m_game_data_initialized;

if (is_init_ok) {
// Write default config once if it doesn't exist.
Expand All @@ -514,6 +516,8 @@ void REFramework::on_frame_d3d11() {
}
}

is_init_ok = first_frame_initialize();

if (!m_has_frame) {
if (!is_init_ok) {
update_fonts();
Expand Down Expand Up @@ -606,7 +610,7 @@ void REFramework::on_frame_d3d12() {
return;
}

const bool is_init_ok = m_error.empty() && m_game_data_initialized;
bool is_init_ok = m_error.empty() && m_game_data_initialized;

if (is_init_ok) {
// Write default config once if it doesn't exist.
Expand All @@ -629,6 +633,8 @@ void REFramework::on_frame_d3d12() {
ImGui_ImplDX12_NewFrame();
};

is_init_ok = first_frame_initialize();

if (!m_has_frame) {
if (!is_init_ok) {
update_fonts();
Expand Down Expand Up @@ -1632,6 +1638,43 @@ bool REFramework::initialize_windows_message_hook() {
return false;
}

// Ran on the first valid frame after pre-initialization of mods has taken place and hasn't failed
// This one allows mods to run any initialization code in the context of the D3D thread (like VR code)
// It also is the one that actually loads any config files
bool REFramework::first_frame_initialize() {
const bool is_init_ok = m_error.empty() && m_game_data_initialized;

if (!is_init_ok || !m_first_frame_d3d_initialize) {
return is_init_ok;
}

std::scoped_lock _{get_hook_monitor_mutex()};

spdlog::info("Running first frame D3D initialization of mods...");

m_first_frame_d3d_initialize = false;
auto e = m_mods->on_initialize_d3d_thread();

if (e) {
if (e->empty()) {
m_error = "An unknown error has occurred.";
} else {
m_error = *e;
}

spdlog::error("Initialization of mods failed. Reason: {}", m_error);
m_game_data_initialized = false;
m_mods_fully_initialized = false;
return false;
} else {
// Do an initial config save to set the default values for the frontend
save_config();
m_mods_fully_initialized = true;
}

return true;
}

void REFramework::call_on_frame() {
const bool is_init_ok = m_error.empty() && m_game_data_initialized;

Expand Down
4 changes: 4 additions & 0 deletions src/REFramework.hpp
Expand Up @@ -147,11 +147,14 @@ class REFramework {
bool initialize_game_data();
bool initialize_windows_message_hook();

bool first_frame_initialize();

void call_on_frame();

HMODULE m_reframework_module{};

bool m_first_frame{true};
bool m_first_frame_d3d_initialize{true};
bool m_is_d3d12{false};
bool m_is_d3d11{false};
bool m_valid{false};
Expand All @@ -160,6 +163,7 @@ class REFramework {
bool m_started_game_data_thread{false};
std::atomic<bool> m_terminating{false}; // Destructor is called
std::atomic<bool> m_game_data_initialized{false};
std::atomic<bool> m_mods_fully_initialized{false};

// UI
bool m_has_frame{false};
Expand Down
4 changes: 3 additions & 1 deletion src/mods/Hooks.cpp
Expand Up @@ -196,7 +196,9 @@ std::optional<std::string> Hooks::hook_update_transform() {
m_update_transform_hook = std::make_unique<FunctionHook>(update_transform, &update_transform_hook);

if (!m_update_transform_hook->create()) {
return "Failed to hook UpdateTransform";
//return "Failed to hook UpdateTransform";
spdlog::error("Failed to hook UpdateTransform");
return std::nullopt; // who cares
}

return std::nullopt;
Expand Down
125 changes: 125 additions & 0 deletions src/mods/IntegrityCheckBypass.cpp
Expand Up @@ -514,4 +514,129 @@ void IntegrityCheckBypass::remove_stack_destroyer() {
static auto patch = Patch::create(*fn, { 0xC3 }, true);

spdlog::info("[IntegrityCheckBypass]: Patched stack destroyer!");
}

// hahahah i hate this
void IntegrityCheckBypass::fix_virtual_protect() try {
spdlog::info("[IntegrityCheckBypass]: Fixing VirtualProtect...");

auto nt_protect_virtual_memory = (NtProtectVirtualMemory_t)GetProcAddress(GetModuleHandleA("ntdll.dll"), "NtProtectVirtualMemory");
if (nt_protect_virtual_memory == nullptr) {
spdlog::error("[IntegrityCheckBypass]: Could not find NtProtectVirtualMemory!");
return;
}

spdlog::info("[IntegrityCheckBypass]: Found NtProtectVirtualMemory at 0x{:X}", (uintptr_t)nt_protect_virtual_memory);

if (*(uint8_t*)nt_protect_virtual_memory == 0xE9) {
spdlog::info("[IntegrityCheckBypass]: Found jmp at 0x{:X}, resolving...", (uintptr_t)nt_protect_virtual_memory);
nt_protect_virtual_memory = (decltype(nt_protect_virtual_memory))utility::calculate_absolute((uintptr_t)nt_protect_virtual_memory + 1);
}

s_pristine_protect_virtual_memory = (decltype(s_pristine_protect_virtual_memory))VirtualAlloc(nullptr, 256, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
memcpy(s_pristine_protect_virtual_memory, nt_protect_virtual_memory, 256);
spdlog::info("[IntegrityCheckBypass]: Copied NtProtectVirtualMemory to 0x{:X}", (uintptr_t)s_pristine_protect_virtual_memory);

// Now disassemble our pristine function and just remove anything that looks like its a displacement or memory reference with nops
// im doing this because im too lazy to fix up the relocations
struct PatchJob {
uintptr_t addr{};
uint32_t size{};
};

std::vector<PatchJob> patch_jobs{};

utility::exhaustive_decode((uint8_t*)s_pristine_protect_virtual_memory, 100, [&](utility::ExhaustionContext& ctx) -> utility::ExhaustionResult {
if (*(uint8_t*)ctx.addr == 0x0f && *(uint8_t*)(ctx.addr + 1) == 0x05) {
spdlog::info("[IntegrityCheckBypass]: Found syscall at 0x{:X}", ctx.addr);
return utility::ExhaustionResult::CONTINUE;
}

if (std::string_view{ctx.instrux.Mnemonic}.starts_with("RET") || std::string_view{ctx.instrux.Mnemonic}.starts_with("INT")) {
return utility::ExhaustionResult::CONTINUE;
}

if (ctx.instrux.BranchInfo.IsBranch) {
spdlog::info("[IntegrityCheckBypass]: Found branch at 0x{:X} ({:x})", ctx.addr, ctx.addr - (uintptr_t)s_pristine_protect_virtual_memory);
spdlog::info("[IntegrityCheckBypass]: {}", ctx.instrux.Mnemonic);
patch_jobs.push_back(PatchJob{ctx.addr, ctx.instrux.Length});
return utility::ExhaustionResult::CONTINUE;
}

if (utility::resolve_displacement(ctx.addr).has_value()) {
spdlog::info("[IntegrityCheckBypass]: Found displacement at 0x{:X} ({:x})", ctx.addr, ctx.addr - (uintptr_t)s_pristine_protect_virtual_memory);
patch_jobs.push_back(PatchJob{ctx.addr, ctx.instrux.Length});
return utility::ExhaustionResult::CONTINUE;
}

// Go through any operands and check if any mem
for (auto i = 0; i < ctx.instrux.OperandsCount; ++i) {
const auto& op = ctx.instrux.Operands[i];

if (op.Type == ND_OP_MEM) {
spdlog::info("[IntegrityCheckBypass]: Found mem operand at 0x{:X} ({:x})", ctx.addr, ctx.addr - (uintptr_t)s_pristine_protect_virtual_memory);
patch_jobs.push_back(PatchJob{ctx.addr, ctx.instrux.Length});
return utility::ExhaustionResult::CONTINUE;
}
}

return utility::ExhaustionResult::CONTINUE;
});

for (auto& patch_job : patch_jobs) {
spdlog::info("[IntegrityCheckBypass]: Patching 0x{:X} with {} nops", patch_job.addr, patch_job.size);
memset((void*)patch_job.addr, 0x90, patch_job.size);
}

// Hook VirtualProtect
s_virtual_protect_hook = std::make_unique<FunctionHook>(VirtualProtect, (uintptr_t)virtual_protect_hook);
if (!s_virtual_protect_hook->create()) {
spdlog::error("[IntegrityCheckBypass]: Could not hook VirtualProtect!");
return;
}

spdlog::info("[IntegrityCheckBypass]: Hooked VirtualProtect!");
} catch(...) {
spdlog::error("[IntegrityCheckBypass]: Could not fix VirtualProtect!");
}

// This allows our calls to VirtualProtect to go through without being hindered by... something.
BOOL WINAPI IntegrityCheckBypass::virtual_protect_hook(LPVOID lpAddress, SIZE_T dwSize, DWORD flNewProtect, PDWORD lpflOldProtect) try {
static bool once = true;
if (once) {
spdlog::info("[IntegrityCheckBypass]: VirtualProtect called");
once = false;
}

static const auto this_process = GetCurrentProcess();

LPVOID address_to_protect = lpAddress;
NTSTATUS result = s_pristine_protect_virtual_memory(this_process, (PVOID*)&address_to_protect, &dwSize, flNewProtect, lpflOldProtect);

constexpr NTSTATUS STATUS_INVALID_PAGE_PROTECTION = 0xC0000045;

// recreated from kernelbase to be correct
if (result == STATUS_INVALID_PAGE_PROTECTION) {
using RtlFlushSecureMemoryCache_t = BOOLEAN (NTAPI*)(PVOID, SIZE_T);
static const auto rtl_flush_secure_memory_cache = (RtlFlushSecureMemoryCache_t)GetProcAddress(GetModuleHandleA("ntdll.dll"), "RtlFlushSecureMemoryCache");

if (rtl_flush_secure_memory_cache != nullptr) {
if (NT_SUCCESS(rtl_flush_secure_memory_cache(address_to_protect, dwSize))) {
result = s_pristine_protect_virtual_memory(this_process, (PVOID*)&address_to_protect, &dwSize, flNewProtect, lpflOldProtect);

if ((result & 0x80000000) == 0) {
return TRUE;
}
}
}
}

if (!NT_SUCCESS(result)) {
spdlog::error("[IntegrityCheckBypass]: NtProtectVirtualMemory(-1, {:x}, {:x}, {:x}, {:x}) failed with {:x}", (uintptr_t)address_to_protect, dwSize, flNewProtect, (uintptr_t)lpflOldProtect, (uint32_t)result);
}

return NT_SUCCESS(result);
} catch(...) {
spdlog::error("[IntegrityCheckBypass]: VirtualProtect hook failed! falling back to original");
return s_virtual_protect_hook->get_original<decltype(virtual_protect_hook)>()(lpAddress, dwSize, flNewProtect, lpflOldProtect);
}
8 changes: 8 additions & 0 deletions src/mods/IntegrityCheckBypass.hpp
Expand Up @@ -5,6 +5,7 @@

#include "Mod.hpp"
#include "utility/Patch.hpp"
#include "utility/FunctionHook.hpp"

// Always on for RE3
// Because we use hooks that modify the integrity of the executable
Expand All @@ -20,8 +21,15 @@ class IntegrityCheckBypass : public Mod {
static void immediate_patch_re8();
static void immediate_patch_re4();
static void remove_stack_destroyer();
static void fix_virtual_protect();

private:
static BOOL WINAPI virtual_protect_hook(LPVOID lpAddress, SIZE_T dwSize, DWORD flNewProtect, PDWORD lpflOldProtect);

using NtProtectVirtualMemory_t = NTSTATUS(NTAPI*)(HANDLE ProcessHandle, PVOID* BaseAddress, SIZE_T* NumberOfBytesToProtect, ULONG NewAccessProtection, PULONG OldAccessProtection);
static inline NtProtectVirtualMemory_t s_pristine_protect_virtual_memory{ nullptr };
static inline std::unique_ptr<FunctionHook> s_virtual_protect_hook{};

#ifdef RE3
// This is what the game uses to bypass its integrity checks altogether or something
bool* m_bypass_integrity_checks{ nullptr };
Expand Down

0 comments on commit 1ed9f31

Please sign in to comment.