黑龙过江小说重庆女人:CLI里面的秘密

来源:百度文库 编辑:偶看新闻 时间:2024/04/20 01:06:30

如何创建强命名程序集(Strong Name Assembly)

创建一个强命名程序集首先需要获得一个用强命名实用工具
(Strong Name Utility,即SN.exe,.NET SDK自带)产生的密钥。
下面简要介绍一下SN.exe的一些用法。要产生一个公钥/私钥对:

   a)SN –k MyCompany.Keys
该命名告诉SN.exe创建一个名为MyCompany.keys的文件。MyCompany.keys文件将包含以对以二进制格式存储的公有密钥和私有密钥。

   b)查看公有密钥:
首先生成一个只包含公有密钥的文件: SN –p
   MyCompany.keys MyCompany.PublicKey
然后用-tp参数查看:SN –tp MyCompany.PublicKeys
   Public key is

   00240000048000009400000006020000002400005253413

   10004000001000100bb7214723ffc13901343df4b9c464ebf

   7ef4312b0ae4d31db04a99673e8163768cc0a2a7062e731d

   beb83b869f0509bf8009e90db5c8728e840e782d2cf928dae

   35c2578ec55f0d11665a30b37f8636c08789976d8ee9fe9a5

   c4a0435f0821738e51d6bdd6e6711a5acb620018658cce93

   df37d7e85f9a0104a5845053995ce8

   Public key token is 2dc940d5439468c2

创建好了公钥/私钥对,创建强命名程序集就很容易了。只需要把System.Reflection.AssemblyKeyFileAttribute特性加入到源代码中就可以了:[assembly:AssemblyKeyFile("MyCompany.keys")]

说明:公钥/私钥对文件的扩展名可以是任意的(也可以没有),因为编译的时候都是以元数据的格式读取的。

   4.程序集的部署方式
一个程序集有两种部署方式:
   a)私有方式
和应用程序部署在同一目录下的程序集称作私有部署程序集。弱命名程序集只能进行私有部署。

   b)全局方式
全局部署方式将程序集部署在一些CLR已确知的地方,当CLR搜索程序集时,它会知道到这些地方去找。强命名程序集既可以进行私有部署,也可以进行全局部署。

   5.如何部署强命名程序集(Strong Name Assembly)和GAC
   a)GAC的概念
如果一个Assembly要被多个应用程序访问,那么他就必须放在一个CLR已确知的目录下,并且CLR在探测到有对该Assembly的引用时,它必须能自动到该目录下寻找这个程序集。这个已确知的目录称作GAC(Global Assembly Cache),就是全局程序集缓存。它一般位于下面的目录下::\Windows\Assembly\GAC。
   GAC的作用就是提供给CLR一个已知的确定的目录去寻找引用的程序集。

   b)GAC的内部结构
   GAC是一个特殊的结构化的目录,用Windows Explorer浏览你会以为它只是一个包含很多程序集的普通目录。其实不是这样的,在命令行下查看,你会发现它实际上包含很多子目录,子目录的名字和程序集的名称是相同的,但它们都不是实际的程序集,实际的程序集位于程序集名对应的目录下。比如进入GCFWK子目录,我们会发现其中又有很多的子目录。机器内每一个安装到GAC的GCFWK.dll在GCFWK中都会有一个子目录。


这里只有一个目录表明只有一个版本的GCFWK程序集被安装。实际的程序集保存在每一个对应的版本目录下。目录的名称以下划线的形式分割为“(Version)_(Culture)_(PublicKeyToken)”。

   GCFWK的语言文化信息为netture,就表示为0.0.0__bf5779af662fc055”。表示得意义是: “GCFWK, Version=1.0.0.0, Culture=neutral,PublicKeyToken=bf5779af662fc055” 如果语言文化信息为”ja”,就表示”1.0.0.0_ja_bf5779af662fc055”

表示得意义是: “GCFWK, Version=1.0.0.0, Culture=ja, PublicKeyToken=bf5779af662fc055”

   c)部署强命名程序集到GAC

   GAC包含很多子目录,这些子目录是用一种算法来产生的,我们最好不要手动将程序集拷贝到GAC中,相反,我们应使用工具来完成这样的工作。因为这些工具知道GAC的内部结构J

在开发和测试中,最常用的工具就是GACUtil.exe。在GAC中注册程序集跟COM注册差不多,但相对更容易:
   1.把程序集添加到GAC中: GACUtil /i sample.dll (参数/i是安装的意思)
   2.把程序集移出GAC GACUtil /u sample.dll (参数/u就移除的意思)
注意:不能将一个弱命名程序集安装到GAC中。
如果你试图把弱命名程序集加入到GAC中,会收到错误信息:”
   Failure adding assembly to the cache: Attempt to install an assembly without a strong name”
   d)强命名程序集的私有部署

把程序集安装到GAC有几个好处。首先,GAC使得很多程序可以共享程序集,这从整体上减少了使用的物理内存;其次,我们很容易将一个新版的程序集部署到 GAC中,并通过一种发布者策略(差不多就是一种重定向方法,比如将原来引用版本为1.0.0.0程序集的程序,通过更改它的配置文件,转而让程序去引用版本为2.0.0.0的程序集)来使用新版本;最后,GAC还提供了对不同版本程序集的并存(side-by-side)管理方式。但是,GAC的安全策略通常只允许管理员更改,同时,向GAC中安装程序集也破坏了.NET框架的简单拷贝部署的许诺。

除了向GAC或者以私有部署方式部署强命名程序集之外,我们还可以将强命名程序集部署在仅为一小部分程序知道的某个任意目录下。配置每一个应用程序的 XML配置文件,让它们指向一个公有目录,这样,在运行时,CLR将知道到哪里去找这个强命名程序集。但这样又有可能会引发”DLL Hell”的问题,因为没有哪个程序可以控制这个程序集何时被卸载。这在.NET中也是不被鼓励的。强命名策略:

生成公钥与私钥对,并对私钥做严格的保护
生成:sn -k keyfile.snk 公钥与私钥对
抽取公钥:sn -p keyfile.snk public.snk 从keyfile中抽取公钥保存到public.snk文件中,以对程序集进行迟签名
跳过验证:sn -Vr assembly 对assembly在开发用机上进行跳过验证处理,该assembly不是已签名的
签名:sn -r assembly keyfile.snk 对assembly 进行迟签名,这一步是在发布之前做
取消跳过验证:sn -Vu assembly 对assembly在开发用机上取消跳过验证,该assembly这时是已签名的
取消所有:sn -Vx 取消所有验证

开发阶段
assembly不是已签名的,但是强命名的strong named,因此需要在开发机上做跳过验证处理

发布阶段
由私钥控制者对assembly进行签名,并在开发机上由开发人员自行取消跳过验证http://leonardleonard.iteye.com/blog/276417


 

http://www.cnblogs.com/sumtec/articles/6872.html

不知道大家对于Common Language Infrastructure有什么认识呢?“噢!天啊!看到那么几个英文我就头痛了!”如果真是这样,那么你就没有办法继续看下去了,因为这里面的东西基本上只能够找到英文的资料。

实际上这个看似很深奥的东西并没有你想象的那么难,当然,也不是一个简单的东西。关于这方面的资料其实非常好找,虽然数量不多,但是却是非常之实用,就在你VS安装的目录里面。假如你安装的是VS2003,装在了E:\VS.NET
2003这个目录里面,那么相关的资料就在E:\VS.NET 2003\SDK\v1.1\Tool Developers
Guide\里面。这个目录下面有两个子目录,一个是docs,全部都是文档,另外一个是Samples,全部都是例子,不过所有的东西都是英文的。

什么是CLI呢?中文应该翻译成“公共语言底层结构”。CLI应该包括CIL和CLR:CIL是Common Intermediate
Language,中文是“公共中间语言”,也就是那个“IL汇编”;CLR是Common Language
Runtime,中文是“公共语言运行库”。除了这些之外,任何一个.NET语言还收到CLS的约束,CLS——Common Language
Specification,公共语言规格说明书,这个东西主要用于约束所有.NET语言,使得他们能够互相协作,不存在某种语言产生的代码不能够被另外一种语言所支持。

如果你真的有兴趣看看我说的那个目录里面的文档,那么你会发现几乎你所能够想得到的底层的东西都有了,包括CIL的语法和二进制代码,CLI可执行程序文件结构,怎么写一个调试器(Debugger),分析器(Profiler),编译器(Compiler)……什么?你不相信连编译器都有了?在
E:\VS.NET 2003\SDK\v1.1\Tool Developers
Guide\Samples里面有三个编译器的文件!一个Lisp.NET,一个MyC,一个Simple Managed
C。还有更多的例子呢,诸公自便。

今天首先讲讲CIL以及CIL的VM。关于CIL的文档,在Partition III
CIL.doc中。如果不想看的话,我可以简单讲讲。CIL的VM是一个栈式机,和x86的依赖寄存器的机制很不一样,所谓的栈式机就是说指令所需要的数据都用堆栈保存。这种栈式机在真实的计算机当中并不多见,尤其在CISC的CPU里面是很难见到的,即使在RISC都很少见(也许我孤陋寡闻吧)。原因简单点讲就是栈式机的指令非常简单,为了进行一个计算需要多个指令来完成,甚至需要多次读出和写入数据。正是由于指令简单,所以由于虚拟机上面却非常的方便,虚拟机程序做起来可以容易许多。栈式机主要的指令有五大类:(数据)压栈、弹出、运算、转移、其他,其中前三个是栈式机的核心。以CIL为例:

int a = a + b + c + d + e + f + g + h + i; 80x86 ASMCIL0000004c 8B 54 24 10  mov  edx,dword ptr [esp+10h]
00000050 03 54 24 14  add  edx,dword ptr [esp+14h]
00000054 03 54 24 18  add  edx,dword ptr [esp+18h]
00000058 03 54 24 1C  add  edx,dword ptr [esp+1Ch]
0000005c 03 54 24 20  add  edx,dword ptr [esp+20h]
00000060 03 D5  add  edx,ebp
00000062 03 D6  add  edx,esi
00000064 03 D3  add  edx,ebx
00000066 03 D7  add  edx,edi
00000068 89 54 24 10  mov  dword ptr [esp+10h],edx
IL_0000: 06 ldloc.0 // a
IL_0001: 07 ldloc.1 // b
IL_0002: 08 ldloc.2 // c
IL_0003: 09 ldloc.3 // d
IL_0004: 11 04 ldloc.s e
IL_0006: 11 05 ldloc.s f
IL_0008: 11 06 ldloc.s g
IL_000A: 11 07 ldloc.s h
IL_000C: 11 08 ldloc.s i
IL_000E: 58 add // stack[top] = stack[top--] + stack[top--]    = stack[top] = h + i;
IL_000F: 58 add // stack[top] = g + h + i
IL_0010: 58 add // = f + g + h + i
IL_0011: 58 add // = e + f + g + h + i
IL_0012: 58 add // = d + e + f + g + h + i
IL_0013: 58 add // = c + d + e + f + g + h + i
IL_0014: 58 add // = b + c + d + e + f + g + h + i
IL_0015: 58 add // = a + b + c + d + e + f + g + h + i
IL_0016: 0A stloc.0 // a = ...

表一

我们先看看右边的CIL,ldloc的意思是把局部变量压到堆栈里面,后面的.0表示第零个局部变量(也就是a了);add则把栈顶的两个元素弹出来并且相加,结果压到堆栈顶上;stloc则把栈顶的内容弹出来,保存到局部变量当中。现在应该看出来了,ld就是load的缩写,st是store的缩写,loc是local。那么还有没有其他的呢?大家可以看一下下面的表:

