.NET平台PE结构分析之Metadata(一)

 

.NET平台PE结构分析之Metadata(一)

强命名及其去除

 

       首先,这不是一篇完整的参考,所以并没有涉及Metadata的各个方面,而只是讨论了与强命名有关的部分。所以,在开始前,先列出一些参考文献,在阅读过程中若遇到问题,可以直接从中查阅。

 

两本书:The Common Language Infrastructure Annotated StandardAddison Wesley

              Inside Microsoft .NET IL AssemblerMicrosoft Corporation

文章:  MS.Net CLR扩展PE结构分析  (作者:Flier Lu

              The .NET File Format             (来自:codeproject.com

当然,还有最权威的Framework SDK的文档。

 

本文的例子文件:

 

1、什么是强命名(StrongName

       我的理解,强命名类似win32平台下PE文件的checksum,用来对原始文件完整性进行验证的。一般有两个作用:一是不同版本的相同文件,Strongname是不一样的,因此可以将其区分开;二是防止文件被修改,被修改的文件是无法运行的。第一点我们不关心,但是第二点就应该引起cracker们的注意了。一个加密非常简单的文件,只要改动一个字节的数据(比如jejne)就可以破解,结果修改了过后却无法运行。郁闷啊!因此,在对有强命名的PE文件修改前,必须去掉其强命名。(太累了,下面一律简称SN。)

 

2、怎么给程序加上SN

       这和cracker关系不大,但是了解一下有好处,至少有个感性的印象。

       第一步,是生成一个key文件,命令是:sdk安装目录\bin\sn.exe –k strong.snk

       第二步,把strong.snk文件的信息加入到你需要加密的Module当中,通常在AssemblyInfo.cs的文件中添加:

       [assembly: AssemblyKeyFile(@"完整路径\strong.snk")]

       然后编译生成就OK了。这样,一个含有SN的文件便生成。可以用我写的工具snView看一下(文件见末尾的pskill.exe,这是我N年前写的一个查杀进程的程序,已被加上SN):

       看一下它的保护效果,用UD打开文件,把末尾的一个字节从00改为01,再运行。报错,如下:

 

3、去除强命名的两种方法

       下面介绍去除SN的两种方法,第一种手动,第二种自动。

 

3.1、反汇编成il代码,修改后再编译成exe文件

       这个方法不多讲了,codeproject上有几篇文章详细说过:Building Security Awareness in .NET Assemblies : Part 3 - Learn to break strong name .NET Assemblies ,只大概说一下过程。

 

1、  ildasm反汇编

2、  .assembly 这个assemblyname块中寻找.publickey,如图:

       注意,会搜索到很多.publickeytoken,而且长度较短。这些都不是该文件(assembly,又叫装配件)的SN,而不过是其中的方法/类等等的唯一性标志。

3、  删除选定的部分

包含两个,一个是key的值,一个是.hash algorithm,这是计算该key的算法。 

4、  再用ilasm进行编译

ilasm /resource=psill.res pskill.il

 

这时就可以对这个文件进行修改了。

BUT,这种方法有两个缺点:一是麻烦,二是某些文件没法反汇编,或反汇编不完全,或反汇编后就无法再次汇编成功。(特别是混淆过的程序)

 

3.2、直接在文件上修改

这样最方便,但是,方便的前提是你知道.NET判断SN的数据及修改方法,这就要牵涉到Metadata了。

原先网上有一个工具,叫snRemove,不过不好用,修改完了运行不了。这里先绍一个偶写的工具:snRemover,可以自动去除程序中的SN。下载请到http://vxer.cn/hmx

 

 

下面介绍snRemover的原理。什么是Metadata?我们都知道,.NET下运行的PE文件类似JAVA,不是将指令编译成机器代码,而是编译成il中间代码,再在运行时进行既时编译(JIT)。这样,用一些软件可以直接打开PE文件,看到类名、方法名、指令等等。所有的这些东东,都是Metadata。我们的任务,就是在Metadata中,找到标识SN的地方并修改之。

下面假定你已经对win32平台下PE结构有些了解了,讲述从简。

PE文件中紧跟PE Header的是16Data Directory Table,最常见的是第1个输出表和第2个输入表。而.NET扩展的PE结构则由倒数第二个表指向,也就是Common Language Runtime header address and size(简称CLI),根据他,我们找到了CLI Header。以pskill.exe为例,CLI HeaderRVA2008,大小是48,算出物理偏移是1008。你现在就可以用UD打开pskill.exe跟着我走了。

00001008h: 48 00 00 00 02 00 05 00 10 42 00 00 60 11 00 00 ; H........B..`...

00001018h: 09 00 00 00 04 00 00 06 A8 26 00 00 65 1B 00 00 ; ........?..e...

00001028h: 50 20 00 00 80 00 00 00 00 00 00 00 00 00 00 00 ; P ..€...........

00001038h: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ; ................

00001048h: 00 00 00 00 00 00 00 00                         ; ........

 

CLI Header的结构如下:

RVA

Field

Contents

0x2008

Cb(结构的大小)

0x48

0x200C

MajorRuntimeVersion

2

0x200E

MinorRuntimeVersion

0

0x2010

MetaData

0x2060

0x2014

Size of the Metadata

0x148 =(RVA of Import Table) – (RVA of MetaData)

0x2018

Flags

1

0x201C

EntryPointToken

0x06000001 (Method #1 in TypeDef table)

0x2020

Resources

0

0x2028

StrongNameSignature

0

0x2030

CodeManagerTable

0

0x2038

VTableFixups

0

0x2040

ExportAddressTableJumps

0

0x2048

ManagedNativeHeader

0

 

这里,出现了两处和SN有关的标识。一处是FLAGS,另一处是StrongNameSignature。对于FLAGS,有这个标志:

COMIMAGE_FLAGS_STRONGNAMESIGNED (0x00000008) 

如果这处标志被置位,则认为有SN。第二处则指出了SN数据的RVA和大小,也就是最开始用snView看到的。

修改时,FLAGS标志位减去0x00000008,然后把StrongNameSignatureRVASIZE 均填0。运行一下试试,还是出错。当然,还有一处最重要的地方要修改,我们继续。

注意第四项Metadata,他指出了Metadata表的RVA和大小。看一下,pskillMetadataRVA=4210处,也就是物理地址3210处。

00003210h: 42 53 4A 42 01 00 01 00 00 00 00 00 0C 00 00 00 ; BSJB............

00003220h: 76 32 2E 30 2E 35 30 37 32 37 00 00 00 00 05 00 ; v2.0.50727......

00003230h: 6C 00 00 00 7C 05 00 00 23 7E 00 00 E8 05 00 00 ; l...|...#~..?..

00003240h: 80 07 00 00 23 53 74 72 69 6E 67 73 00 00 00 00 ; €...#Strings....

00003250h: 68 0D 00 00 80 01 00 00 23 55 53 00 E8 0E 00 00 ; h...€...#US.?..

00003260h: 10 00 00 00 23 47 55 49 44 00 00 00 F8 0E 00 00 ; ....#GUID...?..

00003270h: 68 02 00 00 23 42 6C 6F 62 00 00 00 00 00 00 00 ; h...#Blob.......

00003280h: 02 00 00 01 57 15 02 00 09 01 00 00 00 FA 01 33 ; ....W........?3

00003290h: 00 16 00 00 01 00 00 00 33 00 00 00 02 00 00 00 ; ........3.......

000032a0h: 09 00 00 00 0A 00 00 00 0C 00 00 00 53 00 00 00 ; ............S...

000032b0h: 0D 00 00 00 04 00 00 00 01 00 00 00 05 00 00 00 ; ................

000032c0h: 01 00 00 00 00 00 0A 00 01 00 00 00 00 00 06 00 ; ................

 

看一下文档中对Metadata的定义:

Type

Field

Description

DWORD

lSignature

“Magic” signature for physical metadata, currently 0x424A5342

WORD

iMajorVersion

Major version (1 for the first release of the common language runtime)

WORD

iMinorVersion

Minor version (1 for the first release of the common language runtime)

DWORD

iExtraData

Reserved; set to 0

DWORD

iLength

Length of the version string

BYTE[]

iVersionString

Version string

BYTE

fFlags

Reserved; set to 0

BYTE

 

[padding]

WORD

iStreams

Number of streams

 

第一项,Metadata根部的标识,ASC码“BSJB”。这样,以后我们在寻找它时就可以直接搜索“BSJB”既可。这里有一点注意,就是ASC码串VersionString是可变长度的,结束后再加一个fFlags,然后要和4字节对齐,也就是padding。这里,我们的版本号是v2.0.50727,前面iLength指出了长度是0C(十进制的12,已经是和4对齐的了,能整除),因此fFlags的地址就是00003220+0C=0000322C,后一个字节为空,又是padding。最后,05 00指出了Number of streams,共有几个数据流。

Metadata中的数据都是存放在各种数据流stream里,比较重要的是“#~”和“#Strings”,后者保存了各种名称(比较混淆或者反混淆,就要从这个流着手,如果有机会,下次再讲),而与SN相关的则是#~流。它也是所有当中最复杂的。

紧接着上面的数据,就是各个流的Header了:

00003230h: 6C 00 00 00 7C 05 00 00 23 7E 00 00 E8 05 00 00 ; l...|...#~..?..

00003240h: 80 07 00 00 23 53 74 72 69 6E 67 73 00 00 00 00 ; €...#Strings....

00003250h: 68 0D 00 00 80 01 00 00 23 55 53 00 E8 0E 00 00 ; h...€...#US.?..

00003260h: 10 00 00 00 23 47 55 49 44 00 00 00 F8 0E 00 00 ; ....#GUID...?..

00003270h: 68 02 00 00 23 42 6C 6F 62 00 00 00 00 00 00 00 ; h...#Blob.......

 

这个结构不难,如下:

Type

Field

Description

DWORD

iOffset

Offset in the file for this stream

DWORD

iSize

Size of the stream in bytes

char[]

rcName

Name of the stream; a zero-terminated ANSI string no longer than seven characters

 

 

我们以#~为例

00003230h: 6C 00 00 00 7C 05 00 00 23 7E 00 00 E8 05 00 00 ; l...|...#~..?..

红色部分是RVA,相对于Metadata Root的,蓝色部分是大小,而黑色斜体就是“#~”的ASC码了。那为什么237E后要加两个字节的0呢?又忘了?因为字符串要与4字节对齐。我们来计算#~流的实际物理地址:offset=root + RVA=00003210+6C=0000327C

0000327ch: 00 00 00 00 02 00 00 01 57 15 02 00 09 01 00 00 ; ........W.......

0000328ch: 00 FA 01 33 00 16 00             

 

对应的结构如下:

Size

Field

Description

4 bytes

Reserved

Reserved; set to 0.

1 byte

Major

Major version of the table schema (1 for the first release of the common language runtime).

1 byte

Minor

Minor version of the table schema (0 for the first release of the common language runtime).

1 byte

Heaps

Binary flags indicate the offset sizes to be used within the heaps.

A 4-byte unsigned integer offset is indicated by 0x01 for a string heap, 0x02 for a GUID heap, and 0x04 for a blob heap.

If a flag is not set, the respective heap offset is presumed to be a 2-byte unsigned integer.

 

 

A # stream can also have special flags set: flag 0x20, indicating that the stream contains only changes made during an edit-and-continue session, and flag 0x80, indicating that the metadata might contain items marked as deleted.

1 byte

Rid

Bit count of the maximal record index to all tables of the metadata; calculated at run time (during the metadata stream initialization).

8 bytes

MaskValid

Bit vector of present tables, each bit representing one table (1 if present).

8 bytes

Sorted

Bit vector of sorted tables, each bit representing a respective table (1 if sorted).

 

这里要讲一下#~流中各种数据的保存形式了。该流中保存的主要是各种表,这些表又定义了Metadata中其它的各种数据,所以才说它重要啊。现在微软已经定义的表有

注意结构中的MaskValid数据,它是8字节的,对应2进制数有64位。从最低位开始,如果这个位为1,代表#~流中该表被定义了,如果为0,代表没有该表。我们看一下pskill的数据,为57 15 02 00 09 01 00 00,翻译为2进制为

2进制:0000 0000 0000 0000 0000 0001 0000 1001 0000 0000 0000 0010 0001 0101 0101 0111

16进制: 0   0    0   0    0   1    0   9    0   0    0   2    1   5   5    7

这样我们就知道了一共有C个表被定义了,pskill中存在的表可以用Spices .Net看一下,再与上表对应一下,看看是不是相等:

 

 

同时,我们点击了第20个表,AssemblyDef,看到了右边的数据显示出了PublicKey,那不正是我们要找的SN吗。

              接下来的工作就是计算AssemblyDef前面表的大小,然后直到找到AssemblyDef为止。剩下的不多讲了,可以看codeproject的那篇THE .NET File Format。但是这个过程是非常烦索的,我写的强命名去除工具snRemover也没有说细的计算,而是选择一个比较偷懒的方法。下面再说。我们先来到AssemblyDef处:

0000376eh: 04 80 00 00 01 00 00 00 05 09 64 5F 01 00 00 00 ; .€........d_....

0000377eh: 46 00 1B 00 00                                  ; F....

 

来看一下AssemblyDef的定义:

 

HashAlgId (a 4-byte constant of type AssemblyHashAlgorithm).

MajorVersion, MinorVersion, BuildNumber, RevisionNumber

(2-byte constants).

Flags (a 4-byte bit mask of type AssemblyFlags).

PublicKey (index into Blob heap).

Name (index into String heap).

Culture (index into String heap).

一共有6项,其中Flags项有一个常数为

afPublicKey = 0x0001,

// The assembly ref holds the full (unhashed) public key.

也就是说,如果Flags(数据中蓝色部分)的第一位被置1,则认为它有SN。因此,我们将Flags1,然后将.PublicKey项(黑色斜体部分,指向BLOG中的指针)置0。现在才彻底修改完成。运行一下,OK      

              偶是怎么定义AssemblyDef的地方的呢?因为该表的第一项为HashAlgId,目前只有三种可能:00008004,000080030。如果是0,代表没有SN。因此直接从#~开始,搜索00008004或者00008003,定义既可。但是有失败的可能,因为不能保证AssemblyDef之前的表中没有0000800400008003,那样的话就玩完了。不过我试了那么多程序,暂时没有发现不能用。等回头有空再把snRemover改成精确定位吧!

 

 

              要是你能坚持看到这,真得感谢你了,头晕了吧!我打字都不行了。那就休息一下,下次再讲讲简单的,因为最难的部分已经讲完了。

 

 

By:tankaiha [NE365]

2006.04.28

Any bug, report to http://vxer.cn/

 

原文地址:https://www.cnblogs.com/cxd4321/p/1213313.html