Windows核心编程【22】小结

来源:互联网 发布:淘宝主图视频尺寸要求 编辑:程序博客网 时间:2024/06/17 18:02
第22章 DLL注入和API拦截


在Windows中,每个进程都有自己私有的地址空间。独立的地址空间对开发人员和用户都是非常有利的。对开发人员来说,系统更有可能捕获错误的内存读/写。对用户来说,OS变得更加健壮,因为一个应用程序的错误不会导致其他应用程序或OS崩溃。


APP需要跨越进程边界来访问另一个进程的地址空间的情况如下:
1、想要从另一个进程创建的窗口派生子类窗口。
(subclass a window created by another process, 一个subclass是一个窗口或一组具有相同窗口类的窗口,发往这个或这些窗口的消息在被送到窗口类的窗口过程处理之前,会先被另一个窗口过程截取并处理。这是对窗口的行为进行扩展和定制的一种方法。)
2、需要一些手段来辅助调试——例如,需要确定另一个进程正在使用哪些DLL。
3、想要给另一个进程安装挂钩。


DLL代码进入另一个地址空间,那么我们就可以在那个进程中随心所欲,肆意妄为了。


一、DLL注入的一个例子
~~~~~~~~~~~~


二、使用注册表来注入DLL


整个系统的配置都保存在注册表中,可以通过调整其中的设置来改变系统的行为。要讨论的条目在下面这个注册表项中:
HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\Windows\


AppInit_Dlls键的值可能会包含一个DLL的文件名或一组DLL的文件名(通过空格或逗号分隔)。由于空格是用来分隔文件名的,因此我们必须避免在文件名中包含空格。第一个DLL的文件名可以包含路径,但其他DLL包含的路径则将被忽略。处于这个原因,最好是将自己的DLL放到Windows的系统目录中,这样就不必指定路径了。
LoadAppInit_Dlls,类型为DWORD的注册表项,值为1说明需要初始化。


当User32.dll被映射到一个新的进程时,会收到DLL_PROCESS_ATTACH通知。当User32.dll对它进行处理的时候,会取得上述注册表键的值,并调用LoadLibrary来载入这个字符串中指定的每个DLL。当系统载入每个DLL的时候,会调用它们的DllMain函数并将参数fdwReason的值设为DLL_PROCESS_ATTACH,这样每个DLL就能够对自己进行初始化。


由于被注入的DLL是在进程生命期的早期被载入的,因此我们在调用函数的时候应该慎重。调用Kernel32.dll中的函数应该没问题,但是调用其他DLL中的函数可能会导致问题,甚至可能会导致蓝屏。User32.dll不会检查每个DLL的载入或初始化是否成功。


在用来注入DLL的所有方法中,这是最方便的一种。需要做的只是在注册表中添加两个值。但这种方法也有一些缺点,具体如下:
1、DLL只会被映射到那些使用了User32.dll的进程中。所有基于GUI的APP都使用了User32.dll,但大多数基于CUI的APP都不会使用它。
2、DLL会被映射到每个基于GUI的APP中,但我们可能只想把DLL注入到一个或少数几个APP中。
3、DLL会被映射到每个基于GUI的APP中,在APP终止之前,它将一直存在于进程的地址空间中。时间无法控制。


三、使用Windows挂钩来注入DLL


为了能让挂钩的工作方式与它们在16位Windows中的工作方式相同,MS被迫设计出一种机制,这种机制可以让我们将一个DLL注入到另一个进程的地址空间中。


看一个例子,进程A为了查看系统中各窗口处理了哪些消息,安装了一个WH_GETMESSAGE挂钩。该挂钩通过调用SetWindowsHookEx来安装:
HHOOK hHook = SetWindowsHookEx(WH_GETMESSAGE, GetMsgProc, hInstDll, 0);
第一个参数表示要安装的挂钩的类型;第二个参数是一个函数的地址(在我们的地址空间中),在窗口即将处理一条消息的时候,系统应该调用这个函数;第三个参数标识一个DLL,这个DLL中包含了GetMsgProc函数。(在Windows中,hInstDll的值是进程地址空间中DLL被映射到的虚拟内存地址。)最后一个参数标识要给哪个线程安装挂钩。(一个线程可能会调用SetWindowsHookEx并传入系统中另一个线程的线程标识符)通过传0,告诉系统要给系统中的所有GUI线程安装挂钩。


