dll被加载后线程的问题

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了dll被加载后线程的问题相关的知识,希望对你有一定的参考价值。

如果有个dll被加载到了进程空间里,并且dll里没有创建过线程,那么在这dll中的函数调用,使用的是加载dll时的那个线程的栈空间吗?

参考技术A 现在不管下载什么 ,总是DLL加载失败吗我该怎么办
把你的代码放到一个DLL中;然后用 windows 钩子把它映射到远程进程。
2. 把你的代码放到一个DLL中;然后用 CreateRemoteThread 和 LoadLibrary 把它映射到远程进程。
3. 不用DLL,直接复制你的代码到远程进程(使用WriteProcessMemory)并且用CreateRemoteThread执行之。在这里有详细的说明:

Ⅰ. Windows 钩子

示例程序:HookSpy 和 HokInjEx

Windows钩子的主要作用就是监视某个线程的消息流动。一般可分为:
1. 局部钩子,只监视你自己进程中某个线程的消息流动。
2. 远程钩子,又可以分为:
a. 特定线程的,监视别的进程中某个线程的消息;
b. 系统级的,监视整个系统中正在运行的所有线程的消息。

如果被挂钩(监视)的线程属于别的进程(情况2a和2b),你的钩子过程(hook procedure)必须放在一个动态连接库(DLL)中。系统把这包含了钩子过程的DLL映射到被挂钩的线程的地址空间。Windows会映射整个DLL而不仅仅是你的钩子过程。这就是为什么windows钩子可以用来向其他线程的地址空间注入代码的原因了。

在这里我不想深入讨论钩子的问题(请看MSDN中对SetWindowsHookEx的说明),让我再告诉你两个文档中找不到的诀窍,可能会有用:
1. 当SetWindowHookEx调用成功后,系统会自动映射这个DLL到被挂钩的线程,但并不是立即映射。因为所有的Windows钩子都是基于消息的,直到一个适当的事件发生后这个DLL才被映射。比如:
如果你安装了一个监视所有未排队的(nonqueued)的消息的钩子(WH_CALLWNDPROC),只有一个消息发送到被挂钩线程(的某个窗口)后这个DLL才被映射。也就是说
::GetModuleFileName( hModule, lib_name, MAX_PATH );
::LoadLibrary( lib_name );

// 安全卸载钩子
::UnhookWindowsHookEx( g_hHook );

return TRUE;


我们来看一下。首先,我们用钩子映射这个DLL到远程线程,然后,在DLL被真正映射进去后,我们立即卸载挂钩(unhook)。一般来说当第一个消息到达被挂钩线程后,这DLL会被卸载,然而我们通过LoadLibrary来增加这个DLL的引用次数,避免了DLL被卸载。

剩下的问题是:使用完毕后如何卸载这个DLL?UnhookWindowsHookEx不行了,因为我们已经对那个线程取消挂钩(unhook)了。你可以这么做:
○在你想要卸载这个DLL之前再安装一个钩;
○发送一个“特殊”的消息到远程线程;
○在你的新钩子的钩子过程(hook procedure)中截获该消息,调用FreeLibrary 和 (译者注:对新钩子调用)UnhookwindowsHookEx。
现在,钩子只在映射DLL到远程进程和从远程进程卸载DLL时使用,对被挂钩线程的性能没有影响。也就是说,我们找到了一种(相比第二部分讨论的LoadLibrary技术)WinNT和Win9x下都可以使用的,不影响目的进程性能的DLL映射机制。

但是,我们应该在何种情况下使用该技巧呢?通常是在DLL需要在远程进程中驻留较长时间(比如你要子类[subclass]另一个进程中的控件)并且你不想过于干涉目的进程时比较适合使用这种技巧。我在HookSpy中并没有使用它,因为那个DLL只是短暂地注入一段时间――只要能取得密码就足够了。我在另一个例子HookInjEx中演示了这种方法。HookInjEx把一个DLL映射进“explorer.exe”(当然,最后又从其中卸载),子类了其中的开始按钮,更确切地说我是把开始按钮的鼠标左右键点击事件颠倒了一下。

你可以在本文章的开头部分找到HookSpy和HookInjEx及其源代码的下载包链接。

