[原创]GlassWire 3.11.1164 云许可算法分析
程序名称:GlassWire
程序版本:3.11.1164(x64)
程序语言:多语言
程序平台:Windows 10 / 11
加壳类型:WinLicense 3.x(.themida + .boot)
分析工具:IDA Pro、Python 3、SQLite
文章类型:原创 · 算法分析
试用程序请用官方安装包(附件已传,或自行下载):
安装包 SHA-256(changelog 公布):
0D3C736CDCC73EA134F5392C4DC6D0AA48056C408B518D6A4299D022F36E18A2
下面只改本地云许可存储,不改 GlassWire.exe / GWCtlSrv.exe 的代码。同一套 protobuf 和 Blowfish 密钥还在,新版本一般也能用。
一、先看它怎么存许可
服务启动时走两条通道。
1. 本地文件通道
路径:C:\ProgramData\GlassWire\share\license.dat
格式:[正文][256 字节 RSA-2048 签名]。VerifyLicenseSignature(sub_7FF663BE6180)用内嵌公钥验整段正文。没有私钥就伪造不了。日志里的 License type: N 走的是这条。云许可加载成功时,这行会一直是 0。
2. 云许可通道
路径:C:\ProgramData\GlassWire\service-full\cloud\cloud.db
表:options(name TEXT PRIMARY KEY, value BLOB),行名 License。读取语句:
SELECT name, value FROM options WHERE name LIKE(?)
绑定字面量 License(sub_7FF6637245C0)。这条通道没有签名。后面整篇都对着它。
二、壳和 dump
3.11 的 GlassWire.exe、GWCtlSrv.exe 都是 WinLicense 3。进程内 Frida 在 stub 里活不下来,OEP 抓不到。
实际用的是外部 dump:拉起进程,盯第一个可执行节的熵,连续两次低于 6.8 就 NtSuspendProcess,再用 Scylla 把映像抠出来。dump 入口是占位 RVA 0x1000,只给静态分析用。
GWCtlSrv dump 的 image base 是 0x7FF662EF0000。早先用 0x7FF662EF1000 会整页偏高,跟代码还能蒙对,读 .rdata 就全错。核对方法:Blowfish π 表,IDA 0x7FF664837D70 必须读到:
88 6a 3f 24 d3 08 a3 85
对不上就先别往下跟。
三、云许可的 protobuf
.rdata 里嵌了 cloud_consumer_subscription.proto,包名 glasswire.cloud.consumer.api:
License {
string id = 1;
enum Type { Free = 0; Premium = 1; }
enum Status {
NOSTATUS = 0;
ACTIVE = 1;
PLAIN_TRIAL = 2;
FULLILLED_TRIAL = 3;
NO_SUBSCRIPTION = 4;
MAX_LICENSES_REACHED = 5;
}
Subscription.Duration duration = 4;
}
EncryptedLicense {
Version version = 1; // 磁盘上行里不出现
bytes license = 2; // License 序列化后再 Blowfish
}
Duration {
string create_timestamp = 1;
string expire_timestamp = 2;
}
注意两套枚举不要混:
| 消息 |
字段 |
含义 |
云 License.Type |
0 / 1 |
Free / Premium |
旧 LicenseInfo.LicenseStatus |
0..7 |
Free, Basic, Pro, Elite=3, Trial, SubBasic, SubPro, SubElite |
云通道上写 type=3(旧 Elite),界面直接显示 FREE。3.11 产品侧已经没有 Elite,云 Type 只有两档。
Duration 两个字段都是 string,不是 varint 时间戳。
磁盘上实际存的是只有 field 2 的 EncryptedLicense,再做标准 Base64,以 TEXT 写入 options.value。
四、Blowfish
两边通道共用同一套密码。
- 算法:Blowfish-CBC
- 块序:大端(左移拼 32 位,和
LicenseEncryptor 一致)
- IV:8 字节全 0
- 密钥 24 字节:
1cad28831904f9131fb50b9c7a2f5d40761d6bc5759ceb19
来源有两处,值相同:
- 旧通道:dump 文件偏移
0x1916548(LicenseInfoPrivate::DecryptLicense)
- 云通道:
LicenseEncryptor(sub_7FF66375C820)里六个 DWORD 拼回来
P/S 表是标准 π 常数,小端 DWORD,dump 文件偏移 0x1946970,共 4168 字节。表必须按 S[4][256] 拷。按四个 256 项数组去 S0 + i*256 会走出第一张表,自测能加解密,活服务照样报 license decryption failed。
填充(加密 sub_7FF663869CB0,解密 sub_7FF663869930):
pad = 8 - (len % 8) // 结果在 1..8
payload = plaintext || 00*pad || uint64_le(pad)
解密读 trailer 低 32 位当 pad,再截成 len - pad - 8。trailer 不在 1..8,日志就是 license decryption failed。
活进程崩溃转储里 worker 帧的 Blowfish schedule 和这 24 字节密钥推出来的 schedule 逐字节相同,密钥以活服务为准。
五、value 怎么写进 SQLite
读 value 的路径是 OpenSSL BIO(sub_7FF66386A0C0):
BIO_new_mem_buf(value)
-> BIO_f_base64,BIO_FLAGS_BASE64_NO_PADDING(flag 0x100)
-> BIO_push
-> BIO_read(512) 泵进 memory BIO
-> BIO_ctrl flush / get_mem_data
断言字符串 crypto\bio\bss_mem.c 能对上库。
所以 options.License 只认一种形态:EncryptedLicense protobuf 的裸 Base64,TEXT 存储。不要加 Base64_ 前缀。那个前缀是 glasswire.conf 里 CloudAuthTokens 一类配置用的。
实测过的失败形态:
| 写入内容 |
结果 |
| 原始 protobuf 当 BLOB |
BIO 把二进制当 Base64 吃。pad 爆炸,std::length_error,服务崩溃循环,SCM 停掉 |
| 十六进制文本 |
hex 字母都在 Base64 表里,解出来是另一堆字节。日志 encrypted license parsing failed(FromEncryptedBytes 第 249 行),服务还活着 |
Base64_ 前缀 |
解码器连前缀一起吃,解析失败 |
| 裸 Base64 TEXT |
能加载、能解密、服务会按自己的格式再加密写回 |
加载成功后服务会把同一个 CloudLicense 重新序列化再写回去(Duration 的 varint 更紧)。能看到这一步,说明整条链是通的:Base64 → protobuf → Blowfish → 内存对象 → 再加密 → 再存储。
还有一个坑:LIKE('License') 会同时命中 TEXT 名和 BLOB 名。有效期解析失败时,云组件会留下一行 名字类型是 BLOB 的空 License。下次启动先读到这个空行,日志 encrypted license data is empty,真正的 TEXT 行被挡住。写入前先清干净:
DELETE FROM options WHERE CAST(name AS TEXT) = 'License';
INSERT INTO options(name, value) VALUES('License', ?);
打开库要用 READWRITE。服务挂着 WAL 时,READONLY 贴不上 -shm,读会失败。
六、有效期字符串
服务解析 Duration 用的格式和它自己渲染的格式一样(sub_7FF66381C240):
%Y-%m-%dT%H:%M:%S
没有尾巴上的 Z。
下面三种都会解析成 0:
2025-01-01T00:00:00Z
"1735689600" 这种 epoch 数字串
- 根本不带 duration 字段
解析为 0 时 protobuf 还能进内存,但随后 LicenseEncryptor::Encrypt 失败,日志连续两条:
license serialization failed
license encryption failed
云组件把状态打回未授权,并留下上面说的 BLOB 空行。
过期时间写到年 3000,字符串能解析,32 位 epoch 溢出,关于页倒计时变成乱值。安全上限是 2037-12-31T23:59:59(unix 2145916799)。一组能用的值:
2025-01-01T00:00:00 -> 2037-12-31T23:59:59
付费行必须带 Duration,不能省。
七、CloudAccountState
只写 License 行,界面徽章仍是 FREE。还要改 glasswire.conf 里的 CloudAccountState。
该值是 protobuf 的小写十六进制,同样没有 Base64_ 前缀。
field 1 = ConsumerSession 状态
field 3 = device_info
field 4 = server_info
field 1 在已看过的版本里都排第一(key 字节 0x08)。枚举在 dump VA 0x7FF6647CE990:
kStateNotSet=0
kStateUnregistered=1
kStateRegistering=2
kStateUnauthorized=3
kStateAuthorizing=4
kStateUnauthorizing=5
kStateReady=6
kStateExpired=7
kStateRequestingAuthToken=8
把 field 1 改成 6,后面的 field 3 / field 4 留下。截成只剩 field 1,服务会打:
device id changed: State device id: ,
然后用现场设备信息把整段 protobuf 重写一遍。
状态机之后会请求:
/agent-api/v1/license?device_id=<device>
/api/v1.1/agent/user/me
400 / 503 不会把一条 Duration 合法的行打回去。心跳参数在 Type=1 时是 l=Premium。
测试里看到 field 1 掉到 3 或 8,原因是 Duration 管道失败,不是这几个 HTTP 错误本身。
启动画面上那个「启动服务」是另一条路:许可 id 长度 44,再加 LicenseCore::DoActivationFailed(license_core.cpp 第 1107 行)里的签名判断。磁盘上的云许可行不走这条,服务启动时直接读。
八、算法还原(Python)
pycryptodome 的 Blowfish 按大端块、零 IV、自定义填充用即可。
import datetime
import struct
from Crypto.Cipher import Blowfish
KEY = bytes.fromhex("1cad28831904f9131fb50b9c7a2f5d40761d6bc5759ceb19")
def varint(n):
out = bytearray()
while True:
b = n & 0x7F
n >>= 7
if n:
out.append(b | 0x80)
else:
out.append(b)
return bytes(out)
def vfield(num, value):
return varint((num << 3) | 0) + varint(value)
def bfield(num, value):
return varint((num << 3) | 2) + varint(len(value)) + value
def iso_noz(epoch):
t = datetime.datetime.fromtimestamp(epoch, datetime.timezone.utc)
return t.strftime("%Y-%m-%dT%H:%M:%S").encode()
def build_license(lic_id, from_ts, to_ts):
dur = bfield(1, iso_noz(from_ts)) + bfield(2, iso_noz(to_ts))
return (
bfield(1, lic_id.encode())
+ vfield(2, 1) # Premium
+ vfield(3, 1) # ACTIVE
+ bfield(4, dur)
)
def gw_encrypt(pt):
pad = 8 - (len(pt) % 8)
payload = pt + b"\x00" * pad + struct.pack("<Q", pad)
return Blowfish.new(KEY, Blowfish.MODE_CBC, iv=b"\x00" * 8).encrypt(payload)
def stored_value(lic_id, from_ts, to_ts):
inner = build_license(lic_id, from_ts, to_ts)
enc = gw_encrypt(inner)
blob = bfield(2, enc)
import base64
return base64.b64encode(blob).decode()
if __name__ == "__main__":
print(stored_value(
"GLWFORGE0000PREMIUM000TEST000000010000000000",
1735689600, # 2025-01-01T00:00:00
2145916799, # 2037-12-31T23:59:59
))
已知答案(防自己写错表或端序):明文
0a2c474c57464f52474530303030454c49544530303030544553543030303030303031303030303030303030303010011801
对应密文
5349edacf44cb2f1fa81891024f342d8c0137cc61e27cc0e4448f7858827cf1ff8e7a2078b62a26a4d7dcd36c59ec3198c37a37ba694dcf7de6f08f9083c1df4
对不上就不要写盘。
CloudAccountState 只改 field 1 那个 varint 字节(状态 0..8 都是单字节),其余不动。例如原文以 08 03 ... 开头(未授权)改成 08 06 ...。
九、写入顺序
服务在跑时不要改库。
- 停服务
GlassWire,结束 GlassWire.exe
- 备份
cloud.db、glasswire.conf、glasswire.db
- 按第五节 DELETE + INSERT 写入
License
CloudAccountState field 1 写成 6
- 启动服务,等几秒
- 翻最新日志,下列字符串在启动时刻之后不得出现:
FromEncryptedBytes
encrypted license
decryption failed
serialization failed
encryption failed
- 再开 UI
回滚用备份覆盖这三份文件即可。
十、怎么确认生效
能对上的现象:
- 左上角徽章:PREMIUM
- 绿色 UPGRADE 按钮变成头像
- 底部 Get All Premium Features 横条消失
- 心跳:
l=Premium
对不上、别被骗的现象:
- 日志
License type: N(旧文件通道)
- 关于页「还剩几个小时」(见下一节)
CloudAuthTokens 不用改。头像只是 kStateReady 看起来像已登录,不是重新走了一遍门户。
十一、关于页倒计时
关于页在徽章已经是 Premium 时,仍可能显示 Only a few hours remaining。
UI 里 DaysRemaining(UI dump 0x7ff7b77db220)走 fill_timerange。has_duration 的判定是 get_type() - 4 <= 3,也就是旧类型 4..7(试用 / 订阅)。然后 (expire - now) / 3600 / 24。
磁盘加载伪造的云许可,不会走 API MakeLicense(0x7ff6637ca950)去填 LicenseInfo.time_expire。对 GlassWire.exe 进程做字符串扫描,伪造的 id、ISO 文本、unix 时间戳都不在映像里。GWCtlSrv.exe 里能看到 ISO 文本,看不到解析后的 unix 时间。
glasswire.db 的 license_data 是服务端缓存,UI 进程不映射这块。付费功能和徽章不依赖关于页那一行字。
十二、3.8 的旧方法和 3.11 的差别
3.8.1061 是脱 WinLicense 之后改两处机器码:档位 getter 恒回 6,过期判断恒回假。那两个 hex 特征到 3.11 对不上。
3.11 走云存储,不碰代码,所以不依赖具体指令排布。密钥或 protobuf 一旦换掉,日志里的解密 / 序列化失败会先告诉你,不要硬写。
启动画面激活流程(44 字符 id + 正则)是死的,全局指针还指到一份源码路径字符串。和磁盘云许可不是同一条路。
附件
- 官方 3.11.1164 安装包(或上面的官方链接)
- 还原脚本(第八节源码)
- 独立写入工具源码
gw_patcher.cpp(MSVC C++17,管理员清单,运行时加载 winsqlite3.dll)
分析用的 dump 体积大,不随帖提供。需要 IDA 时自己装官方包,按第二节 dump 即可。