Files
time-mocker-cpp/TimeMocker.UI/main.cpp
T
2026-02-27 17:30:37 +07:00

462 lines
18 KiB
C++

// =============================================================================
// TimeMocker.UI — Console controller
//
// Commands:
// list — list running processes (filtered to accessible ones)
// inject <pid> — inject into process and apply current fake time
// eject <pid> — remove hook from process
// time <datetime> — 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 <glob> — add auto-inject glob rule
// rule add -r <rx> — add auto-inject regex rule
// rule list — list auto-inject rules
// rule del <n> — 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 <Windows.h>
#include <TlHelp32.h>
#include <Psapi.h>
#include <cstdio>
#include <cstdlib>
#include <cwchar>
#include <string>
#include <vector>
#include <sstream>
#include <algorithm>
#include <iostream>
#include <iomanip>
#include <io.h>
#include <fcntl.h>
#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<WORD>(Y);
st.wMonth = static_cast<WORD>(M);
st.wDay = static_cast<WORD>(D);
st.wHour = static_cast<WORD>(h);
st.wMinute = static_cast<WORD>(m);
st.wSecond = static_cast<WORD>(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<ProcInfo> EnumProcesses(const std::wstring& filter = L"")
{
std::vector<ProcInfo> 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<PatternRule> 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<std::string> 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 <pid> inject hook DLL into process\n"
" %seject%s <pid> 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 <pattern> add glob auto-inject rule\n"
" %srule add%s -r <pattern> add regex auto-inject rule\n"
" %srule list%s list rules\n"
" %srule del%s <index> 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 <pid>%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 <pid>%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] <pattern>\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 <index>\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;
}