n5012346 发表于 2026-6-29 16:20

某rust开发语音识别软件获取专业版授权

本帖最后由 n5012346 于 2026-6-29 16:26 编辑


## 1. 前言

目标是一个基于 **Tauri 2.x** 框架构建的桌面应用程序,后端使用 Rust 编写,前端 WebView 渲染。该应用具有用户登录认证和许可证激活系统,未登录或未激活状态下部分功能受限。

本文记录从零开始的完整本地破解过程,不涉及具体软件名称。

---

## 2. 环境与工具

| 工具 | 用途 |
|------|------|
| **PowerShell 7+** | 命令行分析、脚本执行 |
| **Python 3** | 二进制解析 |
| **010 Editor / HxD**(可选) | 十六进制查看和编辑 |
| **Process Monitor** | 监控文件访问 |

**分析环境**:Windows 11 x64

---

## 3. 第一步:初步侦察

### 3.1 安装目录分析

```
TargetApp/
├── TargetApp.exe         # 主程序 (~52MB)
├── DirectML.dll
├── onnxruntime.dll
├── onnxruntime_providers_shared.dll
├── uninstall.exe
```

### 3.2 判断技术栈

Tauri 应用的典型特征:
- 单 EXE 约 40–60MB(Rust 原生代码 + 内嵌 Web 前端资源)
- 带有 `.taubndl` 节区(Tauri Bundle)
- 字符串中包含 `tauri::`、`invoke`、IPC 命令名等 Rust/Tauri 标记

```powershell
$bytes = ::ReadAllBytes("TargetApp.exe")
$text = ::UTF8.GetString($bytes)
$text.IndexOf("tauri")   # 确认 Tauri 框架
$text.IndexOf(".taubndl")# Tauri bundle 节区
```

### 3.3 运行时探测

启动应用后,监控以下行为:

**文件访问**(关键):
- `%APPDATA%\TargetApp\signed_license.json` — 许可证文件
- `%APPDATA%\TargetApp\user_cache.json` — 用户信息缓存
- `%APPDATA%\TargetApp\user_token.json` — 登录 Token 缓存

**日志位置**:
```
%LOCALAPPDATA%\TargetApp\logs\TargetApp.log
```

日志包含关键信息:

```
开始验证签名
签名验证成功,plan=PRO
初始化完成,计划: PRO
刷新(trigger=startup_license_init): plan=PRO, license_ready=true
```

---

## 4. 第二步:二进制字符串分析

### 4.1 提取 IPC 命令

```python
import re
with open('TargetApp.exe', 'rb') as f:
    data = f.read()
text = data.decode('utf-8', errors='replace')
pattern = r'\b\w+_command\b'
commands = set(re.findall(pattern, text))
for cmd in sorted(commands):
    print(cmd)
```

输出:

```
ai_command
ai_providers_command
asr_model_command
dataset_command
file_transcribe_command
hotword_command
license_command
mcp_command
onboarding_command
permission_command
recording_command
save_ui_settings_command
ticket_command
user_command
window_command
```

### 4.2 用户认证命令

```
send_email_code_command
email_login_command
get_current_user_command
get_cached_user_command
logout_command
bind_ticket_command
trigger_device_activation_command
```

### 4.3 错误文案

```
error.auth.unauthorized: 未授权的请求,请先登录
error.auth.login_failed: 登录失败,请重试
error.ticket.login_required: 请先登录才能绑定 Ticket
```

### 4.4 环境变量

```
VOCO_API_BASE_URL=https://targetapp.com/api/v1
```

---

## 5. 第三步:映射认证流程

```
用户输入邮箱 → POST /users/send-email-code
→ POST /users/email-login (email + code)
→ 服务端返回 JWT Token
→ 保存 Token 到 user_token.json
→ GET /users/info (携带 Bearer Token)
→ 获取用户信息缓存到 user_cache.json
```

启动时:
```
→ get_cached_user_command(读取本地缓存)
   ├── 缓存存在、isLoggedIn=true → 显示已登录
   └── 不存在/无效 → 显示未登录
→ 后台 get_current_user_command(在线验证 Token)
   ├── Token 有效 → 更新缓存
   └── 无效/网络不通 → 根据 isOffline 决定行为
```

