pem 格式的文件通常用于数字证书认证机构(Certificate Authorities,CA),其文件形式主要为 base64 编码的文件,头尾有类似于——-BEGIN PUBLIC KEY——-和——-END PUBLIC KEY——-的头尾标记

本文的目的就是如何从 .pem 格式的证书中,获取公钥、私钥信息。在 RSA 加密中,实际上公钥和私钥都是由一系列数字组成的,这些数字在.pem格式的证书中以特定的格式存储。通过解析这些数字,我们可以获取到公钥和私钥的相关信息,从而进行加密和解密操作。

PEM文件生成与使用 🥳

在python中,可以通过 from Crypto.PublicKey import RSA 生成想要的公私钥文件。

生成公钥文件的代码如下:

from Crypto.PublicKey import RSA
from Crypto.Util.number import *
p,q = getPrime(512),getPrime(512)
n = p * q
e = 0x10001
pub = RSA.construct((n,e))
with open('out.pem','wb') as f:
    f.write(pub.exportKey('PEM'))
with open('out.pem','rb') as f:
    print(f.read().decode())
'''
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCuRPouMRTcLWPBEUlhjCrZ6MNQ
rSy29BrHjH4+lGMykB23azPtT9fk7IsEFXoodm6tsPL8kheJ6cP+0WPldlQOw/K3
c9LUGzeCCAhNJuBjUoeW32ruE2HS7RoIF6vkP36zLs167ZZMK7Fg0cqW6VNXoJHT
zaKdqysBe+3W1VHl6QIDAQAB
-----END PUBLIC KEY-----
'''

生成私钥文件的代码如下:

from Crypto.PublicKey import RSA
from Crypto.Util.number import *
p,q = getPrime(512),getPrime(512)
n = p * q
e = 0x10001
d = inverse(e,(p - 1) * (q - 1))
pub = RSA.construct((n,e,d,p,q))
with open('out.pem','wb') as f:
    f.write(pub.exportKey('PEM'))
with open('out.pem','rb') as f:
    print(f.read().decode())
'''
-----BEGIN RSA PRIVATE KEY-----
MIICXAIBAAKBgQCnHIvLP0IERPVRaED+71dlCRBcm3be4jlHgqVqIIXyIvrzc8ZC
IIbIDqlBybNgq32i6PVlzBCsWiiTfBYS6J24qCjYVTywKk+yieNshieNNohmvQRF
bZOZITJiP99URkhtGWo3trQfoAZEQ7NfMoS1N3PDvPet1lMfFK81AyWt1wIDAQAB
AoGAIq74DK0KZJxzVfwPUVoXh27EKJRTrZrCTKc+8bHiWwkLkK+8vEjH8Imqc28L
fcrZ/o/fLsuVwk/MECA27KG+6hiRPJDWZmFgCInCABuhd+xitSBciMSGrO5ITjoq
YdgMsR5xJTI8vhXIJ1iCkkCz6fD6Di7s+3n/+Ti3iuK87jECQQDOY/pLyH0ppOk0
49pieQRshckQMXqsKhahEDZmWMu70JqDLF0U3aIfup1R87uogB8hJj8+K5RQavG4
4l0W5ghTAkEAz0eTNEWTe2iJ/Dq+8JKTN/MA/a8MiG7wvAkXqjx+B+Eow77IdoN8
D5ehD0x6ou73yaorTddidDAplNmvyq0D7QJAJbcPXhndBWclVozss2H59Prdqx/f
kuZ+DCCyUDGZyVBta9sHh3CY18N6TCeF+1yuU5hxpiLAj5F7apWy/SQ8EQJBAMtv
AxGda6cGLc8o1PeF1AlobUONxy4sPAdAoUJKRqNzH7AmEdcHKv6eoctDE2XQRc9e
PUwTpSRFlLnrgLXZYu0CQBKKT+oY4ssnwKDQqGPsh9MtPyilySL9sWJilk1cSXmQ
Xm7gjDq5S/x4k1gJyQFXbUnxTfsbLWs6BljHHlKSRes=
-----END RSA PRIVATE KEY-----
'''

如果是生成一组随机的公私钥文件,可以直接直接使用 RSA.generate(bits),其中的 bits 参数为生成的公钥文件中n的二进制位数,且 bits>=1024,默认为 1024。

