From 1b244687595aaef848fb5d4f22167f26a9bdf259 Mon Sep 17 00:00:00 2001 From: tiennm99 Date: Fri, 27 Feb 2026 17:30:37 +0700 Subject: [PATCH] chore: init first version --- .gitignore | 56 +-- CMakeLists.txt | 63 +++ README.md | 240 ++++++++- Shared/MockTimeInfo.h | 30 ++ TimeMocker.Hook/TimeMocker.Hook.def | 7 + TimeMocker.Hook/TimeMocker.Hook.vcxproj | 140 ++++++ TimeMocker.Hook/dllmain.cpp | 311 ++++++++++++ TimeMocker.Hook/exports.cpp | 7 + TimeMocker.Injector/InjectionManager.cpp | 318 ++++++++++++ TimeMocker.Injector/InjectionManager.h | 110 +++++ TimeMocker.Injector/ProcessWatcher.h | 188 +++++++ .../TimeMocker.Injector.vcxproj | 71 +++ TimeMocker.UI/TimeMocker.UI.vcxproj | 95 ++++ TimeMocker.UI/main.cpp | 461 ++++++++++++++++++ TimeMocker.sln | 41 ++ scripts/setup.ps1 | 64 +++ 16 files changed, 2159 insertions(+), 43 deletions(-) create mode 100644 CMakeLists.txt create mode 100644 Shared/MockTimeInfo.h create mode 100644 TimeMocker.Hook/TimeMocker.Hook.def create mode 100644 TimeMocker.Hook/TimeMocker.Hook.vcxproj create mode 100644 TimeMocker.Hook/dllmain.cpp create mode 100644 TimeMocker.Hook/exports.cpp create mode 100644 TimeMocker.Injector/InjectionManager.cpp create mode 100644 TimeMocker.Injector/InjectionManager.h create mode 100644 TimeMocker.Injector/ProcessWatcher.h create mode 100644 TimeMocker.Injector/TimeMocker.Injector.vcxproj create mode 100644 TimeMocker.UI/TimeMocker.UI.vcxproj create mode 100644 TimeMocker.UI/main.cpp create mode 100644 TimeMocker.sln create mode 100644 scripts/setup.ps1 diff --git a/.gitignore b/.gitignore index d4fb281..5a79219 100644 --- a/.gitignore +++ b/.gitignore @@ -1,41 +1,15 @@ -# Prerequisites -*.d - -# Compiled Object files -*.slo -*.lo -*.o -*.obj - -# Precompiled Headers -*.gch -*.pch - -# Linker files -*.ilk - -# Debugger Files -*.pdb - -# Compiled Dynamic libraries -*.so -*.dylib -*.dll - -# Fortran module files -*.mod -*.smod - -# Compiled Static libraries -*.lai -*.la -*.a -*.lib - -# Executables -*.exe -*.out -*.app - -# debug information files -*.dwo +.vs/ +x64/ +x86/ +Debug/ +Release/ +*.user +*.aps +*.suo +*.db +*.opendb +vcpkg/ +packages/detours/lib/ +packages/detours/include/ +build/ +out/ diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..c622901 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,63 @@ +cmake_minimum_required(VERSION 3.20) +project(TimeMocker CXX) + +# ── vcpkg toolchain (set via: cmake -DCMAKE_TOOLCHAIN_FILE=/scripts/buildsystems/vcpkg.cmake) +find_package(detours CONFIG REQUIRED) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# Require Windows +if (NOT WIN32) + message(FATAL_ERROR "TimeMocker is Windows-only (uses Win32 API and MS Detours).") +endif() + +add_compile_definitions(WIN32_LEAN_AND_MEAN UNICODE _UNICODE) + +# ── Shared headers ──────────────────────────────────────────────────────────── +set(SHARED_DIR ${CMAKE_SOURCE_DIR}/Shared) + +# ── TimeMocker.Hook (DLL) ───────────────────────────────────────────────────── +add_library(TimeMocker.Hook SHARED + TimeMocker.Hook/dllmain.cpp + TimeMocker.Hook/exports.cpp + TimeMocker.Hook/TimeMocker.Hook.def +) +target_include_directories(TimeMocker.Hook PRIVATE ${SHARED_DIR}) +target_link_libraries(TimeMocker.Hook PRIVATE detours::detours) +set_target_properties(TimeMocker.Hook PROPERTIES + OUTPUT_NAME "TimeMocker.Hook" + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin" +) + +# ── TimeMocker.Injector (static lib) ───────────────────────────────────────── +add_library(TimeMocker.Injector STATIC + TimeMocker.Injector/InjectionManager.cpp +) +target_include_directories(TimeMocker.Injector + PUBLIC ${CMAKE_SOURCE_DIR}/TimeMocker.Injector + PRIVATE ${SHARED_DIR} +) +target_link_libraries(TimeMocker.Injector PUBLIC detours::detours Psapi) + +# ── TimeMocker.UI (console exe) ─────────────────────────────────────────────── +add_executable(TimeMocker.UI + TimeMocker.UI/main.cpp +) +target_include_directories(TimeMocker.UI PRIVATE ${SHARED_DIR}) +target_link_libraries(TimeMocker.UI PRIVATE TimeMocker.Injector detours::detours) +set_target_properties(TimeMocker.UI PROPERTIES + OUTPUT_NAME "TimeMocker" + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin" +) + +# Copy hook DLLs next to the UI executable after build +add_custom_command(TARGET TimeMocker.UI POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + $ + "${CMAKE_BINARY_DIR}/bin/TimeMocker.Hook.x64.dll" + COMMENT "Copying Hook DLL (x64) → bin/" +) + +install(TARGETS TimeMocker.UI TimeMocker.Hook + RUNTIME DESTINATION bin) diff --git a/README.md b/README.md index c3cd0af..6fec7c9 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,238 @@ -# time-mocker-cpp -[time-mocker](https://github.com/tiennm99/time-mocker) but written in C++ +# TimeMocker — C++ / MS Detours Edition + +A Windows tool that injects fake time into running processes by hooking Win32 time APIs using **Microsoft Detours**. + +Port/rewrite of the original C# / EasyHook version with identical IPC design but in native C++. + +--- + +## Architecture + +``` +TimeMocker.sln +├── Shared/ +│ └── MockTimeInfo.h ─ Named MMF layout (shared between UI and Hook) +│ +├── TimeMocker.Hook/ ─ DLL injected into target processes +│ ├── dllmain.cpp ─ DllMain: opens MMF, installs/removes Detours hooks +│ └── exports.cpp ─ Sentinel export for version check +│ +├── TimeMocker.Injector/ ─ Static library: injection + IPC management +│ ├── InjectionManager.h/.cpp ─ LoadLibrary remote-thread injection + SharedMemoryHandle +│ └── ProcessWatcher.h ─ Background thread: auto-inject via glob/regex rules +│ +└── TimeMocker.UI/ + └── main.cpp ─ Interactive console controller +``` + +--- + +## Hooked APIs + +| API | DLL | +|-----|-----| +| `GetSystemTime` | kernel32 | +| `GetLocalTime` | kernel32 | +| `GetSystemTimeAsFileTime` | kernel32 | +| `GetSystemTimePreciseAsFileTime` | kernel32 | +| `NtQuerySystemTime` | ntdll | + +All hooks read `DeltaTicks` from the named MMF on each call and return +`real_utc_filetime_ticks + DeltaTicks`. When `DeltaTicks == 0` the real +time passes through unchanged. + +--- + +## IPC Design + +``` +Named Memory-Mapped File: TimeMocker_ +Size: 8 bytes + +[0..7] DeltaTicks (LONGLONG, 100-ns units, signed) + = fakeUtcTicks - realUtcTicks at the moment the user clicks "Set" +``` + +- The UI creates the MMF **before** injection so the hook can open it in `DllMain`. +- Updates are written as atomic `InterlockedExchange64` — no locks on the hot path. +- The hook reads with a single volatile 8-byte load (~4 ns, no syscall). +- When the UI closes the MMF (on eject/exit), the hook's next read returns `0`, + silently falling back to real time. + +--- + +## How Detours Works Here + +``` +Before injection: + kernel32!GetSystemTime → [real implementation] + +After injection: + kernel32!GetSystemTime → Hook_GetSystemTime (our function) + → Real_GetSystemTime (trampoline, via DetourAttach) +``` + +`DetourAttach` patches the first 5–14 bytes of the real function with a +`JMP` to our hook, and saves the overwritten bytes + a JMP back in a +trampoline so we can call the original. + +--- + +## Requirements + +- **Windows 10/11 x64** +- **Visual Studio 2022** (v143 toolset) or **CMake 3.20+** +- **Microsoft Detours** (installed via `scripts/setup.ps1` → vcpkg) +- Must run as **Administrator** (cross-process injection requires elevated privileges) + +--- + +## Setup & Build + +### Option A — Visual Studio 2022 + +```powershell +# 1. Install Detours via vcpkg (one-time) +powershell -ExecutionPolicy Bypass -File scripts\setup.ps1 + +# 2. Open solution +start TimeMocker.sln + +# 3. Build → Release | x64 +``` + +Output: +``` +x64\Release\TimeMocker.exe ← controller +x64\Release\TimeMocker.Hook.x64.dll ← hook DLL (must be next to .exe) +x64\Release\TimeMocker.Hook.x86.dll ← hook DLL for 32-bit targets +``` + +### Option B — CMake + vcpkg + +```powershell +# Install vcpkg + detours +git clone https://github.com/microsoft/vcpkg vcpkg +.\vcpkg\bootstrap-vcpkg.bat -disableMetrics +.\vcpkg\vcpkg install detours:x64-windows detours:x86-windows + +# Configure + build +cmake -B build -DCMAKE_TOOLCHAIN_FILE=vcpkg/scripts/buildsystems/vcpkg.cmake -A x64 +cmake --build build --config Release +``` + +--- + +## Usage (Console Controller) + +``` +timemocker> help + + list [filter] list running processes + inject inject hook DLL into process + eject remove hook from process + time YYYY-MM-DD HH:MM:SS set fake time (local) + time now reset to real time + status show injected processes + delta + rule add add glob auto-inject rule + rule add -r add regex auto-inject rule + rule list list rules + rule del remove rule by index + watch start / stop control auto-inject watcher + quit exit +``` + +### Examples + +``` +# List all processes with "notepad" in name/path +timemocker> list notepad + +# Inject into Notepad (PID 12345) and set time to Jan 1 2020 +timemocker> time 2020-01-01 00:00:00 +timemocker> inject 12345 + +# Change time while injected (takes effect immediately) +timemocker> time 2025-12-31 23:59:00 + +# Auto-inject any process matching a glob +timemocker> rule add C:\Games\MyGame\* + +# Auto-inject chrome.exe by name +timemocker> rule add *chrome* + +# Auto-inject using regex +timemocker> rule add -r ^.*\\MyApp\.exe$ + +# Reset to real time and eject +timemocker> time now +timemocker> eject 12345 +``` + +--- + +## Project Details + +### DLL Injection Method + +Uses the classic **LoadLibrary remote thread** technique: + +1. `OpenProcess` with `PROCESS_CREATE_THREAD | PROCESS_VM_*` rights +2. `VirtualAllocEx` → write DLL path string into target's memory +3. `CreateRemoteThread(LoadLibraryW, dllPath)` → loads the DLL in-process +4. `WaitForSingleObject` → confirm load completed +5. On `DLL_PROCESS_ATTACH` the hook's `DllMain` opens the MMF and installs Detours + +For **new processes** (before they start), `DetourCreateProcessWithDllEx()` can also +be used — it's included in the Detours SDK and injects before the first instruction +runs, before any DRM/anti-cheat initialises. + +### 32-bit vs 64-bit + +- **64-bit target**: `TimeMocker.Hook.x64.dll` is injected +- **32-bit target** (WOW64): `TimeMocker.Hook.x86.dll` is injected +- Both Hook DLLs are identical source; the platform is selected at build time +- The UI (x64) determines which DLL to use by calling `IsWow64Process` + +### Auto-Inject Watcher + +`ProcessWatcher` runs a background thread that polls `CreateToolhelp32Snapshot` +every 1.5 seconds. Any process whose full path or `.exe` name matches an enabled +rule is automatically injected with the current fake time delta. + +Previously-seen PIDs are tracked in a `HashSet` so they're only matched once, +even if the process restarts with the same PID (very rare on Windows). + +### Limitations + +| Limitation | Notes | +|-----------|-------| +| **64-bit injector only** | The UI is x64-only; a separate x86 injector would be needed for 32-bit-only scenarios | +| **Anti-cheat / protected processes** | EAC, BattlEye, and system PPL processes block `OpenProcess` | +| **QueryPerformanceCounter** | Not affected — it reads a hardware MSR; EasyHook/Detours cannot intercept kernel MSR reads | +| **GetTickCount / timeGetTime** | Not hooked in this version — easy to add following the same pattern | +| **Hot eject** | DLL stays loaded but hooks are removed on unload (not forcible without `FreeLibrary` in a remote thread) | + +--- + +## Adding a New Hook + +1. Declare the real function pointer in `dllmain.cpp`: + ```cpp + static DWORD (WINAPI* Real_GetTickCount)() = GetTickCount; + ``` +2. Write the hook function: + ```cpp + static DWORD WINAPI Hook_GetTickCount() + { + // Convert fake UTC ticks to milliseconds since boot (approximate) + return static_cast(GetFakeUtcTicks() / 10000); + } + ``` +3. Add `DetourAttach` / `DetourDetach` calls in `InstallHooks` / `RemoveHooks`. + +--- + +## License + +Apache 2.0 (same as the original C# project). diff --git a/Shared/MockTimeInfo.h b/Shared/MockTimeInfo.h new file mode 100644 index 0000000..4467326 --- /dev/null +++ b/Shared/MockTimeInfo.h @@ -0,0 +1,30 @@ +#pragma once +// ============================================================================= +// TimeMocker Shared IPC +// Named Memory-Mapped File layout shared between UI and Hook DLL. +// One MMF per injected process, named: TimeMocker_ +// ============================================================================= + +#define WIN32_LEAN_AND_MEAN +#include + +static const wchar_t* MMF_PREFIX = L"TimeMocker_"; +static const DWORD MMF_SIZE = sizeof(LONGLONG); // 8 bytes: DeltaTicks (Int64) + +// The only field: signed tick offset to add to QueryUnbiasedInterruptTime / FILETIME. +// A tick = 100 nanoseconds (same unit as FILETIME / SYSTEMTIME internals). +// DeltaTicks == 0 → real time (pass-through) +// DeltaTicks > 0 → future (clock is ahead) +// DeltaTicks < 0 → past (clock is behind) +#pragma pack(push, 1) +struct MockTimeInfo +{ + LONGLONG DeltaTicks; // Offset to add to the real UTC FILETIME ticks +}; +#pragma pack(pop) + +// Helper: build the MMF name for a given PID +inline void GetMmfName(DWORD pid, wchar_t* buf, size_t cchBuf) +{ + swprintf_s(buf, cchBuf, L"%ls%lu", MMF_PREFIX, pid); +} diff --git a/TimeMocker.Hook/TimeMocker.Hook.def b/TimeMocker.Hook/TimeMocker.Hook.def new file mode 100644 index 0000000..9293c4f --- /dev/null +++ b/TimeMocker.Hook/TimeMocker.Hook.def @@ -0,0 +1,7 @@ +LIBRARY TimeMocker.Hook + +EXPORTS + ; No public exports required — Detours helper process detection uses + ; an internal mechanism. We export a sentinel for the injector to + ; verify the DLL loaded correctly. + TimeMockerHookVersion @1 diff --git a/TimeMocker.Hook/TimeMocker.Hook.vcxproj b/TimeMocker.Hook/TimeMocker.Hook.vcxproj new file mode 100644 index 0000000..40210b6 --- /dev/null +++ b/TimeMocker.Hook/TimeMocker.Hook.vcxproj @@ -0,0 +1,140 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + + {A1111111-1111-1111-1111-111111111111} + Win32Proj + TimeMockerHook + 10.0 + + + + + + DynamicLibrary + true + v143 + Unicode + + + DynamicLibrary + false + v143 + true + Unicode + + + DynamicLibrary + true + v143 + Unicode + + + DynamicLibrary + false + v143 + true + Unicode + + + + + + + + Level3 + true + true + + $(SolutionDir)packages\detours\include;$(SolutionDir)Shared;%(AdditionalIncludeDirectories) + WIN32_LEAN_AND_MEAN;%(PreprocessorDefinitions) + + + + detours.lib;%(AdditionalDependencies) + TimeMocker.Hook.def + + + + + + + Disabled + _DEBUG;TIMEMOCKER_HOOK_EXPORTS;%(PreprocessorDefinitions) + MultiThreadedDebugDLL + + + $(SolutionDir)packages\detours\lib\x86;%(AdditionalLibraryDirectories) + + + + + + MaxSpeed + true + true + NDEBUG;TIMEMOCKER_HOOK_EXPORTS;%(PreprocessorDefinitions) + MultiThreadedDLL + + + $(SolutionDir)packages\detours\lib\x86;%(AdditionalLibraryDirectories) + true + true + + + + + + Disabled + _DEBUG;TIMEMOCKER_HOOK_EXPORTS;%(PreprocessorDefinitions) + MultiThreadedDebugDLL + + + $(SolutionDir)packages\detours\lib\x64;%(AdditionalLibraryDirectories) + + + + + + MaxSpeed + true + true + NDEBUG;TIMEMOCKER_HOOK_EXPORTS;%(PreprocessorDefinitions) + MultiThreadedDLL + + + $(SolutionDir)packages\detours\lib\x64;%(AdditionalLibraryDirectories) + true + true + + + + + + + + + + + + + + diff --git a/TimeMocker.Hook/dllmain.cpp b/TimeMocker.Hook/dllmain.cpp new file mode 100644 index 0000000..241332d --- /dev/null +++ b/TimeMocker.Hook/dllmain.cpp @@ -0,0 +1,311 @@ +// ============================================================================= +// TimeMocker.Hook — MS Detours-based time hooking DLL +// +// Injected into a target process by TimeMocker.Injector. +// Opens a named Memory-Mapped File created by the UI: +// Name: TimeMocker_ +// Data: MockTimeInfo { LONGLONG DeltaTicks } +// +// Hooks 5 Win32 time APIs: +// kernel32!GetSystemTime +// kernel32!GetLocalTime +// kernel32!GetSystemTimeAsFileTime +// kernel32!GetSystemTimePreciseAsFileTime +// ntdll!NtQuerySystemTime +// +// The delta (DeltaTicks) is added to the real UTC FILETIME on every call. +// When DeltaTicks == 0 the real time passes through unchanged. +// ============================================================================= + +#define WIN32_LEAN_AND_MEAN +#include +#include +#include +#include + +#include "../Shared/MockTimeInfo.h" + +// --------------------------------------------------------------------------- +// Module-level state +// --------------------------------------------------------------------------- +static HANDLE g_hMapFile = nullptr; +static LPVOID g_pView = nullptr; +static wchar_t g_logPath[MAX_PATH]; + +// --------------------------------------------------------------------------- +// Logging (writes to %TEMP%\TimeMocker.Hook.log) +// --------------------------------------------------------------------------- +static void Log(const char* fmt, ...) +{ + FILE* f = nullptr; + if (_wfopen_s(&f, g_logPath, L"a") == 0 && f) + { + SYSTEMTIME st; + GetLocalTime(&st); // NOTE: intentional real time call before hooks install + fprintf(f, "[%02d:%02d:%02d] ", st.wHour, st.wMinute, st.wSecond); + va_list va; + va_start(va, fmt); + vfprintf(f, fmt, va); + va_end(va); + fprintf(f, "\n"); + fclose(f); + } +} + +// --------------------------------------------------------------------------- +// Read the delta from shared memory (inline, hot path) +// --------------------------------------------------------------------------- +static inline LONGLONG ReadDeltaTicks() +{ + if (!g_pView) return 0LL; + // Atomic-friendly: LONGLONG is 8-byte aligned, single read is atomic on x86/x64 + return *reinterpret_cast(g_pView); +} + +// --------------------------------------------------------------------------- +// FILETIME helpers +// --------------------------------------------------------------------------- +static const LONGLONG FILETIME_EPOCH_BIAS = 116444736000000000LL; // Jan 1, 1601 → Jan 1, 1970 + +// Convert a FILETIME (100-ns ticks since Jan 1, 1601 UTC) to a SYSTEMTIME UTC +static void FileTimeToSystemTimeLocal(LONGLONG ticks, SYSTEMTIME* pSt, bool localTime) +{ + FILETIME ft; + ft.dwLowDateTime = static_cast(ticks & 0xFFFFFFFF); + ft.dwHighDateTime = static_cast(ticks >> 32); + + if (localTime) + { + FILETIME localFt; + FileTimeToLocalFileTime(&ft, &localFt); + FileTimeToSystemTime(&localFt, pSt); + } + else + { + FileTimeToSystemTime(&ft, pSt); + } +} + +// Get real UTC as 100-ns ticks (FILETIME ticks) +static inline LONGLONG GetRealUtcTicks() +{ + FILETIME ft; + // Call the REAL GetSystemTimeAsFileTime (trampoline, set up after DetourAttach) + // We use a raw read of the real function pointer stored in the trampoline. + // To avoid recursion during hook installation we use a flag. + ULARGE_INTEGER ui; + GetSystemTimeAsFileTime(&ft); // will be redirected after hooks are live; see note below + ui.LowPart = ft.dwLowDateTime; + ui.HighPart = ft.dwHighDateTime; + return static_cast(ui.QuadPart); +} + +// --------------------------------------------------------------------------- +// Original function pointers (Detours trampolines) +// --------------------------------------------------------------------------- +static void (WINAPI* Real_GetSystemTime)(LPSYSTEMTIME) = GetSystemTime; +static void (WINAPI* Real_GetLocalTime)(LPSYSTEMTIME) = GetLocalTime; +static void (WINAPI* Real_GetSystemTimeAsFileTime)(LPFILETIME) = GetSystemTimeAsFileTime; +static void (WINAPI* Real_GetSystemTimePreciseAsFileTime)(LPFILETIME) = GetSystemTimePreciseAsFileTime; + +typedef NTSTATUS (NTAPI* NtQuerySystemTimeFn)(PLARGE_INTEGER SystemTime); +static NtQuerySystemTimeFn Real_NtQuerySystemTime = nullptr; + +// --------------------------------------------------------------------------- +// Helper: get fake UTC ticks +// --------------------------------------------------------------------------- +static LONGLONG GetFakeUtcTicks() +{ + // Get real time via the trampoline (not the hook) + FILETIME ft; + Real_GetSystemTimeAsFileTime(&ft); + ULARGE_INTEGER ui; + ui.LowPart = ft.dwLowDateTime; + ui.HighPart = ft.dwHighDateTime; + return static_cast(ui.QuadPart) + ReadDeltaTicks(); +} + +// --------------------------------------------------------------------------- +// Hook implementations +// --------------------------------------------------------------------------- + +static void WINAPI Hook_GetSystemTime(LPSYSTEMTIME lpSystemTime) +{ + if (!lpSystemTime) return; + LONGLONG ticks = GetFakeUtcTicks(); + FileTimeToSystemTimeLocal(ticks, lpSystemTime, false); +} + +static void WINAPI Hook_GetLocalTime(LPSYSTEMTIME lpLocalTime) +{ + if (!lpLocalTime) return; + LONGLONG ticks = GetFakeUtcTicks(); + FileTimeToSystemTimeLocal(ticks, lpLocalTime, true); +} + +static void WINAPI Hook_GetSystemTimeAsFileTime(LPFILETIME lpFileTime) +{ + if (!lpFileTime) return; + LONGLONG ticks = GetFakeUtcTicks(); + lpFileTime->dwLowDateTime = static_cast(ticks & 0xFFFFFFFF); + lpFileTime->dwHighDateTime = static_cast(ticks >> 32); +} + +static void WINAPI Hook_GetSystemTimePreciseAsFileTime(LPFILETIME lpFileTime) +{ + // Same as GetSystemTimeAsFileTime — we can't improve precision beyond real time + Hook_GetSystemTimeAsFileTime(lpFileTime); +} + +static NTSTATUS NTAPI Hook_NtQuerySystemTime(PLARGE_INTEGER SystemTime) +{ + if (!SystemTime) return STATUS_ACCESS_VIOLATION; + SystemTime->QuadPart = GetFakeUtcTicks(); + return STATUS_SUCCESS; +} + +// --------------------------------------------------------------------------- +// Install / remove hooks +// --------------------------------------------------------------------------- +static bool InstallHooks() +{ + // Resolve NtQuerySystemTime from ntdll + HMODULE hNtdll = GetModuleHandleW(L"ntdll.dll"); + if (!hNtdll) + { + Log("ERROR: ntdll.dll not found"); + return false; + } + Real_NtQuerySystemTime = reinterpret_cast( + GetProcAddress(hNtdll, "NtQuerySystemTime")); + if (!Real_NtQuerySystemTime) + { + Log("ERROR: NtQuerySystemTime not found in ntdll"); + return false; + } + + DetourTransactionBegin(); + DetourUpdateThread(GetCurrentThread()); + + DetourAttach(reinterpret_cast(&Real_GetSystemTime), + reinterpret_cast(Hook_GetSystemTime)); + DetourAttach(reinterpret_cast(&Real_GetLocalTime), + reinterpret_cast(Hook_GetLocalTime)); + DetourAttach(reinterpret_cast(&Real_GetSystemTimeAsFileTime), + reinterpret_cast(Hook_GetSystemTimeAsFileTime)); + DetourAttach(reinterpret_cast(&Real_GetSystemTimePreciseAsFileTime), + reinterpret_cast(Hook_GetSystemTimePreciseAsFileTime)); + DetourAttach(reinterpret_cast(&Real_NtQuerySystemTime), + reinterpret_cast(Hook_NtQuerySystemTime)); + + LONG err = DetourTransactionCommit(); + if (err != NO_ERROR) + { + Log("ERROR: DetourTransactionCommit failed: %ld", err); + return false; + } + + Log("Hooks installed successfully (delta=%lld ticks)", ReadDeltaTicks()); + return true; +} + +static void RemoveHooks() +{ + DetourTransactionBegin(); + DetourUpdateThread(GetCurrentThread()); + + DetourDetach(reinterpret_cast(&Real_GetSystemTime), + reinterpret_cast(Hook_GetSystemTime)); + DetourDetach(reinterpret_cast(&Real_GetLocalTime), + reinterpret_cast(Hook_GetLocalTime)); + DetourDetach(reinterpret_cast(&Real_GetSystemTimeAsFileTime), + reinterpret_cast(Hook_GetSystemTimeAsFileTime)); + DetourDetach(reinterpret_cast(&Real_GetSystemTimePreciseAsFileTime), + reinterpret_cast(Hook_GetSystemTimePreciseAsFileTime)); + + if (Real_NtQuerySystemTime) + DetourDetach(reinterpret_cast(&Real_NtQuerySystemTime), + reinterpret_cast(Hook_NtQuerySystemTime)); + + DetourTransactionCommit(); + Log("Hooks removed"); +} + +// --------------------------------------------------------------------------- +// Open the shared memory created by the UI +// --------------------------------------------------------------------------- +static bool OpenSharedMemory(DWORD pid) +{ + wchar_t mmfName[64]; + GetMmfName(pid, mmfName, _countof(mmfName)); + + g_hMapFile = OpenFileMappingW(FILE_MAP_READ | FILE_MAP_WRITE, FALSE, mmfName); + if (!g_hMapFile) + { + Log("ERROR: OpenFileMapping('%ls') failed: %lu", mmfName, GetLastError()); + return false; + } + + g_pView = MapViewOfFile(g_hMapFile, FILE_MAP_READ | FILE_MAP_WRITE, 0, 0, MMF_SIZE); + if (!g_pView) + { + Log("ERROR: MapViewOfFile failed: %lu", GetLastError()); + CloseHandle(g_hMapFile); + g_hMapFile = nullptr; + return false; + } + + Log("Shared memory '%ls' opened (delta=%lld)", mmfName, ReadDeltaTicks()); + return true; +} + +static void CloseSharedMemory() +{ + if (g_pView) { UnmapViewOfFile(g_pView); g_pView = nullptr; } + if (g_hMapFile) { CloseHandle(g_hMapFile); g_hMapFile = nullptr; } +} + +// --------------------------------------------------------------------------- +// DllMain +// --------------------------------------------------------------------------- +BOOL WINAPI DllMain(HINSTANCE /*hInst*/, DWORD reason, LPVOID /*reserved*/) +{ + if (DetourIsHelperProcess()) return TRUE; + + switch (reason) + { + case DLL_PROCESS_ATTACH: + { + // Build log path: %TEMP%\TimeMocker.Hook.log + wchar_t tempDir[MAX_PATH]; + GetTempPathW(MAX_PATH, tempDir); + swprintf_s(g_logPath, _countof(g_logPath), L"%lsTimeMocker.Hook.log", tempDir); + + DWORD pid = GetCurrentProcessId(); + Log("DLL_PROCESS_ATTACH pid=%lu", pid); + + if (!OpenSharedMemory(pid)) + { + // UI hasn't created the MMF yet — this is fatal for injection + return FALSE; + } + + DisableThreadLibraryCalls(/*hInst*/ GetModuleHandleW(nullptr)); + + if (!InstallHooks()) + { + CloseSharedMemory(); + return FALSE; + } + break; + } + + case DLL_PROCESS_DETACH: + RemoveHooks(); + CloseSharedMemory(); + Log("DLL_PROCESS_DETACH"); + break; + } + + return TRUE; +} diff --git a/TimeMocker.Hook/exports.cpp b/TimeMocker.Hook/exports.cpp new file mode 100644 index 0000000..7d01846 --- /dev/null +++ b/TimeMocker.Hook/exports.cpp @@ -0,0 +1,7 @@ +#include + +// Exported sentinel — lets the injector verify the DLL was built correctly +extern "C" __declspec(dllexport) DWORD TimeMockerHookVersion() +{ + return 0x0001'0000; // v1.0 +} diff --git a/TimeMocker.Injector/InjectionManager.cpp b/TimeMocker.Injector/InjectionManager.cpp new file mode 100644 index 0000000..6710823 --- /dev/null +++ b/TimeMocker.Injector/InjectionManager.cpp @@ -0,0 +1,318 @@ +// ============================================================================= +// TimeMocker.Injector — InjectionManager.cpp +// +// Uses the classic LoadLibrary remote-thread injection technique: +// 1. Open the target process with sufficient rights +// 2. Write the DLL path into the target's address space +// 3. Create a remote thread that calls LoadLibraryW +// +// For new processes, DetourCreateProcessWithDllEx() can alternatively be used +// (see TimeMocker.Injector.CLI for an example). +// ============================================================================= + +#define WIN32_LEAN_AND_MEAN +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "InjectionManager.h" + +#pragma comment(lib, "Psapi.lib") + +// ============================================================================ +// SharedMemoryHandle +// ============================================================================ + +SharedMemoryHandle::SharedMemoryHandle(DWORD pid) +{ + wchar_t buf[64]; + GetMmfName(pid, buf, _countof(buf)); + m_name = buf; + + m_hMap = CreateFileMappingW( + INVALID_HANDLE_VALUE, nullptr, + PAGE_READWRITE, 0, MMF_SIZE, + m_name.c_str()); + + if (!m_hMap) return; + + m_pView = MapViewOfFile(m_hMap, FILE_MAP_ALL_ACCESS, 0, 0, MMF_SIZE); + if (!m_pView) + { + CloseHandle(m_hMap); + m_hMap = nullptr; + } + else + { + // Zero-initialise (DeltaTicks = 0 → real time) + ZeroMemory(m_pView, MMF_SIZE); + } +} + +SharedMemoryHandle::~SharedMemoryHandle() +{ + if (m_pView) UnmapViewOfFile(m_pView); + if (m_hMap) CloseHandle(m_hMap); +} + +void SharedMemoryHandle::Write(const MockTimeInfo& info) +{ + if (!m_pView) return; + // Atomic write on aligned 8-byte address on x64/x86 + InterlockedExchange64(reinterpret_cast(m_pView), info.DeltaTicks); +} + +// ============================================================================ +// TimeUtil +// ============================================================================ + +LONGLONG TimeUtil::RealUtcTicks() +{ + FILETIME ft; + GetSystemTimeAsFileTime(&ft); + ULARGE_INTEGER ui; + ui.LowPart = ft.dwLowDateTime; + ui.HighPart = ft.dwHighDateTime; + return static_cast(ui.QuadPart); +} + +LONGLONG TimeUtil::LocalSystemTimeToUtcTicks(const SYSTEMTIME& st) +{ + FILETIME localFt, utcFt; + SystemTimeToFileTime(&st, &localFt); + LocalFileTimeToFileTime(&localFt, &utcFt); + ULARGE_INTEGER ui; + ui.LowPart = utcFt.dwLowDateTime; + ui.HighPart = utcFt.dwHighDateTime; + return static_cast(ui.QuadPart); +} + +LONGLONG TimeUtil::ComputeDelta(LONGLONG fakeUtcTicks) +{ + return fakeUtcTicks - RealUtcTicks(); +} + +// ============================================================================ +// InjectionManager helpers +// ============================================================================ + +void InjectionManager::Log(const wchar_t* fmt, ...) const +{ + if (!OnLog) return; + wchar_t buf[1024]; + va_list va; + va_start(va, fmt); + vswprintf_s(buf, _countof(buf), fmt, va); + va_end(va); + OnLog(buf); +} + +std::wstring InjectionManager::ResolveHookDll(bool x64) const +{ + // Prefer explicit directory; fall back to the injector's own directory + std::wstring dir = m_hookDllDir; + if (dir.empty()) + { + wchar_t exe[MAX_PATH]; + GetModuleFileNameW(nullptr, exe, MAX_PATH); + wchar_t* slash = wcsrchr(exe, L'\\'); + if (slash) { *(slash + 1) = L'\0'; dir = exe; } + } + return dir + (x64 ? L"TimeMocker.Hook.x64.dll" : L"TimeMocker.Hook.x86.dll"); +} + +bool InjectionManager::IsProcess64Bit(HANDLE hProcess) +{ + BOOL wow64 = FALSE; + IsWow64Process(hProcess, &wow64); + // If we are 64-bit and the target is NOT WOW64, target is 64-bit +#ifdef _WIN64 + return !wow64; +#else + return false; // 32-bit injector can only inject x86 +#endif +} + +// ============================================================================ +// InjectionManager +// ============================================================================ + +InjectionManager::InjectionManager(const std::wstring& hookDllDir) + : m_hookDllDir(hookDllDir) +{ +} + +InjectionManager::~InjectionManager() +{ + std::lock_guard lk(m_mutex); + for (auto& kv : m_injected) + delete kv.second; + m_injected.clear(); +} + +bool InjectionManager::Inject(DWORD pid, LONGLONG fakeUtcTicks, std::wstring* pError) +{ + std::lock_guard lk(m_mutex); + + if (m_injected.count(pid)) + { + // Already injected — just update time + m_injected[pid]->Shm->Write({ TimeUtil::ComputeDelta(fakeUtcTicks) }); + return true; + } + + // ---- Open target process ----------------------------------------------- + HANDLE hProcess = OpenProcess( + PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION | + PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ, + FALSE, pid); + + if (!hProcess) + { + std::wstring err = L"OpenProcess failed: " + std::to_wstring(GetLastError()); + Log(L"[Inject] %ls", err.c_str()); + if (pError) *pError = err; + return false; + } + + bool is64 = IsProcess64Bit(hProcess); + std::wstring dllPath = ResolveHookDll(is64); + + // ---- Create shared memory (must exist BEFORE DLL is loaded) ------------- + auto* entry = new InjectedProcessInfo(); + entry->Pid = pid; + entry->Shm = new SharedMemoryHandle(pid); + + if (!entry->Shm->IsValid()) + { + delete entry; + CloseHandle(hProcess); + std::wstring err = L"CreateFileMapping failed: " + std::to_wstring(GetLastError()); + Log(L"[Inject] %ls", err.c_str()); + if (pError) *pError = err; + return false; + } + + // Write initial delta + entry->Shm->Write({ TimeUtil::ComputeDelta(fakeUtcTicks) }); + + // ---- Collect process name / path ---------------------------------------- + wchar_t pathBuf[MAX_PATH] = {}; + DWORD pathLen = MAX_PATH; + QueryFullProcessImageNameW(hProcess, 0, pathBuf, &pathLen); + entry->ProcessPath = pathBuf; + + const wchar_t* slash = wcsrchr(pathBuf, L'\\'); + entry->ProcessName = slash ? (slash + 1) : pathBuf; + + // ---- Inject the DLL via LoadLibraryW remote thread ---------------------- + SIZE_T dllPathBytes = (dllPath.size() + 1) * sizeof(wchar_t); + + LPVOID remoteStr = VirtualAllocEx(hProcess, nullptr, dllPathBytes, + MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); + if (!remoteStr) + { + delete entry; + CloseHandle(hProcess); + std::wstring err = L"VirtualAllocEx failed: " + std::to_wstring(GetLastError()); + Log(L"[Inject] %ls", err.c_str()); + if (pError) *pError = err; + return false; + } + + WriteProcessMemory(hProcess, remoteStr, dllPath.c_str(), dllPathBytes, nullptr); + + HMODULE hKernel = GetModuleHandleW(L"kernel32.dll"); + FARPROC pLoadLib = GetProcAddress(hKernel, "LoadLibraryW"); + + HANDLE hThread = CreateRemoteThread( + hProcess, nullptr, 0, + reinterpret_cast(pLoadLib), + remoteStr, 0, nullptr); + + if (!hThread) + { + VirtualFreeEx(hProcess, remoteStr, 0, MEM_RELEASE); + delete entry; + CloseHandle(hProcess); + std::wstring err = L"CreateRemoteThread failed: " + std::to_wstring(GetLastError()); + Log(L"[Inject] %ls", err.c_str()); + if (pError) *pError = err; + return false; + } + + // Wait for LoadLibraryW to return (give it 5 seconds) + WaitForSingleObject(hThread, 5000); + + // Check the return value (hModule loaded) + DWORD exitCode = 0; + GetExitCodeThread(hThread, &exitCode); + CloseHandle(hThread); + VirtualFreeEx(hProcess, remoteStr, 0, MEM_RELEASE); + CloseHandle(hProcess); + + if (!exitCode) + { + delete entry; + std::wstring err = L"LoadLibraryW in target returned NULL — DLL load failed"; + Log(L"[Inject] %ls", err.c_str()); + if (pError) *pError = err; + return false; + } + + m_injected[pid] = entry; + Log(L"[Inject] pid=%lu '%ls' injected ('%ls')", pid, entry->ProcessName.c_str(), dllPath.c_str()); + return true; +} + +bool InjectionManager::SetFakeTime(DWORD pid, LONGLONG fakeUtcTicks) +{ + std::lock_guard lk(m_mutex); + auto it = m_injected.find(pid); + if (it == m_injected.end()) return false; + it->second->Shm->Write({ TimeUtil::ComputeDelta(fakeUtcTicks) }); + return true; +} + +void InjectionManager::SetFakeTimeAll(LONGLONG fakeUtcTicks) +{ + std::lock_guard lk(m_mutex); + LONGLONG delta = TimeUtil::ComputeDelta(fakeUtcTicks); + for (auto& kv : m_injected) + kv.second->Shm->Write({ delta }); +} + +bool InjectionManager::Eject(DWORD pid) +{ + std::lock_guard lk(m_mutex); + auto it = m_injected.find(pid); + if (it == m_injected.end()) return false; + + // Zero out the delta so the hook passes through real time before we unmap + it->second->Shm->Write({ 0LL }); + Sleep(50); // let any in-flight hook calls complete + + delete it->second; + m_injected.erase(it); + Log(L"[Eject] pid=%lu ejected (shared memory closed)", pid); + return true; +} + +bool InjectionManager::IsInjected(DWORD pid) const +{ + std::lock_guard lk(m_mutex); + return m_injected.count(pid) != 0; +} + +void InjectionManager::ForEach(std::function fn) const +{ + std::lock_guard lk(m_mutex); + for (auto& kv : m_injected) + fn(*kv.second); +} diff --git a/TimeMocker.Injector/InjectionManager.h b/TimeMocker.Injector/InjectionManager.h new file mode 100644 index 0000000..642e399 --- /dev/null +++ b/TimeMocker.Injector/InjectionManager.h @@ -0,0 +1,110 @@ +#pragma once +// ============================================================================= +// TimeMocker.Injector — inject/eject Hook DLL + manage shared memory +// +// Usage: +// InjectionManager mgr; +// mgr.Inject(pid, fakeTimeUtc); // inject and set time +// mgr.SetFakeTime(pid, fakeUtc); // update time while injected +// mgr.Eject(pid); // detach hook +// ============================================================================= + +#define WIN32_LEAN_AND_MEAN +#include +#include +#include +#include +#include +#include "../Shared/MockTimeInfo.h" + +// --------------------------------------------------------------------------- +// SharedMemoryHandle +// Wraps one named MMF per injected process. +// --------------------------------------------------------------------------- +class SharedMemoryHandle +{ +public: + explicit SharedMemoryHandle(DWORD pid); + ~SharedMemoryHandle(); + + SharedMemoryHandle(const SharedMemoryHandle&) = delete; + SharedMemoryHandle& operator=(const SharedMemoryHandle&) = delete; + + bool IsValid() const { return m_pView != nullptr; } + void Write(const MockTimeInfo& info); + const std::wstring& Name() const { return m_name; } + +private: + std::wstring m_name; + HANDLE m_hMap = nullptr; + LPVOID m_pView = nullptr; +}; + +// --------------------------------------------------------------------------- +// InjectedProcessInfo +// --------------------------------------------------------------------------- +struct InjectedProcessInfo +{ + DWORD Pid = 0; + std::wstring ProcessName; + std::wstring ProcessPath; + SharedMemoryHandle* Shm = nullptr; +}; + +// --------------------------------------------------------------------------- +// InjectionManager +// --------------------------------------------------------------------------- +class InjectionManager +{ +public: + explicit InjectionManager(const std::wstring& hookDllDir = L""); + ~InjectionManager(); + + // Inject hook DLL into process and set initial fake time (UTC FILETIME ticks) + bool Inject(DWORD pid, LONGLONG fakeUtcTicks, std::wstring* pError = nullptr); + + // Update fake time for an already-injected process + bool SetFakeTime(DWORD pid, LONGLONG fakeUtcTicks); + + // Set fake time for all injected processes + void SetFakeTimeAll(LONGLONG fakeUtcTicks); + + // Remove hook from process (best-effort — DLL stays loaded but hooks removed on next call) + bool Eject(DWORD pid); + + bool IsInjected(DWORD pid) const; + + // Callback for log messages + std::function OnLog; + + // Iterate injected processes + void ForEach(std::function fn) const; + +private: + std::wstring ResolveHookDll(bool x64) const; + static bool IsProcess64Bit(HANDLE hProcess); + static LONGLONG RealUtcTicks(); + static LONGLONG ToFiletimeDelta(LONGLONG fakeUtcTicks); + + void Log(const wchar_t* fmt, ...) const; + + mutable std::mutex m_mutex; + std::unordered_map m_injected; + std::wstring m_hookDllDir; // directory where Hook DLLs live +}; + +// --------------------------------------------------------------------------- +// Utility: convert a DateTime-style local SYSTEMTIME to UTC FILETIME ticks +// (helper for callers that work with wall-clock time) +// --------------------------------------------------------------------------- +namespace TimeUtil +{ + // Get the current real UTC as FILETIME ticks (100-ns units since Jan 1, 1601) + LONGLONG RealUtcTicks(); + + // Convert a local SYSTEMTIME to UTC FILETIME ticks + LONGLONG LocalSystemTimeToUtcTicks(const SYSTEMTIME& st); + + // Build a DeltaTicks value: how many ticks ahead/behind of real time + LONGLONG ComputeDelta(LONGLONG fakeUtcTicks); +} diff --git a/TimeMocker.Injector/ProcessWatcher.h b/TimeMocker.Injector/ProcessWatcher.h new file mode 100644 index 0000000..debfb81 --- /dev/null +++ b/TimeMocker.Injector/ProcessWatcher.h @@ -0,0 +1,188 @@ +#pragma once +// ============================================================================= +// ProcessWatcher — polls running processes and auto-injects those matching +// a set of glob/regex patterns. +// ============================================================================= + +#define WIN32_LEAN_AND_MEAN +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "InjectionManager.h" + +struct PatternRule +{ + std::wstring Pattern; + bool UseRegex = false; + bool Enabled = true; + + bool IsMatch(const std::wstring& path) const + { + if (path.empty()) return false; + std::wstring regexStr; + if (UseRegex) + { + regexStr = Pattern; + } + else + { + regexStr = L"^"; + for (wchar_t c : Pattern) + { + switch (c) + { + case L'*': regexStr += L".*"; break; + case L'?': regexStr += L'.'; break; + case L'.': regexStr += L"\\."; break; + case L'\\': regexStr += L"\\\\"; break; + default: regexStr += c; break; + } + } + regexStr += L'$'; + } + try + { + std::wregex re(regexStr, std::regex_constants::icase); + return std::regex_match(path, re) || std::regex_search(path, re); + } + catch (...) { return false; } + } +}; + +class ProcessWatcher +{ +public: + explicit ProcessWatcher(InjectionManager& mgr) + : m_mgr(mgr) + { + m_fakeUtcTicks = TimeUtil::RealUtcTicks(); + } + + ~ProcessWatcher() { Stop(); } + + void AddRule(PatternRule rule) + { + std::lock_guard lk(m_rulesMutex); + m_rules.push_back(std::move(rule)); + } + + void RemoveRule(const std::wstring& pattern) + { + std::lock_guard lk(m_rulesMutex); + m_rules.erase( + std::remove_if(m_rules.begin(), m_rules.end(), + [&](const PatternRule& r){ return r.Pattern == pattern; }), + m_rules.end()); + } + + void ClearRules() + { + std::lock_guard lk(m_rulesMutex); + m_rules.clear(); + } + + void SetFakeUtcTicks(LONGLONG ticks) { m_fakeUtcTicks.store(ticks); } + + void Start(DWORD pollIntervalMs = 1500) + { + if (m_running.exchange(true)) return; + m_thread = std::thread([this, pollIntervalMs]() + { + while (m_running.load()) + { + Scan(); + for (DWORD e = 0; m_running.load() && e < pollIntervalMs; e += 100) + Sleep(100); + } + }); + } + + void Stop() + { + if (!m_running.exchange(false)) return; + if (m_thread.joinable()) m_thread.join(); + } + + // Callbacks + std::function OnAutoInjected; + std::function OnLog; + +private: + void Scan() + { + HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); + if (snap == INVALID_HANDLE_VALUE) return; + + PROCESSENTRY32W pe; pe.dwSize = sizeof(pe); + if (!Process32FirstW(snap, &pe)) { CloseHandle(snap); return; } + + std::lock_guard ruleLk(m_rulesMutex); + + do + { + DWORD pid = pe.th32ProcessID; + { std::lock_guard lk(m_seenMutex); if (m_seenPids.count(pid)) continue; } + if (m_mgr.IsInjected(pid)) continue; + + HANDLE hProc = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid); + std::wstring fullPath; + if (hProc) + { + wchar_t buf[MAX_PATH] = {}; DWORD len = MAX_PATH; + QueryFullProcessImageNameW(hProc, 0, buf, &len); + fullPath = buf; + CloseHandle(hProc); + } + + std::wstring procName = pe.szExeFile; + + for (auto& rule : m_rules) + { + if (!rule.Enabled) continue; + if (!rule.IsMatch(fullPath) && !rule.IsMatch(procName)) continue; + + { std::lock_guard lk(m_seenMutex); m_seenPids.insert(pid); } + + std::wstring err; + if (m_mgr.Inject(pid, m_fakeUtcTicks.load(), &err)) + { + DoLog(L"[AutoInject] '%ls' → [%lu] %ls", rule.Pattern.c_str(), pid, procName.c_str()); + if (OnAutoInjected) OnAutoInjected(pid, procName, fullPath); + } + else + { + DoLog(L"[AutoInject] FAIL [%lu] %ls: %ls", pid, procName.c_str(), err.c_str()); + } + break; + } + } while (Process32NextW(snap, &pe)); + + CloseHandle(snap); + } + + void DoLog(const wchar_t* fmt, ...) const + { + if (!OnLog) return; + wchar_t buf[1024]; va_list va; va_start(va, fmt); + vswprintf_s(buf, _countof(buf), fmt, va); va_end(va); + OnLog(buf); + } + + InjectionManager& m_mgr; + mutable std::mutex m_rulesMutex; + std::vector m_rules; + std::mutex m_seenMutex; + std::unordered_set m_seenPids; + std::atomic m_fakeUtcTicks{ 0 }; + std::atomic m_running{ false }; + std::thread m_thread; +}; diff --git a/TimeMocker.Injector/TimeMocker.Injector.vcxproj b/TimeMocker.Injector/TimeMocker.Injector.vcxproj new file mode 100644 index 0000000..65365e3 --- /dev/null +++ b/TimeMocker.Injector/TimeMocker.Injector.vcxproj @@ -0,0 +1,71 @@ + + + + Debug Win32 + ReleaseWin32 + Debug x64 + Releasex64 + + + + {B2222222-2222-2222-2222-222222222222} + TimeMockerInjector + 10.0 + + + + + + StaticLibrarytrue + v143Unicode + + + StaticLibraryfalse + v143trueUnicode + + + StaticLibrarytrue + v143Unicode + + + StaticLibraryfalse + v143trueUnicode + + + + + + + Level3 + true + true + stdcpp17 + $(SolutionDir)packages\detours\include;$(SolutionDir)Shared;%(AdditionalIncludeDirectories) + WIN32_LEAN_AND_MEAN;%(PreprocessorDefinitions) + + + + + DisabledMultiThreadedDebugDLL + + + MaxSpeedMultiThreadedDLL + + + DisabledMultiThreadedDebugDLL + + + MaxSpeedMultiThreadedDLL + + + + + + + + + + + + + diff --git a/TimeMocker.UI/TimeMocker.UI.vcxproj b/TimeMocker.UI/TimeMocker.UI.vcxproj new file mode 100644 index 0000000..562cf2a --- /dev/null +++ b/TimeMocker.UI/TimeMocker.UI.vcxproj @@ -0,0 +1,95 @@ + + + + Debug x64 + Releasex64 + + + + {C3333333-3333-3333-3333-333333333333} + TimeMockerUI + 10.0 + + + + + + Application + true + v143 + Unicode + + + Application + false + v143 + true + Unicode + + + + + + + Level3 + true + true + stdcpp17 + + $(SolutionDir)packages\detours\include; + $(SolutionDir)Shared; + %(AdditionalIncludeDirectories) + + WIN32_LEAN_AND_MEAN;%(PreprocessorDefinitions) + + + Console + detours.lib;%(AdditionalDependencies) + + + + + + Disabled + MultiThreadedDebugDLL + + + + $(SolutionDir)packages\detours\lib\x64; + $(OutDir); + %(AdditionalLibraryDirectories) + + TimeMocker.Injector.lib;detours.lib;%(AdditionalDependencies) + + + + + + MaxSpeed + MultiThreadedDLL + + + + $(SolutionDir)packages\detours\lib\x64; + $(OutDir); + %(AdditionalLibraryDirectories) + + TimeMocker.Injector.lib;detours.lib;%(AdditionalDependencies) + true + true + + + + + + + + + + + + + + + + diff --git a/TimeMocker.UI/main.cpp b/TimeMocker.UI/main.cpp new file mode 100644 index 0000000..64cb33d --- /dev/null +++ b/TimeMocker.UI/main.cpp @@ -0,0 +1,461 @@ +// ============================================================================= +// TimeMocker.UI — Console controller +// +// Commands: +// list — list running processes (filtered to accessible ones) +// inject — inject into process and apply current fake time +// eject — remove hook from process +// time — set fake time e.g. time "2024-06-15 14:30:00" +// time now — reset fake time to real time +// status — show injected processes and current time offset +// rule add — add auto-inject glob rule +// rule add -r — add auto-inject regex rule +// rule list — list auto-inject rules +// rule del — remove rule by index +// watch start — start auto-inject watcher (default: on startup) +// watch stop — stop auto-inject watcher +// quit / exit — exit +// ============================================================================= + +#define WIN32_LEAN_AND_MEAN +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../TimeMocker.Injector/InjectionManager.h" +#include "../TimeMocker.Injector/ProcessWatcher.h" + +#pragma comment(lib, "Psapi.lib") + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +static void EnableAnsi() +{ + HANDLE h = GetStdHandle(STD_OUTPUT_HANDLE); + DWORD mode = 0; + GetConsoleMode(h, &mode); + SetConsoleMode(h, mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING); +} + +static const char* C_RESET = "\033[0m"; +static const char* C_CYAN = "\033[36m"; +static const char* C_GREEN = "\033[32m"; +static const char* C_YELLOW = "\033[33m"; +static const char* C_RED = "\033[31m"; +static const char* C_GRAY = "\033[90m"; + +static void Log(const wchar_t* msg) +{ + // Convert wide to UTF-8 for console output + char buf[1024]; + WideCharToMultiByte(CP_UTF8, 0, msg, -1, buf, sizeof(buf), nullptr, nullptr); + printf("%s[LOG]%s %s\n", C_GRAY, C_RESET, buf); +} + +// Parse "YYYY-MM-DD HH:MM:SS" → SYSTEMTIME (local) +static bool ParseDateTime(const std::string& s, SYSTEMTIME& st) +{ + memset(&st, 0, sizeof(st)); + int Y, M, D, h, m, sec; + if (sscanf_s(s.c_str(), "%d-%d-%d %d:%d:%d", &Y, &M, &D, &h, &m, &sec) == 6 || + sscanf_s(s.c_str(), "%d/%d/%d %d:%d:%d", &Y, &M, &D, &h, &m, &sec) == 6) + { + st.wYear = static_cast(Y); + st.wMonth = static_cast(M); + st.wDay = static_cast(D); + st.wHour = static_cast(h); + st.wMinute = static_cast(m); + st.wSecond = static_cast(sec); + return true; + } + return false; +} + +static std::string FormatDelta(LONGLONG deltaTicks) +{ + // deltaTicks: 100-ns units + bool neg = deltaTicks < 0; + LONGLONG abs = neg ? -deltaTicks : deltaTicks; + + LONGLONG secs = abs / 10000000LL; + LONGLONG mins = secs / 60; secs %= 60; + LONGLONG hours = mins / 60; mins %= 60; + LONGLONG days = hours / 24; hours %= 24; + + char buf[128] = {}; + if (days) sprintf_s(buf, "%s%lldd%02lldh%02lldm%02llds", neg?"-":"+", days, hours, mins, secs); + else if (hours) sprintf_s(buf, "%s%lldh%02lldm%02llds", neg?"-":"+", hours, mins, secs); + else if (mins) sprintf_s(buf, "%s%lldm%02llds", neg?"-":"+", mins, secs); + else sprintf_s(buf, "%s%llds", neg?"-":"+", secs); + return buf; +} + +// --------------------------------------------------------------------------- +// Enumerate accessible processes +// --------------------------------------------------------------------------- +struct ProcInfo { DWORD pid; std::wstring name, path; }; + +static std::vector EnumProcesses(const std::wstring& filter = L"") +{ + std::vector result; + HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); + if (snap == INVALID_HANDLE_VALUE) return result; + + PROCESSENTRY32W pe; pe.dwSize = sizeof(pe); + if (!Process32FirstW(snap, &pe)) { CloseHandle(snap); return result; } + + do + { + HANDLE hProc = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pe.th32ProcessID); + if (!hProc) continue; + + wchar_t pathBuf[MAX_PATH] = {}; DWORD len = MAX_PATH; + QueryFullProcessImageNameW(hProc, 0, pathBuf, &len); + CloseHandle(hProc); + + ProcInfo info; + info.pid = pe.th32ProcessID; + info.name = pe.szExeFile; + info.path = pathBuf; + + if (!filter.empty()) + { + auto contains = [&](const std::wstring& hay, const std::wstring& needle) + { + std::wstring lh = hay, ln = needle; + std::transform(lh.begin(), lh.end(), lh.begin(), ::towlower); + std::transform(ln.begin(), ln.end(), ln.begin(), ::towlower); + return lh.find(ln) != std::wstring::npos; + }; + if (!contains(info.name, filter) && !contains(info.path, filter)) continue; + } + + result.push_back(info); + + } while (Process32NextW(snap, &pe)); + + CloseHandle(snap); + std::sort(result.begin(), result.end(), [](const ProcInfo& a, const ProcInfo& b) + { + return _wcsicmp(a.name.c_str(), b.name.c_str()) < 0; + }); + return result; +} + +// --------------------------------------------------------------------------- +// Main +// --------------------------------------------------------------------------- +int main() +{ + // Switch console to UTF-8 + SetConsoleOutputCP(CP_UTF8); + _setmode(_fileno(stdout), _O_U8TEXT); // redirect wide to UTF-8 + _setmode(_fileno(stdout), _O_TEXT); // reset (we'll use printf) + EnableAnsi(); + + printf("%s╔══════════════════════════════════════════════╗%s\n", C_CYAN, C_RESET); + printf("%s║ TimeMocker — C++ / MS Detours ║%s\n", C_CYAN, C_RESET); + printf("%s╚══════════════════════════════════════════════╝%s\n", C_CYAN, C_RESET); + printf("Type %shelp%s for command list.\n\n", C_YELLOW, C_RESET); + + // Check elevation + { + HANDLE hToken; + OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &hToken); + TOKEN_ELEVATION elev; + DWORD sz = sizeof(elev); + GetTokenInformation(hToken, TokenElevation, &elev, sz, &sz); + CloseHandle(hToken); + if (!elev.TokenIsElevated) + { + printf("%s[WARN] Not running as Administrator. Injection into protected processes will fail.%s\n\n", C_YELLOW, C_RESET); + } + } + + InjectionManager mgr; + mgr.OnLog = [](const std::wstring& msg) { Log(msg.c_str()); }; + + ProcessWatcher watcher(mgr); + watcher.OnLog = [](const std::wstring& msg) { Log(msg.c_str()); }; + watcher.OnAutoInjected = [&](DWORD pid, const std::wstring& name, const std::wstring& /*path*/) + { + char buf[256]; WideCharToMultiByte(CP_UTF8, 0, name.c_str(), -1, buf, sizeof(buf), nullptr, nullptr); + printf("%s[AutoInject]%s [%lu] %s\n", C_GREEN, C_RESET, pid, buf); + }; + + LONGLONG fakeUtcTicks = TimeUtil::RealUtcTicks(); // start at real time (delta = 0) + std::vector ruleList; // local copy for display + + watcher.SetFakeUtcTicks(fakeUtcTicks); + watcher.Start(1500); + printf("%s[Watcher]%s Auto-inject watcher started.\n\n", C_GREEN, C_RESET); + + std::string line; + while (true) + { + printf("%stimemocker>%s ", C_CYAN, C_RESET); + fflush(stdout); + if (!std::getline(std::cin, line)) break; + + // Tokenize + std::istringstream iss(line); + std::vector tokens; + { + std::string tok; + // Handle quoted tokens + bool inQ = false; std::string cur; + for (char c : line) + { + if (c == '"') { inQ = !inQ; } + else if (c == ' ' && !inQ && !cur.empty()) { tokens.push_back(cur); cur.clear(); } + else { cur += c; } + } + if (!cur.empty()) tokens.push_back(cur); + } + if (tokens.empty()) continue; + + std::string cmd = tokens[0]; + std::transform(cmd.begin(), cmd.end(), cmd.begin(), ::tolower); + + // ---- quit ----------------------------------------------------------- + if (cmd == "quit" || cmd == "exit") break; + + // ---- help ----------------------------------------------------------- + else if (cmd == "help") + { + printf( + " %slist%s [filter] list running processes\n" + " %sinject%s inject hook DLL into process\n" + " %seject%s remove hook from process\n" + " %stime%s YYYY-MM-DD HH:MM:SS set fake time (local)\n" + " %stime%s now reset to real time\n" + " %sstatus%s show injected processes + delta\n" + " %srule add%s add glob auto-inject rule\n" + " %srule add%s -r add regex auto-inject rule\n" + " %srule list%s list rules\n" + " %srule del%s remove rule by index\n" + " %swatch start%s / %sstop%s control auto-inject watcher\n" + " %squit%s exit\n", + C_YELLOW,C_RESET, C_YELLOW,C_RESET, C_YELLOW,C_RESET, + C_YELLOW,C_RESET, C_YELLOW,C_RESET, C_YELLOW,C_RESET, + C_YELLOW,C_RESET, C_YELLOW,C_RESET, C_YELLOW,C_RESET, + C_YELLOW,C_RESET, C_YELLOW,C_RESET, C_YELLOW,C_RESET, + C_YELLOW,C_RESET, C_YELLOW,C_RESET); + } + + // ---- list ----------------------------------------------------------- + else if (cmd == "list") + { + std::wstring filter; + if (tokens.size() > 1) + { + std::string fs = tokens[1]; + filter = std::wstring(fs.begin(), fs.end()); + } + + auto procs = EnumProcesses(filter); + printf("%s%-8s %-28s %s%s\n", C_GRAY, "PID", "Name", "Path", C_RESET); + for (auto& p : procs) + { + char name[128] = {}, path[512] = {}; + WideCharToMultiByte(CP_UTF8, 0, p.name.c_str(), -1, name, sizeof(name), nullptr, nullptr); + WideCharToMultiByte(CP_UTF8, 0, p.path.c_str(), -1, path, sizeof(path), nullptr, nullptr); + bool inj = mgr.IsInjected(p.pid); + printf("%-8lu %-28s %s%s\n", p.pid, name, path, inj ? " ✓" : ""); + } + printf(" %lu processes shown\n", (DWORD)procs.size()); + } + + // ---- inject --------------------------------------------------------- + else if (cmd == "inject") + { + if (tokens.size() < 2) { printf("%sUsage: inject %s\n", C_RED, C_RESET); continue; } + DWORD pid = (DWORD)atoul(tokens[1].c_str()); + std::wstring err; + if (mgr.Inject(pid, fakeUtcTicks, &err)) + { + auto delta = TimeUtil::ComputeDelta(fakeUtcTicks); + printf("%s[OK]%s Injected pid=%lu delta=%s\n", + C_GREEN, C_RESET, pid, FormatDelta(delta).c_str()); + } + else + { + char ebuf[512]; WideCharToMultiByte(CP_UTF8,0,err.c_str(),-1,ebuf,sizeof(ebuf),nullptr,nullptr); + printf("%s[FAIL]%s %s\n", C_RED, C_RESET, ebuf); + } + } + + // ---- eject ---------------------------------------------------------- + else if (cmd == "eject") + { + if (tokens.size() < 2) { printf("%sUsage: eject %s\n", C_RED, C_RESET); continue; } + DWORD pid = (DWORD)atoul(tokens[1].c_str()); + if (mgr.Eject(pid)) + printf("%s[OK]%s Ejected pid=%lu\n", C_GREEN, C_RESET, pid); + else + printf("%s[FAIL]%s pid=%lu not injected\n", C_RED, C_RESET, pid); + } + + // ---- time ----------------------------------------------------------- + else if (cmd == "time") + { + if (tokens.size() < 2) { printf("%sUsage: time YYYY-MM-DD HH:MM:SS | time now%s\n", C_RED, C_RESET); continue; } + + std::string arg = tokens[1]; + if (tokens.size() >= 3) arg += " " + tokens[2]; // join date and time + + if (arg == "now") + { + fakeUtcTicks = TimeUtil::RealUtcTicks(); + } + else + { + SYSTEMTIME st; + if (!ParseDateTime(arg, st)) + { + printf("%sInvalid format. Use: YYYY-MM-DD HH:MM:SS%s\n", C_RED, C_RESET); + continue; + } + fakeUtcTicks = TimeUtil::LocalSystemTimeToUtcTicks(st); + } + + mgr.SetFakeTimeAll(fakeUtcTicks); + watcher.SetFakeUtcTicks(fakeUtcTicks); + + auto delta = TimeUtil::ComputeDelta(fakeUtcTicks); + // Display local time + SYSTEMTIME disp; + FILETIME ft; + ULARGE_INTEGER ui; ui.QuadPart = (ULONGLONG)fakeUtcTicks; + ft.dwLowDateTime = ui.LowPart; ft.dwHighDateTime = ui.HighPart; + FILETIME lft; FileTimeToLocalFileTime(&ft, &lft); + FileTimeToSystemTime(&lft, &disp); + + printf("%s[Time]%s Fake time set to %04d-%02d-%02d %02d:%02d:%02d (local) delta=%s\n", + C_GREEN, C_RESET, + disp.wYear, disp.wMonth, disp.wDay, + disp.wHour, disp.wMinute, disp.wSecond, + FormatDelta(delta).c_str()); + } + + // ---- status --------------------------------------------------------- + else if (cmd == "status") + { + LONGLONG delta = TimeUtil::ComputeDelta(fakeUtcTicks); + + // Show local fake time + FILETIME ft; ULARGE_INTEGER ui; ui.QuadPart = (ULONGLONG)fakeUtcTicks; + ft.dwLowDateTime = ui.LowPart; ft.dwHighDateTime = ui.HighPart; + FILETIME lft; FileTimeToLocalFileTime(&ft, &lft); + SYSTEMTIME st; FileTimeToSystemTime(&lft, &st); + + printf("Fake time : %04d-%02d-%02d %02d:%02d:%02d (local) delta=%s\n", + st.wYear, st.wMonth, st.wDay, + st.wHour, st.wMinute, st.wSecond, + FormatDelta(delta).c_str()); + + printf("Injected processes:\n"); + bool any = false; + mgr.ForEach([&](const InjectedProcessInfo& p) + { + any = true; + char name[256] = {}; + WideCharToMultiByte(CP_UTF8, 0, p.ProcessName.c_str(), -1, name, sizeof(name), nullptr, nullptr); + printf(" %s[%lu]%s %s\n", C_GREEN, p.Pid, C_RESET, name); + }); + if (!any) printf(" (none)\n"); + } + + // ---- rule ----------------------------------------------------------- + else if (cmd == "rule") + { + if (tokens.size() < 2) { printf("Subcommands: add, list, del\n"); continue; } + std::string sub = tokens[1]; + std::transform(sub.begin(), sub.end(), sub.begin(), ::tolower); + + if (sub == "list") + { + if (ruleList.empty()) { printf(" (no rules)\n"); continue; } + for (size_t i = 0; i < ruleList.size(); i++) + { + char pat[512]; WideCharToMultiByte(CP_UTF8, 0, ruleList[i].Pattern.c_str(), -1, pat, sizeof(pat), nullptr, nullptr); + printf(" [%zu] %s%s%s (%s)\n", + i, C_YELLOW, pat, C_RESET, + ruleList[i].UseRegex ? "regex" : "glob"); + } + } + else if (sub == "add") + { + if (tokens.size() < 3) { printf("Usage: rule add [-r] \n"); continue; } + bool isRegex = false; + std::string patStr; + if (tokens[2] == "-r" || tokens[2] == "--regex") + { + isRegex = true; + if (tokens.size() < 4) { printf("Missing pattern after -r\n"); continue; } + patStr = tokens[3]; + } + else + { + patStr = tokens[2]; + } + + PatternRule rule; + rule.Pattern = std::wstring(patStr.begin(), patStr.end()); + rule.UseRegex = isRegex; + rule.Enabled = true; + + watcher.AddRule(rule); + ruleList.push_back(rule); + + printf("%s[OK]%s Rule added: '%s' (%s)\n", + C_GREEN, C_RESET, patStr.c_str(), isRegex ? "regex" : "glob"); + } + else if (sub == "del") + { + if (tokens.size() < 3) { printf("Usage: rule del \n"); continue; } + size_t idx = (size_t)atoi(tokens[2].c_str()); + if (idx >= ruleList.size()) { printf("%sIndex out of range%s\n", C_RED, C_RESET); continue; } + + watcher.RemoveRule(ruleList[idx].Pattern); + ruleList.erase(ruleList.begin() + idx); + printf("%s[OK]%s Rule removed\n", C_GREEN, C_RESET); + } + else + { + printf("Unknown rule subcommand: %s\n", sub.c_str()); + } + } + + // ---- watch ---------------------------------------------------------- + else if (cmd == "watch") + { + if (tokens.size() < 2) { printf("Usage: watch start|stop\n"); continue; } + std::string sub = tokens[1]; + if (sub == "start") { watcher.Start(); printf("%s[Watcher]%s Started\n", C_GREEN, C_RESET); } + else if (sub == "stop") { watcher.Stop(); printf("%s[Watcher]%s Stopped\n", C_YELLOW, C_RESET); } + } + + else + { + printf("%sUnknown command: %s%s (type help)\n", C_RED, cmd.c_str(), C_RESET); + } + } + + watcher.Stop(); + printf("\nGoodbye.\n"); + return 0; +} diff --git a/TimeMocker.sln b/TimeMocker.sln new file mode 100644 index 0000000..86dd0e8 --- /dev/null +++ b/TimeMocker.sln @@ -0,0 +1,41 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.11.35222.181 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "TimeMocker.Hook", "TimeMocker.Hook\TimeMocker.Hook.vcxproj", "{A1111111-1111-1111-1111-111111111111}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "TimeMocker.Injector", "TimeMocker.Injector\TimeMocker.Injector.vcxproj", "{B2222222-2222-2222-2222-222222222222}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "TimeMocker.UI", "TimeMocker.UI\TimeMocker.UI.vcxproj", "{C3333333-3333-3333-3333-333333333333}" + ProjectSection(ProjectDependencies) = postProject + {A1111111-1111-1111-1111-111111111111} = {A1111111-1111-1111-1111-111111111111} + {B2222222-2222-2222-2222-222222222222} = {B2222222-2222-2222-2222-222222222222} + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|x64 = Debug|x64 + Release|x64 = Release|x64 + Debug|x86 = Debug|x86 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {A1111111-1111-1111-1111-111111111111}.Debug|x64.ActiveCfg = Debug|x64 + {A1111111-1111-1111-1111-111111111111}.Debug|x64.Build.0 = Debug|x64 + {A1111111-1111-1111-1111-111111111111}.Release|x64.ActiveCfg = Release|x64 + {A1111111-1111-1111-1111-111111111111}.Release|x64.Build.0 = Release|x64 + {A1111111-1111-1111-1111-111111111111}.Debug|x86.ActiveCfg = Debug|Win32 + {A1111111-1111-1111-1111-111111111111}.Debug|x86.Build.0 = Debug|Win32 + {A1111111-1111-1111-1111-111111111111}.Release|x86.ActiveCfg = Release|Win32 + {A1111111-1111-1111-1111-111111111111}.Release|x86.Build.0 = Release|Win32 + {B2222222-2222-2222-2222-222222222222}.Debug|x64.ActiveCfg = Debug|x64 + {B2222222-2222-2222-2222-222222222222}.Debug|x64.Build.0 = Debug|x64 + {B2222222-2222-2222-2222-222222222222}.Release|x64.ActiveCfg = Release|x64 + {B2222222-2222-2222-2222-222222222222}.Release|x64.Build.0 = Release|x64 + {C3333333-3333-3333-3333-333333333333}.Debug|x64.ActiveCfg = Debug|x64 + {C3333333-3333-3333-3333-333333333333}.Debug|x64.Build.0 = Debug|x64 + {C3333333-3333-3333-3333-333333333333}.Release|x64.ActiveCfg = Release|x64 + {C3333333-3333-3333-3333-333333333333}.Release|x64.Build.0 = Release|x64 + EndGlobalSection +EndGlobal diff --git a/scripts/setup.ps1 b/scripts/setup.ps1 new file mode 100644 index 0000000..028d6a2 --- /dev/null +++ b/scripts/setup.ps1 @@ -0,0 +1,64 @@ +# ============================================================================= +# setup.ps1 — Bootstrap MS Detours for TimeMockerCpp +# +# Run once before opening the solution in Visual Studio: +# powershell -ExecutionPolicy Bypass -File scripts\setup.ps1 +# +# What it does: +# 1. Clones / updates vcpkg (if not already present at $env:VCPKG_ROOT or ./vcpkg) +# 2. Installs detours:x64-windows and detours:x86-windows +# 3. Copies the resulting headers + libs into packages\detours\ +# so the vcxproj files can find them without requiring vcpkg integration. +# ============================================================================= + +$ErrorActionPreference = "Stop" + +$scriptDir = $PSScriptRoot +$repoRoot = Split-Path $scriptDir -Parent +$pkgDir = Join-Path $repoRoot "packages\detours" +$vcpkgRoot = if ($env:VCPKG_ROOT) { $env:VCPKG_ROOT } else { Join-Path $repoRoot "vcpkg" } + +# ── 1. Ensure vcpkg ────────────────────────────────────────────────────────── +if (!(Test-Path (Join-Path $vcpkgRoot "vcpkg.exe"))) +{ + Write-Host "Cloning vcpkg into $vcpkgRoot ..." -ForegroundColor Cyan + git clone https://github.com/microsoft/vcpkg.git $vcpkgRoot + & (Join-Path $vcpkgRoot "bootstrap-vcpkg.bat") -disableMetrics +} +else +{ + Write-Host "vcpkg found at $vcpkgRoot" -ForegroundColor Green +} + +$vcpkg = Join-Path $vcpkgRoot "vcpkg.exe" + +# ── 2. Install Detours ─────────────────────────────────────────────────────── +Write-Host "Installing detours:x64-windows ..." -ForegroundColor Cyan +& $vcpkg install "detours:x64-windows" + +Write-Host "Installing detours:x86-windows ..." -ForegroundColor Cyan +& $vcpkg install "detours:x86-windows" + +# ── 3. Copy headers + libs into packages\detours\ ─────────────────────────── +$x64installed = Join-Path $vcpkgRoot "installed\x64-windows" +$x86installed = Join-Path $vcpkgRoot "installed\x86-windows" + +$incSrc = Join-Path $x64installed "include" +$incDst = Join-Path $pkgDir "include" + +Write-Host "Copying headers → $incDst" -ForegroundColor Cyan +New-Item -ItemType Directory -Force -Path $incDst | Out-Null +Copy-Item -Path (Join-Path $incSrc "detours.h") -Destination $incDst -Force + +foreach ($triplet in @("x64", "x86")) +{ + $libSrc = Join-Path (Join-Path $vcpkgRoot "installed\$triplet-windows") "lib\detours.lib" + $libDst = Join-Path $pkgDir "lib\$triplet" + New-Item -ItemType Directory -Force -Path $libDst | Out-Null + Copy-Item -Path $libSrc -Destination (Join-Path $libDst "detours.lib") -Force + Write-Host "Copied $triplet detours.lib → $libDst" -ForegroundColor Green +} + +Write-Host "" +Write-Host "Setup complete! Open TimeMocker.sln in Visual Studio 2022." -ForegroundColor Green +Write-Host "Build configuration: Debug|x64 or Release|x64" -ForegroundColor Green