The purpose of this lab was to play with syscalls once more. More specifically, the goal was to be able to retrieve ntdll syscall stubs from the disk during run-time (before AVs/EDRs get a chance to hook them), rather than hardcoding them, since they may change between different Windows versions.
This lab was sparked by am0nsec's and RtlMateusz's
https://github.com/am0nsec/HellsGate although is very different in implementation and execution.
See my previous post on syscalls too:
{% content-ref url="using-syscalls-directly-from-visual-studio-to-bypass-avs-edrs.md" %} using-syscalls-directly-from-visual-studio-to-bypass-avs-edrs.md {% endcontent-ref %}
I will write some crude code that will do the following:
- Read ntdll.dll file bytes from the disk (before any AV/EDR has a chance to hook its functions) and write them to some memory location
m1
- Parse out
.rdata
and.text
sections of the ntdll.dll file.rdata
contains ntdll exported function names.text
contains code that gets executed by those functions
- Locate the specified function's code (syscall) in the memory location
m1
. In this lab I will find the location where the stub code forNtCreateFile
resides - Extract the stub (23 bytes) of the
NtCreateFile
and write it to some memory locationm2
- Declare a function prototype for
NtCreateFile
- Define a variable
v1
of function typeNtCreateFile
and point it to the memory locationm2
, where the syscall stub forNtCreateFile
is written, as mentioned in step 4. - Invoke the
NtCreateFile
syscall by calling the syscallv1
, which actually points tom2
, whereNtCreateFile
syscall stub is stored NtCreate
syscall gets executed - profit
{% hint style="info" %} Note, that the above process is just one way of achieving the same goal. {% endhint %}
As a reminder, we can easily see the syscall IDs for NT functions via WinDBG or any other debugger.
The syscall ID is 2 bytes in length and starts 4 bytes into the function, so for example, the syscall ID for NtCreateFile
is 0x0055
, NtQueryEvent
is 0x0056
, etc - see below image.
Also - in green are the bytes, that I refer to as syscall stub forNtCreateFile
and these are the bytes that we want to be able to retrieve at run-time for any given NT function, and hence this lab.
I wrote a function GetSyscallStub
, that is responsible for steps 3 and 4 of the processes that I outlined in the Overview
section.
It allows me to find any given function's code location inside the ntdll.dll and carve out its syscall stub (the first 23 bytes):
So, for example, if I wanted to retrieve the syscall stub for NtCreateFile
, I would call GetSyscallStub
like so:
GetSyscallStub(
// function name for which the syscall stub is to be retrieved
"NtCreateFile",
// ntdll export directory
exportDirectory,
// ntdll file bytes
fileData,
// ntdll .text section descriptor - contains code of ntdll exported functions. Required for locating NtCreateFile syscall stub
textSection,
// ntdll .rdata section descriptor - contains name of ntdll exported functions.
rdataSection,
// NtCreateFile stub will be written here
syscallStub
);
Once GetSyscallStub
is called, it will cycle through all the ntdll exported function names (they are resolved to functionNameResolved
) as well as exported function addresses simulatenously, and look for the function we want to extract the syscall stub for, which in our case is the NtCreateFile
(passed to GetSycallStub via functionName
):
Once the needed function name is resolved, the given function's syscall stub is extracted and stored in the syscallStub
variable.
In the below GIF, we can see the instruction mov eax, 0x55
when viewing the syscallStub
variable in a disassembly view. Since we know that the NtCreateFile
syscall ID is 0x0055
, this suggests we have extracted the syscall stub successfully:
In order to be able to invoke the syscall, we need to define a variable NtCreateFile
of type myNtCreateFile
(see code section for the function prototype), point it to the syscallStub
and make syscallStub
executable:
We can now call NtCreateFile
:
NtCreateFile(
&fileHandle,
FILE_GENERIC_WRITE,
&oa,
&osb,
0,
FILE_ATTRIBUTE_NORMAL,
FILE_SHARE_WRITE,
FILE_OVERWRITE_IF,
FILE_SYNCHRONOUS_IO_NONALERT,
NULL,
0
);
Below shows how NtCreateFile
gets called on a file c:\temp\pw.log and a handle to that file is opened, which confirms that NtCreateFile
syscall stub was retrieved and called successfully:
#include <iostream>
#include "Windows.h"
#include "winternl.h"
#pragma comment(lib, "ntdll")
int const SYSCALL_STUB_SIZE = 23;
using myNtCreateFile = NTSTATUS(NTAPI*)(PHANDLE FileHandle, ACCESS_MASK DesiredAccess, POBJECT_ATTRIBUTES ObjectAttributes, PIO_STATUS_BLOCK IoStatusBlock, PLARGE_INTEGER AllocationSize, ULONG FileAttributes, ULONG ShareAccess, ULONG CreateDisposition, ULONG CreateOptions, PVOID EaBuffer, ULONG EaLength);
PVOID RVAtoRawOffset(DWORD_PTR RVA, PIMAGE_SECTION_HEADER section)
{
return (PVOID)(RVA - section->VirtualAddress + section->PointerToRawData);
}
BOOL GetSyscallStub(LPCSTR functionName, PIMAGE_EXPORT_DIRECTORY exportDirectory, LPVOID fileData, PIMAGE_SECTION_HEADER textSection, PIMAGE_SECTION_HEADER rdataSection, LPVOID syscallStub)
{
PDWORD addressOfNames = (PDWORD)RVAtoRawOffset((DWORD_PTR)fileData + *(&exportDirectory->AddressOfNames), rdataSection);
PDWORD addressOfFunctions = (PDWORD)RVAtoRawOffset((DWORD_PTR)fileData + *(&exportDirectory->AddressOfFunctions), rdataSection);
BOOL stubFound = FALSE;
for (size_t i = 0; i < exportDirectory->NumberOfNames; i++)
{
DWORD_PTR functionNameVA = (DWORD_PTR)RVAtoRawOffset((DWORD_PTR)fileData + addressOfNames[i], rdataSection);
DWORD_PTR functionVA = (DWORD_PTR)RVAtoRawOffset((DWORD_PTR)fileData + addressOfFunctions[i + 1], textSection);
LPCSTR functionNameResolved = (LPCSTR)functionNameVA;
if (std::strcmp(functionNameResolved, functionName) == 0)
{
std::memcpy(syscallStub, (LPVOID)functionVA, SYSCALL_STUB_SIZE);
stubFound = TRUE;
}
}
return stubFound;
}
int main(int argc, char* argv[]) {
char syscallStub[SYSCALL_STUB_SIZE] = {};
SIZE_T bytesWritten = 0;
DWORD oldProtection = 0;
HANDLE file = NULL;
DWORD fileSize = NULL;
DWORD bytesRead = NULL;
LPVOID fileData = NULL;
// variables for NtCreateFile
OBJECT_ATTRIBUTES oa;
HANDLE fileHandle = NULL;
NTSTATUS status = NULL;
UNICODE_STRING fileName;
RtlInitUnicodeString(&fileName, (PCWSTR)L"\\??\\c:\\temp\\pw.log");
IO_STATUS_BLOCK osb;
ZeroMemory(&osb, sizeof(IO_STATUS_BLOCK));
InitializeObjectAttributes(&oa, &fileName, OBJ_CASE_INSENSITIVE, NULL, NULL);
// define NtCreateFile
myNtCreateFile NtCreateFile = (myNtCreateFile)(LPVOID)syscallStub;
VirtualProtect(syscallStub, SYSCALL_STUB_SIZE, PAGE_EXECUTE_READWRITE, &oldProtection);
file = CreateFileA("c:\\windows\\system32\\ntdll.dll", GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
fileSize = GetFileSize(file, NULL);
fileData = HeapAlloc(GetProcessHeap(), 0, fileSize);
ReadFile(file, fileData, fileSize, &bytesRead, NULL);
PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)fileData;
PIMAGE_NT_HEADERS imageNTHeaders = (PIMAGE_NT_HEADERS)((DWORD_PTR)fileData + dosHeader->e_lfanew);
DWORD exportDirRVA = imageNTHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress;
PIMAGE_SECTION_HEADER section = IMAGE_FIRST_SECTION(imageNTHeaders);
PIMAGE_SECTION_HEADER textSection = section;
PIMAGE_SECTION_HEADER rdataSection = section;
for (int i = 0; i < imageNTHeaders->FileHeader.NumberOfSections; i++)
{
if (std::strcmp((CHAR*)section->Name, (CHAR*)".rdata") == 0) {
rdataSection = section;
break;
}
section++;
}
PIMAGE_EXPORT_DIRECTORY exportDirectory = (PIMAGE_EXPORT_DIRECTORY)RVAtoRawOffset((DWORD_PTR)fileData + exportDirRVA, rdataSection);
GetSyscallStub("NtCreateFile", exportDirectory, fileData, textSection, rdataSection, syscallStub);
NtCreateFile(&fileHandle, FILE_GENERIC_WRITE, &oa, &osb, 0, FILE_ATTRIBUTE_NORMAL, FILE_SHARE_WRITE, FILE_OVERWRITE_IF, FILE_SYNCHRONOUS_IO_NONALERT, NULL, 0);
return 0;
}
{% embed url="https://github.com/odzhan/injection/blob/ad8e7a11899ffb2d9467a8ea44c6f3755d13b00e/syscalls/inject_dll.c#L260" %}
{% embed url="https://github.com/am0nsec/HellsGate" %}