.NET平台PE结构分析之Metadata(⼀)
.NET平台PE结构分析之Metadata(⼀)
强命名及其去除
⾸先,这不是⼀篇完整的参考,所以并没有涉及Metadata的各个⽅⾯,⽽只是讨论了与强命名有关的部分。所以,在开始前,先列出⼀些参考⽂献,在阅读过程中若遇到问题,可以直接从中查阅。
两本书:The Common Language Infrastructure Annotated Standard(Addison Wesley)
Inside Microsoft .NET IL Assembler(Microsoft Corporation)
⽂章:  MS.Net CLR扩展PE结构分析(作者:Flier Lu)
The .NET File Format             (来⾃:codeproject)
当然,还有最权威的Framework SDK的⽂档。
本⽂的例⼦⽂件:
1、什么是强命名(StrongName)
我的理解,强命名类似win32平台下PE⽂件的checksum,⽤来对原始⽂件完整性进⾏验证的。⼀般有两个作⽤:⼀是不同版本的相同⽂件,Strongname是不⼀样的,因此可以将其区分开;⼆是防⽌⽂件被修改,被修改的⽂件是⽆法运⾏的。第⼀点我们不关⼼,但是第⼆点就应该引起cracker们的注意了。⼀个加密⾮常简单的⽂件,只要改动⼀个字节的数据(⽐如je变jne)就可以破解,结果修改了过后却⽆法运⾏。郁闷啊!因此,在对有强命名的PE⽂件修改前,必须去掉其强命名。(太累了,下⾯⼀律简称SN。)
2、怎么给程序加上SN
这和cracker关系不⼤,但是了解⼀下有好处,⾄少有个感性的印象。
第⼀步,是⽣成⼀个key⽂件,命令是:sdk安装⽬录\ –k strong.snk。
第⼆步,把strong.snk⽂件的信息加⼊到你需要加密的Module当中,通常在AssemblyInfo.cs的⽂件中添加:
[assembly: AssemblyKeyFile(@"完整路径\strong.snk")]
然后编译⽣成就OK了。这样,⼀个含有SN的⽂件便⽣成。可以⽤我写的⼯具snView看⼀下(⽂件见末尾的,这是我N年前写的⼀个查杀进程的程序,已被加上SN):
看⼀下它的保护效果,⽤UD打开⽂件,把末尾的⼀个字节从00改为01,再运⾏。报错,如下:
3、去除强命名的两种⽅法
下⾯介绍去除SN的两种⽅法,第⼀种⼿动,第⼆种⾃动。
3.1、反汇编成il代码,修改后再编译成exe⽂件
这个⽅法不多讲了,codeproject上有⼏篇⽂章详细说过:,只⼤概说⼀下过程。
1、⽤ildasm反汇编
2、在.assembly 这个assembly的name块中寻.publickey,如图:
注意,会搜索到很多.publickeytoken,⽽且长度较短。这些都不是该⽂件(assembly,⼜叫装配件)的SN,⽽不过是其中的⽅法/类等等的唯⼀性标志。
3、删除选定的部分
包含两个,⼀个是key的值,⼀个是.hash algorithm,这是计算该key的算法。 
4、再⽤ilasm进⾏编译
ilasm /s pskill.il
这时就可以对这个⽂件进⾏修改了。
BUT,这种⽅法有两个缺点:⼀是⿇烦,⼆是某些⽂件没法反汇编,或反汇编不完全,或反汇编后就⽆法再次汇编成功。(特别是混淆过的程序)
3.2、直接在⽂件上修改
这样最⽅便,但是,⽅便的前提是你知道.NET判断SN的数据及修改⽅法,这就要牵涉到Metadata了。
原先⽹上有⼀个⼯具,叫snRemove,不过不好⽤,修改完了运⾏不了。这⾥先绍⼀个偶写的⼯具:snRemover,可以⾃动去除程序中的SN。下载请到
下⾯介绍snRemover的原理。什么是Metadata?我们都知道,.NET下运⾏的PE⽂件类似JAVA,不是将指令编译成机器代码,⽽是编译成il中间代码,再在运⾏时进⾏既时编译(JIT)。这样,⽤⼀些软件可以直接打开PE⽂件,看到类名、⽅法名、指令等等。所有的这些东东,都是Metadata。我们的任务,就是在Metadata中,到标识SN的地⽅并修改之。
下⾯假定你已经对win32平台下PE结构有些了解了,讲述从简。
在PE⽂件中紧跟PE Header的是16个Data Directory Table,最常见的是第1个输出表和第2个输⼊表。⽽.NET扩展的PE结构则由倒数第⼆个表指向,也就是Common Language Runtime header address and size(简称CLI),根据他,我们到了CLI Header。以为例,CLI Header的RVA是2008,⼤⼩是48,算出物理偏移是1008。你现在就可以⽤UD打开跟着我⾛了。
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                        ; ........字符串长度可以为1吗
CLI Header的结构如下:
RVA Field Contents
0x2008Cb(结构的⼤⼩)0x48
0x200C MajorRuntimeVersion2
0x200E MinorRuntimeVersion0
0x2010MetaData0x2060
0x2014Size of the Metadata0x148 =(RVA of Import Table) – (RVA of MetaData)
0x2018Flags1
0x201C EntryPointToken0x06000001 (Method #1 in TypeDef table)
0x2020Resources0
0x2028StrongNameSignature0
0x2030CodeManagerTable0
0x2038VTableFixups0
0x2040ExportAddressTableJumps0
0x2048ManagedNativeHeader0
这⾥,出现了两处和SN有关的标识。⼀处是FLAGS,另⼀处是StrongNameSignature。对于FLAGS,有这个标志:
COMIMAGE_FLAGS_STRONGNAMESIGNED (0x00000008)
如果这处标志被置位,则认为有SN。第⼆处则指出了SN数据的RVA和⼤⼩,也就是最开始⽤snView看到的。
修改时,FLAGS标志位减去0x00000008,然后把StrongNameSignature的RVA和SIZE 均填0。运⾏⼀下试试,还是出错。当然,还有⼀处最重要的地⽅要修改,我们继续。
注意第四项Metadata,他指出了Metadata表的RVA和⼤⼩。看⼀下,pskill的Metadata在RVA=4210处,也就是物理地址3210处。
00003210h: 42 53 4A 42 01 00 01 00 00 00 00 00 0C 00 00 00 ;
00003220h: 76 32 2E 30 2E 35 30 37 32 37 00 00 00 00 05 00 ; v2.
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 ; €...#
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 ; ....#?..
00003270h: 68 02 00 00 23 42 6C 6F 62 00 00 00 00 00 00 00 ; h...#
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 ; €...#
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 ; ....#?..
00003270h: 68 02 00 00 23 42 6C 6F 62 00 00 00 00 00 00 00 ; h...#
这个结构不难,如下:
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
Type Field Description
我们以#~为例
00003230h: 6C 00 00 007C 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。因此,我们将Flags减1,然后将.PublicKey项(⿊⾊斜体部分,指向BLOG中的指针)置0。现在才彻底修改完成。运⾏⼀下,OK。
偶是怎么定义AssemblyDef的地⽅的呢?因为该表的第⼀项为HashAlgId,⽬前只有三种可能:00008004,00008003和0。如果
是0,代表没有SN。因此直接从#~开始,搜索00008004或者00008003,定义既可。但是有失败的可能,因为不能保证AssemblyDef之前的表中没有00008004或00008003,那样的话就玩完了。不过我试了那么多程序,暂时没有发现不能⽤。等回头有空再把snRemover改成精确定位吧!
要是你能坚持看到这,真得感谢你了,头晕了吧!我打字都不⾏了。那就休息⼀下,下次再讲讲简单的,因为最难的部分已经讲完了。
By:tankaiha [NE365]
2006.04.28
Any bug, report to

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。