主要操作操作数范围/条件操作数类型操作数缩写全称含义缩写全称含义缩写全称含义缩写全称含义ldload将操作数压到堆栈当中,相当于:
push axargargument参数操作数中的数值.0第零个参数 *.1第一个参数.2 第二个参数.3第三个参数.s xx(short)参数xxaaddress操作数的地址只有 .s xx,参见ldarg.sloclocal局部变量参见ldargfldfield字段(类的全局变量)参见ldargxxxx字段,eg:
ldfld xxcconst常量.i4int 4 bytesC#里面的int,其他的类型例如short需要通过conv转换.m1minus 1-1.00.11 …….8 8.s(short)后面跟一个字节以内的整型数值(有符号的)后面跟四个字节的整型数值.i8int 8 bytesC#里面的long后面跟八个字节的整型数值.r4real 4 bytesC#里面的float后面跟四个字节的浮点数值.r8real 8 bytesC#里面的double后面跟八个字节的浮点数值nullnull空值(也就是0)ststore将堆栈内容弹出到操作数中,相当于:
pop
ax参见ld **convconvert数值类型转换,仅仅用纯粹的数值类型间的转换,例如int/float等.i1int 1 bytesC#里面的sbyte.i2int 2 bytesC#里面的short.i4int 4 bytesC#里面的int.i8int 8 bytesC#里面的long.r4real 4 bytesC#里面的float.r8real 8 bytesC#里面的double.u4uint 4 bytesC#里面的uint.u8uint 8 bytesC#里面的ulongb/brbranch条件和无条件跳转,相当于:
jmp/jxx
label_jumpbr无条件跳转后面跟四个字节的偏移量(有符号).s(short)后面跟一个字节的偏移量(有符号)falsefalse值为零的时候跳转参见brtruetrue值不为零的时候跳转beqequal to相等nenot equal to不相等ununsigned or unordered无氟好的(对于整数)或者无序的(对于浮点)gtgreater than大于ltless than小于gegreater than or equal to大于等于leless than or equal to小于等于callcall调用(非虚函数)virtvirtual虚函数

*
最左边的是参数0,然后是参数1、2、3……。如果不是在静态当中,参数0相当于C#的this(VB的Me),该参数不需要代码传递,此时最左边的参数是参数1,也就是从.1开始。
**
starg只有.s形式,没有.0、.1等形式,除此之外和ldarg相同。
+ 从左到右依次合并,就可以得到一个指令,例如:ld + arg + a +
.s xx? =>? ldarga.s xx,即,读出参数xx的地址并压栈。

上面这些只是一些比较常用的、但是稍微难理解的指令,其他的可以看Partition III
CIL.doc这个文档。现在我们再回到表一的例子,是不是觉得其实CIL非常好理解呢?所以虚拟机用栈式机的形式是比较容易实现的,比起x86里面一堆的寄存器、状态/标志位以及指令对状态/标志位的影响,可简单多了!不过我们也可以看到,用80x86汇编10条指令能够完成的操作,用CIL则需要18条指令,而且这已经是经过手动优化过的CIL了,如果你用C#写,然后编译出来的很可能还要多出一些代码。如果我们数一下包含的字节数,可以看到80x86有32bytes,而CIL只有20bytes,也许会让你觉得CIL似乎更为紧凑,其实是因为这个CIL是一个优化形式,实际的情况CIL并不会比x86汇编小多少,甚至完全可能更大!栈式机另外一个问题是,每一次的操作都必须访问至少两次内存,并且这两次访问的肯定不是同一个地方:一个是某一个内存块,另外一个是堆栈。因此不可能象x86CPU那样,直接访问CPU内部存储器,甚至连访问缓存效率都会打对折(需要访问两个完全无关的地方)。而我们知道CPU内部存储器式最快的,完全没有延时,缓存次之(一级缓存延时约1到2个周期,二级缓存延时3到5个周期),最慢的就是内存了(延时约十个周期左右,甚至更长)。所以一般说来,真实的CPU是很少做成栈式机形式的。

那么我们阅读cil有些什么技巧呢?我觉得需要注意这么几点:

牢记这是一个栈式机,所有指令都和栈有关。
注意当前函数是否为静态函数
就这么多了

说了半天,哪里找什么CIL来看呢?这个就简单了,在E:\VS.NET
2003\SDK\v1.1\Bin里面有一个ildasm.exe的程序,这个程序就是“反汇编”工具,用它来打开一个.NET程序就能够看到实际的CIL了。比如我们可以打开一个System.Windows.Forms.dll(在C:\WINNT\Microsoft.NET\Framework\v1.x.xxxx里面),哇,看到了吧?举个例子?好,你可以看看我举的一个例子。

接下来应该说些什么呢?反正CIL我是说完了。且听下回分解吧……


http://tech.it168.com/knowledgebase/articles/6/6/e/66e8f606032ff05c6b93576d9ac28038.htm

上面一节里面讲到一些总体的东西,比如CLI包括什么,CIL是什么样的东西等等。这一次我准备讲一些完全不同的内容——强命名、元数据以及文件结构。

其实这次要将的东西主要就是围绕着强命名讲的,因为最近有一些需要,所以对强命名在可执行文件里面的位置以及怎么程序化的产生强命名进行了一些比较深入的研究。整个研究过程其实是比较痛苦的,虽然资料齐全,但是一方面是资料太多了需要慢慢整理,另一方面资料里面有些不正确的地方需要试验判断。这么痛苦的一个过程当然不希望有人再来一遍,所以就公开整个的研究过程,希望能够对有需要的人提供一定的帮助。

好了,什么是强命名——StrongName呢?嗯,这个问题我一开始也非常的不清楚,甚至非常不想搞清楚。不可否认,MS的文档自从7.0以来已经改善了很多了(其实我觉得VB的文档一直都比较清晰的,只是不知道为什么VC的文档一直都那么糟糕,我几乎就没有一次能够直接找到我想要的东西),但是有些文档确实依然比较糟糕,也不知道是不是MS故意的。就比如说强命名的文档,在7.0的时候关于他的介绍可以说有点比较随意,虽然我知道他和安全有关,也大概知道是某种加密方式,可是这个东西有什么实际的用处,还是到MSDN上面看别人的文章才逐渐有所了解的。
首先说一下什么是强命名。强命名就是通过某种方式,使得运行环境或者任何个人可以验证某个可执行文件是否完整并且没有被修改过,并且这种验证方式是可以信赖的。如果我们确实可以做到可信赖的验证,那可以给我们带来哪些好处呢?很明显的,第一条就是别人不能够随意修改我的程序,比如把一个许可验证的代码修改成不需要验证,这样可以在一定程度上面防止盗版。第二条是,别人不能够冒充我的程序,因为冒充的程序虽然能够通过验证,但是却不太可能和我的程序有同样的“指纹”,这个问题稍后在讨论。做到了这一点意味着某种木马程序比较容易被识别出来。还有第三条,就是能够唯一的标识你的程序集,即使别人有和你一样的文件名,甚至连命名空间或者类名称等有部分或者全部相同(也就是冲突),也不会让CLR误解为这两者是同一个东西。

