Windows下的代码注入

木马和病毒的好坏很大程度上取决于它的隐蔽性,木马和病毒本质上也是在执行程序代码,如果采用独立进程的方式需要考虑隐藏进程否则很容易被发现,在编写这类程序的时候可以考虑将代码注入到其他进程中,借用其他进程的环境和资源来执行代码。远程注入技术不仅被木马和病毒广泛使用,防病毒软件和软件调试中也有很大的用途,最近也简单的研究过这些东西,在这里将它发布出来。

想要将代码注入到其他进程并能成功执行需要解决两个问题:

  1. 第一个问题是如何让远程进程执行注入的代码。原始进程有它自己的执行逻辑,想要破坏原来的执行流程,使EIP寄存器跳转到注入的代码位置基本是不可能的
  2. 第二个问题是每个进程中地址空间是独立的,比如在调用某个句柄时,即使是同一个内核对象,在不同进程中对应的句柄也是不同的,这就需要进行地址转化。
要进行远程代码注入的要点和难点主要就是这两个问题,下面给出两种不同的注入方式来说明如何解决这两个问题
DLL注入 DLL注入很好的解决了第二个问题,DLL被加载到目标进程之后,它里面的代码中的地址就会自动被转化为对应进程中的地址,这个特性是由于DLL加载的过程决定的,它会自己使用它所在进程中的资源和地址空间,所以只要DLL中不存在硬编码的地址,基本不用担心里面会出现函数或者句柄需要进行地址转化的问题。
那么第一个问题改怎么解决呢?
要执行用户代码,在Windows中最常见的就是使用回调的方式,Windows采用的是事件驱动的方式,只要发生了某些事件就会调用回调,在众多使用回调的场景中,线程的回调是最简单的,它不会干扰到目标进程的正常执行,也就不用考虑最后还原EIP的问题,因此DLL注入采用的最常见的就是创建一个远程线程,让线程加载DLL代码。
DLL注入中一般的思路是:使用CreateRemoteThread来在目标进程中创建一个远程的线程,这个线程主要是加载DLL到目标进程中,由于DLL在入口函数(DLLMain)中会处理进程加载Dll的事件,所以将注入代码写到这个事件中,这样就能执行注入的代码了。那么如何在远程进程中执行DLL的加载操作呢?我们知道加载DLL主要使用的是函数LoadLibrary,仔细分析线程的回调函数和LoadLibrary函数的原型,会发现,它们同样都是传入一个参数,而CreateRemoteThread函数正好需要一个函数的地址作为回调,并且传入一个参数作为回调函数的参数。这样就有思路了,我们让LoadLibrary作为线程的回调函数,将对应dll的文件名和路径作为参数传入,这样就可以在对应进程中加载dll了,进一步也就可以执行dllmain中的对应代码了。
还有一个很重要的问题,我们知道不同进程中,地址空间是隔离的,那么我在注入的进程中传入LoadLibrary函数的地址,这算是一个硬编码的地址,它在目标进程中是否是一样的呢?答案是,二者的地址是一样的,这是由于kernel32.dll在32位程序中加载的基地址是一样的,而LoadLibrary在kernel32.dll中的偏移是一定的(只要不同的进程加载的是同一份kernel32.dll)那么不同进程中的LoadLibrary函数的地址是一样的。其实不光是LoadLibrary函数,只要是kernel32.dll中导出的函数,在不同进程中的地址都是一样的。注意这里只是32位,如果想要使用32位程序往64位目标程序中注入,可能需要考虑地址转换的问题,只要知道kernel32.dll在64位中的偏移,就可以计算出对应函数的地址了。
LoabLibrary函数传入的代表路径的字符串的首地址在不同进程中同样是不同的,而且也没办法利用偏移来计算,这个时候解决的办法就是在远程进程中申请一块虚拟地址空间,并将目标字符串写入对应的地址中,然后将对应的首地址作为参数传入即可。
最后总结一下DLL注入的步骤:
  1. 获取LoadLibrary函数的地址
  2. 调用VirtualAllocEx 函数在远程进程中申请一段虚拟内存
  3. 调用WriteProcessMemory 函数将参数写入对应的虚拟内存
  4. 调用CreateRemoteThread 函数创建远程线程,线程的回调函数为LoadLibrary,参数为对应的字符串的地址
