chore: init first version

This commit is contained in:
2026-02-27 17:30:37 +07:00
parent 7a3f634726
commit 1b24468759
16 changed files with 2159 additions and 43 deletions
+318
View File
@@ -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 <Windows.h>
#include <TlHelp32.h>
#include <Psapi.h>
#include <detours.h>
#include <cstdio>
#include <cassert>
#include <cwchar>
#include <memory>
#include <sstream>
#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<LONGLONG*>(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<LONGLONG>(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<LONGLONG>(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<std::mutex> 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<std::mutex> 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<LPTHREAD_START_ROUTINE>(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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> lk(m_mutex);
return m_injected.count(pid) != 0;
}
void InjectionManager::ForEach(std::function<void(const InjectedProcessInfo&)> fn) const
{
std::lock_guard<std::mutex> lk(m_mutex);
for (auto& kv : m_injected)
fn(*kv.second);
}
+110
View File
@@ -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 <Windows.h>
#include <string>
#include <unordered_map>
#include <functional>
#include <mutex>
#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<void(const std::wstring&)> OnLog;
// Iterate injected processes
void ForEach(std::function<void(const InjectedProcessInfo&)> 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<DWORD, InjectedProcessInfo*> 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);
}
+188
View File
@@ -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 <Windows.h>
#include <TlHelp32.h>
#include <string>
#include <vector>
#include <unordered_set>
#include <functional>
#include <thread>
#include <atomic>
#include <mutex>
#include <regex>
#include <algorithm>
#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<std::mutex> lk(m_rulesMutex);
m_rules.push_back(std::move(rule));
}
void RemoveRule(const std::wstring& pattern)
{
std::lock_guard<std::mutex> 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<std::mutex> 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<void(DWORD pid, const std::wstring& name, const std::wstring& path)> OnAutoInjected;
std::function<void(const std::wstring&)> 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<std::mutex> ruleLk(m_rulesMutex);
do
{
DWORD pid = pe.th32ProcessID;
{ std::lock_guard<std::mutex> 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<std::mutex> 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<PatternRule> m_rules;
std::mutex m_seenMutex;
std::unordered_set<DWORD> m_seenPids;
std::atomic<LONGLONG> m_fakeUtcTicks{ 0 };
std::atomic<bool> m_running{ false };
std::thread m_thread;
};
@@ -0,0 +1,71 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|Win32"> <Configuration>Debug</Configuration> <Platform>Win32</Platform> </ProjectConfiguration>
<ProjectConfiguration Include="Release|Win32"><Configuration>Release</Configuration><Platform>Win32</Platform> </ProjectConfiguration>
<ProjectConfiguration Include="Debug|x64"> <Configuration>Debug</Configuration> <Platform>x64</Platform> </ProjectConfiguration>
<ProjectConfiguration Include="Release|x64"> <Configuration>Release</Configuration><Platform>x64</Platform> </ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<ProjectGuid>{B2222222-2222-2222-2222-222222222222}</ProjectGuid>
<RootNamespace>TimeMockerInjector</RootNamespace>
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="Configuration">
<ConfigurationType>StaticLibrary</ConfigurationType><UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset><CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="Configuration">
<ConfigurationType>StaticLibrary</ConfigurationType><UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset><WholeProgramOptimization>true</WholeProgramOptimization><CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
<ConfigurationType>StaticLibrary</ConfigurationType><UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset><CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
<ConfigurationType>StaticLibrary</ConfigurationType><UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset><WholeProgramOptimization>true</WholeProgramOptimization><CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ItemDefinitionGroup>
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<ConformanceMode>true</ConformanceMode>
<LanguageStandard>stdcpp17</LanguageStandard>
<AdditionalIncludeDirectories>$(SolutionDir)packages\detours\include;$(SolutionDir)Shared;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<PreprocessorDefinitions>WIN32_LEAN_AND_MEAN;%(PreprocessorDefinitions)</PreprocessorDefinitions>
</ClCompile>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<ClCompile><Optimization>Disabled</Optimization><RuntimeLibrary>MultiThreadedDebugDLL</RuntimeLibrary></ClCompile>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<ClCompile><Optimization>MaxSpeed</Optimization><RuntimeLibrary>MultiThreadedDLL</RuntimeLibrary></ClCompile>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<ClCompile><Optimization>Disabled</Optimization><RuntimeLibrary>MultiThreadedDebugDLL</RuntimeLibrary></ClCompile>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<ClCompile><Optimization>MaxSpeed</Optimization><RuntimeLibrary>MultiThreadedDLL</RuntimeLibrary></ClCompile>
</ItemDefinitionGroup>
<ItemGroup>
<ClCompile Include="InjectionManager.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="InjectionManager.h" />
<ClInclude Include="ProcessWatcher.h" />
<ClInclude Include="..\Shared\MockTimeInfo.h" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
</Project>