// Copyright (C) 2026 CoelhoFZ
// SPDX-License-Identifier: GPL-3.0-only
// This file is part of unlocker-CoelhoFZ (GPLv3, see LICENSE).

// stub_loader.cpp - layer 1 of the 2-stage packer (inside winmm.dll).
//
// Harden architecture (2 stages - the mapper does NOT exist readable
// anywhere):
//
//   [winmm.dll (plaintext)]  = winmm proxy + STUB (this file)
//   [BLOB1 (encrypted)]      = mapper: decrypts and maps the payload
//   [BLOB2 (encrypted)]      = payload: the shim (QueryApiImpl hook)
//
// winmm only contains: stub + generic loader (miniloader.h). No payload key,
// no name, no mechanism - the "secret sauce" (keys, payload CRC, internal
// overlay layout) lives INSIDE the encrypted BLOB1.
//
// Overlay at the end of winmm (fixed 12-byte trailer):
//   [BLOB2 payload][BLOB1 mapper][u32 size_blob2][u32 size_blob1][u32 magic]
//
// Stub defense layers:
//   1. light anti-debug (IsDebuggerPresent + PEB BeingDebugged) via apihash
//   2. self-check: CRC32 of the .text section of winmm itself (constant in
//      .rdata via volatile - 2-pass build). Patch on the stub = no unlock.
//   3. masked keys in .data (the key plaintext never appears)
//   4. ChaCha20 (RFC 8439) with key+nonce from masked PER-BUILD words
//      (without the key the BLOB does not open; the old LCG opened by the
//      known plaintext 'MZ')
//   5. trailer magic random PER BUILD and masked
//   6. staging buffers zeroed before free (anti-dump)
//   7. WATCHDOG: thread re-checks the .text periodically; if someone
//      patches winmm AFTER load, it flips the kill-switch (the hook stops
//      lying "owned" - game goes back to vanilla without crash).
//   8. ALL APIs via apihash (PEB walk + hash) - the winmm import table does
//      not list VirtualAlloc/ReadFile/etc. (kills the breakpoint on the API
//      that would see the decrypted blob).

#if defined(HARDENED)

#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <intrin.h>  // __readgsqword (PEB em x64)

#include <cstring>
#include <cstdint>

#include "stub_loader.h"
#include "mapper_params.h"
#include "miniloader.h"
#include "apihash.h"
#include "chacha20.h"   // ChaCha20 (RFC 8439)
#include "antidebug.h"  // F2: shared anti-debug probes (stub+mapper)
#include "stub_cfg.h"   // generated by tools/pack_payload.py gen-mapper (PER BUILD)

