eMudhra CA 利用
准备工具: OpenSSL、Node
原理
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] 中随机选取的整数 |
公钥由私钥算出:
1 | |
G 乘上 n 得到单位元 O,乘上 n+1 回到 G,序列开始循环:
1 | |
所以私钥 d 只需从 [1, n-1] 中选取。n 是曲线参数里的固定常数,P-384 的 n 是一个 384 位的素数:
1 | |
而这批证书的 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 证书里的公钥逐字节相同,下面是该证书的完整公钥点:
1 | |
公钥开头的 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:
1 | |
完整脚本 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:
1 | |
得到的 EM-OV-TLS-CA-G3C.key 就是私钥:
1 | |
1 | |
1 | |
48 字节的私钥里 47 个是 00,最后一个是 04,有效位仅 1 字节
用这个私钥重新签官方给的 message.txt,再用证书里的公钥验证。公钥从 EM-OV-TLS-CA-G3C-emSign.pem 里取:
1 | |
签名通过说明这把还原出的私钥与证书里的公钥是同一对。
官方例子
官方 proof.zip 给的是持有私钥的证明:用同一个 pub.pem 去验证官方附带的签名。
1 | |
两个签名的字节不同(ECDSA 每次签名随机取 k),但都能被同一把公钥验证。上一步证明还原出的私钥对应这把公钥,这一步证明官方签名也对应这把公钥,两者合起来说明还原出的私钥与官方的私钥是同一把。
8 组证书与签名都能这样验证,把文件名换成任意一组即可。私钥可被算出这一点,由上面的枚举过程证明。
Reqable
Reqable 默认给每台设备生成一个自己的根证书,抓 HTTPS 时用它签叶证书,所以正常流程要把这个根装进目标设备。这里把这个根换成 eMudhra 的中级 CA:Reqable 改用该 CA 的私钥签叶证书,链条从叶证书走到中级 CA,再走到客户端本来信任的根,目标设备不需要安装任何根证书。
Reqable 在 3.3.5 引入中间证书支持 github issue
中间证书 p12 的生成
制作 p12,口令设为 123。两个根各签了一张证书,公钥相同、签发者不同,按客户端信任的根来选:
走 AAA 根的:
1 | |
走 emSign 根的:
1 | |
导入方式:证书 菜单 → 根证书管理 → 导入根证书(.p12)
使用以下命令进行测试,安卓设备无需安装任何证书:
1 | |

其他
文件名结构
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 组件就可查看
1 | |
PEM(Privacy-Enhanced Mail)
PEM 把 DER 做 Base64,每 64 个字符换一行,首尾各加一行标记:
1 | |
首尾两行的标记叫 label,说明里面装的是什么:
| label | 内容 |
|---|---|
CERTIFICATE |
X.509 证书 |
PRIVATE KEY |
PKCS #8 私钥 |
EC PRIVATE KEY |
传统 EC 私钥,没有 PKCS #8 外层 |
RSA PRIVATE KEY |
传统 RSA 私钥 |
PUBLIC KEY |
公钥 |
Base64 是可逆的文本编码,转回二进制就是 DER:
1 | |
一个文件里首尾相接放多张证书,就是证书链或 CA bundle,验证时会按顺序逐张尝试:
1 | |
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 | 爆破脚本 |
参考
一款可分享密码的解压缩 app 分析学习
解压专家 (apk)
依据 ELF 结构
分析一款可以共享密码的解压缩 app,其中用到了 native 的知识点
本文将介绍两种 native 解密方法
java 层

1 | |
getVersionName
1 | |

User-Client-Access
1 | |
其中 getQPWAT 调用 libZMCoreUnZip.so 的 executeGBS native 方法
1 | |
native 层
静态分析+vibe
发现能识别 elf,但是 export 符号很奇怪,不是可见字符,且指示条不正常

发现 JNI_OnLoad export 不存在,所以看 init_array