Ⅱ. CreateRemoteThread 和 LoadLibrary 技术
示例程序:LibSpy

中的线程过程的起始地址。
2. 如果把ThreadProc的lpParameter参数当做一个普通的32位整数(FreeLibrary把它当做HMODULE)那么没有如何问题,但是如果把它当做一个指针(LoadLibrary把它当做一个char*),它就必须指向远程进程中的内存数据。

第一个问题其实已经迎刃而解了,因为LoadLibrary和FreeLibrary都是存在于kernel32.dll中的函数,而kernel32可以保证任何“正常”进程中都存在,且其加载地址都是一样的。(参看附录A)于是LoadLibrary/FreeLibrary在任何进程中的地址都是一样的,这就保证了传递给远程进程的指针是个有效的指针。

第二个问题也很简单:把DLL的文件名(LodLibrary的参数)用WriteProcessMemory复制到远程进程。

所以,使用CreateRemoteThread和LoadLibrary技术的步骤如下:
1. 得到远程进程的HANDLE(使用OpenProcess)。
2. 在远程进程中为DLL文件名分配内存(VirtualAllocEx)。

3. 把DLL的文件名(全路径)写到分配的内存中(WriteProcessMemory)
4. 使用CreateRemoteThread和LoadLibrary把你的DLL映射近远程进程。
5. 等待远程线程结束(WaitForSingleObject),即等待LoadLibrary返回。也就是说当我们的DllMain(是以DLL_PROCESS_ATTACH为参数调用的)返回时远程线程也就立即结束了。
6. 取回远程线程的结束码(GetExitCodeThtread),即LoadLibrary的返回值――我们DLL加载后的基地址(HMODULE)。
7. 释放第2步分配的内存(VirtualFreeEx)。
8. 用CreateRemoteThread和FreeLibrary把DLL从远程进程中卸载。调用时传递第6步取得的HMODULE给FreeLibrary(通过CreateRemoteThread的lpParameter参数)。
9. 等待线程的结束(WaitSingleObject)。

同时,别忘了在最后关闭所有的句柄:第4、8步得到的线程句柄,第1步得到的远程进程句柄。

sp; MEM_COMMIT, PAGE_READWRITE );
::WriteProcessMemory( hProcess, pLibRemote, (void*)szLibPath,
sizeof(szLibPath), NULL );

// 加载 "LibSpy.dll" 到远程进程
// (通过 CreateRemoteThread & LoadLibrary)
hThread = ::CreateRemoteThread( hProcess, NULL, 0,
(LPTHREAD_START_ROUTINE) ::GetProcAddress( hKernel32,

nbsp; "LoadLibraryA" ),
pLibRemote, 0, NULL );
::WaitForSingleObject( hThread, INFINITE );

//取得DLL的基地址
::GetExitCodeThread( hThread, &hLibModule );

//扫尾工作
::CloseHandle( hThread );
::VirtualFreeEx( hProcess, pLibRemote, sizeof(szLibPath), MEM_RELEASE );

我们放在DllMain中的真正要注入的代码(比如为SendMessage)现在已经被执行了(由于DLL_PROCESS_ATTACH),所以现在可以把DLL从目的进程中卸载了。

// 从目标进程卸载LibSpu.dll
// (通过 CreateRemoteThread & FreeLibrary)
hThread = ::CreateRemoteThread( hProcess, NULL, 0,

rocess Communications部分)或其他资料中都有很好的说明。我在LibSpy中使用的是#pragma data_seg。

你可以在本文章的开头找到LibSpy及源代码的下载链接。

Ⅲ.CreateRemoteThread和WriteProcessMemory技术
示例程序:WinSpy

另一种注入代码到其他进程地址空间的方法是使用WriteProcessMemory API。这次你不用编写一个独立的DLL而是直接复制你的代码到远程进程(WriteProcessMemory)并用CreateRemoteThread执行之。

