Как понять, что подмена сработала?
С любым другим инструментом такого рода — никак. Приложение запускается, показывает какую-то дату, и вы предполагаете. Но оно может читать часы по пути, который ничто не перехватило, — и тогда «тест пройден» не значит вообще ничего.
Сбой, у которого нет симптомов
Инструмент сдвига времени, пропустивший канал, не падает и не предупреждает. Приложение просто читает настоящую дату через ту единственную функцию, которую никто не перехватил, и ведёт себя точно так же, как в любой обычный день. Тест зеленеет. Ошибка уходит в релиз.
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