dyncvoke-core 0.1.0

PEB walking, dynamic invoke, and Tartarus Gate indirect syscalls for Dyncvoke
// Hell's Gate sweep — broad coverage across the entire Zw* export space of
// ntdll.
//
// For every Zw* export:
//   1. Read the canonical SSN directly from the stub bytes (4C 8B D1 B8
//      <lo> <hi> 00 00), without touching the neighbor walker.
//   2. Run resolve_syscall(name).
//   3. The two MUST agree for any stub that has the canonical prologue.
//
// Stubs that don't have the canonical prologue (e.g. NtQuerySystemTime,
// which reads from KUSER_SHARED_DATA without a syscall) are tracked
// separately and OK — they exercise the Tartarus neighbor walk and we just
// log them.
//
// Catches OS-wide drift on a new Windows build: if a future build
// rearranges the SSDT, this fails loudly with the offending name and SSNs.

use dyncvoke_core::{get_function_address, get_module_base_address, resolve_syscall};

/// Walk the ntdll EAT and collect every `Zw*` export's name. Mirrors what
/// the deleted `get_ntdll_eat` did but discards the BTreeMap layer that
/// gave Bug 1 its second-order behavior.
fn collect_zw_exports(ntdll: usize) -> Vec<String> {
    let mut out = Vec::new();
    unsafe {
        let pe_header = *((ntdll + 0x3C) as *const i32) as usize;
        let opt_header = ntdll + pe_header + 0x18;
        let magic = *(opt_header as *const i16);
        let p_export = if magic == 0x010b {
            opt_header + 0x60
        } else {
            opt_header + 0x70
        };
        let export_rva = *(p_export as *const i32) as usize;
        let number_of_names = *((ntdll + export_rva + 0x18) as *const i32) as usize;
        let names_rva = *((ntdll + export_rva + 0x20) as *const i32) as usize;

        for x in 0..number_of_names {
            let name_rva = *((ntdll + names_rva + x * 4) as *const i32) as usize;
            let mut p = (ntdll + name_rva) as *const u8;
            let mut name = String::new();
            while *p != 0 {
                name.push(*p as char);
                p = p.add(1);
            }
            if name.starts_with("Zw") {
                out.push(name.replacen("Zw", "Nt", 1));
            }
        }
    }
    out
}

/// Read the SSN from the canonical Hell's Gate prologue, or None if the
/// stub doesn't have that exact layout. Distinct from `extract_ssn` which
/// also runs the neighbor walker on hooked stubs.
fn canonical_ssn(stub: usize) -> Option<u16> {
    if stub == 0 {
        return None;
    }
    unsafe {
        let p = stub as *const u8;
        (*p == 0x4C
            && *p.add(1) == 0x8B
            && *p.add(2) == 0xD1
            && *p.add(3) == 0xB8
            && *p.add(6) == 0
            && *p.add(7) == 0)
            .then(|| u16::from_le_bytes([*p.add(4), *p.add(5)]))
    }
}

#[test]
fn resolve_syscall_matches_canonical_byte_derived_ssn_for_every_zw_export() {
    let ntdll = get_module_base_address("ntdll.dll");
    assert!(ntdll != 0, "ntdll.dll not loaded");

    let names = collect_zw_exports(ntdll);
    assert!(
        names.len() > 200,
        "expected ~489 Zw exports on Win11 24H2, got {}",
        names.len()
    );

    let mut atypical: Vec<(String, u16)> = Vec::new();
    let mut mismatches: Vec<(String, u16, u16)> = Vec::new();

    for name in &names {
        let stub = get_function_address(ntdll, name);
        let (resolver_ssn, _) = match resolve_syscall(name) {
            Ok(v) => v,
            Err(e) => panic!("resolve_syscall({}) failed: {}", name, e),
        };

        match canonical_ssn(stub) {
            Some(canonical) => {
                if canonical != resolver_ssn {
                    mismatches.push((name.clone(), canonical, resolver_ssn));
                }
            }
            None => {
                // Atypical stub (e.g. NtQuerySystemTime). resolve_syscall
                // recovered the SSN via the neighbor walk — that's fine.
                atypical.push((name.clone(), resolver_ssn));
            }
        }
    }

    println!("total Zw exports: {}", names.len());
    println!("atypical (recovered via Tartarus neighbor walk): {}", atypical.len());
    for (name, ssn) in atypical.iter().take(10) {
        println!("  atypical: {} -> SSN {}", name, ssn);
    }

    assert!(
        mismatches.is_empty(),
        "{} canonical-vs-resolver SSN mismatches: first few = {:?}",
        mismatches.len(),
        &mismatches[..mismatches.len().min(10)]
    );
}