首页 > 代码库 > 反病毒攻防研究第010篇:DLL注入(中)——DLL注入与卸载器的编写
反病毒攻防研究第010篇:DLL注入(中)——DLL注入与卸载器的编写
一、前言
我在上一篇文章中所讨论的DLL利用方法,对于DLL文件本身来说是十分被动的,它需要等待程序的调用才可以发挥作用。而这次我打算主动出击,编写DLL注入与卸载器,这样就可以主动地对进程进行注入的操作了,从而更好地模拟现实中恶意代码的行为。
二、DLL注入的原理
如果想让DLL文件强制注入某个进程,那么就需要通过创建远程线程来实现。这里需要注意的是,所谓的“远程线程”,并不是跨计算机的,而是跨进程的。举例来说,进程A在进程B中创建一个线程,这就叫做远程线程。从根本上说,DLL注入技术要求目标进程中的一个线程调用LoadLibrary来载入我们想要的DLL。由于我们不能轻易地控制别人进程中的线程,因此这种方法要求我们在目标进程中创建一个新的线程。由于这个线程是我们自己创建的,因此我们可以对它执行的代码加以控制。远程线程不但在木马、外挂方面应用广泛,而且在反病毒软件方面也有着广泛的应用。
现在让我们来归纳一下DLL注入与卸载必须采取的步骤:
(1)用VirtualAllocEx函数在远程进程的地址空间中分配一块内存。
(2)用WriteProcessMemory函数把DLL的路径名复制到第1步分配的内存中。
(3)用GetProcAddress函数来得到LoadLibraryW或LoadLibraryA函数(在Kernel32.dll中)的实际地址。
(4)用CreateRemoteThread函数在远程进程中创建一个线程,让新线程调用正确的LoadLibrary函数并在参数中传入第1步分配的内存地址。这时,DLL已经被注入到远程进程的地址空间中,DLL的DllMain函数会收到DLL_PROCESS_ATTACH通知并且可以执行我们想要执行的代码。当DllMain返回的时候,远程线程会从LoadLibraryW/A调用返回到BaseThreadStart函数。BaseThreadStart然后调用ExitThread,使远程线程终止。
现在远程进程中有一块内存,它是我们在第1步分配的,DLL也还在远程进程的地址空间中。为了对它们进行清理,我们需要在远程线程退出之后执行后续步骤。
(5)用VirtualFreeEx来释放第1步分配的内存。
(6)用GetProcAddress来得到FreeLibrary函数(在Kernel32.dll中)的实际地址。
(7)用CreateRemoteThread函数在远程进程中创建一个线程,让该线程调用FreeLibrary函数并在参数中传入远程DLL的HMODULE。
可以说,DLL的注入与卸载操作也是遵循着一个模板的,所以我们在编程的时候,往往考验的不是个人的记忆力如何,而是遇到问题知不知道如何去寻找答案,如何去查找,这才是最重要的。代码有些时候可能会很长,这时无需去死记硬背,只需知道其原理,知道该怎么查,久而久之,想记不住都难。
三、DLL注入的代码实现
这个程序我用MFC来实现,首先制作程序的界面:
图1 界面外观
然后编写DLL注入文件的代码:
void CInjectDLLDlg::InjectDll(DWORD dwPid, char *szDllName) { if(dwPid == 0 || strlen(szDllName) == 0) { AfxMessageBox("输入信息不全!"); return; } //利用PID值,获取进程的句柄 HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPid); if(hProcess == NULL) { AfxMessageBox("进程打开失败!"); return; } //长度为进程名称的长度加上字符终止符 int nDllLen = strlen(szDllName) + sizeof(char); //申请内存空间 PVOID pDllAddr = VirtualAllocEx( hProcess, //process to allocate memory NULL, //desired starting address nDllLen, //size of region to allocate MEM_COMMIT, //type of allocation PAGE_READWRITE); //type of access protection if(pDllAddr == NULL) { AfxMessageBox("申请内存区域失败!"); CloseHandle(hProcess); return; } DWORD dwWriteNum = 0; if (!WriteProcessMemory(hProcess,pDllAddr,szDllName,nDllLen,&dwWriteNum)) { AfxMessageBox("进程写入失败!"); //失败就释放原先申请的内存区域 撤销内存页的提交状态 VirtualFreeEx(hProcess, pDllAddr, nDllLen, MEM_DECOMMIT); return; } //获取LoadLibraryA的地址 FARPROC pFunAddr = GetProcAddress(GetModuleHandle("kernel32.dll"), "LoadLibraryA"); HANDLE hThread = CreateRemoteThread(hProcess, //handle to process NULL, //SD 0, //initial stack size (LPTHREAD_START_ROUTINE)pFunAddr, //thread function pDllAddr, //thread argument 0, //creation option NULL); //thread identifier if (hThread == NULL) { AfxMessageBox("创建远程线程失败!"); //释放原先申请的内存区域 撤销内存页的提交状态 VirtualFreeEx(hProcess, pDllAddr, nDllLen, MEM_DECOMMIT); return; } AfxMessageBox("成功注入!"); //等待线程退出 WaitForSingleObject(hThread,INFINITE); //释放原先申请的内存区域 撤销内存页的提交状态 VirtualFreeEx(hProcess, pDllAddr, nDllLen, MEM_DECOMMIT); //关闭句柄 CloseHandle(hThread); CloseHandle(hProcess); }
上述代码我已经添加了足够的注释,就不再进行分析。这里我想要强调的是,良好的代码风格有助于我们及时发现程序中的错误。具体来讲,每创建一个进程或是分配一块内存空间,都对其是否成功创建进行判断,特别对于类似上述过程较多的程序而言,出问题时可以让我们知道程序运行到了哪一步。
不要忘记在InjectDLLDlg.h文件中的classCInjectDLLDlg : public Cdialog下添加如下声明:void InjectDll(DWORD dwPid, char *szDllName);最后添加“注入”按键的代码:
void CInjectDLLDlg::OnBtnInject() { char szPath[MAX_PATH] = { 0 }; DWORD pid; GetDlgItemText(IDC_EDIT_DLLPATH, szPath, MAX_PATH); pid = GetDlgItemInt(IDC_EDIT_PID, NULL, TRUE); InjectDll(pid, szPath); }编译成功后运行,这里我以记事本(notepad)程序为例。启动记事本程序,查看其PID值(可以在cmd中输入tasklist查看),然后启动注入程序,将我上次编写的HackedDll.dll文件的完整路径填入程序的“注入/卸载DLL”输入框中,再将记事本的PID值填入“注入/卸载PID值”输入框中,单击“注入”,用于模拟病毒的对话框自动启动,说明已经成功注入:
四、DLL卸载的代码实现
DLL注入技术如果用在木马方面,那么它的危害就会很大。接下来讨论的是如何卸载程序中的DLL。卸载DLL程序的思路和注入的思路差不多,代码改动非常小。而对于卸载的讨论,也相当于讨论了如何通过编程手段来对抗DLL注入。
DLL卸载的代码如下:void CInjectDLLDlg::UnInjectDll(DWORD dwPid, char *szDllName) { BOOL flag = FALSE; if ( dwPid == 0 || strlen(szDllName) == 0 ) { return; } //获取系统运行进程、线程等的列表 HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, dwPid); MODULEENTRY32 Me32 = { 0 }; Me32.dwSize = sizeof(MODULEENTRY32); //检索与进程相关联的第一个模块的信息 BOOL bRet = Module32First(hSnap, &Me32); while ( bRet ) { //查找所注入的DLL if ( strcmp(Me32.szModule, szDllName) == 0 ) { flag = TRUE; break; } //检索下一个模块信息 bRet = Module32Next(hSnap, &Me32); } if (flag == FALSE) { AfxMessageBox("找不到相应的模块!"); return; } CloseHandle(hSnap); HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPid); if ( hProcess == NULL ) { AfxMessageBox("进程打开失败!"); return ; } FARPROC pFunAddr = GetProcAddress(GetModuleHandle("kernel32.dll"),"FreeLibrary"); HANDLE hThread = CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)pFunAddr, Me32.hModule, 0, NULL); if (hThread == NULL) { AfxMessageBox("创建远程线程失败!"); return; } AfxMessageBox("成功卸载!"); //等待线程退出 WaitForSingleObject(hThread, INFINITE); CloseHandle(hThread); CloseHandle(hProcess); }由于程序中使用了CreateToolhelp32Snapshot()函数,所以需要添加头文件:
#include <Tlhelp32.h>之后在InjectDLLDlg.h文件中的classCInjectDLLDlg : public Cdialog下添加如下声明:
void UnInjectDll(DWORD dwPid, char* szDllName);最后添加“卸载”按键的代码:
void CInjectDLLDlg::OnBtnUnInject() { char szDllName[MAX_PATH] = { 0 }; DWORD pid = 0; GetDlgItemText(IDC_EDIT_DLLPATH, szDllName, MAX_PATH); pid = GetDlgItemInt(IDC_EDIT_PID, NULL, TRUE); UnInjectDll(pid, szDllName); }编译成功后,执行程序,只要我们知道目标进程的PID值以及DLL的名称,就能够实现DLL卸载的操作。注意这里的“注入/卸载DLL”输入框,只需要输入 DLL的名称即可,无需输入完整的路径名。经实际测试,程序可行,这里不再赘述。它也可以当做是我们的安全工具,用于DLL注入类木马病毒的清除。
五、知识补充
DLL注入到一个进程中后,只要进程不结束,那么DLL就会一直附加在进程中。比如我的这个DLL对话框程序,当对话框弹出,即便是单击了“确定”,尽管对话框关闭了,但是用“冰刃”进行检查,发现DLL并没有消除,只有关闭了进程(记事本程序),才会消除DLL。或者需要通过我们文章中所编写的DLL卸载器进行卸载。一般来说,恶意程序总会对系统进程(如svchost.exe)进行DLL注入的操作,对于这种情况来说,想要进行查杀,我们自身平时就要多多积累,要清楚这些系统敏感进程在正常情况下会包含哪些DLL,或者实在不知道,可以通过上网查找或者运用相关软件的校验功能对DLL进行检测。需要说明的是,如果要对系统进程进行注入的话,由于有一个OpenProcess()的权限问题,可能会导致无法获得系统进程的句柄。但是这个问题有些敏感,我不打算讨论。
六、小结
我自己比较喜欢建立一个“程序库”,以便于当未来遇到要编写类似的程序时能够方便查找。这样每次就无需从零开始编程,将“程序库”中的程序改动一下就可以。就比如这篇文章所讲的DLL注入/卸载的代码,就可以添加到“程序库”中。当然,建立“程序库”主要还是因为我自己的记忆力不好,加上自己并不是天天都要进行编程,难免生疏。对程序库中的程序,只要记住原理即可,并且添加上足够的注释,这样等到未来使用时,稍微看一看,就有一种“哦,原来如此”的感觉。反病毒攻防研究第010篇:DLL注入(中)——DLL注入与卸载器的编写