Windows下的代码注入
木马和病毒的好坏很大程度上取决于它的隐蔽性,木马和病毒本质上也是在执行程序代码,如果采用独立进程的方式需要考虑隐藏进程否则很容易被发现,在编写这类程序的时候可以考虑将代码注入到其他进程中,借用其他进程的环境和资源来执行代码。远程注入技术不仅被木马和病毒广泛使用,防病毒软件和软件调试中也有很大的用途,最近也简单的研究过这些东西,在这里将它发布出来。
想要将代码注入到其他进程并能成功执行需要解决两个问题:
- 第一个问题是如何让远程进程执行注入的代码。原始进程有它自己的执行逻辑,想要破坏原来的执行流程,使EIP寄存器跳转到注入的代码位置基本是不可能的
- 第二个问题是每个进程中地址空间是独立的,比如在调用某个句柄时,即使是同一个内核对象,在不同进程中对应的句柄也是不同的,这就需要进行地址转化。
要进行远程代码注入的要点和难点主要就是这两个问题,下面给出两种不同的注入方式来说明如何解决这两个问题
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注入的步骤:
- 获取LoadLibrary函数的地址
- 调用VirtualAllocEx 函数在远程进程中申请一段虚拟内存
- 调用WriteProcessMemory 函数将参数写入对应的虚拟内存
- 调用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 == hSnapshot)
{
AfxMessageBox(_T("CreateToolhelp32Snapshot Error"));
return;
}
MODULEENTRY32 me = {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 = (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 = 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;