RSA有多安全,有多不安全?Black Hat 2014 - The Matasano Crypto Challenges解析 - 第一部分

RSA有多安全,有多不安全?Black Hat 2014 - The Matasano Crypto Challenges解析 - 第一部分

各位知友们好!

第1篇Black Hat讲座解析专栏文章提交后,我发现点赞的人不多,倒是关注的人特别多… 想了半天没明白是个什么原因,可能是第1篇专栏文章没什么太大的意思?自己的锅,跪着也得背啊… 因此,第2篇专栏文章如约而至,而这次,我给各位知友们介绍一个广为人知,但是大多数人可能不知道里面有那么多坑的经典公钥算法:RSA。

最早关注专栏的知友们可能知道,我第1篇专栏文章实际上就是写给RSA的。但是因为各种原因,那篇专栏我决定还是删除掉。这样一来,我自己心里一直有个坑没填,就是给大家带来一个RSA相关的专栏文章。正巧,最近听译的一个Black Hat视频就有RSA相关的东西。那么干脆,我就完全根据自己对RSA的理解,为大家带来一篇独创的RSA相关专栏。

===============================

此篇专栏为Black Hat 2014视频《The Matasano Crypto Challenges - 64 Dirty Little Secrets Cryptographers Don't Want You To Know》解析的第1部分,针对RSA的相关内容。

  • 视频原始YouTube链接:youtube.com/watch?
  • 中英双语字幕链接(需注册,不过是免费的):Black Hat USA 2014安全大会演讲课程详情
  • 感谢以下人员对于此字幕制作所提供的帮助!
    • 听写:ScalersTalk 成长会听力狂练小组。感谢@scalers 团队的支持~ 他组织的ScalersTalk 成长会很不错哦!后面部分Black Hat视频,以及Winter School on Cryptography的视频,都会由ScalersTalk团队帮忙听写。毕竟我一个人听写60分钟左右的视频实在是想死… 所以,想要来一起做信息安全类视频的听译工作,可以参加他们的小组哦!顺道帮他打个广告:关于ScalersTalk成长会,请关注微信公众号ScalersTalk,网站:笔耕不辍-ScalersTalk Wonderland
    • 翻译:@刘巍然-学酥,也就是我自己了。其实最后看了一遍,还是有不少问题,大家看的时候多多包涵…
    • 校对、时间轴、压制:@吴诗湄 。其实是 @刘巍然-学酥@scalers@吴诗湄 一起校对的。这视频巨长,而且其中一个演讲者说话巨快… 时间轴也是个巨大的工作,@吴诗湄同学费心啦!
    • 视频制作由i春秋在线平台支持。

由于视频制作由i春秋独家支持,此篇专栏授权由i春秋转载。其他转载请注明出处。

=======我是空降的分割线=======

1 RSA的历史

要想聊聊RSA的安全问题,那就必须得说说RSA的历史,以及什么是RSA了。

早在5000年前,人类就开始对文字的加密和解密进行研究… 好了好了,我们直接跳到1976年。1976年是密码学历史上的旗帜。为什么呢?在这一年,Diffie和Hellman这两位密码学家在著名期刊《IEEE Transactions on Information Theory》上发表了跨时代的论文《New Directions on Cryptography》。在这篇论文中,两位密码学家提出了一个新的思想:公钥密码体制,也叫非对称密码体制。

什么叫公钥密码体制呢?我写过一篇简易讲解公钥密码体制的知乎答案:如何用通俗易懂的话来解释非对称加密? - 刘巍然-学酥的回答。简单地说,如果我们把数据加密比喻成门锁的话。以前锁门和开门都必须要有钥匙。这样的门锁差不多长这样:

结果在1976年,Diffie和Hellman(其实还有一个密码学家叫Merkle,他也作出了重要的贡献)发现,哎?我们可以设计这样的锁啊:

