Chrono Mock
语言简体中文

你怎么知道它生效了?

用其他任何同类工具,你都无从知道。应用启动了,显示出某个日期,而你只能假定一切正常。可它也许通过一条没有被拦截的途径读取时钟——到那时,“测试通过”就毫无意义。

没有症状的失败

漏掉一个通道的时间偏移工具既不会崩溃,也不会警告。应用只是通过那个没人挂钩的函数读到了真实日期,然后像任何一个普通的下午那样照常运行。测试变绿了,缺陷就这样上线了。

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)

最后两块才是重点。这里没有任何东西失败——判定是生效——可报告仍然主动告诉你,这个应用在结束之前与网络通信过,所以本地钩子可能不是事情的全部。这句话,就是一个你能信任的测试和一个你不能信任的测试之间的区别。

Chrono Mock 的结果界面,What the application read 已展开:一张包含 Function、Read from 和 Reads 列的表。NtCreateUserProcess 被标为按设计读取真实时钟,connect 被标为按设计读取真实时钟且较晚才挂钩,其余各行——GetSystemTimeAsFileTime、NtQuerySystemTime、GetTimeZoneInformation 等——读取的是伪造的时钟,每一行都有各自的次数。表格下方有两条警告。
窗口里的同一个答案。应用本来可能询问的每一个函数、它实际读取的来源,以及次数——包括那些有意留在真实时钟上的函数,它们会被点名,而不是被悄悄算作通过。

四种判定,各有一个退出码

判定是在运行中的进程内部得出的,早于你开始工作,并且它就是工具自己的退出码,因此流水线可以据此分支。

判定退出码含义
生效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

亲自试试