发现不是很好找,这里根据 elf 结构我们可以直接根据 Elf64_Dyn 类型引用定位到 ELF Dynamic Information 然后找到 DT_INIT_ARRAY


随后发现 init_array 均为空值 0,说明需要重定向。
查看 DT_RELA 元素为 Elf64_Rela
1 | |
注意我们需要 r_info == 0x403 的项目,且 r_offset 需要位于 [DT_INIT_ARRAY, DT_INIT_ARRAY + DT_INIT_ARRAYSZ) 之中,即 [0x55DB00, 0x55DB00+0x20)

所以 init_array 为
1 | |
可以通过编写以下脚本来应用重定向
1 | |
借助 ai 分析,能够找到关键全局变量并创建结构体
1 | |
1 | |
三个 init 函数分别有以下功能
init1 负责环境探测
| 顺序 | 动作 | 结果 |
|---|---|---|
| 1 | dlopen("libc.so") |
获得 libc 句柄 |
| 2 | dlsym("__system_property_get") |
获得系统属性读取函数 |
| 3 | 读取 ro.build.version.sdk |
记录 Android SDK 版本 |
| 4 | 打开 /proc/self/maps |
枚举当前进程映射 |
| 5 | 按 "%lx-%lx %4s %*x %*x:%*x %*d%n" 解析映射行 |
获得区间和页权限 |
| 6 | 结合 SDK、模块位置和权限写入全局状态 | 给 init3 与 load2_load_packed_segments 使用 |
init2 负责恢复 dynsym、dynstr
1 | |
注意到:
| item | value |
|---|---|
DT_SYMTAB |
0x55FD38 |
DT_STRTAB |
0x5BA5E8 |
DT_STRSZ |
0x817E0 |
DT_HASH.nchain |
0x2F28 |
loader_meta.sym_index_begin |
0x3A |
loader_meta.sym_index_end |
0x2F28 |
loader_meta.dynsym_seed3 |
0x815C0 |
dynstr_dynsym_decode_Impl 的解密过程
dynstr_dynsym_decode_Impl @ 0x54D6B8 做两件事:先解 .dynstr,再修 .dynsym
1 | |
给出修复脚本
1 | |
init3 负责还原代码
1 | |
load2_load_packed_segments
函数概括为六个阶段:
- 从 ELF header 后定位主 packed payload,并定位 payload 尾部的 trailer
- 根据
flags获取可选字符串或构造外部 key - 第一次调用
core_decode,恢复 metadata 后方的 ELF 链接信息块 - 以链接信息块为基址回填 Program Header、
.dynsym、.dynamic和 relocation 地址 - 第二次调用
core_decode,把主 packed payload 还原为带 8 字节长度头的 zlib 包 - 分配独立输出缓冲,调用
zlib_uncompress2_wrapper,再按PT_LOAD解释解压结果
flowchart TD
A["从 image_base 定位 packed_src"]
B["准备 packed_dst 与 ext_key"]
C["解码 `meta+0x48` 数据块"]
D{"flags"}
E["bit5: fixed XOR"]
F["bit1: core_decode"]
G["读取 unpacked_size / compressed_size"]
H["解压为最终 ELF 内存镜像"]
I["回填 elf_parse_ctx_t"]
A --> B --> C --> D
D --> E --> G
D --> F --> G
D -->|无解码标志| G
G --> H --> I
RuntimeMalloc、RuntimeFree、RuntimeMemcpy 等名称对应 API 表里的 libc 函数;AcquireFlag4String/AcquireFlag8String/AcquireFlag10String 只是按控制它们的 flag 命名,具体业务语义仍未确认。
1 | |
该样本将会执行两次 core_decode 而不执行 xor_decode_fixed_key_blocks
| 调用点 | 输入范围 | 解密内容 | 解密后的用途 |
|---|---|---|---|
0x54CB7C |
0x63C020 开始,长度 0x6CC18 |
壳放在 loader_meta 后方的 ELF 链接信息块 |
结果保存到 ctx+0x90;后续以它为基址计算 .dynsym、.dynamic、RELA、JMPREL 和 Android relocation 地址 |
0x54CFE8 |
image_base+0x190 开始,长度 0x5062E0 |
主 packed payload,即压缩的最终代码/数据镜像外层 | 结果前 8 字节给出解压长度,packed_dst+8 再送入解压函数生成最终内存镜像 |
core_decode 伪代码
1 | |
zlib_uncompress2_wrapper
sub_557170 @ 0x557170 是壳内置 zlib 的一次性解压包装器,参数语义与 uncompress2() 基本一致:
1 | |
解压后需要按 PT_LOAD 分段映射
zlib 解压后的 main_payload 输出长度为 0x530268
以下 Elf64_Phdr 来自于第一次 core_decode 解出的 metadata 开头
metadata 开头连续保存 6 个 Elf64_Phdr;筛选 p_type == PT_LOAD 后得到:
p_offset |
p_vaddr |
p_filesz |
p_memsz |
|---|---|---|---|
0x0 |
0x0 |
0x5062E0 |
0x5062E0 |
0x5062E0 |
0x50A2E0 |
0x29CF0 |
0x29D20 |
0x52FFD0 |
0x537FD0 |
0x298 |
0xBAE0 |
完整运行时映射为:
1 | |
第二段的 p_memsz - p_filesz = 0x30,第三段为 0xB848;这两块段尾内存必须清零。
当前 IDB 的 VA 0..0x18F 已经保存原始 ELF header 和 Program Header Table,为了避免静态 patch 时覆盖这些结构,可以仅对首段执行:
1 | |
解密脚本
1 | |
unidbg
编写 test 脚本
1 | |
将原来的前 0x190 字节复制到 dump 的即可分析
其他
其实还应该解密 elf 最后的 .data 段用于实现 GOT、LPT 定位的,但是比较麻烦就先不做了(分配到动态内存中了),而上述静态分析方法可以直接解密(或者这里结合一下静态的脚本解密一下也可以),下面给出一个工具脚本,用于 patch GOT,当然不使用也可以
1 | |
分析 executeGBS
1 | |
伪代码
1 | |
POC
1 | |
Wechat 开启 H5 DevTools
环境:Android 微信 8.0.69
目标:让chrome://inspect能看到并调试微信内打开的 H5 网页 (真实 DOM / Console / Network / 断点), 不包括小程序
背景
微信有两套浏览器内核
微信打开网页不是用一个固定的浏览器,而是在 XWeb 框架下可选多个内核。本文只关心两个:
Pinus 内核(默认)
Pinus 是腾讯基于 Chromium 自研裁剪的浏览器内核(源码路径里叫 weblayer,基于 Chrome 116),
以 libxwebcore.so(135MB)的形式打包在微信里,类名 com.tencent.xweb.pinus.sdk.WebView,
内核类型枚举值 WV_KIND_PINUS
它的调试不走标准通道:Pinus 不开标准的 webview_devtools_remote 调试端口,而是走微信
自己的私有远程调试(往 xweb.weixin.qq.com 拨 WebSocket 隧道),chrome 连不上
系统 WebView 内核
就是 Android 系统自带的 WebView(本机是 libmonochrome_64.so,171MB,Chrome 130),
内核类型 WV_KIND_SYS。它是标准 Chromium,开启调试后会 bind 标准的webview_devtools_remote_<pid> 端口,chrome://inspect 直接就能连
开启 webview 调试为什么无法连接上
调用 android.webkit.WebView.setWebContentsDebuggingEnabled(true) 想开调试但因为渲染页面的是 Pinus,开调试端口的却是系统 WebView。所以 chrome://inspect 里能看到”远程浏览器”,却没有可点的 page。
1 | |
一、系统 WebView 内核方案
1. 做法总览
强制微信用系统 WebView 渲染 H5 + 开启系统 WebView 调试。 setWebContentsDebuggingEnabled(true) 只对 系统 WebView 那套生效
2. 选择内核
1 | |
2.1 枚举 f1
1 | |
2.2 WebView.H0 — 决策入口
1 | |
代码里直接读出三点:
- 只决策一次:
WebView.m != NONE就直接返回。WebView.m由I0(initWebviewCore)→J0在第一个 WebView 创建时写一次(WebView.m = f10),之后全进程复用 —— 运行时改WebView.m或刷页面没用,旧 WebView 早用 Pinus 建好了。 - HardCode 优先级最高:
t0.a.c(module)返回非 NONE 就直接采用,根本不读服务器setwebtype。 - CW / PINUS 兜底成 SYS:落 else 分支
WebView.p=true。我们写WV_KIND_SYS不触发兜底,稳走 SYS。
2.3 u0.c — 读 HardCodeWebView
1 | |
2.4 z3.f — 打开 xweb_debug
1 | |
串起来:往 xweb_debug 写 HardCodeWebViewmm = WV_KIND_SYS → u0.c("mm") 返回 SYS → H0 采用、返回 SYS → I0/J0 写进 WebView.m → 系统内核渲染、标准调试端口有页面。
3. 落地代码
1 | |
4. 注入哪个进程、具体怎么做
核心前提:hookDevTools 只要保证 WebView.H0 在首次点开 H5 网页之前被 hook 住即可。
注入进程:UI 进程 com.tencent.mm(无后缀)。它是最终渲染 H5、H0 选择内核的进程。
完整流程:
1 | |
二、Pinus 内核自己的私有调试(存档)
「不切系统内核、直接在 Pinus 上调试」的可行性分析
结论:技术可行,但要自己搭中转 + 破触发锁,投入产出比低
基于libxwebcore.so(Chrome116)逆向
6. Pinus 调试是「往外拨」,不是「等你连」
系统 WebView 是开个本地端口等 chrome 来连(server 模型)。Pinus 相反 —— 它是客户端,
主动拨号连微信服务器,把 CDP 流量通过 WebSocket 隧道转出去:
1 | |
设备隧道命令(XWebDevTools_OnTunnelData):XWeb.targets / XWeb.version /XWeb.inspect / XWeb.close / XWeb.NetLog*。CDP 会话本身是完整原生的,只是被套进
隧道发去了微信服务器。
7. 帧格式与地址(静态读出)
帧格式:XWebWebSocketAdapter::Write(0x270A3D4)拿一段裸字节直接塞进 WebSocket,
无自定义封包,就是「JSON 文本 over WebSocket」:
- 设备隧道:收
{"method":"XWeb.inspect","params":{...}},发{"event":"...","data":{...}} - 目标隧道:收发就是裸 CDP JSON(
{"id":1,"method":"Runtime.evaluate",...}及其 response),
进出点sub_270CF24(server→CDP)/sub_3627EEC(CDP→server)
三个地址全写死 host = xweb.weixin.qq.com,只有 id/token 是变量:api/rd/create_device、rd/device/%s、rd/target/%s?token=%s。
8. 触发被三重锁死(私有化的真正门槛)
想让 Pinus 自己发起,入口是页面 JS window.xweb_remote_debug.RemoteDebugStart(token),但:
- JS 注册域名锁:
RemoteDebugStart只在页面域名 =xweb.weixin.qq.com时才挂到window.xweb_remote_debug(sub_2715ABC)。普通 H5 页面只有RemoteDebugStatus/Stop。 - 二次域名校验:
HandleRemoteDebugStart(0x2716500)里又查一遍,非该域名返回invalid domain。 - 需要 token + renderer 线程:token 来自微信调试门户,
create_device也要联微信服务器。
9. 私有化路线(若非做不可)
- spawn 时(页面加载前)hook
std::string == "xweb.weixin.qq.com"(0x26D8860,窄化到只对
该域名返回真)绕过两处域名锁。 - hook
XWeb_RD_CreateDevice(0x2711A18)、XWebTarget_StartClientProxy(0x270D194),把xweb.weixin.qq.com改写成ws://localhost:<port>。 - 本地中转扮演三个角色:
create_device(返 deviceId)+ 设备隧道(下发XWeb.inspect{targetId, deviceToken},targetId 从XWeb.targets返回里拿)+ 目标隧道
(裸 CDP 双向桥给 chrome 前端)。 - 注入帧要打进真正渲染内容的 render frame,不是 appbrand 的
about:blank容器帧。
实测尝试运行时抓帧,卡在:触发锁 + 多进程漂移(H5 落 :appbrand0)+ 注入到空容器帧。
比板块一重得多,除非必须用 Pinus 内核渲染又要调试,否则不建议。
附:关键符号
| 符号 | 作用 |
|---|---|
com.tencent.xweb.f1 |
内核类型枚举 (WV_KIND_*) |
com.tencent.xweb.WebView.H0(f1, String) |
内核决策入口 (getPreferedWebviewType) |
com.tencent.xweb.WebView.m |
缓存已决策内核的静态字段 |
com.tencent.xweb.u0.c(String) |
从 xweb_debug SP 读 HardCodeWebView |
br5.z3.f() |
打开 SharedPreferences “xweb_debug” |
com.tencent.xweb.pinus.sdk.WebView |
Pinus 内核 WebView |
Native 模块:
| 模块 | 说明 |
|---|---|
libxwebcore.so |
Pinus 内核 (Chrome 116),渲染页面 |
libmonochrome_64.so |
系统 WebView (Chrome 130),标准调试端口 |
socket webview_devtools_remote_<pid> |
系统 WebView 的调试端口 |
libxwebcore.so Pinus 私有调试函数(libxwebcore.so.i64):
| RVA | 名字 | 作用 |
|---|---|---|
0x2715658 |
XWebFrameHelper_CreateXWebExtend |
注入 window.xweb_remote_debug |
0x2715ABC |
(AddRemoteDebugMethods) | 注册 JS 方法;RemoteDebugStart 有域名锁 |
0x2716500 |
(HandleRemoteDebugStart) | Start 处理,二次域名校验 + 取 token |
0x2711A18 |
XWeb_RD_CreateDevice |
注册设备,硬编码 create_device URL |
0x2711F64 |
(device tunnel WS) | 开设备隧道 rd/device |
0x270EB0C |
XWebDevTools_OnTunnelData |
设备隧道命令分发 |
0x270D194 |
XWebTarget_StartClientProxy |
开目标隧道,硬编码 rd/target URL |
0x270CF24 |
(target onMessage) | 目标隧道 server→CDP 入口 |
0x3627EEC |
(session dispatch) | CDP→server 出口 |
0x270A3D4 |
XWebWebSocketAdapter::Write |
裸字节写 WS(无自定义封包) |
0x26D8860 |
std::string::operator==(char*) |
域名锁字符串比较(绕过点) |
0x3615C5C |
XWeb_GetTabHostById |
按 targetId 查真实 DevToolsAgentHost |
提取
libxwebcore.so:解split_delivery.config.arm64_v8a.apk→ 取libxwebfullpack.so
(是个 zip)→ 再解出libxwebcore.so。
提取libmonochrome_64.so:从com.google.android.trichromelibrary_*的base.apk
取lib/arm64-v8a/libmonochrome_64.so。
2026 腾讯游戏安全 PC 初赛 WP
驱动加载
使用 EfiGuard 进行 PG 和 DSE 修复
1 | |
绿字进系统后关闭 DSE 就可以加载驱动了
1 | |
启动驱动后运行程序

