This started as an attempt to solve a puzzle:
Some virtualization software allows a user to launch a virtual machine with its real-time clock starting ticking from custom base time. For example, a user can launch a virtual machine with base time set to 2020-12-31 23:59:59 UTC, the real-time clock inside this virtual machine will start ticking from that value, regardless of the current date and time set on a host. I use this feature to test artifacts without telling Windows to move the clock.
However, if you decide to go back and restart the same virtual machine without defining custom base time (also without Internet access and without changing the date and time settings in the running operating system), the guest operating system won’t necessary use the current date and time (as set on a host). In some cases, it will continue to run using the previously defined (future) date.
How is that possible?
First, this is observed when the clock is moved forward (by more than a day) and then back.
Second, this can be traced back to Windows 8. Installations of Windows 7 always pick the right value from the real-time clock during the cold boot.
Third, the only boot type considered in this post is the cold one. I observed the same behavior with hibernated installations of Windows 10, but I haven’t debugged them enough to be sure in my conclusions.
Finally, this can be easily reproduced on real hardware. Moving the clock backward in the BIOS/UEFI interface doesn’t necessary rebase the current date and time shown after the boot (this is true without Internet access, otherwise Windows can synchronize the clock with an NTP server).
So, what’s happening?
During the early stage of boot, the NT kernel starts a software clock and sets its initial value to the following: date and time read from the real-time clock by the system loader (winload) plus time zone bias plus time passed since the real-time clock has been read by the system loader. (UTC = local time + bias.)
If the system loader sets a flag telling the NT kernel not to trust the real-time clock, the kernel will set the software clock to the following instead: boot date and time determined by the system loader plus time passed since the boot timestamp has been determined by the system loader.
Additionally, the real-time clock will be set to the following: boot date and time determined by the system loader plus time passed since the boot timestamp has been determined by the system loader minus time zone bias.
(The time zone bias is ignored in both paths if the real-time clock is configured to run in UTC.)
During my tests, I found that the latter path is taken when booting with the real-time clock moved backward.
The system loader, either winload.exe (for BIOS-based systems) or winload.efi (for UEFI-based systems), is responsible for checking whether the real-time clock is sane or not and what date and time should be used if the real-time clock isn’t sane.
It deals with three sources:
- A timestamp read from the real-time clock.
- A timestamp from the \Windows\bootstat.dat file.
- A last written timestamp of the current control set key (ControlSet00X).
The algorithm is:
- Read the current date and time from the real-time clock. If time zone information isn’t set (this is true for BIOS-based systems), add 26 hours to the current date and time. If time zone information is set and it looks valid (this can be true for UEFI-based systems), switch the current date and time to UTC (if time zone information is invalid, add 26 hours to the current date and time).
- Read the \Windows\bootstat.dat file and parse the timestamp stored in its header (the timestamp offset is 32 bytes), validate the timestamp using its checksum (CRC-32) stored in the same file (the checksum offset is 40 bytes).
- If a timestamp calculated in the first step is less than a timestamp read in the second step, use the latter timestamp as the boot system time. (In other words, the system loader will pick a timestamp taken from the \Windows\bootstat.dat file.)
- If a timestamp read in the second step in invalid (it has a wrong checksum), then pick the last written timestamp of the current control set key.
- If a timestamp calculated in the first step is less than a timestamp read in the fourth step, use the latter timestamp as the boot system time. (The system loader will pick a timestamp from the SYSTEM hive.)
- If the boot system time wasn’t set to the date and time read from the real-time clock, mark the real-time clock as not sane. The NT kernel will set such a clock to a calculated date and time (as described above).
In my tests, the timestamp in the \Windows\bootstat.dat file is updated on different events, including the shutdown.
So, examiners can use this timestamp as an alternative way to check if the system clock was set correctly at the last shutdown.