使用libvmi技术在R0层监控进程调用的dll库和dll版本

一、问题提出

在Windows操作系统中,dll属性查看的版本信息是在R3层获取的,如下图所示,在项目中经常要保证监控获取的透明性,能否在R0层直接获取这些信息?

二、libvmi简介

libvmi是Google的一个开源项目,利用Memory introspection技术在Dom0中监视DomU的情况。由于项目的某些需求,需要透明地监控Windows进程的列表,进程调用的dll库和版本。对于特定的操作系统版本,libvmi给的默认配置文件libvmi-example.conf中详细的配置参数,例如Windows 7的32位系统默认是

libvmi中个重要的函数:给定进程的虚拟地址空间,直接读取对应位数的数据。

vmi_read_32_vavmi_instance_t vmi addr_t vaddrvmi_pid_t pid, uint64_t *value

[in]vmilibvmi实例

[in]vaddr: 读取的虚拟地址基地址

[in]pid:进程的pid

[out]value:读取的值

三、获取进程调用的dll基地址

首先使用libvmi自带的例子process-list.c得到进程的列表,以及每个进程的基地址,该例子基本思路是使用vmi_read_addr_ksym这个函数获取PsActiveProcessHead,然后再得到系统中的进程双向链表,获得每个进程的EPROCESS结构体基地址和进程PID。后绪的就是利用上面libvmi函数直接去读内存,遍历进程地址空间(代码没整理好,有点乱,整理好再补充上去,这里使用windbg遍历一次,知道思路写代码就容易了)。

首先找到要监控的具体的进程基地址,然后开始下列步骤找到模块链表:

  1. 通过进程的EPROCESS获取对应的_PEB结构
  2. 通过_PEB找到_PEB_LDR_DATA
  3. 在_PEB_LDR_DATA内部有3个LIST_ENTRY,这也就是进程加载的模块信息的链表头,_PEB_LDR_DATA内部的3个链表就是进程调用的所有dll库,而这三个链表的区别就是dll的排列顺序不同,里面的内容是一致的。
  4. 知道了链表头,只需要知道链表的结点就行了,需要遍历的结点是_LDR_DATA_TABLE_ENTRY结构,在这个结构中有一个FullDllName,也就是模块的路径,就是我们要找的。

下面以notepad为例,使用windbg查看遍历,步骤如下:

  1. 命令!process 0 0 notepad.exe

  1. Peb的地址是小于0x80000000,属于用户地址空间,是不能直接访问的,直接访问的话看到的都是问号,所以切换到这个应用程序后才能访问地址空间,使用下面的命令切换到notepad.exe这个进程,.process /p /r 0x93c85470
  2. 已经切换到notepad.exe,命令dt _EPROCESS 93c85470查看EPROCESS,可以看到Peb的地址7ffdf000,使用命令dt _PEB 0x7ffdf000查看PEB的信息,如图所示,在0x00c处找到了_PEB_LDR_DATA

 

  1. 继续跟进_PEB_LDR_DATA,使用命令dt _PEB_LDR_DATA 0x77827880,进程的三个链表也就是这个了。

  1. 我们已经找到了链表的头部,以InLoadOrderModuleList为例,dt _LDR_DATA_TABLE_ENTRY 0x2219f0,第一个模块是notepad.exe

  1. 继续找下一个模块dt _LDR_DATA_TABLE_ENTRY 0x221a78

遍历到ntdll.dll,继续按照这种方法可以遍历该进程所有的模块。这里以ntdll为例探索,如图,它的基地址是0x77750000。PE文件的格式如下图所示:

四、找资源字节段

下面开始遍历dll文件:

  1. 首先是DOS头,使用命令dt _IMAGE_DOS_HEADER 0x77750000

 

  1. 里面最重要的一个字段,e_lfanew:是32位可执行文件的扩展域,用来表示DOS头之后的NT头相对文件起始地址的偏移。
  2. 使用命令dt /r1 ntdll!_IMAGE_NT_HEADERS notepad+e0查看PE的NT头结构

可以看到在_IMAGE_NT_HEADERS里面的0x018偏移OptionalHeader的偏移0x060处有一个DataDiretory数组,数组的下标和对应的类型如下图所示,下标为2的代表资源段(.rsrc):

  1. 跟进这个结构体_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;

他想表达的策略是:

  1. 当Name的最高位(Bit31)是1时:Name[30:0]是一个offset指向IMAGE_RESOURCE_DIR_STRING的结构,Bit31位为0时,Name表示一个ID
  2. 当OffsetToData的最高位Bit31为1时,OffsetToData[30:0]是一个Offset指向下一个Directory结构,Bit31位为0时,OffsetToData仍然是一个Offset值,它指向一个IMAGE_RESOURCE_DATA_ENTRY结构。
  3. 总的来说,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 域作为限定范围遍历完。

猜你喜欢

转载自blog.csdn.net/qq_24264221/article/details/82319488