diff --git a/csharp/README.md b/csharp/README.md index 9434ce3..67e9e3d 100644 --- a/csharp/README.md +++ b/csharp/README.md @@ -1 +1,100 @@ -# time-mocker \ No newline at end of file +# TimeMocker + +A Windows application that injects fake time into running processes by hooking Win32 time APIs. + +## Architecture + +``` +TimeMocker.sln +├── TimeMocker.UI — WinForms controller app (run as Admin) +│ ├── Forms/MainForm — UI: process chooser, time picker, pattern manager +│ ├── Core/InjectionManager — EasyHook-based injector per process +│ ├── Core/ProcessWatcher — background scanner for auto-inject patterns +│ └── Core/SharedMemoryManager — named MMF shared with the hook DLL +│ +└── TimeMocker.Hook — DLL injected into target processes + └── InjectionEntryPoint — hooks 5 Win32 time functions via EasyHook +``` + +## Hooked APIs + +| API | DLL | +|-----|-----| +| `GetSystemTime` | kernel32 | +| `GetLocalTime` | kernel32 | +| `GetSystemTimeAsFileTime` | kernel32 | +| `GetSystemTimePreciseAsFileTime` | kernel32 | +| `NtQuerySystemTime` | ntdll | + +## Requirements + +- **Windows 10/11 x64** +- **.NET Framework 4.8** (pre-installed on Win10+) +- **Visual Studio 2022** or `dotnet build` +- Must run as **Administrator** (UAC prompt shown automatically) + +## Build + +```bash +# Clone / extract the solution +cd TimeMocker +dotnet restore +dotnet build -c Release -p:Platform=x64 + +# Outputs go to: +# TimeMocker.UI/bin/x64/Release/net48/TimeMocker.exe +# TimeMocker.UI/bin/x64/Release/net48/TimeMocker.Hook.dll ← must be next to .exe +``` + +> In Visual Studio: open `TimeMocker.sln`, set platform to **x64**, build solution. + +## Usage + +### Manual Injection + +1. Launch `TimeMocker.exe` (UAC will prompt for elevation) +2. **Processes tab** → search for your target process → select it +3. Set the desired date/time in the **Mock Time Settings** bar at the top +4. Tick **Enable Mock** → click **Inject →** +5. The target process now sees your fake time immediately + +### Auto-Inject Rules + +1. Go to the **Auto-Inject Rules** tab +2. Enter a pattern matching the process path or name, e.g.: + - Glob: `C:\Games\MyGame\*` + - Glob by name: `*chrome*` + - Regex: `^.*\\MyApp\.exe$` +3. Click **+ Add Rule** +4. Tick **Enable Auto-Inject Watcher** +5. Any process that starts (or is already running) matching the rule gets injected automatically + +### Auto-Advance Time + +Tick **Auto-advance time** in the time bar — the fake time ticks forward in sync with real time from the moment you set it. + +## IPC Design + +The fake time is stored in a **named Memory-Mapped File** (one per injected process): + +``` +Name: TimeMocker_ +Size: 12 bytes + [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 it whenever you click **Apply** or toggle the checkbox. + +## Notes & Limitations + +- **64-bit only** — 32-bit processes require a separate 32-bit hook DLL build +- Processes using `QueryPerformanceCounter` for *monotonic* timing are not affected + (QPC is a hardware register; patching it is unsupported by EasyHook) +- Anti-cheat or heavily protected processes (Epic, BattlEye, etc.) will block injection +- Some .NET apps read time through the CLR, which internally calls the hooked APIs — these *will* be affected +- EasyHook does not fully support hot-eject; to restore real time, disable the mock via the checkbox rather than ejecting + +## License + +MIT diff --git a/csharp/TimeMocker.Hook/InjectionEntryPoint.cs b/csharp/TimeMocker.Hook/InjectionEntryPoint.cs new file mode 100644 index 0000000..dc41eb7 --- /dev/null +++ b/csharp/TimeMocker.Hook/InjectionEntryPoint.cs @@ -0,0 +1,190 @@ +using System; +using System.IO; +using System.IO.Pipes; +using System.Runtime.InteropServices; +using System.Threading; +using EasyHook; + +namespace TimeMocker.Hook +{ + /// + /// Shared memory layout written by the UI and read by the hook. + /// Stored in a named memory-mapped file so no pipe latency on hot path. + /// + [StructLayout(LayoutKind.Sequential)] + public struct MockTimeInfo + { + public long FakeUtcTicks; // DateTime ticks (UTC) + public int Enabled; // 1 = mock active, 0 = passthrough + } + + // ------------------------------------------------------------------------- + // Win32 structs + // ------------------------------------------------------------------------- + [StructLayout(LayoutKind.Sequential)] + public struct SYSTEMTIME + { + public ushort wYear, wMonth, wDayOfWeek, wDay; + public ushort wHour, wMinute, wSecond, wMilliseconds; + + public static SYSTEMTIME FromDateTime(DateTime dt) + { + return new SYSTEMTIME + { + wYear = (ushort)dt.Year, + wMonth = (ushort)dt.Month, + wDayOfWeek = (ushort)dt.DayOfWeek, + wDay = (ushort)dt.Day, + wHour = (ushort)dt.Hour, + wMinute = (ushort)dt.Minute, + wSecond = (ushort)dt.Second, + wMilliseconds = (ushort)dt.Millisecond + }; + } + } + + // ------------------------------------------------------------------------- + // EasyHook entry point – called after DLL is injected + // ------------------------------------------------------------------------- + public class InjectionEntryPoint : IEntryPoint + { + private readonly string _mmfName; + private System.IO.MemoryMappedFiles.MemoryMappedFile _mmf; + private System.IO.MemoryMappedFiles.MemoryMappedViewAccessor _view; + + // Hook handles + private LocalHook _getSystemTimeHook; + private LocalHook _getLocalTimeHook; + private LocalHook _ntQuerySystemTimeHook; + private LocalHook _getSystemTimeAsFileTimeHook; + private LocalHook _getSystemTimePreciseAsFileTimeHook; + + public InjectionEntryPoint(RemoteHooking.IContext context, string mmfName) + { + _mmfName = mmfName; + } + + public void Run(RemoteHooking.IContext context, string mmfName) + { + try + { + // Open the shared memory created by the UI process + _mmf = System.IO.MemoryMappedFiles.MemoryMappedFile.OpenExisting(mmfName); + _view = _mmf.CreateViewAccessor(0, Marshal.SizeOf()); + + InstallHooks(); + RemoteHooking.WakeUpProcess(); + + // Keep alive until process exits + while (true) Thread.Sleep(500); + } + catch (Exception ex) + { + File.AppendAllText( + Path.Combine(Path.GetTempPath(), "TimeMocker.Hook.log"), + $"[{DateTime.Now}] ERROR: {ex}\r\n"); + } + finally + { + _getSystemTimeHook?.Dispose(); + _getLocalTimeHook?.Dispose(); + _ntQuerySystemTimeHook?.Dispose(); + _getSystemTimeAsFileTimeHook?.Dispose(); + _getSystemTimePreciseAsFileTimeHook?.Dispose(); + _view?.Dispose(); + _mmf?.Dispose(); + } + } + + // ------------------------------------------------------------------------- + // Helpers + // ------------------------------------------------------------------------- + private MockTimeInfo ReadMockInfo() + { + _view.Read(0, out MockTimeInfo info); + return info; + } + + private DateTime GetFakeUtc() + { + var info = ReadMockInfo(); + return info.Enabled == 1 + ? new DateTime(info.FakeUtcTicks, DateTimeKind.Utc) + : DateTime.UtcNow; + } + + private void InstallHooks() + { + _getSystemTimeHook = LocalHook.Create( + LocalHook.GetProcAddress("kernel32.dll", "GetSystemTime"), + new GetSystemTimeDelegate(GetSystemTime_Hook), this); + _getSystemTimeHook.ThreadACL.SetExclusiveACL(new[] { 0 }); + + _getLocalTimeHook = LocalHook.Create( + LocalHook.GetProcAddress("kernel32.dll", "GetLocalTime"), + new GetLocalTimeDelegate(GetLocalTime_Hook), this); + _getLocalTimeHook.ThreadACL.SetExclusiveACL(new[] { 0 }); + + _ntQuerySystemTimeHook = LocalHook.Create( + LocalHook.GetProcAddress("ntdll.dll", "NtQuerySystemTime"), + new NtQuerySystemTimeDelegate(NtQuerySystemTime_Hook), this); + _ntQuerySystemTimeHook.ThreadACL.SetExclusiveACL(new[] { 0 }); + + _getSystemTimeAsFileTimeHook = LocalHook.Create( + LocalHook.GetProcAddress("kernel32.dll", "GetSystemTimeAsFileTime"), + new GetSystemTimeAsFileTimeDelegate(GetSystemTimeAsFileTime_Hook), this); + _getSystemTimeAsFileTimeHook.ThreadACL.SetExclusiveACL(new[] { 0 }); + + _getSystemTimePreciseAsFileTimeHook = LocalHook.Create( + LocalHook.GetProcAddress("kernel32.dll", "GetSystemTimePreciseAsFileTime"), + new GetSystemTimeAsFileTimeDelegate(GetSystemTimePreciseAsFileTime_Hook), this); + _getSystemTimePreciseAsFileTimeHook.ThreadACL.SetExclusiveACL(new[] { 0 }); + } + + // ------------------------------------------------------------------------- + // Hook implementations + // ------------------------------------------------------------------------- + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + delegate void GetSystemTimeDelegate(out SYSTEMTIME lpSystemTime); + + void GetSystemTime_Hook(out SYSTEMTIME lpSystemTime) + { + lpSystemTime = SYSTEMTIME.FromDateTime(GetFakeUtc()); + } + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + delegate void GetLocalTimeDelegate(out SYSTEMTIME lpLocalTime); + + void GetLocalTime_Hook(out SYSTEMTIME lpLocalTime) + { + lpLocalTime = SYSTEMTIME.FromDateTime(GetFakeUtc().ToLocalTime()); + } + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + delegate int NtQuerySystemTimeDelegate(out long systemTime); + + int NtQuerySystemTime_Hook(out long systemTime) + { + // FILETIME epoch: Jan 1, 1601 + var epoch = new DateTime(1601, 1, 1, 0, 0, 0, DateTimeKind.Utc); + systemTime = (GetFakeUtc() - epoch).Ticks; + return 0; // STATUS_SUCCESS + } + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + delegate void GetSystemTimeAsFileTimeDelegate(out long lpFileTime); + + void GetSystemTimeAsFileTime_Hook(out long lpFileTime) + { + var epoch = new DateTime(1601, 1, 1, 0, 0, 0, DateTimeKind.Utc); + lpFileTime = (GetFakeUtc() - epoch).Ticks; + } + + void GetSystemTimePreciseAsFileTime_Hook(out long lpFileTime) + { + var epoch = new DateTime(1601, 1, 1, 0, 0, 0, DateTimeKind.Utc); + lpFileTime = (GetFakeUtc() - epoch).Ticks; + } + } +} diff --git a/csharp/TimeMocker.Hook/TimeMocker.Hook.csproj b/csharp/TimeMocker.Hook/TimeMocker.Hook.csproj new file mode 100644 index 0000000..ef7fd8b --- /dev/null +++ b/csharp/TimeMocker.Hook/TimeMocker.Hook.csproj @@ -0,0 +1,13 @@ + + + net48 + x64 + TimeMocker.Hook + TimeMocker.Hook + Library + true + + + + + diff --git a/csharp/TimeMocker.UI/Core/InjectionManager.cs b/csharp/TimeMocker.UI/Core/InjectionManager.cs new file mode 100644 index 0000000..6b8cbe9 --- /dev/null +++ b/csharp/TimeMocker.UI/Core/InjectionManager.cs @@ -0,0 +1,120 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using EasyHook; + +namespace TimeMocker.UI.Core +{ + public class InjectedProcess + { + public int ProcessId { get; set; } + public string ProcessName { get; set; } + public string ProcessPath { get; set; } + public SharedMemoryManager Shm { get; set; } + public bool IsInjected { get; set; } + } + + public class InjectionManager : IDisposable + { + private readonly Dictionary _injected + = new Dictionary(); + + private static readonly string HookDllPath = + Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "TimeMocker.Hook.dll"); + + public event Action LogMessage; + + // ----------------------------------------------------------------------- + // Inject into a specific process + // ----------------------------------------------------------------------- + public InjectedProcess Inject(Process process) + { + if (_injected.ContainsKey(process.Id)) + return _injected[process.Id]; + + var entry = new InjectedProcess + { + ProcessId = process.Id, + ProcessName = process.ProcessName, + ProcessPath = TryGetPath(process), + Shm = new SharedMemoryManager(process.Id) + }; + + try + { + // Write disabled state initially so hook passes through + entry.Shm.Write(new MockTimeInfo { Enabled = 0, FakeUtcTicks = DateTime.UtcNow.Ticks }); + + RemoteHooking.Inject( + process.Id, + InjectionOptions.DoNotRequireStrongName, + HookDllPath, + HookDllPath, + entry.Shm.MmfName); + + entry.IsInjected = true; + _injected[process.Id] = entry; + Log($"Injected into [{process.Id}] {process.ProcessName}"); + } + catch (Exception ex) + { + entry.Shm.Dispose(); + Log($"Failed to inject into [{process.Id}] {process.ProcessName}: {ex.Message}"); + throw; + } + + return entry; + } + + // ----------------------------------------------------------------------- + // Update fake time for a process + // ----------------------------------------------------------------------- + public void SetFakeTime(int processId, DateTime fakeUtc, bool enabled) + { + if (!_injected.TryGetValue(processId, out var entry)) return; + + entry.Shm.Write(new MockTimeInfo + { + FakeUtcTicks = fakeUtc.Ticks, + Enabled = enabled ? 1 : 0 + }); + } + + public void SetFakeTimeAll(DateTime fakeUtc, bool enabled) + { + foreach (var pid in _injected.Keys) + SetFakeTime(pid, fakeUtc, enabled); + } + + public bool IsInjected(int processId) => _injected.ContainsKey(processId); + + public IEnumerable InjectedProcesses => _injected.Values; + + // ----------------------------------------------------------------------- + // Eject (best-effort – EasyHook doesn't fully support unloading) + // ----------------------------------------------------------------------- + public void Eject(int processId) + { + if (!_injected.TryGetValue(processId, out var entry)) return; + entry.Shm.Write(new MockTimeInfo { Enabled = 0 }); // disable mock first + entry.Shm.Dispose(); + _injected.Remove(processId); + Log($"Ejected from [{processId}] {entry.ProcessName}"); + } + + private static string TryGetPath(Process p) + { + try { return p.MainModule?.FileName ?? ""; } + catch { return ""; } + } + + private void Log(string msg) => LogMessage?.Invoke(msg); + + public void Dispose() + { + foreach (var e in _injected.Values) e.Shm?.Dispose(); + _injected.Clear(); + } + } +} diff --git a/csharp/TimeMocker.UI/Core/ProcessWatcher.cs b/csharp/TimeMocker.UI/Core/ProcessWatcher.cs new file mode 100644 index 0000000..fba9dfd --- /dev/null +++ b/csharp/TimeMocker.UI/Core/ProcessWatcher.cs @@ -0,0 +1,118 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Text.RegularExpressions; +using System.Threading; + +namespace TimeMocker.UI.Core +{ + public class PatternRule + { + public string Pattern { get; set; } // glob or regex + public bool UseRegex { get; set; } + public bool Enabled { get; set; } = true; + + private Regex _compiled; + + public bool IsMatch(string path) + { + if (string.IsNullOrEmpty(path)) return false; + + if (UseRegex) + { + _compiled ??= new Regex(Pattern, RegexOptions.IgnoreCase); + return _compiled.IsMatch(path); + } + + // Glob: convert * and ? to regex + var regexStr = "^" + Regex.Escape(Pattern) + .Replace(@"\*", ".*") + .Replace(@"\?", ".") + "$"; + return Regex.IsMatch(path, regexStr, RegexOptions.IgnoreCase); + } + } + + public class ProcessWatcher : IDisposable + { + private readonly InjectionManager _injectionMgr; + private readonly List _rules = new List(); + private readonly HashSet _seen = new HashSet(); + private Timer _timer; + private bool _running; + + // Current fake time settings to apply on auto-inject + public DateTime FakeUtc { get; set; } = DateTime.UtcNow; + public bool MockEnabled { get; set; } = false; + + public event Action LogMessage; + public event Action ProcessAutoInjected; + + public IReadOnlyList Rules => _rules; + + public ProcessWatcher(InjectionManager mgr) + { + _injectionMgr = mgr; + } + + public void AddRule(PatternRule rule) { lock (_rules) _rules.Add(rule); } + public void RemoveRule(PatternRule rule) { lock (_rules) _rules.Remove(rule); } + public void ClearRules() { lock (_rules) _rules.Clear(); } + + public void Start(int pollIntervalMs = 1500) + { + if (_running) return; + _running = true; + _timer = new Timer(_ => Scan(), null, 0, pollIntervalMs); + } + + public void Stop() + { + _running = false; + _timer?.Dispose(); + _timer = null; + } + + private void Scan() + { + try + { + var processes = Process.GetProcesses(); + lock (_rules) + { + foreach (var p in processes) + { + if (_seen.Contains(p.Id)) continue; + + string path = ""; + try { path = p.MainModule?.FileName ?? ""; } catch { continue; } + + foreach (var rule in _rules) + { + if (!rule.Enabled) continue; + if (!rule.IsMatch(path) && !rule.IsMatch(p.ProcessName)) continue; + + _seen.Add(p.Id); + try + { + var entry = _injectionMgr.Inject(p); + _injectionMgr.SetFakeTime(p.Id, FakeUtc, MockEnabled); + Log($"[AutoInject] Matched rule '{rule.Pattern}' → [{p.Id}] {p.ProcessName}"); + ProcessAutoInjected?.Invoke(entry); + } + catch (Exception ex) + { + Log($"[AutoInject] Failed on [{p.Id}] {p.ProcessName}: {ex.Message}"); + } + break; + } + } + } + } + catch { /* scan errors are non-fatal */ } + } + + private void Log(string msg) => LogMessage?.Invoke(msg); + + public void Dispose() => Stop(); + } +} diff --git a/csharp/TimeMocker.UI/Core/SharedMemoryManager.cs b/csharp/TimeMocker.UI/Core/SharedMemoryManager.cs new file mode 100644 index 0000000..b58ea00 --- /dev/null +++ b/csharp/TimeMocker.UI/Core/SharedMemoryManager.cs @@ -0,0 +1,52 @@ +using System; +using System.IO.MemoryMappedFiles; +using System.Runtime.InteropServices; + +namespace TimeMocker.UI.Core +{ + /// + /// Creates a named Memory-Mapped File so the injected hook can read + /// the fake time without any IPC latency on the hot path. + /// One SharedMemoryManager per injected process. + /// + public class SharedMemoryManager : IDisposable + { + public const string MmfPrefix = "TimeMocker_"; + private MemoryMappedFile _mmf; + private MemoryMappedViewAccessor _view; + private readonly int _size; + private bool _disposed; + + public string MmfName { get; } + + public SharedMemoryManager(int processId) + { + MmfName = MmfPrefix + processId; + _size = Marshal.SizeOf(); + _mmf = MemoryMappedFile.CreateOrOpen(MmfName, _size, + MemoryMappedFileAccess.ReadWrite); + _view = _mmf.CreateViewAccessor(0, _size); + } + + public void Write(MockTimeInfo info) + { + _view.Write(0, ref info); + _view.Flush(); + } + + public void Dispose() + { + if (_disposed) return; + _disposed = true; + _view?.Dispose(); + _mmf?.Dispose(); + } + } + + [StructLayout(LayoutKind.Sequential)] + public struct MockTimeInfo + { + public long FakeUtcTicks; + public int Enabled; + } +} diff --git a/csharp/TimeMocker.UI/Forms/MainForm.cs b/csharp/TimeMocker.UI/Forms/MainForm.cs new file mode 100644 index 0000000..782c1a1 --- /dev/null +++ b/csharp/TimeMocker.UI/Forms/MainForm.cs @@ -0,0 +1,638 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Drawing; +using System.Linq; +using System.Windows.Forms; +using TimeMocker.UI.Core; + +namespace TimeMocker.UI.Forms +{ + public partial class MainForm : Form + { + private InjectionManager _injMgr; + private ProcessWatcher _watcher; + + // Controls + private TabControl tabMain; + private TabPage tabProcesses, tabPatterns, tabLog; + + // -- Process tab + private DataGridView dgvProcesses; + private Button btnRefresh, btnInject, btnEject; + private TextBox txtProcSearch; + private Label lblProcSearch; + + // -- Time panel (shared) + private GroupBox grpTime; + private DateTimePicker dtpDate; + private DateTimePicker dtpTime; + private CheckBox chkMockEnabled; + private Button btnApply; + private Label lblPreview; + private CheckBox chkAutoAdvance; + + // -- Patterns tab + private DataGridView dgvPatterns; + private Button btnAddPattern, btnRemovePattern; + private CheckBox chkWatcherEnabled; + private TextBox txtNewPattern; + private RadioButton rdoGlob, rdoRegex; + + // -- Log tab + private RichTextBox rtbLog; + private Button btnClearLog; + + // Time advancing + private Timer _advanceTimer; + private DateTime _fakeTimeBase; + private DateTime _advanceStartReal; + + public MainForm() + { + Text = "TimeMocker – Process Time Injection"; + Size = new Size(900, 680); + MinimumSize = new Size(750, 560); + StartPosition = FormStartPosition.CenterScreen; + Font = new Font("Segoe UI", 9f); + BackColor = Color.FromArgb(30, 30, 35); + ForeColor = Color.FromArgb(220, 220, 220); + + _injMgr = new InjectionManager(); + _watcher = new ProcessWatcher(_injMgr); + + _injMgr.LogMessage += AppendLog; + _watcher.LogMessage += AppendLog; + _watcher.ProcessAutoInjected += entry => + BeginInvoke((Action)(() => RefreshInjectedTab())); + + BuildUI(); + RefreshProcessList(); + UpdateTimePreview(); + } + + // ===================================================================== + // UI Builder + // ===================================================================== + private void BuildUI() + { + // ---- Shared time panel (top) ------------------------------------ + grpTime = new GroupBox + { + Text = "Mock Time Settings", + Dock = DockStyle.Top, + Height = 110, + ForeColor = Color.FromArgb(130, 200, 255), + Padding = new Padding(8) + }; + + var timeFlow = new FlowLayoutPanel + { + Dock = DockStyle.Fill, + FlowDirection= FlowDirection.LeftToRight, + WrapContents = false, + AutoSize = false + }; + + chkMockEnabled = new CheckBox + { + Text = "Enable Mock", + ForeColor = Color.LightGreen, + Width = 110, + Height = 30, + Margin = new Padding(4, 12, 4, 0) + }; + chkMockEnabled.CheckedChanged += (s, e) => ApplyTime(); + + dtpDate = new DateTimePicker + { + Format = DateTimePickerFormat.Short, + Width = 120, + Height = 26, + Value = DateTime.Now, + Margin = new Padding(4, 10, 4, 0) + }; + dtpDate.ValueChanged += (s, e) => UpdateTimePreview(); + + dtpTime = new DateTimePicker + { + Format = DateTimePickerFormat.Time, + ShowUpDown = true, + Width = 100, + Height = 26, + Value = DateTime.Now, + Margin = new Padding(4, 10, 4, 0) + }; + dtpTime.ValueChanged += (s, e) => UpdateTimePreview(); + + btnApply = MakeButton("Apply to All", 100, Color.FromArgb(0, 120, 215)); + btnApply.Margin = new Padding(8, 10, 4, 0); + btnApply.Click += (s, e) => ApplyTime(); + + var btnSetNow = MakeButton("Set to Now", 90, Color.FromArgb(60, 60, 70)); + btnSetNow.Margin = new Padding(4, 10, 4, 0); + btnSetNow.Click += (s, e) => { dtpDate.Value = dtpTime.Value = DateTime.Now; ApplyTime(); }; + + chkAutoAdvance = new CheckBox + { + Text = "Auto-advance time", + ForeColor = Color.FromArgb(220, 220, 220), + Width = 140, + Height = 30, + Margin = new Padding(8, 12, 4, 0) + }; + chkAutoAdvance.CheckedChanged += ToggleAutoAdvance; + + lblPreview = new Label + { + AutoSize = false, + Width = 260, + Height = 20, + ForeColor = Color.FromArgb(180, 180, 180), + Font = new Font("Segoe UI", 8.5f, FontStyle.Italic), + Margin = new Padding(4, 14, 0, 0) + }; + + timeFlow.Controls.AddRange(new Control[] + { + chkMockEnabled, dtpDate, dtpTime, btnApply, btnSetNow, + chkAutoAdvance, lblPreview + }); + grpTime.Controls.Add(timeFlow); + + // ---- Tabs ------------------------------------------------------- + tabMain = new TabControl + { + Dock = DockStyle.Fill, + DrawMode = TabDrawMode.OwnerDrawFixed, + SizeMode = TabSizeMode.Fixed, + ItemSize = new Size(120, 28) + }; + tabMain.DrawItem += DrawTab; + + tabProcesses = new TabPage("Processes"); + tabPatterns = new TabPage("Auto-Inject Rules"); + tabLog = new TabPage("Log"); + StyleTab(tabProcesses); + StyleTab(tabPatterns); + StyleTab(tabLog); + + BuildProcessTab(); + BuildPatternsTab(); + BuildLogTab(); + + tabMain.TabPages.AddRange(new[] { tabProcesses, tabPatterns, tabLog }); + + Controls.Add(tabMain); + Controls.Add(grpTime); + + // advance timer + _advanceTimer = new Timer { Interval = 1000 }; + _advanceTimer.Tick += AdvanceTick; + } + + // ===================================================================== + // Process Tab + // ===================================================================== + private void BuildProcessTab() + { + var panel = new Panel { Dock = DockStyle.Fill }; + + // Top toolbar + var toolbar = new FlowLayoutPanel + { + Dock = DockStyle.Top, + Height = 40, + Padding= new Padding(4) + }; + + lblProcSearch = new Label { Text = "Search:", AutoSize = true, Margin = new Padding(4, 8, 2, 0) }; + txtProcSearch = new TextBox { Width = 180, Margin = new Padding(0, 6, 8, 0) }; + txtProcSearch.TextChanged += (s, e) => FilterProcessList(); + + btnRefresh = MakeButton("⟳ Refresh", 90, Color.FromArgb(60, 60, 70)); + btnRefresh.Margin = new Padding(0, 6, 4, 0); + btnRefresh.Click += (s, e) => RefreshProcessList(); + + btnInject = MakeButton("Inject →", 90, Color.FromArgb(0, 150, 80)); + btnInject.Margin = new Padding(0, 6, 4, 0); + btnInject.Click += OnInjectClick; + + btnEject = MakeButton("✕ Eject", 80, Color.FromArgb(180, 40, 40)); + btnEject.Margin = new Padding(0, 6, 4, 0); + btnEject.Click += OnEjectClick; + + toolbar.Controls.AddRange(new Control[] { lblProcSearch, txtProcSearch, btnRefresh, btnInject, btnEject }); + toolbar.BackColor = Color.FromArgb(30, 30, 35); + + // Grid – split: available processes | injected processes + var split = new SplitContainer + { + Dock = DockStyle.Fill, + Orientation = Orientation.Horizontal, + SplitterDistance = 300, + SplitterWidth = 5, + BackColor = Color.FromArgb(50, 50, 55) + }; + + // Top: available + var lblAvail = MakeSectionLabel("Running Processes"); + dgvProcesses = MakeGrid(); + dgvProcesses.Columns.AddRange( + Col("PID", 50), Col("Name", 160), Col("Path", 380)); + + var topPanel = new Panel { Dock = DockStyle.Fill }; + topPanel.Controls.Add(dgvProcesses); + topPanel.Controls.Add(lblAvail); + + // Bottom: injected + var lblInj = MakeSectionLabel("Injected Processes"); + var dgvInjected = MakeGrid(); + dgvInjected.Tag = "injected"; + dgvInjected.Columns.AddRange( + Col("PID", 50), Col("Name", 160), Col("Path", 380), Col("Status", 80)); + + var botPanel = new Panel { Dock = DockStyle.Fill }; + botPanel.Controls.Add(dgvInjected); + botPanel.Controls.Add(lblInj); + + split.Panel1.Controls.Add(topPanel); + split.Panel2.Controls.Add(botPanel); + + panel.Controls.Add(split); + panel.Controls.Add(toolbar); + + // store for refresh + _dgvInjected = dgvInjected; + + tabProcesses.Controls.Add(panel); + } + + private DataGridView _dgvInjected; + private List _allRows = new List(); + + private class ProcessRow + { + public int Id; public string Name, Path; + } + + private void RefreshProcessList() + { + _allRows.Clear(); + foreach (var p in Process.GetProcesses().OrderBy(x => x.ProcessName)) + { + string path = ""; + try { path = p.MainModule?.FileName ?? ""; } catch { } + _allRows.Add(new ProcessRow { Id = p.Id, Name = p.ProcessName, Path = path }); + } + FilterProcessList(); + RefreshInjectedTab(); + } + + private void FilterProcessList() + { + var q = txtProcSearch.Text.Trim().ToLower(); + dgvProcesses.Rows.Clear(); + foreach (var r in _allRows) + { + if (q.Length > 0 && !r.Name.ToLower().Contains(q) && !r.Path.ToLower().Contains(q)) continue; + dgvProcesses.Rows.Add(r.Id, r.Name, r.Path); + } + } + + private void RefreshInjectedTab() + { + if (_dgvInjected == null) return; + _dgvInjected.Rows.Clear(); + foreach (var e in _injMgr.InjectedProcesses) + _dgvInjected.Rows.Add(e.ProcessId, e.ProcessName, e.ProcessPath, e.IsInjected ? "Active" : "Pending"); + } + + private void OnInjectClick(object sender, EventArgs e) + { + var selected = GetSelectedProcessId(dgvProcesses); + if (selected == null) { ShowInfo("Select a process first."); return; } + + try + { + var p = Process.GetProcessById(selected.Value); + _injMgr.Inject(p); + var dt = GetFakeTime(); + _injMgr.SetFakeTime(p.Id, dt.ToUniversalTime(), chkMockEnabled.Checked); + RefreshInjectedTab(); + AppendLog($"Manually injected into [{p.Id}] {p.ProcessName}"); + } + catch (Exception ex) + { + MessageBox.Show($"Injection failed:\n{ex.Message}", "Error", + MessageBoxButtons.OK, MessageBoxIcon.Error); + } + } + + private void OnEjectClick(object sender, EventArgs e) + { + var selected = GetSelectedProcessId(_dgvInjected); + if (selected == null) { ShowInfo("Select an injected process first."); return; } + _injMgr.Eject(selected.Value); + RefreshInjectedTab(); + } + + // ===================================================================== + // Patterns Tab + // ===================================================================== + private void BuildPatternsTab() + { + var panel = new Panel { Dock = DockStyle.Fill }; + + var toolbar = new FlowLayoutPanel + { + Dock = DockStyle.Top, + Height = 80, + Padding = new Padding(4), + BackColor = Color.FromArgb(30, 30, 35) + }; + + // Pattern input row + var lblNew = new Label { Text = "Pattern:", AutoSize = true, Margin = new Padding(4, 12, 4, 0) }; + txtNewPattern = new TextBox { Width = 280, Margin = new Padding(0, 10, 4, 0), PlaceholderText = "e.g. C:\\Games\\MyGame\\* or ^.*chrome.*$" }; + + rdoGlob = new RadioButton { Text = "Glob", Checked = true, AutoSize = true, Margin = new Padding(4, 12, 4, 0), ForeColor = Color.FromArgb(220, 220, 220) }; + rdoRegex = new RadioButton { Text = "Regex", AutoSize = true, Margin = new Padding(4, 12, 4, 0), ForeColor = Color.FromArgb(220, 220, 220) }; + + btnAddPattern = MakeButton("+ Add Rule", 100, Color.FromArgb(0, 150, 80)); + btnAddPattern.Margin = new Padding(8, 8, 4, 0); + btnAddPattern.Click += OnAddPattern; + + btnRemovePattern = MakeButton("✕ Remove", 90, Color.FromArgb(180, 40, 40)); + btnRemovePattern.Margin = new Padding(4, 8, 4, 0); + btnRemovePattern.Click += OnRemovePattern; + + chkWatcherEnabled = new CheckBox + { + Text = "Enable Auto-Inject Watcher", + ForeColor = Color.LightGreen, + AutoSize = true, + Margin = new Padding(16, 12, 4, 0) + }; + chkWatcherEnabled.CheckedChanged += OnWatcherToggle; + + toolbar.Controls.AddRange(new Control[] + { + lblNew, txtNewPattern, rdoGlob, rdoRegex, + btnAddPattern, btnRemovePattern, chkWatcherEnabled + }); + + var lblSection = MakeSectionLabel("Auto-Inject Rules (process path or name must match)"); + + dgvPatterns = MakeGrid(); + dgvPatterns.Dock = DockStyle.Fill; + dgvPatterns.Columns.AddRange( + Col("Pattern", 320), Col("Type", 60), BoolCol("Enabled")); + + panel.Controls.Add(dgvPatterns); + panel.Controls.Add(lblSection); + panel.Controls.Add(toolbar); + + tabPatterns.Controls.Add(panel); + } + + private void OnAddPattern(object sender, EventArgs e) + { + var pat = txtNewPattern.Text.Trim(); + if (string.IsNullOrEmpty(pat)) return; + + var rule = new PatternRule + { + Pattern = pat, + UseRegex = rdoRegex.Checked, + Enabled = true + }; + _watcher.AddRule(rule); + dgvPatterns.Rows.Add(pat, rule.UseRegex ? "Regex" : "Glob", true); + txtNewPattern.Clear(); + } + + private void OnRemovePattern(object sender, EventArgs e) + { + if (dgvPatterns.SelectedRows.Count == 0) return; + var idx = dgvPatterns.SelectedRows[0].Index; + var pat = dgvPatterns.Rows[idx].Cells[0].Value?.ToString(); + _watcher.ClearRules(); + dgvPatterns.Rows.RemoveAt(idx); + // Re-add remaining + foreach (DataGridViewRow row in dgvPatterns.Rows) + { + _watcher.AddRule(new PatternRule + { + Pattern = row.Cells[0].Value?.ToString() ?? "", + UseRegex = row.Cells[1].Value?.ToString() == "Regex", + Enabled = (bool)(row.Cells[2].Value ?? true) + }); + } + } + + private void OnWatcherToggle(object sender, EventArgs e) + { + if (chkWatcherEnabled.Checked) + { + _watcher.FakeUtc = GetFakeTime().ToUniversalTime(); + _watcher.MockEnabled = chkMockEnabled.Checked; + _watcher.Start(); + AppendLog("Process watcher started."); + } + else + { + _watcher.Stop(); + AppendLog("Process watcher stopped."); + } + } + + // ===================================================================== + // Log Tab + // ===================================================================== + private void BuildLogTab() + { + rtbLog = new RichTextBox + { + Dock = DockStyle.Fill, + BackColor = Color.FromArgb(18, 18, 22), + ForeColor = Color.FromArgb(180, 240, 180), + Font = new Font("Consolas", 9f), + ReadOnly = true, + ScrollBars= RichTextBoxScrollBars.Vertical + }; + + btnClearLog = MakeButton("Clear", 70, Color.FromArgb(60, 60, 70)); + btnClearLog.Dock = DockStyle.Bottom; + btnClearLog.Click += (s, e) => rtbLog.Clear(); + + tabLog.Controls.Add(rtbLog); + tabLog.Controls.Add(btnClearLog); + } + + // ===================================================================== + // Time Logic + // ===================================================================== + private DateTime GetFakeTime() => + dtpDate.Value.Date + dtpTime.Value.TimeOfDay; + + private void ApplyTime() + { + var dt = GetFakeTime().ToUniversalTime(); + _injMgr.SetFakeTimeAll(dt, chkMockEnabled.Checked); + _watcher.FakeUtc = dt; + _watcher.MockEnabled = chkMockEnabled.Checked; + + if (chkAutoAdvance.Checked) + { + _fakeTimeBase = GetFakeTime(); + _advanceStartReal = DateTime.Now; + } + UpdateTimePreview(); + } + + private void UpdateTimePreview() + { + var dt = GetFakeTime(); + lblPreview.Text = chkMockEnabled.Checked + ? $"Fake: {dt:yyyy-MM-dd HH:mm:ss} (local)" + : "Pass-through (real time)"; + } + + private void ToggleAutoAdvance(object sender, EventArgs e) + { + if (chkAutoAdvance.Checked) + { + _fakeTimeBase = GetFakeTime(); + _advanceStartReal = DateTime.Now; + _advanceTimer.Start(); + } + else + { + _advanceTimer.Stop(); + } + } + + private void AdvanceTick(object sender, EventArgs e) + { + if (!chkMockEnabled.Checked) return; + var elapsed = DateTime.Now - _advanceStartReal; + var advanced = _fakeTimeBase + elapsed; + // Silently update dtpDate/dtpTime without triggering ValueChanged loop + dtpDate.ValueChanged -= (s, ev) => UpdateTimePreview(); + dtpTime.ValueChanged -= (s, ev) => UpdateTimePreview(); + dtpDate.Value = advanced; + dtpTime.Value = advanced; + dtpDate.ValueChanged += (s, ev) => UpdateTimePreview(); + dtpTime.ValueChanged += (s, ev) => UpdateTimePreview(); + ApplyTime(); + } + + // ===================================================================== + // Helpers + // ===================================================================== + private void AppendLog(string msg) + { + if (rtbLog == null) return; + if (rtbLog.InvokeRequired) { rtbLog.BeginInvoke((Action)(() => AppendLog(msg))); return; } + rtbLog.AppendText($"[{DateTime.Now:HH:mm:ss}] {msg}\n"); + rtbLog.ScrollToCaret(); + } + + private int? GetSelectedProcessId(DataGridView dgv) + { + if (dgv.SelectedRows.Count == 0) return null; + var cell = dgv.SelectedRows[0].Cells[0].Value; + return cell is int i ? i : (int?)null; + } + + private static DataGridView MakeGrid() + { + var g = new DataGridView + { + Dock = DockStyle.Fill, + ReadOnly = true, + AllowUserToAddRows = false, + AllowUserToDeleteRows = false, + SelectionMode = DataGridViewSelectionMode.FullRowSelect, + MultiSelect = false, + BackgroundColor = Color.FromArgb(25, 25, 30), + ForeColor = Color.FromArgb(220, 220, 220), + GridColor = Color.FromArgb(50, 50, 55), + BorderStyle = BorderStyle.None, + RowHeadersVisible = false, + AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.None, + ColumnHeadersHeight = 28 + }; + g.EnableHeadersVisualStyles = false; + g.ColumnHeadersDefaultCellStyle.BackColor = Color.FromArgb(40, 40, 48); + g.ColumnHeadersDefaultCellStyle.ForeColor = Color.FromArgb(160, 200, 255); + g.DefaultCellStyle.SelectionBackColor = Color.FromArgb(0, 90, 160); + g.AlternatingRowsDefaultCellStyle.BackColor = Color.FromArgb(30, 30, 38); + return g; + } + + private static DataGridViewTextBoxColumn Col(string name, int w) => + new DataGridViewTextBoxColumn { HeaderText = name, Width = w, SortMode = DataGridViewColumnSortMode.NotSortable }; + + private static DataGridViewCheckBoxColumn BoolCol(string name) => + new DataGridViewCheckBoxColumn { HeaderText = name, Width = 65 }; + + private static Button MakeButton(string text, int w, Color bg) + { + return new Button + { + Text = text, + Width = w, + Height = 26, + BackColor = bg, + ForeColor = Color.White, + FlatStyle = FlatStyle.Flat, + Cursor = Cursors.Hand + }; + } + + private static Label MakeSectionLabel(string text) => + new Label + { + Text = text, + Dock = DockStyle.Top, + Height = 22, + ForeColor = Color.FromArgb(130, 200, 255), + Font = new Font("Segoe UI", 8.5f, FontStyle.Bold), + Padding = new Padding(4, 2, 0, 0), + BackColor = Color.FromArgb(35, 35, 42) + }; + + private static void StyleTab(TabPage tab) + { + tab.BackColor = Color.FromArgb(30, 30, 35); + tab.ForeColor = Color.FromArgb(220, 220, 220); + } + + private void DrawTab(object sender, DrawItemEventArgs e) + { + var tab = (TabControl)sender; + var page = tab.TabPages[e.Index]; + var rect = e.Bounds; + bool selected = e.Index == tab.SelectedIndex; + + using var bg = new SolidBrush(selected ? Color.FromArgb(0, 90, 160) : Color.FromArgb(40, 40, 48)); + e.Graphics.FillRectangle(bg, rect); + + var sf = new System.Drawing.StringFormat { Alignment = StringAlignment.Center, LineAlignment = StringAlignment.Center }; + using var fg = new SolidBrush(selected ? Color.White : Color.FromArgb(180, 180, 180)); + e.Graphics.DrawString(page.Text, Font, fg, rect, sf); + } + + private static void ShowInfo(string msg) => + MessageBox.Show(msg, "Info", MessageBoxButtons.OK, MessageBoxIcon.Information); + + protected override void OnFormClosing(FormClosingEventArgs e) + { + _watcher.Stop(); + _injMgr.Dispose(); + base.OnFormClosing(e); + } + } +} diff --git a/csharp/TimeMocker.UI/Program.cs b/csharp/TimeMocker.UI/Program.cs new file mode 100644 index 0000000..45a43a9 --- /dev/null +++ b/csharp/TimeMocker.UI/Program.cs @@ -0,0 +1,52 @@ +using System; +using System.Windows.Forms; +using TimeMocker.UI.Forms; + +namespace TimeMocker.UI +{ + internal static class Program + { + [STAThread] + static void Main() + { + Application.EnableVisualStyles(); + Application.SetCompatibleTextRenderingDefault(false); + + // EasyHook requires elevated privileges for cross-process injection + if (!IsElevated()) + { + var result = MessageBox.Show( + "TimeMocker needs to run as Administrator to inject into other processes.\n\n" + + "Please restart as Administrator.", + "Elevation Required", + MessageBoxButtons.OKCancel, + MessageBoxIcon.Warning); + + if (result == DialogResult.OK) + RestartAsAdmin(); + return; + } + + Application.Run(new MainForm()); + } + + private static bool IsElevated() + { + using var id = System.Security.Principal.WindowsIdentity.GetCurrent(); + var principal = new System.Security.Principal.WindowsPrincipal(id); + return principal.IsInRole(System.Security.Principal.WindowsBuiltInRole.Administrator); + } + + private static void RestartAsAdmin() + { + var info = new System.Diagnostics.ProcessStartInfo + { + FileName = Application.ExecutablePath, + UseShellExecute = true, + Verb = "runas" + }; + try { System.Diagnostics.Process.Start(info); } + catch { /* user cancelled UAC */ } + } + } +} diff --git a/csharp/TimeMocker.UI/TimeMocker.UI.csproj b/csharp/TimeMocker.UI/TimeMocker.UI.csproj new file mode 100644 index 0000000..517c918 --- /dev/null +++ b/csharp/TimeMocker.UI/TimeMocker.UI.csproj @@ -0,0 +1,19 @@ + + + net48 + x64 + WinExe + TimeMocker + TimeMocker.UI + true + true + app.ico + + + + + + + + + diff --git a/csharp/TimeMocker.sln b/csharp/TimeMocker.sln new file mode 100644 index 0000000..09f53f2 --- /dev/null +++ b/csharp/TimeMocker.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TimeMocker.UI", "TimeMocker.UI\TimeMocker.UI.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TimeMocker.Hook", "TimeMocker.Hook\TimeMocker.Hook.csproj", "{B2C3D4E5-F6A7-8901-BCDE-F12345678901}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|x64 = Debug|x64 + Release|x64 = Release|x64 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|x64.ActiveCfg = Debug|x64 + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|x64.Build.0 = Debug|x64 + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x64.ActiveCfg = Release|x64 + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x64.Build.0 = Release|x64 + {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|x64.ActiveCfg = Debug|x64 + {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|x64.Build.0 = Debug|x64 + {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|x64.ActiveCfg = Release|x64 + {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|x64.Build.0 = Release|x64 + EndGlobalSection +EndGlobal