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). Tuy nhiên không phải chỉ chèn được code dạng DLL, chúng ta có thể chèn code ở nhiều dạng khác như exe, handwritten, … Điều quan trọng là chúng ta có đủ quyền hệ thống để thao tác với tiến trình của ứng dụng khác hay không.

Thực ra 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 sẽ tận dụng các API này để thực hiện chèn DLL. Tôi sẽ chia DLL injection thành 4 bước 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

Mỗi bước có thể được thực hiện bởi một hoặc nhiều các kỹ thuật, sẽ được tóm tắt bằng hình ảnh bên dưới. Quan trọng là chúng ta hiểu được chi tiết các kỹ thuật và các mặt tích cực, tiêu cực của chúng để sử dụng trong các trường hợp khác nhau.

Để 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.

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 );

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, còn 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 LoadLibraryA() cùng với những hạn chế tôi đã nói ở 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 );

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()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:

https://github.com/stephenfewer/ReflectiveDLLInjection/

dwReflectiveLoaderOffset = GetReflectiveLoaderOffset(lpBuffer);

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ó. Có một vài cách để process thực thi DLL nhưng trong phạm vi bài viết này tôi chỉ trình bày cách sử dụng CreateRemoteThread()CreateRemoteThread() cũng là cách được sử dụng rộng dã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.
Sau đây là một vài hình ảnh khi tôi demo kỹ thuật.

 

Thư viện mbox.dll tôi inject vào notepad đã có chứa file mbox.dll

Cách phát hiện

Có nhiều cách để phát hiện ra kỹ thuật DLL Injection, nhưng cách phổ biến là kiểm tra danh sách các module import và tìm các vùng nhớ có quyền đọc ghi của process.

Trên đây tôi đã trình bày chi tiết kỹ thuật DLL Injection. Tôi đánh giá đây là một kỹ thuật rất mạnh mẽ, linh hoạt trong nhiều trường hợp và cũng khá là đơn giản để sử dụng, hầu như có thể áp dụng trong mọi trường hợp mà bạn muốn. Hẹn gặp lại mọi người trong những bài viết sau.

REteam

5.606 lượt xem