5.6 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Project Overview
TimeMocker is a Windows application that injects fake time into running processes by hooking Win32 time APIs. It consists of a WinForms UI controller and an EasyHook-based DLL that gets injected into target processes.
Build Commands
# Build the solution (x64 only)
dotnet build TimeMocker.sln -c Release -p:Platform=x64
# Build Debug configuration
dotnet build TimeMocker.sln -c Debug -p:Platform=x64
# Clean and rebuild
dotnet clean TimeMocker.sln && dotnet build TimeMocker.sln -c Release -p:Platform=x64
Important: This is an x64-only project. The solution only has x64 platform configurations. Building for x86 or AnyCPU will fail.
Output locations:
- UI exe:
TimeMocker.UI/bin/x64/Release/net48/TimeMocker.exe - Hook DLL:
TimeMocker.UI/bin/x64/Release/net48/TimeMocker.Hook.dll
The hook DLL must be next to the UI exe (automatically copied via ProjectReference).
Architecture
TimeMocker.sln
├── TimeMocker.UI/ — WinForms controller (requires Admin elevation)
│ ├── Forms/MainForm.cs — Main UI with process selection, time picker, pattern manager
│ ├── Core/InjectionManager.cs — EasyHook-based injector, manages injected processes
│ ├── Core/ProcessWatcher.cs — Background scanner for auto-inject rules (poll-based)
│ └── Core/SharedMemoryManager.cs — Named MMF for IPC with hook DLL
│
└── TimeMocker.Hook/ — DLL injected into target processes
└── InjectionEntryPoint.cs — Hooks 5 Win32 time APIs via EasyHook
IPC Design
The fake time is stored in a named Memory-Mapped File (one per injected process):
- Name format:
TimeMocker_<PID> - Size: 12 bytes (
Marshal.SizeOf<MockTimeInfo>()) - Layout:
[0..7]FakeUtcTicks (Int64 — DateTime.Ticks)[8..11]Enabled (Int32 — 0=passthrough, 1=mock)
The hook reads this on every time API call (~50 ns read, no syscall). The UI writes via SharedMemoryManager.Write().
Hooked APIs
| API | DLL |
|---|---|
GetSystemTime |
kernel32 |
GetLocalTime |
kernel32 |
GetSystemTimeAsFileTime |
kernel32 |
GetSystemTimePreciseAsFileTime |
kernel32 |
NtQuerySystemTime |
ntdll |
All hooked functions share the same GetFakeUtc() logic which reads from shared memory and either returns the fake time or real DateTime.UtcNow depending on the Enabled flag.
Auto-Inject Pattern Matching
The ProcessWatcher supports both glob and regex patterns for matching process paths or names:
- Glob:
C:\Games\MyGame\*or*chrome*(converted to regex internally) - Regex:
^.*\\MyApp\.exe$
Patterns are matched against both the full process path (MainModule.FileName) and process name (ProcessName).
Key Components
InjectionManager
- Manages the dictionary of injected processes (
Dictionary<int, InjectedProcess>) - Uses
RemoteHooking.Inject()to injectTimeMocker.Hook.dllinto target processes - Creates a
SharedMemoryManagerinstance per process for IPC - Handles eject by disabling the mock (setting Enabled=0) before disposing shared memory
SharedMemoryManager
- Creates a named memory-mapped file using
MemoryMappedFile.CreateOrOpen() MockTimeInfostruct is blittable (sequential layout) for direct memory write- Must be disposed when process is ejected
ProcessWatcher
- Polls running processes every ~1.5 seconds (configurable)
- Tracks seen process IDs to avoid duplicate injections
- Auto-injects when a process matches any enabled rule
- Uses current
FakeUtcandMockEnabledsettings at injection time
InjectionEntryPoint (Hook DLL)
- Implements
IEntryPointfrom EasyHook Run()method opens the named MMF, installs hooks, and keeps alive with a sleep loop- Uses
LocalHook.Create()with thread ACL set to exclude thread 0 (all threads) - Hook delegates read the shared memory on each call (hot path optimized)
- Logs errors to
%TEMP%\TimeMocker.Hook.log
Development Notes
- Target Framework: .NET Framework 4.8 (pre-installed on Windows 10+)
- Unsafe Code: Both projects use
AllowUnsafeBlocks=truefor memory-mapped file operations - Dependencies: EasyHook 2.7.7030.0 (available via NuGet)
- Language Version: C# 8
Limitations
- 64-bit only: 32-bit processes require a separate 32-bit hook DLL build
- No hot-unload support: EasyHook doesn't fully support DLL ejection; disable mock instead
- Protected processes: Anti-cheat services (Epic, BattlEye, etc.) will block injection
- QueryPerformanceCounter: Not affected (hardware register, cannot be hooked via EasyHook)
Time Epoch
FILETIME epoch is January 1, 1601 (UTC). Hooked functions returning FILETIME convert from DateTime.Ticks using:
var epoch = new DateTime(1601, 1, 1, 0, 0, 0, DateTimeKind.Utc);
long fileTimeTicks = (fakeUtc - epoch).Ticks;
Common Tasks
Add a new time API hook
- Add the hook delegate in
InjectionEntryPoint.cswith[UnmanagedFunctionPointer(CallingConvention.StdCall)] - Add a
LocalHookfield for the hook handle - Create the hook in
InstallHooks()usingLocalHook.GetProcAddress(dllName, functionName) - Implement the hook function that calls
GetFakeUtc()and returns the appropriate format
Modify auto-inject polling interval
Pass a different interval to ProcessWatcher.Start(int pollIntervalMs) when enabling the watcher in MainForm.cs.
Debug hook DLL
The hook writes errors to %TEMP%\TimeMocker.Hook.log. For more detailed debugging, you may need to attach a debugger to the target process after injection.