乍一看来,第一条和第二条很吸引人,第三条却没有什么作用。其实第三条的作用是非常大的,而且甚至更经常被是用到。很多时候,很多人都会创建了相同的东西,比如说CoolBar。现在假设Sumtec写了一个CoolBar,Ninputer也写了一个CoolBar,然后Vincent用Sumtec的CoolBar写了一个程序A,pine ant用Ninputer的CoolBar也写了一个程序B。这个时候恰好IceSharK同时需要A和B,结果都装完之后发现,不是A用不了就是B用不了。当然,现在的COM+不太可能出现这种问题,但是想对于StrongName来说,其实是相当的脆弱的。这个问题也需要到后面再做出解释。

嗯,那么StrongName是怎么保证上面的三条呢?我们先来学习一下应用密码学的相关内容。呵呵,不要还怕,其实不难。


Bruce Schneier的这本书非常值得一看,传闻当初第一版的时候让NSA非常头痛,甚至差点被列为禁书。最后被列为“军需品”,因为这本书的出口需要通过军需品控制委员会的申请。申请的结果是,书本身可以出口,但是磁盘去不能够出口。现在我们看到的这一本书是第二版(中文,翻译的不错),增加了不少内容,也修正了一些错误,同时附赠了软盘。这个软盘我不知道现在有没有,我买的时候好像是没有的,因为被禁止出口到中国,当时带128位加密的操作系统都是被禁止在中国出售的,所以才有了后来的128位加密补丁(IE6和Win系列),还记得吗?所以这张盘应该是能够火取得了,如果你买的书里面不附带,也很正常,不过应该也可以向本书的作者或者USENET新闻组请求获取。

其实如果你看过这本书,你就会知道:哦,原来什么密码算法还不是最重要的,最重要的是协议。呵呵,协议的东西咱们暂不讨论。先说说加密算法本身的知识。

加密算法可以完成那些任务?加密/解密。也就是间谍电影里面最经常见到的镜头:我不想让别人知道我们两个之间在说什么,所以我把说的话用密语的方式发出,对方用相应的密码本解开。签名。就是说我能够证明这是我的东西,而你却不能交换。包括怎么安全的交换密钥,或者是其他信息。一般来说前者的应用比较多,而这个过程和签名正好是相反的操作方式。验证。就是证明这个东西原来就是这样的,没有被任何人修改过。签名能够达到验证的目的,但是还有一些不能用来签名的方法却可以用来验证,比如说CRC32是一种,虽然是毫无安全可言的。加密算法分那几个大类?机制是什么?对称加密算法。这类的算法加密和解密都用同一个钥匙,就和我们普通的门锁和钥匙一样。通常用于双方都处于安全并且可信的状态下进行信息交换的场合,例如两个间谍需要互通信息,却担心别人知道。这种算法有一个问题:假如其中一个人被CIA抓了,供出了密钥,同时CIA截获了所有加密过的文本并保存下来了,那么就算这个被抓的声称自己忘了发送过什么内容,那样也没有任何关系,只要用供出来的密钥对截获的文本进行解密就OK了。非对称加密算法。这类算法需要两把钥匙,一把用于加密,另一把用于解密。这类算法主要是用于弥补对称算法的不足的。假设还是前面那个情况,但是用的是非对称的算法。那么就算被抓的供出了自己的两把钥匙以及对方给他的一把用于加密的钥匙,CIA还是无法通过这些钥匙把这个人发送出去的信息还原。事实上非对称算法一般都比较慢,所以主要用这个方法交换对称加密算法的密钥(这个密钥就可以每次都不一样而且是一个随机数)。实际上非对称算法的这个特点,还经常被倒过来用,比如说签名。哈希散列算法。这类算法一般只有一把钥匙,同时加密过之后的数值通常远远短于原文,并且不可恢复。这种算法的不可恢复性注定了他不能够被用来交换信息,但是却可以简便快速的进行验证。StrongName又是什么?他是如何工作的?

好,终于进入正题了。在VS.NET 2003\SDK\v1.1\bin里面有一个用来“制作”强命名的工具sn.exe,用/?运行这个程序,就会看到很多的“签名”。这个强命名实际上就是一种签名协议,这个协议包含了两种加密应用、两种加密算法以及若干步骤。下面就开始这个强命名是如何运作的。

首先,需要产生一对RSA的密钥,一个自己使用的叫做私钥private key(简称kd),另一个是公开的叫做公钥public key(简称ke)。然后根据ke产生SHA的密钥ks,接下来计算:
hsh = SHA(CLI, ks)
上面的CLI指的是可执行文件中的CLI部分(不包括DOS头、PE头等),hsh是用SHA算法和ks计算出的文件散列值。然后就开始进行签名:
sn = RSA(hsh, kd)
也就是说,强命名sn实际上是用私钥kd,把文件的散列值加密得到的密文。除了这个sn会被记录在这个被强命名的可执行文件当中之外,还有以下两样东西被记录在同一个文件中:
token = last8bytesOf(hke), ke   其中hke= SHA(ke, ks)
前面一个是公钥的散列值的最后八个字节,后者就是公钥。需要把公钥放在文件当中很好理解:因为验证的时候需要用公钥将sn解开,然后和文件当前计算出来的hsh进行比较。那么前者有什么用呢?有两点作用:官方的说法是用来简化验证过程,因为ke不同则token必然不一样,很多时候只需要证明token不一样就能够证明两者是不一样的;但是我觉得这里面还有一层作用,就是验证ke本身。

