书接上回,说说.NET强命名(StrongName)。首先,强命名是什么?有什么用?简单地说,强命名是一种数字签名,目的是是为了防止程序集被篡改(好吧,还有很多其他作用,比如版本控制,唯一标识,等等。不过本篇blog主要目的是从安全角度来讲强命名)。
先回头怎么利用数字签名防止消息被篡改?参考前一篇blog关于使用消息签名防篡改的部分:“消息发布者使用不可逆算法(如SHA)计算出消息摘要,再用私钥加密消息摘要生成出数字签名,然后将消息与数字签名一起发布;消息接收者使用同样算法根据消息生成出消息摘要,再用消息发布者的公钥解密数字签名并将结果与消息摘要比较,以此验证消息是否被篡改过”。
怎么利用数字签名防止程序集被篡改?程序集本身,也可以看作一种包括了各种元数据,IL等等的消息。所以类似的,要防止程序集被篡改,程序集发布者需要一个公私钥对,并且需要一个生成程序集摘要的不可逆算法(.NET在此使用SHA-1算法)。
有了这些东西,我们先按自己的理解来设计.NET程序集的防篡改机制。假设我们需要发布一个程序集TestLib。首先,我们根据程序集内容生成程序集摘要,然后再用私钥加密摘要生成签名。可以将签名写到程序集文件里面去,这样我们只需要发布一个程序集文件,就能包括程序集内容本身和防篡改的签名。好了,我们可以把包含签名的程序集文件TestLib.dll发布出去了。
用户得到这个程序集,然后在自己的程序里引用这个程序集,为了简化问题,我们先只考虑用户不用程序集全名引用的情况。也就是说,在用户的project文件中(比如TestProject.csproj)中有类似这样的语句:<Reference Include="TestLib"/>。用户编译运行自己的程序,当TestLib被用到的时候,CLR会尝试去加载TestLib并作防篡改验证。首先,CLR根据TestLib的内容生成程序集摘要;然后,CLR用我们的公钥解密TestLib中的签名部分,与生成的程序集摘要做比较…?!等等,CLR怎么得到我们的(TestLib发行者)的公钥呢?
最直观地,当我们发布程序集的时候,类似签名,可以把公钥也写到程序集文件里面。这样,当CLR加载TestLib程序集的时候,就可以从程序集文件里读出公钥和签名,然后用公钥解密签名,再用得到的摘要与生成出来的摘要对比,以验证程序集是否被篡改过。
总结一下,为了防篡改,按照我们的想法,程序集里面除了有本身的的内容(IL,元数据等等),还需要有签名和公钥。翻翻MSDN关于强命名的部分,和我们想象的一样。程序集发布者首先需要(比如用sn.exe)生成自己的公私钥对,然后在build的时候,签名会被生成出来写到程序集中,相应的公钥也会被写到程序集中。这样的一个程序集就是一个强命名的程序集。
…这种做法,漏洞很大。在上一篇blog中提到过,利用消息签名防篡改有个重要的前提:“消息接收者都能拿到消息发送者真正的公钥,设想假如某些消息接收者拿到了假的公钥…那么拥有对应假私钥的篡改者就能任意篡改数据了”。.NET的做法,将公钥与需要防篡改的程序集一起发布,那么假如篡改者拿到这个程序集,改掉部分代码,然后用自己的私钥生成签名,和自己的公钥一起替换掉原来程序集文件里的签名和公钥部分。那么CLR是检测不出来的。更简单一点,篡改者改掉代码,然后直接抹去原来程序集文件里的签名和公钥即可。
有这么大的漏洞为什么.NET还要用这种将公钥写进程序集文件然后一起发布的做法?这是因为一般情况下,用户引用强命名程序集,会用全名,类似“TestLib, Version=1.2, PublicKeyToken="acc2372848326618"”这样。这里解释一下,PublicKeyToken是根据公钥(PublicKey)用某种哈希算法算出来的,目的是引用程序集的时候不用完整写出很长的公钥字段,当然,用户也可以选择不用PublicKeyToken,而直接写PublicKey=”XXXXXX”这样。好,回到正题。那么当CLR加载TestLib的时候,从程序集里读出的公钥(PublicKey)如果与用户指定的公钥不符合,那么加载失败,CLR认为用户想要的程序集要么不存在,要么被篡改过。
基于这种用户指定全名的做法,假设用户在一开始就得到了真正的程序集,里面有真正的公钥(通过某种不存在的100%安全的方式…你懂得- -),那么篡改者就很难篡改了。
再总结一下,没有100%的安全,就算是用户指定全名的做法,也有需要用户在一开始就得到真正的程序集(包括真正的公钥)的假设。这个世界上毕竟没有完美的系统…正如前一篇blog提到的,一个安全系统的建立,还是要在某个安全假设成立的前提下…