最近在 Windows 上排查 LetsVPN 的一个现象:明明已经删掉客户端数据库,重新打开程序后却还是会自动回到之前的旧账号,连被封过的账号状态也会一起恢复。

这个现象第一眼很像“服务端根据设备指纹强行认回旧号”,但这次实际结合 ProcMon 日志、本地 SQLite 文件和注册表状态一起看,结论更接近下面这句话:

LetsVPN 的旧账号恢复,至少在当前版本里,不是单靠服务端完成的,而是强依赖本地持久化状态。关键状态不只在 Lets-Data.db3,也在 HKCU\Software\Lets 下面的一组加密注册表值里。

一、问题现象

排查开始时,现象非常具体:

  1. 客户端曾经登录过某个旧账号。
  2. 删除本地数据库后,重新启动客户端。
  3. 客户端没有变成真正的“全新状态”,而是又自动恢复到了旧账号。

这就带来两个核心问题:

  1. LetsVPN 到底把账号状态保存在什么位置。
  2. 自动恢复旧号时,真正起决定作用的是本地缓存,还是服务端设备识别。

二、排查思路

为了回答这两个问题,我重点观察了三类行为:

  1. 客户端的网络访问。
  2. Lets-Data.db3 的文件访问。
  3. HKCU\Software\Lets 和设备标识相关注册表项的访问。

这次使用的是 ProcMon 导出的 CSV 日志,重点筛选 LetsPRO.exe 的以下行为:

  1. TCP ConnectUDP Send
  2. ReadFileCreateFile,目标为 Lets-Data.db3
  3. RegQueryValueRegSetValue,目标为 HKCU\Software\LetsMachineGuid

三、先看到的不是 SQLite,而是注册表和设备标识

重新检查完整日志后,能确认一件事:客户端在“恢复旧号”的那段时间里确实联网了,不是纯离线逻辑。

但更关键的是时间顺序。

在典型的一次恢复流程里,先发生的是:

  1. 读取 HKCU\Software\Lets 下面的一批应用状态。
  2. 反复读取 HKLM\SOFTWARE\Microsoft\Cryptography\MachineGuid
  3. 对多个 :https 目标发起 UDP SendTCP Connect
  4. 随后才集中读取 Lets-Data.db3

这说明两点:

  1. SQLite 的确参与了恢复流程,但它不是唯一状态源。
  2. 程序在读取数据库之前,就已经开始依赖注册表状态和设备标识了。

四、HKCU\Software\Lets 下面有哪些可疑数据

在 ProcMon 里,LetsPRO.exe 反复读取或写入了下面这些值:

  1. account
  2. recordInfo
  3. userHabit
  4. qjiwx
  5. ewwqj
  6. GetDeviceInfoSendCountry
  7. GetDeviceInfoSendCountryTime
  8. LocalCountryCode

其中最值得注意的是 accountrecordInfouserHabitqjiwxewwqj 这几项。它们大多是 REG_BINARY,而且数据头部带有明显的 DPAPI 特征,也就是 Windows 用户态加密格式。

这意味着:

  1. 数据不是明文缓存。
  2. 数据和当前 Windows 用户上下文绑定。
  3. 这些值很可能就是 LetsVPN 用来恢复登录状态或设备状态的核心本地缓存。

从实验结果回看,最可疑的仍然是 qjiwxewwqj

五、MachineGuid 的作用是什么

日志里还能看到客户端多次读取:

HKLM\SOFTWARE\Microsoft\Cryptography\MachineGuid

这说明设备级标识确实参与了客户端运行逻辑。它可能用于:

  1. 设备识别。
  2. 风控。
  3. 上报设备信息。
  4. 配合本地缓存做账号恢复判断。

但就这次实验结果而言,MachineGuid 更像辅助因素,而不是唯一决定因素。因为如果服务端完全靠设备标识就能强制恢复旧号,那么在本地状态被彻底清掉之后,客户端仍应当稳定回到旧号。实际结果并不是这样。

六、决定性的验证:同时清空注册表和 SQLite

真正把结论钉死的,不是日志本身,而是最后的清理实验。

在完全退出 LetsVPN 客户端之后,我同时清除了两部分本地状态:

  1. HKCU\Software\Lets
  2. %LOCALAPPDATA%\LetsVPN\Lets-Data.db3 及其附属文件

重新启动客户端之后,结果是:

  1. 客户端成功退出了旧账号。
  2. 客户端 ID 发生了变化。
  3. 之前被封的账号状态没有再自动恢复。