到这里,制作强命名的过称就结束了,这个时候文件里面会包含三样东西:sn、token、ke。现在我们看看如何用这三样东西证明那个CoolBar是我Sumtec的,并且没有被修改过的:
首先,ke本身就代表着Sumtec,因为这个东西至少有1024bits长,要想产生一样的公钥几乎是不可能的。通过ke我们同样能够获得原来的ks,接着就可以计算:
hsh1 = SHA(CLI, ks)
hsh2 = RSA(sn,ke)
比较hsh1和hsh2,如果相同就可以证明没有被修改过,不同就肯定被修改过了。为什么这么说呢?现在假设Ninputer是黑客,想要把我的CoolBar里面的版权信息和注册页面去掉,于是他不得不修改SUMTEC的软件。一旦文件被修改了,那么hsh1* = SHA(CLI*, ks)就不可能等于hsh1 ,与是.NET CLR的验证就不会通过,系统直接抛出一个异常而根本就不运行这个修改过的程序。这个时候Ninputer也许就在想,可以修改一些无关紧要的数据比如一个内嵌的图片中的几个字节使得hsh1** = SHA(CLI**, ks)和hsh1 相等。问题是SHA算法本身根本就没有办法让你分析出到底修改那几个字节成什么才能够达到这个效果,如果通过暴力破解时间上的花费将会非常的大。如果要让.NET CLR允许运行这个程序,则必须让hsh1和hsh2相等,使得hsh1*保持不变似乎是不太可能了,那可不可能让hsh2*变得和hsh1*呢?可是问题是Ninputer并不知道kd,因此也就不可能用sn* = RSA(hsh*, kd)这个公式伪造签名。希望通过尝试计算hsh2* = RSA(sn*,ke)并使得hsh2*= hsh1*的方法进行暴力破解是徒劳的,sn的长度是1024bits即使你的计算机能够一秒钟计算264次hsh2* = RSA(sn*,ke),平均也需要
5.6396493121527772455748740670574e+283  天的时间去计算,也就是大约
1.5451094005898019850890065937144e+281  年……最后Ninputer不得不放弃这种荒唐的想法。

那么StrongName是不是就没有漏洞呢?不是,只是这个漏洞相对来说并不是很大。什么漏洞呢?Ninputer想了想,干脆我连kd/ke也伪造好了。不过即使是这个伪造也并不是那么容易的,至少你如果想用伪造的ke*重新签名那绝对是不可能成功的,因为重新签名本身不包括token(好像是的,这些都是我的估计,没有仔细差过资料),新的ke*即使写到里面token仍然是原来的,那么关于ke的验证就不能通过,程序照样启动不了。具体应该怎么伪造,我就不说了,因为这个可能会威胁到我们公司的产品。不过就算被你伪造成功了,那么整个程序的ketoken必然和原来的完全不一样了,如果我们想要判断这个东西是否是伪造的是一个很简单的事情。此外,被伪造了的exe和dll在StrongName上面是完全不同的,那么就必然导致不能够接受升级服务——因为升级包所包含的exe或者dll在StrongName上面和Ninputer破掉的不一样。

让我们回过头来看一下以前的“安全措施”。如果大家是从DOS时代过来的,并且稍微看过MZ文件格式,就知道有一个叫做CheckSum的字段,甚至在PE Optional里面也有这个字段(和MZ里面的互相独立)。可是说我们以前的校验措施基本上就是用的CheckSum这个思路,这个思路有什么问题呢?可是说CheckSum只能够防止自然灾害的破坏——比如硬盘上面的小磁极受到附近的音箱影响改变了一位,或者软盘光盘表面划伤某一位读不出来了。但是如果面对有预谋的修改,CheckSum就像从来就不存在一样了。因为CheckSum本身的计算不依赖于任何的密钥,并且一般来说整个运算过程是可推导的。这样Ninputer完全可以推导一下,看看修改什么无关数据保证整个CheckSum在修改之后不会改变,或者偷懒一点就去找找这个CheckSum算法自己算一遍。对于前者,你根本就不可能证明这个程序是否被修改过,除非你有这个程序的整个副本——着对于人来说办得到,但是对于程序来说就很不现实了,因为这个副本最终会被破解的人发现并且修改成和被破解的一模一样,类似的办法都是行不通的。对于后者,实际上仍然是无能为力的,因为一个程序总需要升级打补丁,这样的话每个版本的CheckSum都不一样,如何证明这个CheckSum改变是正常的补丁造成的还是Ninputer修改出来的呢?所以现在PE里面的CheckSum根本就是一个摆设,只要你在这个字段里面填写0000就能够通过操作系统验证。微软的文档里面关于PE里面的CheckSum,根本就没有提是怎么算出来的,文档说只有核心组建或者系统组建才会使用这个字段,实际上我怀疑用了和没用根本就没有任何区别,而这个算法实际上也不是什么秘密了,无论用imghelper.dll这个工具,还是自己写写都能够制造(伪造)CheckSum。

说了半天,不知道你有没有打算在产品当中使用StrongName呢?甚至有没有打算利用StrongName去防止盗版呢?敬请关注下半部分。

http://www.cnblogs.com/sumtec/articles/6869.aspx

上一节,我介绍了StrongName的意义和机制,这一节就讲一下如何使用StrongName,包括如何利用StrongName防止盗版。因为有一些图表和代码,可能会比较长,希望不会占用大家太多的时间。

首先我们得让自己的程序拥有强命名,这个怎么做呢?在工程下面有一个AssemblyInfo.cs(或者.vb),在里面你会找到AssemblyKeyFile以及AssemblyKeyName的标签(Attribute),找不倒也没有关系,可以自己添加,看:

VB:

C#:
[assembly: AssemblyKeyName("MyKey")]

如果是AssemblyKeyFile的话,MyKey的地方就填上你的密钥对文件的全路径。在这里我建议大家使用KeyName的方式,所谓KeyName方式就是把密钥对放在操作系统所保管的容器当中,上面的MyKey就是某个密钥对所在容器的名称。为什么建议使用容器而不是文件,这个在后面讲解。

现在我们知道怎么在代码里面添加点什么东西了,可是这个MyKey又如何得到呢?我们就要到[vs.net]\sdk\v1.1\bin(这个是Framework 1.1的情况),或者[vs.net]\FrameworkSDK\v1.0.xxxx\bin(这个是Framework 1.0的情况,有没有v1.0.xxxx我忘了),运行sn.exe。可以看到有很多的参数,我只对其中一部分进行解释:

