前言
前两天刷手机的时候,无意间想到了一些问题:存储DLL信息的_LDR_DATA_TABLE_ENTRY
结构体是在什么时候填充的?是在Dllmain运行之前还是之后呢?这个结构体的作用是什么?本文记录一下探究的过程,如有不对望师傅们指出!
实验环境:物理机Windows10 x64 21H2
实验工具:IDA
测试程序:32位/64位
32位分析
动态加载通常使用LoadLibrary进行,一般VS2019默认是W后缀,就从W来分析吧(下文64位同)
LoadLibraryW
LoadLibraryW函数位于kernelBase.dll里
这个函数是LoadLibraryExW的再次封装,这里直接凑齐参数调用ExW函数了,这里有三个参数:
- DLL路径,
- hFile,保留参数,必须是NULL
- dwFlag,可以是NULL,会完整加载DLL,通过设定Flag可以进行部分加载,具体值在MSDN上有
参考自:LoadLibraryExW function (libloaderapi.h) - Win32 apps | Microsoft Docs
LoadLibraryExW
LoadLibraryExW函数位于kernelBase.dll里
刚开始先判断hFile参数和dwFlag参数,如果是不合法的值,就跳走返回
接下来将Ring3的字符串转换成R0的UNICODE_STRING结构体,转换失败就跳转返回,转换成功则进行末尾去0操作,接下来一切正常的话,会跳转走
接下来是Flag值的判断,如果指定位有值,就跳去执行相应的操作,最后会来到LdrLoadDll函数(下文会介绍),第四个参数传址接收加载模块的模块地址,执行完之后,会返回一个值到eax,然后跳转
如果返回值不是负数,也就是执行成功了,就再次跳转
这里从地址里获取得到的模块地址,然后返回
LdrLoadDll
LdrLoadDll函数位于ntdll.dll里
这个函数刚开始用局部变量接收了传入的BaseAddress地址
往下走有一个函数调用,传入了空的LdrEntry结构体(通过动态调试发现这是LdrEntry结构体的)来接收数据,函数(下文介绍)执行完成之后,这个结构体会被填充好
再之后,从这个结构体的18h偏移处(偏移0x18是Dllbase,就是模块地址)取出值设置到BaseAddress的地址里返回
LdrpLoadDllInternal
LdrpLoadDllInternal函数位于ntdll.dll里
进入函数以后,会先判断dll是不是已经加载了,已经加载就获取其模块信息结构并返回,如果不是,会走到这里这里的var_20是局部变量,也是个LdrEntry结构(通过动态调试发现的),把var_20的地址给了eax,然后调用了这个函数
然后把var_20的地址给了参数的LdrEntry赋值,之后操作var_20就是实际操作参数了,程序运行到LdrpPrepareModuleForExecution函数之前,LdrEntry结构体已经是填充完毕的状态了,LdrpPrepareModuleForExecution函数会进一步的进行DLL相关初始化操作,并执行DllMain的DLL_PROCESS_ATTACH
分支程序,然后函数差不多就结束了,本次的分析目标已经达成了,就不往下分析了
64位分析
LoadLibraryW
64位程序简洁好多啊
LoadLibraryExW
类比32位的看吧,流程是一样的,这里进入LdrLoadDll,传入了BaseAddress的指针,作为第四个参数(r9)
LdrLoadDll
开始先把BaseAddress指针从r9存到了r14,然后就是这里:
把空的LdrEntry(准确来说,不是空的,里面前10h字节有值,可能DllBase也有,也可能没有)给r9传参,通过LdrpLoadDll填充了该结构,然后通过偏移取得其中的DllBase,并返回
LdrpLoadDll
这里把结构体地址给到了局部变量上保存,通过rsi传参调用了LdrpLoadDllInternal函数
LdrpLoadDllInternal
首先进来后,先把结构体存在了rsp+20的位置上,然后入栈5个参数,rsp抬高50h字节,这个时候结构体存在了rsp+78h的位置
顺利的话,后面会进到这一块来,继续进行初始化操作,这个函数跟32位的一样,继续初始化DLL运行DllMain函数
到此,64位的分析也到此结束了
总结
LoadLibrary API动态加载DLL的时候,会先进行LdrEntry结构体的填充,然后再继续执行DLL初始化操作和执行DllMain函数,该API返回的模块句柄其实就是模块地址,是在LdrLoadDll这一层从LdrEntry中找到的值
据查阅资料,进程会维护一个“模块数据库”,用来存储模块信息,模块信息结构是LDR_DATA_TABLE_ENTRY
,所有的模块信息通过双向链表连接在一起,进程寻找模块中的内容的时候,会基于该结构获取的模块基址进行寻址
参考资料:《Windows 核心编程》《深入解析Windows操作系统》