思绪思维导图vip注册机成因分析

从 Electron 授权链路看一次本地 VIP 绕过

这次看的是一个 Electron 桌面客户端的 VIP 注册机制。现象很直接:应用提供 VIP 功能,正常情况下需要在线名单或离线许可证才能解锁;但在本地分析后,可以通过修改客户端授权判定点,让程序稳定进入 VIP 状态。

这类问题的重点不在"能不能构造一个漂亮的注册码",而在客户端授权链路本身:当最终授权结论由用户本地可修改的 JavaScript 代码产出时,本地保护做得再复杂,也很容易在最后一个布尔返回值上被截断。

攻击面入口:Electron 的 ASAR 包

目录结构里能看到典型 Electron 应用形态:

text 复制代码
thoughts/
├── 思绪思维导图.exe
└── resources/
    └── app.asar

exe 更像运行壳,真正的业务逻辑被打在 resources/app.asar 里。解包后可以看到主进程代码、前端 bundle 和静态资源。

package.json 指向开源项目:

text 复制代码
https://github.com/wanglin2/mind-map
version: 0.19.0

但对比对应 tag 后发现,开源版本里没有这套 VIP 主逻辑。也就是说,授权代码是发行包里额外加入的,分析时应以当前 ASAR 运行时代码为准,而不是以 GitHub 源码为准。

这个判断很关键。挖客户端洞时,源码仓库只能帮助理解框架和业务背景,真正的攻击面永远是当前用户机器上实际执行的产物。

授权链路:VIP 状态不是一个单点开关

前端 bundle 中的 VIP 判断逻辑大致是三段式:

text 复制代码
在线 VIP 名单
    ↓ 未命中
本地 VIP 缓存
    ↓ 未命中
本地离线许可证

在线逻辑会请求:

text 复制代码
https://simple-mind-map.oss-cn-beijing.aliyuncs.com/vip.json

返回内容是客户端 UUID 白名单。当前机器的 getClientUUID() 如果在名单里,前端会保存一个短期本地 VIP 状态:

js 复制代码
saveVIPStatus({
  isVip: true,
  expire: Date.now() + 2592e6
})

这是一种常见授权设计:在线名单负责快速放行,本地缓存负责离线续期,离线许可证负责长期授权。

从攻击视角看,重点不是这三条路径哪条最容易伪造,而是它们最后都会汇聚成同一个事实:

text 复制代码
前端是否认为当前用户是 VIP

所以真正值得追的是状态汇聚点,而不是一开始就陷进某个数据源里。

缓存保护:为什么不直接改本地 JSON

主进程暴露了和 VIP 缓存相关的 IPC:

text 复制代码
getVIPStatus
saveVIPStatus

本地数据目录在:

text 复制代码
C:\Users\15225\AppData\Roaming\thoughts\storage

如果这里保存的是明文 JSON,那直接写入类似下面的数据就可能完成绕过:

json 复制代码
{
  "isVip": true,
  "expire": 9999999999999
}

但实际不是这样。主进程对本地状态做了两类保护。

第一类是加密存储。敏感状态通过 AES-256-CBC 保存,如果系统支持 Electron safeStorage,还会用 safeStorage 保护本地加密密钥;否则才回退到固定 key。

第二类是机器绑定。读取 VIP 缓存时,会校验缓存中的 clientUUID 是否等于当前机器的 machineId()

所以本地缓存实际更接近:

text 复制代码
AES 加密数据 + safeStorage 保护的密钥 + machineId 绑定 + expire 过期时间

这套设计的目的很明确:防止复制别人的 VIP 缓存,或者手动篡改本地 JSON。

从漏洞利用角度看,硬写缓存不是不能做,但成本被抬高了。需要复现加密流程、处理 safeStorage、填对机器码,还要保持字段结构和过期时间都符合预期。这个方向工作量大,而且和漏洞本质关系不大。

离线许可证:RSA 验签挡住了"真注册机"

主进程里还有离线许可证逻辑:

text 复制代码
activateLicense
getIsLicenseVIP

许可证数据保存到一个加密存储键中:

text 复制代码
1cJwF4oVPI8XCMkphqjG

核心逻辑可以抽象成:

js 复制代码
license = readLocalLicense()
result = verifyByPublicKey(license.licenseCode)
return result.success

这里使用的是 RSA 公钥验签。正常授权模型如下:

text 复制代码
服务端或管理员持有私钥
    ↓
签发 licenseCode
    ↓
客户端内置公钥
    ↓
验证 licenseCode 签名

公钥能验证签名,但不能生成合法签名。只要私钥不在客户端里,且签名实现没有明显漏洞,就不能从客户端直接推导出任意合法 licenseCode。

这也是这类题最容易误判的点。看到 activateLicense,第一反应可能是"逆注册码算法";但 RSA 验签已经说明它不是简单的 hash、xor 或固定格式拼接。客户端只有验签能力,没有签发能力。

