以文本方式查看主题

-  中文XML论坛 - 专业的XML技术讨论区  (http://bbs.xml.org.cn/index.asp)
--  『 Dot NET,C#,ASP,VB 』  (http://bbs.xml.org.cn/list.asp?boardid=43)
----  MS.Net CLR 扩展PE结构分析(3)  (http://bbs.xml.org.cn/dispbbs.asp?boardid=43&rootid=&id=11790)


--  作者:admin
--  发布时间:11/9/2004 2:25:00 AM

--  MS.Net CLR 扩展PE结构分析(3)


发信人: flier (小海->找啊找工作 :)), 信区: DotNET        
标  题: MS.Net CLR 扩展PE结构分析(3)
发信站: BBS 水木清华站 (Mon Mar 18 23:03:30 2002)

MS.Net CLR 扩展PE结构分析

Flier Lu <flier_">lu@sina.com.cn>

注意:本系列文章在水木清华BBS(smth.org)之.Net版首发,
     转载请保留以上信息,发表请与作者联系

Metadata 篇

第二章 Metadata在PE中的结构分析

  上一章大概介绍了Metadata在CLR架构中的作用,以及其在PE
文件中的组织结构。本章将从实际的二进制流的角度来分析Metadata
流在PE中的结构。

2.1 Metadata Header

  前言中我们分析了CLR Header的结构,里面有一个Metadata字段
  IMAGE_DATA_DIRECTORY    MetaData;
  字段指向一个数据块,里面包含着Metadata Header,是关于
Metadata信息所在,结构定义伪代码如下

  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;

  首先是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类型。注意这里Version字符串是按四字节对齐的,
在分析二进制流时应按(m+3) and (not $3)来计算实际的长度。
  Streams和StreamHeaders则是Metadata流的信息数组,保存有Metadata不同类型
流的Offset, Size, Name等信息,没记录结构如下

  TClrStreamHeader = packed record
    Offset,      // Memory offset to start of this stream from start of the me
tadata 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;

  根据这些信息,我们可以从PE映像中读取相应流进行分析。

2.2 Metadata Stream

  上一章我们曾经提过,Metadata Stream有五种常见类型,#String, #Blob,
#Guid, #US(User String)和#~流,每种类型流最多只能出现一次。

2.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流的索引是字符串偏移值。

2.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;

2.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(Se
lf.Size)) then
      ABlob := TJclClrBlobRecord.Create(Self, Pointer(Integer(ABlob.Memory) +  
ABlob.Size))
    else
      ABlob := nil;
  end;
end;

  #US流是User String的存储空间,结构上和#Blob流相同,只是所有数据都是
Unicode格式的字符串。
  #Blob和#US流的索引都是数据块偏移。

2.2.4 #~ 流

  #~流是Metadata中最复杂也是最重要的信息所在,几乎所有Metadata信息
都以表的形式组织存放在#~流中。流起始处有一个#~ Stream Header如下
  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 b
its 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..n-1] of DWORD;
    Tables: array[0..n-1] of ???
  end;
  字段MajorVersion.MinorVersion是表格式的版本号,一般设置为 1.0;
  在介绍其他字段意义前,我们要先了解Metadata中表的表示方法。
  在Metadata中,表最多有64种,每种表有一个唯一的确定的编号,如Assembly表的
