0x0、序
解析过程并没有介绍对pe结构的相关解析过程,网上此类相关资料很多可自行查阅,本文只介绍了网上资料较少的从pe结构的可选头中的数据目录表中获取dotnet目录的rva和size,到完全解析dotnet文件格式特有数据结构的部分。
了解dotnet文件格式你可能需要一款名为CFF Explorer的工具;你也可能在很多时候需要查阅书籍《Expert .NET 2.0 IL Assembler》,该书籍的中文版本名为《.NET探秘MSIL权威指南》。简要的文件格式图,可以参考下面:
分析文件的md5为:79D7AF997C9224CFF7B82E539C71FCDB。
0x1、IMAGE_COR20_HEADER结构
通过可执行文件nt头下的可选头中数据目录中最后一项获取dotnet目录的rva和size。获取的数据为:rva=0x00002008;size=0x00000048,使用stud_pe将rva地址转化为raw。得到raw地址为:0x208。读取出其中数据,数据内容如下:
00000200 48 00 00 00 02 00 05 00 H 00000210 A0 23 00 00 58 10 00 00 03 00 02 00 06 00 00 06 ? X 00000220 30 22 00 00 70 01 00 00 00 00 00 00 00 00 00 00 0" p 00000230 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00000240 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
该长度为0x48的数据的数据结构如下:
typedef struct IMAGE_COR20_HEADER { ULONG cb; USHORT MajorRuntimeVersion; USHORT MinorRuntimeVersion; //符号表和开始信息 IMAGE_DATA_DIRECTORY MetaData; //信息都在这个元数据表中可以获取。 ULONG Flags; union{ DWORD EntryPointToken; DWORD EntryPointRVA; }; //绑定信息 IMAGE_DATA_DIRECTORY Resource; IMAGE_DATA_DIRECTORY StrongNameSignature; //常规的定位和绑定信息 IMAGE_DATA_DIRECTORY CodeMagagerTable; IMAGE_DATA_DIRECTORY VTableFixups; IMAGE_DATA_DIRECTORY ExprotAddressTableJumps; IMAGE_DATA_DIRECTORY MagageNativeHeader; }IMAGE_COR20_HEADER |
通过对结构解析,读取结构中的元数据MetaData。该结构中可以得到一个元数据的rva和size。具体数据内容为:MetaData_rva=000023a0,MetaData_size= 00001058。MetaData_rva转换成MetaData_raw后为5a0。需要注意的是后面好多数据的定位都是以这个地址为基准的相对偏移。
0x2、元数据
MetaData_rva指向的数据结构如下:
typedef struct STORAGE_SIGNATURE { DWORD lSignature; WORD iMajorVersion; WORD iMinorVersion; DWORD iExtraData; DWORD iLength; BYTE iVersionString[]; //字符串编译环境版本号的长度由iLength决定,包含尾部0,且按4字节对齐。 }STORAGE_SIGNATURE; |
结构的数据内容为:
000005A0 42 53 4A 42 01 00 01 00 00 00 00 00 0C 00 00 00 BSJB 000005B0 76 34 2E 30 2E 33 30 33 31 39 00 00 v4.0.30319 |
紧跟存储标志结构后面的数据结构定义如下:
typedef struct STORAGE_HEADER { BYTE fFlags; BYTE reserved; WORD NumberOfStreams; }STORAGE_HEADER; |
结构的数据内容为:
000005B0 00 00 05 00 |
0x3、流描述表
存储头结构的最后一个成员是流的个数,在该结构后面将跟的是每一个流的描述结构,该成员有几个流个数,后面就有几个流描述结构,分析的文件流个数为5,后面将跟5个流描述结构。每个结构的定义如下:
typedef struct STREAM_HEADER { DWORD iOffset; //相对于MetaData_raw的偏移 DWORD iSize; BYTE rcName[]; //流名称字符串得长度由iSize决定,包含尾部0,且按4字节对齐。 }STREAM_HEADER; |
结构数据(不同的数据流之间已经用不同的颜色区分)为:
000005C0 6C 00 00 00 44 05 00 00 23 7E 00 00 B0 05 00 00 l D #~ ? 000005D0 8C 07 00 00 23 53 74 72 69 6E 67 73 00 00 00 00 ? #Strings 000005E0 3C 0D 00 00 88 00 00 00 23 55 53 00 C4 0D 00 00 < ? #US ? 000005F0 10 00 00 00 23 47 55 49 44 00 00 00 D4 0D 00 00 #GUID ? 00000600 84 02 00 00 23 42 6C 6F 62 00 00 00 ? #Blob |
流名称字符串中的名称是由微软定义好的字符串,不会出现非定义的字符串。已经定义的流名称有:
名称 |
含义 |
#~ |
存储压缩(优化)后的元数据信息,存在#~流后不会出现#-流。 |
#- |
存储未压缩(优化)的元数据信息,存在#-流后不会出现#~流。 |
#Strings |
存储元数据的各种字符串,比如类名称,方法名称,成员名称,参数名称等。字符串格式为UTF8格式。该流数据首部会存在一个空字符串;且此处定义的字符串最大长度不超过1024。 |
#Blob |
存储程序中的非字符串信息,包括常量值,方法的签名,强名称等。每个数据的长度由数据的前1-3位决定:0表示1字节;10表示2字节;110表示4字节。 |
#GUID |
存储所有GUID |
#US |
存储IL代码中使用的各种字符串,字符串格式为Unicode格式。 |
0x4、元数据信息流
微软定义的六个数据流中,除去一个互斥的元数据信息流外,其他的流都是可能存在的。而其他的流信息都是通过元数据信息流中的结构对其进行引用的。并且紧跟流表结构的也是元数据信息流,该地址可以通过流表中的元数据信息流流表中的相对便移地址计算得到:该流数据内容的地址相对元数据起始地址偏移为0x6c。和metadata_raw相加得到的地址为:0x60c。该地址数据结构如下:
typedef struct METADATA_HEADER { DWORD Reserved; BYTE Major; BYTE Minor; BYTE Heaps; BYTE Rid; QWORD MaskValid; QWORD Sorted; }METADATA_HEADER; |
结构下的数据内容为:
00000600 00 00 00 00 00000610 02 00 00 01 57 15 A2 15 09 01 00 00 00 FA 25 33 W ? ?3 00000620 00 16 00 00 |
结构中有两个域需要说明,分别是Heaps域和MaskValid域。
结构中MaskValid是一个位向量,每一个二进制位表示某一个特定的表存在。该域所占大小为64位。但对应的表个数为45个,所有只有低45位有意义,其他位没有含义。位和表的对应关系如下:
位数 |
表名 |
位数 |
表名 |
0 |
Module |
23 |
Property |
1 |
TypeRef |
24 |
MethodSemantics |
2 |
TypeDef |
25 |
MethodImpl |
3 |
FiledPtr |
26 |
ModuleRef |
4 |
Filed |
27 |
TypeSpec |
5 |
MethodPtr |
28 |
ImplMap |
6 |
MethodDef |
29 |
FiledRVA |
7 |
ParamPtr |
30 |
ENCLog |
8 |
Param |
31 |
ENCMap |
9 |
MethodImpl |
32 |
AssemblyRef |
10 |
MemberRef |
33 |
AssemblyProcessor |
11 |
Constant |
34 |
AssemblyOS |
12 |
CustomAttribute |
35 |
Assembly |
13 |
FieldMarshal |
36 |
AssemblyRefProcessor |
14 |
DeclSecurity |
37 |
AssemblyRefOS |
15 |
ClassLayout |
38 |
File |
16 |
FieldLayout |
39 |
ExportedType |
17 |
StandAloneSig |
40 |
ManifestResource |
18 |
EventMap |
41 |
NestedClass |
19 |
EventPtr |
42 |
GenericParam |
20 |
Event |
43 |
MethodSpec |
21 |
PropertyMap |
44 |
GenericParamConstraint |
22 |
PropertyPtr |
|
|
分析文件的MaskValid值转换成二进制数值如下:
MaskValid值(16进制) |
MaskValid值(2进制) |
0000010915A21557 |
10000100100010101101000100001010101010111 |
分析MaskValid的二进制数值,可以发现共有17个二进制位被置1,所以该文件的元数据流中存在17个类型的表。根据位对应编号得到存在的各个类型的表的序号分别为:0、1、2、4、6、8、10、12、17、21、23、24、26、28、32、35、40。查找上面的序号名称关系表可以查到对应的表名称。再通过紧跟其后的4字节数组确定表中有多少个记录。记录的个数可以确定表的长度。4字节数组内容如下:
00000620 01 00 00 00 31 00 00 00 05 00 00 00 1 00000630 05 00 00 00 0D 00 00 00 04 00 00 00 36 00 00 00 6 00000640 16 00 00 00 01 00 00 00 02 00 00 00 03 00 00 00 00000650 04 00 00 00 01 00 00 00 01 00 00 00 01 00 00 00 00000660 04 00 00 00 02 00 00 00 |
4字节数组后将是按照数组各个元素对应的记录表,各个记录表的顺序和4字节数组的中顺序相同,也是按照结构编号从小到大排列。但每个表里每个元素的结构可能并不相同。以第一个表Module表为例,如果文件存在该结构,由于该结构序号为0,所以必然为第一个结构。其记录结构如下:
typedef struct Module_Struct { WORD Generation; WORD/DWORD Name; //(在#String流中的偏移量)大小可能是word也可能是dword WORD/DWORD Mvid; //(在#GUID流中的偏移量)大小可能是word也可能是dword WORD/DWORD EncId; //(在#GUID流中的偏移量)大小可能是word也可能是dword WORD/DWORD EncBaseId; //(在#GUID流中的偏移量)大小可能是word也可能是dword }METADATA_HEADER; |
需要说明的是结构中后4个便宜量才用DWORD还是WORD由METADATA_HEADER结构中的Heaps域决定。
结构中Heaps的含义为:元数据信息流中引用其他流中数据时,索引值的大小。
Heaps数值 |
含义 |
0 |
所有索引值均采用16位索引值 |
0x01 |
表示引用#String流时,索引值均为32位 |
0x02 |
表示引用#GUID流时,索引值均为32位 |
0x04 |
表示引用#Blob流时,索引值均为32位 |
从上面的METADATA_HEADER结构中我们可以看到heaps的数值为0,索引均采用WORD类型大小,而通过4字节数组可以确定Module记录表的长度为1,所以具体的Module数据内容如下:
00000660 00 00 0A 00 01 00 00 00 00000670 00 00 |
其他记录数据同样通过该方式进行查询。
其他表中元素结构相关信息可以查阅相关资料文档《.NET探秘MSIL权威指南》或《Expert .NET 2.0 IL Assembler》附录B。
文章pdf版本、分析文件、简要文件格式图 打包下载