所以严格来说,这里做不出真正意义上由私钥签发的"合法注册码生成器"。更实际的路线是做本地授权补丁器。

漏洞点:前端信任主进程的布尔结论

虽然缓存和许可证都有保护,但前端并不理解这些细节。前端最后只需要一个结论:

text 复制代码
当前是不是 VIP?

这个结论来自主进程 IPC。

关键代码如下:

js 复制代码
r.ipcMain.handle("getIsLicenseVIP", async () => {
  const e = await C("1cJwF4oVPI8XCMkphqjG")
  return !(!e || !e.licenseCode) && (await R(e.licenseCode)).success
})

这段代码是离线许可证链路的最后一跳:

text 复制代码
读 license
    ↓
检查 licenseCode
    ↓
RSA 公钥验签
    ↓
返回 true / false

对前端来说,IPC 返回的 boolean 就是最终事实。前端不会再验签,也不会二次读取许可证。只要这个 handler 返回 true,前端就会认为本机具有 VIP 权限。

因此最小补丁是:

js 复制代码
r.ipcMain.handle("getIsLicenseVIP", async () => !0)

这不是破解 RSA,也不是伪造 licenseCode,而是直接修改了本地可信结论的产出点。

漏洞根因可以概括为一句话:

text 复制代码
授权结果由本地可修改代码最终裁决,缺少不可绕过的服务端强制校验或完整性约束。

只要攻击者能改本地客户端代码,任何纯本地授权判断最终都可以被改写。缓存加密和 RSA 验签能保护"授权数据",但不能保护"使用授权数据的本地判断逻辑"。

利用思路:等长补丁比重打包更轻

常规做法可以是:

text 复制代码
解包 app.asar
修改 background.js
重新打包 app.asar

但这里补丁点非常短,适合做等长二进制替换。

原始片段:

js 复制代码
r.ipcMain.handle("getIsLicenseVIP",async()=>{const e=await C("1cJwF4oVPI8XCMkphqjG");return!(!e||!e.licenseCode)&&(await R(e.licenseCode)).success})

替换成:

js 复制代码
r.ipcMain.handle("getIsLicenseVIP",async()=>!0/* padding */)

后半段用 JS 注释填充,让替换前后字节长度一致。这样不用重新构建 ASAR,也不会引起包内偏移变化。

这个技巧适合这种场景:

  • 目标代码是 ASCII JavaScript 片段
  • 原始片段稳定且唯一
  • 新逻辑比旧逻辑短
  • 可以用注释填充剩余长度
  • 只需要改一个判断点

PoC:本地 VIP 授权补丁器

下面是完整 PowerShell 脚本。它会定位当前目录下的 resources\app.asar,搜索原始许可证校验片段,备份 ASAR,然后写入等长补丁。

powershell 复制代码
param(
    [string]$AppDir = $PSScriptRoot,
    [switch]$NoBackup
)

$ErrorActionPreference = "Stop"

function Find-Bytes {
    param(
        [byte[]]$Haystack,
        [byte[]]$Needle
    )

    if ($Needle.Length -eq 0 -or $Haystack.Length -lt $Needle.Length) {
        return -1
    }

    for ($i = 0; $i -le $Haystack.Length - $Needle.Length; $i++) {
        $matched = $true
        for ($j = 0; $j -lt $Needle.Length; $j++) {
            if ($Haystack[$i + $j] -ne $Needle[$j]) {
                $matched = $false
                break
            }
        }
        if ($matched) {
            return $i
        }
    }

    return -1
}

$asarPath = Join-Path $AppDir "resources\app.asar"
if (-not (Test-Path -LiteralPath $asarPath)) {
    throw "app.asar not found: $asarPath. Run this script in the app directory, or pass -AppDir."
}

$encoding = [System.Text.Encoding]::ASCII
$originalText = 'r.ipcMain.handle("getIsLicenseVIP",async()=>{const e=await C("1cJwF4oVPI8XCMkphqjG");return!(!e||!e.licenseCode)&&(await R(e.licenseCode)).success})'
$patchedPrefix = 'r.ipcMain.handle("getIsLicenseVIP",async()=>!0/*'
$patchedSuffix = '*/)'
$patchedMarker = 'r.ipcMain.handle("getIsLicenseVIP",async()=>!0'

$paddingLength = $originalText.Length - $patchedPrefix.Length - $patchedSuffix.Length
if ($paddingLength -lt 0) {
    throw "Patch length calculation failed."
}
$patchedText = $patchedPrefix + (" " * $paddingLength) + $patchedSuffix

$originalBytes = $encoding.GetBytes($originalText)
$patchedBytes = $encoding.GetBytes($patchedText)
$markerBytes = $encoding.GetBytes($patchedMarker)

$bytes = [System.IO.File]::ReadAllBytes($asarPath)

