-显示地载入DLL模块
HMODULE WINAPI LoadLibrary(
_In_ LPCTSTR lpFileName
);
HMODULE WINAPI LoadLibraryEx(
_In_ LPCTSTR lpFileName,
// 为将来扩展预备,NULL。
_Reserved_ HANDLE hFile,
// 0
// DONT_RESOLVE_DLL_REFERENCES 正常下,一个DLL被映射到地址空间时,系统调用DLL的DllMain。且会载入,此DLL关联的DLL。此标志设置时,不调用DllMain,不载入关联DLL。
// LOAD_LIBRARY_AS_DATAFILE 将DLL作为数据文件映射。对用此标志载入的DLL调用GetProcAddress时,返回值为NULL。调用LoadLibraryEx来载入.exe时,必须指定此标志。
// LOAD_LIBRARY_AS_DATAFILE_EXCLUSIVE LOAD_LIBRARY_AS_DATAFILE+以独占方式打开此DLL
// LOAD_LIBRARY_AS_IMAGE_RESOURCE 系统载入DLL时,会对相对虚拟地址进行修复。
// LOAD_WITH_ALTERED_SEARCH_PATH 改变对DLL定位时的搜索算法。
// lpFileName,不含\字符,使用标准搜索路径。
// lpFileName,含\字符,
// 如传入参数为全路径或网络共享路径(如C:\Apps|Libraries\MyLibrary.dll或\\Server\Share\MyLibrary.dll),则,直接载入该DLL。如文件不存在,返回NULL。
// 其它情况,将下列文件夹与lpFileName连接。
// a.进程当前目录
// b.windows系统目录(System32)
// c.16位windows系统目录(System)
// d.windows目录
// e.PATH环境变量的目录
// 如lpFileName中出现"."或"..",搜索过程的每一步骤都会将它们考虑在内来构建一个相对路径。
// 如将("..\\MyLibrary.dll")作为参数传入,则,LoadLibraryEx在下列位置搜索:
// a的上一级目录
// b的上一级目录
// c的上一级目录
// d的上一级目录
// e的上一级目录
// 如果构建应用时,希望从一个zhong'suo'z
_In_ DWORD dwFlags
);
以上两函数可以用来将一个DLL映射到进程的地址空间。
会在用户的系统中对DLL的文件映像进行定位,并试图将文件映像映射到调用进程地址空间。
返回的HMODULE等价于HINSTANCE,表示文件映像被映射到的虚拟内存地址。无法将DLL映射到进程的地址空间,函数会返回NULL。
如果希望从众所周知的文件夹动态载入DLL,应调用SetDllDirectory,传入文件夹。
此时搜索时:
a.进程的当前目录
b.SetDllDirectory指定的。
c.windows系统目录
d.16位windows系统目录
e.windows目录
f.PATH环境变量中列出的目录
GetDllDirectory。
SetDllDirectory会恢复使用默认算法
-显式地卸载DLL模块
// 将DLL从进程的地址空间中卸载
BOOL WINAPI FreeLibrary(
_In_ HMODULE hModule
);
-DLL使用计数
每个DLL在进程中有一个使用计数。LoadLibrary(Ex)增加,FreeLibrary减1。进程内多次载入同一DLL,后续载入时仅仅改变该DLL使用计数。
FreeLibrary递减使用计数,使用计数变为0,从进程地址空间撤销对DLL文件映像的映射。
系统会在每个进程中为每个DLL维护一个使用计数。
进程A和B分别执行LoadLibrary载入同一DLL,此DLL会被映射到两个进程的地址空间,在进程A,进程B的使用计数均为1。若进程B执行FreeLibrary,该DLL在进程B使用计数为0,从进程B地址空间撤销对该DLL的映射。但对进程A无映像,进程A
中该DLL使用计数仍为1。
// 返回载入到进程的模块句柄
HMODULE WINAPI GetModuleHandle(
// 模块名
_In_opt_ LPCTSTR lpModuleName
);
// 由模块句柄得到模块全路径
DWORD WINAPI GetModuleFileName(
_In_opt_ HMODULE hModule,
_Out_ LPTSTR lpFilename,
_In_ DWORD nSize
);
-其它
当我们用
LOAD_LIBRARY_AS_DATAFILE,
LOAD_LIBRARY_AS_DATAFILE_EXCLUSIVE ,
LOAD_LIBRARY_AS_IMAGE_RESOURCE
标志调用LoadLibraryEx时,系统会先检测该DLL是否已被LoadLibrary或LoadLibraryEx(但没使用这些标志)载入过。如果已被载入过,那么函数返回DLL已被映射到的地址。如果尚未载入,windows会将该DLL载入到地址空间中一个可用的地址,但并不认为它是一个完全载入的DLL。这时如用此模块句柄调用GetModuleFileName,返回0。这是一中让我们知道一个DLL模块不含动态函数的方法,无法通过GetProcAddress来得到函数地址并调用函数。
对同一个DLL,前后分别用上述3个标志载入,DLL会被载入3次,映射到三个地址空间部分。
-显示地链接到导出符号
FARPROC WINAPI GetProcAddress(
_In_ HMODULE hModule,
// 符号名字符串
_In_ LPCSTR lpProcName
);
// 用法举例:
void DynamicDumpModule(HMODULE hModule);
typedef void (CALLBACK *PFN_DUMPMODULE)(HMODULE hModule);
PFN_DUMPMODULE pfnDumpModule = (PFN_DUMPMODULE)GetProcAddress(hDll, "DumpModule");
if(pfnDumpModule != NULL)
{
pfnDumpModule(hDll);
}
-DLL的入口点函数
// HINSTANCE hInstDll 表示DLL文件映像被映射到进程地址空间中的虚拟地址
BOOL WINAPI DllMain(HINSTANCE hInstDll,
// 调用入口点函数的原因
DWORD fdwReason,
// DLL隐式载入 不为0
// DLL显示载入 为0
PVOID fImpLoad)
{
switch(fdwReason)
{
// 第一次将一个DLL映射到进程的地址空间时。之后再载入此DLL时,仅仅递增其使用计数。
// 处理时,成功,返回TRUE。失败,返回FALSE。其它通知消息,系统会忽略返回值。
case DLL_PROCESS_ATTACH:
break;
case DLL_THREAD_ATTACH:
break;
case DLL_THREAD_DETACH:
break;
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
1.DLL_PROCESS_ATTACH
创建新进程时,
系统分配地址空间,并将EXE和DLL映射到进程地址空间。
创建进程主线程,用此线程调用每个DLL的DllMain,传入DLL_PROCESS_ATTACH。
所有已映射的DLL完成对通知处理后,主线程开始执行可执行模块的C/C++运行时启动代码。
执行可执行模块的入口点函数。
任何一个DLL的DllMain返回FALSE,系统会把所有的文件映像从地址空间清除,终止进程。
显示载入DLL时,
调用LoadLibrary(Ex)时,系统对指定的DLL定位,将该DLL映射到进程的地址空间。
系统用调用LoadLibrary(Ex)的线程来调用DLL的DllMain,传入DLL_PROCESS_ATTACH。
DllMain处理完毕,LoadLibrary(Ex)返回。
如DllMain返回FALSE,则,系统会自动从进程的地址空间撤销对DLL文件映像的映射,让LoadLibrary(Ex)返回NULL。
2.DLL_PROCESS_DETACH
将一个DLL从进程的地址空间撤销映射时,会调用DLL的DllMain,传入DLL_PROCESS_DETACH。
DLL_PROCESS_ATTACH返回FALSE,导致的撤销映射,不会调用DLL_PROCESS_DETACH。
撤销映射的原因是进程终止,则,调用ExitProcess的线程负责执行DllMain的代码。
撤销映射的原因是进程中的一个线程调用了FreeLibrary或FreeLibraryAndExitThread,则,发出调用的线程将执行DllMain函数的代码。
TerminateProcess导致的进程终止,不会调用DllMain。
3.DLL_THREAD_ATTACH
进程创建线程时,检查映射到进程地址空间的所有DLL文件映像,用DLL_THREAD_ATTACH调用每个DLL的DllMain。
新创建线程负责执行所有DLL的DllMain,所有DllMain处理完通知DLL_THREAD_ATTCH,线程继续执行。
主线程创建时,不需要。
4.DLL_THREAD_DETACH
ExitThread让线程终止,让即将终止的线程用DLL_THREAD_DETACH调用所有已映射DLL的DllMain,再终止。
要终止的线程,用DLL_THREAD_DETACH来调用所有已映射DLL的DllMain。
C/C++运行库这时会释放那些用来管理多线程应用程序的数据块。
TerminateThread导致的线程终止,不会调用DllMain。
-DllMain的序列化调用
例:
进程有线程A,线程B。
进程地址空间映射了SomeDLL.dll。
线程A和B都准备用CreateThread创建线程C,线程D。
序列化,就是多线程同时调用DllMain时,线程1执行完毕,才会让线程2执行。
线程1执行中,线程2如需要执行DllMain,则,挂起等待。
系统创建进程时,会同时创建一个锁。
进程中的线程调用映射到进程地址空间的DLL的DllMain时,用这个锁来同步各个线程。
程序调用CreateThread时,先创建线程内核对象和线程栈。
系统再内部调用WaitForSingleObject。
新线程得到互斥量所有权后,系统让新线程用DLL_THREAD_ATTACH调用每个DLL的DllMain。
这时,系统让线程放弃对进程互斥量所有权。
故,为避免死锁,不要在DllMain调用创建线程函数。
-DllMain和C/C++运行库
链接DLL时,链接器会将DLL的入口点函数的地址嵌入到生成的DLL文件映像中。
可用链接器的/ENTRY指定入口点函数地址。
如用Microsoft链接器并指定/DLL,链接器认为入口点函数函数名为:_DllMainCRTStartup。这个函数包含在C/C++运行库中,链接DLL时,会被静态地链接到DLL的文件映像。即使用C/C++运行库的DLL版本,对这个函数的链接仍然是静态的。
系统将DLL的文件映像映射到进程的地址空间时,实际调用的是_DllMainCRTStartup,而不是DllMain。
将所有通知转发到__DllMainCRTStartup前,
为支持/GS开关提供的安全性特性,_DllMainCRTStartup会对DLL_PROCESS_ATTCH处理。
__DllMainCRTStartup会初始化C/C++运行库,确保_DllMainCRTStartup收到DLL_PROCESS_ATTACH时,
所有全局或静态的C++对象都已构造完毕。
C/C++运行时,初始化完成后,__DllMainCRTStartup会调用我们的DllMain。
DLL收到DLL_PROCESS_DETACH通知时,系统再次调用__DllMainCRTStartup。
这次,该函数会调用我们的DllMain。
DllMain返回时,__DllMainCRTStartup会调用DLL中所有全局或静态C++对象的析构函数。
收到DLL_THREAD_ATTACH或DLL_THREAD_DETACH通知时,__DllMainCRTStartup不会做任何特俗处理。
如DLL没有提供DllMain,则,会使用C/C++运行库提供的。
// 大致如下:
BOOL WINAPI DllMain(HINSTANCE hInstDll, DWORD fdwReason, PVOID fImpLoad)
{
if(fdwReason == DLL_PROCESS_ATTACH)
{
DisableThreadLibraryCalls(hInstDll);
}
return TRUE;
}
链接DLL时,如链接器无法在DLL的.obj文件中找到一个名为DllMain的函数,则,它会链接到C/C++运行库的DllMain。
-延迟载入DLL
一个延迟载入的DLL是隐式链接的,一开始不将该DLL载入。只在代码试图引用DLL包含的一个符号时,才载入。
1.延迟载入DLL局限
一个导出了字段(全局变量)的DLL无法延迟载入。
Kernel32.dll模块无法延迟载入。
不应在DllMain中调用一个延迟载入的函数。
2.延迟载入
创建一个DLL。
创建一个可执行文件。
链接可执行文件时,须修改一些链接器开关。
项目属性中设置:
2.1./Lib:DelayImp.lib
将指定的函数__delayLoadHelper2嵌入我们的可执行文件。
#pragma comment(lib, "Delayimp.lib")
2..2./DelayLoad:MyDll.dll
将MyDll.dll从可执行模块导入段去除。进程初始化时,系统加载程序就不会隐式载入该DLL。
在可执行模块中嵌入新的延迟载入段,表示从MyDll.dll中导入那些函数。
让对延迟载入函数的调用转到__delayLoadHelper2,完成对延迟载入函数的解析。
应用运行时,对延迟载入函数调用会调用__delayLoadHelper2,这个函数会引用特殊的延迟载入段,并调用LoadLibrary和GetProcAddress,得到对应的延迟载入函数地址后,__delayLoadHelper2会修复对该函数的调用。
同一个DLL中的其它函数仍须在第一次被调用时修复。
延迟载入函数被调用时,加载程序找不到DLL,会抛出异常。找到,但里面没有引用函数,也抛出异常。
软件异常码:
没找到DLL
VcppException(ERROR_SEVERITY_ERROR, ERROR_MOD_NOT_FOUND)
没找到引用函数
VcppException(ERROR_SEVERITY_ERROR, ERROR_PROC_NOT_FOUND)
// __delayLoadHelper2分配和初始化
typedef struct DelayLoadInfo
{
// 版本控制
DWORD cb;
// 嵌入在模块中的延迟载入段。
// 包含延迟载入DLL和函数列表
PCImgDelayDescr pidd;
// 查找成功时,保存函数地址
FARPROC* ppfn;
// DLL名字
LPCSTR szDll;
// 要找的函数信息
DelayLoadProc dlp;
// DLL被载入处虚拟内存地址
HMODULE hmodCur;
// 异常处理中此值应为NULL,因为此时,找不到引用的函数,故函数地址会是NULL
FARPROC pfnCur;
// 引发异常的错误
DWORD dwLastError;
}
DelayLoadInfo;
可执行模块属性中Linker里设置 延迟载入DLL。
-卸载一个延迟载入的DLL
须在构建可执行文件时,指定一个额外的链接器开关。(/Delay:unload)
须修改源代码,在想卸载DLL的地方调用__FUnloadDelayLoadedDLL2。
BOOL __FUnloadDelayLoadedDLL2(PCSTR szDll);
链接器开关/Delay:unload告诉链接器在文件中嵌入另一个段。
这个段包含必要的信息,来重置我们已经调用过的函数。
程序调用这些函数时,再次调用__delayLoadHelper2。
调用__FUnloadDelayLoadedDLL2时,传入要卸载的延迟载入DLL名字。
函数会引用文件的卸载段,将DLL所有函数地址重置,再调用FreeLibrary卸载该DLL。
调用__FUnloadDelayLoadedDLL2时,传入的DLL名字不应包含路径,名字中字母的大小写须和传给/DelayLoad链接器开关的DLL名字相同。
在一个模块中调用__FUnloadDelayLoadedDLL2,但模块构建时没用/Delay:unload,函数会失败并返回FALSE。
-得到通知&覆盖默认行为
1.编写挂钩函数。
2.将全局变量__pfnDliNotifyHook2或__pfnDliFailureHook2设为挂钩函数地址。
一个报告通知,一个报告失败。
PfnDliHook __pfnDliNotifyHook2 = DliHook;
PfnDliHook __pfnDliFailureHook2 = DliHook;
-函数转发器
函数转发器是DLL输出段中的一个条目,将一个函数调用转发到另一个DLL中的另一个函数。
C:\Windows\System32>DumpBin -Exports Kernel32.dll
...
CloseThreadpoolIo (forwarded to NTDLL.TpReleaseIoCompletion)
...
在自己的DLL模块中使用函数转发器
#pragma comment(linker, "/export:SomeFunc=DllWork.SomeOtherFunc")
-已知的DLL
系统在载入已知的DLL时总是在同一个目录中查找。
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\KnownDLLs
注册表项:
Name Type Data
例:
user23 REG_SZ user32
LoadLibrary(Ex)被调用时,函数首先检查传入的DLL的名字是否包含了.dll扩展名。
如没包含,用正常搜索规则,搜索。
如包含,去掉扩展名,在KnownDLLs注册表项中搜索。搜索失败,用正常搜索规则搜索。
如找到匹配的项(名称匹配),试图载入数据内容。
试图载入数据对应DLL时,
先从注册表项DllDirectory执行目录搜索。默认为%SystemRoot%\System32。
如在此目录找不到,LoadLibrary(Ex)失败并返回NULL。
-DLL重定向
建议:
将应用的文件放到自己的目录,不修改windows系统目录的东西。
DLL重定向,强制系统加载程序先从应用目录载入模块,无法找到时,才在其它目录查找。
做法:
1.将一个名字为:AppName.exe.local的文件放到应用目录。
除了创建一个.local文件,还可创建一个名为.local的文件夹。此时,可将自己的DLL保存在这个文件夹中。
// 要打开此特性
HKLM\Software\Microsoft\WindowsNT\CurrentVersion\Image File Execution Options\
加一项:
DWORD DevOverrideEnable
-模块的基地址重定位
每个可执行文件和DLL模块有一个首选基地址。
构建可执行模块时,默认下,链接器将模块基地址设为0x00400000。对DLL,默认的首选基地址为0x10000000。
如果一个可执行文件或DLL模块,不能被映射到其首选基地址,对其进行重定位耗时。
编译和链接生成DLL或EXE时,机器码中地址都按首选基地址进行了确定。
链接器在构建我们的模块时,会将重定位段嵌入到生成的文件中。
这个段包含一个字节偏移量的列表,每个字节偏移量表示一条机器指令所使用的一个内存地址。
加载程序将模块载入到首选基地址时,系统不会访问模块的重定位段。
加载程序无法将模块载入首选基地址时,系统会打开模块的重定位段,遍历其中条目,对每个条目,加载程序找到包含机器指令的那个存储页面,将模块首选基地址和模块实际映射地址的差值,加到机器指令当前使用的内存地址上。
此时,
加载程序,遍历重定位段,并修改模块大量代码,性能损耗。
修改模块代码页时,写时复制会强制这些页面以系统页交换文件为后备存储器。
构建模块时,用/FIXED使模块不包含重定位段。此时模块无法被重定位。
链接器允许我们在创建模块时,在文件头嵌入一些信息,来表示模块之所以不包含重定位信息,是因为没必要(如资源DLL)。此时,即使无法被重定位也不会报错终止。
创建一个不包含重定位信息的映像,用/SUBSYSTEM:WINDOWS, 5.0或/SUBSYSTEM:CONSOLE, 5.0。
-载入大量模块时,确定所有模块基地址的方法
1.Rebase
执行Rebase工具时,传给它一组映像文件名,它会:
a.模拟创建一个进程地址空间
b.打开应被载入到这个地址空间的所有模块,得到每个模块大小和首选基地址
c.在模拟的地址空间,对模块重定位进行模拟,使各模块没有交叠。
d.对重定位过的模块,解析该模块的重定位段,修改模块在磁盘文件中的代码
e.更新每个重定位过模块的模块头
等应用所有模块已经构建完成后,运行rebase。
运行后,可以忽略project properitys对话框的基地址,rebase会覆盖它。
不需要对随操作系统一起发布的模块重定位。
2.
BOOL ReBaseImage(
_In_ PCSTR CurrentImageName,
_In_ PCSTR SymbolPath,
_In_ BOOL fReBase,
_In_ BOOL fRebaseSysfileOk,
_In_ BOOL fGoingDown,
_In_ ULONG CheckImageSize,
_Out_ ULONG *OldImageSize,
_Out_ ULONG_PTR *OldImageBase,
_Out_ ULONG *NewImageSize,
_Inout_ ULONG_PTR *NewImageBase,
_In_ ULONG TimeStamp
);
-模块的绑定
用法:
1.Bind.exe
在执行Bind工具时,传给它一个映像文件名,它会执行下列操作:
打开映像文件导入段
对导入段的每个DLL,查看该DLL的文件头,确定该DLL的首选基地址
在DLL的导出段查看每个符号
取得符号的RVA,将它与模块的首选基地址相加,将得到的地址写入映像文件的导入段。
在映像文件的导入段添加一些额外的信息,包括映像文件被绑定到的各DLL模块的名称,各模块的时间戳。
正确的假设前提:
DLL会被载入其首选基地址。
DLL导出段所引用符号的相对位置没变化。(通过查看时间戳判断)
2.
BOOL BindImageEx(
_In_ DWORD Flags,
_In_ PSTR ImageName,
_In_ PSTR DllPath,
_In_ PSTR SymbolPath,
_In_ PIMAGEHLP_STATUS_ROUTINE StatusRoutine
);
BOOL StatusRoutine(
_In_ IMAGEHLP_STATUS_REASON Reason,
_In_ PSTR ImageName,
_In_ PSTR DllName,
_In_ ULONG_PTR Va,
_In_ ULONG_PTR Parameter
);
// DumpBin查看模块信息。