#include "native_patch_support.h" #include "Engine/Game/Build/supported_build.h" #include "Engine/Game/Build/addresses.h" #include "Engine/Game/Build/20260929/native_patch_support.h" #include #include "Engine/Core/Hooks/hooks.h" #include "Engine/Core/Log/logging.h" #include #include #include #include #include #include namespace dingosdk { namespace { using EnvelopeVerifier = std::uint8_t* (*)(std::uint8_t*, std::uint64_t*); using SignatureVerifier = decltype(&BCryptVerifySignature); std::atomic original_verifier{}; std::atomic original_signature_verifier{}; std::atomic signature_return_address{}; std::uintptr_t installed_base{}; std::mutex install_mutex; namespace patch = addr::native_patch_support; constexpr std::uintptr_t verifier_rva = patch::verifier; constexpr std::uintptr_t signature_import_rva = patch::signature_import; constexpr std::uintptr_t signature_return_rva = patch::signature_return; constexpr NTSTATUS invalid_signature = static_cast(0xc000a000U); struct DataVerification { const std::uint8_t* signature{}; std::uint64_t payload_size{}; }; thread_local DataVerification active_verification; class DataVerificationScope { public: DataVerificationScope(const std::uint8_t* raw, std::uint64_t length) noexcept : previous_(active_verification) { active_verification = raw ? DataVerification{raw + 8, length - native_patch_envelope_size} : DataVerification{}; } ~DataVerificationScope() { active_verification = previous_; } DataVerificationScope(const DataVerificationScope&) = delete; DataVerificationScope& operator=(const DataVerificationScope&) = delete; private: DataVerification previous_; }; // Independent implementation of edited-container signature acceptance, inspired // by InitfsTools' BCrypt proxy. This hook retains the actual CNG result outside // the inspected data verifier; no certificate-query or termination API changes. __declspec(noinline) NTSTATUS WINAPI verify_data_signature(BCRYPT_KEY_HANDLE key, void* padding, PUCHAR hash, ULONG hash_size, PUCHAR signature, ULONG signature_size, ULONG flags) { const auto caller = reinterpret_cast(_ReturnAddress()); const auto original = original_signature_verifier.load(std::memory_order_acquire); const auto result = original(key, padding, hash, hash_size, signature, signature_size, flags); if (result != invalid_signature || caller != signature_return_address.load(std::memory_order_acquire) || !active_verification.signature || signature != active_verification.signature || !key || !padding || !hash || hash_size != 20 || signature_size != 256 || flags != BCRYPT_PAD_PKCS1) return result; logging::log(logging::Level::info, logging::Channel::assets, "Edited InitFS/package data accepted after signature mismatch ({} payload bytes).", active_verification.payload_size); return 0; } bool read_memory(const void* source, void* destination, std::size_t size) noexcept { SIZE_T read{}; return ReadProcessMemory(GetCurrentProcess(), source, destination, size, &read) && read == size; } bool validate_contract(std::uintptr_t base) noexcept { if (!base || base > std::numeric_limits::max() - supported_build::game_image_size) return false; IMAGE_DOS_HEADER dos{}; IMAGE_NT_HEADERS64 nt{}; if (!read_memory(reinterpret_cast(base), &dos, sizeof(dos)) || dos.e_magic != IMAGE_DOS_SIGNATURE || dos.e_lfanew < static_cast(sizeof(dos)) || dos.e_lfanew > 0x1000 || !read_memory(reinterpret_cast(base + dos.e_lfanew), &nt, sizeof(nt)) || nt.Signature != IMAGE_NT_SIGNATURE || nt.FileHeader.Machine != IMAGE_FILE_MACHINE_AMD64 || nt.OptionalHeader.Magic != IMAGE_NT_OPTIONAL_HDR64_MAGIC || nt.OptionalHeader.SizeOfImage != supported_build::game_image_size) return false; for (const auto& contract : patch::contracts) { std::array actual{}; if (!read_memory(reinterpret_cast(base + contract.rva), actual.data(), contract.bytes.size()) || std::memcmp(actual.data(), contract.bytes.data(), contract.bytes.size()) != 0) return false; } return true; } bool writable_length(std::uint64_t* length) noexcept { const auto address = reinterpret_cast(length); if (!address || address % alignof(std::uint64_t) != 0) return false; MEMORY_BASIC_INFORMATION page{}; if (!VirtualQuery(length, &page, sizeof(page)) || page.State != MEM_COMMIT || (page.Protect & (PAGE_GUARD | PAGE_NOACCESS)) != 0) return false; const auto protection = page.Protect & 0xff; const auto offset = address - reinterpret_cast(page.BaseAddress); return offset <= page.RegionSize && sizeof(*length) <= page.RegionSize - offset && (protection == PAGE_READWRITE || protection == PAGE_WRITECOPY || protection == PAGE_EXECUTE_READWRITE || protection == PAGE_EXECUTE_WRITECOPY); } // An aligned compare/exchange commits the single out-parameter only if it has // not changed since classification. SEH handles a concurrent protection change; // no partial write, no shortened-length fallback, and no source-buffer writes. bool commit_length(std::uint64_t* length, std::uint64_t expected) noexcept { __try { return static_cast(InterlockedCompareExchange64( reinterpret_cast(length), static_cast(expected - native_patch_envelope_size), static_cast(expected))) == expected; } __except (EXCEPTION_EXECUTE_HANDLER) { return false; } } std::uint8_t* verify_envelope(std::uint8_t* raw, std::uint64_t* length) { const auto incoming_error = GetLastError(); std::uint64_t count{}; std::array header{}; const auto address = reinterpret_cast(raw); const auto length_address = reinterpret_cast(length); const bool native_envelope = read_memory(length, &count, sizeof(count)) && count > native_patch_envelope_size && count <= std::numeric_limits::max() - address && // Native callers use a separate stack local; never change envelope/body // bytes through an aliased length pointer, even for otherwise valid data. (length_address >= address + count || (length_address < address && address - length_address >= sizeof(count))) && read_memory(raw, header.data(), header.size()) && is_native_data_envelope(header, count, address); if (native_envelope && is_unsigned_native_patch_envelope(header, count, address) && writable_length(length) && commit_length(length, count)) { SetLastError(incoming_error); return reinterpret_cast(address + native_patch_envelope_size); } const auto original = original_verifier.load(std::memory_order_acquire); // Tie acceptance to this particular signature buffer and this thread, even // when the verifier re-enters or other engine work invokes CNG concurrently. const DataVerificationScope scope(native_envelope ? raw : nullptr, count); SetLastError(incoming_error); return original(raw, length); } } // namespace bool start_native_patch_support(std::uintptr_t base, std::string& error) { std::lock_guard lock(install_mutex); error.clear(); if (installed_base) { if (installed_base == base) return true; error = "Native patch support is already installed on another image"; return false; } if (!validate_contract(base)) { error = "Native package-envelope contract does not match the supported game"; return false; } // bcrypt.h does not mark this declaration dllimport in this build. Taking // &BCryptVerifySignature therefore produces our linker jump thunk, whereas // Skate's IAT contains the resolved Windows export. Compare resolved exports // so a normal loader binding does not fail this contract check. const auto bcrypt_module = GetModuleHandleW(L"bcrypt.dll"); const auto signature_export = bcrypt_module ? reinterpret_cast( GetProcAddress(bcrypt_module, "BCryptVerifySignature")) : nullptr; if (!signature_export) { error = "Cannot resolve the loaded BCryptVerifySignature export"; return false; } SignatureVerifier signature_import{}; if (!read_memory(reinterpret_cast(base + signature_import_rva), &signature_import, sizeof(signature_import)) || signature_import != signature_export) { error = "Native data signature import does not match BCryptVerifySignature"; return false; } auto* target = reinterpret_cast(base + verifier_rva); auto* signature_target = reinterpret_cast(signature_import); EnvelopeVerifier original{}; const auto created = hook_prepare(target, reinterpret_cast(&verify_envelope), reinterpret_cast(&original)); if (created != HookOk) { error = "Cannot create native patch support (Detours hook service status " + std::to_string(created) + ")"; return false; } if (!original) { hook_remove(target); error = "Native patch verifier trampoline is unavailable"; return false; } SignatureVerifier original_signature{}; const auto signature_created = hook_prepare(signature_target, reinterpret_cast(&verify_data_signature), reinterpret_cast(&original_signature)); if (signature_created != HookOk || !original_signature) { if (signature_created == HookOk) hook_remove(signature_target); hook_remove(target); error = "Cannot create native data signature support (Detours hook service status " + std::to_string(signature_created) + ")"; return false; } original_verifier.store(original, std::memory_order_release); original_signature_verifier.store(original_signature, std::memory_order_release); signature_return_address.store(base + signature_return_rva, std::memory_order_release); // Both hooks become visible in one Detours transaction before game startup. auto enabled = hook_queue_enable(target); if (enabled == HookOk) enabled = hook_queue_enable(signature_target); if (enabled == HookOk) enabled = hook_apply_queued(); if (enabled != HookOk) { hook_remove(signature_target); hook_remove(target); signature_return_address.store(0, std::memory_order_release); original_signature_verifier.store(nullptr, std::memory_order_release); original_verifier.store(nullptr, std::memory_order_release); error = "Cannot enable native InitFS/package support (Detours hook service status " + std::to_string(enabled) + ")"; return false; } installed_base = base; return true; } } // namespace dingosdk