Tag Archives: 安全

安全,安全…2…强命名程序集

书接上回,说说.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提到的,一个安全系统的建立,还是要在某个安全假设成立的前提下…

安全,安全…1…公钥私钥

前一段做了很多安全相关的东西,简单总结一下。

Public Key Cryptography

首先,最基础的,也是目前应用广泛的公私钥加解密理论(公开密钥加密 public key cryptography,又称非对称加密),简单地说指的是公钥和私钥成对被创建,一段信息被公钥加密后只能用对应私钥解密,类似的,一段信息被私钥加密后只能用对应公钥解密。

为什么把这对密钥叫做公钥和私钥呢?假设某人A生成了一对公私钥,公钥是公共的可以告诉其他人的密钥,而私钥是私有的需要保密的的密钥。

这个理论有什么用呢?典型的两个用途,一是消息加密(防截获破解),二是消息签名(防篡改)。

消息加密

先说消息加密。假设A需要发送消息给B。那么A可以用B的公钥加密消息,然后将加密后的消息发送给B。B得到加密后的消息,用自己的私钥解密,就能获得这条消息。假设因为传输不安全,加密后的消息被C截获,因为C并没有B的私钥。也就不能解密这条消息。但是这样做并不能防篡改,假设B获得一条声称是发给自己的消息,B并没有办法验证这条消息有没有被篡改。防篡改的内容在下一节讲到。

先暂时跳出公私钥加解密理论的框架。说下对称加密。对称加密很好理解,一段消息用一个密钥加密过后,也要用同样的密钥解密。回过头看上面的例子,假如A要安全地发送消息给B,或者发给很多人,那假如大家都知道密钥的情况下。A只需要用这个密钥加密消息,然后发送给大家,接收者各自用这个密钥解密。这样在大家都安全地拥有这个密钥的情况下自然不会有问题。不过问题又来了,在密钥被生成出来的时候,如何安全地把这个密钥传送给A以及其他接收者呢?鸡生蛋,蛋生鸡…当然,可以跳出这个系统,怎样安全地把这个密钥传送给A以及其他人呢?走过去直接告诉他们,确保安全即可…

消息签名

再说消息签名。假设A需要对外发布一条消息“明天去徐家汇吃饭”,并且让接收者能证明这条消息是A发送的。那么A可以用自己的私钥加密消息,然后发布加密后的消息(假设是“ASDFGGHKI”)。这时任何接收者都可以用A的公钥解密消息,问题来了,怎么防止消息本身被篡改过呢?比如B得到的是没篡改过的“ASDFGGHKI”,但C得到的是被篡改过的“DSADFALKDJS”。B用A的公钥解密得到“明天去徐家汇吃饭”,C用A的公钥解密得到“明天去莲花路吃饭”。怎么判断哪个才是A原本发布的消息呢?

显然,除了解密过程,我们还需要验证过程。如果A发布的消息包括加密后的消息“ASDFGGHKI”作为签名和原本的消息“明天去徐家汇吃饭”,会怎样?假设B得到没篡改过的“ASDFGGHKI明天去徐家汇吃饭”,那么B用A的公钥解密签名得到“明天去徐家汇吃饭”,前后比较一致,说明消息没有被篡改过。假设C得到篡改过的消息“DSADFALKDJS明天去徐家汇吃饭”,C用A的公钥解密签名得到“明天去莲花路吃饭”与后面部分不合,说明消息被篡改。假设D得到篡改过的消息“ASDFGGHKI明天不吃饭”,D解密签名获得“明天去徐家汇吃饭”与后面不合,说明消息被篡改。

看起来没问题了?还是有问题的…前面两个例子都是假设篡改者或者只改签名,或者只改消息。假设篡改者两者都改,有两种情况:

情况一, 假设篡改者希望将消息改为“明天不吃饭”。那么篡改者必须得到“明天不吃饭”被A的私钥加密后的字符串,然后替换掉原来的签名。A的私钥他能拿到吗?不能,所以不可行。

情况二,假设篡改者只是简单地希望破坏掉这条消息,那么他可以把签名部分任意篡改比如改成“SDDF”,然后用A的公钥解密“SDDF”得到比如“呵呵呵”,那么他就能把消息篡改为“SDDF呵呵呵”。接收者并不能判断出这条A发布的消息经过篡改。

