美文网首页程序员
RSA算法逆向总结

RSA算法逆向总结

作者: 秦砖 | 来源:发表于2018-12-15 15:59 被阅读5次

    遇到RSA算法后,首先进入脑海的念头是OpenSSL,这是一年多来实践经验的反射,一般情况下它都能很好的解决问题。直到我遇到了这样一个课题:

    在逆向Hello语音的授权登陆协议时,需要将其授权登陆页面的JS代码中RSA加密算法翻译成OC/C#代码。该文件中RSA加密算法的最关键代码如下:

    var rsaPubkey_m = "b5f53d3e7ab166d99b91bdee1414364e97a5569d9a4da971dcf241e9aec4ee4ee7a27b203f278be7cc695207d19b9209f0e50a3ea367100e06ad635e4ccde6f8a7179d84b7b9b7365a6a7533a9909695f79f3f531ea3c329b7ede2cd9bb9722104e95c0f234f1a72222b0210579f6582fcaa9d8fa62c431a37d88a4899ebce3d";
    var rsaPubkey_e = "10001"; 
    
    var RSAKeyPair = function(encryptionExponent, decryptionExponent, modulus) {
        var $dmath = RSAUtils;
        this.e = $dmath.biFromHex(encryptionExponent);
        this.d = $dmath.biFromHex(decryptionExponent);
        this.m = $dmath.biFromHex(modulus);
        // We can do two bytes per digit, so
        // chunkSize = 2 * (number of digits in modulus - 1).
        // Since biHighIndex returns the high index, not the number of digits, 1 has
        // already been subtracted.
        this.chunkSize = 2 * $dmath.biHighIndex(this.m);
        this.radix = 16;
        this.barrett = new that.BarrettMu(this.m);
    };
    
    RSAUtils.getKeyPair = function(encryptionExponent, decryptionExponent, modulus) {
        return new RSAKeyPair(encryptionExponent, decryptionExponent, modulus);
    };
    
    if(typeof that.twoDigit === 'undefined') {
        that.twoDigit = function(n) {
            return (n < 10 ? "0" : "") + String(n);
        };
    }
    
    // Altered by Rob Saunders (rob@robsaunders.net). New routine pads the
    // string after it has been converted to an array. This fixes an
    // incompatibility with Flash MX's ActionScript.
    RSAUtils.encryptedString = function(s) {
        if(s == null || RSAUtils.containsChinese(s)){// do not encrypt when contains chinese
            return "";
        }
        s = s.split("").reverse().join("");
        var key = new RSAUtils.getKeyPair(rsaPubkey_e, "", rsaPubkey_m);
        var a = [];
        var sl = s.length;
        var i = 0;
        while (i < sl) {
            a[i] = s.charCodeAt(i);
            i++;
        }
    
        while (a.length % key.chunkSize != 0) {
            a[i++] = 0;
        }
    
        var al = a.length;
        var result = "";
        var j, k, block;
        for (i = 0; i < al; i += key.chunkSize) {
            block = new BigInt();
            j = 0;
            for (k = i; k < i + key.chunkSize; ++j) {
                block.digits[j] = a[k++];
                block.digits[j] += a[k++] << 8;
            }
            var crypt = key.barrett.powMod(block, key.e);
            var text = key.radix == 16 ? RSAUtils.biToHex(crypt) : RSAUtils.biToString(crypt, key.radix);
            result += text + " ";
        }
        result = result.substring(0, result.length - 1);// Remove last space.
        if(result.length == 256){// when result.length not pubkey.length, it is wrong!
            return result; 
        }else if(result.length == 252){  // 个别特殊字符串(如dlknfk101) 加密左边会少4个0000,补齐
            return "0000" + result;
        }else{
            return "";
        }
        
    };
    

    从这段JS代码中很容易定位出RSA算法生成PublicKey所需要的Modules与Exponent。同时依据这段代码同一源串每次加密结果都相同的特征,猜测其使用了OpenSSL里的NoPadding。那么至此OpenSSL中RSA算法所需要的三个维度Modules/Exponent/Padding都确定了,再梳理下JS代码,发现其在源串前补位0,使源串长度达到126位后做加密处理。是时候用OpenSSL开始表演来见证奇迹了。

        const char* hex = "b5f53d3e7ab166d99b91bdee1414364e97a5569d9a4da971dcf241e9aec4ee4ee7a27b203f278be7cc695207d19b9209f0e50a3ea367100e06ad635e4ccde6f8a7179d84b7b9b7365a6a7533a9909695f79f3f531ea3c329b7ede2cd9bb9722104e95c0f234f1a72222b0210579f6582fcaa9d8fa62c431a37d88a4899ebce3d";
        int lenModules = 0;
        const unsigned char* modules = hex2bin(hex, strlen(hex), &lenModules);
    
        int lenE = 0;
        const char* srcE = "00010001";
        const unsigned char* e = hex2bin(srcE, strlen(srcE), &lenE);
    
        BIGNUM* bnn = BN_new();
        BIGNUM* bne = BN_new();
        BN_bin2bn(modules, lenModules, bnn);
        BN_bin2bn((const unsigned char*)e, lenE, bne);
    
        RSA* r = RSA_new();
        r->n = bnn;
        r->e = bne;
        r->iqmp=NULL;
        r->d=NULL;
        r->p=NULL;
        r->q=NULL;
    
        int size = RSA_size(r);
        unsigned char* buffer = (unsigned char*)malloc(size);
        bzero(buffer, size);
        unsigned char* src = (unsigned char*)malloc(size);
        bzero(src, size);
        for (int i = 0, j = size - 1; i < [source length]; i++) {
            src[j--] = [source characterAtIndex:i];
        }
    
        ret = RSA_public_encrypt(size, src, buffer, r, RSA_NO_PADDING);
        if (ret > 0) {
            NSString* value = [[NSString alloc] initWithUTF8String:(const char*) buffer];
            NSLog(@"value = %@", value);
        }
    

    然而奇迹并没有如想象般的发生,OC代码相同的输入得到了与JS不一样的输出,再尝试其他类型的Padding方式,依然无效。通过Google找到这段RSA算法JS实现的源头,原来它是一个老外的算法练手之作,后来慢慢的流传开来,而它第一个版本与OpenSSL有很多兼容性的问题,当然这些问题在后来的版本更新中得到了修复,但这并不能解决现有的问题。最终我决定去啃RSA算法的数学原理,它最终帮助我攻克了这个难题。

    RSA加解密的数学原理

    相信很多同学到这里应该就没有读下去的欲望了。为此我不会在罗列网上那一堆又一堆的方程式,只提及与加解密最密切相关的两个方程式(最终还是方程式。。。):

    • 加密
      c = (p ^ e) mod n;
    • 解密
      p = (c ^ d) mod n;

    方程中p表示源串,c表示加密串,n就是文件上面说的Modules,e则为Exponent,(n, e)表示PublicKey,(n, d)表示PrivateKey。原来RSA加解密的具体计算工作如此简单,当然这是将n/e/d三个参数生成算法过滤掉的结果,但对于实际应用这两方程式已经足够了。

    要明白的是,这两个方程式中的所有的数都不是编程语言里普通的数字对象了,它们是由输入字符组成的大数对象。OpenSSL中的BigNumber类、BouncyCastle中的BigInteger类、C#中的BigInteger类等等都是用来表示大数这一概念的。

    破题

    理解了RSA加解密算法的计算过程,而OpenSSL提供的RSA加密api无法适用于这个问题,那么尝试下抛弃OpenSSL原有的那套加解api,自己编写一套RSA的加密算法吧,当然大数库依然使用OpenSSL提供的那套。你总不至于无聊到自己造一套大数的轮子吧。

        const char* hex = "b5f53d3e7ab166d99b91bdee1414364e97a5569d9a4da971dcf241e9aec4ee4ee7a27b203f278be7cc695207d19b9209f0e50a3ea367100e06ad635e4ccde6f8a7179d84b7b9b7365a6a7533a9909695f79f3f531ea3c329b7ede2cd9bb9722104e95c0f234f1a72222b0210579f6582fcaa9d8fa62c431a37d88a4899ebce3d";
        int lenModules = 0;
        const unsigned char* modules = hex2bin(hex, strlen(hex), &lenModules);
    
        int lenE = 0;
        const char* srcE = "00010001";
        const unsigned char* e = hex2bin(srcE, strlen(srcE), &lenE);
    
        BIGNUM* bnn = BN_new();
        BIGNUM* bne = BN_new();
        BN_bin2bn(modules, lenModules, bnn);
        BN_bin2bn((const unsigned char*)e, lenE, bne);
    
        int size = 126;
        unsigned char* buffer = (unsigned char*)malloc(size);
        bzero(buffer, size);
        for (int i = 0, j = size - 1; i < [source length]; i++) {
            buffer[j--] = [source characterAtIndex:i];
        }
        BIGNUM* message = BN_new();
        BN_bin2bn(buffer, size, message);
        
        BN_CTX* ctx = BN_CTX_new();
        BIGNUM* c = BN_new();
        //RSA加密的数学算法函数,仅此一行代码足矣,是不是很出乎意料。。。。
        int succ = BN_mod_exp(c, message, bne, bnn, ctx);
        if (succ == 1) {
            char* encryptValue = BN_bn2hex(c);
            if (encryptValue != NULL) {
                printf("encryptValue = %s", encryptValue);
                OPENSSL_free(encryptValue);
            }
        }
    

    程序跑起来,奇迹出现了,相同的输入等到了同JS代码一致的输出。至此才明白原来这段JS中的RSA算法只是实现了自己的一套填充规则,与主流的填充规则有些许不同。

    总结

    RSA加解密是一种数学运算,而OpenSSL/BouncyCastle算法库提供的RSA api只是按照市面上通用的规则实现了这套数学运算,比如主流的PKCS1填充规则。如果遇上不按常理出牌,非得自己搞一套填充规则的主,使用合适的大数库按照它的这套规则实现RSA算法就是了。

    这段JS代码的C#版本也已完成并验证OK,但其间也碰到了因大小端字节序导致的问题,这里就不在说明了。

    参考资料

    相关文章

      网友评论

        本文标题:RSA算法逆向总结

        本文链接:https://www.haomeiwen.com/subject/ytnohqtx.html