---

## 6. 第四步:数据结构逆向

通过搜索 `struct Xxx with N elements` 还原结构体:

```python
# 在二进制中可找到:
struct TokenStorage with 2 elements
struct Jwt with 2 elements
struct JwtPayload with 1 element
struct UserInfoResponse with 4 elements
struct UserCache with 5 elements
struct SignedLicense with 6 elements
struct SignedLicenseResponse with 2 elements
```

### 还原的结构

**TokenStorage**(2 字段):
```rust
struct TokenStorage {
    token: String,       // JWT 字符串
    token_type: String,// "Bearer"
}
```

**UserInfoResponse**(4 字段):
```rust
struct UserInfoResponse {
    tokenType: String,
    email: String,
    expireAt: u64,
    plan: String,      // FREE / PRO / ULTRA
}
```

**UserCache**(5 字段):
```rust
struct UserCache {
    userId: String,
    email: String,
    lastSyncTime: u64,
    isLoggedIn: bool,
    isOffline: bool,
}
```

**SignedLicense**(6 字段):
```rust
struct SignedLicense {
    issuedAt: String,
    machineId: String,
    motherboardUuid: String,
    cpuBrand: String,
    plan: String,
    expireAt: String,
}
```

**磁盘存储格式**:
```rust
struct SignedLicenseStorage {
    signedLicenseBase64: String,// SignedLicense JSON 的 Base64
    signature: String,            // RSA-SHA256 签名的 Base64
}
```

关键发现:**签名对象是 `signedLicenseBase64` 字符串本身的 UTF-8 字节,不是解码后的数据**。

---

## 7. 第五步:提取编译后的 RSA 公钥

### 7.1 查找原理

Rust 代码中,RSA 公钥以 PEM 格式的字符串字面量硬编码在二进制中。由于字符串字面量在编译后以明文形式保留,我们可以直接搜索 PEM 标记来定位。

PEM 格式标记:
```
-----BEGIN PUBLIC KEY-----
```

### 7.2 搜索方法

方法一:直接在二进制中搜索 PEM 标记

```powershell
$bytes = ::ReadAllBytes("TargetApp.exe")
$text = ::UTF8.GetString($bytes)
$idx = $text.IndexOf("-----BEGIN PUBLIC KEY-----")
Write-Host "公钥位置: 0x$('{0:X}' -f $idx) ($idx)"
```

方法二:用 Python 搜索

```python
with open('TargetApp.exe', 'rb') as f:
    data = f.read()

marker = b'-----BEGIN PUBLIC KEY-----'
idx = data.find(marker)
print(f'公钥位置: 0x{idx:X} ({idx})')

# 提取公钥全文 (450 字节固定长度)
key_bytes = data
pubkey_pem = key_bytes.decode('ascii', errors='replace').rstrip('\x00 ')
print(pubkey_pem)
```

### 7.3 公钥的关键特征

| 属性 | 值 |
|------|-----|
| **偏移量** | `0x23F8C91`(十进制 37731857) |
| **格式** | PEM(`-----BEGIN PUBLIC KEY-----` ... `-----END PUBLIC KEY-----`) |
| **固定长度** | **450 字节**(不足部分用空格 `0x20` 填充) |
| **算法** | RSA 2048 位 |
| **换行** | 每行 64 个 Base64 字符,`\n` 换行 |

### 7.4 出厂内置公钥

```
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsFVgzHidHKm3n87riq+c
c4weMZA9TIPmO9bCx7lFw7pkSsO/8GahZgR0U+0PvokhWSuFlF6uwSFTG5kJ4SF9
+bLgyw7atgsFVbaM9DxcrKqcgt5gc3w8FPiV9Pc9ChqZZp4cmFKAhTQt33lNzGZm
gvCT2wwL8GEeGIbTU1pjmXmJswjwiYEIhHQ8Tb9YOSaFKjgPF6SxB7LGLoawfgpJ
9cCCbN7iFc4hUuRSupnYajCGaiKszSah/5WI7cSbHijSY4VTgEz6YEcZwy7r2aFI
7BUUKvh9qCpLu61AFxxXKFQ1p5v94oTYCX1kc31hRhXgECJ07uhSJ3LUFfTCC5wr
nwIDAQAB
-----END PUBLIC KEY-----
```