这个结果说明:

旧账号之所以能“回来”,不是因为服务端单靠机器就无条件认回来了,而是因为本地仍然保留着足以恢复身份的状态。

换句话说,只删除 Lets-Data.db3 并不够,因为 HKCU\Software\Lets 里还保留着另一套关键缓存。

七、正确的清理命令

下面是一组更稳妥的 PowerShell 命令,适合用于测试“清空本地状态后,客户端是否还会自动回旧号”。

它会做三件事:

  1. 先结束进程。
  2. 先备份注册表和数据库。
  3. 再删除 HKCU\Software\LetsLets-Data.db3 相关文件。
$backup = Join-Path $env:USERPROFILE ("Desktop\\lets_backup_" + (Get-Date -Format 'yyyyMMdd_HHmmss'))
New-Item -ItemType Directory -Path $backup -Force | Out-Null

# 1. 结束客户端进程
Get-Process LetsPRO -ErrorAction SilentlyContinue | Stop-Process -Force

# 2. 备份注册表
reg export HKCU\Software\Lets (Join-Path $backup 'Lets.reg') /y | Out-Null

# 3. 备份数据库文件
$dataDir = Join-Path $env:LOCALAPPDATA 'LetsVPN'
if (Test-Path $dataDir) {
	Get-ChildItem $dataDir -Force -ErrorAction SilentlyContinue |
		Where-Object { $_.Name -like 'Lets-Data.db3*' } |
		Copy-Item -Destination $backup -Force -ErrorAction SilentlyContinue
}

# 4. 删除注册表
if (Test-Path 'HKCU:\Software\Lets') {
	Remove-Item 'HKCU:\Software\Lets' -Recurse -Force
}

# 5. 删除数据库及附属文件
if (Test-Path $dataDir) {
	Get-ChildItem $dataDir -Force -ErrorAction SilentlyContinue |
		Where-Object { $_.Name -like 'Lets-Data.db3*' } |
		Remove-Item -Force -ErrorAction SilentlyContinue
}

# 6. 验证是否删干净
'Registry Exists: ' + (Test-Path 'HKCU:\Software\Lets')
if (Test-Path $dataDir) {
	Get-ChildItem $dataDir -Force -ErrorAction SilentlyContinue |
		Where-Object { $_.Name -like 'Lets-Data.db3*' } |
		Select-Object FullName
}

如果你只想做最小化测试,也可以直接用下面这组更短的命令:

Get-Process LetsPRO -ErrorAction SilentlyContinue | Stop-Process -Force
Remove-Item 'HKCU:\Software\Lets' -Recurse -Force -ErrorAction SilentlyContinue
Get-ChildItem (Join-Path $env:LOCALAPPDATA 'LetsVPN') -Force -ErrorAction SilentlyContinue |
	Where-Object { $_.Name -like 'Lets-Data.db3*' } |
	Remove-Item -Force -ErrorAction SilentlyContinue

八、如何验证是不是本地状态导致的

建议把测试分成三步,不要一次性下结论。

1. 同时清空后启动客户端

如果这一步启动后不再自动回旧号,而且客户端生成了新的 ID,那么说明本地状态确实是关键因素。

2. 关闭并重新启动一次客户端

如果新的 ID 稳定保留,没有再次恢复旧号,说明新的本地状态已经接管了客户端身份。

3. 联网一段时间后再重启一次

如果联网后仍然不会回旧号,那么可以基本判定:至少在当前版本里,恢复旧号的主导因素就是本地持久化数据,而不是服务端单独靠设备指纹强行识别。

九、结论

这次排查最后得到的结论并不复杂,但很关键:

  1. Lets-Data.db3 不是 LetsVPN 唯一的本地状态源。
  2. HKCU\Software\Lets 下面还保存着一组关键的加密缓存。
  3. 只删除 SQLite 文件,往往不足以让客户端真正“失忆”。
  4. 同时删除注册表状态和数据库后,客户端才会真正退出旧身份并生成新的本地状态。

所以,如果你遇到的是“删库后还是会回旧号”,那问题大概率不是删库没生效,而是你删得还不够全。

更直白一点说:

让 LetsVPN 自动回旧号的,不是某一个神秘的云端开关,而是一套混合的本地持久化机制。数据库只是其中一部分,注册表缓存同样是关键。

Logo

更多推荐