按照这个思路可以编写如下的代码:
typedef HMODULE(WINAPI *pfnLoadLibrary)(LPCWSTR); if (!DebugPrivilege()) //提权代码,在Windows Vista 及以上的版本需要将进程的权限提升,否则打开进程会失败 { return FALSE; }//打开目标进程 HANDLE hRemoteProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPid); //dwPid是对应的进程ID if (NULL == hRemoteProcess) { AfxMessageBox(_T("OpenProcess Error")); }//查找LoadLibrary函数地址 pfnLoadLibrary lpLoadLibrary = (pfnLoadLibrary)GetProcAddress(GetModuleHandle(_T("kernel32.dll")), "LoadLibraryW"); //在远程进程中申请一块内存用于保存对应线程的参数 PVOID pBuffer = VirtualAllocEx(hRemoteProcess, NULL, MAX_PATH, MEM_COMMIT, PAGE_READWRITE); //在对应内存位置处写入参数值 DWORD dwWritten = 0; WriteProcessMemory(hRemoteProcess, pBuffer, m_csDLLName.GetString(), (m_csDLLName.GetLength() + 1) * sizeof(TCHAR), &dwWritten); //创建远程线程并传入对应参数 HANDLE hRemoteThread = CreateRemoteThread(hRemoteProcess, NULL, 0, (LPTHREAD_START_ROUTINE)lpLoadLibrary, pBuffer, 0, NULL); WaitForSingleObject(hRemoteThread, INFINITE); VirtualFreeEx(hRemoteProcess, pBuffer, 0, MEM_RELEASE); CloseHandle(hRemoteThread); CloseHandle(hRemoteProcess);