里面弄个弹簧,要锁门的时候不再用钥匙了,直接把按钮按下去,把门撞上不就行了嘛。这就是直观意义上的公钥体制了:加密和解密的密钥不一样。

  • 加密时使用公钥,公钥可以公开给任何人;
  • 每个公钥对应一个私钥,这个私钥用户自己留着。

加密时,任何人都可以使用公钥进行加密,解密时,用户使用对应的私钥解密。这个体制最大的好处就是,我们在加密前不需要事先约定一个相同的密钥了。现在,在我们生活中已经广泛使用这个密码学体制了。我们在访问一些网站、使用网银进行在线支付、或者进行网购等等,都在使用这种密码学体制。

Diffie和Hellman虽然提出了公钥密码体制的概念,但是很遗憾,他们没有提出一种公钥加密算法,而是提出一种密钥协商协议,史称Diffie-Hellman密钥协商协议。这个协议我们其实也在天天用,比如安全通信协议TLS就使用这个协议进行密钥协商。具体Diffie-Hellman密钥协商协议是什么呢?在本专栏的第2部分我会具体进行阐述。

好啦。1976年Diffie和Hellman的这篇论文发表后,Rivest、Shamir、Adleman这三个人看到了这篇论文。他们三个人有很好的数学功底,于是,他们经过1年的努力,构造出了人类历史上第一个公钥加密体制,这个体制的名字就用他们三个人的缩写来命名,这就是RSA加密体制了。他们也因此在2002年拿到了计算机领域的最高奖项:图灵奖。

其实这个奖本来应该属于Diffie和Hellman的,但是就差一点,这两个人就差一点点就构造出了公钥加密算法(如果你看看另一个公钥加密算法ElGamal的话,你一定会为Diffie和Hellman惋惜的…)

==============================

2 RSA算法简述

我们来看看这个RSA算法。RSA算法非常非常简单,加密就一行,解密也就一行:

据说看不清楚?我们放大点:

很简单吧?公钥是e,加密的时候就是加密的消息p求e次方模n,得到加密结果c;私钥是d,解密的时候就是加密结果求d次方模n,就能够回复出消息p了。

这个算法的安全性在于,我们有一个大合数n = pq,其中p和q都是特别大的质数(现在要求差不多150位十进制),如果只给定这个大合数n的话,人们还没找到一个比较高效的算法,在只知道n的条件下,算出n到底是哪两个质数相乘得来的。实际上,这三个人在提出RSA算法后,组建了一个叫RSA的公司,这个公司为能分解大合数的人悬赏奖金,奖金有多少呢?差不多这么多(RSA Factoring Challenge):

到现在为止,人类只能分解到232位的合数。如果你能分解300位的大合数,那10万美金就到手啦(感谢知乎密码学交流群刘一同学,这里有笔误,以前写成1万美金了…)!我们现在在网络上使用的这个合数,至少也有300多位十进制(1024位),有的甚至600多位十进制(2048位),不信?我打开百度搜索的链接,把证书截个图大家看看:

看到吗?算法使用的是RSA,公钥长度是2048位。

==============================

3 RSA有漏洞吗?Maybe... 如果用的不对的话…

相信看到上面的论述,大家肯定觉得,我靠,这世界这么安全!RSA真的有这么安全吗?从理论上分析,RSA的安全性没问题。但实际上,在用真正的代码实现RSA(或者其他密码学方案)的时候,由于程序员可能不是非常理解RSA的原理,写出的代码可能有无数个坑… 而这就是Black Hat 2014这个视频的目的。演讲者们总结了64个密码学方案实现过程中的坑(不光是RSA),在尽可能让程序员少理解密码学中数学的条件下,通过做题的形式,让程序员们明白密码学中有哪些坑,正如演讲者所说:

为了方面下面的论述,我在这里先给出一个RSA实现,这个实现是我自己写的,目的是为了演示后面的攻击代码。代码有点渣,各位凑合看哈。

/**
 * Created by Weiran Liu on 2015/11/13.
 *
 * An RSA implementation
 */
public class RSA {
    private int securityParam;
    private Random random;
    private BigInteger bigIntN;
    private BigInteger bigIntP;
    private BigInteger bigIntQ;