from Crypto.PublicKey import RSA
from Crypto.Util.number import *
key = RSA.generate(1024)
print(key.publickey)
with open('out.pem','wb') as f:
    f.write(key.exportKey('PEM'))
with open('out.pem','rb') as f:
    print(f.read().decode())
'''
<bound method RsaKey.public_key of RsaKey(n=99929675131146107818047790971049703803161677332600984800831117021637597471437271599719384035180982326470249457001083005224626356572009901255841391966616564984242588913592789383087326511196585443834216472384770224810255153922375944881215835774119177009570247468911673932973239275741131654695253944455708641727, e=65537, d=27501740780061900509028777360107084282858928440567277451824014559115713670042546183199393779857817534085793587304881725189343016621314014739780326627796196251470626677550096120689732366473717193895036548926100990548453802714993577259273117223098888966957225092204230317866243049867167880710514827901086931493, p=9959145719455053414348729655771312861460726396890001110536788452571591154277537325754067618769136991544158484849077735057792084414015625760536769751415507, q=10033960536990122517580769502852456118641921945920255759007869111269242067226468943041901092934902353013133963062909073461567977701122665256982872188799461, u=2111771661302217183343493746406805348965531581492223651147849337466575464897027597797323383307376291792334461891498947953036261381547003770335520720819326)>
-----BEGIN RSA PRIVATE KEY-----
MIICXQIBAAKBgQCOTfkFWAc5UsCT07Y4bU9jKfpT5pt+ScHFIk/39jsE8yDHSFAo
pcQMlJ6GKRYfU7F/3xbY2udbvXBj9jJVLBmh3ORbZT06xwYDeqhvE2kJB+QfGk+J
POqzjCkFiDorxMUzWEq4us8nXmkv6WsrJMGSQZ0SQ7c29N9M0/8K622JvwIDAQAB
AoGAJyntaVuXLV8JcgW3piLrUNLKOpICZEi/Q9ZUJN2G069n64CK0wz//ihe0nR3
SqrZdGQ84PSp7LUfu9sTch5fdSQLH/OAQtVf/rwWuwulm8wS0njrU9xxQQWrwZoe
L9W7DlBWf5+PbmYdgyoLIwL3+wskvxiWswlvVcSR7RkXGiUCQQC+J0pN+1W2U3qe
a8rhAwX7XMSXoblFuqfBfH/duLZvseEEjtuTcj0ISlJgts2aZT1J8yHnllUoiqAu
WQPUK/rTAkEAv5T6OwPHj+4J/4WzQgJTN89Xj9ihR45wpr8Rr6WPXUCgXTe82UFz
ny68nIzALvKrLTf58yu8/E5wy2+SejBJ5QJASNUnwsK3y8QhvTgwVwsfaW3Y5vNM
0YZy5stW9offaNzLAUHunIUvF1PQRbb+/Vo1pXN40wljyMmAHQB/VO8bfQJBAIGQ
nVKAEdyzHavjngHMVL9vyEYOObSNDn6Wxb1GeJiWdl3UrjE35JwJHaG6Rtb5Yu7n
5nCgaeUwn3PV9vgP5EkCQQCWIiD5b8Qb5Mma8/iDNhchmRkosUug/iCBeqNh3nJc
txZnUy1fjc0toXxJY3ZBzTl3TNsk8p8x4H9y/NXoDtvu
-----END RSA PRIVATE KEY-----
'''

在 python 中,利用公私钥文件,通过 PKCS1_OAEP 算法填充可以实现加解密,例如:

from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_OAEP
from Crypto.Util.number import *
flag = b'This_is_a_message_qwer'
​
key = RSA.generate(1024)
pub = key.publickey().exportKey()
public_key = RSA.importKey(pub)
pk = PKCS1_OAEP.new(public_key)
enc = pk.encrypt(flag)
print(enc)
​
priv = key.exportKey()
priv_key = RSA.importKey(priv)
sk = PKCS1_OAEP.new(priv_key)
msg = sk.decrypt(enc)
print(msg)
# b'\xd1/Ou\xae\xba]z\xc0\t\xc6\xd7\xc4f\x13\x96\x9a\xeb5\xdb8\x92\xc7\x19\x12y\x9c\x18\xf7A\x9d\xe9\n&=<\x16\x07\xefz\xad2-\x983\xec\x932\xcb\xf0\x87~\xdf\xc1\xd2\x9f\xd7@\\H\x1f\x87#\xf3\x84\xa0\xfc\xd9\xcfV$>\xd7Of\xe6G\x06\xcb\x91\xa1\xcc\x0c\xad\xc2\x9a\xad\xe46\x91x\xad\xa51\xbb\xfb\xc1E\x93~e%\xd1~\xf8l\x19n\x88\xff\xac^\xca\x8fs*}\xb9c0\xc0N\xf2\xfa\xa4\xd8\x18g'
# b'This_is_a_message_qwer'

PEM文件解析 🤠

在文件进行 base64 解码后,可以看到 pem 文件的结构(专门用于 RSA 私钥)如下:

RSAPrivateKey ::= SEQUENCE {
    version           Version,     0表示两个素数的RSA  1表示多素数的RSA
    modulus           INTEGER,  -- n
    publicExponent    INTEGER,  -- e
    privateExponent   INTEGER,  -- d
    prime1            INTEGER,  -- p
    prime2            INTEGER,  -- q
    exponent1         INTEGER,  -- d mod (p-1) dp
    exponent2         INTEGER,  -- d mod (q-1) dq
    coefficient       INTEGER,  -- (inverse of q) mod p
    otherPrimeInfos   OtherPrimeInfos OPTIONAL
}

十六进制格式如下:
表示头 30
表示长度 82 xx xx
版本信息 02 01 00
n 02 xx xx
e 02 xx
d 02 xx xx
p 02 xx
q 02 xx
dp 02 xx
dq 02 xx
inv 02 xx
如果数据长度超过 128 字节,则长度部分会多出两个字节表示实际长度

对于完整的 pem 格式文件,我们可以使用 openssl 工具进行解析,例如:

读公钥 openssl rsa -pubin -text -modulus -in 1.pem
读私钥 openssl rsa -in 1.pem -text

当然,对于残缺的 pem 格式文件,我们也可以 base64 解码后,通过分析上述结构,手动解析出其中的数字信息,这也是我们重点讲解的内容。

残缺私钥解析 👻

我们从上面的结构知道,我们要的数据都是以 02 开头的整数类型,后面跟着长度和数据内容。对于残缺的 pem 格式文件,我们可以通过分析这些整数类型的数据,来获取我们需要的信息。

残缺私钥解析例题

残缺私钥解析例题下载

Ciphertext = 0x6d482cbbc60b146ce064754910f5ce845a6eedeff2a20589b97c342677219c324debe4fc7324106bbeccc633ec819383f019856e700f027a39987500873a5700491f2828df4d5c01d179febac4f7589ed6846701e4d78daea6265df0fe1853e43984a3760b5257144842f9f4d316c3d314dfedb2ded08b90b957bf040258400a

-----BEGIN RSA PRIVATE KEY-----
MIICewIBAAKBgQCdA0+pqynKvc3/BH5ojnUABMdWy9Lzi9TwSAOgiJ6ky//QBrWG
CNAn98CYNcoKuA2yOtHKIjAUrPh+LdgjmW+/pAWPJyrnoQw5SqD+FvqNTjG7AkvA
+TUAyMflL9qodrWBEbl/AmN01Qbivo36+U1mFDB+6LENk/3BwWAHVj0DvQIhAL7t
Vc3/3jEFe+pAWKNoTV++2B8D1T+ii7ZYsy3kU1yNAoGAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAkCQQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAkEAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAwJAJrKFhI5pl/oBN2BZqLTf+NAcGqTFrzmbi7RVFAN43kAYAu11urAy
LTncfJABpRhtGFdsL31jiswhiYRp9yjT+wJBAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMCQQAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAA
-----END RSA PRIVATE KEY-----

在这个例题中,我们可以看到,私钥文件中的一些数据残缺了,不过我们可以通过解码后的 02 头直接解析,因为数据的顺序都是严格按照上面的结构来的。

解析代码示例:

from base64 import b64decode
import binascii

