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

// mapper_dllmain.cpp - layer 2 of the packer (BLOB1 itself, encrypted).
//
// This file is compiled as a separate PE (build/mapper.dll) and then
// encrypted by pack_payload.py (gen-mapper) -> BLOB1, embedded in winmm.
// At runtime the stub (layer 1) decrypts and maps it; the DllMain below runs
// in anonymous memory outside the loader.
//
// The mapper is the ONLY place that knows the payload secrets:
//   * PAYLOAD_KEY_W0..W7 + PAYLOAD_NONCE_W0..W2 (PER-BUILD ChaCha20 of BLOB2)
//   * PAYLOAD_CRC (payload anti-tamper)
// Since the mapper itself is encrypted, those values stay invisible on disk
// (winmm.dll contains none of them).
//
// Flow: reads winmm.dll (path received via MapperParams) -> trailer ->
// BLOB2 -> decrypts with the payload key -> miniloader maps and runs the
// payload DllMain (installs the hook) -> zeroes the staging (anti-dump).
// Passes the watchdog kill-switch (stub) to the payload via lpReserved.
//
// APIs via apihash (no import table of its own - only msvcrt at runtime).

#if defined(HARDENED)

#define WIN32_LEAN_AND_MEAN
#include <windows.h>

#include <cstring>
#include <cstdint>

#include "miniloader.h"
#include "mapper_params.h"
#include "apihash.h"
#include "chacha20.h"    // ChaCha20 (RFC 8439)
#include "antidebug.h"   // F2: anti-debug probes (2nd layer, after the stub)
#include "payload_key.h" // generated by tools/pack_payload.py gen (PER-BUILD key)

namespace {

// Decrypts BLOB2 with the SAME ChaCha20 as pack_payload.py (RFC 8439). The
// key/nonce words can stay literal here (the whole mapper is
// encrypted as BLOB1 - winmm.dll never contains them).
void DecryptBlob2(uint8_t* buf, uint32_t n) {
    const uint32_t key[8] = { PAYLOAD_KEY_W0, PAYLOAD_KEY_W1, PAYLOAD_KEY_W2,
                              PAYLOAD_KEY_W3, PAYLOAD_KEY_W4, PAYLOAD_KEY_W5,
                              PAYLOAD_KEY_W6, PAYLOAD_KEY_W7 };
    const uint32_t nonce[3] = { PAYLOAD_NONCE_W0, PAYLOAD_NONCE_W1,
                                PAYLOAD_NONCE_W2 };
    chacha20::Cipher(buf, n, key, 0, nonce);
}

// Re-reads winmm.dll, re-parses the trailer (consistency with the stub) and
// reads BLOB2. Layout: [BLOB2][BLOB1][size2][size1][magic].
uint8_t* ReadBlob2(const MapperParams* p, uint32_t* out_size2) {
    HANDLE f = apihash::CreateFileA()(p->winmm_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);
    // Checks against what the stub passed (tamper in the trailer or in the params).
    if (size1 != p->size1 || size2 != p->size2 || magic != p->magic ||
        size1 + size2 + 12 > file_size) {
        apihash::CloseHandle()(f);
        return nullptr;
    }

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

}  // namespace

BOOL WINAPI DllMain(HINSTANCE, DWORD reason, LPVOID lpReserved) {
    if (reason != DLL_PROCESS_ATTACH) return TRUE;
    // No params (never supposed to happen) = inert.
    if (lpReserved == nullptr) return TRUE;
    const MapperParams* p = static_cast<const MapperParams*>(lpReserved);

    // F2: 2nd anti-debug layer. The mapper runs decrypted in anonymous
    // memory - if the attacker "fixed" the stub checks (NOP) or attached a
    // debugger between the two layers, this is where it catches. Returns FALSE ->
    // the stub MiniLoadAndRun frees the image and the chain aborts silently.
    if (antidebug::NtDebugProbe()) return FALSE;
    if (antidebug::HardwareBreakpointsProbe()) return FALSE;

    uint32_t size2 = 0;
    uint8_t* blob2 = ReadBlob2(p, &size2);
    if (blob2 == nullptr) return TRUE;

    // F2: timing around the payload decryption (single-step blows up).
    const uint64_t t0 = antidebug::Tsc();
    DecryptBlob2(blob2, size2);
    if (antidebug::RegionTooSlow(t0, antidebug::kSlowBudgetCycles)) {
        std::memset(blob2, 0, size2);
        apihash::VirtualFree()(blob2, 0, MEM_RELEASE);
        return FALSE;  // stepping suspicion: aborts the chain, silent
    }

    // Maps the payload (BLOB2) and runs its DllMain (installs the hook). The
    // payload lpReserved = watchdog kill-switch (stub).
    // const_cast removes the `volatile` (the payload re-applies it when casting
    // to LONG*).
    uint8_t* mapped = miniloader::MiniLoadAndRun(
        blob2, size2, PAYLOAD_CRC,
        static_cast<LPVOID>(const_cast<LONG*>(p->kill_flag)));

    // anti-dump: zeroes the decrypted staging before freeing. (The BLOB1
    // staging is zeroed by the stub.)
    std::memset(blob2, 0, size2);
    apihash::VirtualFree()(blob2, 0, MEM_RELEASE);

    // F2: passes the payload base to the stub -> the watchdog CRCs the
    // payload .text in memory (anti-patch of the hook code at runtime).
    const_cast<MapperParams*>(p)->mapped_payload = mapped;

    // Always returns TRUE (payload ok or not): payload failure = vanilla
    // game, no crash. Anti-debug detection above returns FALSE (aborts).
    (void)mapped;
    return TRUE;
}

#endif  // HARDENED
