selph
selph
发布于 2021-10-13 / 1282 阅读
0
0

DLL加载过程初探

前言

前两天刷手机的时候,无意间想到了一些问题:存储DLL信息的_LDR_DATA_TABLE_ENTRY结构体是在什么时候填充的?是在Dllmain运行之前还是之后呢?这个结构体的作用是什么?本文记录一下探究的过程,如有不对望师傅们指出!

实验环境:物理机Windows10 x64 21H2

测试程序:32位/64位

32位分析

动态加载通常使用LoadLibrary进行,一般VS2019默认是W后缀,就从W来分析吧(下文64位同)

LoadLibraryW

LoadLibraryW函数位于kernelBase.dll里

image-20211013094913918

这个函数是LoadLibraryExW的再次封装,这里直接凑齐参数调用ExW函数了,这里有三个参数:

  • DLL路径,
  • hFile,保留参数,必须是NULL
  • dwFlag,可以是NULL,会完整加载DLL,通过设定Flag可以进行部分加载,具体值在MSDN上有

参考自:LoadLibraryExW function (libloaderapi.h) - Win32 apps | Microsoft Docs

LoadLibraryExW

LoadLibraryExW函数位于kernelBase.dll里

image-20211013095040737

刚开始先判断hFile参数和dwFlag参数,如果是不合法的值,就跳走返回

image-20211013095134931

接下来将Ring3的字符串转换成R0的UNICODE_STRING结构体,转换失败就跳转返回,转换成功则进行末尾去0操作,接下来一切正常的话,会跳转走

image-20211013095356138

接下来是Flag值的判断,如果指定位有值,就跳去执行相应的操作,最后会来到LdrLoadDll函数(下文会介绍),第四个参数传址接收加载模块的模块地址,执行完之后,会返回一个值到eax,然后跳转

image-20211013095621981

如果返回值不是负数,也就是执行成功了,就再次跳转

image-20211013095709277

这里从地址里获取得到的模块地址,然后返回

LdrLoadDll

LdrLoadDll函数位于ntdll.dll里

image-20211013095947451

这个函数刚开始用局部变量接收了传入的BaseAddress地址

image-20211013100237722

往下走有一个函数调用,传入了空的LdrEntry结构体(通过动态调试发现这是LdrEntry结构体的)来接收数据,函数(下文介绍)执行完成之后,这个结构体会被填充好

image-20211013124500676

再之后,从这个结构体的18h偏移处(偏移0x18是Dllbase,就是模块地址)取出值设置到BaseAddress的地址里返回

LdrpLoadDllInternal

LdrpLoadDllInternal函数位于ntdll.dll里

image-20211013125034633

进入函数以后,会先判断dll是不是已经加载了,已经加载就获取其模块信息结构并返回,如果不是,会走到这里这里的var_20是局部变量,也是个LdrEntry结构(通过动态调试发现的),把var_20的地址给了eax,然后调用了这个函数

image-20211013125313605

然后把var_20的地址给了参数的LdrEntry赋值,之后操作var_20就是实际操作参数了,程序运行到LdrpPrepareModuleForExecution函数之前,LdrEntry结构体已经是填充完毕的状态了,LdrpPrepareModuleForExecution函数会进一步的进行DLL相关初始化操作,并执行DllMain的DLL_PROCESS_ATTACH分支程序,然后函数差不多就结束了,本次的分析目标已经达成了,就不往下分析了

64位分析

LoadLibraryW

image-20211013131357920

64位程序简洁好多啊

LoadLibraryExW

image-20211013131500427

类比32位的看吧,流程是一样的,这里进入LdrLoadDll,传入了BaseAddress的指针,作为第四个参数(r9)

LdrLoadDll

开始先把BaseAddress指针从r9存到了r14,然后就是这里:

image-20211013155141941

把空的LdrEntry(准确来说,不是空的,里面前10h字节有值,可能DllBase也有,也可能没有)给r9传参,通过LdrpLoadDll填充了该结构,然后通过偏移取得其中的DllBase,并返回

LdrpLoadDll

image-20211013163744493

这里把结构体地址给到了局部变量上保存,通过rsi传参调用了LdrpLoadDllInternal函数

LdrpLoadDllInternal

image-20211013163849768

首先进来后,先把结构体存在了rsp+20的位置上,然后入栈5个参数,rsp抬高50h字节,这个时候结构体存在了rsp+78h的位置

image-20211013163958780

顺利的话,后面会进到这一块来,继续进行初始化操作,这个函数跟32位的一样,继续初始化DLL运行DllMain函数

到此,64位的分析也到此结束了

总结

LoadLibrary API动态加载DLL的时候,会先进行LdrEntry结构体的填充,然后再继续执行DLL初始化操作和执行DllMain函数,该API返回的模块句柄其实就是模块地址,是在LdrLoadDll这一层从LdrEntry中找到的值

据查阅资料,进程会维护一个“模块数据库”,用来存储模块信息,模块信息结构是LDR_DATA_TABLE_ENTRY,所有的模块信息通过双向链表连接在一起,进程寻找模块中的内容的时候,会基于该结构获取的模块基址进行寻址

参考资料:《Windows 核心编程》《深入解析Windows操作系统》


评论