---

## 8. 第六步:替换公钥与生成许可证

### 8.1 核心原理

1. 应用启动时读取 `signed_license.json`
2. 提取 `signedLicenseBase64` 和 `signature` 字段
3. 用内嵌 RSA 公钥验证签名 (PKCS1v1.5 SHA256)
4. 验证通过 → 读取 `plan` 字段决定功能级别
5. 验证失败 → 回退到 FREE

**破解方法**:用自己生成的 RSA 密钥对**替换内嵌公钥**,然后用私钥签发许可证。

### 8.2 签名验证流程详解

```
Step 1 - Base64 解码 signature
signature_bytes = Base64Decode(signature)
失败 → "Signature Base64 解码失败"

Step 2 - RSA 验证
用内嵌公钥验证:
    VerifyData(
      data = UTF8Bytes(signedLicenseBase64),← 关键:签名对象是 Base64 字符串的 UTF-8 字节
      signature = signature_bytes,
      hashAlgorithm = SHA256,
      padding = PKCS1v1.5
    )
失败 → "SignedLicense 签名验证失败: verification error"

Step 3 - Base64 解码许可证数据
license_json_bytes = Base64Decode(signedLicenseBase64)
license_data = JSONParse(license_json_bytes)
失败 → "SignedLicense JSON 解析失败"

Step 4 - 设备匹配验证
比较 license_data.machineId == 本机 machineId
比较 license_data.motherboardUuid == 本机 motherboardUuid
比较 license_data.cpuBrand == 本机 cpuBrand
不匹配 → "设备不匹配"

Step 5 - 有效期检查
比较 license_data.expireAt > 当前时间
已过期 → "License 已过期"

Step 6 - 通过
输出 plan = license_data.plan
```

注意点:
- 签名对象是 **Base64 编码字符串本身的 UTF-8 字节**,不是 JSON 对象,也不是 Base64 解码后的数据
- 许可证中的设备信息必须与当前机器匹配

### 8.3 生成密钥对

```powershell
Add-Type -AssemblyName System.Security.Cryptography

$rsa = ::Create(2048)

# 导出公钥 PEM(450 字节)
$pubKeyRaw = $rsa.ExportSubjectPublicKeyInfo()
$pubKeyB64 = ::ToBase64String($pubKeyRaw)
$pubKeyPem = "-----BEGIN PUBLIC KEY-----`n"
for ($i = 0; $i -lt $pubKeyB64.Length; $i += 64) {
    $rem = ::Min(64, $pubKeyB64.Length - $i)
    $pubKeyPem += $pubKeyB64.Substring($i, $rem) + "`n"
}
$pubKeyPem += "-----END PUBLIC KEY-----"

# 导出私钥
$privKeyRaw = $rsa.ExportPkcs8PrivateKey()
$privKeyB64 = ::ToBase64String($privKeyRaw)
$privKeyPem = "-----BEGIN PRIVATE KEY-----`n"
for ($i = 0; $i -lt $privKeyB64.Length; $i += 64) {
    $rem = ::Min(64, $privKeyB64.Length - $i)
    $privKeyPem += $privKeyB64.Substring($i, $rem) + "`n"
}
$privKeyPem += "-----END PRIVATE KEY-----"

Write-Host $pubKeyPem
```

### 8.4 替换 EXE 中的公钥

```powershell
$exeBytes = ::ReadAllBytes("TargetApp.exe")

# 搜索 PEM 起始标记
$marker = ]@(0x2D,0x2D,0x2D,0x2D,0x2D,0x42,0x45,0x47,0x49,0x4E,0x20,0x50,0x55,0x42,0x4C,0x49,0x43,0x20,0x4B,0x45,0x59,0x2D,0x2D,0x2D,0x2D,0x2D)
# ASCII: "-----BEGIN PUBLIC KEY-----"