    private BigInteger bigIntPhiN;
    private BigInteger e;
    private BigInteger d;

    public RSA(int securityParam) {
        this.securityParam = securityParam;
        this.random = new Random();
        this.bigIntP = null;
        this.bigIntQ = null;
        while (true) {
            try {
                //In some cases, e does not have inverse in phi(N), repeat until find valid phi(N)
                bigIntP = BigInteger.probablePrime(securityParam / 2, random);
                bigIntQ = BigInteger.probablePrime(securityParam / 2, random);
                this.bigIntN = bigIntP.multiply(bigIntQ);
                this.bigIntPhiN = this.bigIntN.subtract(bigIntP).subtract(bigIntQ).add(BigInteger.ONE);
                //We choose a recommended public key e = 65537
                this.e = new BigInteger("65537");
                this.d = e.modInverse(bigIntPhiN);
                break;
            } catch (ArithmeticException e) {
                continue;
            }
        }
    }

    public void setE(BigInteger bigIntE) {
        while (true) {
            try {
                //In some cases, e does not have inverse in phi(N), repeat until find valid phi(N)
                this.bigIntP = BigInteger.probablePrime(securityParam / 2, random);
                this.bigIntQ = BigInteger.probablePrime(securityParam / 2, random);
                this.bigIntN = bigIntP.multiply(bigIntQ);
                this.bigIntPhiN = this.bigIntN.subtract(bigIntP).subtract(bigIntQ).add(BigInteger.ONE);
                //We choose a recommended public key e = 65537
                this.e = bigIntE;
                this.d = e.modInverse(bigIntPhiN);
                break;
            } catch (ArithmeticException e) {
                continue;
            }
        }
    }

    public void setD(BigInteger bigIntD) {
        while (true) {
            try {
                //In some cases, e does not have inverse in phi(N), repeat until find valid phi(N)
                this.bigIntP = BigInteger.probablePrime(securityParam / 2, random);
                this.bigIntQ = BigInteger.probablePrime(securityParam / 2, random);
                this.bigIntN = bigIntP.multiply(bigIntQ);
                this.bigIntPhiN = this.bigIntN.subtract(bigIntP).subtract(bigIntQ).add(BigInteger.ONE);
                //We choose a recommended public key e = 65537
                this.d = bigIntD;
                this.e = d.modInverse(bigIntPhiN);
                break;
            } catch (ArithmeticException e) {
                continue;
            }
        }
    }

    public void setPandQ(BigInteger bigIntP, BigInteger bigIntQ) {
        this.bigIntN = bigIntP.multiply(bigIntQ);
        this.bigIntPhiN = this.bigIntN.subtract(bigIntP).subtract(bigIntQ).add(BigInteger.ONE);
        BigInteger bigIntE = new BigInteger("65537");
        while (true) {
            try {
                //In some cases, e does not have inverse in phi(N), repeat until find valid phi(N)
                this.e = bigIntE;
                this.d = e.modInverse(bigIntPhiN);
                break;
            } catch (ArithmeticException e) {
                bigIntE = bigIntE.add(BigInteger.ONE);
                continue;
            }
        }
    }

    public BigInteger getE() {
        return this.e;
    }

    public BigInteger getD() {
        return this.d;
    }

    public BigInteger getN() {
        return this.bigIntN;
    }

    public BigInteger getP() { return this.bigIntP; }

    public BigInteger getQ() { return this.bigIntQ; }

    public byte[] encrypt(String message) {
        byte[] byteMessage = message.getBytes();
        BigInteger bigIntMessage = new BigInteger(byteMessage);
        if (bigIntMessage.compareTo(this.bigIntN) >= 0) {
            throw new RuntimeException("Message is bigger than N");
        }
        return bigIntMessage.modPow(this.e, this.bigIntN).toByteArray();
    }

    public String decrypt(byte[] ciphertext) {
        BigInteger bigIntCiphertext = new BigInteger(ciphertext);
        byte[] byteMessage = bigIntCiphertext.modPow(this.d, this.bigIntN).toByteArray();
        return new String(byteMessage);
    }
}