则会发生如下:
1、进程B中的一个线程准备向一个窗口派送一条消息。
2、系统检查该线程是否已经安装了WH_GETMESSAGE挂钩。
3、系统检查GetMsgProc所在的DLL是否已经被映射到进程B的地址空间中。
4、如果DLL尚未被映射,那么系统会强制将该DLL映射到进程B的地址空间中,并将进程B中该DLL的锁计数器(lock count)递增。
5、由于DLL的hInstDll是在进程B中映射的,因此系统会对它进行检查,看它与该DLL在进程A中的位置是否相同。
如果hInstDll相同,那么在两个进程的地址空间中,GetMsgProc函数位于相同位置,系统可以直接在进程A的地址空间中调用GetMsgProc。
如果hInstDll不同,那么系统必须确定GetMsgProc函数在进程B的地址空间中的虚拟内存地址。
GetMsgProc B = hInstDll B + (GetMsgProc A - hInstDll A)
6、系统在进程B中递增该DLL的锁计数器。(为何再次递增???)
7、系统在进程B的地址空间中调用GetMsgProc函数。
8、当GetMsgProc返回的时候,系统递减该DLL在进程B中的锁计数器。


注意:当系统把挂钩过滤函数(hook filter function)所在的DLL注入或映射到地址空间时,会映射整个DLL,而不仅仅只是挂钩过滤函数。这意味着该DLL内的所有函数存在于进程B中,能够为进程B中的任何线程调用。


因此,为了从另一个进程的窗口来创建一个子类窗口,可以先给创建窗口的线程设置一个WH_GETMESSAGE挂钩,然后当GetMsgProc函数被调用的时候,就可以调用SetWindowLongPtr来派生子类窗口。当然,子类窗口的窗口过程必须和GetMsgProc函数在同一个DLL中。


~~~~~


四、使用远程线程来注入DLL


Windows提供了一些函数来让一个进程对另一个进程进行操作。虽然这些函数中的大多数一开始都是为调试器或其他工具设计的,但是任何应用程序都可以调用这些函数。


从根本上说,DLL注入技术要求目标进程中的一个线程调用LoadLibrary来载入我们想要的DLL。由于我们不能轻易地控制别人进程中的线程,因此这种方法要求我们在目标进程中创建一个新的线程。由于这个线程使我们自己创建的,因此我们可以对它执行的代码加以控制。
Windows提供了如下所示的CreateRemoteThread函数,使得在另一个进程中创建线程变得非常容易:http://msdn.microsoft.com/en-us/library/windows/desktop/ms682437(v=vs.85).aspx
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);



除了有一个额外的参数hProcess之外,与CreateThread完全相同。这个参数用来表示新创建的线程归哪个进程所有。参数pfnStartAddr是线程函数的内存地址。当然,这个内存地址应该在远程进程(remote process)的地址空间中,因为线程函数的代码不能在我们自己进程的地址空间中。


再让我们创建的远程线程调用LoadLibrary函数,就可以实现DLL注入了。


巧合的是,LoadLibrary函数的原型和线程函数的原型基本相同。下面是线程函数的函数原型:
DWORD WINAPI ThreadFunc(PVOID pvParam);
因此我们可以直接创建这个新线程:
HANDLE hThread = CreateRemoteThread(hProcessRemote, NULL, 0, LoadLibraryW, L"C:\\MyLib.dll", 0, NULL);
ANSI版本:
HANDLE hThread = CreateRemoteThread(hProcessRemote, NULL, 0, LoadLibraryA, "C:\\MyLib.dll", 0, NULL);


但是,有两个问题。


1、不能直接把LoadLibraryW或A作为第4个参数传给CreateRemoteThread。其原因不是那么明显。在编译和链接一个程序的时候,生成的二进制文件中包含一个导入段(19章中介绍)。这个段由一系列转换函数构成,这些转换函数用来跳转到导入的函数。因此,在代码调用诸如LoadLibraryW之类的函数,链接器会生成一个调用,来调用我们模块的导入段中的一个转换函数,这个转换函数然后会跳转到实际的函数。


如果在调用CreateRemoteThread的直接引用LoadLibraryW,该引用会被解析为我们模块的导入段中LoadLibraryW转换函数的地址。如果把这个转换函数的地址作为远程线程的起始地址传入,可能会访问违规。为了强制代码略过转换函数并直接调用LoadLibraryW函数,必须通过调用GetProcAddress来得到LoadLibraryW的确切地址。
(导入段怎么回事?)




对CreateRemoteThread的调用假定在本地进程(local process)和远程进程中,Kernel32.dll被映射到地址空间中的同一内存地址。每个应用程序都需要Kernel32.dll,而且根据的经验,系统在进程中都会将Kernel32.dll映射到同一个地址。即使这个地址在系统重启之后可能会改变,就像14章的地址空间布局随机化(Address Space Layout Randomization,ASLR)看到的那样,这个假定仍然成立。