$pkOffset = -1
for ($i = 0; $i -lt $exeBytes.Length - $marker.Length; $i++) {
    $match = $true
    for ($j = 0; $j -lt $marker.Length; $j++) {
      if ($exeBytes[$i + $j] -ne $marker[$j]) { $match = $false; break }
    }
    if ($match) { $pkOffset = $i; break }
}

Write-Host "公钥偏移: 0x$('{0:X}' -f $pkOffset)"

# 新公钥字节
$pubKeyPemBytes = ::ASCII.GetBytes($pubKeyPem)

# 替换(450 字节固定)
for ($j = 0; $j -lt $pubKeyPemBytes.Length; $j++) {
    $exeBytes[$pkOffset + $j] = $pubKeyPemBytes[$j]
}
for ($j = $pubKeyPemBytes.Length; $j -lt 450; $j++) {
    $exeBytes[$pkOffset + $j] = 0x20# 空格填充
}

::WriteAllBytes("TargetApp.exe", $exeBytes)
Write-Host "公钥替换完成!"
```

### 8.5 采集本机设备指纹

```powershell
$machineId = (Get-CimInstance Win32_ComputerSystemProduct).UUID
$motherboardUuid = (Get-CimInstance Win32_BaseBoard).SerialNumber
$cpuBrand = (Get-CimInstance Win32_Processor).Name

Write-Host "machineId      = $machineId"
Write-Host "motherboardUuid = $motherboardUuid"
Write-Host "cpuBrand      = $cpuBrand"
```

### 8.6 生成签名许可证

```powershell
# 构建 SignedLicense JSON(字段顺序必须严格匹配 Rust struct)
$licenseObj = @{
    issuedAt         = (Get-Date -Format "yyyy-MM-ddTHH:mm:ssZ")
    machineId      = $machineId
    motherboardUuid= $motherboardUuid
    cpuBrand         = $cpuBrand
    plan             = "PRO"
    expireAt         = "2099-12-31T23:59:59Z"
}
$licenseJson = $licenseObj | ConvertTo-Json -Compress

# Base64 编码
$signedLicenseBase64 = ::ToBase64String(::UTF8.GetBytes($licenseJson))

# 签名(签名对象是 Base64 字符串的 UTF-8 字节,不是 JSON 也不是解码后数据)
$base64StringBytes = ::UTF8.GetBytes($signedLicenseBase64)
$signature = $rsa.SignData(
    $base64StringBytes,
    ::SHA256,
    ::Pkcs1
)
$signatureBase64 = ::ToBase64String($signature)

# 构造 SignedLicenseStorage
$storageObj = @{
    signedLicenseBase64 = $signedLicenseBase64
    signature         = $signatureBase64
}
$storageJson = $storageObj | ConvertTo-Json -Compress

# 部署到 %APPDATA%
Set-Content -Path "$env:APPDATA\TargetApp\signed_license.json" -Value $storageJson -Encoding UTF8

Write-Host "许可证已部署!"
Write-Host "内容: $storageJson"
```

---

## 9. 第七步:伪造本地登录缓存

### 9.1 为什么要伪造登录

即使激活了 PRO 许可,部分 AI 后处理功能可能仍要求登录状态。前端的登录状态由 `user_cache.json` 中的 `isLoggedIn` 字段决定。

### 9.2 伪造 user_cache.json

```json
{
"userId": "local_user",
"email": "admin@localhost",
"lastSyncTime": 9999999999999,
"isLoggedIn": true,
"isOffline": true
}
```

关键:
- `isLoggedIn: true` — 前端据此显示已登录
- `isOffline: true` — 即使在线验证失败也不退出登录

### 9.3 伪造 user_token.json

```json
{
"token": "any_jwt_token_string",
"token_type": "Bearer"
}
```

### 9.4 部署命令

```powershell
$dir = "$env:APPDATA\TargetApp"

Set-Content "$dir\user_cache.json" -Value '{"userId":"local_user","email":"admin@localhost","lastSyncTime":9999999999999,"isLoggedIn":true,"isOffline":true}' -Encoding UTF8

Set-Content "$dir\user_token.json" -Value '{"token":"any_jwt_token_string","token_type":"Bearer"}' -Encoding UTF8

