Chrono Mock
JęzykPolski

Skąd wiesz, że zadziałało?

Przy każdym innym narzędziu tego typu - nie wiesz. Aplikacja startuje, pokazuje jakąś datę, a ty zakładasz. Tyle że może czytać zegar drogą, której nic nie przechwyciło - i wtedy „test przeszedł" nie znaczy zupełnie nic.

Porażka, która nie ma objawów

Narzędzie do przesuwania czasu, które pominęło kanał, nie wywala się i nie ostrzega. Aplikacja po prostu czyta prawdziwą datę tą jedną funkcją, której nikt nie podpiął, i zachowuje się dokładnie tak, jak zachowałaby się w zwykłe popołudnie. Test świeci na zielono. Błąd trafia do klienta.

RunAsDate pisze o tym uczciwie we własnej dokumentacji - że „nie wpłynie na aplikacje, które biorą bieżącą datę i godzinę z innego źródła" - i zostawia cię z ustaleniem, czy twoja jest jedną z nich. To narzędzie traktuje to samo zdanie jako coś do zmierzenia.

Co sesja naprawdę raportuje

Prawdziwe wyjście, z uruchomienia narzędzia na jednej z własnych sond tego projektu - takiej, która celowo otwiera połączenie sieciowe:

$ 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)
            ... i każdy pozostały kanał, łącznie z nigdy niewywołanymi
  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)

Dwa ostatnie bloki są sednem. Nic tu nie zawiodło - werdykt brzmi works - a raport i tak sam z siebie mówi, że ta aplikacja rozmawiała z siecią, zanim skończyła, więc lokalny hook może nie być całą historią. To zdanie jest różnicą między testem, któremu można ufać, a takim, któremu nie można.

Ekran wyniku Chrono Mock z rozwiniętą sekcją mówiącą, co aplikacja odczytała: tabela z kolumnami funkcja, źródło odczytu i liczba odczytów. NtCreateUserProcess jest oznaczona jako czytana z prawdziwego zegara z rozmysłu, a connect jako czytana z prawdziwego zegara z rozmysłu i podpięta późno, podczas gdy pozostałe wiersze - GetSystemTimeAsFileTime, NtQuerySystemTime, GetTimeZoneInformation i reszta - czytają z fałszywego zegara, każdy z własną liczbą. Pod tabelą stoją dwa ostrzeżenia.
Ta sama odpowiedź w oknie. Każda funkcja, o którą aplikacja mogła zapytać, to, z czego naprawdę czytała, i ile razy - łącznie z tymi zostawionymi na prawdziwym zegarze z rozmysłu, które są nazwane, a nie po cichu policzone jako sukces.

Cztery werdykty i kod wyjścia do każdego

Werdykt powstaje z wnętrza działającego procesu, zanim zaczniesz pracować, i jest zarazem kodem wyjścia narzędzia, więc potok może się na nim rozgałęzić.

WerdyktKodZnaczenie
działa0każdy proces, który czytał czas, widział zegar sesji
działa częściowo10część kanałów objęta, część nie - nieobjęte są wypisane
nie działa11podmiana nie dotarła do kluczowych kanałów
nieokreślony4nie dało się ustalić pokrycia - uczciwe „nie wiem"

Gdy werdykt brzmi „nie działa", proces jest natychmiast zatrzymywany, zanim zdążysz z nim cokolwiek zrobić. Werdykt nie może wyprzedzić uruchomienia, bo jedynym źródłem prawdy jest odczyt z wnętrza działającego procesu. Gwarantowane jest to, że poznasz prawdę, zanim zaczniesz pracować - a nie to, że skazana sesja nigdy nie wystartuje. Można to nadpisać i lecieć dalej, a sesja jest wtedy oznaczona jako nierzetelna wszędzie, gdzie się pojawia, łącznie z eksportowanym dowodem.

Pozostałe kody mówią, że zawiódł sam przebieg, a nie podmiana - błąd użycia, cel, którego nie dało się uruchomić, przebieg ucięty w połowie, inna sesja już działa - i żaden z nich nie jest werdyktem. Pełna tabela jest w dokumentacji wiersza poleceń.

Objęte, obserwowane, nieobjęte

Kanały dzielą się na trzy kubełki, a najciekawszy jest środkowy.

  • Objęte - podpięte i podmienione. Aplikacja zapytała i dostała zegar sesji. Licznik mówi, ile razy.
  • Obserwowane - przechwycone, policzone i celowo zostawione przy prawdzie. Są tu czekania na obiekty, bo skrócenie limitu czasu mogłoby udawać timeout na prawdziwym uchwycie, i timer multimedialny, bo jego skalowanie przesunęłoby dźwięk i MIDI - oba obserwowane, gdy sesja skaluje oś czasu trwania. Połączenia sieciowe i jedna ścieżka tworzenia procesów są tu w każdej sesji, bo to dowody, a nie zegary. Licznika wysokiej rozdzielczości w tej grupie nie ma: zostaje nietknięty, dopóki nie poprosisz o niego przez --scale-qpc, a wtedy jest objęty.
  • Nieobjęte - kanał, który aplikacja czytała, a podmiana do niego nie dotarła. To właśnie zamienia werdykt w działa częściowo i jest wypisywane z nazwy, a nie zwijane do podsumowania.

Zero wywołań też jest informacją: kanał był podpięty, a aplikacja nigdy nie zapytała. Dlatego raport je wypisuje, zamiast chować.

Czego audyt nie widzi

Raport jest uczciwy co do pokrycia, co znaczy, że musi być uczciwy co do własnych granic. Te źródła są czytane bez żadnego wywołania funkcji do przechwycenia albo w ogóle poza procesem:

  • bezpośrednie odczyty współdzielonej strony danych trybu użytkownika - wartość leży w pamięci, więc nie ma czego podpiąć. Rzadkie w zwykłych aplikacjach, częste w kodzie licencyjnym i antytamperowym
  • bezpośrednie wywołania systemowe omijające standardowe eksporty - ten sam wzorzec
  • czas pobierany poza procesem, przez WMI, pozaprocesowy serwer COM albo RPC
  • czas z sieci - nagłówek HTTP Date, NTP, odpowiedź API albo własny zegar serwera bazy danych
  • czas ze sterowników jądra

Przypadek sieciowy jest tym jednym, który audyt potrafi przynajmniej zauważyć, i stąd ostrzeżenie w raporcie powyżej. Dla reszty werdykt works znaczy, że kanały, które to narzędzie widzi, zostały objęte - a nie że żadna inna droga nie istnieje.

W potoku

--json wypisuje te same fakty w formie maszynowej, --report zapisuje wersję dla człowieka do pliku, a sesja z werdyktem innym niż works prowadzi ten plik banerem o nierzetelnym dowodzie, zamiast chować go na dole.

chrono run "C:\apps\test.exe" --at 2027-12-31T23:59:59 --json --report evidence.txt

Wypróbuj