namespace {

// Single mask for ALL the secret constants (the masked value is what stays
// in the binary; the XOR happens at runtime and the result lives only in a
// register). The mask literal reveals nothing on its own.
constexpr uint32_t kXorMask = 0x9E3779B9u;

// Volatile globals -> the compiler CANNOT constant-fold (the masked value
// stays in .data; the runtime XOR produces the key only in a register).
volatile uint32_t g_keyMasked[8]   = { MAPPER_KEY_W0_MASKED, MAPPER_KEY_W1_MASKED,
                                       MAPPER_KEY_W2_MASKED, MAPPER_KEY_W3_MASKED,
                                       MAPPER_KEY_W4_MASKED, MAPPER_KEY_W5_MASKED,
                                       MAPPER_KEY_W6_MASKED, MAPPER_KEY_W7_MASKED };
volatile uint32_t g_nonceMasked[3] = { MAPPER_NONCE_W0_MASKED, MAPPER_NONCE_W1_MASKED,
                                       MAPPER_NONCE_W2_MASKED };
volatile uint32_t g_crcM   = MAPPER_CRC_MASKED;
volatile uint32_t g_magicM = TRAILER_MAGIC_MASKED;
volatile uint32_t g_selfM  = EXPECTED_CRC_MASKED;

inline uint32_t Unmask(volatile uint32_t& m) { return m ^ kXorMask; }
inline uint32_t Crc()   { return g_crcM ^ kXorMask; }
inline uint32_t Magic() { return g_magicM ^ kXorMask; }
inline uint32_t SelfCrcExpected() { return g_selfM ^ kXorMask; }

// ---- ChaCha20 (RFC 8439) with PER-BUILD key+nonce unmasked here.
// Without the key the blob does not open (the old LCG opened by the known
// PE plaintext 'MZ'). Contract identical to common/chacha20.h and the packer. ----
void DecryptBlob1(uint8_t* buf, uint32_t n) {
    uint32_t key[8];
    uint32_t nonce[3];
    for (int i = 0; i < 8; ++i) key[i] = Unmask(g_keyMasked[i]);
    for (int i = 0; i < 3; ++i) nonce[i] = Unmask(g_nonceMasked[i]);
    chacha20::Cipher(buf, n, key, 0, nonce);
}

// ---- anti-debug: 4 layers BEFORE any work (apihash, no import;   ----
// ---- tool names only as hash)                                    ----
bool StubDebuggerPresent() {
    if (apihash::IsDebuggerPresent()()) return true;
    const uintptr_t peb = __readgsqword(0x60);
    if (*reinterpret_cast<const uint8_t*>(peb + 0x02) != 0) return true;
    if (antidebug::NtDebugProbe()) return true;          // classic attach
    if (antidebug::HardwareBreakpointsProbe()) return true;  // DR0-DR3
    return false;
}

// ---- self-check: CRC32 of the .text section of the file itself (2-pass) ----
uint32_t FileTextCrc(const char* path) {
    HANDLE f = apihash::CreateFileA()(path, GENERIC_READ, FILE_SHARE_READ,
                                      nullptr, OPEN_EXISTING,
                                      FILE_ATTRIBUTE_NORMAL, nullptr);
    if (f == INVALID_HANDLE_VALUE) return 0;
    const DWORD sz = apihash::GetFileSize()(f, nullptr);
    if (sz == INVALID_FILE_SIZE || sz < 0x1000) {
        apihash::CloseHandle()(f);
        return 0;
    }
    uint8_t* buf = static_cast<uint8_t*>(apihash::VirtualAlloc()(
        nullptr, sz, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE));
    if (buf == nullptr) {
        apihash::CloseHandle()(f);
        return 0;
    }
    DWORD rd = 0;
    const BOOL ok = apihash::ReadFile()(f, buf, sz, &rd, nullptr) && rd == sz;
    apihash::CloseHandle()(f);
    if (!ok) {
        apihash::VirtualFree()(buf, 0, MEM_RELEASE);
        return 0;
    }
    const IMAGE_DOS_HEADER* dos = reinterpret_cast<const IMAGE_DOS_HEADER*>(buf);
    uint32_t crc = 0;
    if (dos->e_magic == IMAGE_DOS_SIGNATURE &&
        dos->e_lfanew + sizeof(IMAGE_NT_HEADERS) < sz) {
        const IMAGE_NT_HEADERS* nt =
            reinterpret_cast<const IMAGE_NT_HEADERS*>(buf + dos->e_lfanew);
        if (nt->Signature == IMAGE_NT_SIGNATURE &&
            nt->OptionalHeader.Magic == IMAGE_NT_OPTIONAL_HDR64_MAGIC) {
            const IMAGE_SECTION_HEADER* sec = IMAGE_FIRST_SECTION(nt);
            for (WORD i = 0; i < nt->FileHeader.NumberOfSections; ++i) {
                if ((sec[i].Characteristics & IMAGE_SCN_CNT_CODE) == 0) continue;
                const uint32_t off = sec[i].PointerToRawData;
                const uint32_t len = sec[i].SizeOfRawData;
                if (off + len <= sz && len != 0)
                    crc = miniloader::Crc32(buf + off, len);
                break;
            }
        }
    }
    apihash::VirtualFree()(buf, 0, MEM_RELEASE);
    return crc;
}
bool SelfIntegrityOk(const char* path) {
    const uint32_t expected = SelfCrcExpected();
    if (expected == 0) return false;  // pass 1 placeholder (never deployed)
    return FileTextCrc(path) == expected;
}

// ---- watchdog kill-switch (the payload reads it via MapperParams) ----
volatile LONG g_kill = 0;
// Path of winmm itself (validated in ReadBlob1; used by the watchdog).
char g_winmmPath[MAX_PATH] = {};

// ---- F2 anti-tamper in MEMORY (the watchdog re-CRCs the loaded .text, not
//      the file): catches patch/NOP/0xCC done on the stub or the payload
//      AFTER load - including against whoever "fixes" the stub checks live. ----
uint32_t MemTextCrc(uintptr_t base) {
    if (base == 0) return 0;
    const IMAGE_DOS_HEADER* dos =
        reinterpret_cast<const IMAGE_DOS_HEADER*>(base);
    if (dos->e_magic != IMAGE_DOS_SIGNATURE) return 0;
    const IMAGE_NT_HEADERS* nt = reinterpret_cast<const IMAGE_NT_HEADERS*>(
        base + dos->e_lfanew);
    if (nt->Signature != IMAGE_NT_SIGNATURE) return 0;
    const IMAGE_SECTION_HEADER* sec = IMAGE_FIRST_SECTION(nt);
    for (WORD i = 0; i < nt->FileHeader.NumberOfSections; ++i) {
        if ((sec[i].Characteristics & IMAGE_SCN_CNT_CODE) == 0) continue;
        const uint32_t va = sec[i].VirtualAddress;
        const uint32_t raw = sec[i].SizeOfRawData;
        const uint32_t vsz = sec[i].Misc.VirtualSize;
        const uint32_t len = (vsz != 0 && vsz < raw) ? vsz : raw;
        if (len == 0) break;
        return miniloader::Crc32(reinterpret_cast<const uint8_t*>(base + va),
                                 len);
    }
    return 0;
}
volatile uintptr_t g_selfBase = 0;      // winmm base (stub)
volatile uint32_t g_stubTextSnap = 0;   // stub .text CRC right after load
volatile uintptr_t g_payloadBase = 0;   // mapped payload base (from the mapper)
volatile uint32_t g_payTextSnap = 0;    // payload .text CRC post-load

// ---- reads its own file and extracts BLOB1 from the 12-byte trailer ----
uint8_t* ReadBlob1(HMODULE self, MapperParams* params, uint32_t* out_size1) {
    char path[MAX_PATH];
    if (apihash::GetModuleFileNameA()(self, path, MAX_PATH) == 0)
        return nullptr;
    std::memcpy(params->winmm_path, path, MAX_PATH);
    if (!SelfIntegrityOk(path)) return nullptr;

    HANDLE f = apihash::CreateFileA()(path, GENERIC_READ, FILE_SHARE_READ,
                                      nullptr, OPEN_EXISTING,
                                      FILE_ATTRIBUTE_NORMAL, nullptr);
    if (f == INVALID_HANDLE_VALUE) return nullptr;
    const DWORD file_size = apihash::GetFileSize()(f, nullptr);
    if (file_size <= 12) {
        apihash::CloseHandle()(f);
        return nullptr;
    }

    uint8_t tail[12];
    DWORD rd = 0;
    apihash::SetFilePointer()(f, static_cast<LONG>(file_size) - 12, nullptr,
                              FILE_BEGIN);
    if (!apihash::ReadFile()(f, tail, 12, &rd, nullptr) || rd != 12) {
        apihash::CloseHandle()(f);
        return nullptr;
    }
    uint32_t size2 = 0, size1 = 0, magic = 0;
    std::memcpy(&size2, tail, 4);
    std::memcpy(&size1, tail + 4, 4);
    std::memcpy(&magic, tail + 8, 4);
    if (magic != Magic() || size1 == 0 || size2 == 0 ||
        size1 + size2 + 12 > file_size) {
        apihash::CloseHandle()(f);
        return nullptr;
    }

    const DWORD blob1_off = file_size - 12 - size1;
    uint8_t* blob1 = static_cast<uint8_t*>(apihash::VirtualAlloc()(
        nullptr, size1, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE));
    if (blob1 == nullptr) {
        apihash::CloseHandle()(f);
        return nullptr;
    }
    apihash::SetFilePointer()(f, static_cast<LONG>(blob1_off), nullptr,
                              FILE_BEGIN);
    if (!apihash::ReadFile()(f, blob1, size1, &rd, nullptr) || rd != size1) {
        apihash::VirtualFree()(blob1, 0, MEM_RELEASE);
        apihash::CloseHandle()(f);
        return nullptr;
    }
    apihash::CloseHandle()(f);

    params->size1 = size1;
    params->size2 = size2;
    params->magic = magic;
    *out_size1 = size1;
    std::memcpy(g_winmmPath, path, MAX_PATH);  // for the watchdog
    return blob1;
}

// Watchdog (F2): at NON-deterministic intervals (6-20s, rdtsc seed) it
// re-checks IN MEMORY the .text of the stub AND of the payload (CRC vs. load
// snapshot) and the file on disk. If someone patches either one AFTER load
// (NOP the checks, breakpoint 0xCC, remove self-CRC, ...), it flips the
// kill-switch -> the payload hook becomes passthrough (vanilla game, no
// crash). A later debugger attach (live dump) also kills it.
DWORD WINAPI WatchdogMain(void*) {
    uint32_t rnd = static_cast<uint32_t>(antidebug::Tsc()) ^ 0x9E3779B9u;
    for (;;) {
        rnd = rnd * 1664525u + 1013904223u;  // LCG for interval jitter
        const uint32_t delay = 6000u + (rnd % 14000u);
        apihash::Sleep()(delay);
        if (g_stubTextSnap != 0 &&
            MemTextCrc(g_selfBase) != g_stubTextSnap) {
            InterlockedExchange(&g_kill, 1);
            return 0;
        }
        if (g_payTextSnap != 0 &&
            MemTextCrc(g_payloadBase) != g_payTextSnap) {
            InterlockedExchange(&g_kill, 1);
            return 0;
        }
        if (antidebug::NtDebugProbe()) {  // attach after load
            InterlockedExchange(&g_kill, 1);
            return 0;
        }
        if (!SelfIntegrityOk(g_winmmPath)) {
            InterlockedExchange(&g_kill, 1);
            return 0;
        }
    }
}

}  // namespace

