在笔者的上一篇文章《驱动开发:内核特征码扫描PE代码段》
中LyShark
带大家通过封装好的LySharkToolsUtilKernelBase
函数实现了动态获取内核模块基址,并通过ntimage.h
头文件中提供的系列函数解析了指定内核模块的PE节表
参数,本章将继续延申这个话题,实现对PE文件导出表的解析任务,导出表无法动态获取,解析导出表则必须读入内核模块到内存才可继续解析,所以我们需要分两步走,首先读入内核磁盘文件到内存,然后再通过ntimage.h
中的系列函数解析即可。
当PE文件执行时Windows装载器将文件装入内存并将导入表中登记的DLL文件一并装入,再根据DLL文件中函数的导出信息对可执行文件的导入表(IAT)进行修正。导出函数在DLL文件中,导出信息被保存在导出表,导出表就是记载着动态链接库的一些导出信息。通过导出表,DLL文件可以向系统提供导出函数的名称、序号和入口地址等信息,以便Windows装载器能够通过这些信息来完成动态链接的整个过程。
导出函数存储在PE文件的导出表里,导出表的位置存放在PE文件头中的数据目录表中,与导出表对应的项目是数据目录中的首个IMAGE_DATA_DIRECTORY
结构,从这个结构的VirtualAddress
字段得到的就是导出表的RVA值,导出表同样可以使用函数名或序号这两种方法导出函数。
导出表的起始位置有一个IMAGE_EXPORT_DIRECTORY
结构,与导入表中有多个IMAGE_IMPORT_DESCRIPTOR
结构不同,导出表只有一个IMAGE_EXPORT_DIRECTORY
结构,该结构定义如下:
typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics;
DWORD TimeDateStamp; // 文件的产生时刻
WORD MajorVersion;
WORD MinorVersion;
DWORD Name; // 指向文件名的RVA
DWORD Base; // 导出函数的起始序号
DWORD NumberOfFunctions; // 导出函数总数
DWORD NumberOfNames; // 以名称导出函数的总数
DWORD AddressOfFunctions; // 导出函数地址表的RVA
DWORD AddressOfNames; // 函数名称地址表的RVA
DWORD AddressOfNameOrdinals; // 函数名序号表的RVA
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
上面的_IMAGE_EXPORT_DIRECTORY
结构如果总结成一张图,如下所示:
在上图中最左侧AddressOfNames
结构成员指向了一个数组,数组里保存着一组RVA,每个RVA指向一个字符串即导出的函数名,与这个函数名对应的是AddressOfNameOrdinals
中的结构成员,该对应项存储的正是函数的唯一编号并与AddressOfFunctions
结构成员相关联,形成了一个导出链式结构体。
获取导出函数地址时,先在AddressOfNames
中找到对应的名字MyFunc1
,该函数在AddressOfNames
中是第1项,然后从AddressOfNameOrdinals
中取出第1项的值这里是1,然后就可以通过导出函数的序号AddressOfFunctions[1]
取出函数的入口RVA,然后通过RVA加上模块基址便是第一个导出函数的地址,向后每次相加导出函数偏移即可依次遍历出所有的导出函数地址。
其解析过程与应用层基本保持一致,如果不懂应用层如何解析也可以去看我以前写过的《PE格式:手写PE结构解析工具》
里面具体详细的分析了解析流程。
首先使用InitializeObjectAttributes()
打开文件,打开后可获取到该文件的句柄,InitializeObjectAttributes
宏初始化一个OBJECT_ATTRIBUTES
结构体, 当一个例程打开对象时由此结构体指定目标对象的属性,此函数的微软定义如下;
VOID InitializeObjectAttributes(
[out] POBJECT_ATTRIBUTES p, // 权限
[in] PUNICODE_STRING n, // 文件名
[in] ULONG a, // 输出文件
[in] HANDLE r, // 权限
[in, optional] PSECURITY_DESCRIPTOR s // 0
);
当权限句柄被初始化后则即调用ZwOpenFile()
打开一个文件使用权限FILE_SHARE_READ
打开,打开文件函数微软定义如下;
NTSYSAPI NTSTATUS ZwOpenFile(
[out] PHANDLE FileHandle, // 返回打开文件的句柄
[in] ACCESS_MASK DesiredAccess, // 打开的权限,一般设为GENERIC_ALL。
[in] POBJECT_ATTRIBUTES ObjectAttributes, // OBJECT_ATTRIBUTES结构
[out] PIO_STATUS_BLOCK IoStatusBlock, // 指向一个结构体的指针。该结构体指明打开文件的状态。
[in] ULONG ShareAccess, // 共享的权限。可以是FILE_SHARE_READ 或者 FILE_SHARE_WRITE。
[in] ULONG OpenOptions // 打开选项,一般设为 FILE_SYNCHRONOUS_IO_NONALERT。
);
接着文件被打开后,我们还需要调用ZwCreateSection()
该函数的作用是创建一个Section
节对象,并以PE结构中的SectionALignment
大小对齐映射文件,其微软定义如下;
NTSYSAPI NTSTATUS ZwCreateSection(
[out] PHANDLE SectionHandle, // 指向 HANDLE 变量的指针,该变量接收 section 对象的句柄。
[in] ACCESS_MASK DesiredAccess, // 指定一个 ACCESS_MASK 值,该值确定对 对象的请求访问权限。
[in, optional] POBJECT_ATTRIBUTES ObjectAttributes, // 指向 OBJECT_ATTRIBUTES 结构的指针,该结构指定对象名称和其他属性。
[in, optional] PLARGE_INTEGER MaximumSize, // 指定节的最大大小(以字节为单位)。
[in] ULONG SectionPageProtection, // 指定要在 节中的每个页面上放置的保护。
[in] ULONG AllocationAttributes, // 指定确定节的分配属性的SEC_XXX 标志的位掩码。
[in, optional] HANDLE FileHandle // (可选)指定打开的文件对象的句柄。
);
最后读取导出表就要将一个磁盘中的文件映射到内存中,内存映射核心文件时ZwMapViewOfSection()
该系列函数在应用层名叫MapViewOfSection()
只是一个是内核层一个应用层,这两个函数参数传递基本一致,以Zw