Write-Host "登录缓存已部署,重启应用后生效"
```

---

## 10. 完整激活脚本

以下一键脚本完成全部操作:生成密钥、替换公钥、采集设备指纹、生成签名许可证、部署登录缓存。

```powershell
# activate_all.ps1 - 一键激活脚本
# 以管理员身份运行

param(
    $ExePath = "C:\Program Files\TargetApp\TargetApp.exe",
    $Plan = "PRO",
    $ExpireAt = "2099-12-31T23:59:59Z"
)

Add-Type -AssemblyName System.Security.Cryptography

# 1. 停止进程
Get-Process -Name TargetApp -ErrorAction SilentlyContinue | Stop-Process -Force
Start-Sleep -Seconds 2

# 2. 生成 RSA 密钥对
$rsa = ::Create(2048)

$pubKeyRaw = $rsa.ExportSubjectPublicKeyInfo()
$pubKeyB64 = ::ToBase64String($pubKeyRaw)
$pubKeyPem = "-----BEGIN PUBLIC KEY-----`n"
for ($i = 0; $i -lt $pubKeyB64.Length; $i += 64) {
    $rem = ::Min(64, $pubKeyB64.Length - $i)
    $pubKeyPem += $pubKeyB64.Substring($i, $rem) + "`n"
}
$pubKeyPem += "-----END PUBLIC KEY-----"
$pubKeyPemBytes = ::ASCII.GetBytes($pubKeyPem)

# 3. 替换 EXE 公钥
$exeBytes = ::ReadAllBytes($ExePath)
$marker = ]@(0x2D,0x2D,0x2D,0x2D,0x2D,0x42,0x45,0x47,0x49,0x4E,0x20,0x50,0x55,0x42,0x4C,0x49,0x43,0x20,0x4B,0x45,0x59,0x2D,0x2D,0x2D,0x2D,0x2D)
$pkOffset = -1
for ($i = 0; $i -lt $exeBytes.Length - $marker.Length; $i++) {
    $match = $true
    for ($j = 0; $j -lt $marker.Length; $j++) {
      if ($exeBytes[$i + $j] -ne $marker[$j]) { $match = $false; break }
    }
    if ($match) { $pkOffset = $i; break }
}
for ($j = 0; $j -lt $pubKeyPemBytes.Length; $j++) { $exeBytes[$pkOffset + $j] = $pubKeyPemBytes[$j] }
for ($j = $pubKeyPemBytes.Length; $j -lt 450; $j++) { $exeBytes[$pkOffset + $j] = 0x20 }
::WriteAllBytes($ExePath, $exeBytes)

# 4. 采集设备指纹
$machineId = (Get-CimInstance Win32_ComputerSystemProduct).UUID
$motherboardUuid = (Get-CimInstance Win32_BaseBoard).SerialNumber
$cpuBrand = (Get-CimInstance Win32_Processor).Name

# 5. 生成签名许可证
$licenseObj = @{
    issuedAt         = (Get-Date -Format "yyyy-MM-ddTHH:mm:ssZ")
    machineId      = $machineId
    motherboardUuid= $motherboardUuid
    cpuBrand         = $cpuBrand
    plan             = $Plan
    expireAt         = $ExpireAt
}
$licenseJson = $licenseObj | ConvertTo-Json -Compress
$signedLicenseBase64 = ::ToBase64String(::UTF8.GetBytes($licenseJson))
$base64StringBytes = ::UTF8.GetBytes($signedLicenseBase64)
$signature = $rsa.SignData($base64StringBytes, ::SHA256, ::Pkcs1)
$signatureBase64 = ::ToBase64String($signature)
$storageObj = @{ signedLicenseBase64 = $signedLicenseBase64; signature = $signatureBase64 }
$storageJson = $storageObj | ConvertTo-Json -Compress

$appDataDir = "$env:APPDATA\TargetApp"
if (-not (Test-Path $appDataDir)) { New-Item -ItemType Directory -Path $appDataDir -Force | Out-Null }
Set-Content -Path "$appDataDir\signed_license.json" -Value $storageJson -Encoding UTF8

