欢迎光临
我们一直在努力

一机一码授权:软件试用期怎么防止改系统时间绕过(时间水位实现,C#)

一机一码授权:软件试用期怎么防止改系统时间绕过(时间水位实现,C#)

试用期和到期控制是离线授权里最容易被戳破的一环,而且破法简单到让人无语:
把系统时间调回上个月。

这篇文章讲清楚为什么"读一次本机时间"一定会失效,以及我们最后用的方案:
时间水位(Time Vault)——记录这台机器上见过的最晚时间,校验时用 max(本机时间, 水位)。
附可跑的代码结构和一条"反证测试"。


一、天真的写法,以及它是怎么被绕过的

// 常见写法:装个文件记录首次运行,然后算天数
var firstRun = File.ReadAllText(trialFile);
var days = (DateTime.Now DateTime.Parse(firstRun)).TotalDays;
if (days > 30) { 弹窗("试用已结束"); return; }

两个场景直接失效:

  • 把系统时间调回去:DateTime.Now 变小,days 跟着变小,30 天永远用不完。
  • 删掉那个文件:试用期重新开始。除非你到处藏副本,但藏副本这件事本身就是运维负担。
  • 第二点还能靠多位置存储缓解,第一点只能靠不信任本机时钟来解决。

    二、时间水位的核心思路

    不要让"当前时间"决定一切,而是维护一份记录:

    这台机器历史上见过的最晚时间(水位线)。

    每次启动:

    有效时间 = max(本机时间, 水位线)
    如果 本机时间 > 水位线 → 把水位线抬到本机时间(时间只能前进)
    回拨检测:本机时间 < 水位线 – 2 小时 → 记一次回拨

    于是"把时间调回上个月"这件事毫无收益:有效时间不会跟着回去,过期就是过期。
    而且客户照常能用——我们并不因为检测到回拨就停机,只是回拨不再能延长试用。

    三、记录长什么样

    就是一个带几个字段的文本(我们用的格式带自校验,防止被手改成看起来合法的内容):

    format = MLV/1
    product = UperDemo
    highWater= 2026-09-17T14:22:31Z ← 见过的最大时间(UTC)
    pendingJump= 2026-09-18T09:00:00Z ← 待确认的"向前大跳",见第四节
    rollbacks= 3 ← 累计回拨次数(诊断用)

    关键点:只用 UTC。本地时间会被时区、夏令时、手动改时区影响,
    而 UTC 不受这些干扰,少一整类诡异 bug。

    四、两个阈值:为什么是 2 小时和 30 天

    守卫不能太敏感,否则会误伤正常客户。我们把规则定成三条:

    情况判定处理
    本机时间比水位早,但差距 ≤ 2 小时 正常漂移 不计数、不处理(NTP 同步、手动校时都会这样)
    本机时间比水位早超过 2 小时 回拨 rollbacks++,水位不降
    本机时间比水位晚超过 30 天 可疑前跳 先不接受,记下 pendingJump,等下一次运行再确认

    第三条是这个方案里最值得抄的一点。

    客户主板电池没电、或者手滑把年份从 2026 设成 2062,如果你当场就把水位抬到 2062,
    客户的授权就永久性地废了——他改回正确时间也救不回来,只能找你重新签发。
    永久损坏一个客户的授权,代价比少赚一单大得多。

    所以规则是:大幅前跳只在连续两次运行都观察到相近的时间(允许 1 天内差异)时才接受;
    只看到一次就沿用旧水位、把这次记成 pendingJump。真跑了一个月?
    第二次运行时间自然对得上,正常推进。

    五、状态存哪里:四个位置 + 取"最晚的那份"

    单点存储一定会被删。我们写到四个位置:

    %ProgramData%\\<产品>\\clock.dat ← 所有用户共用,工厂电脑常有多账号
    %LOCALAPPDATA%\\<产品>\\clock.dat
    %APPDATA%\\<产品>\\clock.dat
    HKCU\\Software\\<产品> (注册表值)

    读的时候不是取第一个能读到的,而是全读一遍、应用"水位只前进"的合并规则:

    TimeVaultRecord best = null;
    foreach (var text in _store.ReadAll()) { // 四个位置全读
    var record = TimeVaultRecord.TryDecode(text, _productId);
    if (record == null) continue; // 解不开的副本直接忽略
    if (best == null ||
    record.HighWaterUtc > best.HighWaterUtc ||
    (record.HighWaterUtc == best.HighWaterUtc && record.RollbackCount > best.RollbackCount)) {
    best = record; // 取水位最高的那份
    }
    }

    这么写以后,"删掉其中一个文件"没有任何收益——另外三个位置里水位最高的那份说了算。
    需要注意的是 ProgramData 可能写不进去(非管理员 + 目录被加固),
    所以写入失败要静默跳过、由其他位置兜底,绝对不能让激活流程因此报错。

    六、一定要写一条"反证测试"

    只测"守卫生效"是测不出什么的,因为关掉守卫时那条测试照样绿。
    我们专门写了一条证明守卫必要的测试:

    // 关掉时间守卫,构造一个"改过系统时间"的授权场景
    var unguarded = new LicenseOptions("UperDemo") { UseTimeGuard = false };
    var bypassed = LicenseRuntime.Initialize(unguarded);
    // 断言:没有守卫时,这个场景**确实**会被绕过
    Assert(bypassed.Status == LicenseStatus.Licensed);

    这条测试的价值在于:它会在有人"顺手把守卫优化掉"的时候立刻失败。
    一个能失败的测试,才是真的在保护东西。

    七、边界:它挡得住谁

    • 挡得住:改系统时间、改时区、删状态文件、重命名目录 —— 这些是最常见的"劝退级"绕过。
    • 挡不住:会逆向、能改字节、能直接 patch 掉校验分支的人。
    • 物理限制:把四个状态文件全部删掉、且从没在系统里跑过带守卫的版本,仍可能重置。
      这是纯离线方案的物理限制,谁跟你说"离线授权绝对破解不了",那是话术,不是技术。

    顺便说一句:检测到回拨时不要停机。停机伤的是正常客户(主板电池没电、改了时区),
    而对真想绕的人毫无作用。把回拨计数记录下来、在界面上提示"检测到系统时间异常"就够了。

    八、上线前建议实测这 5 条

  • 试用第 3 天把系统时间调到上个月 → 试用期不能变长;
  • 把系统时间调到 2030 年 → 不能立刻把授权烧掉(应该被 pendingJump 拦下);
  • 连续两次在 2030 年运行 → 水位正常抬升(说明确认逻辑生效);
  • 删掉 %ProgramData% 里那份、只留 LOCALAPPDATA% → 试用期不能重置;
  • 非管理员账户下运行 → 写不进去时静默降级,不能弹异常或崩溃。
  • 第 5 条最容易被忽略,而它恰恰是工厂现场最常见的情况。


    (文中这套流程的评估包我传到 CSDN 下载区了,免费,含 76 项自检和完整的"机器码 → 申请 → 签发 → 激活"演示:
    https://download.csdn.net/download/jhjjbb/93466800 )

    文中的代码是简化片段。完整实现我在自己的项目上跑通了一版,需要的话可以私信我。

    赞(0)
    未经允许不得转载:171主机测评 » 一机一码授权:软件试用期怎么防止改系统时间绕过(时间水位实现,C#)
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址