feat(init): from claude

This commit is contained in:
2026-02-25 14:03:46 +07:00
parent 2184ab5d6c
commit ee2ebfa67f
10 changed files with 1327 additions and 1 deletions
+100 -1
View File
@@ -1 +1,100 @@
# time-mocker
# 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_<PID>
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
@@ -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
{
/// <summary>
/// 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.
/// </summary>
[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<MockTimeInfo>());
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;
}
}
}
@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net48</TargetFramework>
<Platforms>x64</Platforms>
<AssemblyName>TimeMocker.Hook</AssemblyName>
<RootNamespace>TimeMocker.Hook</RootNamespace>
<OutputType>Library</OutputType>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="EasyHook" Version="2.7.7030.0" />
</ItemGroup>
</Project>
@@ -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<int, InjectedProcess> _injected
= new Dictionary<int, InjectedProcess>();
private static readonly string HookDllPath =
Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "TimeMocker.Hook.dll");
public event Action<string> 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<InjectedProcess> 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();
}
}
}
+118
View File
@@ -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<PatternRule> _rules = new List<PatternRule>();
private readonly HashSet<int> _seen = new HashSet<int>();
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<string> LogMessage;
public event Action<InjectedProcess> ProcessAutoInjected;
public IReadOnlyList<PatternRule> 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();
}
}
@@ -0,0 +1,52 @@
using System;
using System.IO.MemoryMappedFiles;
using System.Runtime.InteropServices;
namespace TimeMocker.UI.Core
{
/// <summary>
/// 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.
/// </summary>
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<MockTimeInfo>();
_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;
}
}
+638
View File
@@ -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<ProcessRow> _allRows = new List<ProcessRow>();
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);
}
}
}
+52
View File
@@ -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 */ }
}
}
}
+19
View File
@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net48</TargetFramework>
<Platforms>x64</Platforms>
<OutputType>WinExe</OutputType>
<AssemblyName>TimeMocker</AssemblyName>
<RootNamespace>TimeMocker.UI</RootNamespace>
<UseWindowsForms>true</UseWindowsForms>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<ApplicationIcon>app.ico</ApplicationIcon>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="EasyHook" Version="2.7.7030.0" />
</ItemGroup>
<ItemGroup>
<!-- Ensure the hook DLL is always next to the UI exe -->
<ProjectReference Include="..\TimeMocker.Hook\TimeMocker.Hook.csproj" />
</ItemGroup>
</Project>
+25
View File
@@ -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