Windows操作系统下,为了避免各个进程相互影响,每个进程地址空间都是被隔离的。所谓 “远程线程”,并不是跨计算机的,而是跨进程的。简单来说,就是进程A要在进程B中创建一个线程,这就叫远程线程。
公司主营业务:成都网站设计、做网站、成都外贸网站建设公司、移动网站开发等业务。帮助企业客户真正实现互联网宣传,提高企业的竞争能力。创新互联建站是一支青春激扬、勤奋敬业、活力青春激扬、勤奋敬业、活力澎湃、和谐高效的团队。公司秉承以“开放、自由、严谨、自律”为核心的企业文化,感谢他们对我们的高要求,感谢他们从不同领域给我们带来的挑战,让我们激情的团队有机会用头脑与智慧不断的给客户带来惊喜。创新互联建站推出镇康免费做网站回馈大家。
远程线程被木马、外挂等程序广泛使用,反病毒软件中也离不开远程线程的技术。技术应用的两面性取决于自己的个人行为意识,良性的技术学习对自己的人生发展是非常有好处的,就算谈不上好处,至少不会给自己带来不必要的麻烦。
关于远程线程的知识,本文介绍3个例子,分别是DLL的注入、卸载远程DLL和不依赖DLL进行代码注入。
1. DLL远程注入
木马或病毒编写的好坏取决于其隐藏的程度,而不在于其功能的多少。无论是木马还是病毒,都是可执行程序。如果它们是EXE文件的话,那么在运行时必定会产生一个进程,就很容易被发现。为了不被发现,在编写木马或病毒时可以选择将其编写为DLL文件。DLL文件的运行不会单独创建一个进程,它的运行被加载到进程的地址空间中,因此其隐蔽性相对较好。DLL文件如果不被进程加载又如何在进程的地址空间中运行呢?方式是强制让某进程加载DLL文件到其地址空间中去,这个强制的手段就是现在要介绍的远程线程。
创建远程线程的函数CreateRemoteThread()的定义如下:
- HANDLE CreateRemoteThread(
- HANDLE hProcess,
- LPSECURITY_ATTRIBUTES lpThreadAttributes,
- DWORD dwStackSize,
- LPTHREAD_START_ROUTINE lpStartAddress,
- LPVOID lpParameter,
- DWORD dwCreationFlags,
- LPDWORD lpThreadId
- );
该函数的功能是创建一个远程的线程。我们把CreateThread()函数和CreateRemoteThread()函数进行比较。对于CreateThread()函数来说,CreateRem oteThread()函数比其多了一个hProcess参数,该参数是指定要创建线程的进程句柄。其实CreateThread()函数的内容实现就是依赖于CreateRemoteThread()函数来完成的。CreateThread()函数的代码实现如下:
- /*
- * @implemented
- */
- HANDLE
- WINAPI
- CreateThread(LPSECURITY_ATTRIBUTES lpThreadAttributes,
- DWORD dwStackSize,
- LPTHREAD_START_ROUTINE lpStartAddress,
- LPVOID lpParameter,
- DWORD dwCreationFlags,
- LPDWORD lpThreadId)
- {
- /* 创建远程线程
- return CreateRemoteThread(NtCurrentProcess(),
- lpThreadAttributes,
- dwStackSize,
- lpStartAddress,
- lpParameter,
- dwCreationFlags,
- lpThreadId);
- }
在上面的代码中,NtGetCurrentProcess()函数的功能是获得当前进程的句柄。
CreateRemoteThread()函数是给其他进程创建线程使用的,其第一个参数是指定某进程的句柄,获取进程的句柄使用API函数OpenProcess(),该函数需要提供PID作为参数。
除了hProcess参数以外,剩余的关键参数就只有lpStartAddress和lpParameter两个了。lpStartAddress指定线程函数的地址,lpParameter指定传递给线程函数的参数。前面提到,每个进程的地址空间是隔离的,那么新创建的线程函数的地址也应该在目标进程中,而不应该在调用CreateRemoteThread()函数的进程中。同样,传递给线程函数的参数也应该在目标进程中。
如何让线程函数的地址在目标进程中呢?如何让线程函数的参数也可以传递到目标进程中呢?在讨论这个问题以前,先来考虑线程函数要完成的功能。这里主要完成的功能是注入一个DLL文件到目标进程中,那么线程函数的功能就是加载DLL文件。加载DLL文件使用的是LoadLibrary()函数。LoadLibrary()函数的定义:
- HMODULE LoadLibrary(
- LPCTSTR lpFileName
- );
- 看一下线程函数的定义格式,具体如下:
- DWORD WINAPI ThreadProc(
- LPVOID lpParameter
- );
比较两个函数可以发现,除了函数的返回值类型和参数类型以外,其函数格式是相同的。这里只考虑其相同的部分。因为其函数的格式相同,首先调用约定相同,都是WINAPI(也就是__stdcall方式);其次函数个数相同,都只有一个。那么,可以直接把LoadLibrary()函数作为线程函数创建到指定的进程中。LoadLibrary()的参数是欲加载的DLL文件的完整路径,只要在CreateRemoteThread()函数中赋值一个指向DLL文件完整路径的指针给LoadLibrary()函数即可。这样使用CreateRemoteThread()函数就可以创建一个远程线程了。不过,还有两个问题没有解决,首先是如何将LoadLibrary()函数的地址放到目标进程空间中让CreateRemoteThread()调用,其次是传递给LoadLibrary()函数的参数也需要在目标进程空间中,并且要通过CreateRemoteThread()函数指定给LoadLibrary()函数。
首先解决第1个问题,即如何将LoadLibrary()函数的地址放到目标进程空间中。LoadLibrary()函数是系统中的Kernel32.dll的导出函数,Kernel32.dll这个DLL文件在任何进程中的加载位置都是相同的,也就是说,LoadLibrary()函数的地址在任何进程中的地址都是相同的。因此,只要在进程中获得LoadLibrary()函数的地址,那么该地址在目标进程中也可以使用。CreateRemoteThread()函数的线程地址参数直接传递LoadLibrary()函数的地址即可。
其次解决第2个问题,即如何将欲加载的DLL文件完整路径写入目标进程中。这需要借助WriteProcessMemory()函数,其定义如下:
- BOOL WriteProcessMemory(
- HANDLE hProcess, // handle to process
- LPVOID lpBaseAddress, // base of memory area
- LPVOID lpBuffer, // data buffer
- DWORD nSize, // number of bytes to write
- LPDWORD lpNumberOfBytesWritten // number of bytes written
- );
该函数的功能是把lpBuffer中的内容写到进程句柄是hProcess进程的lpBaseAddress地址处,写入长度为nSize。
参数说明如下。
hProcess:该参数是指定进程的进程句柄。
lpBaseAddress:该参数是指定写入目标进程内存的起始地址。
lpBuffer:该参数是要写入目标进程内存的缓冲区起始地址。
nSize:该参数是指定写入目标内存中的缓冲区的长度。
lpNumberOfBytesWritten:该参数用于接收实际写入内容的长度。
该函数的功能非常强大,比如在破解方面,用该函数可以实现一个“内存补丁”;在开发方面,该函数可以用于修改目标进程中指定的值(比如游戏修改器可以修改游戏中的钱、红、蓝等)。
使用该函数可以把DLL文件的完整路径写入到目标进程的内存地址中,这样就可以在目标进程中用LoadLibrary()函数加载指定的DLL文件了。解决了上面的两个问题,还有第3个问题需要解决。WriteProcessMemory()函数的第2个参数是指定写入目标进程内存的缓冲区起始地址。这个地址在目标进程中,那么这个地址在目标进程的哪个位置呢?目标进程中的内存块允许把DLL文件的路径写进去吗?
第3个要解决的问题是如何确定应该将DLL文件的完整路径写入目标进程的哪个地址。对于目标进程来说,事先是不会准备一块地址让用户进行写入的,用户能做的是自己在目标进程中申请一块内存,然后把DLL文件的路径进行写入,写入在目标进程新申请到的内存空间中。在目标进程中申请内存的函数是VirtualAllocEx(),其定义如下:
- LPVOID VirtualAllocEx(
- HANDLE hProcess,
- LPVOID lpAddress,
- SIZE_T dwSize,
- DWORD flAllocationType,
- DWORD flProtect
- );
VirtualAllocEx()函数的参数说明如下。
hProcess:该参数是指定进程的进程句柄。
lpAddress:该参数是指在目标进程中申请内存的起始地址。
dwSize:该参数是指在目标进程中申请内存的长度。
flAllocationType:该参数指定申请内存的状态类型。
flProtect:该参数指定申请内存的属性。
该函数的返回值是在目标进程申请到的内存块的起始地址。
到此,关于编写一个DLL注入的所有知识都已经具备了。现在开始编写一个DLL注入的工具,其界面如图1所示。
图1 DLL注入/卸载器
该工具有2个作用,分别是注入DLL和卸载被注入的DLL。关于卸载被注入的DLL的功能,将在后面进行介绍。在界面上要求输入两部分内容,第1部分是欲注入的DLL文件的完整路径(一定要是完整路径),第2部分是进程的名称。
首先看一下关于界面的操作,代码如下:
- void CInjectDllDlg::OnBtnInject()
- {
- // 添加处理程序代码
- char szDllName[MAX_PATH] = { 0 };
- char szProcessName[MAXBYTE] = { 0 };
- DWORD dwPid = 0;
- GetDlgItemText(IDC_EDIT_DLLFILE, szDllName, MAX_PATH);
- GetDlgItemText(IDC_EDIT_PROCESSNAME, szProcessName, MAXBYTE);
- // 由进程名获得 PID
- dwPid = GetProcId(szProcessName);
- // 注入 szDllName 到 dwPid
- InjectDll(dwPid, szDllName);
- }
代码中调用了另外两个函数,第1个是由进程名获得PID的函数,第2个是用于DLL注入的函数。GetProcId()函数的代码如下:
- DWORD CInjectDllDlg::GetProcId(char *szProcessName)
- {
- BOOL bRet;
- PROCESSENTRY32 pe32;
- HANDLE hSnap;
- hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL);
- pe32.dwSize = sizeof(pe32);
- bRet = Process32First(hSnap, &pe32);
- while ( bRet )
- {
- // strupr()函数是将字符串转化为大写
- if ( lstrcmp(strupr(pe32.szExeFile),strupr(szProcessName)) == 0 )
- {
- return pe32.th32ProcessID;
- }
- bRet = Process32Next(hSnap, &pe32);
- }
- return 0;
- } +
InjectDll()函数的代码如下:
- VOID CInjectDllDlg::InjectDll(DWORD dwPid, char *szDllName)
- {
- if ( dwPid == 0 || lstrlen(szDllName) == 0 )
- {
- return ;
- }
- char *pFunName = "LoadLibraryA";
- // 打开目标进程
- HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS,FALSE, dwPid);
- if ( hProcess == NULL )
- {
- return ;
- }
- // 计算欲注入 DLL 文件完整路径的长度
- int nDllLen = lstrlen(szDllName) + sizeof(char);
- // 在目标进程申请一块长度为 nDllLen 大小的内存空间
- PVOID pDllAddr = VirtualAllocEx(hProcess,NULL, nDllLen,MEM_COMMIT,PAGE_READWRITE);
- if ( pDllAddr == NULL )
- {
- CloseHandle(hProcess);
- return ;
- }
- DWORD dwWriteNum = 0;
- // 将欲注入 DLL 文件的完整路径写入在目标进程中申请的空间内
- WriteProcessMemory(hProcess, pDllAddr, szDllName,nDllLen, &dwWriteNum);
- // 获得 LoadLibraryA()函数的地址
- FARPROC pFunAddr = GetProcAddress(GetModuleHandle("kernel32.dll"),pFunName);
- // 创建远程线程
- HANDLE hThread = 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()宏定义如下:
- #ifdef UNICODE
- #define LoadLibrary LoadLibraryW
- #else
- #define LoadLibrary LoadLibraryA
- #endif // !UNICODE
只要涉及字符串的函数,都会有相应的ANSI版本和UNICODE版本;其余不涉及字符串的函数,没有ANSI版本和UNICODE版本的区别。
为了测试DLL加载是否成功,在代码的DllMain()函数中加入如下代码:
- case DLL_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(),其定义如下:
- BOOL FreeLibrary(
- HMODULE hModule // handle to DLL module
- );
该函数的参数是要卸载的模块的句柄。
FreeLibrary()函数使用的模块句柄可以通过Module32First()和Module32Next()两个函数获取。在使用Module32First()和Module32Next()两个函数的时候,需要用到MODULEENTRY32结构体,该结构体中保存了模块的句柄。MODULEENTRY32结构体的定义如下:
- typedef struct tagMODULEENTRY32 {
- DWORD dwSize;
- DWORD th32ModuleID;
- DWORD th32ProcessID;
- DWORD GlblcntUsage;
- DWORD ProccntUsage;
- BYTE * modBaseAddr;
- DWORD modBaseSize;
- HMODULE hModule;
- TCHAR szModule[MAX_MODULE_NAME32 + 1];
- TCHAR szExePath[MAX_PATH];
- } MODULEENTRY32;
- typedef MODULEENTRY32 *PMODULEENTRY32;
该结构体中的hModule为模块的句柄,szModule为模块的名称,szExePath是完整的模块的名称(所谓完整,包括路径和模块名称)。
卸载远程进程中DLL模块的代码如下:
- VOID CInjectDllDlg::UnInjectDll(DWORD dwPid, char *szDllName)
- {
- if ( dwPid == 0 || lstrlen(szDllName) == 0 )
- {
- return ;
- }
- HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE,dwPid);
- MODULEENTRY32 me32;
- me32.dwSize = sizeof(me32);
- // 查找匹配的进程名称
- BOOL bRet = Module32First(hSnap, &me32);
- while ( bRet )
- {
- if ( lstrcmp(strupr(me32.szExePath),
- strupr(szDllName)) == 0 )
- {
- break;
- }
- bRet = Module32Next(hSnap, &me32);
- }
- CloseHandle(hSnap);
- char *pFunName = "FreeLibrary";
- HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS,FALSE, dwPid);
- if ( hProcess == NULL )
- {
- return ;
- }
- FARPROC pFunAddr = GetProcAddress(GetModuleHandle("kernel32.dll"),pFunName);
- HANDLE hThread = 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函数在前面都已经介绍过了。根据前面的步骤先来定义一个结构体,其定义如下:
- #define STRLEN 20
- typedef struct _DATA
- {
- DWORD dwLoadLibrary;
- DWORD dwGetProcAddress;
- DWORD dwGetModuleHandle;
- DWORD dwGetModuleFileName;
- char User32Dll[STRLEN];
- char MessageBox[STRLEN];
- char Str[STRLEN];
- }DATA, *PDATA;
该结构体中保存了LoadLibraryA()、GetProcAddress()、GetModuleHandle()和GetModu leFileName()四个API函数的地址。这四个API函数都属于Kernel32.dll的导出函数,因此可以在注入前进行获取。User32Dll中保存“User32.dll”字符串,因为MessageBoxA()函数是由User32.dll的导出函数。Str中保存的是通过MessageBoxA()函数弹出的字符串。
注入代码类似于前面介绍的注入代码,不过需要在注入代码中定义一个结构体变量,并进行相应的初始化,代码如下:
- VOID CNoDllInjectDlg::InjectCode(DWORD dwPid)
- {
- // 打开进程并获取进程句柄
- HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS,FALSE, dwPid);
- if ( hProcess == NULL )
- {
- return ;
- }
- DATA Data = { 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, "Inject Code !!!");
- // 在目标进程申请空间
- LPVOID lpData = VirtualAllocEx(hProcess, NULL, sizeof(Data),
- MEM_COMMIT | MEM_RELEASE,PAGE_READWRITE);
- DWORD dwWriteNum = 0;
- WriteProcessMemory(hProcess, lpData, &Data,
- sizeof(Data), &dwWriteNum);
- // 在目标进程空间申请的用于保存代码的长度
- DWORD dwFunSize = 0x4000;
- LPVOID lpCode = VirtualAllocEx(hProcess, NULL, dwFunSize,
- MEM_COMMIT,PAGE_EXECUTE_READWRITE);
- WriteProcessMemory(hProcess, lpCode, &RemoteThreadProc,
- dwFunSize, &dwWriteNum);
- HANDLE hThread = CreateRemoteThread(hProcess, NULL, 0
- (LPTHREAD_START_ROUTINE)lpCode,lpData, 0, NULL);
- WaitForSingleObject(hThread, INFINITE);
- CloseHandle(hThread);
- CloseHandle(hProcess);
- }
上面的注入代码除了对结构体变量初始化外,还将线程函数代码写入目标进程空间的内存中。线程函数的代码如下:
- 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))
- pData->dwGetModuleFileName;
- // 加载 User32.dll
- HMODULE hModule = MyLoadLibrary(pData->User32Dll);
- // 获得 MessageBoxA 函数的地址
- MyMessageBox = (int (__stdcall *)(HWND, LPCTSTR, LPCTSTR, UINT))
- MyGetProcAddress(hModule, pData->MessageBox);
- char szModuleFileName[MAX_PATH] = { 0 };
- MyGetModuleFileName(NULL, szModuleFileName, MAX_PATH);
- MyMessageBox(NULL, pData->Str, szModuleFileName, MB_OK);
- return 0;
- }
上面就是无DLL注入的全部代码,编译连接并运行它。启动一个记事本程序来进行测试,可惜报错了。问题出在哪里呢?VC6的默认编译是Debug版本,这样会加入很多调试信息。而某些调试信息并不存在于代码中,而是在其他DLL模块中。这样,当执行到调试相关的代码时会访问不存在的DLL模块中的代码,就导致了报错。
将以上代码使用Release方式进行编译连接,然后可以无误地执行,如图4所示。
图4 Release方式下编译注入成功
编译的Debug版也可以进行无DLL的注入,只是实现起来略有不同。
标题名称:网络安全编程:远程线程编程
分享URL:http://www.shufengxianlan.com/qtweb/news26/500076.html
网站建设、网络推广公司-创新互联,是专注品牌与效果的网站制作,网络营销seo公司;服务项目有等
声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 创新互联