bool StubLoadAndRunPayload(HINSTANCE self) {
    if (StubDebuggerPresent()) return false;  // under a debugger: silent
    // F2: analysis tools open (x64dbg/CE/IDA/windbg) = silent.
    if (antidebug::AnalysisToolRunning()) return false;

    MapperParams params = {};
    uint32_t size1 = 0;
    uint8_t* blob1 = ReadBlob1(self, &params, &size1);
    if (blob1 == nullptr) return false;

    // F2: timing around the decryption (single-step blows the budget).
    params.kill_flag = &g_kill;
    const uint64_t t0 = antidebug::Tsc();
    DecryptBlob1(blob1, size1);
    if (antidebug::RegionTooSlow(t0, antidebug::kSlowBudgetCycles)) {
        std::memset(blob1, 0, size1);
        apihash::VirtualFree()(blob1, 0, MEM_RELEASE);
        return false;  // stepping suspicion: silent
    }
    // The mapper (BLOB1) runs inside here: decrypts BLOB2, maps the payload
    // and installs the hook. lpReserved = &params (the blob CRT passes it
    // through). If the mapper detects something (2nd layer) it returns FALSE ->
    // MiniLoadAndRun returns nullptr -> silent failure here.
    uint8_t* mapped = miniloader::MiniLoadAndRun(blob1, size1, Crc(), &params);
    // anti-dump: zeroes the decrypted staging before freeing.
    std::memset(blob1, 0, size1);
    apihash::VirtualFree()(blob1, 0, MEM_RELEASE);
    if (mapped == nullptr) return false;

    // ---- F2 anti-dump: the mapper (mapped image) carries the payload keys
    // IN THE CLEAR in .rdata/.text. After its DllMain ran, the mapper image is
    // dead code -> free it. Only the payload remains in memory (mandatory: it
    // is what runs the hooks) + the stub.
    g_payloadBase = reinterpret_cast<uintptr_t>(params.mapped_payload);
    apihash::VirtualFree()(mapped, 0, MEM_RELEASE);

    // Snapshots for the watchdog (CRC of the .text IN MEMORY, post-load).
    g_selfBase = reinterpret_cast<uintptr_t>(self);
    g_stubTextSnap = MemTextCrc(g_selfBase);
    g_payTextSnap = MemTextCrc(g_payloadBase);

    // Post-load anti-tamper watchdog (light thread, dies with the process).
    HANDLE wd = apihash::CreateThread()(nullptr, 0, &WatchdogMain, nullptr, 0,
                                        nullptr);
    if (wd != nullptr) apihash::CloseHandle()(wd);
    return true;
}

#endif  // HARDENED
