Fixing our Call Stack
In response to a valid caveat pointed out by Chetan (here and here) in regards to TamperingSyscall's, he previously pointed out that some EDRs may check the origin of various problematic NT API function calls; if they originated from an unbacked RX (Read/Executable) region, the function call would be marked as malicious and potentially killed. Thus this short blog post will look to detail how it is possible to use an "indirect syscall" (if that is the correct terminology).
In the previous posts, we detail the basic application and usage of TamperingSyscalls. When making function calls in Windows using the Win32 API, more often than not, they will eventually call a function in ntdll.dll. As such, kernel32.VirtualAlloc will call kernelbase.VirtualAlloc, which in turn calls ntdll.NtAllocateVirtualMemory. In TamperingSyscalls, we call ntdll.NtAllocateVirtualMemory directly, a possible IoC.
It is pretty effortless to have the function call originate from the Win32 counterpart API instead of calling the NT API directly. We can then fix the required parameters in the exception handler as has been done when the hardware breakpoint is hit.
kernel32.VirtualAlloc is just a wrapper function for ntdll.NtAllocateVirtualMemory with the parameters being more or less the same. Suppose we have a look at the kernel32.VirtualAlloc definition, we see that it is missing some arguments which ntdll.NtAllocateVirtualMemory has.
LPVOID VirtualAlloc(
[in, optional] LPVOID lpAddress,
[in] SIZE_T dwSize,
[in] DWORD flAllocationType,
[in] DWORD flProtect
);
NTSYSCALLAPI NTSTATUS NtAllocateVirtualMemory(
[in] HANDLE ProcessHandle,
[in, out] PVOID *BaseAddress,
[in] ULONG_PTR ZeroBits,
[in, out] PSIZE_T RegionSize,
[in] ULONG AllocationType,
[in] ULONG Protect
);
We can quickly see pairs of matching argument type names such as flAllocationType, AllocationType and then flProtect, Protect, e.g. Recalling upon the previous posts, we know that we can only pass the first four arguments of the Nt function as NULL (alternatively, our spoofed arguments).
Here is where it becomes a bit tricky as the order and number of arguments differ; we must consider this. The only arguments we need to set are the ones which come after the fourth parameter in the Nt function; in this case, it is AllocationType and Protect. We can set the rest of the arguments in VirtualAlloc to NULL.
status = (NTSTATUS)VirtualAlloc( NULL, NULL, pNtAllocateVirtualMemoryArgs.AllocationType, pNtAllocateVirtualMemoryArgs.Protect );
Any kernel-level EDRs that will walk our stack frame will observe that the NT call originated from kernel32.VirtualAlloc as opposed to ntdll.NtAllocateVirtualMemory.
Applying this should be enough to get around various EDRs that may use EtwTi to find where the call originated. I recommend reading this blog on syscall detection if you are interested.
Here are some links on ETW stack tracing which will be utilized to see where a function call originates from.
https://github.com/pathtofile/Sealighter/blob/main/docs/SCENARIOS.md#use-stack-traces
Two examples of modifying the generated wrapper functions for VirtualAlloc and VirtualProtect are available here.
NTSTATUS pNtAllocateVirtualMemory( HANDLE ProcessHandle, PVOID* BaseAddress, ULONG ZeroBits, PSIZE_T RegionSize, ULONG AllocationType, ULONG Protect ) {
LPVOID FunctionAddress;
NTSTATUS status;
hash( NtAllocateVirtualMemory );
FunctionAddress = GetProcAddrExH( hashNtAllocateVirtualMemory, hashNTDLL );
typeNtAllocateVirtualMemory fNtAllocateVirtualMemory;
pNtAllocateVirtualMemoryArgs.ProcessHandle = ProcessHandle;
pNtAllocateVirtualMemoryArgs.BaseAddress = BaseAddress;
pNtAllocateVirtualMemoryArgs.ZeroBits = ZeroBits;
pNtAllocateVirtualMemoryArgs.RegionSize = RegionSize;
pNtAllocateVirtualMemoryArgs.AllocationType = AllocationType;
pNtAllocateVirtualMemoryArgs.Protect = Protect;
fNtAllocateVirtualMemory = (typeNtAllocateVirtualMemory)FunctionAddress;
EnumState = NTALLOCATEVIRTUALMEMORY_ENUM;
SetOneshotHardwareBreakpoint( FindSyscallAddress( FunctionAddress ) );
/// This call is not backed by Kernel32 (walking would reveal this)
//status = fNtAllocateVirtualMemory( NULL, NULL, NULL, NULL, pNtAllocateVirtualMemoryArgs.AllocationType, pNtAllocateVirtualMemoryArgs.Protect );
/// We can back our function call from Kernel32 as required.
status = (NTSTATUS)VirtualAlloc( NULL, NULL, pNtAllocateVirtualMemoryArgs.AllocationType, pNtAllocateVirtualMemoryArgs.Protect );
return status;
}
NTSTATUS pNtProtectVirtualMemory( HANDLE ProcessHandle, PVOID* BaseAddress, PSIZE_T RegionSize, ULONG NewProtect, PULONG OldProtect ) {
LPVOID FunctionAddress;
NTSTATUS status;
hash( NtProtectVirtualMemory );
FunctionAddress = GetProcAddrExH( hashNtProtectVirtualMemory, hashNTDLL );
typeNtProtectVirtualMemory fNtProtectVirtualMemory;
pNtProtectVirtualMemoryArgs.ProcessHandle = ProcessHandle;
pNtProtectVirtualMemoryArgs.BaseAddress = BaseAddress;
pNtProtectVirtualMemoryArgs.RegionSize = RegionSize;
pNtProtectVirtualMemoryArgs.NewProtect = NewProtect;
pNtProtectVirtualMemoryArgs.OldProtect = OldProtect;
fNtProtectVirtualMemory = (typeNtProtectVirtualMemory)FunctionAddress;
EnumState = NTPROTECTVIRTUALMEMORY_ENUM;
SetOneshotHardwareBreakpoint( FindSyscallAddress( FunctionAddress ) );
//status = fNtProtectVirtualMemory( NULL, NULL, NULL, NULL, pNtProtectVirtualMemoryArgs.OldProtect );
status = (NTSTATUS)VirtualProtect( NULL, NULL, NULL, pNtProtectVirtualMemoryArgs.OldProtect );
return status;
}
Comments
Post a Comment