卸载远程DLL 上面进行了代码的注入,作为一个文明的程序,自然得考虑卸载dll,毕竟现在提倡环保,谁使用,谁治理。这里既然注入了,自然得考虑卸载。
卸载的思路与注入的类似,只是函数变为了FreeLibrary,传入的参数变成了对应的dll的句柄了。
如何获取这个模块的句柄呢?我们可以枚举进程中的模块,根据模块的名称来找到对应的模块并获取它的句柄。枚举的方式一般是使用toolhelp32中对应的函数,下面是卸载的例子代码
HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, m_dwPid); if (INVALID_HANDLE_VALUE =https://www.it610.com/article/= hSnapshot) { AfxMessageBox(_T("CreateToolhelp32Snapshot Error")); return; }MODULEENTRY32me = {0}; me.dwSize = sizeof(MODULEENTRY32); BOOL bRet = Module32First(hSnapshot, &me); while (bRet) { CString csModuleFile = _tcsupr(me.szExePath); if (csModuleFile == _tcsupr((LPTSTR)m_csDLLName.GetString()) != -1) { break; }ZeroMemory(&me, sizeof(me)); me.dwSize = sizeof(PROCESSENTRY32); bRet = Module32Next(hSnapshot, &me); }CloseHandle(hSnapshot); typedef BOOL (*pfnFreeLibrary)(HMODULE); pfnFreeLibrary FreeLibrary = (pfnFreeLibrary)GetProcAddress(GetModuleHandle(_T("kernel32.dll")), "FreeLibrary"); HANDLE hRemoteProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, m_dwPid); if (hRemoteProcess == NULL) { AfxMessageBox(_T("OpenProcess Error")); return; }HANDLE hRemoteThread = CreateRemoteThread(hRemoteProcess, NULL, 0, (LPTHREAD_START_ROUTINE)FreeLibrary, me.modBaseAddr, 0, NULL); WaitForSingleObject(hRemoteThread, INFINITE); CloseHandle(hRemoteThread); CloseHandle(hRemoteProcess);

无DLL的注入 注入不一定需要使用DLL,虽然使用DLL比较简单一点,无DLL注入在解决上述两个问题的第一个思路是一样的,也是使用CreateRemoteThread来创建一个远程线程来执行目标代码。
无dll的注入主要麻烦是在进行地址转化上,在调用API的时候,如果无法保证对应的dll的基地址不变的话,就得在目标进程中自行调用LoadLibrary来动态获取函数地址,并调用。
在动态获取API函数的地址的时候,主要使用的函数是LoadLibrary、GetModuleHandle、GetProcAddress这三个函数,而线程的回调函数只能传入一个参数,所以我们需要将对应的需要传入的参数组成一个结构体,并将结构体对应的数据写入到目标进程的内存中,特别要注意的是,里面不要使用指针或者句柄这种与地址有关的东西。
例如我们想在目标进程中注入一段代码,让它弹出一个对话框,以便测试是否注入成功。这种情况除了要传入上述三个函数的地址外,还需要MesageBox,而MessageBox是在user32.dll中,user32.dll在每个进程中的基地址并不相同,因此在注入的代码中需要动态加载,因此可以定义下面一个结构
typedef struct REMOTE_DATA { DWORD dwLoadLibrary; DWORD dwGetProcAddress; DWORD dwGetModuleHandle; DWORD dwGetModuelFileName; //辅助函数 char szUser32dll[MAX_PATH]; //存储user32dll的路径,以便调用LoadLibrary加载 char szMessageBox[128]; //存储字符串MessageBoxA 这个字符串,以便使用GetProcAddress加载MesageBox函数 char szMessage[512]; //弹出对话框上显示的字符 }

不使用DLL注入与使用DLL注入的另一个区别是,不使用DLL注入的时候需要自己加载目标代码到对应的进程中,这个操作可以借由WriteProcessMemory 将函数代码写到对应的虚拟内存中。
最后注入的代码主要如下:
DWORD WINAPI RemoteThreadProc(LPVOID lpParam) { LPREMOTE_DATA lpData = https://www.it610.com/article/(LPREMOTE_DATA)lpParam; typedef HMODULE (WINAPI *pfnLoadLibrary)(LPCSTR); typedef FARPROC (WINAPI *pfnGetProcAddress)(HMODULE, LPCSTR); typedef HMODULE (*pfnGetModuleHandle)(LPCSTR); typedef DWORD (WINAPI *pfnGetModuleFileName)( HMODULE,LPSTR, DWORD); pfnGetModuleHandle MyGetModuleHandle = (pfnGetModuleHandle)lpData->dwGetModuleHandle; pfnGetModuleFileName MyGetModuleFileName = (pfnGetModuleFileName)lpData->dwGetModuleFileName; pfnGetProcAddress MyGetProcAddress = (pfnGetProcAddress)lpData->dwGetProcAddress; pfnLoadLibrary MyLoadLibrary = (pfnLoadLibrary)lpData->dwGetProcAddress; typedef int (WINAPI *pfnMessageBox)(HWND, LPCSTR, LPCSTR, UINT); //加载User32.dll HMODULE hUser32Dll = MyLoadLibrary(lpData->szUerDll); //加载MessageBox函数 pfnMessageBox MyMessageBox = (pfnMessageBox)MyGetProcAddress(hUser32Dll, lpData->szMessageBox); char szTitlte[MAX_PATH] = ""; MyGetModuleFileName(NULL, szTitlte, MAX_PATH); MyMessageBox(NULL, lpData->szMessage, szTitlte, MB_OK); return 0; }m_dwPid = GetPid(); //获取目标进程ID DebugPrivilege(); //进程提权 HANDLE hRemoteProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE,m_dwPid); if (NULL == hRemoteProcess) { AfxMessageBox(_T("OpenProcess Error")); return; }LPREMOTE_DATA lpData = https://www.it610.com/article/new REMOTE_DATA; ZeroMemory(lpData, sizeof(REMOTE_DATA)); //获取对应函数的地址 lpData->dwGetModuleFileName = (DWORD)GetProcAddress(GetModuleHandle(_T("kernel32.dll")), "GetModuleFileNameA"); lpData->dwGetModuleHandle = (DWORD)GetProcAddress(GetModuleHandle(_T("kernel32.dll")), "GetModuleHandleA"); lpData->dwGetProcAddress = (DWORD)GetProcAddress(GetModuleHandle(_T("kernel32.dll")), "GetProcAddress"); lpData->dwLoadLibrary = (DWORD)GetProcAddress(GetModuleHandle(_T("kernel32.dll")), "LoadLibraryA"); // 拷贝对应的字符串 StringCchCopyA(lpData->szMessage, MAX_STRING_LENGTH, "Inject Success!!!"); StringCchCopyA(lpData->szUerDll, MAX_PATH, "user32.dll"); StringCchCopyA(lpData->szMessageBox, MAX_PROC_NAME_LENGTH, "MessageBoxA"); //在远程空间中申请对应的内存,写入参数和函数的代码 LPVOID lpRemoteBuf = VirtualAllocEx(hRemoteProcess, NULL, sizeof(REMOTE_DATA), MEM_COMMIT, PAGE_READWRITE); // 存储data结构的数据 LPVOID lpRemoteProc = VirtualAllocEx(hRemoteProcess, NULL, 0x4000, MEM_COMMIT, PAGE_EXECUTE_READWRITE); // 存储函数的代码 DWORD dwWrittenSize = 0; WriteProcessMemory(hRemoteProcess, lpRemoteProc, &RemoteThreadProc, 0x4000, &dwWrittenSize); WriteProcessMemory(hRemoteProcess, lpRemoteBuf, lpData, sizeof(REMOTE_DATA), &dwWrittenSize); HANDLE hRemoteThread = CreateRemoteThread(hRemoteProcess, NULL, 0, (LPTHREAD_START_ROUTINE)lpRemoteProc, lpRemoteBuf, 0, NULL); WaitForSingleObject(hRemoteThread, INFINITE); VirtualFreeEx(hRemoteProcess, lpRemoteBuf, 0, MEM_RELEASE); VirtualFreeEx(hRemoteProcess, lpRemoteProc, 0, MEM_RELEASE); CloseHandle(hRemoteThread); CloseHandle(hRemoteProcess); delete[] lpData;

【Windows下的代码注入】

    推荐阅读