四、使用远程线程来插入DLL
插入DLL的第三种方法是使用远程线程。这种方法具有更大的灵活性。
原理
特点
基本操作步骤
执行的操作步骤:
- 使用VirtualAllocEx函数,分配远程进程的地址空间中的内存。
- 使用WriteProcessMemory函数,将D L L的路径名拷贝到第一个步骤中已经分配的内存中。
- 使用GetProcAddress函数,获取 LoadLibraryA或LoadLibraryW函数的实地址(在Kernel32.dll中)。
- 使用CreateRemoteThread函数,在远程进程中创建一个线程,它调用正确的 LoadLibrary函数,为它传递第一个步骤中分配的内存的地址。这时, DLL已经被插入远程进程的地址空间中,同时 DLL的DllMain函数接收到一个DLL_PROCESS_ATTACH通知,并且能够执行需要的代码。当 DllMain函数返回时,远程线程从它对LoadLibrary的调用返回到 BaseThreadStart函数(第 6章中已经介绍)。然后BaseThreadStart调用ExitThread,使远程线程终止运行。现在远程进程拥有第一个步骤中分配的内存块,而 DLL则仍然保留在它的地址空间中。若要将它删除,需要在远程线程退出后执行下面的步骤:
- 使用VirtualFreeEx函数,释放第一个步骤中分配的内存。
- 使用GetProcAddress函数,获得FreeLibrary函数的实地址(在Kernel32.dll中)。
- 使用CreateRemoteThread函数,在远程进程中创建一个线程,它调用 FreeLibrary函数,传递远程DLL的HINSTANCE。
OpenProcess函数
OpenProcess 函数用来打开一个已存在的进程对象,并返回进程的句柄。
HANDLE OpenProcess( DWORD dwDesiredAccess, //渴望得到的访问权限(标志) BOOL bInheritHandle, // 是否继承句柄 DWORD dwProcessId// 进程标示符 );
dwDesiredAccess
:获取的权限,可分为以下几种
PROCESS_ALL_ACCESS:获取所有权限
PROCESS_CREATE_PROCESS:创建进程
PROCESS_CREATE_THREAD:创建线程
PROCESS_DUP_HANDLE:使用DuplicateHandle()函数复制一个新句柄
PROCESS_QUERY_INFORMATION:获取进程的令牌、退出码和优先级等信息
PROCESS_QUERY_LIMITED_INFORMATION:获取进程特定的某个信息
PROCESS_SET_INFORMATION:设置进程的某种信息
PROCESS_SET_QUOTA:使用SetProcessWorkingSetSize函数设置内存限制
PROCESS_SUSPEND_RESUME:暂停或者恢复一个进程
PROCESS_TERMINATE:使用Terminate函数终止进程
PROCESS_VM_OPERATION:在进程的地址空间执行操作
PROCESS_VM_READ:使用ReadProcessMemory函数在进程中读取内存
PROCESS_VM_WRITE:使用WriteProcessMemory函数在进程中写入内存
SYNCHRONIZE:使用wait函数等待进程终止
bInheritHandle
:TRUE或者FALSE
dwProcessId
:pid
VirtualAllocEx函数
分配远程进程的地址空间中的内存
WriteProcessMemory函数
GetProcAddress函数
获取 LoadLibraryA或LoadLibraryW函数的实地址(在Kernel32.dll中)
CreateRemoteThread函数
能够创建一个在其它进程地址空间中运行的线程(也称为创建远程线程)。
HANDLE CreateRemoteThread( HANDLE hProcess, LPSECURITY_ATTRIBUTES lpThreadAttributes, SIZE_T dwStackSize, LPTHREAD_START_ROUTINE lpStartAddress, LPVOID lpParameter, DWORD dwCreationFlags, LPDWORD lpThreadId );
hProcess
:进程句柄
lpThreadAttributes
: 线程安全描述字,指向SECURITY_ATTRIBUTES结构的指针
dwStackSize
:线程栈大小,以字节表示
lpStartAddress
: 一个LPTHREAD_START_ROUTINE类型的指针,指向在远程进程中执行的函数地址
lpParameter
: 传入参数
dwCreationFlags
:创建线程的其它标志
lpThreadId
:[输出] 线程身份标志,如果为NULL,则不返回
返回值
:成功返回新线程句柄,失败返回NULL,并且可调用GetLastError获得错误值。
4.1 Inject Library示例应用程序
使用任务管理获取进程的I D。使用这个I D,该程序将设法通过调用 OpenProcess函数来打开正在运行的进程的句柄,申请相应的访问权。
原理
InjLib.exe应用程序使用CreateRemoteThread函数来插入ImgWalk.DLL
过程
- 使用界面输入的ProcesssId,然后获取ImgWalk.dll文件位置。
- 调用InjectLib:InjectLib内部调用OpenProcess获得ProcessId的句柄hProcess
- 通过进程句柄hProcess,
调用VirtualAllocEx,分配远程进程的地址空间中的内存 - 使用WriteProcessMemory函数,将D L L的路径名拷贝到第一个步骤中已经分配的内存中。
- 调用GetProcAddress
获取 LoadLibraryA或LoadLibraryW函数的实地址(在Kernel32.dll中) - CreateRemoteThread,创建远程线程。
- 结束InjectLib后释放句柄hProcess,线程hThread,内存。
- 注入成功后,调用EjectLib,同InjectLib,只不过获得FreeLibrary地址,然后通过CreateRemoteThread调用FreeLibrary。
4.2 Image Walk DLL
ImgWalk.dll 是个D L L,一旦它被插入进程的地址空间,就能够报告该进程正在使用的所有 DLL。
原理
ImgWalk遍历进程的地址空间,查找已经映射的文件映像,反复调用 Virtual Query函数,填入一个MEMORY_BASIC_ INFORMATION结构中。运用循环的每个重复操作, ImgWalk找出一个文件路径名,并与一个字符串相连接。该字符串显示在消息框中。
五、使用特洛伊DLL来插入DLL
原理
利用自己伪造的同名DLL取代软件内的将要加载的Xyz.DLL。
特点
- 在你的Xyz.dll中,输出的全部符号必须与原始的Xyz.dll输出的符号相同
实现方法 :
- 使用函数转发器
注意:
虽然函数转发器使你能够非常容易地挂接某些函数,你应该避免使用这种方法,因为它不具备版本升级能力。例如,如果你取代了一个系统 D L L, 而M i c r o s o f t在将来增加了一些新函数,那么你的 D L L将不具备它们的函数转发器。引用这些新函数的应用程序将无法加载和执行。
- 改变应用程序的.exe模块的输入节
注意:。
输入节只包含模块需要的 D L L的名字。你可以仔细搜索文件中的这个输入节,并且将它改变,使加载程序加载你自己的 D L L。这种方法相当不错,但是必须要非常熟悉. e x e和D L L文件的格式。
六、将DLL作为调试程序来插入
原理
调试程序能够对被调试的进程执行特殊的操作。
当被调试进程加载时,在被调试进程的地址空间已经作好准备,但是被调试进程的主线程尚未执行任何代码之前,系统将自动将这个情况通知调试程序时,调试程序可以强制将某些代码插入被调试进程的地址空间中(比如使用WriteProcessMemory函数来插入),然后使被调试进程的主线程执行该代码。
特点
- 这种方法要求你对被调试线程的 CONTEXT结构进行操作,意味着必须编写特定 CPU的代码。必须修改你的源代码,使之能够在不同的 C P U平台上正确地运行。另外,必须对你想让被调试进程执行的机器语言指令进行硬编码。而且调试程序与它的被调试程序之间必须存在固定的关系。
- 如果调试程序终止运行,Windows将自动撤消被调试进程。而你则无法阻止它。
七、用Windows 98上的内存映射文件插入代码
原理
在Windows 98上插入你自己的代码是非常简单的。在 Windows 98上运行的所有 3 2位 Wi n d o w s应用程序均共享同样的最上面的2 GB地址空间。如果你分配这里面的某些存储器,那么该存储器在每个进程的地址空间中均可使用。若要分配 2 GB以上的存储器,只要使用内存映射文件(第 1 7章已经介绍)。可以创建一个内存映射文件,然后调用 MapViewOfFile函数,使它显示出来。然后将数据填入你的地址空间区域(这是所有进程地址空间中的相同区域)。
特点
- 必须使用硬编码的机器语言来进行填入数据到地址空间区域,很难移植到别的CPU平台。
- 必须让其他进程中的线程来执行内存映射文件中的代码。要做到这一点,需要某种方法来控制远程进程中的线程。CreateRemoteThread函数能够很好地执行这个任务,可惜Windows 98不支持该函数的运行。
八、用CreateProcess插入代码
原理
通过当前进程来生成目标进程,在目标进程作为子进程启动之前先将其挂起。同时会得到其主线程的句柄。通过这个句柄可以对线程执行的代码进行修改。
方法
让一个进程对其子进程的主线程进行控制的一种方法:
- 让进程生成一个被挂起的子进程
- 从exe模块的文件头中得到主线程的起始内存地址
- 将位于该内存地址处的机器指令保存起来
- 强制将一些手动编写的机器指令写入到该内存地址处。这些指令应该调用一个LoadLibrary来载入一个DLL
- 让子进程的主线程恢复运行,从而让这些指令得到执行。
- 把保存起来的原始指令恢复到起始地址处
- 让进程从起始地址继续执行,就好像什么都没发生一样。
特点
优点:
- 它在应用程序执行之前就能得到地址空间
- 它既能在Windows 98上使用,也能在Windows 2000上使用
- 由于你不是调试者,因此能够很容易使用插入的D L L来调试应用程序
- 这种方法可以同时用于控制台和 G U I应用程序。
缺点:
- 只有当你的代码在父进程时,才能插入 DLL。
- 不能跨越不同的C P U来运行,必须对不同的C P U平台进行相应的修改。
九、拦截API的一个示例
9.1 通过改写代码来拦截API
原理
用汇编代码JUMP覆盖需要拦截目标函数。
步骤
拦截ExitProcess的一个方案是用代码覆盖来拦截。
工作方式如下:
- 在内存中,对想要拦截的函数(假设是Kernel32.dll中的ExitProcess)进行定位,从而得到它的内存地址。
- 把这个函数起始的几个字节保存到自己的内存中
- 用CPU的一条JUMP指令来覆盖这个函数的起始几个字节,这条JUMP指令用来跳转到我们替代函数的内存地址。所有替代函数的签名必须要与拦截函数的签名完全相同:所有参数必须相同,返回值必须相同,调用约定也必须相同。
- 当线程调用被拦截函数(hooked function)的时候,跳转指令实际上会跳转到我们的替代函数。这是可以执行自己想要执行的代码。
- 为了撤销对该函数的拦截,必须把步骤2)中保存下来的直接放回被拦截函数的起始字节。
- 我们调用被拦截函数,让该函数正常处理
- 当原来的函数返回时,再次执行步骤2和步骤3,这样我们的代替函数将来还好被调用到。
特点
- 它对cpu有依赖性x86, x64, ia-64其他cpu的JUMP指令各不相同,为了让这种方法工作必须手工编写机器指令。
- 其次,这种方法在抢占式,多线程环境下根本不能工作。一个线程覆盖一个函数起始位置的代码是需要时间的,这个过程如果另一个线程试图调用同一个函数,结果是灾难性的。(除非保证任何时候只有一个线程会调用该函数)
9.2 通过操作模块的输入节来拦截API
一个模块的导入段包含一组DLL,为了让模块能够运行,这些DLL是必须的。此外导入段还包含一个符号表,列出了该模块从各DLL中导入的符号。当该模块调用一个导入函数的时候,实际上是先从该模块的导入表中得到相应的导入函数的地址,然后再跳转到那个地址。
原理
修改特点函数在模块的输入节(导入段)中的地址。
特点
- 完全不存在对cpu的依赖性。
- 而且并不需要修改函数的代码,
- 也不用担心线程同步问题。
9.3 LastMsgBoxInfo示例应用程序
该例子拦截了所有对MessageBox(位于User32.dll中)的调用,为了在所有进程中拦截这个函数,应用程序使用了Windows挂钩技术来注入DLL。
总结
1. 第三节 使用Windows挂钩来插入DLL 发现隐藏窗口Wintellect DIPS为NULL
使用64位运行