==============================

3.1 一个合数由两个大质数相乘得来,并不意味着这个合数一定不好分解

我们先从数学上看看一些坑。第一个问题是,如果一个合数真的是两个很大的质数相乘得来的,那么这个合数就一定不好分解吗?答案是否定的!首先,数学家们证实了下面的内容(crypto.biu.ac.il/sites/,P14):

  • (1978)如果p-1或者q-1没有大质因子的话,那么n比较好分解;
  • (1981)如果p+1或者q+1没有大质因子的话,那么n比较好分解;
  • (1982)如果p-1或者q-1中最大的质因子是p'或者q',且p'-1或者q'-1没有大质因子的话,那么n比较好分解;
  • (1984)如果p+1或者q+1中最大的质因子是p'或者q',且p'-1或者q'-1没有大质因子的话,那么n比较好分解;
  • ......

我靠,跟绕口令似的。如果知友们看懂了,那就看懂了,要是没看懂也没关系~ 我们来看个比较好理解的另一个例子,这个例子更广义的破解方法是IBM研究院的密码学家Don Coppersmith在1996年提出的(Finding a small root of a bivariate integer equation; factoring with high bits known):

  • 如果p和q这两个质数离得特别近的话,那么n比较好分解。

具体算法嘛,有点数学… Dan Boneh在他的Coursera公开课上给出了算法,链接为:Homework | Coursera;实际上这是一个编程题:给定了一个合数n,这个合数是由两个离着特别近的质数相乘而来。你要使用算法,在很快的时间内把n分解了。由于这是公开课上的习题,我这里就不好公开答案了,不过我可以告诉大家分解的速度:

  • 如果两个质数离着很近(两个质数的差小于2^{11} N^{1/4}),那么这个算法分解这个质数的时间,在我2010年购买的台式机上用Java跑,大概时间是:100ms。

有人要问了,谁没事这么实现RSA啊… 还真有,如Dan Boneh所说:

Normally, the primes that comprise an RSA modulus are generated independently of one another. But suppose a developer decides to generate the first prime p by choosing a random number R and scanning for a prime close by. The second prime q is generated by scanning for some other random prime also close to R. We show that the resulting RSA modulus N=pq can be easily factored.

总的来说,如果两个质数选的满足了一定的条件,那么很可能你自己实现的RSA就容易被破解。

==============================

3.2 在使用RSA时,永远不要使用相同的N

第二个遇到的问题也是一个RSA实现的坑。既然质数不能离得很近,那么我每次随机产生两个足够大的质数相乘就好了呗。不过,有的时候程序员为了测试方便(或者故意而为之?),在随机数产生的时候设置了个种子,结果每次产生的p和q都一样。但是,这个程序员本身挺聪明,既然p和q都一样,虽然用的n一样,那我对于每一个用户生成一个不同的公私钥对(e, d)不就好啦?每个用户的公私钥对都不一样,这样系统岂不是也能用?

这就是我们要说的第二个坑了:

  • 如果用n=pq产生了一个公私钥对(e,d),那么只知道这个公私钥对以及n的人,可以非常快速地分解n,从而可以在知道任意与n相关的其他公钥e'的条件下,求得私钥d'。



这话说的有点绕,简单地说就是:谁知道私钥d,谁就能分解整数n。所以我们不能对不同的用户使用相同的n,否则这两个用户可以分别互相算出对方的私钥。

这个算法有点复杂,需要有点数学知识,尤其是有关二次剩余的知识… 在此知友们可以参考Dan Boneh的一篇论文《Twenty years of attacks on the RSA cryptosystem》。我在这里给出Java源代码,大家可以自己跑跑试试看。