if ((Find-Bytes -Haystack $bytes -Needle $markerBytes) -ge 0) {
    Write-Host "[+] VIP patch already exists."
    Write-Host "[+] Current status: getIsLicenseVIP always returns true; local VIP is enabled."
    exit 0
}

$offset = Find-Bytes -Haystack $bytes -Needle $originalBytes
if ($offset -lt 0) {
    throw "Expected VIP license check code was not found. Version may be unsupported or already modified."
}

if (-not $NoBackup) {
    $timestamp = Get-Date -Format "yyyyMMdd-HHmmss"
    $backupPath = "$asarPath.bak-$timestamp"
    Copy-Item -LiteralPath $asarPath -Destination $backupPath -Force
    Write-Host "[+] Backup written: $backupPath"
}

[System.Array]::Copy($patchedBytes, 0, $bytes, $offset, $patchedBytes.Length)
[System.IO.File]::WriteAllBytes($asarPath, $bytes)

$verifyBytes = [System.IO.File]::ReadAllBytes($asarPath)
if ((Find-Bytes -Haystack $verifyBytes -Needle $markerBytes) -lt 0) {
    throw "Patch verification failed after write."
}

Write-Host "[+] VIP patch written successfully."
Write-Host "[+] Fully quit and reopen the app to get local VIP access."

运行方式:

powershell 复制代码
cd "目标客户端安装目录"
powershell -NoProfile -ExecutionPolicy Bypass -File .\vip-register.ps1

如果不想在当前目录运行,也可以指定应用目录:

powershell 复制代码
powershell -NoProfile -ExecutionPolicy Bypass -File .\vip-register.ps1 -AppDir "目标客户端安装目录"

补丁成功后,重新打开客户端即可看到本地 VIP 状态生效。

验证点

补丁后在 ASAR 中应能搜到:

text 复制代码
getIsLicenseVIP",async()=>!0

如果重复运行脚本,会提示:

text 复制代码
[+] VIP patch already exists.
[+] Current status: getIsLicenseVIP always returns true; local VIP is enabled.

这说明补丁器支持幂等检测,不会重复破坏文件。

修复建议

这个问题不是某个 if 写错了,而是授权架构上的天然弱点。只要关键权益完全依赖本地代码裁决,客户端就有被 patch 的空间。

更合理的修复方向包括:

  • 高价值权益由服务端强制校验,客户端只展示服务端授权结果。
  • VIP 功能调用服务端接口时二次校验权限,而不是只信任本地 isVIP
  • 离线许可证只作为短期凭据,定期和服务端换取可撤销票据。
  • 对 ASAR 和关键 bundle 做完整性校验,并把校验结果纳入功能解锁逻辑。
  • 使用代码签名、混淆、反调试和自校验提高补丁成本。

不过需要承认:只要功能必须完全离线可用,且攻击者拥有本地文件读写权限,本地授权绕过就很难被彻底消除。防护目标更多是提高成本、缩短离线授权有效期,以及避免把真正高价值能力完全放在客户端本地。

总结

这次 VIP 绕过的关键不是伪造注册码,而是识别授权链路中的最后一个信任点:

text 复制代码
前端不验 license
    ↓
前端只信主进程 IPC 返回值
    ↓
主进程 IPC 逻辑在本地 ASAR 中
    ↓
修改 getIsLicenseVIP 返回 true
    ↓
本地 VIP 状态成立

RSA 验签、机器码绑定、加密缓存都在保护授权数据,但最终的授权结论仍然由本地可修改代码输出。漏洞的根因正在这里。

相关推荐
Swift社区1 小时前
AI 接管操作系统:鸿蒙 PC AI Native OS 架构揭秘
人工智能·架构·harmonyos
大模型最新论文速读1 小时前
TRUST:RL 时保留模型的不确定性,效果提升 8%
论文阅读·人工智能·深度学习·机器学习·自然语言处理
HannahTx1 小时前
河南电商设计培训避坑指南:2026行业现状、课程拆解与机构客观分析
人工智能
陈老老老板1 小时前
如何用 Bright Data Web Scraper API + Coze 搭建 Reddit 行业情报聚合 Bot(2026 实战指南)
前端·人工智能
科技每日热闻1 小时前
舒视蓝4.0 AI版!EVNIA弈威海王星系列护眼电竞显示器27M4P5501U来袭
人工智能·科技·游戏·计算机外设
byte轻骑兵1 小时前
【LE Audio】CAS精讲[2]: 服务核心规则,落地音频设备的标准化标识
人工智能·音视频·le audio·低功耗音频·车机蓝牙
果丁智能1 小时前
物联网智能锁落地实践:破解网约房、民宿身份核验与远程权限管控难题
大数据·人工智能·物联网·智能家居
取个鸣字真的难1 小时前
Image2 生成 PPT 的最后分水岭:Prompt
人工智能·prompt·powerpoint
云烟成雨TD1 小时前
Agent Scope Java 2.x 系列【13】权限系统
java·人工智能·agent