好吧,我们只好得出结论,发布消息本身和签名,也不能100%地防止篡改。

怎样才能解决掉上面第二种情况呢?首先,因为A的公钥是公开的,篡改者总是可以根据A的公钥去“解密”一个他给定的字符串。所以问题就出在验证过程上。我们目前的做法是用解密后的数据与消息相比较。所以篡改者可以直接用解密后的字符串替换掉消息。如果我们的验证过程是用解密后的数据与经过不可逆算法处理的消息相比较呢?换句话讲,对消息发布者而言,先用经过不可逆算法处理消息M得到N(消息摘要),再用私钥加密N得到签名,是否可行?

假设我们有算法S,能够根据输入数据I生成数据O,整个过程不可逆,且I如果不同,则O一定不同。A用算法S处理消息“明天去徐家汇吃饭”,得到消息摘要比如“SDFDFS”,然后用再用私钥加密“SDFDFS”得到比如“SDLKFJDD”作为签名。这时候A发送出去的消息是“SDLKFJDD明天去徐家汇吃饭”。这样能够防篡改吗?

假设B得到没有篡改过的消息。B先用A的公钥解密签名得到“SDFDFS”,再用S算法处理“明天去徐家汇吃饭”得到消息摘要“SDFDFS”,两者匹配,证明消息没被篡改过。类似的,假设这条消息只是消息本身或者签名被篡改,接收者也能够验证。那么,这个方案能解决掉上面第二种情况吗?

假设篡改者只是简单地希望破坏掉这条消息,那么他可以把签名部分任意篡改比如改成“SDDF”,然后用A的公钥解密“SDDF”得到比如“呵呵呵”。这时候他需要S的逆算法,通过消息摘要“呵呵呵”来生成出消息比如“这不是真的”,最后将消息篡改为“SDDF这不是真的”。假如S逆算法存在,那么他能够将消息篡改为“SDDF这不是真的”。S逆算法存在吗?不存在,所以篡改者不能这么篡改消息。所以,公私钥+生成消息摘要的不可逆算法,可以防止来自公私钥拥有者的消息被篡改

总结一下使用数字签名防止消息被篡改的过程:消息发布者使用不可逆算法计算出消息摘要,再用私钥加密消息摘要生成出数字签名,然后将消息与数字签名一起发布;消息接收者使用同样算法根据消息生成出消息摘要,再用消息发布者的公钥解密数字签名并将结果与消息摘要比较,以此验证消息是否被篡改过。

有一点值得一提的是,上面的做法,有个重要的假设:消息接收者都能拿到消息发送者真正的公钥,设想假如某些消息接收者拿到了假的公钥…那么拥有对应假私钥的篡改者就能任意篡改数据了。

消息签名+消息加密

好了,在以上两节的基础上。如果A需要发送消息“明天去徐家汇吃饭”给B,且既需要防截获破解,又需要防篡改。那么结合以上两节的做法,A需要发送这样的信息:“用A的私钥加密(S(明天去徐家汇吃饭))用B的公钥加密(明天去徐家汇吃饭)”。

杂项&参考

RSA算法:一种公开密钥加密算法

SHA算法:一种不可逆的生成报文摘要的哈希算法。

MD5算法:一种不可逆的生成报文摘要的哈希算法。

注:注意在上面防篡改的证明中,用了不严谨的三段论。比如,篡改者需要A的私钥才能生成加密后的数据,篡改者没有A的私钥,所以篡改者不能生成加密后的数据。这是充分条件当作必要条件来用,但是你懂的,写东西很累的,每次都反证必要条件很烦的…

安全,安全…木有100%的安全,假设有黑客能以某种方式破解公开密钥加密算法,或者能破解S算法,或者简单点直接拿了你的私钥,或者让你拿到了假的公钥,那神马都是浮云…有意思的是,正如上面黑体字提到的,利用消息签名防篡改,还是要建立在“消息接收者能拿到消息发送者真正的公钥”这个基础上的。那对消息接收者来说,怎样才能100%安全地拿到这个真正的没被篡改过的公钥呢?…很有意思,这个防篡改安全系统的建立,还是要基于另一个安全假设成立的前提。

To be continued…

本来这篇blog的目的是从理论一直讲到各种应用,比如.NET强命名,授权认证等等。由于我的本事就是把很简单的事情讲的很复杂,所以篇幅很长得分开了…