/**
 * Created by Weiran Liu on 2015/11/13.
 *
 * This attack is from the paper: Twenty Years of Attacks on the RSA Cryptosystem, by Dan Boneh.
 * RSA Attack 1: Common Modulus attack, showing that an RSA modulus should never be used by more than one entity.
 *
 * Related Theorem:
 *
 * Let N,e be an RSA public key. Given the private key d, one can efficiently factor the modulus N = pq.
 * Conversely, given the factorization of N, one can efficiently recover d.
 */

public class RSA1CommonModulusAttack {
    private RSA rsa;
    private BigInteger bigIntE;
    private BigInteger bigIntD;
    private BigInteger bigIntN;

    private BigInteger bigIntP;
    private BigInteger bigIntQ;

    public RSA1CommonModulusAttack(int securityParams){
        this.rsa = new RSA(securityParams);
        this.bigIntE = rsa.getE();
        this.bigIntD = rsa.getD();
        this.bigIntN = rsa.getN();
    }

    public RSA getRSA() {
        return this.rsa;
    }

    public void commonModulusAttack() {
        BigInteger bigInt2 = new BigInteger("2");
        BigInteger bigIntK = this.bigIntD.multiply(this.bigIntE).subtract(BigInteger.ONE);
       int t = 0;
        while (bigIntK.mod(bigInt2).equals(BigInteger.ZERO)) {
            bigIntK = bigIntK.divide(bigInt2);
            t++;
        }
        BigInteger[] bigIntToK = new BigInteger[t];
        bigIntToK[0] = bigIntK;
        for (int i=1; i<bigIntToK.length; i++) {
            bigIntToK[i] = bigIntToK[i-1].multiply(bigInt2);
        }

        for (BigInteger g = bigInt2; g.compareTo(this.bigIntN) < 0; g = g.add(BigInteger.ONE)) {
            for (int i=0; i<bigIntToK.length; i++) {
                BigInteger bigIntGToK = g.modPow(bigIntToK[i], this.bigIntN);
                if (bigIntGToK.multiply(bigIntGToK).mod(this.bigIntN).equals(BigInteger.ONE)) {
                    if (!bigIntGToK.equals(BigInteger.ONE) && !bigIntGToK.equals(this.bigIntN.subtract(BigInteger.ONE))) {
                        this.bigIntP = BigIntegerMath.euclid(bigIntGToK.subtract(BigInteger.ONE), this.bigIntN)[0];
                        this.bigIntQ = this.bigIntN.divide(this.bigIntP);
                        System.out.println("p = " + this.bigIntP);
                        System.out.println("q = " + this.bigIntQ);
                        System.out.println("pq = " + this.bigIntP.multiply(this.bigIntQ));
                        return;
                    }
                }
            }
        }
    }

    public static void main(String[] args) {
        RSA1CommonModulusAttack rsa1CommonModulusAttack = new RSA1CommonModulusAttack(1024);
        System.out.println(" N = " + rsa1CommonModulusAttack.getRSA().getN());
        System.out.println("e = " + rsa1CommonModulusAttack.getRSA().getE());
        System.out.println("d = " + rsa1CommonModulusAttack.getRSA().getD());
        rsa1CommonModulusAttack.commonModulusAttack();
    }
}

==============================

3.3 RSA中,e不能选的特别小,d也不能选的特别小

观察RSA算法,我们可以留意到,加密的时候计算的是指数哎,指数运算很慢的!怎么办呢?人们想出了一个有趣的方法:加密的时候不是p的e次方嘛。干脆,我公钥固定为一个很小的数,比如3。虽然公钥固定了,但是n都不一样,这样岂不是也安全?

在Black Hat演讲中,演讲者发现有的程序员把e设的太小了…大家看看,Salt Stack(一个远程命令执行工具)的RSA实现中有什么问题吗?

问题特别简单,就是:

然后,演讲者们还补了个刀:

那不设置成1,设置成3或者其他比较小的e呢?密码学家告诉你,也不行!我们有如下事实:

  • 如果RSA中的公钥e非常小,那么有快速算法可以在知道加密结果c的条件下,恢复出消息m。