# 6. 伪造登录缓存
Set-Content "$appDataDir\user_cache.json" -Value '{"userId":"local_user","email":"admin@localhost","lastSyncTime":9999999999999,"isLoggedIn":true,"isOffline":true}' -Encoding UTF8
Set-Content "$appDataDir\user_token.json" -Value '{"token":"any_jwt_token_string","token_type":"Bearer"}' -Encoding UTF8

# 7. 保存密钥
$keyDir = "$env:USERPROFILE\targetapp_keys"
if (-not (Test-Path $keyDir)) { New-Item -ItemType Directory -Path $keyDir -Force | Out-Null }
Set-Content "$keyDir\private.pem" -Value $privKeyPem
Set-Content "$keyDir\public.pem" -Value $pubKeyPem

Write-Host "=========================================="
Write-Host "激活完成!"
Write-Host "计划: $Plan"
Write-Host "过期: $ExpireAt"
Write-Host "设备: $machineId"
Write-Host "=========================================="
Write-Host "请启动 TargetApp 并查看日志验证。"
```

---

## 11. 总结

### 关键突破口

| 突破口 | 说明 |
|--------|------|
| **PEM 公钥明文嵌入** | 直接在二进制中搜索 `-----BEGIN PUBLIC KEY-----` 定位,450 字节固定长度 |
| **RSA 签名算法** | PKCS1v1.5 SHA256,签名对象是 Base64 字符串的 UTF-8 字节 |
| **设备指纹可控** | 许可证需包含本机 machineId / motherboardUuid / cpuBrand |
| **客户端登录状态** | `user_cache.json` 的 `isLoggedIn` + `isOffline` 直接控制前端显示 |

### 公钥提取方法论

1. 在 EXE 中搜索 `-----BEGIN PUBLIC KEY-----` 定位公钥起始
2. 确认固定长度 450 字节(`-----END PUBLIC KEY-----` 后可能跟空格填充)
3. 验证提取的公钥:用 ::ImportSubjectPublicKeyInfo() 导入测试
4. 用自己的 RSA 2048 密钥对替换
5. 用新私钥签发许可证

ljkgpxs 发表于 2026-6-30 15:42

msmvc 发表于 2026-6-30 15:37
私钥你自己保存
公钥用于检验

程序要拿私钥做加密操作,所以才放进去的,另外还用到了pgp公钥用来验签,无论放私钥还是公钥,如果被分析出来了,还是免不了密钥被替换,所以想知道存P和Q这种形式,会不会增加逆向难度,或者有没有别的改进办法

ljkgpxs 发表于 2026-6-30 15:16

请教楼主一个问题,我自己也做rust程序的,也会有授权相关的内容,考虑到程序需要离线使用,所以程序内部塞了rsa密钥,但是不像楼主分析的这个app那样,他是明文存储,我存的是rsa密钥所需的P和Q(两串很长的数字,使用两个无符号8位数组存储),程序启动时根据P和Q计算出私钥和公钥,针对这种形式,不知道对攻破程序有没有什么思路,会不会增加逆向难度,如果这种形式作用不大,以逆向工程师的思维来看,我该如何改进

Ignorantlu 发表于 2026-6-29 17:15

舒服的内容和排版😊

chenfugui 发表于 2026-6-30 08:58

虽然看不懂,但是感觉很牛逼

博爵 发表于 2026-6-30 12:02

感谢分享,看着不错

msmvc 发表于 2026-6-30 14:31

思路清晰,排版整齐,看着真舒服

另外第一次见使用powershell代替C#编码的

youyu48 发表于 2026-6-30 14:43

看不懂,感觉很牛逼!

msmvc 发表于 2026-6-30 15:37

ljkgpxs 发表于 2026-6-30 15:16
请教楼主一个问题,我自己也做rust程序的,也会有授权相关的内容,考虑到程序需要离线使用,所以程序内部塞 ...

私钥你自己保存
公钥用于检验

deeppy 发表于 2026-6-30 17:01

楼主对ps1代码用得还是很666
页: [1] 2 3 4
查看完整版本: 某rust开发语音识别软件获取专业版授权