exe 分析
1 | |
重点在驱动交互,其余均为负责输入的逻辑
IO_8001200C
初始化
1 | |
1 | |
IO_80012004
1 | |
负责移动,发送给驱动的数据结构见下
1 | |
1 | |
IO_80012008_Reset
重置迷宫
1 | |
sys 分析
有一些难以分析的 call,nop 掉看整体流程

载入点
1 | |
Contorl 里面也有两处难以分析 call,nop 掉看整体逻辑
1 | |
CODE 0x8001200C
传递迷宫大小,位置信息
CODE 0x80012008
重置
CODE 0x80012004
移动,主要逻辑
移动延迟
KeDelayExecutionThread 对每步移动都进行一点延迟,所以算法爆破特别慢,需要 patch

VA 0x1400026BD,RVA 0x26BD 全 nop (5字节)
1 | |
五个泄漏点
sys data 段起始有一个花指令魔数,通过该魔数可以定位四个泄漏点

1. Event 泄漏
exe 0x140001340 创建 Global Event

sys 0x1400022B0 中 event 泄漏:n2==0/2 置 MazeMoveOK,否则置 MazeMoveWall

2. 信号量 泄漏
exe 0x14021B91F 创建信号量,异或 0x4B 解密字符串


得到
1 | |
sys 0x140319A37 中 semaphore 泄漏,_EDX ==0/2 释放 {A7F3B2C1-9E4D-4C8A-B5D6-1F2E3A4B5C6D} ,否则释放 {B8E2C3D0-0F5A-5D9B-C6E7-2A3F4B5C6D7E}