这个攻击的一般形式需要用到数学中格论(Lattice)的LLL算法。这个LLL算法可是有点复杂… 不过在此,我为大家展示一个非常简单的攻击方法。这个攻击方法在视频中也提到了,不过没细讲,叫做广播攻击(Broadcast Attack),也叫作立方根攻击(Cube Root Attack)

设想一个场景:我现在想给3个人发相同的消息。比如,我和另外3个朋友想商量个吃饭的地方,我选的地方是“盘古七星”(好吧,我不是土豪,只是听说过,求知友们科普…)。这3个朋友都用RSA,公钥都是e=3。现在,我把“盘古七星”这四个字分别用这3个朋友的公钥加密,然后发送给他们。这样他们就都可以用自己的私钥分别解密,知道我们吃饭的地方了呗。

不过,密码学家告诉我们,要是这么用RSA的话,谁都能知道吃饭的地方,从而敲诈你:

  • 如果3个RSA的公钥都是e=3(3个RSA的n互不相同),且一个消息分别被这3个RSA加密了。那么,攻击者在得到这3个密文后,可以在不知道私钥的情况下把明文恢复出来。

我靠,这限制条件太多了吧… 没关系,密码学家们还告诉了我们更广义的结论:

  • 如果有e个RSA的公钥加密算法,他们的公钥都是e,且一个消息至少分别被这e个RSA加密了。那么,攻击者在得到这e个密文后,可以在不知道私钥的情况下把明文恢复出来。

其实我们还有更广义的结论… 现在呢,我们就用程序实现一个e=3的广播攻击。代码如下,大家有兴趣可以自己跑跑试试。

/**
 * Created by Weiran Liu on 2015/11/13.
 *
 * This attack is from the paper: Twenty Years of Attacks on the RSA Cryptosystem, by Dan Boneh.
 * RSA Attack 3: Hastad's Broadcast Attack.
 *
 * Related Theorem: Theorem 6 (Hastad). Let $N_1, ..., N_k$ be pairwise relatively prime integers, and set $N_{min} = \min_i(N_i)$.
 * Let $g_i \in \mathbb{Z}_{N_i}[x]$ be $k$ polynomials of maximum degree $d$.
 * Suppose there exists a unique $M < N_{min}$ satisfying
 * $g_i(M) = 0 \bmod N_i$ for all $i = 1, ..., k$
 * Under the assumption that $k > d$, one can efficiently find $M$ given $<N_i, g_i>^k_{i = 1}$.
 */

public class RSA3BroadcastAttack {
    private BigInteger[] cts;
    private BigInteger[] ns;

    public RSA3BroadcastAttack(BigInteger[] ct, BigInteger[] n) {
        assert(ct.length == n.length);
        this.cts = new BigInteger[ct.length];
        this.ns = new BigInteger[n.length];

        System.arraycopy(ct, 0, this.cts, 0, ct.length);
        System.arraycopy(n, 0, this.ns, 0, n.length);
    }

    private static BigInteger cbrt(BigInteger n)
    {
        BigDecimal THREE_D=BigDecimal.valueOf(3);
        int UP=BigDecimal.ROUND_HALF_UP;

        BigDecimal m=new BigDecimal(n);                          // BigDecimal copy
        BigInteger r=BigInteger.ZERO.setBit(n.bitLength()/3);    // initial estimate
        for(BigDecimal s=BigDecimal.ZERO;                        // different from r
            !r.equals(s.toBigInteger());                         // loop test: does r=s?
            s=new BigDecimal(r),r=new BigDecimal(r.shiftLeft(1)) // Convert to BigDecimal,
                    .add(m.divide(s.multiply(s),UP))                    // do the tricky division,
                    .divide(THREE_D,UP).toBigInteger());                // and convert back.
        return r;                                                // return the value
    }

    public String broadcastAttack(){
        BigInteger crtResult = BigIntegerMath.solveCRT(cts, ns)[0];
        byte[] byteMessage = this.cbrt(crtResult).toByteArray();
        return new String(byteMessage);
    }

