-
-
[分享]转四篇关于MS.Net CLR扩展PE结构分析的文章
-
发表于: 2006-8-30 23:15 5512
-
分析的相当详细,如其中的第一篇
MS.Net CLR扩展PE结构分析 (1)
作者:Flier Lu <flier@nsfocus.com>
主页:http://www.nsfocus.com
日期:2002-08-16
1.概述
本文从系统底层角度,通过分析MS.Net CLR架构在Win32平台上对PE可执行文件
映像结构的扩展,解析CLR架构底层的部分运行机制,帮助读者从更深层次理解CLR架构
中某些重要概念。
本文读者应具备基本的Win32编程经验,了解CLR中常见概念意义,并对Win32平台
之PE可执行文件映像结构有一定了解,具体说明请参看Matt Pietrek于1994年3月发表在
MSJ上的经典文章《Peering Inside the PE: A Tour of the Win32 Portable
Executable File Format》,与之重复的部分本文一概跳过。
2.前言
对一个优秀Win32平台程序员来说,对PE结构的了解是对Win32架构了解的必经之路,
而从Chicago(Win95的开发代号,Win95正式发布以前的文档中对Win95的称呼)以来,
PE结构就相对稳定,直到MS.Net的出现,才发生了一些不大不小的变化。
之所以说是不大不小的变化,是因为CLR并没有对PE结构进行结构上的调整,只是利用现有
PE结构的优良可扩展性,将CLR所需的信息扩展到PE映像中。具体一点说,就是利用了PE结构中的
IMAGE_OPTIONAL_HEADER.DataDirectory[IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR]
来保存服务于CLR的IMAGE_COR20_HEADER结构,其它的PE结构基本不变。
3.CLR 头信息
与传统PE可执行文件一样,CLR程序也有一个文件头保存CLR相关的全局性信息。
但因为要保证现有PE结构的稳定性,CLR程序仅使用PE结构的一个Directory Entry
也就是前言中提到的IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR。
此入口原本是设计用于COM,但不知为何一直没有被使用。
3.1 IMAGE_COR20_HEADER
IMAGE_COR20_HEADER结构的定义,可以在FrameworkSDK\include\CorHdr.h
头文件中找到,如下:
// CLR 2.0 header structure.
typedef struct IMAGE_COR20_HEADER
{
// Header versioning
ULONG cb;
USHORT MajorRuntimeVersion;
USHORT MinorRuntimeVersion;
// Symbol table and startup information
IMAGE_DATA_DIRECTORY MetaData;
ULONG Flags;
ULONG EntryPointToken;
// Binding information
IMAGE_DATA_DIRECTORY Resources;
IMAGE_DATA_DIRECTORY StrongNameSignature;
// Regular fixup and binding information
IMAGE_DATA_DIRECTORY CodeManagerTable;
IMAGE_DATA_DIRECTORY VTableFixups;
IMAGE_DATA_DIRECTORY ExportAddressTableJumps;
// Precompiled image info (internal use only - set to zero)
IMAGE_DATA_DIRECTORY ManagedNativeHeader;
} IMAGE_COR20_HEADER;
详细的说明信息,可以在FrameworkSDK\Tool Developers Guide\docs
目录中找到。此结构虽然字段较多,但实际上其核心在于MetaData,其他信息都是围绕着
MetaData服务。
3.2 实例分析
下面我们分析两个实际的CLR程序的Cor20Header结构,wsdl.exe是.Net Framework
提供的wsdl分析工具、MgrCpp.exe是一个简单的VC.Net编写的Managed C++和
Unmanaged C++并存的程序,MgrCpp.cpp 代码如下
#include "stdafx.h"
#using <mscorlib.dll>
#include <tchar.h>
#include <iostream>
using namespace System;
// This is the entry point for this application
int _tmain(void)
{
// TODO: Please replace the sample code below with your own.
Console::WriteLine(S"Hello World");
std::cout << "Hello World" << std::endl;
return 0;
}
wsdl.exe 可以用Dumpbin /all wsdl.exe > wsdl.dump 导出,关键数据如下
...
500000 image base (00500000 to 00511FFF)
...
2008 [ 48] RVA [size] of COM Descriptor Directory
...
RAW DATA #1
00502000: ?? ?? ?? ?? ?? ?? ?? ?? 48 00 00 00 02 00 00 00 àü......H.......
00502010: F8 67 00 00 B0 74 00 00 09 00 00 00 21 00 00 06 g..°t......!...
00502020: 09 51 00 00 8E 16 00 00 50 20 00 00 80 00 00 00 .Q......P ......
00502030: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00502040: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
MgrCpp.exe 类似处理,关键数据如下
...
400000 image base (00400000 to 0045CFFF)
...
4B24C [ 48] RVA [size] of COM Descriptor Directory
...
0044B240: ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? 48 00 00 00 @.@.à#@.H...
0044B250: 02 00 00 00 70 EB 04 00 D0 42 00 00 02 00 00 00 ....p..DB......
0044B260: 73 00 00 06 00 00 00 00 00 00 00 00 00 00 00 00 s...............
0044B270: 00 00 00 00 00 00 00 00 00 00 00 00 78 2E 05 00 ............x...
0044B280: C0 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 à...............
0044B290: 00 00 00 00 ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ....1E.@?@.....
3.3 字段分析
cb(0x48)是IMAGE_COR20_HEADER结构的大小。
MajorRuntimeVersion(0x02)和MinorRuntimeVersion(0x00)是版本号,
指执行此程序所需的最低CLR版本号。
Metadata是核心数据所在,我们等会再详细分析。
Flags是Runtime Image描述标志,描述此CLR映像的执行属性。wsdl.exe的Flags
是0x09或1001B,MgrCpp.exe的Flags是0x02或0010B,具体含义如下:
// CLR Header entry point flags.
COMIMAGE_FLAGS_ILONLY =0x00000001,
COMIMAGE_FLAGS_32BITREQUIRED =0x00000002,
COMIMAGE_FLAGS_IL_LIBRARY =0x00000004,
COMIMAGE_FLAGS_STRONGNAMESIGNED =0x00000008,
COMIMAGE_FLAGS_TRACKDEBUGDATA =0x00010000,
COMIMAGE_FLAGS_ILONLY 标志位说明此CLR程序由纯IL代码组成,IL代码是类似
Java的Bytecode的中间代码,由纯IL代码组成就代表此程序可以在任何实现了CLR环境
的平台,如支持CLI的FreeBSD上执行。一般由C#之类纯.Net语言生成的程序,都会设置此位,
如wsdl.exe就设置此位,而混合编译的VC.Net程序如MgrCpp因为代码由IL和Native Code
组成,因而此位置空。
COMIMAGE_FLAGS_32BITREQUIRED 标志位说明此CLR映像只能在32位系统上执行,
对以后的64位或嵌入式系统上的CLR无效(MS.Net很重要的一个目的就是为以后平滑过渡到64位
平台做准备,想想以前16位平台到32位平台过渡时的混乱,以及现在比以前翻了n倍的代码量就恐怖,
MS真是未雨绸缪啊,呵呵)。一般纯IL代码的程序都是具有良好移植性的,混合编码则必须32bit。
COMIMAGE_FLAGS_IL_LIBRARY标志位说明此CLR映像是作为IL代码库存在的。
COMIMAGE_FLAGS_STRONGNAMESIGNED说明此映像有strong name signature
这个strong name signature在CLR架构里起到了非常重要的作用。为什么这么说呢,
因为这个strong name signature起到Assembly的身份证的作用,
它关系到CLR中大量安全概念的实现,如保证代码完整性,作为代码权限获取凭证等等。
因此在发布.Net程序前,一定要加上strong name signature。
COMIMAGE_FLAGS_TRACKDEBUGDATA标志位目前没有使用到,缺省置0。
EntryPointToken则是指向IL程序的入口点,类似于以前PE结构中
IMAGE_OPTIONAL_HEADER.AddressOfEntryPoint的作用,只是以前的
AddressOfEntryPoint是一个RVA,直接指向程序入口代码所在地址,
(不要告诉我你不知道RVA是什么啊,呵呵,赶快去看Peering Inside the PE)
而现在EntryPointToken指向一个Token。(注意,是Token,而不是地址)
因为IL代码是JIT编译的,存在于映像中的都是IL形式的P-code(pseudo code),
在需要时才由CLR动态读取,在内存中编译展开为本机代码(Native Code),进而执行。
因此这里的程序入口只是一个MethodDef或File表的入口,一个Token而已。
如wsdl的EntryPointToken值0x06000021指向0x06号表(MethodDef)的21项
Microsoft.DevApps.WebServices.WebServiceUtil.Main方法。
这里的MethodDef是一个MetaData表,每行定义一个函数或方法;而File表则是
每行有一个File定义的表,每个File表项包含一个外部文件的信息。也就是说,
在执行程序时可以直接编译执行此映像中的一个方法的IL代码,也可能是重定向到另一个文件,
这就是Assembly作为一个逻辑代码单元,与传统DLL之类相比一个很大的不同。
DLL是一个完整的代码实体,而Assembly则是由一组物理上可分布存在的实体组成的
逻辑单元组。
剩下没有解说的几个字段都是IMAGE_DATA_DIRECTORY类型,这个类型是一个数据块
定义结构,类似一个数组定义。在Winnt.h中有定义
//
// Directory format.
//
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress;
DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
Resources定义资源所在;StrongNameSignature定义刚刚提到的strong name
signature所在;此外CodeManagerTable,ExportAddressTableJumps,
MangedNativeHeader目前都没用到。VTableFixups是由使用VTable的语言如C++
使用的,类似原来PE结构中的重定向表,结构如下
_IMAGE_COR_VTABLEFIXUP = packed record
RVA: DWORD; // Offset of v-table array in image.
Count, // How many entries at location.
Kind: Word; // COR_VTABLE_xxx type of entries.
end;
IMAGE_COR_VTABLEFIXUP = _IMAGE_COR_VTABLEFIXUP;
TImageCorVTableFixup = _IMAGE_COR_VTABLEFIXUP;
PImageCorVTableFixup = ^TImageCorVTableFixup;
TImageCorVTableFixupArray = array[0..MaxWord-1] of TImageCorVTableFixup;
PImageCorVTableFixupArray = ^TImageCorVTableFixupArray;
4.Metadata 的逻辑结构
在分析完CLR的头信息后,我们来看看核心数据Metadata的结构
4.1 什么是 Metadata
Metadata翻译成中文是“元数据”,可以理解为Type of Type,
实际上就是描述类型信息的类型信息。从最初级的语言层面支持的RTTI
(“近代”的编程语言基本上都提供了足够的支持,如C++,Delphi等,
部分较“落伍”的语言也通过不同形式如扩展库提供了模拟支持,
“现代”的编程语言则提供了强力的支持,如Java和C#<本质是CLR>),
到后来二进制级的COM的IDL和类型库(类型库是IDL编译后的二进制形式),
到现在的Metadata,到即将到来的WebService的SOAP/WSDL,
其实是遵循着相同的发展思路。只是出于不同的需求、设计、实现,
有这各自的优点缺点罢了。但随着语言的发展,更多的需求将集中到
灵活性方面,因而语言发展的趋势是元数据的使用越来越多、支持越来越强。
举个最简单的例子,在IDE中,动态显示当前对象的方法、属性名列表的功能
(MS叫IntelliSense,Borland叫CodeInsight),就得宜于元类型信息。
以前在VC里实现,比较麻烦,得预编译生成专用的符号库;在VB里强一点,可以通过
COM的IDispatch,ITypeInfo,ITypeLib等接口动态获取,但编程使用复杂;
到CLR,在基础库一级直接提供支持,可以通过System.Reflection完全控制,
甚至比COM类型库更高一级地支持动态创建。
对用户来说,可以完全了解当前程序结构、接口,有哪些Module,哪些Class,
哪些Method等等,这给开发者提供了巨大的创造空间。如DUnit(DotNet下
的XUnit单元测试平台)实现上就大量使用Reflection机制。
以后有时间我会写一篇CLR程序的静态、动态分析,专门讨论这个问题。
4.2 Metadata在CLR中的作用
对于CLR架构来说,Metadata可以算是核心数据所在,几乎绝大多数功能
都需要使用其数据。从静态的IL代码构成(二进制编码中直接使用Metadata里的Token)
到动态JIT编译器(使用Metadata定位IL代码及其关系);从简单的代码载入执行
(Class Loader通过Metadata定位代码入口、编译执行)到复杂的不同语言互操作
(如VB.Net继承C#的类,实际上是直接继承CLR中Metadata中的类)等等……
几乎所有地方都能看到Metadata的身影。
4.3 如何访问和使用 Metadata
在CLR里使用Metadata,可以在三个层面进行操作。
最简单的方法是直接通过.Net BCL基础类库提供的System.Reflection
命名空间中的辅助工具类进行访问,例如
using System.Reflection;
using System;
public class Simple
{
public static void Main ()
{
Module mod = Assembly.GetExecutingAssembly().GetModules () [0];
Console.WriteLine ("Module Name is " + mod.Name);
Console.WriteLine ("Module FullyQualifiedName is " + mod.FullyQualifiedName);
Console.WriteLine ("Module ScopeName is " + mod.ScopeName);
}
}
这种访问方式使用起来最简单,功能也足够强大,能够完成我们绝大多数的需要,
特别是在System.Reflection.Emit命名空间中,更提供了动态生成、修改代码的支持。
不过这种方式必须有CLR环境的支持,受到库功能的限制(后面我们会看到一些
在Reflection库一级里不提供支持的信息),因此MS为工具软件开发商提供了另一套
较底层的开发库:Metadata Unmanaged API。这套支持库通过一系列COM接口,
提供了直接访问Metadata的强大支持,有兴趣的朋友可以参看
FrameworkSDK\Tool Developers Guide\docs\Metadata Unmanaged API.doc文档,
里面有详细的使用说明。如同其名字所示,它必须用Unmanaged代码来使用,如VC,Delphi等。
可以说99%的工作,都可以通过上面两套库来完成,不过总有些象我这样的人,
喜欢对技术追根究底,想把隐藏在美好面纱下的底层架构纠出来暴露一把,呵呵
因此有了第三个层面,二进制层面的逆向工程分析。
好在MS为了让其CLI(CLR的子集)标准化,公开了大量底层实现文档,
FrameworkSDK\Tool Developers Guide\docs\Partition II Metadata.doc
文档中对Metadata的二进制格式实现给出了比较详尽的说明,MS也公布了一个支持Win32和
FreeBSD平台的CLI实现的源代码(已移植到Linux等其它平台)。加上GNOME的mono项目已经
做了很多工作,因而对Metadata的二进制层面分析不是那么困难。
接下去的文章中,会试图一步步将Metadata在PE中的组织结构剥离开来,
让大家能够了解这个神秘的CLR核心:Metadata到底是什么,里面隐藏了些什么,我们能够通过
他做什么,为什么要这样设计,等等……
4.4 Metadata在PE中的组织结构
回到正体上来,谈谈Metadata在PE中的组织结构。
上章我们提到CLR的头信息里面专门有一个字段指向Metadata数据块,
实际上这个数据块只是Metadata的一个头结构,保存有Metadata的全局信息,
而Metadata的实际数据,是通过若干不同的Heap或者说Stream保存的。
这里我统一使用Stream“流“作为他的名字,但很多文档中以Heap”堆“作为
其称呼,我们可以理解他是一个二进制流,其中数据以堆的结构进行组织。
Metadata里最常见的有五种流,#String, #Blob, #Guid,
#US(User String)和#~流("#"是流名字的前缀)
String流就是一个字符串堆,Metadata内部用到的所有字符串如类或方法
的名字等等都以UTF8编码保存在此堆内。而用户的字符串如字符串常量,
则以Unicode编码保存在US(User String)堆内。值得注意的是,
US流和String流在二进制结构组织上不同,我们后面将分析时会详细提及。
Guid流是保存程序中使用到的Guid的数组,如Assembly中Module的MVID。
Blob流是一个通用存储空间,除了Guid和字符串以外基本上所有
剩下的东西都以二进制数据形式放在里面,如PublicKey,常量的值等等。
最重要的是#~流,这是Metadata核心信息存放的地方。#~流物理上以
若干张表(Table)的形式组织,每张表存储某一方面的Metadata信息,
如MethodDef表存储所有方法的信息。每张表又由若干的行(Row)组成
每行有n个列(Column),每列代表一种信息,如MethodDef表中每一行
都有一个方法的RVA,类型标志,名字,Signature等等信息。在其中通过
各种索引来相互关联,整个组织结构和关系数据库很相似。
比较特殊的是,这里所有的表通过一个64bit的有效位图来表示表存在与否
每种类型的表有一个编号,如MethodDef表的编号是6,则第(1<<(6-1))位置1
因而每个表的每一行,可以使用一个唯一的Token表示。此Token是一个32bit
无符号整型数,最高一个字节表示表的序号,低三个字节表示表中的索引号。
如0x06000003表示0x06表(MethodDef)中第3行(如MyApp::Add)
这个Token概念在CLR中频繁使用,如IL代码调用函数、使用变量都是使用Token。
与之类似的还有Coded Index,等会讲二进制实现时再说。
5.Metadata 的物理结构
5.1 Metadata Header
前言中我们分析了CLR Header的结构,里面有一个Metadata字段
IMAGE_DATA_DIRECTORY MetaData;
字段指向一个数据块,里面包含着Metadata Header,是关于
Metadata信息所在,结构定义伪代码如下
PClrMetadataHeader = ^TClrMetadataHeader;
TClrMetadataHeader = packed record
Signature: DWORD; // Magic signature for physical metadata : $424A5342.
MajorVersion, // Major version, 1
MinorVersion: Word; // Minor version, 0
Reserved, // Reserved, always 0
Length: DWORD; // Length of version string in bytes, say m.
Version: array[0..0] of Char;
// UTF8-encoded version string of length m
// Padding to next 4 byte boundary, say x.
{
Version: array[0..((m+3) and (not $3))-1] of Char;
Flags, // Reserved, always 0
Streams: Word; // Number of streams, say n.
// Array of n StreamHdr structures.
StreamHeaders: array[0..n-1] of TClrStreamHeader;
}
end;
wsdl.exe的Metadata头信息
...
67F8 [ 74B0] RVA [size] of MetaData Directory
...
005067F0: ?? ?? ?? ?? ?? ?? ?? ?? 42 53 4A 42 01 00 01 00 l.pdb...BSJB....
00506800: 00 00 00 00 0C 00 00 00 76 31 2E 78 38 36 72 65 ........v1.x86re
00506810: 74 00 00 00 00 00 05 00 6C 00 00 00 64 1E 00 00 t.......l...d...
00506820: 23 7E 00 00 D0 1E 00 00 70 1F 00 00 23 53 74 72 #~..D...p...#Str
00506830: 69 6E 67 73 00 00 00 00 40 3E 00 00 18 13 00 00 ings....@>......
00506840: 23 55 53 00 58 51 00 00 10 00 00 00 23 47 55 49 #US.XQ......#GUI
00506850: 44 00 00 00 68 51 00 00 48 23 00 00 23 42 6C 6F D...hQ..H#..#Blo
00506860: 62 00 00 00 00 00 00 00 01 00 00 01 57 1F A2 03 b...........W.¢.
...
首先是Signature,是一个DWORD或4字符长的标记,$424A5342或'BSJB'
可以通过此标记判断Metadata是否存在,如
function TJclPeCLRHeader.GetHasMetadata: Boolean;
const
METADATA_SIGNATURE = $424A5342;
begin
with Header.MetaData do
Result := (VirtualAddress <> 0) and
(PDWORD(FImage.RvaToVa(VirtualAddress))^ = METADATA_SIGNATURE);
end;
通过检测Metadata节是否存在,以及头一个DWORD是否为Metadata标记来判断
Metadata是否存在。
接着的MajorVersion和MinorVersion保存Metadata格式的版本号,
一般设置为1.1。(文档中说明是1.0,实际为1.1)
跟着的Length+Version指定了UTF8格式的编译环境版本号,这个和你的CLR的编译环境
版本相同,如.Net Framework工具的编译环境版本号为v1.x86ret,你用它编译代码
生成的版本号则是诸如v1.0.3705类型,而MS CLI实现的编译环境版本号为v1.0.0。
注意这里Version字符串是按四字节对齐的,在分析二进制流时应按(m+3) and (not $3)
来计算实际的长度。
Streams和StreamHeaders则是Metadata流的信息数组,保存有Metadata不同类型
流的Offset, Size, Name等信息,每记录的结构如下
PClrStreamHeader = ^TClrStreamHeader;
TClrStreamHeader = packed record
Offset, // Memory offset to start of this stream from start of the metadata root
Size: DWORD; // Size of this stream in bytes, shall be a multiple of 4.
// Name of the stream as null terminated variable length
// array of ASCII characters, padded with \0 characters
Name: array[0..MaxWord] of Char;
end;
Offset是流实际位置相对Metadata头的偏移;Size是流的长度,以DWORD为单位;
Name是以#0字符结束的字符串,表示流的名称。
根据这些信息,我们可以从PE映像中读取相应流进行分析。
5.2 Metadata Stream
上一章我们曾经提过,Metadata Stream有五种常见类型,#String, #Blob,
#Guid, #US(User String)和#~流,每种类型流最多只能出现一次,#US和#Blob流
可省略。
5.2.1 #String 流
#String流是一个字符串堆,程序用到的字符串,如类名、函数名、参数名等字符串
保存在其中。字符串以UTF8编码保存,以#0字符分隔。流首总有一个#0字符代表一个
空字符串,分析代码如下。
constructor TJclClrStringsStream.Create(
const AMetadata: TJclPeMetadata;
const AHeader: PClrStreamHeader);
var
pch: PChar;
off: DWORD;
begin
inherited;
FStrings := TStringList.Create;
pch := Data;
off := 0;
while off < Size do
begin
if pch^ <> #0 then
FStrings.AddObject(pch, TObject(off));
pch := pch + StrLen(pch) + 1;
off := DWORD(pch - Data);
end;
end;
如碰到#0字符,直接作为空字符串跳过之,否则,以StrLen取得字符串长度,
并将字符串保存到一个字符串列表中,使用时需要动态将UTF8解码为Unicode。
function TJclClrStringsStream.GetString(const Idx: Integer): WideString;
begin
Result := UTF8ToWideString(FStrings.Strings[Idx]);
end;
因为流中数据是以堆形式存放,指向#String流的索引是字符串相对偏移值。
function TJclClrStringsStream.At(const Offset: DWORD): WideString;
var
Idx: Integer;
begin
Idx := FStrings.IndexOfObject(TObject(Offset));
if Idx <> -1 then
Result := GetString(Idx)
else
Result := '';
end;
CLR中包和类名的最大长度都是1024
5.2.2 #Guid 流
#Guid流格式很简单,就是一个GUID结构的数组,以流长度除以SizeOf(TGuid)就是
数组元素个数。因为流中数据是以数组形式存放,指向#Guid流的索引是以1开始的索引值,
如索引为0则表示此项索引不存在。(注意,CLR中所有索引都是以1开始的,0表示索引值
不存在)示例代码如下:
constructor TJclClrGuidStream.Create(
const AMetadata: TJclPeMetadata;
const AHeader: PClrStreamHeader);
var
I: Integer;
pg: PGUID;
begin
inherited;
SetLength(FGuids, Size div SizeOf(TGuid));
pg := Data;
for I:=0 to GetGuidCount-1 do
begin
FGuids[I] := pg^;
Inc(pg);
end;
end;
5.2.3 #Blob 和 #US 流
#Blob流是一个二进制数据堆,程序中的所有非字符串形式数据都堆放在这个流里面,
如常数的值,Public Key的值,方法的Signature等等。
在每个二进制数据块头,都有一个块长度数据,但为了节约存储空间,CLR使用了比较
麻烦的编码方法。
如果开始一个字节最高位为0,则此数据块长度为一个字节;
如果开始一个字节最高位为10,则此数据块长度为两个字节;
如果开始一个字节最高位为110,则此数据块长度为四个字节;
在屏蔽标志位后,通过移位运算即可计算出数据块的实际长度值,并依据此获得数据。
constructor TJclClrBlobRecord.Create(
const AStream: TJclClrStream;
const APtr: PByteArray);
var
b: Byte;
AData: Pointer;
ASize: DWORD;
begin
FPtr := APtr;
FOffset := DWORD(FPtr) - DWORD(AStream.Data);
b := FPtr[0];
if b = 0 then
begin
AData := @FPtr[1];
ASize := 0;
end
else if ((b and $C0) = $C0) and ((b and $20) = 0) then // 110bs
begin
AData := @FPtr[4];
ASize := ((b and $1F) shl 24) + (FPtr[1] shl 16) + (FPtr[2] shl 8) + FPtr[3];
end
else if ((b and $80) = $80) and ((b and $40) = 0) then // 10bs
begin
AData := @FPtr[2];
ASize := ((b and $3F) shl 8) + FPtr[1];
end
else
begin
AData := @FPtr[1];
ASize := b and $7F;
end;
Assert(not IsBadReadPtr(AData, ASize));
inherited Create(AData, ASize);
end;
constructor TJclClrBlobStream.Create(
const AMetadata: TJclPeMetadata;
const AHeader: PClrStreamHeader);
var
ABlob: TJclClrBlobRecord;
begin
inherited;
FBlobs := TObjectList.Create;
ABlob := TJclClrBlobRecord.Create(Self, Data);
while Assigned(ABlob) do
begin
if ABlob.Size > 0 then
FBlobs.Add(ABlob);
if (Integer(ABlob.Memory) + ABlob.Size) < (Integer(Self.Data) + Integer(Self.Size)) then
ABlob := TJclClrBlobRecord.Create(Self, Pointer(Integer(ABlob.Memory) + ABlob.Size))
else
ABlob := nil;
end;
end;
#US流是User String的存储空间,结构上和#Blob流相同,只是所有数据都是
Unicode格式的字符串。
#Blob和#US流的索引都是数据块偏移。
5.2.4 #~ 流
#~流是Metadata中最复杂也是最重要的信息所在,几乎所有Metadata信息
都以表的形式组织存放在#~流中。流起始处有一个#~ Stream Header如下
PClrTableStreamHeader = ^TClrTableStreamHeader;
TClrTableStreamHeader = packed record
Reserved: DWORD; // Reserved, always 0
MajorVersion, // Major version of table schemata, always 1
MinorVersion, // Minor version of table schemata, always 0
HeapSizes, // Bit vector for heap sizes.
Reserved2: Byte; // Reserved, always 1
Valid, // Bit vector of present tables, let n be the number of bits that are 1.
Sorted: Int64; // Bit vector of sorted tables.
// Array of n four byte unsigned integers indicating the number of rows
// for each present table.
Rows: array[0..MaxWord] of DWORD;
//Rows: array[0..n-1] of DWORD;
//Tables: array
end;
字段MajorVersion.MinorVersion是表格式的版本号,一般设置为 1.0
在介绍其他字段意义前,我们要先了解Metadata中表的表示方法。
在Metadata中,表最多有64种,每种表有一个唯一的确定的编号,如Assembly表的
编号为$20、Module表的编号为$00等等。目前的CLR只使用到了最多$2B(43)种表,
其间还有一些表编号是被保留或非公开的。经过对多个信息来源的整理,完整对应表编号如下
TJclClrTableKind = (
ttModule, // $00
ttTypeRef, // $01
ttTypeDef, // $02
ttFieldPtr, // $03
ttFieldDef, // $04
ttMethodPtr, // $05
ttMethodDef, // $06
ttParamPtr, // $07
ttParamDef, // $08
ttInterfaceImpl, // $09
ttMemberRef, // $0a
ttConstant, // $0b
ttCustomAttribute, // $0c
ttFieldMarshal, // $0d
ttDeclSecurity, // $0e
ttClassLayout, // $0f
ttFieldLayout, // $10
ttSignature, // $11
ttEventMap, // $12
ttEventPtr, // $13
ttEventDef, // $14
ttPropertyMap, // $15
ttPropertyPtr, // $16
ttPropertyDef, // $17
ttMethodSemantics, // $18
ttMethodImpl, // $19
ttModuleRef, // $1a
ttTypeSpec, // $1b
ttImplMap, // $1c
ttFieldRVA, // $1d
ttENCLog, // $1e
ttENCMap, // $1f
ttAssembly, // $20
ttAssemblyProcessor, // $21
ttAssemblyOS, // $22
ttAssemblyRef, // $23
ttAssemblyRefProcessor, // $24
ttAssemblyRefOS, // $25
ttFile, // $26
ttExportedType, // $27
ttManifestResource, // $28
ttNestedClass, // $29
ttTypeTyPar, // $2a
ttMethodTyPar); // $2b
在#~流头中,使用了两个64位位图表示这些表的状态,如Valid位图中第$20位为1
则Assembly表在当前Metadata中存在,Sorted位图中第$00位为1则Module表已排序等等。
这里就涉及到在Metadata中非常重要的一个概念Token。一个Token是在一个Metadata
中可以唯一性确定一个记录的标识符,他是一个32位无符号整型数,高8位表示表的编号
低24位表示记录在表中的索引。因此一个Token为$20000001实际上表示编号为$20的
Assembly表中第1项记录。Metadata Unmanaged API就是使用Token来表示每个记录。
对Valid位图进行从低到高位扫描,发现一个表存在,则可到Rows中获取此表中记录
的行数,Rows数组大小为Valid位图中设置的位的数量,也就是Metadata中存在的表
的数量。而Tables则是实际数据的开始。
constructor TJclClrTableStream.Create(
const AMetadata: TJclPeMetadata;
const AHeader: PClrStreamHeader);
function BitCount(const Value: Int64): Integer;
var
AKind: TJclClrTableKind;
begin
Result := 0;
for AKind:=Low(TJclClrTableKind) to High(TJclClrTableKind) do
if (Value and (Int64(1) shl Integer(AKind))) <> 0 then
Inc(Result);
end;
procedure EnumTables;
var
AKind: TJclClrTableKind;
pTable: Pointer;
begin
pTable := @Header.Rows[BitCount(Header.Valid)];
FTableCount := 0;
for AKind:=Low(TJclClrTableKind) to High(TJclClrTableKind) do
begin
if (Header.Valid and (Int64(1) shl Integer(AKind))) <> 0 then
begin
FTables[AKind] := ValidTableMapping[AKind].Create(Self, pTable, Header.Rows[FTableCount]);
pTable := Pointer(DWORD(pTable) + FTables[AKind].Size);
Inc(FTableCount);
end
else
FTables[AKind] := nil;
end;
end;
begin
inherited;
FHeader := Data;
EnumTables;
end;
这里还有一个HeapSizes,表示其他几个流中索引的大小,位$01表示#String流,
$02表示#Guid流,#04表示Blob流。如相应位设置为1,则表示对应流大于MaxWord
也就是大于2^16,因此需要以四字节来做索引,否则缺省用两字节做索引。
我们可以看到,为了节省存储空间,Metadata中使用了相对多的编码方式来合并字段。
6.小结
至此,MS.Net CLR对PE文件结构的扩展的分析,可以告一段落,更深入的对Metadata
表一级的分析,有兴趣的朋友可以参看FrameworkSDK\Tool Developers Guide中的文档、
mscli和mono的代码、也可向我索取为JCL项目编写的JclCLR解析代码(未完成)。
此外网上也有一些独立的开源项目完成了类似的工作。也欢迎大家来信与我讨论。
7.备注
.Net Framework SDK
http://msdn.microsoft.com/downloads/default.asp?url=/downloads/sample.asp?url=/msdn-files/027/000/976/msdncompositedoc.xml&frame=true
.NET Framework Service Pack 2
http://msdn.microsoft.com/netframework/downloads/sp/download.asp
ms cli
http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dndotnet/html/mssharsourcecli2.asp
mono
http://www.go-mono.com/index.html
anakrino
http://test.saurik.net/anakrino/
<待续>
为了论坛节省空间,给个连接:http://www.nsfocus.net/index.php?periodical=&kind_id=&keyword=MS.Net+CLR&act=magazine&do=search
MS.Net CLR扩展PE结构分析 (1)
作者:Flier Lu <flier@nsfocus.com>
主页:http://www.nsfocus.com
日期:2002-08-16
1.概述
本文从系统底层角度,通过分析MS.Net CLR架构在Win32平台上对PE可执行文件
映像结构的扩展,解析CLR架构底层的部分运行机制,帮助读者从更深层次理解CLR架构
中某些重要概念。
本文读者应具备基本的Win32编程经验,了解CLR中常见概念意义,并对Win32平台
之PE可执行文件映像结构有一定了解,具体说明请参看Matt Pietrek于1994年3月发表在
MSJ上的经典文章《Peering Inside the PE: A Tour of the Win32 Portable
Executable File Format》,与之重复的部分本文一概跳过。
2.前言
对一个优秀Win32平台程序员来说,对PE结构的了解是对Win32架构了解的必经之路,
而从Chicago(Win95的开发代号,Win95正式发布以前的文档中对Win95的称呼)以来,
PE结构就相对稳定,直到MS.Net的出现,才发生了一些不大不小的变化。
之所以说是不大不小的变化,是因为CLR并没有对PE结构进行结构上的调整,只是利用现有
PE结构的优良可扩展性,将CLR所需的信息扩展到PE映像中。具体一点说,就是利用了PE结构中的
IMAGE_OPTIONAL_HEADER.DataDirectory[IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR]
来保存服务于CLR的IMAGE_COR20_HEADER结构,其它的PE结构基本不变。
3.CLR 头信息
与传统PE可执行文件一样,CLR程序也有一个文件头保存CLR相关的全局性信息。
但因为要保证现有PE结构的稳定性,CLR程序仅使用PE结构的一个Directory Entry
也就是前言中提到的IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR。
此入口原本是设计用于COM,但不知为何一直没有被使用。
3.1 IMAGE_COR20_HEADER
IMAGE_COR20_HEADER结构的定义,可以在FrameworkSDK\include\CorHdr.h
头文件中找到,如下:
// CLR 2.0 header structure.
typedef struct IMAGE_COR20_HEADER
{
// Header versioning
ULONG cb;
USHORT MajorRuntimeVersion;
USHORT MinorRuntimeVersion;
// Symbol table and startup information
IMAGE_DATA_DIRECTORY MetaData;
ULONG Flags;
ULONG EntryPointToken;
// Binding information
IMAGE_DATA_DIRECTORY Resources;
IMAGE_DATA_DIRECTORY StrongNameSignature;
// Regular fixup and binding information
IMAGE_DATA_DIRECTORY CodeManagerTable;
IMAGE_DATA_DIRECTORY VTableFixups;
IMAGE_DATA_DIRECTORY ExportAddressTableJumps;
// Precompiled image info (internal use only - set to zero)
IMAGE_DATA_DIRECTORY ManagedNativeHeader;
} IMAGE_COR20_HEADER;
详细的说明信息,可以在FrameworkSDK\Tool Developers Guide\docs
目录中找到。此结构虽然字段较多,但实际上其核心在于MetaData,其他信息都是围绕着
MetaData服务。
3.2 实例分析
下面我们分析两个实际的CLR程序的Cor20Header结构,wsdl.exe是.Net Framework
提供的wsdl分析工具、MgrCpp.exe是一个简单的VC.Net编写的Managed C++和
Unmanaged C++并存的程序,MgrCpp.cpp 代码如下
#include "stdafx.h"
#using <mscorlib.dll>
#include <tchar.h>
#include <iostream>
using namespace System;
// This is the entry point for this application
int _tmain(void)
{
// TODO: Please replace the sample code below with your own.
Console::WriteLine(S"Hello World");
std::cout << "Hello World" << std::endl;
return 0;
}
wsdl.exe 可以用Dumpbin /all wsdl.exe > wsdl.dump 导出,关键数据如下
...
500000 image base (00500000 to 00511FFF)
...
2008 [ 48] RVA [size] of COM Descriptor Directory
...
RAW DATA #1
00502000: ?? ?? ?? ?? ?? ?? ?? ?? 48 00 00 00 02 00 00 00 àü......H.......
00502010: F8 67 00 00 B0 74 00 00 09 00 00 00 21 00 00 06 g..°t......!...
00502020: 09 51 00 00 8E 16 00 00 50 20 00 00 80 00 00 00 .Q......P ......
00502030: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00502040: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
MgrCpp.exe 类似处理,关键数据如下
...
400000 image base (00400000 to 0045CFFF)
...
4B24C [ 48] RVA [size] of COM Descriptor Directory
...
0044B240: ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? 48 00 00 00 @.@.à#@.H...
0044B250: 02 00 00 00 70 EB 04 00 D0 42 00 00 02 00 00 00 ....p..DB......
0044B260: 73 00 00 06 00 00 00 00 00 00 00 00 00 00 00 00 s...............
0044B270: 00 00 00 00 00 00 00 00 00 00 00 00 78 2E 05 00 ............x...
0044B280: C0 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 à...............
0044B290: 00 00 00 00 ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ....1E.@?@.....
3.3 字段分析
cb(0x48)是IMAGE_COR20_HEADER结构的大小。
MajorRuntimeVersion(0x02)和MinorRuntimeVersion(0x00)是版本号,
指执行此程序所需的最低CLR版本号。
Metadata是核心数据所在,我们等会再详细分析。
Flags是Runtime Image描述标志,描述此CLR映像的执行属性。wsdl.exe的Flags
是0x09或1001B,MgrCpp.exe的Flags是0x02或0010B,具体含义如下:
// CLR Header entry point flags.
COMIMAGE_FLAGS_ILONLY =0x00000001,
COMIMAGE_FLAGS_32BITREQUIRED =0x00000002,
COMIMAGE_FLAGS_IL_LIBRARY =0x00000004,
COMIMAGE_FLAGS_STRONGNAMESIGNED =0x00000008,
COMIMAGE_FLAGS_TRACKDEBUGDATA =0x00010000,
COMIMAGE_FLAGS_ILONLY 标志位说明此CLR程序由纯IL代码组成,IL代码是类似
Java的Bytecode的中间代码,由纯IL代码组成就代表此程序可以在任何实现了CLR环境
的平台,如支持CLI的FreeBSD上执行。一般由C#之类纯.Net语言生成的程序,都会设置此位,
如wsdl.exe就设置此位,而混合编译的VC.Net程序如MgrCpp因为代码由IL和Native Code
组成,因而此位置空。
COMIMAGE_FLAGS_32BITREQUIRED 标志位说明此CLR映像只能在32位系统上执行,
对以后的64位或嵌入式系统上的CLR无效(MS.Net很重要的一个目的就是为以后平滑过渡到64位
平台做准备,想想以前16位平台到32位平台过渡时的混乱,以及现在比以前翻了n倍的代码量就恐怖,
MS真是未雨绸缪啊,呵呵)。一般纯IL代码的程序都是具有良好移植性的,混合编码则必须32bit。
COMIMAGE_FLAGS_IL_LIBRARY标志位说明此CLR映像是作为IL代码库存在的。
COMIMAGE_FLAGS_STRONGNAMESIGNED说明此映像有strong name signature
这个strong name signature在CLR架构里起到了非常重要的作用。为什么这么说呢,
因为这个strong name signature起到Assembly的身份证的作用,
它关系到CLR中大量安全概念的实现,如保证代码完整性,作为代码权限获取凭证等等。
因此在发布.Net程序前,一定要加上strong name signature。
COMIMAGE_FLAGS_TRACKDEBUGDATA标志位目前没有使用到,缺省置0。
EntryPointToken则是指向IL程序的入口点,类似于以前PE结构中
IMAGE_OPTIONAL_HEADER.AddressOfEntryPoint的作用,只是以前的
AddressOfEntryPoint是一个RVA,直接指向程序入口代码所在地址,
(不要告诉我你不知道RVA是什么啊,呵呵,赶快去看Peering Inside the PE)
而现在EntryPointToken指向一个Token。(注意,是Token,而不是地址)
因为IL代码是JIT编译的,存在于映像中的都是IL形式的P-code(pseudo code),
在需要时才由CLR动态读取,在内存中编译展开为本机代码(Native Code),进而执行。
因此这里的程序入口只是一个MethodDef或File表的入口,一个Token而已。
如wsdl的EntryPointToken值0x06000021指向0x06号表(MethodDef)的21项
Microsoft.DevApps.WebServices.WebServiceUtil.Main方法。
这里的MethodDef是一个MetaData表,每行定义一个函数或方法;而File表则是
每行有一个File定义的表,每个File表项包含一个外部文件的信息。也就是说,
在执行程序时可以直接编译执行此映像中的一个方法的IL代码,也可能是重定向到另一个文件,
这就是Assembly作为一个逻辑代码单元,与传统DLL之类相比一个很大的不同。
DLL是一个完整的代码实体,而Assembly则是由一组物理上可分布存在的实体组成的
逻辑单元组。
剩下没有解说的几个字段都是IMAGE_DATA_DIRECTORY类型,这个类型是一个数据块
定义结构,类似一个数组定义。在Winnt.h中有定义
//
// Directory format.
//
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress;
DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
Resources定义资源所在;StrongNameSignature定义刚刚提到的strong name
signature所在;此外CodeManagerTable,ExportAddressTableJumps,
MangedNativeHeader目前都没用到。VTableFixups是由使用VTable的语言如C++
使用的,类似原来PE结构中的重定向表,结构如下
_IMAGE_COR_VTABLEFIXUP = packed record
RVA: DWORD; // Offset of v-table array in image.
Count, // How many entries at location.
Kind: Word; // COR_VTABLE_xxx type of entries.
end;
IMAGE_COR_VTABLEFIXUP = _IMAGE_COR_VTABLEFIXUP;
TImageCorVTableFixup = _IMAGE_COR_VTABLEFIXUP;
PImageCorVTableFixup = ^TImageCorVTableFixup;
TImageCorVTableFixupArray = array[0..MaxWord-1] of TImageCorVTableFixup;
PImageCorVTableFixupArray = ^TImageCorVTableFixupArray;
4.Metadata 的逻辑结构
在分析完CLR的头信息后,我们来看看核心数据Metadata的结构
4.1 什么是 Metadata
Metadata翻译成中文是“元数据”,可以理解为Type of Type,
实际上就是描述类型信息的类型信息。从最初级的语言层面支持的RTTI
(“近代”的编程语言基本上都提供了足够的支持,如C++,Delphi等,
部分较“落伍”的语言也通过不同形式如扩展库提供了模拟支持,
“现代”的编程语言则提供了强力的支持,如Java和C#<本质是CLR>),
到后来二进制级的COM的IDL和类型库(类型库是IDL编译后的二进制形式),
到现在的Metadata,到即将到来的WebService的SOAP/WSDL,
其实是遵循着相同的发展思路。只是出于不同的需求、设计、实现,
有这各自的优点缺点罢了。但随着语言的发展,更多的需求将集中到
灵活性方面,因而语言发展的趋势是元数据的使用越来越多、支持越来越强。
举个最简单的例子,在IDE中,动态显示当前对象的方法、属性名列表的功能
(MS叫IntelliSense,Borland叫CodeInsight),就得宜于元类型信息。
以前在VC里实现,比较麻烦,得预编译生成专用的符号库;在VB里强一点,可以通过
COM的IDispatch,ITypeInfo,ITypeLib等接口动态获取,但编程使用复杂;
到CLR,在基础库一级直接提供支持,可以通过System.Reflection完全控制,
甚至比COM类型库更高一级地支持动态创建。
对用户来说,可以完全了解当前程序结构、接口,有哪些Module,哪些Class,
哪些Method等等,这给开发者提供了巨大的创造空间。如DUnit(DotNet下
的XUnit单元测试平台)实现上就大量使用Reflection机制。
以后有时间我会写一篇CLR程序的静态、动态分析,专门讨论这个问题。
4.2 Metadata在CLR中的作用
对于CLR架构来说,Metadata可以算是核心数据所在,几乎绝大多数功能
都需要使用其数据。从静态的IL代码构成(二进制编码中直接使用Metadata里的Token)
到动态JIT编译器(使用Metadata定位IL代码及其关系);从简单的代码载入执行
(Class Loader通过Metadata定位代码入口、编译执行)到复杂的不同语言互操作
(如VB.Net继承C#的类,实际上是直接继承CLR中Metadata中的类)等等……
几乎所有地方都能看到Metadata的身影。
4.3 如何访问和使用 Metadata
在CLR里使用Metadata,可以在三个层面进行操作。
最简单的方法是直接通过.Net BCL基础类库提供的System.Reflection
命名空间中的辅助工具类进行访问,例如
using System.Reflection;
using System;
public class Simple
{
public static void Main ()
{
Module mod = Assembly.GetExecutingAssembly().GetModules () [0];
Console.WriteLine ("Module Name is " + mod.Name);
Console.WriteLine ("Module FullyQualifiedName is " + mod.FullyQualifiedName);
Console.WriteLine ("Module ScopeName is " + mod.ScopeName);
}
}
这种访问方式使用起来最简单,功能也足够强大,能够完成我们绝大多数的需要,
特别是在System.Reflection.Emit命名空间中,更提供了动态生成、修改代码的支持。
不过这种方式必须有CLR环境的支持,受到库功能的限制(后面我们会看到一些
在Reflection库一级里不提供支持的信息),因此MS为工具软件开发商提供了另一套
较底层的开发库:Metadata Unmanaged API。这套支持库通过一系列COM接口,
提供了直接访问Metadata的强大支持,有兴趣的朋友可以参看
FrameworkSDK\Tool Developers Guide\docs\Metadata Unmanaged API.doc文档,
里面有详细的使用说明。如同其名字所示,它必须用Unmanaged代码来使用,如VC,Delphi等。
可以说99%的工作,都可以通过上面两套库来完成,不过总有些象我这样的人,
喜欢对技术追根究底,想把隐藏在美好面纱下的底层架构纠出来暴露一把,呵呵
因此有了第三个层面,二进制层面的逆向工程分析。
好在MS为了让其CLI(CLR的子集)标准化,公开了大量底层实现文档,
FrameworkSDK\Tool Developers Guide\docs\Partition II Metadata.doc
文档中对Metadata的二进制格式实现给出了比较详尽的说明,MS也公布了一个支持Win32和
FreeBSD平台的CLI实现的源代码(已移植到Linux等其它平台)。加上GNOME的mono项目已经
做了很多工作,因而对Metadata的二进制层面分析不是那么困难。
接下去的文章中,会试图一步步将Metadata在PE中的组织结构剥离开来,
让大家能够了解这个神秘的CLR核心:Metadata到底是什么,里面隐藏了些什么,我们能够通过
他做什么,为什么要这样设计,等等……
4.4 Metadata在PE中的组织结构
回到正体上来,谈谈Metadata在PE中的组织结构。
上章我们提到CLR的头信息里面专门有一个字段指向Metadata数据块,
实际上这个数据块只是Metadata的一个头结构,保存有Metadata的全局信息,
而Metadata的实际数据,是通过若干不同的Heap或者说Stream保存的。
这里我统一使用Stream“流“作为他的名字,但很多文档中以Heap”堆“作为
其称呼,我们可以理解他是一个二进制流,其中数据以堆的结构进行组织。
Metadata里最常见的有五种流,#String, #Blob, #Guid,
#US(User String)和#~流("#"是流名字的前缀)
String流就是一个字符串堆,Metadata内部用到的所有字符串如类或方法
的名字等等都以UTF8编码保存在此堆内。而用户的字符串如字符串常量,
则以Unicode编码保存在US(User String)堆内。值得注意的是,
US流和String流在二进制结构组织上不同,我们后面将分析时会详细提及。
Guid流是保存程序中使用到的Guid的数组,如Assembly中Module的MVID。
Blob流是一个通用存储空间,除了Guid和字符串以外基本上所有
剩下的东西都以二进制数据形式放在里面,如PublicKey,常量的值等等。
最重要的是#~流,这是Metadata核心信息存放的地方。#~流物理上以
若干张表(Table)的形式组织,每张表存储某一方面的Metadata信息,
如MethodDef表存储所有方法的信息。每张表又由若干的行(Row)组成
每行有n个列(Column),每列代表一种信息,如MethodDef表中每一行
都有一个方法的RVA,类型标志,名字,Signature等等信息。在其中通过
各种索引来相互关联,整个组织结构和关系数据库很相似。
比较特殊的是,这里所有的表通过一个64bit的有效位图来表示表存在与否
每种类型的表有一个编号,如MethodDef表的编号是6,则第(1<<(6-1))位置1
因而每个表的每一行,可以使用一个唯一的Token表示。此Token是一个32bit
无符号整型数,最高一个字节表示表的序号,低三个字节表示表中的索引号。
如0x06000003表示0x06表(MethodDef)中第3行(如MyApp::Add)
这个Token概念在CLR中频繁使用,如IL代码调用函数、使用变量都是使用Token。
与之类似的还有Coded Index,等会讲二进制实现时再说。
5.Metadata 的物理结构
5.1 Metadata Header
前言中我们分析了CLR Header的结构,里面有一个Metadata字段
IMAGE_DATA_DIRECTORY MetaData;
字段指向一个数据块,里面包含着Metadata Header,是关于
Metadata信息所在,结构定义伪代码如下
PClrMetadataHeader = ^TClrMetadataHeader;
TClrMetadataHeader = packed record
Signature: DWORD; // Magic signature for physical metadata : $424A5342.
MajorVersion, // Major version, 1
MinorVersion: Word; // Minor version, 0
Reserved, // Reserved, always 0
Length: DWORD; // Length of version string in bytes, say m.
Version: array[0..0] of Char;
// UTF8-encoded version string of length m
// Padding to next 4 byte boundary, say x.
{
Version: array[0..((m+3) and (not $3))-1] of Char;
Flags, // Reserved, always 0
Streams: Word; // Number of streams, say n.
// Array of n StreamHdr structures.
StreamHeaders: array[0..n-1] of TClrStreamHeader;
}
end;
wsdl.exe的Metadata头信息
...
67F8 [ 74B0] RVA [size] of MetaData Directory
...
005067F0: ?? ?? ?? ?? ?? ?? ?? ?? 42 53 4A 42 01 00 01 00 l.pdb...BSJB....
00506800: 00 00 00 00 0C 00 00 00 76 31 2E 78 38 36 72 65 ........v1.x86re
00506810: 74 00 00 00 00 00 05 00 6C 00 00 00 64 1E 00 00 t.......l...d...
00506820: 23 7E 00 00 D0 1E 00 00 70 1F 00 00 23 53 74 72 #~..D...p...#Str
00506830: 69 6E 67 73 00 00 00 00 40 3E 00 00 18 13 00 00 ings....@>......
00506840: 23 55 53 00 58 51 00 00 10 00 00 00 23 47 55 49 #US.XQ......#GUI
00506850: 44 00 00 00 68 51 00 00 48 23 00 00 23 42 6C 6F D...hQ..H#..#Blo
00506860: 62 00 00 00 00 00 00 00 01 00 00 01 57 1F A2 03 b...........W.¢.
...
首先是Signature,是一个DWORD或4字符长的标记,$424A5342或'BSJB'
可以通过此标记判断Metadata是否存在,如
function TJclPeCLRHeader.GetHasMetadata: Boolean;
const
METADATA_SIGNATURE = $424A5342;
begin
with Header.MetaData do
Result := (VirtualAddress <> 0) and
(PDWORD(FImage.RvaToVa(VirtualAddress))^ = METADATA_SIGNATURE);
end;
通过检测Metadata节是否存在,以及头一个DWORD是否为Metadata标记来判断
Metadata是否存在。
接着的MajorVersion和MinorVersion保存Metadata格式的版本号,
一般设置为1.1。(文档中说明是1.0,实际为1.1)
跟着的Length+Version指定了UTF8格式的编译环境版本号,这个和你的CLR的编译环境
版本相同,如.Net Framework工具的编译环境版本号为v1.x86ret,你用它编译代码
生成的版本号则是诸如v1.0.3705类型,而MS CLI实现的编译环境版本号为v1.0.0。
注意这里Version字符串是按四字节对齐的,在分析二进制流时应按(m+3) and (not $3)
来计算实际的长度。
Streams和StreamHeaders则是Metadata流的信息数组,保存有Metadata不同类型
流的Offset, Size, Name等信息,每记录的结构如下
PClrStreamHeader = ^TClrStreamHeader;
TClrStreamHeader = packed record
Offset, // Memory offset to start of this stream from start of the metadata root
Size: DWORD; // Size of this stream in bytes, shall be a multiple of 4.
// Name of the stream as null terminated variable length
// array of ASCII characters, padded with \0 characters
Name: array[0..MaxWord] of Char;
end;
Offset是流实际位置相对Metadata头的偏移;Size是流的长度,以DWORD为单位;
Name是以#0字符结束的字符串,表示流的名称。
根据这些信息,我们可以从PE映像中读取相应流进行分析。
5.2 Metadata Stream
上一章我们曾经提过,Metadata Stream有五种常见类型,#String, #Blob,
#Guid, #US(User String)和#~流,每种类型流最多只能出现一次,#US和#Blob流
可省略。
5.2.1 #String 流
#String流是一个字符串堆,程序用到的字符串,如类名、函数名、参数名等字符串
保存在其中。字符串以UTF8编码保存,以#0字符分隔。流首总有一个#0字符代表一个
空字符串,分析代码如下。
constructor TJclClrStringsStream.Create(
const AMetadata: TJclPeMetadata;
const AHeader: PClrStreamHeader);
var
pch: PChar;
off: DWORD;
begin
inherited;
FStrings := TStringList.Create;
pch := Data;
off := 0;
while off < Size do
begin
if pch^ <> #0 then
FStrings.AddObject(pch, TObject(off));
pch := pch + StrLen(pch) + 1;
off := DWORD(pch - Data);
end;
end;
如碰到#0字符,直接作为空字符串跳过之,否则,以StrLen取得字符串长度,
并将字符串保存到一个字符串列表中,使用时需要动态将UTF8解码为Unicode。
function TJclClrStringsStream.GetString(const Idx: Integer): WideString;
begin
Result := UTF8ToWideString(FStrings.Strings[Idx]);
end;
因为流中数据是以堆形式存放,指向#String流的索引是字符串相对偏移值。
function TJclClrStringsStream.At(const Offset: DWORD): WideString;
var
Idx: Integer;
begin
Idx := FStrings.IndexOfObject(TObject(Offset));
if Idx <> -1 then
Result := GetString(Idx)
else
Result := '';
end;
CLR中包和类名的最大长度都是1024
5.2.2 #Guid 流
#Guid流格式很简单,就是一个GUID结构的数组,以流长度除以SizeOf(TGuid)就是
数组元素个数。因为流中数据是以数组形式存放,指向#Guid流的索引是以1开始的索引值,
如索引为0则表示此项索引不存在。(注意,CLR中所有索引都是以1开始的,0表示索引值
不存在)示例代码如下:
constructor TJclClrGuidStream.Create(
const AMetadata: TJclPeMetadata;
const AHeader: PClrStreamHeader);
var
I: Integer;
pg: PGUID;
begin
inherited;
SetLength(FGuids, Size div SizeOf(TGuid));
pg := Data;
for I:=0 to GetGuidCount-1 do
begin
FGuids[I] := pg^;
Inc(pg);
end;
end;
5.2.3 #Blob 和 #US 流
#Blob流是一个二进制数据堆,程序中的所有非字符串形式数据都堆放在这个流里面,
如常数的值,Public Key的值,方法的Signature等等。
在每个二进制数据块头,都有一个块长度数据,但为了节约存储空间,CLR使用了比较
麻烦的编码方法。
如果开始一个字节最高位为0,则此数据块长度为一个字节;
如果开始一个字节最高位为10,则此数据块长度为两个字节;
如果开始一个字节最高位为110,则此数据块长度为四个字节;
在屏蔽标志位后,通过移位运算即可计算出数据块的实际长度值,并依据此获得数据。
constructor TJclClrBlobRecord.Create(
const AStream: TJclClrStream;
const APtr: PByteArray);
var
b: Byte;
AData: Pointer;
ASize: DWORD;
begin
FPtr := APtr;
FOffset := DWORD(FPtr) - DWORD(AStream.Data);
b := FPtr[0];
if b = 0 then
begin
AData := @FPtr[1];
ASize := 0;
end
else if ((b and $C0) = $C0) and ((b and $20) = 0) then // 110bs
begin
AData := @FPtr[4];
ASize := ((b and $1F) shl 24) + (FPtr[1] shl 16) + (FPtr[2] shl 8) + FPtr[3];
end
else if ((b and $80) = $80) and ((b and $40) = 0) then // 10bs
begin
AData := @FPtr[2];
ASize := ((b and $3F) shl 8) + FPtr[1];
end
else
begin
AData := @FPtr[1];
ASize := b and $7F;
end;
Assert(not IsBadReadPtr(AData, ASize));
inherited Create(AData, ASize);
end;
constructor TJclClrBlobStream.Create(
const AMetadata: TJclPeMetadata;
const AHeader: PClrStreamHeader);
var
ABlob: TJclClrBlobRecord;
begin
inherited;
FBlobs := TObjectList.Create;
ABlob := TJclClrBlobRecord.Create(Self, Data);
while Assigned(ABlob) do
begin
if ABlob.Size > 0 then
FBlobs.Add(ABlob);
if (Integer(ABlob.Memory) + ABlob.Size) < (Integer(Self.Data) + Integer(Self.Size)) then
ABlob := TJclClrBlobRecord.Create(Self, Pointer(Integer(ABlob.Memory) + ABlob.Size))
else
ABlob := nil;
end;
end;
#US流是User String的存储空间,结构上和#Blob流相同,只是所有数据都是
Unicode格式的字符串。
#Blob和#US流的索引都是数据块偏移。
5.2.4 #~ 流
#~流是Metadata中最复杂也是最重要的信息所在,几乎所有Metadata信息
都以表的形式组织存放在#~流中。流起始处有一个#~ Stream Header如下
PClrTableStreamHeader = ^TClrTableStreamHeader;
TClrTableStreamHeader = packed record
Reserved: DWORD; // Reserved, always 0
MajorVersion, // Major version of table schemata, always 1
MinorVersion, // Minor version of table schemata, always 0
HeapSizes, // Bit vector for heap sizes.
Reserved2: Byte; // Reserved, always 1
Valid, // Bit vector of present tables, let n be the number of bits that are 1.
Sorted: Int64; // Bit vector of sorted tables.
// Array of n four byte unsigned integers indicating the number of rows
// for each present table.
Rows: array[0..MaxWord] of DWORD;
//Rows: array[0..n-1] of DWORD;
//Tables: array
end;
字段MajorVersion.MinorVersion是表格式的版本号,一般设置为 1.0
在介绍其他字段意义前,我们要先了解Metadata中表的表示方法。
在Metadata中,表最多有64种,每种表有一个唯一的确定的编号,如Assembly表的
编号为$20、Module表的编号为$00等等。目前的CLR只使用到了最多$2B(43)种表,
其间还有一些表编号是被保留或非公开的。经过对多个信息来源的整理,完整对应表编号如下
TJclClrTableKind = (
ttModule, // $00
ttTypeRef, // $01
ttTypeDef, // $02
ttFieldPtr, // $03
ttFieldDef, // $04
ttMethodPtr, // $05
ttMethodDef, // $06
ttParamPtr, // $07
ttParamDef, // $08
ttInterfaceImpl, // $09
ttMemberRef, // $0a
ttConstant, // $0b
ttCustomAttribute, // $0c
ttFieldMarshal, // $0d
ttDeclSecurity, // $0e
ttClassLayout, // $0f
ttFieldLayout, // $10
ttSignature, // $11
ttEventMap, // $12
ttEventPtr, // $13
ttEventDef, // $14
ttPropertyMap, // $15
ttPropertyPtr, // $16
ttPropertyDef, // $17
ttMethodSemantics, // $18
ttMethodImpl, // $19
ttModuleRef, // $1a
ttTypeSpec, // $1b
ttImplMap, // $1c
ttFieldRVA, // $1d
ttENCLog, // $1e
ttENCMap, // $1f
ttAssembly, // $20
ttAssemblyProcessor, // $21
ttAssemblyOS, // $22
ttAssemblyRef, // $23
ttAssemblyRefProcessor, // $24
ttAssemblyRefOS, // $25
ttFile, // $26
ttExportedType, // $27
ttManifestResource, // $28
ttNestedClass, // $29
ttTypeTyPar, // $2a
ttMethodTyPar); // $2b
在#~流头中,使用了两个64位位图表示这些表的状态,如Valid位图中第$20位为1
则Assembly表在当前Metadata中存在,Sorted位图中第$00位为1则Module表已排序等等。
这里就涉及到在Metadata中非常重要的一个概念Token。一个Token是在一个Metadata
中可以唯一性确定一个记录的标识符,他是一个32位无符号整型数,高8位表示表的编号
低24位表示记录在表中的索引。因此一个Token为$20000001实际上表示编号为$20的
Assembly表中第1项记录。Metadata Unmanaged API就是使用Token来表示每个记录。
对Valid位图进行从低到高位扫描,发现一个表存在,则可到Rows中获取此表中记录
的行数,Rows数组大小为Valid位图中设置的位的数量,也就是Metadata中存在的表
的数量。而Tables则是实际数据的开始。
constructor TJclClrTableStream.Create(
const AMetadata: TJclPeMetadata;
const AHeader: PClrStreamHeader);
function BitCount(const Value: Int64): Integer;
var
AKind: TJclClrTableKind;
begin
Result := 0;
for AKind:=Low(TJclClrTableKind) to High(TJclClrTableKind) do
if (Value and (Int64(1) shl Integer(AKind))) <> 0 then
Inc(Result);
end;
procedure EnumTables;
var
AKind: TJclClrTableKind;
pTable: Pointer;
begin
pTable := @Header.Rows[BitCount(Header.Valid)];
FTableCount := 0;
for AKind:=Low(TJclClrTableKind) to High(TJclClrTableKind) do
begin
if (Header.Valid and (Int64(1) shl Integer(AKind))) <> 0 then
begin
FTables[AKind] := ValidTableMapping[AKind].Create(Self, pTable, Header.Rows[FTableCount]);
pTable := Pointer(DWORD(pTable) + FTables[AKind].Size);
Inc(FTableCount);
end
else
FTables[AKind] := nil;
end;
end;
begin
inherited;
FHeader := Data;
EnumTables;
end;
这里还有一个HeapSizes,表示其他几个流中索引的大小,位$01表示#String流,
$02表示#Guid流,#04表示Blob流。如相应位设置为1,则表示对应流大于MaxWord
也就是大于2^16,因此需要以四字节来做索引,否则缺省用两字节做索引。
我们可以看到,为了节省存储空间,Metadata中使用了相对多的编码方式来合并字段。
6.小结
至此,MS.Net CLR对PE文件结构的扩展的分析,可以告一段落,更深入的对Metadata
表一级的分析,有兴趣的朋友可以参看FrameworkSDK\Tool Developers Guide中的文档、
mscli和mono的代码、也可向我索取为JCL项目编写的JclCLR解析代码(未完成)。
此外网上也有一些独立的开源项目完成了类似的工作。也欢迎大家来信与我讨论。
7.备注
.Net Framework SDK
http://msdn.microsoft.com/downloads/default.asp?url=/downloads/sample.asp?url=/msdn-files/027/000/976/msdncompositedoc.xml&frame=true
.NET Framework Service Pack 2
http://msdn.microsoft.com/netframework/downloads/sp/download.asp
ms cli
http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dndotnet/html/mssharsourcecli2.asp
mono
http://www.go-mono.com/index.html
anakrino
http://test.saurik.net/anakrino/
<待续>
为了论坛节省空间,给个连接:http://www.nsfocus.net/index.php?periodical=&kind_id=&keyword=MS.Net+CLR&act=magazine&do=search
赞赏记录
参与人
雪币
留言
时间
shinratensei
为你点赞~
2023-10-21 00:31
Youlor
为你点赞~
2023-9-29 00:01
伟叔叔
为你点赞~
2023-6-26 00:03
一笑人间万事
为你点赞~
2023-5-14 01:17
心游尘世外
为你点赞~
2023-4-22 01:30
飘零丶
为你点赞~
2023-4-20 04:48
QinBeast
为你点赞~
2023-4-8 01:48
赞赏
他的文章
- 有事外出,向坛主请辞 4389
- [分享]关于windbg进行双机调试的一些资料 6474
- [分享]转四篇关于MS.Net CLR扩展PE结构分析的文章 5513
- Microsoft.NET.框架程序设计-中文版.rar[推荐] 2875
- [翻译]Reversing .NET 21015
看原图
赞赏
雪币:
留言: