Windows操作系统下,为了避免各个进程相互影响,每个进程地址空间都是被隔离的。所谓 “远程线程”,它不是跨计算机的,而是跨过程的。简单地说,这是过程A要在进程B创建一个线程叫做远程线程。
远程线程被木马、插件和其他程序广泛使用,远程线程技术在反病毒软件中是不可或缺的。技术应用的两面性取决于他们自己的个人行为意识。良性的技术学习对他们的生活发展非常有益。即使没有好处,至少也不会给自己带来不必要的麻烦。
本文介绍了远程线程知识的三个例子,即DLL远程注入卸载DLL和不依赖DLL注入代码。
1. DLL远程注入
木马或病毒的编写取决于它的隐藏程度,而不是它的功能。木马和病毒都是可执行的程序。EXE如果是文件,在运行过程中会产生一个过程,很容易被发现。为了不被发现,在编写木马或病毒时马或病毒时编写它DLL文件。DLL文件的运行不会单独创建一个过程,其运行被加载到过程的地址空间中,因此其隐蔽性相对较好。DLL如果文件不被过程加载,如何在过程的地址空间中运行? *** 是强制加载过程DLL文件到其地址空间,这种强制手段是现在要介绍的远程线程。
创建远程线程函数CreateRemoteThread()定义如下:
HANDLECreateRemoteThread(HANDLEhProcess,LPSECURITY_ATTRIBUTESlpThreadAttributes,DWORDdwStackSize,LPTHREAD_START_ROUTINElpStartAddress,LPVOIDlpParameter,DWORDdwCreationFlags,LPDWORDlpThreadId);该函数的功能是创建一个远程的线程。我们把CreateThread()函数和CreateRemoteThread对函数进行比较。CreateThread()函数方面,CreateRem oteThread()函数比其多一个函数hProcess参数是指定要创建线程的过程句柄。CreateThread()函数内容的实现取决于CreateRemoteThread()函数完成。CreateThread()函数代码实现如下:
/**@implemented*/HANDLEWINAPICreateThread(LPSECURITY_ATTRIBUTESlpThreadAttributes,DWORDdwStackSize,LPTHREAD_START_ROUTINElpStartAddress,LPVOIDlpParameter,DWORDdwCreationFlags,LPDWORDlpThreadId){/*创建远程线程returnCreateRemoteThread(NtCurrentProcess(),lpThreadAttributes,dwStackSize,lpStartAddress,lpParameter,dwCreationFlags,lpThreadId);}在上述代码中,NtGetCurrentProcess()函数的功能是获得当前过程的句柄。
CreateRemoteThread()函数用于为其他过程创建线程。之一个参数是指定一个过程的句柄,使用获取过程的句柄API函数OpenProcess()需要提供函数PID作为参数。
除了hProcess除了参数,剩余的关键参数只是lpStartAddress和lpParameter两个了。lpStartAddress指定线程函数的地址,lpParameter指定传递给线程函数的参数。上述每个过程的地址空间都是隔离的,因此新创建的线程函数的地址也应该在目标过程中,而不是调用CreateRemoteThread()函数的过程。同样,传递给线程函数的参数也应该在目标过程中。
如何让线程函数的地址在目标过程中?如何将线程函数的参数传输到目标过程中?在讨论这个问题之前,首先考虑线程函数的功能。这里的主要功能是注入一个DLL当文件到达目标时,线程函数的功能是加载DLL文件。加载DLL使用文件LoadLibrary()函数。LoadLibrary()函数的定义:
HMODULELoadLibrary(LPCTSTRlpFileName);具体如下:DWORDWINAPIThreadProc(LPVOIDlpParameter);通过比较两个函数,我们可以发现函数格式是相同的,除了函数的返回值类型和参数类型。这里只考虑相同的部分。由于函数格式相同,首先调用协议相同WINAPI(也就是__stdcall *** );其次,函数数量相同,只有一个。然后,你可以直接把它拿走。LoadLibrary()将函数作为线程函数创建到指定的过程中。LoadLibrary()参数需要加载DLL只要文件的完整路径在CreateRemoteThread()函数中赋值的指向DLL文件完整路径的指针LoadLibrary()函数即可。这样使用。CreateRemoteThread()函数可以创建远程线程。然而,还有两个问题没有解决。首先是如何解决LoadLibrary()将函数的地址放入目标过程空间CreateRemoteThread()调用,其次是传递LoadLibrary()函数的参数也需要在目标过程空间中通过CreateRemoteThread()函数指定给LoadLibrary()函数。
首先,如何解决之一个问题LoadLibrary()将函数的地址放入目标过程空间。LoadLibrary()函数在系统中Kernel32.dll导出函数,Kernel32.dll这个DLL文件在任何进程中的加载位置都是相同的,也就是说,LoadLibrary()函数的地址在任何过程中都是一样的。因此,只要在过程中获得LoadLibrary()函数的地址也可以在目标过程中使用。CreateRemoteThread()函数的线程地址参数直接传输LoadLibrary()函数地址。
第二个问题是如何加载DLL在目标过程中写入文件的完整路径。这需要帮助。WriteProcessMemory()函数的定义如下:
BOOLWriteProcessMemory(HANDLEhProcess,//handletoprocessLPVOIDlpBaseAddress,//baseofmemoryareaLPVOIDlpBuffer,//databufferDWORDnSize,//numberofbytestowriteLPDWORDlpNumberOfBytesWritten//numberofbyteswritten);该函数的功能是把lpBuffer内容写在过程句柄上hProcess进程的lpBaseAddress写入长度为地址nSize。
参数说明如下。
hProcess:该参数是指定过程的过程句柄。
lpBaseAddress:该参数是指定写入目标过程内存的起始地址。
lpBuffer:该参数是缓冲区的起始地址,应写入目标过程内存。
nSize:该参数是指定写入目标内存的缓冲区的长度。
lpNumberOfBytesWritten:该参数用于接收实际写入内容的长度。
例如,在破解方面,该函数的功能非常强大,可以实现“内存补丁”;该函数可用于修改目标过程中指定的值(如游戏修改器可修改游戏中的钱、红、蓝等)。
可以使用此函数DLL将文件的完整路径写入目标过程的内存地址,以便在目标过程中使用LoadLibrary()函数加载指定DLL文件。以上两个问题已经解决,第三个问题需要解决。WriteProcessMemory()函数的第二个参数是指定写入目标过程内存的缓冲区的起始地址。这个地址在目标过程中,那么目标过程中的地址在哪里呢?允许在目标过程中使用内存块DLL写文件的路径吗?
如何确定应该解决的第三个问题?DLL将文件的完整路径写入目标过程的哪个地址。对于目标过程,用户不会提前准备一个地址来写入。用户所能做的就是在目标过程中申请一个内存,然后把它拿走DLL写入文件的路径,申请的内存空间中的文件路径。在目标过程中申请内存的函数是VirtualAllocEx()其定义如下:
LPVOIDVirtualAllocEx(HANDLEhProcess,LPVOIDlpAddress,SIZE_TdwSize,DWORDflAllocationType,DWORDflProtect);VirtualAllocEx()函数参数说明如下。
hProcess:该参数是指定过程的过程句柄。
lpAddress:该参数是指在目标过程中申请内存的起始地址。
dwSize:该参数是指在目标过程中申请内存的长度。
flAllocationType:指定申请内存的状态类型。
flProtect:指定申请内存的属性。
该函数的返回值是在目标过程中申请的内存块的起始地址。
至此,关于写一个DLL所有注入的知识都已经有了。现在开始写一个DLL注入工具的界面如图1所示。
图1 DLL注入/卸载器
该工具分别注入两个功能DLL注入卸载DLL。卸载注入DLL以后会介绍功能。界面需要输入两部分,之一部分需要注入DLL文件的完整路径(必须是完整路径),第二部分是过程的名称。
首先,查看界面操作,代码如下:
voidCInjectDllDlg::OnBtnInject(){///添加处理程序代码charszDllName[MAX_PATH]={0};charszProcessName[MAXBYTE]={0};DWORDdwPid=0;GetDlgItemText(IDC_EDIT_DLLFILE,szDllName,MAX_PATH);GetDlgItemText(IDC_EDIT_PROCESSNAME,szProcessName,MAXBYTE);//由进程名获得PIDdwPid=GetProcId(szProcessName);//注入szDllName到dwPidInjectDll(dwPid,szDllName);}另外两个函数在代码中调用,之一个是通过程名获得的PID第二个用于函数DLL注入函数。GetProcId()函数代码如下:
DWORDCInjectDllDlg::GetProcId(char*szProcessName){BOOLbRet;PROCESSENTRY32pe32;HANDLEhSnap;hSnap=CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS,NULL);pe32.dwSize=sizeof(pe32);bRet=Process32First(hSnap,&pe32);while(bRet){//strupr()函数将字符串转化为大写if(lstrcmp(strupr(pe32.szExeFile),strupr(szProcessName))==0){returnpe32.th32ProcessID;}bRet=Process32Next(hSnap,&pe32);}return0;} InjectDll()函数代码如下:
VOIDCInjectDllDlg::InjectDll(DWORDdwPid,char*szDllName){if(dwPid==0||lstrlen(szDllName)==0){return;}char*pFunName="LoadLibraryA";//打开目标流程HANDLEhProcess=OpenProcess(PROCESS_ALL_ACCESS,FALSE,dwPid);if(hProcess==NULL){return;}//计算欲注入DLL文件完整路径的长度intnDllLen=lstrlen(szDllName) sizeof(char);///在目标过程中申请一个长度nDllLen内存空间大小PVOIDpDllAddr=VirtualAllocEx(hProcess,NULL,nDllLen,MEM_COMMIT,PAGE_READWRITE);if(pDllAddr==NULL){CloseHandle(hProcess);return;}DWORDdwWriteNum=0;///注入欲望DLL在目标过程中申请的空间中写入文件的完整路径WriteProcessMemory(hProcess,pDllAddr,szDllName,nDllLen,&dwWriteNum);//获得LoadLibraryA()函数地址FARPROCpFunAddr=GetProcAddress(GetModuleHandle("kernel32.dll"),pFunName);//创建远程线程HANDLEhThread=CreateRemoteThread(hProcess,NULL,0,(LPTHREAD_START_ROUTINE)pFunAddr,pDllAddr,0,NULL);WaitForSingleObject(hThread,INFINITE);CloseHandle(hThread);CloseHandle(hProcess);}InjectDll()函数有 2 参数,即目标过程 ID 值和注入 DLL 文件的完整路径。代码中没有 LoadLibrary()函数的地址是 LoadLibraryA()函数地址。系统中没有 LoadLibrary()函数,有的只是 LoadLibraryA()和 LoadLibraryW()两个函数。这两个函数分别针对 ANSI 字符串和 UNICODE 字符串LoadLibrary()函数只是一个宏。在编写程序时,可以直接使用宏。如果你想获得 LoadLibrary()函数地址应明确指定为获取 LoadLibraryA()还是 LoadLibraryW()。
LoadLibrary()宏定义如下:
#ifdefUNICODE#defineLoadLibraryLoadLibraryW#else#defineLoadLibraryLoadLibraryA#endif//!UNICODE只要涉及到字符串的函数,就会有相应的函数ANSI版本和UNICODE版本;其他不涉及字符串的函数,没有ANSI版本和UNICODE版本差异。
为了测试DLL代码加载成功与否DllMain()函数中添加以下代码:
caseDLL_PROCESS_ATTACH:{MsgBox("!DLL_PROCESS_ATTACH!");break;}现在测试一下注入的效果,如图2和图3所示。
图2 DLL注入成功提示的文件
图3 在此过程中查看DLL列表确认装载成功
在图2中,弹出对话框是DLL程序在DLL_PROCESS_ATTACH它出现了。它的过程是notepad.exe。从图2中可以看出,弹出提示框的标题处是notepad.exe过程路径。图3是用工具检查过程中加载的DLL通过注入工具注入文件列表过注入工具注入DLL文件已加载notepad.exe在过程空间中。
如果要注入系统过程,由于过程权限的关系,无法成功注入。用于打开目标过程OpenProcess()函数,由于权限不足,无法打开过程并获得过程句柄。通过调整当前过程的权限,可以打开系统过程并获得过程句柄。Win8如果注入程序在更高版本上运行,则需要单击右键选择注入工具“作为管理员运行”可完成注入。
2. 卸载注入DLL文件
DLL如果如果应用于木马,危害很大。在这里注入卸载DLL注入卸载程序DLL程序的想法与注入的想法相同,代码的变化也很小。不同之处在于,当前的功能是卸载,而不是注入。
DLL卸载使用的API函数是FreeLiabrary()其定义如下:
BOOLFreeLibrary(HMODULEhModule//handletoDLLmodule);该函数的参数是要卸载的模块的句柄。
FreeLibrary()函数中使用的模块句柄可以通过Module32First()和Module32Next()获取两个函数。正在使用Module32First()和Module32Next()需要使用两个函数MODULEENTRY32模块的句柄保存在结构体中。MODULEENTRY32结构体的定义如下:
typedefstructtagMODULEENTRY32{DWORDdwSize;DWORDth32ModuleID;DWORDth32ProcessID;DWORDGlblcntUsage;DWORDProccntUsage;BYTE*modBaseAddr;DWORDmodBaseSize;HMODULEhModule;TCHARszModule[MAX_MODULE_NAME32 1];TCHARszExePath[MAX_PATH];}MODULEENTRY32;typedefMODULEENTRY32*PMODULEENTRY32;在结构体中hModule为模块句柄,szModule模块名称,szExePath是完整模块的名称(包括路径和模块名称)。
在卸载远程过程中DLL模块的代码如下:
VOIDCInjectDllDlg::UnInjectDll(DWORDdwPid,char*szDllName){if(dwPid==0||lstrlen(szDllName)==0){return;}HANDLEhSnap=CreateToolhelp32Snapshot(TH32CS_SNAPMODULE,dwPid);MODULEENTRY32me32;me32.dwSize=sizeof(me32);////找到匹配的过程名称BOOLbRet=Module32First(hSnap,&me32);while(bRet){if(lstrcmp(strupr(me32.szExePath),strupr(szDllName))==0){break;}bRet=Module32Next(hSnap,&me32);}CloseHandle(hSnap);char*pFunName="FreeLibrary";HANDLEhProcess=OpenProcess(PROCESS_ALL_ACCESS,FALSE,dwPid);if(hProcess==NULL){return;}FARPROCpFunAddr=GetProcAddress(GetModuleHandle("kernel32.dll"),pFunName);HANDLEhThread=CreateRemoteThread(hProcess,NULL,0,(LPTHREAD_START_ROUTINE)pFunAddr,me32.hModule,0,NULL);WaitForSingleObject(hThread,INFINITE);CloseHandle(hThread);CloseHandle(hProcess);}在卸载远程过程中DLL实现代码比DLL注入的代码要简单,这里就不多介绍了。
3. 无DLL的代码注入
DLL文件的注入和卸载已经完成,整个注入和卸载过程实际上是一次执行远程线程LoadLibrary()函数或FreeLibrary()函数。远程线程装载一个DLL文件,通过DllMain()调用DLL注入具体的功能代码DLL后就可以让DLL做很多事情。你能不依赖它吗?DLL为了完成特定的功能,文件直接将要执行的代码写入目标过程?答案是肯定的。
要在目标过程中完成某些功能,需要使用相关功能API函数,不同的API不同的函数实现DLL中。Kernel32.dll每个过程中文件的地址都是一样的,但不代表其他DLL每个过程中文件的地址都是一样的。这样,在目标过程中调用API必须使用函数LoadLibrary()函数和GetProcAddress()动态调用函数的每个函数API函数。使用你想要的API函数及API函数所在的DLL所有文件都包装在一个结构中,直接写入目标过程的空间中。同时,远程执行代码也直接写入目标过程的内存空间,最后调用CreateRemoteThread()函数可以运行。
远程线程通过实现一个简单的例子弹出一个提示对话框,但没有帮助DLL。使用本程序API函数已经在前面介绍过了。根据前面的步骤面的步骤定义的,定义如下:
#defineSTRLEN20typedefstruct_DATA{DWORDdwLoadLibrary;DWORDdwGetProcAddress;DWORDdwGetModuleHandle;DWORDdwGetModuleFileName;charUser32Dll[STRLEN];charMessageBox[STRLEN];charStr[STRLEN];}DATA,*PDATA;该结构体中保存了LoadLibraryA()、GetProcAddress()、GetModuleHandle()和GetModu leFileName()四个API函数地址。这四个API函数都属于Kernel32.dll可在注入前获得导出函数。User32Dll中保存“User32.dll”因为MessageBoxA()函数是由的User32.dll导出函数。Str保存的是通过MessageBoxA()函数弹出的字符串。
注入代码类似于上述注入代码,但需要在注入代码中定义结构变量并进行初始化,代码如下:
VOIDCNoDllInjectDlg::InjectCode(DWORDdwPid){///打开过程并获取过程句柄HANDLEhProcess=OpenProcess(PROCESS_ALL_ACCESS,FALSE,dwPid);if(hProcess==NULL){return;}DATAData={0};//获取kernel32.dll相关导出函数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(GetModuleHandle("kernel32.dll"),"GetModuleFileNameA");其他需要的DLL和导出函数lstrcpy(Data.User32Dll,"user32.dll");lstrcpy(Data.MessageBox,"MessageBoxA");//MessageBoxA()弹出字符串lstrcpy(Data.Str,"InjectCode!!!");///在目标过程中申请空间LPVOIDlpData=VirtualAllocEx(hProcess,NULL,sizeof(Data),MEM_COMMIT|MEM_RELEASE,PAGE_READWRITE);DWORDdwWriteNum=0;WriteProcessMemory(hProcess,lpData,&Data,sizeof(Data),&dwWriteNum);///用于保存代码的长度DWORDdwFunSize=0x4000;LPVOIDlpCode=VirtualAllocEx(hProcess,NULL,dwFunSize,MEM_COMMIT,PAGE_EXECUTE_READWRITE);WriteProcessMemory(hProcess,lpCode,&RemoteThreadProc,dwFunSize,&dwWriteNum);HANDLEhThread=CreateRemoteThread(hProcess,NULL,0(LPTHREAD_START_ROUTINE)lpCode,lpData,0,NULL);WaitForSingleObject(hThread,INFINITE);CloseHandle(hThread);CloseHandle(hProcess);}除了初始化结构变量外,上述注入代码还将线程函数代码写入目标过程空间的内存中。线程函数的代码如下:
DWORDWINAPIRemoteThreadProc(LPVOIDlpParam){PDATApData=(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))pData->dwGetModuleFileName;//加载User32.dllHMODULEhModule=MyLoadLibrary(pData->User32Dll);//获得MessageBoxA函数的地址MyMessageBox=(int(__stdcall*)(HWND,LPCTSTR,LPCTSTR,UINT))MyGetProcAddress(hModule,pData->MessageBox);charszModuleFileName[MAX_PATH]={0};MyGetModuleFileName(NULL,szModuleFileName,MAX_PATH);MyMessageBox(NULL,pData->Str,szModuleFileName,MB_OK);return0;}上面就是无DLL所有注入的代码,编译、连接和操作。启动记事本程序进行测试,但报错了。有什么问题吗?VC6默认编译是Debug在版本中,添加大量的调试信息。代码中不存在一些调试信息,而是其他信息DLL模块中。这样,当执行到调试相关代码时,就会访问不存在的代码DLL模块中的代码导致报错。
使用上述代码Release编译连接的方式,然后可以正确执行,如图4所示。
图4 Release成功注入编译 ***
编译的Debug版也可以进行无DLL只是实现了一点点不同。