s = """
-----BEGIN RSA PRIVATE KEY-----
MIICewIBAAKBgQCdA0+pqynKvc3/BH5ojnUABMdWy9Lzi9TwSAOgiJ6ky//QBrWG
CNAn98CYNcoKuA2yOtHKIjAUrPh+LdgjmW+/pAWPJyrnoQw5SqD+FvqNTjG7AkvA
+TUAyMflL9qodrWBEbl/AmN01Qbivo36+U1mFDB+6LENk/3BwWAHVj0DvQIhAL7t
Vc3/3jEFe+pAWKNoTV++2B8D1T+ii7ZYsy3kU1yNAoGAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAkCQQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAkEAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAwJAJrKFhI5pl/oBN2BZqLTf+NAcGqTFrzmbi7RVFAN43kAYAu11urAy
LTncfJABpRhtGFdsL31jiswhiYRp9yjT+wJBAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMCQQAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAA
-----END RSA PRIVATE KEY-----
"""

s = b64decode(s)
print(binascii.hexlify(s))

"""手动分行解析,注意02后面的长度部分,这是我们确定数据长度的方式,如果长度超过128字节,则会多出两个字节表示实际长度
3082027b
020100
028181 #n
009d034fa9ab29cabdcdff047e688e751704c756cbd2f38bd4f049a3a0889ea4cbffd006b58608d6a7f7c09835ca0abb1db23ad1ca223c54acf87e2dd823996fbfa5a58f272ae7a10c394aa0fe16fa8d4e31bb024bf1f93517c8c7e52fdaa876b58111b97fc66374d506e2be8dfaf94d6614307ee8b10d93fdc1c165c7563d03bd
0221   #e
00beed55cdffde31057bea5a58a3684d5fbed81f03d53fa28bb658b32de4535c8d
028180 #d
0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000009
0241   #p
0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
0241   #q
0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003
0240   #dp
26b285848e6997fa01376059a8b4dff8d69c1aa4c5af399b8bb45515a378de45d85eed75bab5f22d39dc7c95c1a5186d18576c2f7d638acc21898469f728d3fb
0241   #dq
0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003
0241   #inv
0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
"""

通过上述代码,我们可以成功解析出私钥中的各个参数,我们发现就是一个 $d_p$ 泄露的情况,利用这个参数,我们可以通过以下代码计算出 $p$ 和 $q$ 的值:

from Crypto.Util.number import long_to_bytes, bytes_to_long, inverse, GCD
from secret import m

n_hex = "0x9d034fa9ab29cabdcdff047e688e751704c756cbd2f38bd4f049a3a0889ea4cbffd006b58608d6a7f7c09835ca0abb1db23ad1ca223c54acf87e2dd823996fbfa5a58f272ae7a10c394aa0fe16fa8d4e31bb024bf1f93517c8c7e52fdaa876b58111b97fc66374d506e2be8dfaf94d6614307ee8b10d93fdc1c165c7563d03bd"
n = int(n_hex,16)

e_hex = "0xbeed55cdffde31057bea5a58a3684d5fbed81f03d53fa28bb658b32de4535c8d"
e = int(e_hex,16)

c_hex = "0x6d482cbbc60b146ce064754910f5ce845a6eedeff2a20589b97c342677219c324debe4fc7324106bbeccc633ec819383f019856e700f027a39987500873a5700491f2828df4d5c01d179febac4f7589ed6846701e4d78daea6265df0fe1853e43984a3760b5257144842f9f4d316c3d314dfedb2ded08b90b957bf040258400a"
c = int(c_hex,16)

dp_hex = "0x26b285848e6997fa01376059a8b4dff8d69c1aa4c5af399b8bb45515a378de45d85eed75bab5f22d39dc7c95c1a5186d18576c2f7d638acc21898469f728d3fb"
dp = int(dp_hex,16)

p = GCD((pow(2,e*dp,n) -2) ,n)
q = n // p
if n == p * q and p > 1 and q > 1:
    print("成功分解n:")
    print("p =", p)
    print("q =", q)
    phi = (p - 1) * (q - 1)
    d = inverse(e, phi)
    m = pow(c, d, n)
    print(long_to_bytes(m))
# b'flag{u_know_the_pem_structure}'

上一章:RSA加解密专题 - 私钥 d 相关攻击 👈
下一章:RSA加解密专题 - Coppersmith 相关攻击一 👈
回到开始:关于我 👈