必须通过下面这样来调用CreateRemoteThread:(UNICODE版本)
PTHREAD_START_ROUTINE pfnThreadRtn = (PTHREAD_START_ROUTINE)
GetProcAddress(GetModuleHandle(TEXT("Kernel32")),"LoadLibraryW");
HANDLE hThread = CreateRemoteThread(hProcessRemote, NULL, 0, pfnThreadRtn, L"C:\\MyLib.dll", 0, NULL);


2、与DLL陆军字符串有关,字符串"C:\\MyLib.dll"位于调用进程的地址空间中。把这个地址传给先创建的远程线程,远程线程再把它传入LoadLibraryW,当LoadLibraryW访问该内存地址的时候,DLL的路径字符串并不在那里,远程进程的线程很可能会引发访问违规。远程进程会被弄崩。


为解决这个问题,需要把DLL的路径字符串存放到远程进程的地址空间中去。然后再调用CreateRemoteThread的时候,传入(在远程进程的地址空间中)存放字符串的地址。碰巧的是,Windows提供了VirtualAllocEx函数,可以让一个进程在另一个进程的地址空间中分配一块内存。http://msdn.microsoft.com/en-us/library/windows/desktop/aa366890(v=vs.85).aspx
LPVOID WINAPI VirtualAllocEx(  __in      HANDLE hProcess,  __in_opt  LPVOID lpAddress,  __in      SIZE_T dwSize,  __in      DWORD flAllocationType,  __in      DWORD flProtect);


另一个函数可以让我们释放这块内存,VirtualFreeEx:http://msdn.microsoft.com/en-us/library/windows/desktop/aa366894(v=vs.85).aspx
BOOL WINAPI VirtualFreeEx(  __in  HANDLE hProcess,  __in  LPVOID lpAddress,  __in  SIZE_T dwSize,  __in  DWORD dwFreeType);



一旦为字符串分配了一块内存,还需要一种方法来把字符串从进程的地址空间中复制到远程进程的地址空间中去。Windows提供了一些函数,可以让一个进程对另一个进程的地址空间进程读写。
ReadProcessMemory(http://msdn.microsoft.com/en-us/library/windows/desktop/ms680553(v=vs.85).aspx)和WriteProcessMemory(http://msdn.microsoft.com/en-us/library/windows/desktop/ms681674(v=vs.85).aspx)
BOOL WINAPI ReadProcessMemory(  __in   HANDLE hProcess,  __in   LPCVOID lpBaseAddress,  __out  LPVOID lpBuffer,  __in   SIZE_T nSize,  __out  SIZE_T *lpNumberOfBytesRead);
BOOL WINAPI WriteProcessMemory(  __in   HANDLE hProcess,  __in   LPVOID lpBaseAddress,  __in   LPCVOID lpBuffer,  __in   SIZE_T nSize,  __out  SIZE_T *lpNumberOfBytesWritten);



远程进程由hProcess参数来识别。参数pvAddressRemote表示远程进程中的地址,pvBufferLoacl是本地进程中的内存地址,dwSize是要传输的字节数,pdwNumBytesRead和pdwNumBytesWritten分别表示实际传输的字节数。可以在函数返回后查看这两个参数的值。


总结步骤:
1、用VirtualAllocEx函数在远程进程的地址空间中分配一块内存。
2、用WriteProcessMemory函数把DLL的路径名复制到第1步分配的内存中。
3、用GetProcAddress函数来得到LoadLibraryW或LoadLibraryA函数(在Kernel32.dll)的实际地址。
4、用CreateRemoteThread函数在远程进程中创建一个线程,让新线程调用正确的LoadLibrary函数并在参数中传入第1不分配的内存地址。
5、用VirtualFreeEx来释放第1步分配的内存。
6、用GetProcAddress来得到FreeLibrary函数(在Kernel32.dll中)的实际地址。
7、用CreateRemoteThread函数在远程进程中创建一个线程,让该线程调用FreeLibrary函数并在参数中传入远程DLL的HMODULE。


五、使用木马DLL来注入DLL


注入DLL的另一种方式是,把我们知道的进程比如会载入的一个DLL替换掉。在我们的DLL的内部,导出原来的DLL导出的所欲符号。这用20章介绍的函数转发器很容易实现,这样一来,给某些函数安装挂钩只不过是小事一桩。但是,不能自动适应版本变化,应该避免使用它。


如果只是想把这种方法用在一个应用程序中,则可以给我们的DLL起一个独一无二的名称,并修改APP的.exe模块的导入段。说得更具体点,导入段包含了一个模块所需的所有DLL的名称。可以在文件的导入段中找到那个要被替换的DLL的名称,对它进程修改。前提是要相当熟悉.exe和DLL的文件格式。


六、把DLL作为调试器来注入
原创粉丝点击