《windows核心编程系列》十九谈谈使用远程线程来注入DLL。
windows内的各个进程有各自的地址空间。它们相互独立互不干扰保证了系统的安全性。但是windows也为调试器或是其他工具设计了一些函数,这些函数可以让一个进程对另一个进程进行操作。虽然他们是为调试器设计的,但是任何应用程序都可以调用它们 。接下来我们来谈谈使用远程线程来注入DLL。
从根本上说,DLL注入就是将某一DLL注入到某一进程的地址空间。该进程中的一个线程调用LoadLibrary来载入想要注入的DLL。由于我们不能直接控制其他进程内的线程,因此我们必须在其他进程内创建一个我们自己的线程。我们可以对新创建的线程加以控制,让他调用LoadLibrary来载入DLL。windows提供了一个函数,可以让我们在其他进程内创建一个线程:
在其他进程内创建的线程被称为:远程线程,该进程被称为远程进程。
- <span style="font-size:18px;"> HANDLE WINAPI CreateRemoteThread(
- __in HANDLE hProcess,
- __in LPSECURITY_ATTRIBUTES lpThreadAttributes,
- __in SIZE_T dwStackSize,
- __in LPTHREAD_START_ROUTINE lpStartAddress,
- __in LPVOID lpParameter,
- __in DWORD dwCreationFlags,
- __out LPDWORD lpThreadId
- );
- </span>
很容易吧。该函数除了第一个参数hProcess,标识要创建的线程所属的进程外,其他参数与CreateThread的参数完全相同。
参数lpstartAddress是线程函数的地址。由于是在远程进程创建的,所以该函数一定必须在远程进程的地址空间内。
现在知道了如何在另一个进程创建一个线程,那么我们如何让该线程载入我们的DLL呢?
先别急着让线程调用LoadLibrary载入DLL,现在要考虑的是如何让线程运行起来,即为线程选择线程函数。因为线程是在其他进程内运行的,所以该线程函数必须符合以下条件:
1:该函数符合线程函数的原型,
2:存在于远程线程地址空间内。
仔细分析下,远程线程的任务只有一个。就是调用LoadLibray加载DLL。
既然如此可不可以让LoadLibrary直接作为线程函数呢?
先看第一个条件:函数签名是否相同。你还别说,除了参数类型有点不一样外,其他一摸一样的。由于参数类型可以通过强转实现,所以第一个条件是满足的。
再看第二个条件:该函数是否在远程进程地址空间内。用屁股想一下我们都知道肯定在。另外他们都有相同的函数调用约定,也就是说他们的参数传递是从右到左压栈的,有子程序平衡堆栈。OK,太棒了。使用LoadLibrary作为线程函数真的是太方便了 。
难道是微软故意为我们这样设计的?无从知晓。但在这里要谢谢发现这一技巧的牛人。
查看MSDN可以发现LoadLibrary并不是一个API,它其实是一个宏。
在WinBase.h可以发现这样一句话:
#ifdef UNICODE
#define LoadLibrary LoadLibraryW
#else
#define LoadLibrary LoadLibraryA
#endif
明白了吗?实际上有两个Load*函数,他们的唯一区别就是参数类型不同。如果DLL文件名是以ANSI形式保存的,我们就必须调用LoadLibraryA,如果是UNICODE形式保存的我们就必须调用LoadLibraryW。
接下来我们要做的事情就简单了,只需要调用CreateThread函数,传给标识线程函数的参数LoadLibraryA或是LoadLibraryW。然后将我们要远程进程加载的DLL的路径名的地址作为参数传给它。哈哈,很兴奋吧!一切都是那么的顺!
不要高兴的太早。你就没发现哪有不对的地方吗?传给线程函数的参数是DLL路径名的地址。但是该地址是在我们进城内的。如果远程进程引用此地址的数据,很可能会导致访问违规,远程进程被终止。怎么样很严重吧。但这也给我们一个破坏其他进程的思路。哈哈。自己发挥吧!
为了解决这个问题,我们应该将该字符串放到远程地址的地址空间去。有没有相应的函数呢?当然有!
首先应该在远程进程的地址空间分配一块儿内存。如何做呢!或许你很熟悉VirtualAlloc,但是他没有这个功能。他兄弟VirtualAllocEx可以解决这个问题。看原型:
- <span style="font-size:18px;"> LPVOID WINAPI VirtualAllocEx(
- __in HANDLE hProcess,
- __in_opt LPVOID lpAddress,
- __in SIZE_T dwSize,
- __in DWORD flAllocationType,
- __in DWORD flProtect
- );
- </span>
hProcess应该知道是干嘛的吧。他就是标识你要想在那个进程的地址空间申请内存的进程句柄。其他参数跟VirtualAlloc完全相同。此处不再介绍。
当然知道如何申请还有知道如何释放!看他搭档:VirtualFreeEx
- <span style="font-size:18px;"> BOOL WINAPI VirtualFreeEx(
- __in HANDLE hProcess,
- __in LPVOID lpAddress,
- __in SIZE_T dwSize,
- __in DWORD dwFreeType
- );
- </span>
与VirtualFree的区别这只是多一个进程句柄。
现在申请空间的任务完成了,要怎么样将本进程的数据复制到另外一个进程呢?可以使用ReadProcessMemory和WriteProcessMemory
- <span style="font-size:18px;"> BOOL WINAPI ReadProcessMemory(
- __in HANDLE hProcess,
- __in LPCVOID lpBaseAddress,
- __out LPVOID lpBuffer,
- __in SIZE_T nSize,
- __out SIZE_T *lpNumberOfBytesRead
- );
- </span>
- <span style="font-size:18px;"> BOOL WINAPI WriteProcessMemory(
- __in HANDLE hProcess,
- __in LPVOID lpBaseAddress,
- __in LPCVOID lpBuffer,
- __in SIZE_T nSize,
- __out SIZE_T *lpNumberOfBytesWritten
- );
- </span>
由于他们签名类似,此处放在一块介绍。
hProcess是用来标识远程进程的。
lpBaseAddress是在远程进程地址空间的地址,是VirtualAllocEx的返回值。
lpBuffer是在本进程的内存地址。此处也就是DLL路径名的地址。
nSize为要传输的字符串。
lpNumberOfByteRead和lpNumberOfByteWrite为实际传输的字节数。
注意:当调用WriteProcessMemory时有时会导致失败。此时可以尝试调用VirtualProtect来修改写入页面的属性,写入之后再改回来。
到此为止,看起来没啥东西了,但是还有一个比较隐晦的问题,如果不对PE文件格式和DLL加载的方式有所了解的话是很难发现的。
我们知道导入函数的真实地址是在DLL加载的时候获得的。加载程序从导入表取得每一个导入函数的函数名(字符串),然后在被加载到进程地址空间的DLL中查询之后,填到导入表的相应位置(IAT)的。也就是说在运行之前我们并不知道导入函数的地址(当然模块绑定过得除外)。那么程序代码中是如何表示对导入函数的调用呢?有没有想过这个问题呢。
你或许觉得应该是:CALL DWORD PTR[004020108] ( [ ]内仅表示导入函数地址,无实际意义)。
由于程序的代码在经过编译连接之后就已经确定,而导入表的地址如00402010是在程序运行的时候获得的。所以程序在调用导入函数的时候并不能这样实现。那到底是如何实现的呢?
[ ]内有一个确定的地址这是毋庸置疑的,但是他的值并不是导入函数的地址,而是一个子程序的地址。该子程序被称为转换函数(thunk)。这些转换函数用来跳转到导入函数。当程序调用导入函数时,先会调用转换函数,转换函数从导入表的IAT获得导入函数的真实地址时在调用相应地址。
所以对导入函数的调用形如如下的形式:
- CALL 00401164 ;转换函数的地址。
- 。。。。。。
- :00401164
- 。。。。。
- CALL DWORD PTR [00402010] ;调用导入函数。
分析到这儿,我们也可以明白为什么在声明一个导出函数的时候要加上_decllpec(dllimport)前缀。
原因是:编译器无法区分应用程序是对一般函数的调用还是对导入函数的调用。当我们在一个函数前加上此前缀就是告诉编译器此函数来自导入函数,编译器就会产生如上的指令。而不是CALL XXXXXXXX的形式。
所以在写一个输出函数的时候一定要在函数声明前加上修饰符:_decllpec(dllimport)。
言归正传.之所以说这么多,就是因为我们传给CreateRemoteThread的线程函数LoadLibrary*,会被解析成我们进程内的转换函数的地址。如果把这个转换函数的地址作为线程函数的起始地址很可能导致访问违规。解决方法是:强制代码略过转换函数而直接调用LoadLibrary*.
这可以通过GetProAddress来实现。
- <span style="font-size:18px;"> FARPROC WINAPI GetProcAddress(
- __in HMODULE hModule,
- __in LPCSTR lpProcName
- );
- </span>
hModule是模块句柄。标志某一模块。
lpProcName是该模块内某一函数的函数名。
它返回该函数在模块所属进程地址空间的地址。
如GetProcAddress(GetModuleHandle("Kernel.dll","LoadLibraryW"));
此语句取得LoadLibrary在Kernel.dll所在进程空间的真实地址。注意此时仅仅是取得在本进程Kernel.dll的地址和LoadLibraryW的地址。难道在远程进程内也是一样吗?
《windows核心编程》第五版 589页第三段中说,”从作者的经验来看,Kernel.dll映射到每个进程的地址都是相同的。“基于此,我们可以认为,我们调用此语句是取得了Kernel.dll和LoadLibraryW在远程地址空间的地址。
下面来介绍一个例子。通过远程线程向explorer.exe进程注入DLL。
explorer.exe:资源管理器进程。随系统启动而启动,且一直运行。因此它经常被用来被当做远程线程的寄主。
步骤:
1:获得explorer进程的句柄。
这可以通过调用CreatehlpSnapshot,获得此时系统的一个快照。然后遍历该快照。找到进程名称为explorer.exe的进程。并得到起进程对象句柄。
看代码:
- <span style="font-size:18px;"> PROCESSENTRY32 pe32;
- pe32.dwSize=sizeof(pe32);
- HANDLE hSnapshot=CreateToolhelp32Snapshot(TH32CS_SNAPALL,0);
- int ret=Process32First(hSnapshot,&pe32);
- CString a;
- UpdateData();
- if(-1==m_processToFind.Find(".exe",0))
- m_processToFind+=".exe";
- while(ret)
- {
- if(pe32.szExeFile==m_processToFind)
- {
- a.Format("进程:%s找到,它的进程ID为:%d",m_processToFind,pe32.th32ProcessID);
- MessageBox(a);
- break ;
- }
- ret=Process32Next(hSnapshot,&pe32);
- }</span>
- <span style="font-size:18px;">HANDLE WINAPI CreateToolhelp32Snapshot(
- __in DWORD dwFlags,
- __in DWORD th32ProcessID
- );
- </span>
该函数用于获取指定进程的快照,以及该进程使用的堆,线程等。
dwFlags用来表示此快照中包含的项目。具体参考MSDN。
此处传入TH32CS_SNAPALL,表示此快照包括系统中所有的进程和线程,以及在th32ProcessID中指定的进程的各模块和线程的信息。
th32ProessID指定要包括到此快照的进程ID,当传入0时表示当前进程。
执行成功返回快照句柄。否则返回INVALID_HANDLE_VALUE。可以调用GetLastError查看更多错误信息。
- <span style="font-size:18px;">BOOL WINAPI Process32First(
- __in HANDLE hSnapshot,
- __inout LPPROCESSENTRY32 lppe
- );
- </span>
BOOL WINAPI Process32Next( __in HANDLE hSnapshot, __out LPPROCESSENTRY32 lppe );
以上两个函数,用于在CreateHlpSnapshot中遍历各项。用法请参考上例,其他信息请参考MSDN。
2:获得explorer的进程ID之后,还要调用OpenProcess来获得该进程的句柄。函数执行成功返回进程句柄。
- <span style="font-size:18px;"> HANDLE hProcess=OpenProcess(PROCESS_ALL_ACCESS,0,pe32.th32ProcessID);</span>
3:在explorer的地址空间中申请存储要注入的DLL的路径名的空间。
- <span style="font-size:18px;"> PVOID addr=VirtualAllocEx(hProcess,NULL,50,MEM_COMMIT, PAGE_READWRITE);
- if(addr==NULL)
- {
- CString a;
- int ret=GetLastError();
- a.Format("在远程进程申请空间失败!错误码为:%d",ret);
- MessageBox(a);
- }
- else
- {
- MessageBox("远程进程地址空间中申请空间成功!");
- } </span>
4:将路径名写入在explorer进程申请的空间。
- <span style="font-size:18px;">char path[50]="F:\\injectDll.dll";
- int retval=WriteProcessMemory(hProcess,addr,(LPVOID)path,sizeof(path),NULL);
- if(retval)
- {
- MessageBox("写入成功!");
- }
- else
- MessageBox("写入失败!");
- </span>
5:创建远程线程。//获得LoadLibraryA在远程进程中的地址。(与本进程的地址相同。)
- <span style="font-size:18px;">PTHREAD_START_ROUTINE pfnThread=(PTHREAD_START_ROUTINE)
- GetProcAddress(GetModuleHandle("kernel32.DLL"),"LoadLibraryA");</span>
- <span style="font-size:18px;">HANDLE hRemoteThread=CreateRemoteThread(
- hProcess,//in HANDLE hProcess,
- NULL,
- 0,//__in SIZE_T dwStackSize,
- pfnThread,
- addr,
- 0,
- NULL); </span>
- <span style="font-size:18px;">if(hRemoteThread==INVALID_HANDLE_VALUE)
- {
- MessageBox("远程线程穿件失败!");
- }
- else
- {
- MessageBox("远程线程创建成功!");
- }
- </span>
6:创建要注入到远程进程的DLL。此处不再介绍。可以参考《windows核心编程系列》谈谈DLL基础。如果在注入的DLL创建一个线程,就可以执行我们想让它做的工作。
比如监控某程序的运行,一旦程序运行,就将另一个DLL加载到此进程。此DLL会挂在全局钩子,获得用户键盘的动作。这也是键盘盗取qq的原理。可以自己发挥啊。
到此为止,关于远程线程就介绍完毕。
参考自《windows核心编程》第五版 第二十二章 ,《加密与解密》第二版 段钢著,第十章
以上仅仅在参考各书籍的基础之上加以总结。如有错误,请不吝赐教。