你怎么知道它生效了?
用其他任何同类工具,你都无从知道。应用启动了,显示出某个日期,而你只能假定一切正常。可它也许通过一条没有被拦截的途径读取时钟——到那时,“测试通过”就毫无意义。
没有症状的失败
漏掉一个通道的时间偏移工具既不会崩溃,也不会警告。应用只是通过那个没人挂钩的函数读到了真实日期,然后像任何一个普通的下午那样照常运行。测试变绿了,缺陷就这样上线了。
RunAsDate 在自己的文档里对此很坦诚——它说自己“对从其他来源获取当前日期/时间的应用不起作用”——然后让你自己去弄清楚你的应用是不是其中之一。这个工具把同一句话当作需要测量的东西。
会话实际报告了什么
真实的输出,来自对本项目自带的一个测试探针运行该工具——这个探针会故意打开一个网络连接:
$ chrono run "...\pnet-x64.exe" --at 2038-01-19T03:14:07 --report session.txt Chrono Mock - session report target: C:\ChronoMock\tools\probes\bin\pnet-x64.exe verdict: WORKS - time substitution took effect (processes: 1) every process that read time saw the session clock session: fake clock reached 2038-01-19T03:14:10 real elapsed 3.4s, fake elapsed 3.4s exited: the target closed itself with code 0 covered channels (substituted, with call counts, 4 calls in all): - pid 9152: GetSystemTimeAsFileTime (1 call) - pid 9152: NtQuerySystemTime (3 calls) ... every other channel, including the ones never called observed channels (hooked but left real): - pid 9152: NtCreateUserProcess (0 calls) - pid 9152: connect (1 call) warnings: - the target opened a network connection - it may read time from a server, which no local hook can cover (source.network_at_start) requested: 2038-01-19T03:14:07 (zone +02:00 (host default), mode flow)
最后两块才是重点。这里没有任何东西失败——判定是生效——可报告仍然主动告诉你,这个应用在结束之前与网络通信过,所以本地钩子可能不是事情的全部。这句话,就是一个你能信任的测试和一个你不能信任的测试之间的区别。
四种判定,各有一个退出码
判定是在运行中的进程内部得出的,早于你开始工作,并且它就是工具自己的退出码,因此流水线可以据此分支。
| 判定 | 退出码 | 含义 |
|---|---|---|
| 生效 | 0 | 所有读取过时间的进程看到的都是会话时钟 |
| 部分生效 | 10 | 部分通道已覆盖,部分未覆盖——未覆盖的会逐一列出 |
| 未生效 | 11 | 替换没有到达关键通道 |
| 无法确定 | 4 | 无法确认覆盖情况——一句诚实的“我不知道” |
当判定为“未生效”时,进程会被立即停止,在你与它交互之前。判定不可能先于启动,因为唯一的事实依据,是从运行中的进程内部取得的读数。能保证的是你在开始工作之前就会得知真相——而不是注定失败的启动永远不会发生。你可以选择覆盖并继续,此后这个会话会在它出现的所有地方——包括你导出的任何证据——被标记为不可靠。
其他代码表示出问题的是运行本身,而不是替换 - 用法错误、无法启动的目标、被中途截断的运行、已在运行的另一个会话等,它们都不是判定。完整的表格在命令行参考中。
已覆盖、已观察、未覆盖
通道分为三类,而中间那一类最值得注意。
- 已覆盖——已挂钩并被替换。应用询问了,得到的是会话时钟。调用次数说明了询问的次数。
- 已观察 - 被拦截、被计数,并故意让它们如实返回。对象等待在这里,因为缩短超时可能在真实句柄上伪造超时,多媒体定时器也在这里,因为缩放它会让音频和 MIDI 的时序错位 - 这两者在会话缩放经过时间轴时才被监视。网络连接和一条进程创建路径在每个会话中都在这里,因为它们是证据,而不是时钟。高分辨率计数器不属于这一组:除非你用
--scale-qpc要求,否则它保持原样,要求之后它就被覆盖。 - 未覆盖——应用读取过、而替换没有触及的通道。正是它让判定变成部分生效,并且会逐个点名列出,而不是被概括掉。
次数为零同样是信息:通道已被挂钩,而应用从未询问过。这就是报告把它们列出来而不是隐藏的原因。
审计看不到的东西
报告对覆盖情况是诚实的,这意味着对它自身的边界也是诚实的。下面这些,读取时没有可供拦截的函数调用,或者完全发生在进程之外:
- 对共享用户模式数据页的直接读取——数值就在内存里,所以没有什么可挂钩的。在普通应用中很少见,在授权和防篡改代码中很常见
- 绕过标准导出函数的直接系统调用——同样的模式
- 在进程外通过 WMI、进程外 COM 服务器或 RPC 获得的时间
- 来自网络的时间——HTTP 的
Date头、NTP、API 响应,或者数据库服务器自己的时钟 - 来自内核驱动的时间
网络这一种,是审计至少能察觉的,这就是上面的报告里为什么有那条警告。对于其余的情形,生效的判定意味着这个工具能看到的通道已被覆盖——而不是不存在其他途径。
在流水线中
--json 以机器可读的形式输出同样的事实,--report 把给人读的版本写入文件,判定不是生效的会话,会在该文件开头放一条证据不可靠的横幅,而不是把它埋在末尾。
chrono run "C:\apps\test.exe" --at 2027-12-31T23:59:59 --json --report evidence.txt