首先用sn -k keyfile.snk产生密钥对文件,这里面的keyfile.snk就是文件名,文件名的后缀没有任何限制,这个文件就是AssemblyKeyFile所允许使用的文件。如果你想把这个文件直接生成到某个目录底下,那么就需要给出全路径。但是为了安全理由,我们需要做下一步工作:安装到容器当中。我们运行sn -i keyfile.snk MyKey 就可以把某个文件安装到容器当中了。在这里我建议大家:
1、保管好原始的snk文件,因为这个密钥对是无法从容器当中完全导出到一个文件当中的,其中的公开密钥可以导出来,但是私钥不可以。所以如果万一你的系统需要重装,或者需要在别的机子上进行签名操作,那么就必须要有这个原始的snk文件来安装到容器当中。
2、为了避免你忘记哪一个文件被安装到哪一个容器,请最好保持文件名与容器名完全一致(除了后缀之外)
此外,请大家注意这一点:容器名称是大小写敏感的,也就是说MyKey和mykey是两码事。

当你完成了这一步之后,程序就可以被编译了,编译出来的就是强命名程序集了。这样的程序已经是不可以修改的了,因为普通用户是没有私钥的(请参考前一节)。那么我们如何利用强命名来防止盗版呢?我们知道,只要让这个程序能够绑定到一个物理设备(的序号)上面,就能够防止盗版了。现在很多的技术都是这样的,除了没有强命名。比如说,帮定硬盘的序列号,网卡的序列号,等等。很多时候为了不让黑客知道到底保存了什么,程序都会通过加密的步骤,包括数据加密和执行代码加密。而我们现在可以有一个简单一点的,但是相对来说还是比较安全的办法:

首先我们在程序里面包含一个类:

public class AntiPirate
{
static private string sHardDiskID = “1234567890ABCDEF“; // 我们假设一共有16位
static public bool CheckIt()
   {
return (GetHardDiskID() == sHardDIskID);
   }
}

然后在程序启动的时候调用AntiPirate.CheckIt(),如果为true就正常继续运行,否则你爱怎么处理怎么处理。这里面的GetHardDiskID是一个函数,也许是API也许是你自己写的,无论如何,我建议您把这个函数用static private的方式写在AntiPirate里面。到了这一步,这个程序是没有办法运行的,因为一个硬盘的序列号基本上不可能等于1234567890ABCDEF。我们需要在程序正式添加到安装包之前,对这个123……进行修改。直接修改源代码可以吗?可以,但是每次都要重新编译一遍,显然不划算,对于大批量生产也十分不利:首先必须要有VS.NET,其次必须在制作安装的地方拥有一个源代码,最后,本身的一些序列号管理问题使得你还是需要另外写一个程序来完成制作安装的部分工作。那么怎么办呢?我的办法是,直接对编译出来的exe或者dll进行改动。但是这样又会面临一个问题:改动过之后就无法通过强命名验证了,所以我们要重新签名。

重新签名有两种办法:第一种就是通过sn这个工具进行。sn -R abc.exe keyfile.snk 或者sn -Rc abc.exe MyKey,就可以进行重新签名,可是这里有两个问题制约着我们选用这个方法。首先,sn不能够自动判断应该使用哪一个文件或者容器进行签名,需要手动来指定。其次,我们如果要写一个程序进行防盗版修改工作的自动化,将很难控制签名操作,比如说如何判断签名是否成功以及签名工作是否已经完成等等,因此我选择了用自己写的代码来完成签名步骤。

这个计划听起来有点不可思议,实际上确实是困难重重——如果没有人指导的话,但是当你看完这个贴子之后就会觉得其实听容易的。

首先我要告诉大家的是,签名这个操作是有现成的东西可以用的,这个东西甚至在MSDN里面就提到了。你在MSDN里面可以查找StrongNameSignatureGeneration,就可以看到一部分的文档了。但是,这里的文档并不完整,建议您直接看strongname.h,这个文件在sdk\v1.1\include里面。这里我着重讲两个函数,我们主要使用这两个函数:

// Hash and sign a manifest.
SNAPI StrongNameSignatureGeneration(LPCWSTR     wszFilePath,        // [in] valid path to the PE file for the assembly
                                    LPCWSTR     wszKeyContainer,    // [in] desired key container name
                                    BYTE       *pbKeyBlob,          // [in] public/private key blob (optional)
                                    ULONG       cbKeyBlob,
                                    BYTE      **ppbSignatureBlob,   // [out] signature blob
                                    ULONG      *pcbSignatureBlob);

// Verify a strong name/manifest against a public key blob.
SNAPI StrongNameSignatureVerification(LPCWSTR wszFilePath,      // [in] valid path to the PE file for the assembly
                                      DWORD   dwInFlags,        // [in] flags modifying behaviour (see below)
                                      DWORD  *pdwOutFlags);     // [out] additional output info (see below)

看到这些英文提示,大概就能够猜出分别是什么用途了吧?很明显,第一个用于签名,第二个用于验证。其中签名函数的参数有以下几点需要解释:

1、keyBlob和keyContainer是互斥的,只有其中一个会生效,keyBlob优先。而keyBlob你可以直接从snk文件读出来。
2、改函数仅仅将签名的结果返回给你(通过ppbSignatureBlob),而不会直接帮你写到文件当中去的。

对于第二点我们就比较头痛了,天晓得签名的结果应该放到文件的什么地方,也许我能够看出来,但是程序怎么计算出来是另外一码事。不用担心,这个后面会给您介绍的。好了,现在通过StrongNameSignatureGeneration获得签名了,并且假设已经写到文件当中,是不是就万事大吉什么事情也没有了呢?“不是!”很快就有人告诉我应该再用StrongNameSignatureVerification进行验证。这个没错,确实是需要进行验证,确保没有用错密钥。但是即使完成了这一步,也仅仅保证CLI部分是正确的,如果您足够仔细,您还会发现另外一个地方需要改动。如果您生成的是一个.NET CF的exe/dll,那么修改之后再用sn进行重新签名,你就会发现在CLI之前的部分还有一个位置被修改了。这个位置通过查找PE文件格式的说明,你就会发现那个是PE Optional Header - WinNT Specific Fields里面的CheckSum。

60

4

Header Size

Combined size of MS-DOS Header, PE Header, PE Optional Header and padding; shall be a multiple of the file alignment.