3. TEB.LastError 泄漏
sys 0x140316ADF 中修改 exe LastErrorValue ,n2 为 0 时设置 0xC0DE0001, 2 时设置 0xC0DE0002, 其他时设置 0xC0DE0000

4. TEB + 0x1748 泄漏
exe 0x14021BC88 在释放时有部分提示

sys 0x14031857E 中,使用 PsGetThreadTeb 获取 TEB 后使用 ZwSetInfomationObject 对 teb + 0x1748 处进行设置
在 icall 分发处能够看到实际调用的 call 为 PsGetThreadTeb 和 ZwSetInfomationObject

两个 icall 都清理为 call rax 不然反编译有问题

n2 = 0/2,设置 1,否则设置 0

5. 内存属性 泄漏
在 _guard_dispatch_icall_nop 下断点,当输入断在 rax=ZwProtectVirtualMemory 时就是触发第五个漏洞
调用参数为
rdx = ShadowGateApp.exe 的 .data 页
rcx = ShadowGateApp.exe 的 .data 页长度
将 .data 页的属性修改为 RW 或 ERW
1 | |

找了下没找到,看堆栈这个是真的在 vm 里面了,该泄漏通过观察页得到,猜测 sys 会修改名为 .data 的页


根据上面规则猜测 n2 = 0/2 时,页属性变成 PAGE_EXECUTE_READWRITE 其他时,保持 PAGE_READWRITE
查询迷宫 & 探索迷宫
方法一:r3 下通过泄漏探索迷宫
伪代码,完整见附件
1 | |
得到以下迷宫图
1 | |
方法二:可以通过 sys 直接读取分配内存读取到迷宫


最短路径求解
bfs
1 | |
Flag
flag{SHAD0WNT_HYPERVMX}
1 | |
内网 ss 服务端搭建
在某某内网环境下,手机需要通过某内网设备来连接到互联网(内网设备已连接外网),且为了安全考虑,所以采用 ss 的加密,故有此篇记录
二编(2026年10月26日)
配置文件一些细节配置
1 | |
下载&启动 ss 服务端
选取 https://github.com/shadowsocks/shadowsocks-rust
笔者使用 shadowsocks-v1.24.0.x86_64-pc-windows-gnu.zip 进行测试
1 | |
生成密钥
1 | |
编写 config.json
1 | |
启动服务端(这里不做服务安装,仅作为 cli server)
1 | |
导出 ss 链接
1 | |
注意
由于服务端接收 0.0.0.0 也就是所有 ip 请求,所以生成的 ss 链接也是 0.0.0.0 所以需要改成目标设备 ip,例如 192.168.1.2@8388
注意最后要开启对应端口的防火墙,例如 8388