All Posts

逆向之RSA初探

May 24
·
views
·
likes

今天撸一个测试网NFT,批量铸造在领水的时候卡住了,需要传一段加密的数据进行验证,最开始以为还以为单纯只是base64编码

但是请求一直说无效请求而且也不知道编码的什么东西,根据分析堆栈,下断点后发现是取了当前时间的毫秒时间戳用RSA加密

RSA加密和base64编码特征相似

前置分析

知道是RSA加密后就简单了:

这里由于只是需要提交请求验证一下,所以不用解密了(懒得爬私钥了)

然后还要分析一下使用的什么 填充方式,这个很关键

请求失效

我在这里卡了一阵子(一直说请求无效),才发现是他妈的填充方式选用错了

然后继续爬堆栈,最后找到了关键的加密代码:

s.prototype.encrypt = function(e) {
                var t = function(e, t) {
                    if (t < e.length + 11)
                        return console.error("Message too long for RSA"),
                        null;
                    for (var n = [], i = e.length - 1; i >= 0 && t > 0; ) {
                        var s = e.charCodeAt(i--);
                        s < 128 ? n[--t] = s : s > 127 && s < 2048 ? (n[--t] = 63 & s | 128,
                        n[--t] = s >> 6 | 192) : (n[--t] = 63 & s | 128,
                        n[--t] = s >> 6 & 63 | 128,
                        n[--t] = s >> 12 | 224)
                    }
                    n[--t] = 0;
                    for (var o = new a.SecureRandom, l = []; t > 2; ) {
                        for (l[0] = 0; 0 == l[0]; )
                            o.nextBytes(l);
                        n[--t] = l[0]
                    }
                    return n[--t] = 2,
                    n[--t] = 0,
                    new r.BigInteger(n)
                }(e, this.n.bitLength() + 7 >> 3);
                if (null == t)
                    return null;
                var n = this.doPublic(t);
                if (null == n)
                    return null;
                var i = n.toString(16);
                return 0 == (1 & i.length) ? i : "0" + i
            }

这段代码确实是实现PKCS#1 v1.5填充的一部分,特别是在RSA加密中使用。让我们一起分析代码中的关键部分来看如何体现这个填充方案的特征:

  1. 消息长度检查

    if (t < e.length + 11)
        return console.error("Message too long for RSA"), null;

    这一行确保提供的加密块至少比消息长11个字节,符合PKCS#1 v1.5的要求,其中至少预留了3个字节用于填充标识和至少8个字节的填充数据。

  2. 逆向遍历原始消息

    for (var n = [], i = e.length - 1; i >= 0 && t > 0; ) {
        var s = e.charCodeAt(i--);
        s < 128 ? n[--t] = s : s > 127 && s < 2048 ? (n[--t] = 63 & s | 128,
        n[--t] = s >> 6 | 192) : (n[--t] = 63 & s | 128,
        n[--t] = s >> 6 & 63 | 128,
        n[--t] = s >> 12 | 224)
    }

    这部分将消息的每个字符编码并倒序存放到数组 n 中,处理了ASCII以及多字节UTF-8编码的字符。

  3. 添加终止符0

    n[--t] = 0;

    在填充和消息之间加入一个0字节,作为分隔符。

  4. 添加随机填充字节

    for (var o = new a.SecureRandom, l = []; t > 2; ) {
        for (l[0] = 0; 0 == l[0]; )
            o.nextBytes(l);
        n[--t] = l[0]
    }

    这里使用安全的随机数生成器来生成随机字节,直到数组的第二个位置,确保填充字节是非零的,这是PKCS#1 v1.5填充中的要求。

  5. 标记填充的开始

    n[--t] = 2;

    填充开始处标记为2,这是PKCS#1 v1.5规范的一部分,标志着填充开始。

通过这些步骤,可以看出该代码实现了PKCS#1 v1.5加密填充方案,其中明确使用了随机非零字节的填充和必要的格式标记。这些特征使得PKCS#1 v1.5在实际代码中的识别与理解变得明显。

然后终于解决了

代码实现

from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_v1_5
import base64
# 公钥字符串
public_key_str = """-----BEGIN PUBLIC KEY-----
公钥部分
-----END PUBLIC KEY-----"""
# 加载公钥
public_key = RSA.import_key(public_key_str)
encryptor = PKCS1_v1_5.new(public_key)
data = str(int(time.time() *1000))
encrypted_message = encryptor.encrypt(data.encode('utf-8'))
encrypted_message_base64 = base64.b64encode(encrypted_message).decode('utf-8')

RSA常用填充方案以特征

以下是更新的表格,更加注重于描述如何在代码中识别各种加密填充方案的特征:

填充方案目的机制代码中的识别特征
PKCS#1 v1.5提供基本的安全性,适用于RSA加密在数据前添加随机非零字节,前置0x00和0x02标记数据块以0x00, 0x02开头,后跟至少8个随机非零字节,通常在RSA加密函数调用中可以看到
PKCS#1 OAEP防止选择密文攻击,提供更强的安全性使用随机种子和两个哈希函数,通过哈希和异或操作生成填充数据包含随机数生成、两次哈希处理,以及数据和哈希结果的异或操作
PKCS#7确保对称密钥加密中数据块的固定大小每个填充字节的值都是填充的长度所有填充字节的值等于填充的长度,且这一值在加密前被添加到数据末尾
Zero Padding简单填充,用于数据不足时补充至所需的数据块大小在数据的末尾添加零字节直到满足块大小要求数据末尾添加的零字节,特别是在处理固定块大小的对称加密时可见
ISO/IEC 7816-4用于智能卡和特定的加密系统第一个填充字节是 0x80,随后全部填充 0x00数据填充开始处的0x80字节,后续跟随一个或多个0x00字节