64

4

File Checksum

Always 0 (see Section 23.1).

68

2

SubSystem

Subsystem required to run this image.  Shall be either IMAGE_SUBSYSTEM_WINDOWS_CE_GUI (0x3) or IMAGE_SUBSYSTEM_WINDOWS_GUI (0x2).

上面这个表是节录自sdk\v1.1\Tool Developers Guide\docs\Partition II Metadata.doc的一部分,很可惜的是,无论是sdk里面的所有文档还是到Google上面搜索,你几乎都不会知道这个FileChecksum应该怎么样计算。绝大部分的文档都只会告诉你:这个位置一般情况下为0,除了一些核心文件会要求不为0,因此我们不需要关心。稍微好一点的描述会告诉你:这个计算方法微软没有公开,而我们只需要设为0就可以让系统跳过检查。可是如果原来的文件有Checksum,设为0在PC上面是可以跳过检查的,但是在PDA上面就不好说了。此外设为0是一种逃避,我觉得还是要做得尽善尽美比较好。于是我们继续Goooo,好不容易Goooo到一条告诉你,可以通过ImageHlp.dll来计算获得,可是用起来实在是太麻烦了。别急,我千辛万苦终于找到一个计算的源代码(出处已经忘了,用汇编写的),可惜有点小错误,不要紧,自己慢慢调试吧。

这个Checksum其实很简单,总体上说来就是用带进位加法计算每一个字节,然后加上文件的总长度,最后减去高32bits就得到Checksum了。这个是我的C++代码:

FileStream *fs = new FileStream(“Afile.exe“);
  BinaryWriter *bw = new BinaryWriter(fs);
unsigned int checksum;
int i, c, j, c2, cx;
int filelen;

  fs->Seek(0, SeekOrigin::Begin);
  filelen = (int) fs->Length;
  checksum = 0;
  cx = filelen>>1;
  c = cx;
for (i = 0; i < c;)
  {
   c2 = cx - i;
if (c2 > 0x10000)
   {
    c2 = 0x10000;
   }

for (j = 0; j < c2; j++)
   {
    checksum += br->ReadUInt16();
   }
   checksum = (checksum & 0xffff) + (checksum >> 16);
   i += c2;
  }
  checksum = (checksum & 0xffff) + (checksum >> 16);
  checksum += filelen;
  checksum = (checksum & 0xffff) - (checksum >> 16);

  bw->Seek(iPECheckSumPos, SeekOrigin::Begin);
  bw->Write(checksum & 0xffff);

有点凌乱,因为这是经过优化的。奇怪为什么会有FileStream?这个是托管扩展的代码,因为用C++来写全部的东西太麻烦了,我只打算把签名相关的部分做成一个托管类库,然后其它东西就让C#来完成。之所以用C++托管是因为签名(strongname)必须用C++来写,而且还有一个东西也最好用C++来完成。嗯,现在好像关于要修改些什么都有了很清楚的答案了,让我们回过头来看看还缺一些什么。

现在我们没解决的问题有两个:第一个是怎么知道某个exe/dll应该用哪一个容器还是哪一个文件来签名,第二个是签名数据应该写到文件的什么位置,这个位置应该怎么计算。

关于第一个问题,估计大家跟我一样,第一个反应就是安通过在.NET里面用Reflection加载Assembly,然后通过GetCustomAttributes的方式就可以获得啊。问题是.NET的Reflection只提供了加载不提供卸载,加载了的文件会被锁定无法写入,因此我们就可能会想自己手动从exe/dll当中直接获得。在研究过CLI之后,我们可以得知大部分的结构信息以及标签(Attribute)信息都是以MetaData的形式保存的。而这些信息在CLI里面分别存在#String、#US以及#Blob这三个流里面,这其中也包括前面提到的这个标签。如果说我们自己写代码直接从文件当中获取,理论上是完全可行的,但是这么做也未免也太麻烦了。其实大家可以参考[vs.net]\SDK\v1.1\Tool Developers Guide\Samples\metainfo里面的例子,这里用到了CLR所提供的非托管接口。这个例子包含了将所有的MetaData读出来的代码(准确点讲,他不包括非程序集的Attribute),所以您需要做的仅仅是将获得Assembly里面的Attribute这部分代码拷贝出来就行了。

说到这里,顺便给大家介绍一下MetaData。MetaData里面用处最大的地方是记录参数类型信息,比如说:string FunctionA(int a, int b); 在MetaData里面就会类似这样记录:
// Argument Token
0x10000001 string int int
// Function Token
0x80000023 0x0000001A 0x10000001
// String Table
0x0000001A “FunctionA”
上面的数据都是不真实的,很可能连格式都不正确,但是能够示意出大概的含义。比如说如果要调用FunctionA,那么在IL里面就会用call 0x80000023来表示。执行这一句的时候,系统会自动找到0x10000001得出应该需要两个int的结论,然后就会察看参数堆栈里面是否有两个int。当然,MetaData还会包括各种的字符串数据,比如说MessageBox.Show("hello");这个源代码,编译之后"hello"就通过Meta来保存了。所有的字符串都不会直接出现在IL里面,而是通过这个字符串在MetaData里面的Token(相当于唯一标识符)来表示。

关于MetaData,目前我的研究并不深入,等以后有时间全部研究清楚了再给大家写一篇文章。现在我们还是回到“防盗版”这个任务里面来。

可以说第一个问题本身虽然比较复杂,但是解决起来非常简单,因为所要做的也就是拷贝一下代码。但是第二个问题要解决就比较困难了,因为我们得自行研究PE格式以及CLI格式,这里我就给大家快速攻关一下:

首先我们要找到CLI格式当中保存强命名的地方在那里,我们可以看下面这个表格(节选):

24

8

Resources

Location of CLI resources. (SeePartition V_alink=Partition_V).

32

8

StrongNameSignature

RVA of the hash data for this PE file used by the CLI loader for binding and versioning

40

8

CodeManagerTable

Always 0 (see Section 23.1).

也就是说强命名的RVA值在CLI Header偏移32字节处,这个RVA是什么,我们可以先不管它,我们先找找CLI Header在什么位置,我们看PE Optional Header里面的Data Directories(节选):

200

8

Delay Import Descriptor