让我们看一下CreateRemoteThread的声明:
HANDLE CreateRemoteThread(
HANDLE hProcess, // handle to process to create thread in
LPSECURITY_ATTRIBUTES lpThreadAttributes, // pointer to security
&nb

p; // attributes
DWORD dwStackSize, // initial thread stack size, in bytes
LPTHREAD_START_ROUTINE lpStartAddress, // pointer to thread
// function
LPVOID lpPa
teThread启动远程的ThreadFunc。
7. 等待远程线程的结束(WaitForSingleObject)。
8. 从远程进程取回指执行结果(ReadProcessMemory 或 GetExitCodeThread)。
9. 释放第2、4步分配的内存(VirtualFreeEx)。
10. 关闭第6、1步打开打开的句柄。

另外,编写ThreadFunc时必须遵守以下规则:
1. ThreadFunc不能调用除kernel32.dll和user32.dll之外动态库中的API函数。只有kernel32.dll和user32.dll(如果被加载)可以保证在本地和目的进程中的加载地址是一样的。(注意:user32并不一定被所有的Win32进程加载!)参考附录A。如果你需要调用其他库中的函数,在注入的代码中使用LoadLibrary和GetProcessAddress强制加载。如果由于某种原因,你需要的动态库已经被映射进了目的进程,你也可以使用GetMoudleHandle代替LoadLibrary。同样,如果你想在ThreadFunc中调用你自己的函数,那么就分别复制这些函数到远程进程并通过INJDATA把地址提供给ThreadFunc。
2.

不要使用static字符串。把所有的字符串提供INJDATA传递。为什么?编译器会把所有的静态字符串放在可执行文件的“.data”段,而仅仅在代码中保留它们的引用(即指针)。这样,远程进程中的ThreadFunc就会执行不存在的内存数据(至少没有在它自己的内存空间中)。
3. 去掉编译器的/GZ编译选项。这个选项是默认的(看附录B)。
4. 要么把ThreadFunc和AfterThreadFunc声明为static,要么关闭编译器的“增量连接(incremental linking)”(看附录C)。
5. ThreadFunc中的局部变量总大小必须小于4k字节(看附录D)。注意,当degug编译时,这4k中大约有10个字节会被事先占用。
6. 如果有多于3个switch分支的case语句,必须像下面这样分割开,或用if-else if代替:

switch( expression )
case constant1: statement1; goto END;
case constant2: statement2; goto END;
case constant3: statement2; goto END;

switch( expression
的缓冲

返回值:
成功复制的字符数。

让我们看以下它的部分代码,特别是注入的数据和代码。为了简单起见,没有包含支持Unicode的代码。

INJDATA

typedef LRESULT (WINAPI *SENDMESSAGE)(HWND,UINT,WPARAM,LPARAM);

typedef struct
HWND hwnd; // handle to edit control
SENDMESSAGE fnSendMessage; // pointer to user32!SendMessageA

char psText[

28]; // buffer that is to receive the password
INJDATA;

什么是动态链接库

什么是dll:

dll只是一组源代码的模块,每个模块包含一些可供应用程序或者其他dll调用的函数,在应用程序调用一个dll里面的函数的时候,操作系统会将dll的文件映像映射到进程的地址空间中,这样进程中所有的线程就可以调用dll中的函数了
 
dll加载完成后,这个时候dll对于进程中的线程来说只是一些被放在地址进程空间附加的代码和数据,操作系统为了节省内存空间,同一个dll在内存中只有一个,也就是说如果你的的两个应用程序都需要加载user32.dll,那么操作系统也只会加载一次user32.dll到内存中
 
因为代码段在内存中的权限都是为只读的,所以当多个应用程序加载同一个dll的时候,不用担心应用程序会修改dll的代码段。当线程调用dll的一个函数,函数会在线程栈中取得传递给他的参数,并使用线程栈来存放他需要的变量,dll函数创建的任何对象都为调用线程或者调用进程拥有,dll不会拥有任何对象,也就是说如果dll中的一个函数调用了VirtualAlloc,系统会从调用进程的地址空间预定地址,即使撤销了对dll的映射,调用进程的预定地址依然会存在,直到 用户取消预定或者进程结束
 
示例代码:
mylib.h
复制代码
1 #ifdef MYLIBAPI
2 #else
3      #define MYLIBAPI extern "C" __desclspec(dllimport)
4 #endif
5 
6 MYLIBAPI int g_nResult;
7 
8 MYLIBAPI int Add(int nLeft,int nRight)
复制代码
mylib.cpp
复制代码
 1 #include <windows.h>
 2 
 3 #define MYLIBAPI     extern "C"     __declspec(dllexport)
 4 #include "mylib.h"
 5 int g_nResult;
 6 
 7 int Add(int nLeft,int nRight)
 8 {
 9      g_nResult = nLeft + nRight;
10      return g_nResult;
11 }
复制代码
输入命令:
cl /LDd mylib.cpp
可以生成可供调试的dll
 
这个时候会多出四个文件,分别是mylib.exp,mylib.lib,mylib.dll,mylib.obj
 
mylib.obj保存的是在链接器生成dll的需要的信息
 
当链接器检测到应用程序导出了一个函数或者变量,链接器就会生成mylib.lib文件,这个只是列出了导出的函数和变量的符号名
输入命令查看lib里面的导出段
dumpbin     -exports     mylib.lib
 

 

我们可以看到这个lib里面export了_Add和_g_nResult
如果我们使用dumpbin -imports mylib.lib
 
imports里面没有变量或者函数,这是因为lib里面记录的只是导出的函数和变量,只有在声明有导出函数或者变量的时候,才会生成这个文件

mylib.dll则是我们最终生成的模块
如果使用dumpbin查看mylib.dll的导出

 

导入则因为太多,所以不贴出来
 
到我们需要将一个函数导出的时候,可以使用__desclspec(dllexport)来声明为导出函数,需要从dll使用一个函数的时候,可以使用__desclspecc(dllimport)来前置声明一个函数,当然,也可以不使用import前置声明,但是使用improt可以明确告诉编译器这些函数是从dll导入的,提高效率
 
什么是导出?
当将函数或者变量声明为导出后,编译器在生成obj的时候会嵌入一些额外的信息,以便于让链接器在生成dll的时候使用,并且会生成一个记录导出函数和变量的lib文件,在生成可执行文件的时候,我们需要通过链接这个lib来取得dll的一些信息,链接器在生成dll的时候,会在dll文件中嵌入一个导出符号表,这个符号表记录了导出的函数和变量的符号名,并且保存对应的文件偏移量地址,这样当可执行文件需要调用dll里面的函数的时候,可以通过这个符号表来找到对应函数的地址
 
最后我们开始构建可执行文件,代码如下
myexe.cpp
 
#include <cstdio>
#include "mylib.h"

int main(void)
{
     int nLeft = 10,nRight = 20;
     printf("%d\\n",Add(nLeft,nRight));
}
 
cl myexe.cpp mylib.lib
我们在编译的时候一定要链接mylib.lib,这样编译器才知道要到哪里去找mylib的变量和函数相关信息,并且可执行文件也才知道程序需要mylib.dll这个dll,这样程序在加载的时候会搜索用户磁盘上的dll,如果没找到则会报错,找到则将dll映射到进程的内存空间里面
 
当dll映射到进程的内存空间里面后,加载程序会查看在对应的dll的导出段符号是否存在,如果不存在,则报错,如果存在,那么加载程序会将该符号加载到该符号的所在的文件偏移量(RVA,虚拟地址,但在dll里面实际上是该符号所在文件的位置),加上该dll加载的虚拟地址,保存到可执行程序的导入段中,当代码引用到导入符号的时候,可执行文件会去查看导入段并且得到导入符号的地址,这样就能访问导入的变量或者函数
 
例如我们生成的mylib.dlll文件,利用dumpbin可以得到输出:

我们可以看到Add的RVA是1000,假设我们的dll被应用程序映射到1000的地址空间中,那么在应用程序执行的时候,Add函数最终会被加载到1000+1000即2000处,这个就是我们前面所说的dll在映射到地址空间后,对于应用程序来说不过是一堆附加的代码和数据

以上是关于dll被加载后线程的问题的主要内容,如果未能解决你的问题,请参考以下文章

加载 DLL 阻塞 UI 线程

C#动态加载dll 时程序集的卸载问题

什么是动态链接库

在一个dll里分配的内存,在另一个dll或exe释放会有问题吗?

mshtml.dll被瑞星查毒删除,网页无法打开,瑞星也进不来如何修复

如何在动态加载的 DLL 中正确实现(C++)线程本地存储?