一、问题提出
在Windows操作系统中,dll属性查看的版本信息是在R3层获取的,如下图所示,在项目中经常要保证监控获取的透明性,能否在R0层直接获取这些信息?
二、libvmi简介
libvmi是Google的一个开源项目,利用Memory introspection技术在Dom0中监视DomU的情况。由于项目的某些需求,需要透明地监控Windows进程的列表,进程调用的dll库和版本。对于特定的操作系统版本,libvmi给的默认配置文件libvmi-example.conf中详细的配置参数,例如Windows 7的32位系统默认是
libvmi中个重要的函数:给定进程的虚拟地址空间,直接读取对应位数的数据。
vmi_read_32_va(vmi_instance_t vmi, addr_t vaddr,vmi_pid_t pid, uint64_t *value)
[in]vmi:libvmi实例
[in]vaddr: 读取的虚拟地址基地址
[in]pid:进程的pid
[out]value:读取的值
三、获取进程调用的dll基地址
首先使用libvmi自带的例子process-list.c得到进程的列表,以及每个进程的基地址,该例子基本思路是使用vmi_read_addr_ksym这个函数获取PsActiveProcessHead,然后再得到系统中的进程双向链表,获得每个进程的EPROCESS结构体基地址和进程PID。后绪的就是利用上面libvmi函数直接去读内存,遍历进程地址空间(代码没整理好,有点乱,整理好再补充上去,这里使用windbg遍历一次,知道思路写代码就容易了)。
首先找到要监控的具体的进程基地址,然后开始下列步骤找到模块链表:
- 通过进程的EPROCESS获取对应的_PEB结构
- 通过_PEB找到_PEB_LDR_DATA
- 在_PEB_LDR_DATA内部有3个LIST_ENTRY,这也就是进程加载的模块信息的链表头,_PEB_LDR_DATA内部的3个链表就是进程调用的所有dll库,而这三个链表的区别就是dll的排列顺序不同,里面的内容是一致的。
- 知道了链表头,只需要知道链表的结点就行了,需要遍历的结点是_LDR_DATA_TABLE_ENTRY结构,在这个结构中有一个FullDllName,也就是模块的路径,就是我们要找的。
下面以notepad为例,使用windbg查看遍历,步骤如下:
- 命令!process 0 0 notepad.exe
- Peb的地址是小于0x80000000,属于用户地址空间,是不能直接访问的,直接访问的话看到的都是问号,所以切换到这个应用程序后才能访问地址空间,使用下面的命令切换到notepad.exe这个进程,.process /p /r 0x93c85470
- 已经切换到notepad.exe,命令dt _EPROCESS 93c85470查看EPROCESS,可以看到Peb的地址7ffdf000,使用命令dt _PEB 0x7ffdf000查看PEB的信息,如图所示,在0x00c处找到了_PEB_LDR_DATA
- 继续跟进_PEB_LDR_DATA,使用命令dt _PEB_LDR_DATA 0x77827880,进程的三个链表也就是这个了。
- 我们已经找到了链表的头部,以InLoadOrderModuleList为例,dt _LDR_DATA_TABLE_ENTRY 0x2219f0,第一个模块是notepad.exe
- 继续找下一个模块dt _LDR_DATA_TABLE_ENTRY 0x221a78
遍历到ntdll.dll,继续按照这种方法可以遍历该进程所有的模块。这里以ntdll为例探索,如图,它的基地址是0x77750000。PE文件的格式如下图所示:
四、找资源字节段
下面开始遍历dll文件:
- 首先是DOS头,使用命令dt _IMAGE_DOS_HEADER 0x77750000
- 里面最重要的一个字段,e_lfanew:是32位可执行文件的扩展域,用来表示DOS头之后的NT头相对文件起始地址的偏移。
- 使用命令dt /r1 ntdll!_IMAGE_NT_HEADERS notepad+e0查看PE的NT头结构
可以看到在_IMAGE_NT_HEADERS里面的0x018偏移OptionalHeader的偏移0x060处有一个DataDiretory数组,数组的下标和对应的类型如下图所示,下标为2的代表资源段(.rsrc):
- 跟进这个结构体_IMAGE_DATA_DIRECTORY继续看,dt _IMAGE_DATA_DIRECTORY 0x77750000+0n208+0x018+0x060+0x008+0x008(0n208+0x018+0x060代表DataDirectory第一个元素的基地址,每个成员大小是0x008)。
可以看到VirtualAddress的值是0xe0000,这个值代表.rsrc字节段相对于dll基地址的偏移量。它的大小是0x560d8。
五、获取dll文件版本
找到这个偏移量后,就可以对它进行遍历了,首先看看resource table的结构:
resource table的整体结构组成成员
1.IMAGE_RESOURCE_DIRECTORY结构(简称Directory)
2.IMAGE_RESOURCE_DIRECTORY_ENTRY结构(简称Entry)
3.IMAGE_RESOURCE_DIRECTORY_STRING结构(或者IMAGE_RESOURCE_DIR_STRING_U)
4.IMAGE_RESOURCE_DATA_ENTRY结构
整个resource结构是个树型结构,只有一个root根节点,然后长出许多的树枝,最后是叶子。根部和每个树枝是一个节点,该节点由IMAGE_RESOURCE_DIRECTORY结构和IMAGE_RESOURCE_DIRECTORY_ENTRY结构组成,并且他们是紧密排列的,而IMAGE_RESOURCE_DIRECTORY_STRING和IMAGE_RESOURCE_DATA_ENTRY都是整个资源树的叶子。
资源树的节点由IMAGE_RESOURCE_DIRECTORY与IMAGE_RESOURCE_DIRECTORY_ENTRY构成,他们的关系是:
1.由Directory指出有多少个Entry
2.然后由每个Entry延伸出一个Directory或者最终的Data数据。
下面来看具体的_IMAGE_RESOURCE_DIRECTORY的具体结构
这个结构里的Characteristics,MajorVersion以及MinorVersion一直为0不使用。NumberOfNamedEntries代表一个数量,以Name作为子树的个数。NameOfIdEntries代表一个数量,以ID作为子树的个数。而Directory引出的Entry数量是NumberOfNamedEntries和NumberOfIdEntries之和。并且Entry是排列紧密的。这里注意到Directory的大小是0x010。
Entry表组成
在WinNT.h文件中,Entry结构定义为
这个结构看上去很复杂,实际上他成员只有两个DEWORD,供8个字节,如下:
DWORD Name;
DWORD OffsetToData;
他想表达的策略是:
- 当Name的最高位(Bit31)是1时:Name[30:0]是一个offset指向IMAGE_RESOURCE_DIR_STRING的结构,Bit31位为0时,Name表示一个ID值
- 当OffsetToData的最高位Bit31为1时,OffsetToData[30:0]是一个Offset指向下一个Directory结构,Bit31位为0时,OffsetToData仍然是一个Offset值,它指向一个IMAGE_RESOURCE_DATA_ENTRY结构。
- 总的来说,Entry的职责是给出下一个Directory或Data,从资源树的角度来看,Entry要么给出下一个节点,要么给出叶子。Entry中当Name表示ID值的时候,它的类型如下:也就是说,dll版本信息的ID值是16,对应的16进制是0x10。
由于只有3个Entry(Entry大小是0x008),我们遍历完,如下图所示:可以看到在第三个Entry是dll版本相关的信息(Name为0x10)。
根据偏移量找到下一个Directory的位置,偏移量0x58,如上图所示,它只有1个节点。按照上面的方式继续跟进:
终于找到了叶子节点,它的偏移量是0xd8。这里找到dll版本信息的Raw data区域,偏移量是 0xe00f0,大小是0x380。
使用命令:db 0x77750000+0xe00f0 L380可以dump出该便宜量的0x380大小的所有字节数据。
获得了版本数据,还没有结束,还需要解析这一段数据,抽取里面的内容。
六、解析版本信息数据
位于版本信息数据起始位置的是一个VS_VERSIONINFO类型的结构,其定义如下
typedef struct tag_VS_VERSIONINFO
{
WORD wLength; // 00 length of entire version resource
WORD wValueLength; // 02 length of fixed file info, if any
WORD wType; // 04 type of resource (1 = text, 0 = binary)
WCHAR szKey[16]; // 06 key -- VS_VERSION_INFO
WORD Padding1; // 26 padding byte 1
VS_FIXEDFILEINFO Value; // 28 fixed information about this file (13 dwords)
} VS_VERSIONINFO, *PVS_VERSIONINFO; // 5C
wLength:是一个WORD类型的数,表示整个版本信息资源数据块的字节数;
wValueLength指示成员VS_FIXEDFILEINFO Value的字节数;如果当前版本信息结构体未指定Value成员,则该值为0;
wType:表示资源类型,值为1表示文本,值为2表示二进制数据;
szKey:是一个char类型的元素个数为16的字符数组,其值始终为L”VS_VERSION_INFO”,并且最后一个元素为0值;
value:是一个VS_FIXEDFILEINFO类型的结构,存储当前版本信息结构指定的任意数据,其值目前可以无需过多关注,除了其中的DWOR dwSignature成员,其值恒为0xFEEF04BD。根据结构体对齐原则,在szKey和Value之间有2字节的间隔;
Padding1:占位的作用。
在此之后是VS_VERSIONINFO结构的子节点,它的子节点是由StringFileInfo结构体和VarFileInfo结构体组成的数组,而且他们扔收结构地对齐的约束,这两个数组都有可能为0,并且其先后顺序没有固定,我们需要获得的是StringFileInfo结构体。因此这里提供一个结构体对齐的函数:
// offset: 当前成员对于文件缓冲区基地址的偏移
// base: 当前结构体起始地址对于文件缓冲区基地址的偏移
#define DWORD_ALIGN(offset, base) (((offset + base + 3) & 0xfffffffcL) - (base & 0xfffffffcL))
首先需要判断是不是VarFileInfo结构体,该结构体定义如下:
typedef struct {
WORD wLength;
WORD wValueLength;
WORD wType;
WCHAR szKey[12]; // WCHAR L"VarFileInfo"
// WORD Padding;
Var Children[1];
} VarFileInfo;
这里有两个关键字段:
szKey:恒为L”VarFileInfo”字符串的WCHAR字符数组,所以只需要判断数组的内容是否为L”VarFileInfo”字符串,若是,直接根据wLength的长度跳过。
wLength:该结构体的大小
StringFileInfo结构体和VarFileInfo结构体类似,区别在于,它的szKey是一个大小为15的字符数组,值恒为L”StringFileInfo”,最后一个成员由Var Children的数组变成StringTable类型的变长Children数组,长度至少为1,具体的版本信息就包含在StringTable成员中。
typedef struct {
WORD wLength;
WORD wValueLength;
WORD wType;
WCHAR szKey[9]; // WCHAR L"88888888"
// WORD Padding;
String Children[1];
} StringTable;
数组中每个不同的 StringTable 元素表示各个不同语言的版本信息,StringTable 结构的 szKey 成员表示该 StringTable 数据块中展示文本的语言编码和代码页。现在按通常情况定位到数组的第 0 个元素。如果有解析多语言版本 PE 文件的特殊需求,可针对不同语言的版本信息,对数组中每个 StringTable 元素单独解析。
定位到当前 StringTable 结构的 Children[] 成员。该成员是一个 String 类型的变长数组。需要注意的是,这里的 String 类型并非 C++ 中定义的 std::string 数据类型,而只是 PE 文件结构定义中的一种结构体类型。
typedef struct {
WORD wLength;
WORD wValueLength;
WORD wType;
WCHAR szKey[1]; // WCHAR L"String"
// WORD Padding;
// WCHAR Value[1];
} String;
与前面的各个结构体定义不同的是,String 结构中的 szKey 是个不定长的 WCHAR 字符数组,其内容表示当前版本信息类型的名称。其可取的内容如下:
L"Comments" // 为诊断信息展示的附加信息
L"CompanyName" // 公司名称
L"FileDescription" // 文件描述
L"FileVersion" // 文件版本
L"InternalName" // 内部名称
L"LegalCopyright" // 版权信息
L"LegalTrademarks" // 应用于该文件的商标或注册商标
L"OriginalFilename" // 原始文件名
L"PrivateBuild" // PrivateBuild *
L"ProductName" // 产品名称
L"ProductVersion" // 产品版本
L"SpecialBuild" // SpecialBuild *
获取版本信息类型名称后,绕过该字符数组长度的内存地址,定位到 String 结构的 WORD Value 成员位置。成员 Value 由于它前面的 szKey 成员导致是一个不固定位置的成员。所以只能根据 szKey 的长度来辅助定位。注意字节对齐。
Value 成员是一个以 0 结尾的 WCHAR 字符数组。其内容则是当前版本信息类型的值,长度通过 wValueLength 成员指示。
下一个 String 元素紧随当前 Value 成员的结尾之后,通过宏 DWORD_ALIGN 获取其地址偏移之后,计算其实际内存地址,并根据与前面同样的获取方法,获取下一个版本信息类型的内容。可根据当前 StringTable 结构的 wLength 域作为限定范围遍历完。