Always 0 (see Section 23.1).

208

8

CLI Header

CLI Header with directories for runtime data, (see clause 24.3.1).

216

8

Reserved

Always 0 (see Section 23.1).

哦,CLI Header的RVA值在PE Optional Header偏移208字节处(根据定义,在Data Directories里面的都是RVA),那么PE Optional Header应该在什么位置呢?PE Optional Header的前面有一个20个字节的PE File Header,而这个PE File Header应该立即跟在PE\0\0的后面,PE\0\0的位置则在DOS Header偏移0x3c这里用4个字节记录。好,现在要解决什么是RVA了。关于RVA的定义及计算方法,原文如下:

The PE format frequently uses the term RVA (Relative Virtual Address). An RVA is the address of an item once loaded into memory, with the base address of the image file subtracted from it (i.e. the offset from the base address where the file is loaded). The RVA of an item will almost always differ from its position within the file on disk. To compute the file position of an item with RVA r, search all the sections in the PE file to find the section with RVA s, length l and file position p in which the RVA lies, ie s £r < s+l. The file position of the item is then given by p+(r-s).

也就是说RVA包括两个值:位置和长度,这里我们只需要关心位置就够了。但是上面这段文字里面所提到的通过RVA得到文件偏移量的计算方法我没看懂,尤其是s应该是什么弄不清楚,我也从来没有办法计算出来。比如说:
RVA-Position of CLI Header = 0x2008,File Position of CLI Header = 0x1008(此时File Alignment = 0x1000)
或者
RVA-Position of CLI Header = 0x2008,File Position of CLI Header = 0x208(此时File Alignment = 0x0200)
对于这两组数据,我实在是搞不懂怎么从RVA-Position=>FilePosition。也许这个还跟内存块对齐大小有关系,并且没个执行文件Mapped之后前面会有一块内存用于保存环境变量,然后是文件头,这样就构成了RVA-Position里面的0x2000,与此对应的FilePostion就是一个FileAlignment的大小。但是我对于这么去计算实在是没有兴趣,因此我用了一个比较偷懒的办法去计算着一堆的东西。

根据大量的观测,可以得到这么一些经验性的结论:
如果是通过MS的各种编译器编译出来的.NET程序,那么CLI Header必然在PE Optional Header结束之后(并且文件对齐之后)再加上8个字节处,而PE Optional Header的大小可以在PE File Header里面查到,File Alignment也可以在PE Optional Header里面查到,因此CLI Header的计算位置就不难得到了。设ΔRVA为,用Data Directories里面查到的CLR Header RVA-Position值减去刚才计算出来的位置,可得:
StrongNameSignature_File_Position = StrongNameSignature_RVA_Position - ΔRVA
这样我们就可以得到签名信息应该写入的位置了,StrongNameSignatureGeneration声称得128个字节往这里写就没错了。做完这一步,我们就可以重新计算Checksum。当新的Checksum写入之后,整个“防盗版”工作就已经完成了。

应用密码学里面说,一个安全的加密算法不是那种宣称是最安全的,但却不让别人知道源代码的那些算法,而是“任何人都能够知道源代码,但是只要没有你的密钥,就不能够做你能够做的事情”这样的算法。这句话在反盗版里面也一样,StrongName恰好能够达到我们的目的——你甚至可以明确知道我就是对比你的硬盘序列号,也知道在什么地方有一个bne指令,但就是没有办法改。

那么这一种防盗版的方法有什么弱点呢?

第一个弱点是,需要确保外围可信。比如说FBI职员找一个贫民百姓做调查,问这个HardDisk ID是多少,这时候FBI职员因为FBI监管的作用是可信的,但是那么贫民百姓是不可信的,于是整个调查就不可信了。也就是说,如果你获取唯一标识的DLL本身就可以被篡改,那么就明显可以通过修改那个DLL使值输出某个固定值,就可以达到破解的目的了。这就是一旦你的主程序有StrongName,VS.NET会要求所有的依赖项都必须是StrongName的原因。问题是你用来获取硬件标识的DLL基本上不可能完全由托管代码实现,甚至根本就不能够保证本身不会被篡改(否则哪里来那么多的病毒问题)。至于怎么去解决,还得大家自己想办法了。因为我用在PDA上面,通过CoreDll获取PDA的ID,CoreDll用户只有执行权限而没有其它的任何访问权限,不可能被篡改,因此我就可以不考虑这个问题了。

第二个弱点是,需要保证这个安全策略被执行。也就是说您得保证CLR在运行你的程序之前必须进行正常的StrongName检测。在PC上面,首先可以通过更改安全策略跳过某个程序的StrongName检测,这个是MS提供的标准CLR所支持的功能,这个问题可以通过增加“权限许可标签”来限制。其次可以自己写一个CLR来执行.NET程序(比如说Mono),以次达到绕过所有安全检查的步骤。由于强命名只是一个验证步骤,而不是加/解密这样的强制要求,跳过去不作检查是完全可能的。对于PDA来说,前面的问题不存在,因为.NET CF不提供这样的能力。关于这第二个弱点,一般人还是没有办法突破的,除非是MS项要盗你的版。

第三个弱点是,这个不能说,说出来可能会威胁到我们这里现有产品的安全。但是我可以告诉大家,如果有人针对这个弱点进行攻击即使是成功了,也没有办法进行升级操作,并且完全可以被升级程序检验出来。

第四个,也是我所知道的最后一个弱点就是:如果别人得到了你的密钥对,就可以做和你一样的事情了。在这里提醒大家一个事实:堡垒最容易从内部突破,并且是无可救药的。这个弱点对于任何的安全措施来说都是存在的,所以请大家保管好密钥对,千万不能够泄漏出去。这个弱点使得我在前面建议大家,尽量使用AssemblyName的方式,也就是说要把密钥对安装到容器当中再用,而原始的密钥文件就钥小心保管。这样就算你因为需要在某个连接到公网的服务器上面提供类似XP的“在线激活”能力,也不需要担心万一黑客把这个服务器攻陷了会把密钥泄漏出来了,因为密钥在容器当中,没有人能够导出私钥。

关于利用StrongName进行防盗版设计的内容,就讲到这里了。下一次说什么?我也不晓得啊。