    public static void main(String[] args) {
        RSA rsa_1 = new RSA(1024);
        rsa_1.setE(new BigInteger("3"));
        RSA rsa_2 = new RSA(1024);
        rsa_2.setE(new BigInteger("3"));
        RSA rsa_3 = new RSA(1024);
        rsa_3.setE(new BigInteger("3"));

        String message = "It is dangerous to set e = 3!";
        System.out.println("The message is: " + message);
        BigInteger[] cts = new BigInteger[] {
                new BigInteger(rsa_1.encrypt(message)),
                new BigInteger(rsa_2.encrypt(message)),
                new BigInteger(rsa_3.encrypt(message)),
        };
        BigInteger[] ns = new BigInteger[] {
                rsa_1.getN(),
                rsa_2.getN(),
                rsa_3.getN(),
        };

        System.out.println("N1 = " + ns[0]);
        System.out.println("C1 = " + cts[0]);
        System.out.println("N2 = "  +ns[1]);
        System.out.println("C1 = " + cts[1]);
        System.out.println("N3 = " + ns[2]);
        System.out.println("C1 = " + cts[2]);

        RSA3BroadcastAttack rsa3BroadcastAttack = new RSA3BroadcastAttack(cts, ns);
        String breakMessage = rsa3BroadcastAttack.broadcastAttack();
        System.out.println("The break message is: " + breakMessage);
    }
}

另一方面,如果RSA中选的私钥d特别小,也不行… 这个具体攻击算法有点复杂。

总而言之,在使用RSA的时候,e不能特别小,d也不能特别小。现在建议e至少大于65537,d至少大于n^{1/4}。

==============================

3.4 还有其他坑吗?

RSA自1977年提出以后,数学家们和密码学家们研究了里面的各种坑。除了我上面提到的一些坑以外,

  1. 可以通过故意让RSA计算错误(比如破坏电路什么的),从而获知私钥d;
  2. 如果加密的消息比较短,没有经过填充(Padding),那么也容易破解;
  3. 如果加密的消息经过填充了,但是检查填充是否正确的程序有漏洞,俺么也容易破解;

等等等等… 视频中后面讲到的Padding RSA攻击,也为Bleichenbacher攻击算法,就是上面提到的第3点。在实际使用中,确实有这个漏洞,而且这个漏洞是在开源TLS中找到的。

顺带一提,如果各位看视频的话,这个攻击演示特别像电影中的黑客…


==============================

4 我们怎么办?

RSA中有这么多的坑,而且很多都是从真正的代码中找出来的,那么作为开发人员,应该怎么正确使用RSA呢?我觉得吧,大家都学学密码学可能不太合适…作为开发人员,至少做到以下几点吧:

  1. 使用可以信任的开源库。虽然即使是如OpenSSL这样的开源库也发现过漏洞(心脏滴血),不过毕竟可信任的开源库更新还是很快的,BUG也消得快,用吧!
  2. 不要自己实现密码学方案!千万,千万不要自己实现密码学方案,用现成的。谁知道自己实现的密码学方案里面有什么坑呢?
  3. 不要使用来历不明的算法参数。我们知道,很多密码学方案都要制定个参数。不过如果这个参数来历不明的话,就不要用了。谁知道这个参数里面有没有什么坑呢?

==============================

5 下一篇专栏?

下一篇专栏中,我将简析此Black Hat视频的第2部分内容:有关Diffie-Hellman密钥协商协议中的一些坑。各位知友们还请耐心等待哦,码字,很累的…

谢谢大家!

编辑于 2015-11-20

文章被以下专栏收录

    本专栏主要发布如下内容: 1. 国际黑帽子大会(Black Hat),国际黑客大会(Def Con)上的演讲内容分析; 2. 密码学、网络安全相关的源代码分析; 3. 密码学、网络安全相关的知识介绍; 4. 业内可公开的安全架构思想; 5. 其他与信息安全相关的内容或转载文章; 欢迎投稿,欢迎指出任何错误~ 希望我们能够一起为普及、分享、研究信息安全相关理论和知识贡献自己的一份力量!