编号为$20、Module表的编号为$00等等。目前的CLR只使用到了最多$2B(43)种表,
其间还有一些表编号是被保留或非公开的。对应表编号如下

  TJclClrTableKind = (
    ttModule,               //  $00
    ttTypeRef,              //  $01
    ttTypeDef,              //  $02
    ttUnknown03,            //  $03
    ttFieldDef,             //  $04
    ttUnknown05,            //  $05
    ttMethodDef,            //  $06
    ttUnknown07,            //  $07
    ttParamDef,             //  $08
    ttInterfaceImpl,        //  $09
    ttMemberRef,            //  $0a
    ttConstant,             //  $0b
    ttCustomAttribute,      //  $0c
    ttFieldMarshal,         //  $0d
    ttDeclSecurity,         //  $0e
    ttClassLayout,          //  $0f
    ttFieldLayout,          //  $10
    ttSignature,            //  $11
    ttEventMap,             //  $12
    ttUnknown13,            //  $13
    ttEvent,                //  $14
    ttPropertyMap,          //  $15
    ttUnknown16,            //  $16
    ttProperty,             //  $17
    ttMethodSemantics,      //  $18
    ttMethodImpl,           //  $19
    ttModuleRef,            //  $1a
    ttTypeSpec,             //  $1b
    ttImplMap,              //  $1c
    ttFieldRVA,             //  $1d
    ttUnknown1e,            //  $1e
    ttUnknown1f,            //  $1f
    ttAssembly,             //  $20
    ttAssemblyProcessor,    //  $21
    ttAssemblyOS,           //  $22
    ttAssemblyRef,          //  $23
    ttAssemblyRefProcessor, //  $24
    ttAssemblyRefOS,        //  $25
    ttFile,                 //  $26
    ttExportedType,         //  $27
    ttManifestResource,     //  $28
    ttNestedClass,          //  $29
    ttUnknown2a,            //  $2a
    ttUnknown2b);           //  $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则是实际数据的开始。
  这里还有一个HeapSizes,表示其他几个流中索引的大小,位$01表示#String流,
$02表示#Guid流,#04表示Blob流。如相应位设置为1,则表示对应流大于MaxWord
也就是大于2^16,因此需要以四字节来做索引,否则缺省用两字节做索引。
  我们可以看到,为了节省存储空间,Metadata中使用了相对多的编码方式来合并字段
缩减尺寸,如后面将提到的Coded Index亦是如此。

2.3 相关信息

  关于Metadata结构的详细定义可以在.Net FrameworkSDK的Tool Developers Guide
目录下的Partition II Metadata.doc文档中找到。
  示例代码可以参考mono工程对metadata的分析,http://go-mono.com/
  或者下载JCL(Delphi Jedi Code Library)项目最新代码,
http://groups.yahoo.com/group/jedi-jcl,或到www.sourceforge.org
上查找JCL项目,里面有我的较新版本的CLR分析代码JclCLR和相关使用示例ClrDemo
或者使用FreeVCS工具到demos.href.com:2106以账号jcluser1:jcluser1下载
最新版本的JCL代码。

2.4 小结

  这一章我们大概了解了Metadata在PE中实际的物理结构和分析方法,在对整体结构
有所了解,对一般概念如Token等有所了解后,我们将在下一章开始对实际的Metadata
表进行分析,看看在Reflection或者Metadata Unmanaged API后面到底隐藏了些什么。
--
.  生命的意义在于   /\   ____\ /\_ \   /\_\                             .  
.      希望         \ \  \___/_\/\ \   \/_/__     __    _ _★           .  
.      工作          \ \   ____\\ \ \    /\  \  /'__`\ /\`'_\           .  
.    爱你的人         \ \  \___/ \ \ \___\ \  \/\  __//\ \ \/           .  
.   和你爱的人         \ \___\    \ \_____\ \__\ \____\ \ \_\           .  
.      ……             \/___/     \/_____/\/__/\/____/  \/_/ @126.com.  


※ 修改:·flier 於 Mar 18 23:04:54 修改本文·[FROM:  202.114.32.216]
※ 来源:·BBS 水木清华站 smth.org·[FROM: 202.114.32.216]
上一篇
返回上一页
回到目录
回到页首
下一篇



W 3 C h i n a ( since 2003 ) 旗 下 站 点
苏ICP备05006046号《全国人大常委会关于维护互联网安全的决定》《计算机信息网络国际联网安全保护管理办法》
6,316.406ms