diff --git a/.gitignore b/.gitignore index 42e0eb5..b5c9e97 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,12 @@ -# Build directories /cmake-build-*/ /build/ /out/ /.idea/ - -# Dependency directories -*/vendor/*/ \ No newline at end of file +.gitignore +.vscode/ +nyx_core.txt +nyx_injector.bat +nyx_style.txt +logs +*.logs +.* \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 93697ba..180e685 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,9 +6,7 @@ set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) -add_definitions(-DENGINE_IMGUI -DIMGUI_USE_WCHAR32) - add_subdirectory(Engine) #add_subdirectory(SampleApp) add_subdirectory(CheatStengine) -add_subdirectory(CheatStengineDriver) +add_subdirectory(CheatStengineDriver) \ No newline at end of file diff --git a/CheatStengine/CMakeLists.txt b/CheatStengine/CMakeLists.txt index 666f7f2..596baee 100644 --- a/CheatStengine/CMakeLists.txt +++ b/CheatStengine/CMakeLists.txt @@ -1,28 +1,109 @@ +# Matches the version required by the top-level CMakeLists.txt. Present here too so +# this file is self-consistent and never emits a "No cmake_minimum_required" warning. +cmake_minimum_required(VERSION 3.30) + +if (CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR) + get_filename_component(_cs_repo_root "${CMAKE_CURRENT_SOURCE_DIR}" DIRECTORY) + message(FATAL_ERROR + "CheatStengine is a subproject and cannot be configured on its own.\n" + "It links the 'Engine' target and requires C++20, both set up by the " + "top-level CMakeLists.txt. Configure from the repository root instead:\n" + " cmake -S \"${_cs_repo_root}\" -B \"${_cs_repo_root}/out/build/x64-Release\" -G Ninja -DCMAKE_BUILD_TYPE=Release\n" + " cmake --build \"${_cs_repo_root}/out/build/x64-Release\"\n" + "See the 'Compiling from Source' section of the README.") +endif () + project(CheatStengine) include(vendor/zasm.cmake) include(vendor/icon_font_cpp_headers.cmake) +include(vendor/json.cmake) +include(vendor/httplib.cmake) #include(vendor/imgui_hex_editor.cmake) -file(GLOB_RECURSE SOURCE_FILES src/*.cpp) -add_executable(${PROJECT_NAME} ${SOURCE_FILES}) +add_executable(${PROJECT_NAME} + src/CheatStengine/AddressEvaluator/Ast.cpp + src/CheatStengine/AddressEvaluator/Evaluator.cpp + src/CheatStengine/AddressEvaluator/Lexer.cpp + src/CheatStengine/AddressEvaluator/Parser.cpp + src/CheatStengine/Assembly/Assembler.cpp + src/CheatStengine/Assembly/Formatter.cpp + src/CheatStengine/main.cpp + src/CheatStengine/MainLayer.cpp + src/CheatStengine/Panes/DebugPane.cpp + src/CheatStengine/Panes/DisassemblyPane.cpp + src/CheatStengine/Panes/MemoryScannerPane.cpp + src/CheatStengine/Panes/MemoryViewerPane.cpp + src/CheatStengine/Panes/ModulesPane.cpp + src/CheatStengine/Panes/PatternScannerPane.cpp + src/CheatStengine/Panes/PEViewer.cpp + src/CheatStengine/Panes/StructDissectPane.cpp + src/CheatStengine/Panes/WatchPane.cpp + src/CheatStengine/Process/Device.cpp + src/CheatStengine/Process/KernelProcess.cpp + src/CheatStengine/Process/Monitor.cpp + src/CheatStengine/Process/Process.cpp + src/CheatStengine/Process/WinAPIProcess.cpp + src/CheatStengine/Server/AuthManager.cpp + src/CheatStengine/Server/JobManager.cpp + src/CheatStengine/Server/McpInstaller.cpp + src/CheatStengine/Server/McpServer.cpp + src/CheatStengine/Server/SessionManager.cpp + src/CheatStengine/Server/ToolRegistry.cpp + src/CheatStengine/Settings/ComboSettings.cpp + src/CheatStengine/Settings/KeybindSetting.cpp + src/CheatStengine/Settings/KernelSettings.cpp + src/CheatStengine/Settings/SettingsCategory.cpp + src/CheatStengine/Settings/SettingsManager.cpp + src/CheatStengine/Settings/SliderSetting.cpp + src/CheatStengine/Settings/ToggleSetting.cpp + src/CheatStengine/Tools/MemoryScanner.cpp + src/CheatStengine/Tools/PatternGenerator.cpp + src/CheatStengine/Tools/PatternScanner.cpp + src/CheatStengine/Tools/RTTI.cpp + src/CheatStengine/Tools/StructDissect.cpp + src/CheatStengine/UI/ImGui/EditableLabel.cpp + src/CheatStengine/UI/ImGui/Fonts.cpp + src/CheatStengine/UI/ImGui/IconCache.cpp + src/CheatStengine/UI/ImGui/ImGuiHex.cpp + src/CheatStengine/UI/ImGui/Menu.cpp + src/CheatStengine/UI/MenuBar.cpp + src/CheatStengine/UI/TitleBar.cpp + src/CheatStengine/Utils.cpp +) if (WIN32) - add_definitions(-DNOMINMAX) + target_compile_definitions(${PROJECT_NAME} PRIVATE + NOMINMAX + WIN32_LEAN_AND_MEAN + PLATFORM_WINDOWS + WINVER=0x0A00 + _WIN32_WINNT=0x0A00 + NTDDI_VERSION=0x0A000008 + ) endif () -target_include_directories(${PROJECT_NAME} PUBLIC +target_include_directories(${PROJECT_NAME} PRIVATE src + ${CMAKE_CURRENT_SOURCE_DIR}/../Shared ${ICON_FONT_CPP_HEADERS} ) -target_link_libraries(${PROJECT_NAME} PUBLIC +target_compile_options(${PROJECT_NAME} PRIVATE + $<$:/W4> +) +target_link_libraries(${PROJECT_NAME} PRIVATE Engine zasm + nlohmann_json::nlohmann_json + httplib::httplib Dbghelp + Shell32 + Cfgmgr32 + d3d11 ) add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_directory ${PROJECT_SOURCE_DIR}/Resources $/Resources -) \ No newline at end of file +) diff --git a/CheatStengine/Resources/favicon.ico b/CheatStengine/Resources/favicon.ico new file mode 100644 index 0000000..d1e1dd1 Binary files /dev/null and b/CheatStengine/Resources/favicon.ico differ diff --git a/CheatStengine/build/CMakeCache.txt b/CheatStengine/build/CMakeCache.txt new file mode 100644 index 0000000..5e74abf --- /dev/null +++ b/CheatStengine/build/CMakeCache.txt @@ -0,0 +1,56 @@ +# This is the CMakeCache file. +# For build in directory: f:/Downloads/CheatStengine-master/CheatStengine/build +# It was generated by CMake: C:/Program Files/Microsoft Visual Studio/2022/Community/Common7/IDE/CommonExtensions/Microsoft/CMake/CMake/bin/cmake.exe +# You can edit this file to change values found and used by cmake. +# If you do not want to change any of the values, simply exit the editor. +# If you do want to change a value, simply edit, save, and exit the editor. +# The syntax for the file is as follows: +# KEY:TYPE=VALUE +# KEY is the name of a variable in the cache. +# TYPE is a hint to GUIs for the type of VALUE, DO NOT EDIT TYPE!. +# VALUE is the current value for the KEY. + +######################## +# EXTERNAL cache entries +######################## + +//Value Computed by CMake. +CMAKE_FIND_PACKAGE_REDIRECTS_DIR:STATIC=F:/Downloads/CheatStengine-master/CheatStengine/build/CMakeFiles/pkgRedirects + + +######################## +# INTERNAL cache entries +######################## + +//This is the directory where this CMakeCache.txt was created +CMAKE_CACHEFILE_DIR:INTERNAL=f:/Downloads/CheatStengine-master/CheatStengine/build +//Major version of cmake used to create the current loaded cache +CMAKE_CACHE_MAJOR_VERSION:INTERNAL=3 +//Minor version of cmake used to create the current loaded cache +CMAKE_CACHE_MINOR_VERSION:INTERNAL=31 +//Patch version of cmake used to create the current loaded cache +CMAKE_CACHE_PATCH_VERSION:INTERNAL=6 +//Path to CMake executable. +CMAKE_COMMAND:INTERNAL=C:/Program Files/Microsoft Visual Studio/2022/Community/Common7/IDE/CommonExtensions/Microsoft/CMake/CMake/bin/cmake.exe +//Path to cpack program executable. +CMAKE_CPACK_COMMAND:INTERNAL=C:/Program Files/Microsoft Visual Studio/2022/Community/Common7/IDE/CommonExtensions/Microsoft/CMake/CMake/bin/cpack.exe +//Path to ctest program executable. +CMAKE_CTEST_COMMAND:INTERNAL=C:/Program Files/Microsoft Visual Studio/2022/Community/Common7/IDE/CommonExtensions/Microsoft/CMake/CMake/bin/ctest.exe +//Name of external makefile project generator. +CMAKE_EXTRA_GENERATOR:INTERNAL= +//Name of generator. +CMAKE_GENERATOR:INTERNAL=Visual Studio 17 2022 +//Generator instance identifier. +CMAKE_GENERATOR_INSTANCE:INTERNAL= +//Name of generator platform. +CMAKE_GENERATOR_PLATFORM:INTERNAL= +//Name of generator toolset. +CMAKE_GENERATOR_TOOLSET:INTERNAL= +//Source directory with the top level CMakeLists.txt file for this +// project +CMAKE_HOME_DIRECTORY:INTERNAL=F:/Downloads/CheatStengine-master/CheatStengine +//number of local generators +CMAKE_NUMBER_OF_MAKEFILES:INTERNAL=1 +//Path to CMake installation. +CMAKE_ROOT:INTERNAL=C:/Program Files/Microsoft Visual Studio/2022/Community/Common7/IDE/CommonExtensions/Microsoft/CMake/CMake/share/cmake-3.31 + diff --git a/CheatStengine/build/CMakeFiles/cmake.check_cache b/CheatStengine/build/CMakeFiles/cmake.check_cache new file mode 100644 index 0000000..3dccd73 --- /dev/null +++ b/CheatStengine/build/CMakeFiles/cmake.check_cache @@ -0,0 +1 @@ +# This file is generated by cmake for dependency checking of the CMakeCache.txt file diff --git a/CheatStengine/out/build/x64-Release/CMakeCache.txt b/CheatStengine/out/build/x64-Release/CMakeCache.txt new file mode 100644 index 0000000..1e80079 --- /dev/null +++ b/CheatStengine/out/build/x64-Release/CMakeCache.txt @@ -0,0 +1,59 @@ +# This is the CMakeCache file. +# For build in directory: f:/Downloads/CheatStengine-master/CheatStengine/out/build/x64-Release +# It was generated by CMake: C:/Program Files/Microsoft Visual Studio/2022/Community/Common7/IDE/CommonExtensions/Microsoft/CMake/CMake/bin/cmake.exe +# You can edit this file to change values found and used by cmake. +# If you do not want to change any of the values, simply exit the editor. +# If you do want to change a value, simply edit, save, and exit the editor. +# The syntax for the file is as follows: +# KEY:TYPE=VALUE +# KEY is the name of a variable in the cache. +# TYPE is a hint to GUIs for the type of VALUE, DO NOT EDIT TYPE!. +# VALUE is the current value for the KEY. + +######################## +# EXTERNAL cache entries +######################## + +//No help, variable specified on the command line. +CMAKE_BUILD_TYPE:UNINITIALIZED=Release + +//Value Computed by CMake. +CMAKE_FIND_PACKAGE_REDIRECTS_DIR:STATIC=F:/Downloads/CheatStengine-master/CheatStengine/out/build/x64-Release/CMakeFiles/pkgRedirects + + +######################## +# INTERNAL cache entries +######################## + +//This is the directory where this CMakeCache.txt was created +CMAKE_CACHEFILE_DIR:INTERNAL=f:/Downloads/CheatStengine-master/CheatStengine/out/build/x64-Release +//Major version of cmake used to create the current loaded cache +CMAKE_CACHE_MAJOR_VERSION:INTERNAL=3 +//Minor version of cmake used to create the current loaded cache +CMAKE_CACHE_MINOR_VERSION:INTERNAL=31 +//Patch version of cmake used to create the current loaded cache +CMAKE_CACHE_PATCH_VERSION:INTERNAL=6 +//Path to CMake executable. +CMAKE_COMMAND:INTERNAL=C:/Program Files/Microsoft Visual Studio/2022/Community/Common7/IDE/CommonExtensions/Microsoft/CMake/CMake/bin/cmake.exe +//Path to cpack program executable. +CMAKE_CPACK_COMMAND:INTERNAL=C:/Program Files/Microsoft Visual Studio/2022/Community/Common7/IDE/CommonExtensions/Microsoft/CMake/CMake/bin/cpack.exe +//Path to ctest program executable. +CMAKE_CTEST_COMMAND:INTERNAL=C:/Program Files/Microsoft Visual Studio/2022/Community/Common7/IDE/CommonExtensions/Microsoft/CMake/CMake/bin/ctest.exe +//Name of external makefile project generator. +CMAKE_EXTRA_GENERATOR:INTERNAL= +//Name of generator. +CMAKE_GENERATOR:INTERNAL=Ninja +//Generator instance identifier. +CMAKE_GENERATOR_INSTANCE:INTERNAL= +//Name of generator platform. +CMAKE_GENERATOR_PLATFORM:INTERNAL= +//Name of generator toolset. +CMAKE_GENERATOR_TOOLSET:INTERNAL= +//Source directory with the top level CMakeLists.txt file for this +// project +CMAKE_HOME_DIRECTORY:INTERNAL=F:/Downloads/CheatStengine-master/CheatStengine +//number of local generators +CMAKE_NUMBER_OF_MAKEFILES:INTERNAL=1 +//Path to CMake installation. +CMAKE_ROOT:INTERNAL=C:/Program Files/Microsoft Visual Studio/2022/Community/Common7/IDE/CommonExtensions/Microsoft/CMake/CMake/share/cmake-3.31 + diff --git a/CheatStengine/out/build/x64-Release/CMakeFiles/cmake.check_cache b/CheatStengine/out/build/x64-Release/CMakeFiles/cmake.check_cache new file mode 100644 index 0000000..3dccd73 --- /dev/null +++ b/CheatStengine/out/build/x64-Release/CMakeFiles/cmake.check_cache @@ -0,0 +1 @@ +# This file is generated by cmake for dependency checking of the CMakeCache.txt file diff --git a/CheatStengine/src/CheatStengine/Core/ThreadPool.h b/CheatStengine/src/CheatStengine/Core/ThreadPool.h index 876bdc1..78b222d 100644 --- a/CheatStengine/src/CheatStengine/Core/ThreadPool.h +++ b/CheatStengine/src/CheatStengine/Core/ThreadPool.h @@ -1,3 +1,5 @@ +#pragma once + #include #include #include @@ -6,6 +8,7 @@ #include #include #include +#include #include class ThreadPool { @@ -51,9 +54,9 @@ class ThreadPool { } template - std::future> Enqueue(F&& f, Args&&... args) + std::future> Enqueue(F&& f, Args&&... args) { - using return_type = std::result_of_t; + using return_type = std::invoke_result_t; auto task = std::make_shared>( std::bind(std::forward(f), std::forward(args)...)); diff --git a/CheatStengine/src/CheatStengine/MainLayer.cpp b/CheatStengine/src/CheatStengine/MainLayer.cpp index da608a9..828c457 100644 --- a/CheatStengine/src/CheatStengine/MainLayer.cpp +++ b/CheatStengine/src/CheatStengine/MainLayer.cpp @@ -11,15 +11,22 @@ #include #include #include +#include +#include #include #include #include #include +#include #include #include #include +#include + +#include + #include DEFINE_ENUM_SETTING(ProcessMode, @@ -34,6 +41,21 @@ MainLayer::MainLayer(Window& window) { SettingsCategory& general = m_SettingsManager.AddCategory("General"); m_ProcessModeSetting = general.AddSetting>("Process Mode", "What should be used to interact with processes?", ProcessMode::WinAPI); + general.AddSetting("Kernel", m_Monitor, [this] { + return m_ProcessModeSetting->GetPendingValue() == ProcessMode::Kernel; + }); + + // MCP control server settings. Auth is off by default so an agent connects + // with just the URL; flip that on to require the bearer token. The toggle + // values are synced to the live server each frame in OnUpdate. The rest of + // the tab (enable/disable, Open Web, Install, live URL/token) is drawn by a + // McpControls so it can act immediately instead of on Apply. + SettingsCategory& server = m_SettingsManager.AddCategory("MCP"); + m_ServerEnabledSetting = server.AddSetting("Enable MCP Server", + "Master switch. When off, the control server is stopped and no agent can connect.", true); + m_RequireAuthSetting = server.AddSetting("Require Auth Token", + "When on, MCP requests must carry the bearer token. When off, the loopback bind is the only gate.", false); + server.AddSetting("MCP Server", [this]() { DrawMcpControls(); }); // Register modals m_ModalManager.RegisterModal("Settings", BIND_FN(MainLayer::SettingsModal)); @@ -84,10 +106,52 @@ MainLayer::MainLayer(Window& window) void MainLayer::OnAttach() { + // Bind the icon cache to the live D3D11 device so the process selector can + // show executable icons. GetGraphicsContext() is the concrete DX11 backend. + auto& context = static_cast(m_Window.GetGraphicsContext()); + m_IconCache.Initialize(context.GetDevice()); + + // Bring up the MCP control server so an external agent can drive the engine. + // Binds to loopback with a per-run bearer token (both logged on startup). + m_McpServer.Start(m_State); +} + +void MainLayer::OnDetach() +{ + m_Monitor.Stop(); + m_McpServer.Stop(); + + // Release icon textures while the device is still alive. + m_IconCache.Shutdown(); } void MainLayer::OnUpdate(float deltaTime) { + if (m_ProcessModeSetting->GetValue() == ProcessMode::Kernel) { + m_Monitor.Start(); + } else { + m_Monitor.Stop(); + } + + // Run any tool work the server queued from its listener thread. Must happen + // on this (main) thread before we touch State below. + m_McpServer.DrainCommands(); + + // Keep the server's auth requirement in step with the settings toggle. + if (m_RequireAuthSetting) { + m_McpServer.SetAuthRequired(m_RequireAuthSetting->GetValue()); + } + + // Start or stop the control server to match the master enable toggle. + if (m_ServerEnabledSetting) { + bool wantRunning = m_ServerEnabledSetting->GetValue(); + if (wantRunning && !m_McpServer.IsRunning()) { + m_McpServer.Start(m_State); + } else if (!wantRunning && m_McpServer.IsRunning()) { + m_McpServer.Stop(); + } + } + if (m_State.Process->IsValid()) { m_State.Modules = m_State.Process->GetModuleEntries(); } @@ -139,6 +203,29 @@ void MainLayer::OpenProcess(uint32_t pid) } } +void MainLayer::DrawRowIcon(uint64_t texture) +{ + // Keep the glyph aligned with the row text and vertically centred against a + // one-line Selectable. A 0 texture still consumes the same width so labels + // line up whether or not an icon was available. + constexpr float iconSize = 16.0f; + float offset = (ImGui::GetTextLineHeight() - iconSize) * 0.5f; + if (offset > 0.0f) { + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + offset); + } + + if (texture) { + ImGui::Image(static_cast(texture), ImVec2 { iconSize, iconSize }); + } else { + ImGui::Dummy(ImVec2 { iconSize, iconSize }); + } + + if (offset > 0.0f) { + ImGui::SetCursorPosY(ImGui::GetCursorPosY() - offset); + } + ImGui::SameLine(0.0f, 6.0f); +} + void MainLayer::OpenProcessModal(const std::string& name, const std::any& payload) { ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), ImGuiCond_Appearing, ImVec2 { 0.5f, 0.5f }); @@ -197,6 +284,14 @@ void MainLayer::DrawOpenProcessList() ImGui::PushID(static_cast(i)); + // Row icon: the process's own executable icon, falling back to the + // generic app glyph. Drawn inline so the Selectable still owns the row. + uint64_t icon = m_IconCache.GetProcessIcon(proc.th32ProcessID); + if (!icon) { + icon = m_IconCache.GetFallbackIcon(); + } + DrawRowIcon(icon); + if (ImGui::Selectable(proc.szExeFile)) { OpenProcess(proc.th32ProcessID); ImGui::CloseCurrentPopup(); @@ -233,6 +328,13 @@ void MainLayer::DrawOpenWindowList() ImGui::PushID(static_cast(i)); + // Row icon: the owning process's icon, resolved via its window handle. + uint64_t icon = m_IconCache.GetWindowIcon(window.Handle); + if (!icon) { + icon = m_IconCache.GetFallbackIcon(); + } + DrawRowIcon(icon); + if (ImGui::Selectable(window.Title.c_str())) { OpenProcess(window.Pid); ImGui::CloseCurrentPopup(); @@ -249,3 +351,59 @@ void MainLayer::SettingsModal(const std::string& name, const std::any& payload) { m_SettingsManager.DrawSettingsPopup(name); } + +void MainLayer::DrawMcpControls() +{ + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Spacing(); + + if (!m_McpServer.IsRunning()) { + ImGui::TextDisabled("Server stopped. Enable it above to accept connections."); + return; + } + + const std::string& url = m_McpServer.GetUrl(); + const std::string& token = m_McpServer.GetToken(); + + ImGui::TextColored(ImVec4 { 0.5f, 0.73f, 1.0f, 1.0f }, "%s", url.c_str()); + ImGui::SameLine(); + if (ImGui::SmallButton("Copy##url")) { + ImGui::SetClipboardText(url.c_str()); + } + + ImGui::TextDisabled("Endpoint"); + ImGui::SameLine(); + std::string endpoint = url + "/mcp"; + ImGui::TextUnformatted(endpoint.c_str()); + ImGui::SameLine(); + if (ImGui::SmallButton("Copy##endpoint")) { + ImGui::SetClipboardText(endpoint.c_str()); + } + + ImGui::TextDisabled("Token"); + ImGui::SameLine(); + ImGui::TextUnformatted(token.c_str()); + ImGui::SameLine(); + if (ImGui::SmallButton("Copy##token")) { + ImGui::SetClipboardText(token.c_str()); + } + + ImGui::Spacing(); + + if (ImGui::Button("Open Web UI")) { + ShellExecuteA(nullptr, "open", url.c_str(), nullptr, nullptr, SW_SHOWNORMAL); + } + ImGui::SameLine(); + if (ImGui::Button("Open Configuration")) { + std::string configUrl = url + "/config.html"; + ShellExecuteA(nullptr, "open", configUrl.c_str(), nullptr, nullptr, SW_SHOWNORMAL); + } + ImGui::SameLine(); + if (ImGui::Button("Reinstall Client Configs")) { + m_McpServer.ReinstallClients(); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Rewrite the MCP entries for Claude Code, Cursor, Claude Desktop, Codex and the skill."); + } +} diff --git a/CheatStengine/src/CheatStengine/MainLayer.h b/CheatStengine/src/CheatStengine/MainLayer.h index a4ec26c..87a62ce 100644 --- a/CheatStengine/src/CheatStengine/MainLayer.h +++ b/CheatStengine/src/CheatStengine/MainLayer.h @@ -3,8 +3,12 @@ #include #include #include +#include +#include #include #include +#include +#include #include #include #include @@ -15,6 +19,7 @@ class MainLayer final : public Layer { ~MainLayer() override = default; void OnAttach() override; + void OnDetach() override; void OnUpdate(float deltaTime) override; void OnImGuiRender() override; void OnImGuiRenderDock() override; @@ -41,8 +46,15 @@ class MainLayer final : public Layer { void DrawOpenProcessList(); void DrawOpenWindowList(); + // Draws a 16px row icon (or a blank same-size spacer when texture is 0) and + // advances the cursor so the following Selectable sits beside it. + static void DrawRowIcon(uint64_t texture); + void SettingsModal(const std::string& name, const std::any& payload); + // Draws the extra MCP tab controls (live URL/token, Open Web, Install). + void DrawMcpControls(); + private: Window& m_Window; @@ -52,16 +64,27 @@ class MainLayer final : public Layer { // Managers ModalManager m_ModalManager; KeybindManager m_KeybindManager; + Monitor m_Monitor; SettingsManager m_SettingsManager; MenuBar m_MenuBar; TitleBar m_TitleBar; + // Agent-facing MCP control server. Owns its own listener thread; we only + // pump its command queue from OnUpdate so engine access stays on this thread. + Server::McpServer m_McpServer; + + // Small cache of process/window shell icons for the selector. Bound to the + // D3D11 device in OnAttach and released in OnDetach. + IconCache m_IconCache; + std::vector m_ProcessEntries; std::vector m_WindowEntries; // Settings EnumSetting* m_ProcessModeSetting; + ToggleSetting* m_ServerEnabledSetting = nullptr; + ToggleSetting* m_RequireAuthSetting = nullptr; friend class MenuBar; friend class DisassemblyPane; @@ -83,4 +106,4 @@ template T, typename... Args> T& MainLayer::AddPane(Args&&... args) { return static_cast(*m_Panes.emplace_back(std::make_unique(std::forward(args)...))); -} \ No newline at end of file +} diff --git a/CheatStengine/src/CheatStengine/Panes/MemoryScannerPane.cpp b/CheatStengine/src/CheatStengine/Panes/MemoryScannerPane.cpp index 60e4f31..6a7f9ba 100644 --- a/CheatStengine/src/CheatStengine/Panes/MemoryScannerPane.cpp +++ b/CheatStengine/src/CheatStengine/Panes/MemoryScannerPane.cpp @@ -10,7 +10,7 @@ #include #include -#include +#include static const char* s_ScanTypeNames[] = { "Exact Value", @@ -35,6 +35,35 @@ static const char* s_ValueTypeNames[] = { }; static_assert(IM_ARRAYSIZE(s_ValueTypeNames) == static_cast(ValueType::COUNT)); +static bool IsIntegerType(ValueType type) +{ + return type != ValueType::Float + && type != ValueType::Double + && type != ValueType::COUNT; +} + +static std::string GetScanInput(const std::string& input, ValueType type, bool isHex) +{ + if (!isHex || !IsIntegerType(type)) { + return input; + } + + const size_t first = input.find_first_not_of(" \t\r\n"); + if (first == std::string::npos) { + return input; + } + const size_t last = input.find_last_not_of(" \t\r\n"); + std::string value = input.substr(first, last - first + 1); + const size_t prefix = value.front() == '+' || value.front() == '-' ? 1 : 0; + if (value.size() >= prefix + 2 + && value[prefix] == '0' + && (value[prefix + 1] == 'x' || value[prefix + 1] == 'X')) { + return value; + } + value.insert(prefix, "0x"); + return value; +} + MemoryScannerPane::MemoryScannerPane(State& state) : Pane(ICON_MDI_RADAR " Memory Scanner", state) { @@ -44,7 +73,32 @@ void MemoryScannerPane::Draw(double deltaTime) { ImGui::Begin(m_Name.c_str(), &m_Open); - ImGui::Text("Found %llu", m_Scanner ? m_Scanner->GetResults().size() : 0); + const uint32_t processId = m_State.Process && m_State.Process->IsValid() + ? m_State.Process->GetPid() + : 0; + if (processId != m_ProcessId) { + m_Scanner.reset(); + m_ProcessId = processId; + m_ModuleFilter = ModuleFilter::Any; + m_SelectedModuleIndex = 0; + m_MinAddress = 0; + m_MaxAddress = 0x7FFFFFFFFFFF; + } + if (m_ModuleFilter == ModuleFilter::Specific + && (m_SelectedModuleIndex < 0 + || static_cast(m_SelectedModuleIndex) >= m_State.Modules.size())) { + m_ModuleFilter = ModuleFilter::Any; + m_SelectedModuleIndex = 0; + m_MinAddress = 0; + m_MaxAddress = 0x7FFFFFFFFFFF; + } + + size_t resultCount = 0; + if (m_Scanner) { + std::lock_guard lock(m_Scanner->GetMutex()); + resultCount = m_Scanner->GetResults().size(); + } + ImGui::Text("Found %llu", static_cast(resultCount)); if (ImGui::BeginTable("MemoryScannerView", 2, ImGuiTableFlags_Resizable)) { ImGui::TableNextRow(); ImGui::TableSetColumnIndex(0); @@ -104,18 +158,13 @@ void MemoryScannerPane::Draw(double deltaTime) ImGui::SetNextItemWidth(100.0f); if (m_Scanner) { - const bool isDisabled = m_Scanner->IsScanning(); - if (isDisabled) { - ImGui::PushItemFlag(ImGuiItemFlags_Disabled, true); - ImGui::PushStyleVar(ImGuiStyleVar_Alpha, ImGui::GetStyle().Alpha * 0.5f); - } - if (ImGui::Button("New Scan", ImVec2 { 80.0f, 0.0f })) { + if (m_Scanner->IsScanning()) { + if (ImGui::Button("Cancel Scan", ImVec2 { 80.0f, 0.0f })) { + m_Scanner->Cancel(); + } + } else if (ImGui::Button("New Scan", ImVec2 { 80.0f, 0.0f })) { m_Scanner.reset(); } - if (isDisabled) { - ImGui::PopStyleVar(); - ImGui::PopItemFlag(); - } } else { if (ImGui::Button("First Scan", ImVec2 { 80.0f, 0.0f })) { StartFirstScan(); @@ -131,7 +180,9 @@ void MemoryScannerPane::Draw(double deltaTime) if (ImGui::Button("Next Scan", ImVec2 { 80.0f, 0.0f })) { if (m_Scanner) { - m_Scanner->NextScan(m_ValueType, m_ScanType, m_MinAddress, m_MaxAddress, m_ScanInputLower, m_ScanInputUpper); + const std::string lowerValue = GetScanInput(m_ScanInputLower, m_ValueType, m_IsInputHex); + const std::string upperValue = GetScanInput(m_ScanInputUpper, m_ValueType, m_IsInputHex); + m_Scanner->NextScan(m_ValueType, m_ScanType, m_MinAddress, m_MaxAddress, lowerValue, upperValue); } } @@ -178,6 +229,9 @@ void MemoryScannerPane::Draw(double deltaTime) ImGui::EndCombo(); } + if (m_Scanner) { + ImGui::BeginDisabled(); + } if (ImGui::BeginCombo("Value Type", s_ValueTypeNames[static_cast(m_ValueType)])) { for (size_t n = 0; n < IM_ARRAYSIZE(s_ValueTypeNames); n++) { bool isSelected = (m_ValueType == static_cast(n)); @@ -190,6 +244,9 @@ void MemoryScannerPane::Draw(double deltaTime) } ImGui::EndCombo(); } + if (m_Scanner) { + ImGui::EndDisabled(); + } std::string comboLabel = (m_ModuleFilter == ModuleFilter::Any) ? "Any" : m_State.Modules[m_SelectedModuleIndex].szModule; if (ImGui::BeginCombo("Module", comboLabel.c_str())) { @@ -211,7 +268,10 @@ void MemoryScannerPane::Draw(double deltaTime) m_ModuleFilter = ModuleFilter::Specific; m_SelectedModuleIndex = i; m_MinAddress = reinterpret_cast(m_State.Modules[i].modBaseAddr); - m_MaxAddress = m_MinAddress + m_State.Modules[i].modBaseSize; + const uintptr_t moduleSize = static_cast(m_State.Modules[i].modBaseSize); + m_MaxAddress = moduleSize > std::numeric_limits::max() - m_MinAddress + ? std::numeric_limits::max() + : m_MinAddress + moduleSize; } if (isSelected) { ImGui::SetItemDefaultFocus(); @@ -231,9 +291,16 @@ void MemoryScannerPane::Draw(double deltaTime) void MemoryScannerPane::StartFirstScan() { + if (!m_State.Process || !m_State.Process->IsValid()) { + ERR("Select a valid process before starting a memory scan"); + return; + } + INFO("Scanning..."); + const std::string lowerValue = GetScanInput(m_ScanInputLower, m_ValueType, m_IsInputHex); + const std::string upperValue = GetScanInput(m_ScanInputUpper, m_ValueType, m_IsInputHex); m_Scanner = std::make_unique(m_State.Process); - if (!m_Scanner->FirstScan(m_ValueType, m_ScanType, m_MinAddress, m_MaxAddress, m_ScanInputLower, m_ScanInputUpper)) { + if (!m_Scanner->FirstScan(m_ValueType, m_ScanType, m_MinAddress, m_MaxAddress, lowerValue, upperValue)) { m_Scanner = nullptr; } } diff --git a/CheatStengine/src/CheatStengine/Panes/MemoryScannerPane.h b/CheatStengine/src/CheatStengine/Panes/MemoryScannerPane.h index adc9b36..cd6f5e6 100644 --- a/CheatStengine/src/CheatStengine/Panes/MemoryScannerPane.h +++ b/CheatStengine/src/CheatStengine/Panes/MemoryScannerPane.h @@ -35,6 +35,7 @@ class MemoryScannerPane final : public Pane { uintptr_t m_MaxAddress = 0x7FFFFFFFFFFF; ModuleFilter m_ModuleFilter = ModuleFilter::Any; int m_SelectedModuleIndex = 0; // Index in the modules vector + uint32_t m_ProcessId = 0; // Scan Results std::unique_ptr m_Scanner; diff --git a/CheatStengine/src/CheatStengine/Process/Device.cpp b/CheatStengine/src/CheatStengine/Process/Device.cpp new file mode 100644 index 0000000..91f653f --- /dev/null +++ b/CheatStengine/src/CheatStengine/Process/Device.cpp @@ -0,0 +1,828 @@ +#include "Device.h" + +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace { + +constexpr DWORD ControlTimeoutMilliseconds = 5000; +constexpr DWORD CancellationTimeoutMilliseconds = 1000; +constexpr wchar_t ControlDevicePath[] = L"\\\\.\\MemoryAccess"; + +enum class InterfaceResult { + Found, + Missing, + Failed, +}; + +struct InterfacePaths { + InterfaceResult Result = InterfaceResult::Failed; + std::vector Paths; + DWORD Error = ERROR_GEN_FAILURE; +}; + +struct PendingControl { + OVERLAPPED Overlapped {}; + HANDLE Event = nullptr; + std::vector Input; + std::vector Buffer; + + ~PendingControl() + { + if (Event) { + CloseHandle(Event); + } + } +}; + +InterfacePaths FindInterfacePaths() +{ + GUID interfaceGuid = Protocol::DeviceInterfaceGuid; + ULONG pathLength = 0; + CONFIGRET result = CM_Get_Device_Interface_List_SizeW( + &pathLength, + &interfaceGuid, + nullptr, + CM_GET_DEVICE_INTERFACE_LIST_PRESENT); + if (result != CR_SUCCESS) { + return { + InterfaceResult::Failed, + {}, + CM_MapCrToWin32Err(result, ERROR_GEN_FAILURE), + }; + } + if (pathLength <= 1) { + return { InterfaceResult::Missing, {}, ERROR_FILE_NOT_FOUND }; + } + + std::vector buffer(pathLength, L'\0'); + result = CM_Get_Device_Interface_ListW( + &interfaceGuid, + nullptr, + buffer.data(), + pathLength, + CM_GET_DEVICE_INTERFACE_LIST_PRESENT); + if (result != CR_SUCCESS) { + return { + InterfaceResult::Failed, + {}, + CM_MapCrToWin32Err(result, ERROR_GEN_FAILURE), + }; + } + + InterfacePaths paths; + paths.Result = InterfaceResult::Found; + paths.Error = ERROR_SUCCESS; + const wchar_t* current = buffer.data(); + const wchar_t* end = buffer.data() + buffer.size(); + while (current < end && *current) { + const size_t remaining = static_cast(end - current); + const size_t length = wcsnlen_s(current, remaining); + if (length == remaining) { + return { InterfaceResult::Failed, {}, ERROR_INVALID_DATA }; + } + paths.Paths.emplace_back(current, length); + current += length + 1; + } + + if (paths.Paths.empty()) { + paths.Result = InterfaceResult::Missing; + paths.Error = ERROR_FILE_NOT_FOUND; + } + return paths; +} + +bool IsResponseValid( + const Protocol::PacketHeader& header, + size_t expectedSize, + DWORD transferred) +{ + return header.Size >= expectedSize + && header.Size <= transferred + && header.MajorVersion == Protocol::VersionMajor + && header.MinorVersion >= Protocol::VersionMinor + && header.Flags == 0 + && header.Reserved == 0; +} + +bool IsAddressRangeValid(uintptr_t address, size_t size) +{ + return address != 0 + && size != 0 + && size - 1 <= std::numeric_limits::max() - address; +} + +void WaitForAbandonedControl(std::shared_ptr control) +{ + WaitForSingleObject(control->Event, INFINITE); +} + +} + +Device::Device() +{ + static_cast(Open()); +} + +Device::~Device() +{ + Close(); +} + +bool Device::IsOpen() const +{ + std::scoped_lock lock(m_ControlMutex); + return m_Handle != INVALID_HANDLE_VALUE; +} + +bool Device::Bind(uint32_t processId) const +{ + if (processId == 0) { + SetError(ERROR_INVALID_PARAMETER); + return false; + } + if (!HasCapability(Protocol::BindProcess)) { + SetError(ERROR_NOT_SUPPORTED); + return false; + } + + Protocol::BindProcessRequest request {}; + request.Header = Protocol::CreateHeader(); + request.ProcessId = processId; + + DWORD transferred = 0; + if (!Control( + Protocol::BindProcessControlCode, + &request, + sizeof(request), + nullptr, + nullptr, + 0, + transferred)) { + return false; + } + if (transferred != 0) { + SetError(ERROR_INVALID_DATA); + return false; + } + return true; +} + +std::optional Device::Ping(uint64_t nonce) const +{ + if (!HasCapability(Protocol::Ping)) { + SetError(ERROR_NOT_SUPPORTED); + return std::nullopt; + } + + Protocol::PingRequest request {}; + request.Header = Protocol::CreateHeader(); + request.Nonce = nonce; + Protocol::PingResponse response {}; + DWORD transferred = 0; + if (!Control( + Protocol::PingControlCode, + &request, + sizeof(request), + nullptr, + &response, + sizeof(response), + transferred)) { + return std::nullopt; + } + if (transferred != sizeof(response) + || !IsResponseValid(response.Header, sizeof(response), transferred) + || response.Nonce != nonce + || (response.Capabilities & Protocol::RequiredCapabilities) != Protocol::RequiredCapabilities + || response.MaximumTransferSize == 0 + || response.IdleTimeoutSeconds < Protocol::MinimumIdleTimeoutSeconds + || response.IdleTimeoutSeconds > Protocol::MaximumIdleTimeoutSeconds + || response.RemainingIdleSeconds > response.IdleTimeoutSeconds + || response.LoadMode == static_cast(Protocol::LoadMode::Unknown) + || response.LoadMode > static_cast(Protocol::LoadMode::Mapped) + || response.State > static_cast(Protocol::RuntimeState::Removed) + || response.Reserved != 0) { + SetError(ERROR_INVALID_DATA); + return std::nullopt; + } + + SetError(ERROR_SUCCESS); + return response; +} + +std::optional Device::Heartbeat(uint64_t sequence) const +{ + if (!HasCapability(Protocol::Heartbeat)) { + SetError(ERROR_NOT_SUPPORTED); + return std::nullopt; + } + + Protocol::HeartbeatRequest request {}; + request.Header = Protocol::CreateHeader(); + request.Sequence = sequence; + Protocol::HeartbeatResponse response {}; + DWORD transferred = 0; + if (!Control( + Protocol::HeartbeatControlCode, + &request, + sizeof(request), + nullptr, + &response, + sizeof(response), + transferred)) { + return std::nullopt; + } + if (transferred != sizeof(response) + || !IsResponseValid(response.Header, sizeof(response), transferred) + || response.Sequence != sequence + || response.State > static_cast(Protocol::RuntimeState::Removed)) { + SetError(ERROR_INVALID_DATA); + return std::nullopt; + } + + SetError(ERROR_SUCCESS); + return response; +} + +std::optional Device::Configure(uint32_t idleTimeoutSeconds) const +{ + if (idleTimeoutSeconds < Protocol::MinimumIdleTimeoutSeconds + || idleTimeoutSeconds > Protocol::MaximumIdleTimeoutSeconds) { + SetError(ERROR_INVALID_PARAMETER); + return std::nullopt; + } + if (!HasCapability(Protocol::Configure)) { + SetError(ERROR_NOT_SUPPORTED); + return std::nullopt; + } + + Protocol::ConfigureRequest request {}; + request.Header = Protocol::CreateHeader(); + request.IdleTimeoutSeconds = idleTimeoutSeconds; + Protocol::ConfigureResponse response {}; + DWORD transferred = 0; + if (!Control( + Protocol::ConfigureControlCode, + &request, + sizeof(request), + nullptr, + &response, + sizeof(response), + transferred)) { + return std::nullopt; + } + if (transferred != sizeof(response) + || !IsResponseValid(response.Header, sizeof(response), transferred) + || response.IdleTimeoutSeconds < Protocol::MinimumIdleTimeoutSeconds + || response.IdleTimeoutSeconds > Protocol::MaximumIdleTimeoutSeconds + || response.Reserved != 0) { + SetError(ERROR_INVALID_DATA); + return std::nullopt; + } + + SetError(ERROR_SUCCESS); + return response; +} + +bool Device::Read(uintptr_t address, void* buffer, size_t size) const +{ + if (size == 0) { + SetError(ERROR_SUCCESS); + return true; + } + if (!buffer || !IsAddressRangeValid(address, size)) { + SetError(ERROR_INVALID_PARAMETER); + return false; + } + if (!HasCapability(Protocol::ReadMemory)) { + SetError(ERROR_NOT_SUPPORTED); + return false; + } + + const uint64_t maximumTransferSize = GetMaximumTransferSize(); + if (maximumTransferSize == 0) { + SetError(ERROR_INVALID_HANDLE); + return false; + } + + size_t offset = 0; + while (offset < size) { + const size_t chunk = static_cast(std::min( + maximumTransferSize, + static_cast(size - offset))); + Protocol::ReadMemoryRequest request {}; + request.Header = Protocol::CreateHeader(); + request.Address = static_cast(address + offset); + request.Size = static_cast(chunk); + + DWORD transferred = 0; + if (!Control( + Protocol::ReadMemoryControlCode, + &request, + sizeof(request), + nullptr, + static_cast(buffer) + offset, + static_cast(chunk), + transferred)) { + return false; + } + if (transferred != chunk) { + SetError(ERROR_PARTIAL_COPY); + return false; + } + offset += chunk; + } + + SetError(ERROR_SUCCESS); + return true; +} + +bool Device::Write(uintptr_t address, const void* buffer, size_t size) const +{ + if (size == 0) { + SetError(ERROR_SUCCESS); + return true; + } + if (!buffer || !IsAddressRangeValid(address, size)) { + SetError(ERROR_INVALID_PARAMETER); + return false; + } + if (!HasCapability(Protocol::WriteMemory)) { + SetError(ERROR_NOT_SUPPORTED); + return false; + } + + const uint64_t maximumTransferSize = GetMaximumTransferSize(); + if (maximumTransferSize == 0) { + SetError(ERROR_INVALID_HANDLE); + return false; + } + + size_t offset = 0; + while (offset < size) { + const size_t chunk = static_cast(std::min( + maximumTransferSize, + static_cast(size - offset))); + Protocol::WriteMemoryRequest request {}; + request.Header = Protocol::CreateHeader(); + request.Address = static_cast(address + offset); + request.Size = static_cast(chunk); + + DWORD transferred = 0; + if (!Control( + Protocol::WriteMemoryControlCode, + &request, + sizeof(request), + static_cast(buffer) + offset, + nullptr, + static_cast(chunk), + transferred)) { + return false; + } + if (transferred != chunk) { + SetError(ERROR_PARTIAL_COPY); + return false; + } + offset += chunk; + } + + SetError(ERROR_SUCCESS); + return true; +} + +std::optional Device::Query(uintptr_t address) const +{ + if (!HasCapability(Protocol::QueryMemory)) { + SetError(ERROR_NOT_SUPPORTED); + return std::nullopt; + } + + Protocol::QueryMemoryRequest request {}; + request.Header = Protocol::CreateHeader(); + request.Address = static_cast(address); + Protocol::QueryMemoryResponse response {}; + DWORD transferred = 0; + if (!Control( + Protocol::QueryMemoryControlCode, + &request, + sizeof(request), + nullptr, + &response, + sizeof(response), + transferred)) { + return std::nullopt; + } + if (transferred != sizeof(response) + || !IsResponseValid(response.Header, sizeof(response), transferred) + || response.RegionSize == 0 + || response.RegionSize > std::numeric_limits::max()) { + SetError(ERROR_INVALID_DATA); + return std::nullopt; + } + + MEMORY_BASIC_INFORMATION information {}; + information.BaseAddress = reinterpret_cast(static_cast(response.BaseAddress)); + information.AllocationBase = reinterpret_cast(static_cast(response.AllocationBase)); + information.AllocationProtect = response.AllocationProtect; + information.RegionSize = static_cast(response.RegionSize); + information.State = response.State; + information.Protect = response.Protect; + information.Type = response.Type; + SetError(ERROR_SUCCESS); + return information; +} + +uintptr_t Device::Allocate( + size_t size, + uint32_t protection, + uint32_t allocationType, + uintptr_t preferredAddress) const +{ + if (size == 0 + || (preferredAddress != 0 + && size - 1 > std::numeric_limits::max() - preferredAddress)) { + SetError(ERROR_INVALID_PARAMETER); + return 0; + } + if (!HasCapability(Protocol::AllocateMemory)) { + SetError(ERROR_NOT_SUPPORTED); + return 0; + } + + Protocol::AllocateMemoryRequest request {}; + request.Header = Protocol::CreateHeader(); + request.Address = static_cast(preferredAddress); + request.Size = static_cast(size); + request.AllocationType = allocationType; + request.Protect = protection; + Protocol::AllocateMemoryResponse response {}; + DWORD transferred = 0; + if (!Control( + Protocol::AllocateMemoryControlCode, + &request, + sizeof(request), + nullptr, + &response, + sizeof(response), + transferred)) { + return 0; + } + if (transferred != sizeof(response) + || !IsResponseValid(response.Header, sizeof(response), transferred) + || response.Address == 0 + || response.Address > std::numeric_limits::max() + || response.Size < size) { + SetError(ERROR_INVALID_DATA); + return 0; + } + + SetError(ERROR_SUCCESS); + return static_cast(response.Address); +} + +bool Device::Free(uintptr_t address, uint32_t freeType, size_t size) const +{ + if (address == 0) { + SetError(ERROR_INVALID_PARAMETER); + return false; + } + if (!HasCapability(Protocol::FreeMemory)) { + SetError(ERROR_NOT_SUPPORTED); + return false; + } + + Protocol::FreeMemoryRequest request {}; + request.Header = Protocol::CreateHeader(); + request.Address = static_cast(address); + request.Size = static_cast(size); + request.FreeType = freeType; + + DWORD transferred = 0; + if (!Control( + Protocol::FreeMemoryControlCode, + &request, + sizeof(request), + nullptr, + nullptr, + 0, + transferred)) { + return false; + } + if (transferred != 0) { + SetError(ERROR_INVALID_DATA); + return false; + } + return true; +} + +std::optional Device::Protect( + uintptr_t address, + size_t size, + uint32_t protection) const +{ + if (!IsAddressRangeValid(address, size)) { + SetError(ERROR_INVALID_PARAMETER); + return std::nullopt; + } + if (!HasCapability(Protocol::ProtectMemory)) { + SetError(ERROR_NOT_SUPPORTED); + return std::nullopt; + } + + Protocol::ProtectMemoryRequest request {}; + request.Header = Protocol::CreateHeader(); + request.Address = static_cast(address); + request.Size = static_cast(size); + request.Protect = protection; + Protocol::ProtectMemoryResponse response {}; + DWORD transferred = 0; + if (!Control( + Protocol::ProtectMemoryControlCode, + &request, + sizeof(request), + nullptr, + &response, + sizeof(response), + transferred)) { + return std::nullopt; + } + if (transferred != sizeof(response) + || !IsResponseValid(response.Header, sizeof(response), transferred) + || response.Address == 0 + || response.Size == 0 + || response.Reserved != 0) { + SetError(ERROR_INVALID_DATA); + return std::nullopt; + } + + SetError(ERROR_SUCCESS); + return response.OldProtect; +} + +Protocol::LoadMode Device::GetMode() const +{ + return m_Mode.load(std::memory_order_relaxed); +} + +uint64_t Device::GetCapabilities() const +{ + return m_Capabilities.load(std::memory_order_relaxed); +} + +uint64_t Device::GetMaximumTransferSize() const +{ + return m_MaximumTransferSize.load(std::memory_order_relaxed); +} + +DWORD Device::GetError() const +{ + return m_Error.load(std::memory_order_relaxed); +} + +bool Device::Open() +{ + const InterfacePaths interfaces = FindInterfacePaths(); + DWORD pnpError = interfaces.Error; + + if (interfaces.Result == InterfaceResult::Found) { + for (const std::wstring& path : interfaces.Paths) { + if (OpenPath(path, Protocol::LoadMode::Pnp)) { + return true; + } + pnpError = GetError(); + } + } + + if (OpenPath(ControlDevicePath, Protocol::LoadMode::Mapped)) { + return true; + } + + const DWORD mappedError = GetError(); + const bool mappedMissing = mappedError == ERROR_FILE_NOT_FOUND + || mappedError == ERROR_PATH_NOT_FOUND; + const bool pnpMissing = pnpError == ERROR_FILE_NOT_FOUND + || pnpError == ERROR_PATH_NOT_FOUND; + if (mappedMissing && !pnpMissing && pnpError != ERROR_SUCCESS) { + SetError(pnpError); + } + return false; +} + +bool Device::OpenPath(const std::wstring& path, Protocol::LoadMode mode) +{ + HANDLE handle = CreateFileW( + path.c_str(), + GENERIC_READ | GENERIC_WRITE, + FILE_SHARE_READ | FILE_SHARE_WRITE, + nullptr, + OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, + nullptr); + if (handle == INVALID_HANDLE_VALUE) { + SetError(GetLastError()); + return false; + } + + { + std::scoped_lock lock(m_ControlMutex); + m_Handle = handle; + m_Mode.store(mode, std::memory_order_relaxed); + } + + if (ReadInformation()) { + return true; + } + + const DWORD error = GetError(); + Close(); + SetError(error); + return false; +} + +bool Device::ReadInformation() +{ + Protocol::InformationRequest request {}; + request.Header = Protocol::CreateHeader(); + Protocol::InformationResponse response {}; + DWORD transferred = 0; + if (!Control( + Protocol::InformationControlCode, + &request, + sizeof(request), + nullptr, + &response, + sizeof(response), + transferred)) { + return false; + } + if (transferred != sizeof(response) + || !IsResponseValid(response.Header, sizeof(response), transferred) + || response.LoadMode == static_cast(Protocol::LoadMode::Unknown) + || response.LoadMode > static_cast(Protocol::LoadMode::Mapped) + || response.Reserved != 0) { + SetError(ERROR_REVISION_MISMATCH); + return false; + } + if ((response.Capabilities & Protocol::RequiredCapabilities) + != Protocol::RequiredCapabilities + || response.MaximumTransferSize == 0) { + SetError(ERROR_NOT_SUPPORTED); + return false; + } + + const uint64_t maximumTransferSize = std::min({ + response.MaximumTransferSize, + Protocol::MaximumTransferSize, + static_cast(std::numeric_limits::max()), + }); + if (maximumTransferSize == 0) { + SetError(ERROR_INVALID_DATA); + return false; + } + + m_Capabilities.store(response.Capabilities, std::memory_order_relaxed); + m_MaximumTransferSize.store(maximumTransferSize, std::memory_order_relaxed); + m_Mode.store(static_cast(response.LoadMode), std::memory_order_relaxed); + SetError(ERROR_SUCCESS); + return true; +} + +bool Device::HasCapability(uint64_t capability) const +{ + return (GetCapabilities() & capability) == capability; +} + +bool Device::Control( + DWORD code, + const void* input, + DWORD inputSize, + const void* directInput, + void* output, + DWORD bufferSize, + DWORD& transferred) const +{ + transferred = 0; + if ((inputSize != 0 && !input) + || (bufferSize != 0 && !directInput && !output) + || (directInput && output)) { + SetError(ERROR_INVALID_PARAMETER); + return false; + } + + std::shared_ptr control; + try { + control = std::make_shared(); + control->Input.resize(inputSize); + control->Buffer.resize(bufferSize); + } catch (const std::bad_alloc&) { + SetError(ERROR_NOT_ENOUGH_MEMORY); + return false; + } + + if (inputSize != 0) { + std::memcpy(control->Input.data(), input, inputSize); + } + if (bufferSize != 0 && directInput) { + std::memcpy(control->Buffer.data(), directInput, bufferSize); + } + + control->Event = CreateEventW(nullptr, TRUE, FALSE, nullptr); + if (!control->Event) { + SetError(GetLastError()); + return false; + } + control->Overlapped.hEvent = control->Event; + + std::scoped_lock lock(m_ControlMutex); + if (m_Handle == INVALID_HANDLE_VALUE) { + SetError(ERROR_INVALID_HANDLE); + return false; + } + + DWORD immediateTransferred = 0; + const BOOL started = DeviceIoControl( + m_Handle, + code, + inputSize == 0 ? nullptr : control->Input.data(), + inputSize, + bufferSize == 0 ? nullptr : control->Buffer.data(), + bufferSize, + &immediateTransferred, + &control->Overlapped); + if (!started) { + const DWORD error = GetLastError(); + if (error != ERROR_IO_PENDING) { + SetError(error); + return false; + } + } + + const DWORD waitResult = WaitForSingleObject( + control->Event, + ControlTimeoutMilliseconds); + if (waitResult != WAIT_OBJECT_0) { + const DWORD waitError = waitResult == WAIT_TIMEOUT ? ERROR_TIMEOUT : GetLastError(); + CancelIoEx(m_Handle, &control->Overlapped); + const DWORD cancellationResult = WaitForSingleObject( + control->Event, + CancellationTimeoutMilliseconds); + if (cancellationResult != WAIT_OBJECT_0) { + CloseLocked(); + try { + std::thread(WaitForAbandonedControl, control).detach(); + } catch (...) { + WaitForSingleObject(control->Event, INFINITE); + } + } + SetError(waitError); + return false; + } + + if (!GetOverlappedResult( + m_Handle, + &control->Overlapped, + &transferred, + FALSE)) { + SetError(GetLastError()); + return false; + } + if (transferred > bufferSize) { + SetError(ERROR_INVALID_DATA); + return false; + } + if (output && transferred != 0) { + std::memcpy(output, control->Buffer.data(), transferred); + } + + SetError(ERROR_SUCCESS); + return true; +} + +void Device::Close() +{ + std::scoped_lock lock(m_ControlMutex); + CloseLocked(); +} + +void Device::CloseLocked() const +{ + if (m_Handle != INVALID_HANDLE_VALUE) { + CloseHandle(m_Handle); + m_Handle = INVALID_HANDLE_VALUE; + } + m_Mode.store(Protocol::LoadMode::Unknown, std::memory_order_relaxed); + m_Capabilities.store(0, std::memory_order_relaxed); + m_MaximumTransferSize.store(0, std::memory_order_relaxed); +} + +void Device::SetError(DWORD error) const +{ + m_Error.store(error, std::memory_order_relaxed); +} diff --git a/CheatStengine/src/CheatStengine/Process/Device.h b/CheatStengine/src/CheatStengine/Process/Device.h new file mode 100644 index 0000000..65b3ca4 --- /dev/null +++ b/CheatStengine/src/CheatStengine/Process/Device.h @@ -0,0 +1,77 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include +#include + +class Device final { +public: + Device(); + ~Device(); + + Device(const Device&) = delete; + Device& operator=(const Device&) = delete; + Device(Device&&) = delete; + Device& operator=(Device&&) = delete; + + [[nodiscard]] bool IsOpen() const; + [[nodiscard]] bool Bind(uint32_t processId) const; + + [[nodiscard]] std::optional Ping(uint64_t nonce = 0) const; + [[nodiscard]] std::optional Heartbeat(uint64_t sequence) const; + [[nodiscard]] std::optional Configure(uint32_t idleTimeoutSeconds) const; + + [[nodiscard]] bool Read(uintptr_t address, void* buffer, size_t size) const; + [[nodiscard]] bool Write(uintptr_t address, const void* buffer, size_t size) const; + + [[nodiscard]] std::optional Query(uintptr_t address) const; + [[nodiscard]] uintptr_t Allocate( + size_t size, + uint32_t protection, + uint32_t allocationType = MEM_COMMIT | MEM_RESERVE, + uintptr_t preferredAddress = 0) const; + [[nodiscard]] bool Free( + uintptr_t address, + uint32_t freeType = MEM_RELEASE, + size_t size = 0) const; + [[nodiscard]] std::optional Protect( + uintptr_t address, + size_t size, + uint32_t protection) const; + + [[nodiscard]] Protocol::LoadMode GetMode() const; + [[nodiscard]] uint64_t GetCapabilities() const; + [[nodiscard]] uint64_t GetMaximumTransferSize() const; + [[nodiscard]] DWORD GetError() const; + +private: + [[nodiscard]] bool Open(); + [[nodiscard]] bool OpenPath(const std::wstring& path, Protocol::LoadMode mode); + [[nodiscard]] bool ReadInformation(); + [[nodiscard]] bool HasCapability(uint64_t capability) const; + [[nodiscard]] bool Control( + DWORD code, + const void* input, + DWORD inputSize, + const void* directInput, + void* output, + DWORD bufferSize, + DWORD& transferred) const; + + void Close(); + void CloseLocked() const; + void SetError(DWORD error) const; + +private: + mutable std::mutex m_ControlMutex; + mutable HANDLE m_Handle = INVALID_HANDLE_VALUE; + mutable std::atomic m_Mode = Protocol::LoadMode::Unknown; + mutable std::atomic m_Capabilities = 0; + mutable std::atomic m_MaximumTransferSize = 0; + mutable std::atomic m_Error = ERROR_SUCCESS; +}; diff --git a/CheatStengine/src/CheatStengine/Process/KernelProcess.cpp b/CheatStengine/src/CheatStengine/Process/KernelProcess.cpp index 39f4a79..830090e 100644 --- a/CheatStengine/src/CheatStengine/Process/KernelProcess.cpp +++ b/CheatStengine/src/CheatStengine/Process/KernelProcess.cpp @@ -1,166 +1,75 @@ #include "KernelProcess.h" #include -#include - -#define IOCTL_CS_COMMAND CTL_CODE(FILE_DEVICE_UNKNOWN, 0x6969, METHOD_BUFFERED, FILE_ANY_ACCESS) - -struct CommandHeader { - enum Type : uint32_t { - ReadMemory = 0, - WriteMemory = 1, - QueryMemory = 2, - AllocateMemory = 3, - FreeMemory = 4, - ProtectMemory = 5, - } Type; - - uint32_t Pid; - - union { - struct - { - uintptr_t Address; - size_t Size; - void* Buffer; - } ReadMemoryData; - - struct - { - uintptr_t Address; - size_t Size; - void* Buffer; - } WriteMemoryData; - - struct - { - uintptr_t Address; - MEMORY_BASIC_INFORMATION* Mbi; - } QueryMemoryData; - - struct - { - uintptr_t Address; - size_t Size; - uint32_t AllocationType; - uint32_t Protect; - uintptr_t* AllocatedAddressPtr; - } AllocateMemoryData; - struct - { - uintptr_t Address; - size_t Size; - uint32_t FreeType; - } FreeMemoryData; - - struct - { - uintptr_t Address; - size_t Size; - uint32_t NewProtect; - uint32_t* OldProtectPtr; - } ProtectMemoryData; - }; -}; - -static bool SendBuffer(HANDLE deviceHandle, CommandHeader* command, void* outputBuffer = nullptr, size_t outputBufferSize = 0) -{ - DWORD bytesReturned = 0; - BOOL result = FALSE; - result = DeviceIoControl( - deviceHandle, - IOCTL_CS_COMMAND, - command, - sizeof(CommandHeader), - outputBuffer, - outputBufferSize, - &bytesReturned, - nullptr); - if (!result) { - ERR("DeviceIoControl failed. Error Code: {}", GetLastError()); - return false; - } - - return true; -} +#include -KernelProcess::KernelProcess(DWORD pid) - : Process(pid) -{ - m_DeviceHandle = CreateFileW( - L"\\\\.\\CheatStengineDriver", - GENERIC_READ | GENERIC_WRITE, - FILE_SHARE_READ | FILE_SHARE_WRITE, - nullptr, - OPEN_EXISTING, - 0, - nullptr); - if (m_DeviceHandle == INVALID_HANDLE_VALUE) { - ERR("Failed to open handle to driver. Error Code: {}", GetLastError()); - } -} +namespace { -KernelProcess::KernelProcess(const std::string& procName) - : KernelProcess(0) +DWORD FindProcessId(std::string_view processName) { HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); if (snapshot == INVALID_HANDLE_VALUE) { - return; + return 0; } - PROCESSENTRY32 entry; - entry.dwSize = sizeof(PROCESSENTRY32); + const std::string name(processName); + DWORD processId = 0; + PROCESSENTRY32 entry {}; + entry.dwSize = sizeof(entry); if (Process32First(snapshot, &entry)) { do { - if (strncmp(entry.szExeFile, procName.c_str(), procName.size()) == 0) { - m_Pid = entry.th32ProcessID; + if (_stricmp(entry.szExeFile, name.c_str()) == 0) { + processId = entry.th32ProcessID; break; } } while (Process32Next(snapshot, &entry)); } CloseHandle(snapshot); + return processId; } -KernelProcess::~KernelProcess() -{ } -uintptr_t KernelProcess::Allocate(size_t size, uint32_t protection, uint32_t allocationType) const +KernelProcess::KernelProcess(DWORD pid) + : Process(pid) { - if (!IsValid()) { - return 0; + if (Process::IsValid()) { + m_Bound = m_Device.Bind(m_Pid); + if (!m_Bound) { + ERR("Failed to bind the driver to process {}: {}", m_Pid, m_Device.GetError()); + } } +} - CommandHeader command {}; - command.Type = CommandHeader::Type::AllocateMemory; - command.Pid = m_Pid; - command.AllocateMemoryData.Address = 0; - command.AllocateMemoryData.Size = size; - command.AllocateMemoryData.AllocationType = allocationType; - command.AllocateMemoryData.Protect = protection; +KernelProcess::KernelProcess(const std::string& procName) + : Process(FindProcessId(procName)) +{ + if (Process::IsValid()) { + m_Bound = m_Device.Bind(m_Pid); + if (!m_Bound) { + ERR("Failed to bind the driver to process {}: {}", m_Pid, m_Device.GetError()); + } + } +} - uintptr_t allocatedAddress = 0; - command.AllocateMemoryData.AllocatedAddressPtr = &allocatedAddress; +KernelProcess::~KernelProcess() = default; - SendBuffer(m_DeviceHandle, &command); - return allocatedAddress; +uintptr_t KernelProcess::Allocate(size_t size, uint32_t protection, uint32_t allocationType) const +{ + if (!IsValid() || size == 0) { + return 0; + } + return m_Device.Allocate(size, protection, allocationType); } bool KernelProcess::Free(uintptr_t address, uint32_t freeType) const { - if (!IsValid()) { + if (!IsValid() || address == 0) { return false; } - - CommandHeader command {}; - command.Type = CommandHeader::Type::FreeMemory; - command.Pid = m_Pid; - command.FreeMemoryData.Address = address; - command.FreeMemoryData.Size = 0; - command.FreeMemoryData.FreeType = freeType; - return SendBuffer(m_DeviceHandle, &command); + return m_Device.Free(address, freeType); } std::optional KernelProcess::Query(uintptr_t address) const @@ -168,37 +77,18 @@ std::optional KernelProcess::Query(uintptr_t address) if (!IsValid()) { return std::nullopt; } - - CommandHeader command {}; - command.Type = CommandHeader::Type::QueryMemory; - command.Pid = m_Pid; - command.QueryMemoryData.Address = address; - MEMORY_BASIC_INFORMATION mbi {}; - if (!SendBuffer(m_DeviceHandle, &command, &mbi, sizeof(MEMORY_BASIC_INFORMATION))) { - return std::nullopt; - } - return mbi; + return m_Device.Query(address); } -std::optional KernelProcess::Protect(uintptr_t address, size_t size, uint32_t protection) const +std::optional KernelProcess::Protect( + uintptr_t address, + size_t size, + uint32_t protection) const { - if (!IsValid()) { - return false; - } - - CommandHeader command {}; - command.Type = CommandHeader::Type::ProtectMemory; - command.Pid = m_Pid; - command.ProtectMemoryData.Address = address; - command.ProtectMemoryData.Size = size; - command.ProtectMemoryData.NewProtect = protection; - - uint32_t oldProtect = 0; - command.ProtectMemoryData.OldProtectPtr = &oldProtect; - if (!SendBuffer(m_DeviceHandle, &command)) { + if (!IsValid() || address == 0 || size == 0) { return std::nullopt; } - return oldProtect; + return m_Device.Protect(address, size, protection); } bool KernelProcess::ReadBuffer(uintptr_t address, void* buffer, size_t size) const @@ -206,14 +96,7 @@ bool KernelProcess::ReadBuffer(uintptr_t address, void* buffer, size_t size) con if (!IsValid()) { return false; } - - CommandHeader command {}; - command.Type = CommandHeader::Type::ReadMemory; - command.Pid = m_Pid; - command.ReadMemoryData.Address = address; - command.ReadMemoryData.Size = size; - command.ReadMemoryData.Buffer = buffer; - return SendBuffer(m_DeviceHandle, &command); + return m_Device.Read(address, buffer, size); } bool KernelProcess::WriteBuffer(uintptr_t address, const void* buffer, size_t size) const @@ -221,14 +104,7 @@ bool KernelProcess::WriteBuffer(uintptr_t address, const void* buffer, size_t si if (!IsValid()) { return false; } - - CommandHeader command {}; - command.Type = CommandHeader::Type::WriteMemory; - command.Pid = m_Pid; - command.WriteMemoryData.Address = address; - command.WriteMemoryData.Size = size; - command.WriteMemoryData.Buffer = const_cast(buffer); // It's up to the kernel driver to ensure this is not written to. - return SendBuffer(m_DeviceHandle, &command); + return m_Device.Write(address, buffer, size); } MODULEENTRY32 KernelProcess::GetModuleEntry(std::string_view name) const @@ -238,12 +114,12 @@ MODULEENTRY32 KernelProcess::GetModuleEntry(std::string_view name) const return {}; } - MODULEENTRY32 entry; + const std::string moduleName(name); + MODULEENTRY32 entry {}; entry.dwSize = sizeof(entry); - if (Module32First(snapshot, &entry)) { do { - if (_stricmp(entry.szModule, name.data()) == 0) { + if (_stricmp(entry.szModule, moduleName.c_str()) == 0) { CloseHandle(snapshot); return entry; } @@ -256,20 +132,19 @@ MODULEENTRY32 KernelProcess::GetModuleEntry(std::string_view name) const std::vector KernelProcess::GetModuleEntries(bool refresh) const { + static_cast(refresh); HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE | TH32CS_SNAPMODULE32, m_Pid); if (snapshot == INVALID_HANDLE_VALUE) { return {}; } - MODULEENTRY32 entry; + MODULEENTRY32 entry {}; entry.dwSize = sizeof(entry); - std::vector modules; + std::unordered_set seenModules; if (Module32First(snapshot, &entry)) { - std::unordered_set seenModules; do { - std::string moduleName = entry.szModule; - if (seenModules.insert(moduleName).second) { + if (seenModules.insert(entry.szModule).second) { modules.push_back(entry); } } while (Module32Next(snapshot, &entry)); @@ -286,22 +161,21 @@ std::string KernelProcess::GetName() } HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); - if (snapshot == INVALID_HANDLE_VALUE) - return ""; - - PROCESSENTRY32 pe; - pe.dwSize = sizeof(PROCESSENTRY32); + if (snapshot == INVALID_HANDLE_VALUE) { + return {}; + } - if (Process32First(snapshot, &pe)) { + PROCESSENTRY32 entry {}; + entry.dwSize = sizeof(entry); + if (Process32First(snapshot, &entry)) { do { - if (pe.th32ProcessID == m_Pid) { - CloseHandle(snapshot); - m_Name = pe.szExeFile; - return pe.szExeFile; + if (entry.th32ProcessID == m_Pid) { + m_Name = entry.szExeFile; + break; } - } while (Process32Next(snapshot, &pe)); + } while (Process32Next(snapshot, &entry)); } CloseHandle(snapshot); - return {}; + return m_Name; } diff --git a/CheatStengine/src/CheatStengine/Process/KernelProcess.h b/CheatStengine/src/CheatStengine/Process/KernelProcess.h index 1d7ad28..2f8e88f 100644 --- a/CheatStengine/src/CheatStengine/Process/KernelProcess.h +++ b/CheatStengine/src/CheatStengine/Process/KernelProcess.h @@ -1,5 +1,6 @@ #pragma once +#include "Device.h" #include "Process.h" class KernelProcess final : public Process { @@ -9,7 +10,7 @@ class KernelProcess final : public Process { ~KernelProcess() override; [[nodiscard]] uintptr_t Allocate(size_t size, uint32_t protection, uint32_t allocationType = MEM_COMMIT | MEM_RESERVE) const override; - bool Free(uintptr_t address, uint32_t freeType = MEM_DECOMMIT) const override; + bool Free(uintptr_t address, uint32_t freeType = MEM_RELEASE) const override; [[nodiscard]] std::optional Query(uintptr_t address) const override; std::optional Protect(uintptr_t address, size_t size, uint32_t protection) const override; @@ -21,9 +22,10 @@ class KernelProcess final : public Process { [[nodiscard]] std::vector GetModuleEntries(bool refresh = false) const override; [[nodiscard]] std::string GetName() override; - [[nodiscard]] bool IsValid() const override { return Process::IsValid() && m_DeviceHandle != INVALID_HANDLE_VALUE; } + [[nodiscard]] bool IsValid() const override { return Process::IsValid() && m_Bound && m_Device.IsOpen(); } private: - HANDLE m_DeviceHandle = nullptr; + Device m_Device; + bool m_Bound = false; std::string m_Name; -}; \ No newline at end of file +}; diff --git a/CheatStengine/src/CheatStengine/Process/Monitor.cpp b/CheatStengine/src/CheatStengine/Process/Monitor.cpp new file mode 100644 index 0000000..6e30c3b --- /dev/null +++ b/CheatStengine/src/CheatStengine/Process/Monitor.cpp @@ -0,0 +1,125 @@ +#include "Monitor.h" + +#include "Device.h" + +#include +#include +#include + +using namespace std::chrono_literals; + +Monitor::~Monitor() +{ + Stop(); +} + +void Monitor::Start() +{ + std::scoped_lock lock(m_ThreadMutex); + if (m_Thread.joinable()) { + return; + } + + MonitorSnapshot snapshot; + snapshot.Running = true; + SetSnapshot(snapshot); + m_Running.store(true, std::memory_order_release); + m_Thread = std::jthread([this](std::stop_token stopToken) { + Run(stopToken); + }); +} + +void Monitor::Stop() +{ + std::scoped_lock lock(m_ThreadMutex); + if (m_Thread.joinable()) { + m_Thread.request_stop(); + m_Wake.notify_all(); + m_Thread.join(); + } + + m_Running.store(false, std::memory_order_release); + MonitorSnapshot snapshot = GetSnapshot(); + snapshot.Running = false; + snapshot.Connected = false; + snapshot.Mode = Protocol::LoadMode::Unknown; + SetSnapshot(snapshot); +} + +bool Monitor::IsRunning() const +{ + return m_Running.load(std::memory_order_acquire); +} + +MonitorSnapshot Monitor::GetSnapshot() const +{ + std::scoped_lock lock(m_SnapshotMutex); + return m_Snapshot; +} + +void Monitor::Run(std::stop_token stopToken) +{ + std::unique_ptr device; + while (!stopToken.stop_requested()) { + bool newConnection = false; + if (!device) { + device = std::make_unique(); + newConnection = device->IsOpen(); + } + + MonitorSnapshot snapshot = GetSnapshot(); + snapshot.Running = true; + snapshot.Connected = device->IsOpen(); + snapshot.Mode = device->GetMode(); + snapshot.Error = device->GetError(); + + if (snapshot.Connected) { + const uint64_t sequence = m_NextSequence.fetch_add(1, std::memory_order_relaxed); + if (sequence == 0 + || sequence > static_cast(std::numeric_limits::max())) { + snapshot.Connected = false; + snapshot.Error = ERROR_ARITHMETIC_OVERFLOW; + SetSnapshot(snapshot); + break; + } + + const std::optional response = device->Heartbeat(sequence); + if (response) { + if (newConnection) { + snapshot.Generation = m_NextGeneration.fetch_add(1, std::memory_order_relaxed); + } + snapshot.Connected = true; + snapshot.Sequence = response->Sequence; + snapshot.KernelTime = response->KernelTime; + snapshot.RemainingIdleSeconds = response->RemainingIdleSeconds; + snapshot.State = static_cast(response->State); + snapshot.Error = ERROR_SUCCESS; + } else { + snapshot.Connected = false; + snapshot.Error = device->GetError(); + device.reset(); + } + } else { + device.reset(); + } + + SetSnapshot(snapshot); + if (Wait(stopToken)) { + break; + } + } +} + +bool Monitor::Wait(std::stop_token stopToken) +{ + std::unique_lock lock(m_WaitMutex); + return m_Wake.wait_for(lock, 5s, [&stopToken] { + return stopToken.stop_requested(); + }); +} + +void Monitor::SetSnapshot(const MonitorSnapshot& snapshot) +{ + std::scoped_lock lock(m_SnapshotMutex); + m_Snapshot = snapshot; +} diff --git a/CheatStengine/src/CheatStengine/Process/Monitor.h b/CheatStengine/src/CheatStengine/Process/Monitor.h new file mode 100644 index 0000000..3cd533d --- /dev/null +++ b/CheatStengine/src/CheatStengine/Process/Monitor.h @@ -0,0 +1,58 @@ +#pragma once + +#include + +#include + +#include +#include +#include +#include +#include + +struct MonitorSnapshot { + bool Running = false; + bool Connected = false; + Protocol::LoadMode Mode = Protocol::LoadMode::Unknown; + Protocol::RuntimeState State = Protocol::RuntimeState::Starting; + uint64_t Sequence = 0; + uint64_t Generation = 0; + uint64_t KernelTime = 0; + uint32_t RemainingIdleSeconds = 0; + DWORD Error = ERROR_SUCCESS; +}; + +class Monitor final { +public: + Monitor() = default; + ~Monitor(); + + Monitor(const Monitor&) = delete; + Monitor& operator=(const Monitor&) = delete; + Monitor(Monitor&&) = delete; + Monitor& operator=(Monitor&&) = delete; + + void Start(); + void Stop(); + + [[nodiscard]] bool IsRunning() const; + [[nodiscard]] MonitorSnapshot GetSnapshot() const; + +private: + void Run(std::stop_token stopToken); + [[nodiscard]] bool Wait(std::stop_token stopToken); + void SetSnapshot(const MonitorSnapshot& snapshot); + +private: + mutable std::mutex m_ThreadMutex; + std::jthread m_Thread; + std::atomic m_Running = false; + std::atomic m_NextSequence = 1; + std::atomic m_NextGeneration = 1; + + mutable std::mutex m_SnapshotMutex; + MonitorSnapshot m_Snapshot; + + std::mutex m_WaitMutex; + std::condition_variable m_Wake; +}; diff --git a/CheatStengine/src/CheatStengine/Process/Process.h b/CheatStengine/src/CheatStengine/Process/Process.h index ff19579..d5f03b0 100644 --- a/CheatStengine/src/CheatStengine/Process/Process.h +++ b/CheatStengine/src/CheatStengine/Process/Process.h @@ -26,7 +26,7 @@ class Process { virtual ~Process() = default; [[nodiscard]] virtual uintptr_t Allocate(size_t size, uint32_t protection, uint32_t allocationType = MEM_COMMIT | MEM_RESERVE) const = 0; - virtual bool Free(uintptr_t address, uint32_t freeType = MEM_DECOMMIT) const = 0; + virtual bool Free(uintptr_t address, uint32_t freeType = MEM_RELEASE) const = 0; [[nodiscard]] virtual std::optional Query(uintptr_t address) const = 0; virtual std::optional Protect(uintptr_t address, size_t size, uint32_t protection) const = 0; @@ -36,11 +36,7 @@ class Process { [[nodiscard]] virtual MODULEENTRY32 GetModuleEntry(std::string_view name) const = 0; [[nodiscard]] virtual std::vector GetModuleEntries(bool refresh = false) const = 0; - [[nodiscard]] virtual std::string GetName() = 0; - - // Implemented methods that rely on the above virtual methods - [[nodiscard]] bool IsAddressReadable(uintptr_t address) const; template @@ -108,4 +104,4 @@ class Process { protected: DWORD m_Pid = 0; -}; \ No newline at end of file +}; diff --git a/CheatStengine/src/CheatStengine/Process/WinAPIProcess.h b/CheatStengine/src/CheatStengine/Process/WinAPIProcess.h index e35fe4b..6aa6c16 100644 --- a/CheatStengine/src/CheatStengine/Process/WinAPIProcess.h +++ b/CheatStengine/src/CheatStengine/Process/WinAPIProcess.h @@ -9,7 +9,7 @@ class WinAPIProcess final : public Process { ~WinAPIProcess() override; [[nodiscard]] uintptr_t Allocate(size_t size, uint32_t protection, uint32_t allocationType = MEM_COMMIT | MEM_RESERVE) const override; - bool Free(uintptr_t address, uint32_t freeType = MEM_DECOMMIT) const override; + bool Free(uintptr_t address, uint32_t freeType = MEM_RELEASE) const override; [[nodiscard]] std::optional Query(uintptr_t address) const override; std::optional Protect(uintptr_t address, size_t size, uint32_t protection) const override; @@ -25,4 +25,4 @@ class WinAPIProcess final : public Process { private: HANDLE m_Handle = nullptr; -}; \ No newline at end of file +}; diff --git a/CheatStengine/src/CheatStengine/Server/AuthManager.cpp b/CheatStengine/src/CheatStengine/Server/AuthManager.cpp new file mode 100644 index 0000000..1dd2e35 --- /dev/null +++ b/CheatStengine/src/CheatStengine/Server/AuthManager.cpp @@ -0,0 +1,84 @@ +#include "AuthManager.h" + +#include + +#include + +#include + +#include +#include +#include + +#pragma comment(lib, "bcrypt.lib") + +namespace Server { + + AuthManager::AuthManager() + : m_Token(GenerateToken()) + { + } + + bool AuthManager::IsAuthorized(const httplib::Request& request) const + { + if (!m_Required) { + return true; + } + + auto it = request.headers.find("Authorization"); + if (it == request.headers.end()) { + return false; + } + + std::string value = it->second; + + constexpr std::string_view scheme = "bearer "; + if (value.size() >= scheme.size()) { + std::string prefix = value.substr(0, scheme.size()); + std::ranges::transform(prefix, prefix.begin(), ::tolower); + if (prefix == scheme) { + value = value.substr(scheme.size()); + } + } + + return ConstantTimeEquals(value, m_Token); + } + + std::string AuthManager::GenerateToken(size_t byteCount) + { + std::vector bytes(byteCount); + + NTSTATUS status = BCryptGenRandom(nullptr, bytes.data(), static_cast(bytes.size()), + BCRYPT_USE_SYSTEM_PREFERRED_RNG); + if (status != 0) { + WARN("BCryptGenRandom failed (0x{:X}); falling back to a weaker token source", static_cast(status)); + uint64_t ticks = GetTickCount64(); + for (size_t i = 0; i < bytes.size(); ++i) { + bytes[i] = static_cast(ticks >> (i % 8 * 8)) ^ static_cast(i * 131 + 7); + } + } + + static constexpr std::array hex { '0', '1', '2', '3', '4', '5', '6', '7', + '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; + + std::string token; + token.reserve(bytes.size() * 2); + for (uint8_t byte : bytes) { + token.push_back(hex[byte >> 4]); + token.push_back(hex[byte & 0x0F]); + } + return token; + } + + bool AuthManager::ConstantTimeEquals(const std::string& a, const std::string& b) + { + size_t diff = a.size() ^ b.size(); + size_t length = b.size(); + for (size_t i = 0; i < length; ++i) { + uint8_t lhs = i < a.size() ? static_cast(a[i]) : 0; + diff |= static_cast(lhs ^ static_cast(b[i])); + } + return diff == 0; + } + +} diff --git a/CheatStengine/src/CheatStengine/Server/AuthManager.h b/CheatStengine/src/CheatStengine/Server/AuthManager.h new file mode 100644 index 0000000..67dc08e --- /dev/null +++ b/CheatStengine/src/CheatStengine/Server/AuthManager.h @@ -0,0 +1,26 @@ +#pragma once + +#include + +#include + +namespace Server { + + class AuthManager { + public: + AuthManager(); + [[nodiscard]] const std::string& GetToken() const { return m_Token; } + [[nodiscard]] bool IsAuthorized(const httplib::Request& request) const; + + void SetRequired(bool required) { m_Required = required; } + [[nodiscard]] bool IsRequired() const { return m_Required; } + + private: + [[nodiscard]] static std::string GenerateToken(size_t byteCount = 32); + [[nodiscard]] static bool ConstantTimeEquals(const std::string& a, const std::string& b); + + std::string m_Token; + bool m_Required = false; + }; + +} diff --git a/CheatStengine/src/CheatStengine/Server/CommandQueue.h b/CheatStengine/src/CheatStengine/Server/CommandQueue.h new file mode 100644 index 0000000..3e04983 --- /dev/null +++ b/CheatStengine/src/CheatStengine/Server/CommandQueue.h @@ -0,0 +1,90 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include + +class CommandQueue { +public: + using Task = std::function; + + [[nodiscard]] std::future Push(Task task) + { + auto packaged = std::make_shared>(std::move(task)); + std::future future = packaged->get_future(); + + { + std::unique_lock lock(m_Mutex); + if (m_Stopping) { + lock.unlock(); + (*packaged) = std::packaged_task([] { + return nlohmann::json { { "error", "server shutting down" }, { "code", 503 } }; + }); + (*packaged)(); + return future; + } + + m_Tasks.emplace([packaged] { (*packaged)(); }); + } + + return future; + } + + void Drain(size_t maxTasks = 256) + { + for (size_t processed = 0; processed < maxTasks; ++processed) { + std::function task; + { + std::unique_lock lock(m_Mutex); + if (m_Tasks.empty()) { + return; + } + task = std::move(m_Tasks.front()); + m_Tasks.pop(); + } + task(); + } + } + + void Shutdown() + { + { + std::unique_lock lock(m_Mutex); + m_Stopping = true; + } + + while (true) { + std::function task; + { + std::unique_lock lock(m_Mutex); + if (m_Tasks.empty()) { + return; + } + task = std::move(m_Tasks.front()); + m_Tasks.pop(); + } + task(); + } + } + + void Reset() + { + std::unique_lock lock(m_Mutex); + m_Stopping = false; + } + + [[nodiscard]] size_t GetPendingCount() const + { + std::unique_lock lock(m_Mutex); + return m_Tasks.size(); + } + +private: + std::queue> m_Tasks; + mutable std::mutex m_Mutex; + std::atomic m_Stopping = false; +}; diff --git a/CheatStengine/src/CheatStengine/Server/JobManager.cpp b/CheatStengine/src/CheatStengine/Server/JobManager.cpp new file mode 100644 index 0000000..caa7796 --- /dev/null +++ b/CheatStengine/src/CheatStengine/Server/JobManager.cpp @@ -0,0 +1,111 @@ +#include "JobManager.h" + +#include + +#include +#include +#include + +namespace Server { + + uint64_t JobManager::Submit(const std::string& label, std::function work) + { + uint64_t id; + { + std::lock_guard lock(m_Mutex); + id = m_NextId++; + m_Jobs.emplace(id, Job { id, label }); + } + + ++m_ActiveCount; + m_Pool.Enqueue([this, id, work = std::move(work)] { + nlohmann::json result; + std::string error; + bool failed = false; + + try { + result = work(); + } catch (const std::exception& e) { + failed = true; + error = e.what(); + ERR("Job {} failed: {}", id, error); + } + + { + std::lock_guard lock(m_Mutex); + auto it = m_Jobs.find(id); + if (it != m_Jobs.end()) { + if (failed) { + it->second.Status = Status::Failed; + it->second.Error = std::move(error); + } else { + it->second.Status = Status::Completed; + it->second.Result = std::move(result); + } + } + } + + --m_ActiveCount; + }); + + return id; + } + + nlohmann::json JobManager::Describe(uint64_t id) const + { + std::lock_guard lock(m_Mutex); + auto it = m_Jobs.find(id); + if (it == m_Jobs.end()) { + return nlohmann::json { { "error", "unknown job id" }, { "code", 404 } }; + } + + const Job& job = it->second; + nlohmann::json out { + { "id", job.Id }, + { "label", job.Label }, + { "status", StatusToString(job.Status) }, + }; + + if (job.Status == Status::Completed) { + out["result"] = job.Result; + } else if (job.Status == Status::Failed) { + out["error"] = job.Error; + } + + return out; + } + + nlohmann::json JobManager::DescribeAll() const + { + std::lock_guard lock(m_Mutex); + + std::vector ids; + ids.reserve(m_Jobs.size()); + for (const auto& [id, job] : m_Jobs) { + ids.push_back(id); + } + std::sort(ids.begin(), ids.end(), std::greater {}); + + nlohmann::json jobs = nlohmann::json::array(); + for (uint64_t id : ids) { + const Job& job = m_Jobs.at(id); + jobs.push_back({ + { "id", job.Id }, + { "label", job.Label }, + { "status", StatusToString(job.Status) }, + }); + } + return jobs; + } + + const char* JobManager::StatusToString(Status status) + { + switch (status) { + case Status::Running: return "running"; + case Status::Completed: return "completed"; + case Status::Failed: return "failed"; + default: return "unknown"; + } + } + +} diff --git a/CheatStengine/src/CheatStengine/Server/JobManager.h b/CheatStengine/src/CheatStengine/Server/JobManager.h new file mode 100644 index 0000000..1a1f2f2 --- /dev/null +++ b/CheatStengine/src/CheatStengine/Server/JobManager.h @@ -0,0 +1,54 @@ +#pragma once + +#include + +#include + +#include +#include +#include +#include +#include +#include + +namespace Server { + + class JobManager { + public: + enum class Status { + Running, + Completed, + Failed, + }; + + explicit JobManager(size_t numThreads = 2) + : m_Pool(numThreads) + { + } + + [[nodiscard]] uint64_t Submit(const std::string& label, std::function work); + [[nodiscard]] nlohmann::json Describe(uint64_t id) const; + [[nodiscard]] nlohmann::json DescribeAll() const; + [[nodiscard]] size_t GetActiveCount() const { return m_ActiveCount; } + [[nodiscard]] bool HasActiveJobs() const { return m_ActiveCount > 0; } + + private: + struct Job { + uint64_t Id; + std::string Label; + Status Status = Status::Running; + nlohmann::json Result; + std::string Error; + }; + + [[nodiscard]] static const char* StatusToString(Status status); + + ThreadPool m_Pool; + + mutable std::mutex m_Mutex; + std::unordered_map m_Jobs; + uint64_t m_NextId = 1; + std::atomic m_ActiveCount = 0; + }; + +} diff --git a/CheatStengine/src/CheatStengine/Server/McpInstaller.cpp b/CheatStengine/src/CheatStengine/Server/McpInstaller.cpp new file mode 100644 index 0000000..5ca357e --- /dev/null +++ b/CheatStengine/src/CheatStengine/Server/McpInstaller.cpp @@ -0,0 +1,378 @@ +#include "McpInstaller.h" + +#include + +#include + +#include + +#include + +#include +#include +#include + +#pragma comment(lib, "Shell32.lib") + +namespace Server { + + namespace { + + std::string ReadFile(const std::string& path) + { + std::ifstream stream(path, std::ios::binary); + if (!stream) { + return {}; + } + std::ostringstream buffer; + buffer << stream.rdbuf(); + return buffer.str(); + } + + bool WriteFile(const std::string& path, const std::string& content) + { + try { + std::filesystem::path fsPath(path); + if (fsPath.has_parent_path()) { + std::filesystem::create_directories(fsPath.parent_path()); + } + std::ofstream stream(path, std::ios::binary | std::ios::trunc); + if (!stream) { + WARN("MCP install: could not open '{}' for writing", path); + return false; + } + stream << content; + return true; + } catch (const std::exception& e) { + WARN("MCP install: write to '{}' failed: {}", path, e.what()); + return false; + } + } + + } + + void McpInstaller::InstallAll() const + { + WritePortableConfig(); + WriteClaudeCodeConfig(); + WriteCursorConfig(); + WriteClaudeDesktopConfig(); + WriteCodexConfig(); + WriteSkill(); + INFO("MCP client configs and skill written; agents can connect to {}/mcp", m_Url); + } + + nlohmann::json McpInstaller::HttpServerEntry() const + { + return { + { "type", "http" }, + { "url", m_Url + "/mcp" }, + { "headers", { { "Authorization", "Bearer " + m_Token } } }, + }; + } + + nlohmann::json McpInstaller::BridgeServerEntry() const + { + return { + { "command", "npx" }, + { "args", { "-y", "mcp-remote", m_Url + "/mcp", "--header", "Authorization: Bearer " + m_Token } }, + }; + } + + std::vector McpInstaller::ResolveTargets() const + { + std::vector targets; + + std::string exeDir = GetExecutableDir(); + targets.push_back({ "portable", "Portable (.mcp.json next to the exe)", + exeDir + "\\.mcp.json", !exeDir.empty(), false }); + + std::string cwd; + try { + cwd = std::filesystem::current_path().string(); + } catch (const std::exception&) { + cwd.clear(); + } + targets.push_back({ "claude-code", "Claude Code (project .mcp.json)", + cwd.empty() ? std::string() : cwd + "\\.mcp.json", !cwd.empty(), false }); + targets.push_back({ "cursor", "Cursor (.cursor/mcp.json)", + cwd.empty() ? std::string() : cwd + "\\.cursor\\mcp.json", !cwd.empty(), false }); + + std::string appData = GetAppDataDir(); + targets.push_back({ "claude-desktop", "Claude Desktop (via mcp-remote)", + appData.empty() ? std::string() : appData + "\\Claude\\claude_desktop_config.json", + !appData.empty(), false }); + + std::string home = GetHomeDir(); + targets.push_back({ "codex", "Codex CLI (~/.codex/config.toml)", + home.empty() ? std::string() : home + "\\.codex\\config.toml", !home.empty(), false }); + targets.push_back({ "skill", "Claude Code skill (~/.claude/skills)", + home.empty() ? std::string() : home + "\\.claude\\skills\\cheatstengine\\SKILL.md", + !home.empty(), false }); + + return targets; + } + + bool McpInstaller::IsInstalled(const std::string& key, const std::string& path) const + { + if (path.empty()) { + return false; + } + std::string existing = ReadFile(path); + if (existing.empty()) { + return false; + } + + if (key == "codex") { + return existing.find("[mcp_servers.cheatstengine]") != std::string::npos; + } + if (key == "skill") { + return true; + } + + try { + nlohmann::json root = nlohmann::json::parse(existing); + return root.contains("mcpServers") && root["mcpServers"].is_object() + && root["mcpServers"].contains("cheatstengine"); + } catch (const std::exception&) { + return false; + } + } + + std::vector McpInstaller::Describe() const + { + std::vector targets = ResolveTargets(); + for (Target& t : targets) { + t.Installed = t.Available && IsInstalled(t.Key, t.Path); + } + return targets; + } + + bool McpInstaller::InstallOne(const std::string& key) const + { + if (key == "portable") { + WritePortableConfig(); + return true; + } + if (key == "claude-code") { + WriteClaudeCodeConfig(); + return true; + } + if (key == "cursor") { + WriteCursorConfig(); + return true; + } + if (key == "claude-desktop") { + WriteClaudeDesktopConfig(); + return true; + } + if (key == "codex") { + WriteCodexConfig(); + return true; + } + if (key == "skill") { + WriteSkill(); + return true; + } + WARN("MCP install: unknown target '{}'", key); + return false; + } + + void McpInstaller::MergeJsonServer(const std::string& path, const std::string& topKey, + const std::string& serverKey, const std::string& serverJson) const + { + nlohmann::json root = nlohmann::json::object(); + + std::string existing = ReadFile(path); + if (!existing.empty()) { + try { + root = nlohmann::json::parse(existing); + } catch (const std::exception&) { + WARN("MCP install: '{}' wasn't valid JSON; rewriting it", path); + root = nlohmann::json::object(); + } + } + + if (!root.contains(topKey) || !root[topKey].is_object()) { + root[topKey] = nlohmann::json::object(); + } + + try { + root[topKey][serverKey] = nlohmann::json::parse(serverJson); + } catch (const std::exception& e) { + WARN("MCP install: internal server json invalid: {}", e.what()); + return; + } + + WriteFile(path, root.dump(2)); + } + + void McpInstaller::WritePortableConfig() const + { + nlohmann::json root { { "mcpServers", { { "cheatstengine", HttpServerEntry() } } } }; + + std::string dir = GetExecutableDir(); + std::string payload = root.dump(2); + WriteFile(dir + "\\.mcp.json", payload); + WriteFile(dir + "\\mcp.json", payload); + } + + void McpInstaller::WriteClaudeCodeConfig() const + { + std::string cwd; + try { + cwd = std::filesystem::current_path().string(); + } catch (const std::exception&) { + return; + } + if (cwd == GetExecutableDir()) { + return; + } + + MergeJsonServer(cwd + "\\.mcp.json", "mcpServers", "cheatstengine", HttpServerEntry().dump()); + } + + void McpInstaller::WriteCursorConfig() const + { + std::string cwd; + try { + cwd = std::filesystem::current_path().string(); + } catch (const std::exception&) { + return; + } + + MergeJsonServer(cwd + "\\.cursor\\mcp.json", "mcpServers", "cheatstengine", HttpServerEntry().dump()); + } + + void McpInstaller::WriteClaudeDesktopConfig() const + { + std::string appData = GetAppDataDir(); + if (appData.empty()) { + return; + } + + MergeJsonServer(appData + "\\Claude\\claude_desktop_config.json", "mcpServers", "cheatstengine", + BridgeServerEntry().dump()); + } + + void McpInstaller::WriteCodexConfig() const + { + std::string home = GetHomeDir(); + if (home.empty()) { + return; + } + + std::string path = home + "\\.codex\\config.toml"; + std::string existing = ReadFile(path); + if (existing.find("[mcp_servers.cheatstengine]") != std::string::npos) { + return; // already registered + } + + std::ostringstream block; + block << "\n[mcp_servers.cheatstengine]\n"; + block << "command = \"npx\"\n"; + block << "args = [\"-y\", \"mcp-remote\", \"" << m_Url << "/mcp\", \"--header\", \"Authorization: Bearer " + << m_Token << "\"]\n"; + + WriteFile(path, existing + block.str()); + } + + std::string McpInstaller::SkillContent() const + { + std::ostringstream skill; + skill << "---\n"; + skill << "name: cheatstengine\n"; + skill << "description: Drive Cheat Stengine (a Windows reverse-engineering tool) over MCP: " + "list/attach processes, read/write and scan memory, disassemble, assemble, dissect " + "structs, and generate byte signatures. Use when inspecting or modifying a running " + "process's memory on this machine.\n"; + skill << "---\n\n"; + skill << "# Cheat Stengine MCP\n\n"; + skill << "This tool exposes a live reverse-engineering engine over MCP at `" << m_Url << "/mcp`.\n"; + skill << "Auth is off by default, so you can connect with just the URL. If the operator turns " + "auth on, send `Authorization: Bearer " << m_Token << "`.\n\n"; + skill << "## Workflow\n\n"; + skill << "1. `list_processes` - find the target's pid.\n"; + skill << "2. `open_process` { pid } - attach. Everything below needs an attached process.\n"; + skill << "3. Inspect: `list_modules`, `read_memory`, `query_memory`, `disassemble`, `dissect_struct`.\n"; + skill << "4. Resolve addresses symbolically with `resolve_address` { expression: \"module.dll+0x1234\" }; " + "most tools also accept an expression string directly in place of a numeric address.\n"; + skill << "5. Modify: `write_memory` { address, hex }, or `assemble` { source, address, write: true }.\n\n"; + skill << "## Async tools (poll, don't block)\n\n"; + skill << "- `pattern_scan` { pattern } returns a `jobId`. Poll `job_status` { id } until status is " + "`completed`, then read `result.matches`.\n"; + skill << "- `scan_value` { valueType, scanType, value } starts a value scan. Poll `scan_results` " + "until `scanning` is false. Refine with `scan_value` { ..., next: true }.\n\n"; + skill << "## Notes\n\n"; + skill << "- Addresses in results are hex strings like `0x7FF6...`.\n"; + skill << "- Value types: int8/16/32/64, uint8/16/32/64, float, double.\n"; + skill << "- Scan types: exact, bigger, smaller, between, unknown.\n"; + skill << "- `pattern` is space-separated hex with `??` wildcards, e.g. `48 8B ?? C3`.\n"; + + return skill.str(); + } + + void McpInstaller::WriteSkill() const + { + std::string content = SkillContent(); + + std::string home = GetHomeDir(); + if (!home.empty()) { + WriteFile(home + "\\.claude\\skills\\cheatstengine\\SKILL.md", content); + } + WriteFile(GetExecutableDir() + "\\SKILL.md", content); + } + + std::string McpInstaller::GetExecutableDir() + { + wchar_t buffer[MAX_PATH]; + DWORD len = GetModuleFileNameW(nullptr, buffer, MAX_PATH); + if (len == 0) { + return "."; + } + try { + return std::filesystem::path(std::wstring(buffer, len)).parent_path().string(); + } catch (const std::exception&) { + return "."; + } + } + + std::string McpInstaller::GetHomeDir() + { + if (const char* profile = std::getenv("USERPROFILE")) { + return profile; + } + + PWSTR path = nullptr; + if (SUCCEEDED(SHGetKnownFolderPath(FOLDERID_Profile, 0, nullptr, &path))) { + std::string result; + try { + result = std::filesystem::path(path).string(); + } catch (const std::exception&) { + } + CoTaskMemFree(path); + return result; + } + return {}; + } + + std::string McpInstaller::GetAppDataDir() + { + if (const char* appData = std::getenv("APPDATA")) { + return appData; + } + + PWSTR path = nullptr; + if (SUCCEEDED(SHGetKnownFolderPath(FOLDERID_RoamingAppData, 0, nullptr, &path))) { + std::string result; + try { + result = std::filesystem::path(path).string(); + } catch (const std::exception&) { + } + CoTaskMemFree(path); + return result; + } + return {}; + } + +} diff --git a/CheatStengine/src/CheatStengine/Server/McpInstaller.h b/CheatStengine/src/CheatStengine/Server/McpInstaller.h new file mode 100644 index 0000000..e38e0b6 --- /dev/null +++ b/CheatStengine/src/CheatStengine/Server/McpInstaller.h @@ -0,0 +1,57 @@ +#pragma once + +#include + +#include +#include + +namespace Server { + + class McpInstaller { + public: + McpInstaller(std::string url, std::string token) + : m_Url(std::move(url)) + , m_Token(std::move(token)) + { + } + + struct Target { + std::string Key; + std::string Label; + std::string Path; + bool Available = true; + bool Installed = false; + }; + + void InstallAll() const; + bool InstallOne(const std::string& key) const; + [[nodiscard]] std::vector Describe() const; + + private: + [[nodiscard]] std::vector ResolveTargets() const; + + [[nodiscard]] bool IsInstalled(const std::string& key, const std::string& path) const; + + void WritePortableConfig() const; + void WriteClaudeCodeConfig() const; + void WriteCursorConfig() const; + void WriteClaudeDesktopConfig() const; + void WriteCodexConfig() const; + void WriteSkill() const; + + [[nodiscard]] nlohmann::json HttpServerEntry() const; + [[nodiscard]] nlohmann::json BridgeServerEntry() const; + [[nodiscard]] std::string SkillContent() const; + + void MergeJsonServer(const std::string& path, const std::string& topKey, + const std::string& serverKey, const std::string& serverJson) const; + + [[nodiscard]] static std::string GetExecutableDir(); + [[nodiscard]] static std::string GetHomeDir(); + [[nodiscard]] static std::string GetAppDataDir(); + + std::string m_Url; + std::string m_Token; + }; + +} diff --git a/CheatStengine/src/CheatStengine/Server/McpServer.cpp b/CheatStengine/src/CheatStengine/Server/McpServer.cpp new file mode 100644 index 0000000..abf1b41 --- /dev/null +++ b/CheatStengine/src/CheatStengine/Server/McpServer.cpp @@ -0,0 +1,411 @@ +#include "McpServer.h" + +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace Server { + + static constexpr const char* kProtocolVersion = "2025-06-18"; + + McpServer::McpServer() = default; + + McpServer::~McpServer() + { + Stop(); + } + + bool McpServer::Start(State& state, const std::string& host, uint16_t port, uint16_t portAttempts) + { + if (m_Running) { + return true; + } + + m_State = &state; + m_Host = host; + + m_CommandQueue.Reset(); + + if (!m_RoutesRegistered) { + RegisterRoutes(); + m_RoutesRegistered = true; + } + + uint16_t bound = 0; + for (uint16_t offset = 0; offset < portAttempts; ++offset) { + int candidate = static_cast(port) + offset; + if (candidate > 65535) { + break; // don't wrap past the valid port range + } + if (m_Http.bind_to_port(m_Host.c_str(), static_cast(candidate))) { + bound = static_cast(candidate); + break; + } + } + + if (bound == 0) { + ERR("MCP server could not bind any port in [{}, {}]", port, port + portAttempts - 1); + return false; + } + + m_Port = bound; + m_Url = std::format("http://{}:{}", m_Host, m_Port); + + m_Running = true; + m_ListenThread = std::thread([this] { + m_Http.listen_after_bind(); + }); + + INFO("MCP control server listening on {}", m_Url); + INFO("MCP endpoint: {}/mcp (auth {})", m_Url, m_Auth.IsRequired() ? "required" : "off"); + INFO("MCP bearer token: {}", m_Auth.GetToken()); + + McpInstaller(m_Url, m_Auth.GetToken()).InstallAll(); + return true; + } + + void McpServer::ReinstallClients() const + { + McpInstaller(m_Url, m_Auth.GetToken()).InstallAll(); + } + + void McpServer::Stop() + { + if (!m_Running) { + return; + } + + m_Running = false; + m_Http.stop(); + + m_CommandQueue.Shutdown(); + + if (m_ListenThread.joinable()) { + m_ListenThread.join(); + } + + if (m_Scanner) { + for (int waited = 0; m_Scanner->IsScanning() && waited < 5000; waited += 10) { + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + } + + INFO("MCP control server stopped"); + } + + void McpServer::RegisterRoutes() + { + m_Http.Get("/", [](const httplib::Request&, httplib::Response& res) { + res.set_content(kDashboardHtml, "text/html"); + }); + + m_Http.Get("/config.html", [](const httplib::Request&, httplib::Response& res) { + res.set_content(kConfigHtml, "text/html"); + }); + + // Serve the branded icon straight off disk (Resources sits next to the + // executable). Browsers request /favicon.ico automatically, and the + // dashboard pages point their here too. + m_Http.Get("/favicon.ico", [](const httplib::Request&, httplib::Response& res) { + std::ifstream file("Resources/favicon.ico", std::ios::binary); + if (!file) { + res.status = 404; + return; + } + std::string bytes((std::istreambuf_iterator(file)), std::istreambuf_iterator()); + res.set_content(bytes, "image/x-icon"); + }); + + auto authorized = [this](const httplib::Request& req, httplib::Response& res) -> bool { + if (!m_Auth.IsAuthorized(req)) { + res.status = 401; + res.set_content(R"({"error":"unauthorized"})", "application/json"); + return false; + } + return true; + }; + + m_Http.Get("/status", [this, authorized](const httplib::Request& req, httplib::Response& res) { + if (!authorized(req, res)) { + return; + } + + nlohmann::json process = m_CommandQueue.Push([this]() -> nlohmann::json { + bool attached = m_State && m_State->Process && m_State->Process->IsValid(); + nlohmann::json out { { "attached", attached } }; + if (attached) { + out["processName"] = m_State->Process->GetName(); + out["pid"] = m_State->Process->GetPid(); + } + return out; + }) + .get(); + + process["sessions"] = m_Sessions.DescribeAll(); + process["jobs"] = m_Jobs.DescribeAll(); + res.set_content(process.dump(), "application/json"); + }); + + m_Http.Get("/config", [this](const httplib::Request&, httplib::Response& res) { + McpInstaller installer(m_Url, m_Auth.GetToken()); + nlohmann::json targets = nlohmann::json::array(); + for (const auto& t : installer.Describe()) { + targets.push_back({ + { "key", t.Key }, + { "label", t.Label }, + { "path", t.Path }, + { "available", t.Available }, + { "installed", t.Installed }, + }); + } + + nlohmann::json out { + { "url", m_Url }, + { "endpoint", m_Url + "/mcp" }, + { "token", m_Auth.GetToken() }, + { "authRequired", m_Auth.IsRequired() }, + { "targets", targets }, + }; + res.set_content(out.dump(), "application/json"); + }); + + m_Http.Post("/install", [this](const httplib::Request& req, httplib::Response& res) { + std::string target; + try { + nlohmann::json body = nlohmann::json::parse(req.body); + target = body.value("target", ""); + } catch (const std::exception&) { + res.status = 400; + res.set_content(R"({"ok":false,"error":"invalid json"})", "application/json"); + return; + } + + McpInstaller installer(m_Url, m_Auth.GetToken()); + bool ok = installer.InstallOne(target); + res.status = ok ? 200 : 400; + res.set_content(nlohmann::json { { "ok", ok }, { "target", target } }.dump(), "application/json"); + }); + + m_Http.Post("/auth", [this](const httplib::Request& req, httplib::Response& res) { + try { + nlohmann::json body = nlohmann::json::parse(req.body); + m_Auth.SetRequired(body.value("required", m_Auth.IsRequired())); + } catch (const std::exception&) { + res.status = 400; + res.set_content(R"({"ok":false,"error":"invalid json"})", "application/json"); + return; + } + res.set_content(nlohmann::json { { "ok", true }, { "authRequired", m_Auth.IsRequired() } }.dump(), + "application/json"); + }); + + // The tool catalogue with each tool's enabled flag, for the dashboard's + // control panel. Authenticated like /status. + m_Http.Get("/tools", [this, authorized](const httplib::Request& req, httplib::Response& res) { + if (!authorized(req, res)) { + return; + } + res.set_content(nlohmann::json { { "tools", m_Tools.DescribeAll() } }.dump(), "application/json"); + }); + + // Enable or disable a single tool. Body: {"name":"read_memory","enabled":false}. + m_Http.Post("/tools/toggle", [this, authorized](const httplib::Request& req, httplib::Response& res) { + if (!authorized(req, res)) { + return; + } + std::string name; + bool enabled = true; + try { + nlohmann::json body = nlohmann::json::parse(req.body); + name = body.value("name", ""); + enabled = body.value("enabled", true); + } catch (const std::exception&) { + res.status = 400; + res.set_content(R"({"ok":false,"error":"invalid json"})", "application/json"); + return; + } + bool ok = m_Tools.SetEnabled(name, enabled); + res.status = ok ? 200 : 404; + res.set_content(nlohmann::json { { "ok", ok }, { "name", name }, { "enabled", enabled } }.dump(), + "application/json"); + }); + + m_Http.Post("/mcp", [this, authorized](const httplib::Request& req, httplib::Response& res) { + if (!authorized(req, res)) { + return; + } + + // Keep the client's session alive on activity. MCP echoes the id from + // initialize back in this header; touching it stops an actively-used + // session from being reaped at its TTL and keeps /status honest. + if (auto it = req.headers.find("Mcp-Session-Id"); it != req.headers.end()) { + (void)m_Sessions.Touch(it->second); + } + + nlohmann::json request; + try { + request = nlohmann::json::parse(req.body); + } catch (const std::exception&) { + res.set_content(MakeError(nullptr, -32700, "parse error").dump(), "application/json"); + return; + } + + nlohmann::json response = HandleRpc(request); + if (response.is_null()) { + res.status = 202; + return; + } + res.set_content(response.dump(), "application/json"); + }); + + m_Http.Get("/sse", [this, authorized](const httplib::Request& req, httplib::Response& res) { + if (!authorized(req, res)) { + return; + } + + res.set_header("Content-Type", "text/event-stream"); + res.set_header("Cache-Control", "no-cache"); + res.set_chunked_content_provider("text/event-stream", + [this](size_t, httplib::DataSink& sink) -> bool { + std::string event = "event: endpoint\ndata: /mcp\n\n"; + if (!sink.write(event.data(), event.size())) { + return false; + } + std::this_thread::sleep_for(std::chrono::seconds(15)); + return m_Running.load(); + }); + }); + } + + nlohmann::json McpServer::HandleRpc(const nlohmann::json& request) + { + if (!request.is_object() || !request.contains("method")) { + return MakeError(request.value("id", nlohmann::json(nullptr)), -32600, "invalid request"); + } + + std::string method = request.value("method", ""); + nlohmann::json id = request.value("id", nlohmann::json(nullptr)); + nlohmann::json params = request.value("params", nlohmann::json::object()); + + bool isNotification = !request.contains("id"); + + if (method == "initialize") { + return { { "jsonrpc", "2.0" }, { "id", id }, { "result", HandleInitialize(params) } }; + } + if (method == "notifications/initialized" || method == "notifications/cancelled") { + return nullptr; + } + if (method == "ping") { + return { { "jsonrpc", "2.0" }, { "id", id }, { "result", nlohmann::json::object() } }; + } + if (method == "tools/list") { + return { { "jsonrpc", "2.0" }, { "id", id }, { "result", { { "tools", m_Tools.ListTools() } } } }; + } + if (method == "tools/call") { + return { { "jsonrpc", "2.0" }, { "id", id }, { "result", HandleToolsCall(params) } }; + } + + if (isNotification) { + return nullptr; + } + return MakeError(id, -32601, "method not found: " + method); + } + + nlohmann::json McpServer::HandleInitialize(const nlohmann::json& params) + { + std::string clientName = "unknown"; + std::string clientVersion = "?"; + if (params.contains("clientInfo")) { + clientName = params["clientInfo"].value("name", clientName); + clientVersion = params["clientInfo"].value("version", clientVersion); + } + + std::string sessionId = m_Sessions.Create(clientName, clientVersion); + + // MCP spec: echo the client's requested protocol if we support it, + // otherwise return our latest and let the client decide. + static constexpr const char* kSupported[] = { + "2025-06-18", "2025-03-26", "2024-11-05" + }; + std::string negotiated = kProtocolVersion; // our latest as default + if (params.contains("protocolVersion")) { + std::string requested = params["protocolVersion"].get(); + for (const char* v : kSupported) { + if (requested == v) { negotiated = requested; break; } + } + } + + INFO("MCP client initialized: {} {} (session {}), protocol {}", + clientName, clientVersion, sessionId.substr(0, 8), negotiated); + + return { + { "protocolVersion", negotiated }, + { "capabilities", { { "tools", nlohmann::json::object() } } }, + { "serverInfo", { { "name", "CheatStengine" }, { "version", "0.1.0" } } }, + { "sessionId", sessionId }, + }; + } + + nlohmann::json McpServer::HandleToolsCall(const nlohmann::json& params) + { + std::string name = params.value("name", ""); + nlohmann::json arguments = params.value("arguments", nlohmann::json::object()); + if (name.empty()) { + return { + { "isError", true }, + { "content", { { { "type", "text" }, { "text", "tools/call requires a tool name" } } } }, + }; + } + return DispatchToolCall(name, arguments); + } + + nlohmann::json McpServer::DispatchToolCall(const std::string& name, const nlohmann::json& arguments) + { + std::future future = m_CommandQueue.Push([this, name, arguments]() -> nlohmann::json { + try { + ToolContext context { *m_State, m_Jobs, m_Scanner }; + nlohmann::json result = m_Tools.CallTool(context, name, arguments); + return { { "ok", true }, { "data", result } }; + } catch (const std::exception& e) { + return { { "ok", false }, { "message", e.what() } }; + } + }); + + nlohmann::json outcome = future.get(); + + if (!outcome.value("ok", false)) { + // Normal handler failures carry "message"; a queue that's shutting + // down resolves the future with {"error","code":503} instead, so fall + // back to "error" before the generic text. + std::string message = outcome.contains("message") + ? outcome.value("message", "tool failed") + : outcome.value("error", "tool failed"); + return { + { "isError", true }, + { "content", { { { "type", "text" }, { "text", message } } } }, + }; + } + + nlohmann::json data = outcome["data"]; + return { + { "content", { { { "type", "text" }, { "text", data.dump(2) } } } }, + { "structuredContent", data }, + }; + } + + nlohmann::json McpServer::MakeError(const nlohmann::json& id, int code, const std::string& message) + { + return { + { "jsonrpc", "2.0" }, + { "id", id }, + { "error", { { "code", code }, { "message", message } } }, + }; + } + +} diff --git a/CheatStengine/src/CheatStengine/Server/McpServer.h b/CheatStengine/src/CheatStengine/Server/McpServer.h new file mode 100644 index 0000000..8b2ee9f --- /dev/null +++ b/CheatStengine/src/CheatStengine/Server/McpServer.h @@ -0,0 +1,74 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include + +namespace Server { + + class McpServer { + public: + McpServer(); + ~McpServer(); + + McpServer(const McpServer&) = delete; + McpServer& operator=(const McpServer&) = delete; + + bool Start(State& state, const std::string& host = "127.0.0.1", uint16_t port = 13777, uint16_t portAttempts = 32); + + void Stop(); + + [[nodiscard]] bool IsRunning() const { return m_Running; } + + void DrainCommands() { m_CommandQueue.Drain(); } + + [[nodiscard]] const std::string& GetToken() const { return m_Auth.GetToken(); } + [[nodiscard]] uint16_t GetPort() const { return m_Port; } + [[nodiscard]] const std::string& GetUrl() const { return m_Url; } + void SetAuthRequired(bool required) { m_Auth.SetRequired(required); } + [[nodiscard]] bool IsAuthRequired() const { return m_Auth.IsRequired(); } + + [[nodiscard]] nlohmann::json DescribeTools() const { return m_Tools.DescribeAll(); } + bool SetToolEnabled(const std::string& name, bool enabled) { return m_Tools.SetEnabled(name, enabled); } + + void ReinstallClients() const; + + private: + void RegisterRoutes(); + [[nodiscard]] nlohmann::json HandleRpc(const nlohmann::json& request); + [[nodiscard]] nlohmann::json HandleInitialize(const nlohmann::json& params); + [[nodiscard]] nlohmann::json HandleToolsCall(const nlohmann::json& params); + [[nodiscard]] nlohmann::json DispatchToolCall(const std::string& name, const nlohmann::json& arguments); + [[nodiscard]] static nlohmann::json MakeError(const nlohmann::json& id, int code, const std::string& message); + + State* m_State = nullptr; + + AuthManager m_Auth; + SessionManager m_Sessions; + ToolRegistry m_Tools; + CommandQueue m_CommandQueue; + JobManager m_Jobs; + + std::unique_ptr m_Scanner; + + httplib::Server m_Http; + std::thread m_ListenThread; + std::string m_Host; + std::string m_Url; + uint16_t m_Port = 0; + std::atomic m_Running = false; + bool m_RoutesRegistered = false; + }; + +} diff --git a/CheatStengine/src/CheatStengine/Server/SessionManager.cpp b/CheatStengine/src/CheatStengine/Server/SessionManager.cpp new file mode 100644 index 0000000..5f3a47c --- /dev/null +++ b/CheatStengine/src/CheatStengine/Server/SessionManager.cpp @@ -0,0 +1,117 @@ +#include "SessionManager.h" + +#include + +#include + +#include + +#pragma comment(lib, "bcrypt.lib") + +namespace Server { + + std::string SessionManager::Create(const std::string& clientName, const std::string& clientVersion) + { + std::lock_guard lock(m_Mutex); + ReapExpired(); + + auto now = std::chrono::steady_clock::now(); + std::string id = GenerateId(); + + m_Sessions.emplace(id, Session { + .Id = id, + .ClientName = clientName, + .ClientVersion = clientVersion, + .CreatedAt = now, + .LastSeenAt = now, + }); + + return id; + } + + bool SessionManager::Touch(const std::string& id) + { + std::lock_guard lock(m_Mutex); + ReapExpired(); + + auto it = m_Sessions.find(id); + if (it == m_Sessions.end()) { + return false; + } + + it->second.LastSeenAt = std::chrono::steady_clock::now(); + return true; + } + + void SessionManager::Remove(const std::string& id) + { + std::lock_guard lock(m_Mutex); + m_Sessions.erase(id); + } + + nlohmann::json SessionManager::DescribeAll() + { + std::lock_guard lock(m_Mutex); + ReapExpired(); + + auto now = std::chrono::steady_clock::now(); + nlohmann::json sessions = nlohmann::json::array(); + for (const auto& [id, session] : m_Sessions) { + auto ageSeconds = std::chrono::duration_cast(now - session.CreatedAt).count(); + + sessions.push_back({ + { "id", id.substr(0, 8) }, + { "client", session.ClientName }, + { "version", session.ClientVersion }, + { "ageSeconds", ageSeconds }, + }); + } + return sessions; + } + + size_t SessionManager::GetActiveCount() + { + std::lock_guard lock(m_Mutex); + ReapExpired(); + return m_Sessions.size(); + } + + void SessionManager::ReapExpired() + { + auto now = std::chrono::steady_clock::now(); + for (auto it = m_Sessions.begin(); it != m_Sessions.end();) { + if (now - it->second.LastSeenAt > m_Ttl) { + it = m_Sessions.erase(it); + } else { + ++it; + } + } + } + + std::string SessionManager::GenerateId() + { + std::array bytes {}; + + NTSTATUS status = BCryptGenRandom(nullptr, bytes.data(), static_cast(bytes.size()), + BCRYPT_USE_SYSTEM_PREFERRED_RNG); + if (status != 0) { + static uint64_t counter = 0; + uint64_t seed = GetTickCount64() ^ (++counter << 24); + for (size_t i = 0; i < bytes.size(); ++i) { + bytes[i] = static_cast(seed >> ((i % 8) * 8)); + } + } + + static constexpr std::array hex { '0', '1', '2', '3', '4', '5', '6', '7', + '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; + + std::string id; + id.reserve(bytes.size() * 2); + for (uint8_t byte : bytes) { + id.push_back(hex[byte >> 4]); + id.push_back(hex[byte & 0x0F]); + } + return id; + } + +} diff --git a/CheatStengine/src/CheatStengine/Server/SessionManager.h b/CheatStengine/src/CheatStengine/Server/SessionManager.h new file mode 100644 index 0000000..77987b5 --- /dev/null +++ b/CheatStengine/src/CheatStengine/Server/SessionManager.h @@ -0,0 +1,45 @@ +#pragma once + +#include + +#include +#include +#include +#include + +namespace Server { + + class SessionManager { + public: + struct Session { + std::string Id; + std::string ClientName; + std::string ClientVersion; + std::chrono::steady_clock::time_point CreatedAt; + std::chrono::steady_clock::time_point LastSeenAt; + }; + + explicit SessionManager(std::chrono::seconds ttl = std::chrono::minutes(30)) + : m_Ttl(ttl) + { + } + + [[nodiscard]] std::string Create(const std::string& clientName, const std::string& clientVersion); + [[nodiscard]] bool Touch(const std::string& id); + void Remove(const std::string& id); + [[nodiscard]] nlohmann::json DescribeAll(); + [[nodiscard]] size_t GetActiveCount(); + + private: + void ReapExpired(); + + [[nodiscard]] static std::string GenerateId(); + + std::chrono::seconds m_Ttl; + + std::mutex m_Mutex; + std::unordered_map m_Sessions; + uint64_t m_NextId = 1; + }; + +} diff --git a/CheatStengine/src/CheatStengine/Server/ToolRegistry.cpp b/CheatStengine/src/CheatStengine/Server/ToolRegistry.cpp new file mode 100644 index 0000000..ffafd20 --- /dev/null +++ b/CheatStengine/src/CheatStengine/Server/ToolRegistry.cpp @@ -0,0 +1,743 @@ +#include "ToolRegistry.h" + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace Server { + + namespace { + + [[noreturn]] void Fail(const std::string& message) + { + throw std::runtime_error(message); + } + + Process& RequireProcess(ToolContext& context) + { + if (!context.AppState.Process || !context.AppState.Process->IsValid()) { + Fail("no process is attached; call open_process first"); + } + return *context.AppState.Process; + } + + uintptr_t ResolveAddress(ToolContext& context, const nlohmann::json& value) + { + if (value.is_number_unsigned()) { + return value.get(); + } + + if (value.is_string()) { + Process& process = RequireProcess(context); + AddressEvaluator::Result result = AddressEvaluator::Evaluate(value.get(), process); + if (result.IsError()) { + Fail(std::format("could not resolve address expression: {}", value.get())); + } + return result.Value; + } + + Fail("address must be a number or an expression string"); + } + + const nlohmann::json& Require(const nlohmann::json& args, const std::string& key) + { + auto it = args.find(key); + if (it == args.end()) { + Fail(std::format("missing required argument '{}'", key)); + } + return *it; + } + + ValueType ParseValueType(const std::string& name) + { + static const std::unordered_map map { + { "int8", ValueType::Int8 }, { "int16", ValueType::Int16 }, + { "int32", ValueType::Int32 }, { "int64", ValueType::Int64 }, + { "uint8", ValueType::UInt8 }, { "uint16", ValueType::UInt16 }, + { "uint32", ValueType::UInt32 }, { "uint64", ValueType::UInt64 }, + { "float", ValueType::Float }, { "double", ValueType::Double } + }; + auto it = map.find(name); + if (it == map.end()) { + Fail(std::format("unknown value type '{}'", name)); + } + return it->second; + } + + ScanType ParseScanType(const std::string& name) + { + static const std::unordered_map map { + { "exact", ScanType::ExactValue }, + { "bigger", ScanType::BiggerThan }, + { "smaller", ScanType::SmallerThan }, + { "between", ScanType::ValueBetween }, + { "unknown", ScanType::UnknownInitialValue } + }; + auto it = map.find(name); + if (it == map.end()) { + Fail(std::format("unknown scan type '{}'", name)); + } + return it->second; + } + + } + + ToolRegistry::ToolRegistry() + { + RegisterCoreTools(); + RegisterMemoryTools(); + RegisterAnalysisTools(); + RegisterScanTools(); + } + + void ToolRegistry::Register(Tool tool) + { + m_Tools.push_back(std::move(tool)); + } + + nlohmann::json ToolRegistry::ListTools() const + { + std::lock_guard lock(m_Mutex); + nlohmann::json tools = nlohmann::json::array(); + for (const Tool& tool : m_Tools) { + if (auto it = m_Disabled.find(tool.Name); it != m_Disabled.end() && it->second) { + continue; // user switched this one off + } + tools.push_back({ + { "name", tool.Name }, + { "description", tool.Description }, + { "inputSchema", tool.InputSchema }, + }); + } + return tools; + } + + nlohmann::json ToolRegistry::DescribeAll() const + { + std::lock_guard lock(m_Mutex); + nlohmann::json tools = nlohmann::json::array(); + for (const Tool& tool : m_Tools) { + bool disabled = false; + if (auto it = m_Disabled.find(tool.Name); it != m_Disabled.end()) { + disabled = it->second; + } + tools.push_back({ + { "name", tool.Name }, + { "description", tool.Description }, + { "enabled", !disabled }, + }); + } + return tools; + } + + bool ToolRegistry::SetEnabled(const std::string& name, bool enabled) + { + std::lock_guard lock(m_Mutex); + auto known = std::ranges::find_if(m_Tools, [&name](const Tool& t) { return t.Name == name; }); + if (known == m_Tools.end()) { + return false; + } + m_Disabled[name] = !enabled; + return true; + } + + bool ToolRegistry::IsEnabled(const std::string& name) const + { + std::lock_guard lock(m_Mutex); + auto it = m_Disabled.find(name); + return it == m_Disabled.end() || !it->second; + } + + nlohmann::json ToolRegistry::CallTool(ToolContext& context, const std::string& name, const nlohmann::json& arguments) const + { + for (const Tool& tool : m_Tools) { + if (tool.Name != name) { + continue; + } + + { + std::lock_guard lock(m_Mutex); + if (auto it = m_Disabled.find(name); it != m_Disabled.end() && it->second) { + Fail(std::format("tool '{}' is disabled by the operator", name)); + } + } + + const nlohmann::json args = arguments.is_object() ? arguments : nlohmann::json::object(); + return tool.Invoke(context, args); + } + + Fail(std::format("unknown tool '{}'", name)); + } + + void ToolRegistry::RegisterCoreTools() + { + Register(Tool { + .Name = "list_processes", + .Description = "List running processes as { pid, name }. Use this to find a target to attach to.", + .InputSchema = { { "type", "object" }, { "properties", nlohmann::json::object() } }, + .Invoke = [](ToolContext&, const nlohmann::json&) { + nlohmann::json list = nlohmann::json::array(); + for (const PROCESSENTRY32& entry : Process::EnumerateProcesses()) { + list.push_back({ { "pid", entry.th32ProcessID }, { "name", entry.szExeFile } }); + } + return nlohmann::json { { "processes", list }, { "count", list.size() } }; + }, + }); + + Register(Tool { + .Name = "open_process", + .Description = "Attach to a process by pid. Replaces any currently attached process.", + .InputSchema = { + { "type", "object" }, + { "properties", { { "pid", { { "type", "integer" }, { "description", "Target process id" } } } } }, + { "required", { "pid" } } }, + .Invoke = [](ToolContext& context, const nlohmann::json& args) { + uint32_t pid = Require(args, "pid").get(); + + if (context.Jobs.HasActiveJobs()) { + Fail("a scan job is still running; wait for it to finish (poll job_status) before attaching"); + } + if (context.Scanner && context.Scanner->IsScanning()) { + Fail("a value scan is still running; wait for scan_results to report scanning=false before attaching"); + } + + context.Scanner.reset(); + + context.AppState.Process = Process::Create(pid, ProcessMode::WinAPI); + if (!context.AppState.Process || !context.AppState.Process->IsValid()) { + Fail(std::format("failed to attach to pid {}", pid)); + } + + context.AppState.Modules = context.AppState.Process->GetModuleEntries(true); + return nlohmann::json { + { "attached", true }, + { "pid", pid }, + { "name", context.AppState.Process->GetName() }, + { "moduleCount", context.AppState.Modules.size() }, + }; + }, + }); + + Register(Tool { + .Name = "get_status", + .Description = "Report whether a process is attached and its pid/name/module count.", + .InputSchema = { { "type", "object" }, { "properties", nlohmann::json::object() } }, + .Invoke = [](ToolContext& context, const nlohmann::json&) { + bool attached = context.AppState.Process && context.AppState.Process->IsValid(); + nlohmann::json out { { "attached", attached } }; + if (attached) { + out["pid"] = context.AppState.Process->GetPid(); + out["name"] = context.AppState.Process->GetName(); + out["moduleCount"] = context.AppState.Modules.size(); + } + return out; + }, + }); + + Register(Tool { + .Name = "list_modules", + .Description = "List loaded modules of the attached process as { name, base, size }.", + .InputSchema = { { "type", "object" }, { "properties", nlohmann::json::object() } }, + .Invoke = [](ToolContext& context, const nlohmann::json&) { + Process& process = RequireProcess(context); + std::vector modules = process.GetModuleEntries(true); + context.AppState.Modules = modules; + + nlohmann::json list = nlohmann::json::array(); + for (const MODULEENTRY32& mod : modules) { + list.push_back({ + { "name", mod.szModule }, + { "base", std::format("0x{:X}", reinterpret_cast(mod.modBaseAddr)) }, + { "size", mod.modBaseSize }, + }); + } + return nlohmann::json { { "modules", list }, { "count", list.size() } }; + }, + }); + + Register(Tool { + .Name = "job_status", + .Description = "Query a long-running job (e.g. pattern_scan) by id, or list all jobs when id is omitted.", + .InputSchema = { + { "type", "object" }, + { "properties", { { "id", { { "type", "integer" }, { "description", "Job id from a scan tool" } } } } } }, + .Invoke = [](ToolContext& context, const nlohmann::json& args) { + auto it = args.find("id"); + if (it == args.end()) { + return nlohmann::json { { "jobs", context.Jobs.DescribeAll() } }; + } + return context.Jobs.Describe(it->get()); + }, + }); + } + + void ToolRegistry::RegisterMemoryTools() + { + Register(Tool { + .Name = "resolve_address", + .Description = "Evaluate an address expression like \"module.dll+0x1234\" or a pointer chain into a numeric address.", + .InputSchema = { + { "type", "object" }, + { "properties", { { "expression", { { "type", "string" } } } } }, + { "required", { "expression" } } }, + .Invoke = [](ToolContext& context, const nlohmann::json& args) { + Process& process = RequireProcess(context); + std::string expr = Require(args, "expression").get(); + AddressEvaluator::Result result = AddressEvaluator::Evaluate(expr, process); + if (result.IsError()) { + Fail(std::format("could not resolve '{}'", expr)); + } + return nlohmann::json { + { "expression", expr }, + { "address", std::format("0x{:X}", result.Value) }, + { "value", result.Value }, + }; + }, + }); + + Register(Tool { + .Name = "read_memory", + .Description = "Read up to 4096 bytes at an address (number or expression). Returns hex plus common integer interpretations.", + .InputSchema = { + { "type", "object" }, + { "properties", { + { "address", { { "description", "Numeric address or expression string" } } }, + { "size", { { "type", "integer" }, { "description", "Byte count (1-4096, default 8)" } } } } }, + { "required", { "address" } } }, + .Invoke = [](ToolContext& context, const nlohmann::json& args) { + Process& process = RequireProcess(context); + uintptr_t address = ResolveAddress(context, Require(args, "address")); + + size_t size = 8; + if (auto it = args.find("size"); it != args.end()) { + size = it->get(); + } + if (size == 0 || size > 4096) { + Fail("size must be between 1 and 4096"); + } + + std::vector bytes = process.ReadBytes(address, size); + if (bytes.empty()) { + Fail(std::format("read failed at 0x{:X} (unreadable region?)", address)); + } + + std::string hex; + hex.reserve(bytes.size() * 2); + for (uint8_t byte : bytes) { + hex += std::format("{:02X}", byte); + } + + nlohmann::json out { + { "address", std::format("0x{:X}", address) }, + { "size", bytes.size() }, + { "hex", hex }, + }; + + auto decode = [&bytes](auto sample) { + std::memcpy(&sample, bytes.data(), sizeof(sample)); + return sample; + }; + + if (bytes.size() >= 1) out["u8"] = bytes[0]; + if (bytes.size() >= 2) out["u16"] = decode(uint16_t {}); + if (bytes.size() >= 4) { + out["u32"] = decode(uint32_t {}); + out["f32"] = decode(float {}); + } + if (bytes.size() >= 8) { + out["u64"] = decode(uint64_t {}); + out["f64"] = decode(double {}); + } + return out; + }, + }); + + Register(Tool { + .Name = "write_memory", + .Description = "Write raw bytes (hex string) at an address (number or expression). Length is taken from the hex.", + .InputSchema = { + { "type", "object" }, + { "properties", { + { "address", { { "description", "Numeric address or expression string" } } }, + { "hex", { { "type", "string" }, { "description", "Bytes as hex, e.g. \"90 90\" or \"9090\"" } } } } }, + { "required", { "address", "hex" } } }, + .Invoke = [](ToolContext& context, const nlohmann::json& args) { + Process& process = RequireProcess(context); + uintptr_t address = ResolveAddress(context, Require(args, "address")); + std::string hex = Require(args, "hex").get(); + + std::vector bytes; + std::string nibble; + for (char c : hex) { + if (std::isspace(static_cast(c))) continue; + if (!std::isxdigit(static_cast(c))) { + Fail(std::format("invalid hex digit '{}' in byte string", c)); + } + nibble += c; + if (nibble.size() == 2) { + bytes.push_back(static_cast(std::stoul(nibble, nullptr, 16))); + nibble.clear(); + } + } + if (!nibble.empty()) { + Fail("hex string has an odd number of digits"); + } + if (bytes.empty()) { + Fail("no bytes to write"); + } + + if (!process.WriteBuffer(address, bytes.data(), bytes.size())) { + Fail(std::format("write failed at 0x{:X} (protection?)", address)); + } + return nlohmann::json { + { "address", std::format("0x{:X}", address) }, + { "written", bytes.size() }, + }; + }, + }); + + Register(Tool { + .Name = "query_memory", + .Description = "Query the memory region containing an address: base, size, state, protection, type.", + .InputSchema = { + { "type", "object" }, + { "properties", { { "address", { { "description", "Numeric address or expression string" } } } } }, + { "required", { "address" } } }, + .Invoke = [](ToolContext& context, const nlohmann::json& args) { + Process& process = RequireProcess(context); + uintptr_t address = ResolveAddress(context, Require(args, "address")); + + std::optional mbi = process.Query(address); + if (!mbi) { + Fail(std::format("query failed at 0x{:X}", address)); + } + return nlohmann::json { + { "baseAddress", std::format("0x{:X}", reinterpret_cast(mbi->BaseAddress)) }, + { "regionSize", mbi->RegionSize }, + { "state", std::format("0x{:X}", mbi->State) }, + { "protect", std::format("0x{:X}", mbi->Protect) }, + { "type", std::format("0x{:X}", mbi->Type) }, + }; + }, + }); + } + + void ToolRegistry::RegisterAnalysisTools() + { + Register(Tool { + .Name = "disassemble", + .Description = "Disassemble up to 64 instructions at an address (number or expression). Returns address, bytes and text per line.", + .InputSchema = { + { "type", "object" }, + { "properties", { + { "address", { { "description", "Numeric address or expression string" } } }, + { "count", { { "type", "integer" }, { "description", "Instruction count (1-64, default 10)" } } } } }, + { "required", { "address" } } }, + .Invoke = [](ToolContext& context, const nlohmann::json& args) { + Process& process = RequireProcess(context); + uintptr_t address = ResolveAddress(context, Require(args, "address")); + + size_t count = 10; + if (auto it = args.find("count"); it != args.end()) { + count = it->get(); + } + count = std::clamp(count, 1, 64); + + std::vector code = process.ReadBytes(address, count * 16); + if (code.empty()) { + Fail(std::format("read failed at 0x{:X}", address)); + } + + zasm::Decoder decoder(zasm::MachineMode::AMD64); + nlohmann::json lines = nlohmann::json::array(); + + size_t offset = 0; + for (size_t i = 0; i < count && offset < code.size(); ++i) { + uintptr_t lineAddress = address + offset; + zasm::Decoder::Result res = decoder.decode(code.data() + offset, code.size() - offset, lineAddress); + if (!res) { + break; + } + + const zasm::InstructionDetail& detail = res.value(); + FormattedInstruction formatted = Formatter::Format(detail.getInstruction()); + + std::string bytes; + for (size_t b = 0; b < detail.getLength(); ++b) { + bytes += std::format("{:02X}", code[offset + b]); + } + + lines.push_back({ + { "address", std::format("0x{:X}", lineAddress) }, + { "bytes", bytes }, + { "text", formatted.Text }, + }); + offset += detail.getLength(); + } + return nlohmann::json { { "instructions", lines }, { "count", lines.size() } }; + }, + }); + + Register(Tool { + .Name = "assemble", + .Description = "Assemble x86-64 source into machine code bytes. If write=true and a process is attached, patches them at address.", + .InputSchema = { + { "type", "object" }, + { "properties", { + { "source", { { "type", "string" }, { "description", "Assembly, newline separated" } } }, + { "address", { { "description", "Base address for encoding / patch target" } } }, + { "write", { { "type", "boolean" }, { "description", "Write bytes to the process (default false)" } } } } }, + { "required", { "source" } } }, + .Invoke = [](ToolContext& context, const nlohmann::json& args) { + std::string source = Require(args, "source").get(); + + uintptr_t base = 0; + bool write = args.value("write", false); + if (auto it = args.find("address"); it != args.end()) { + base = ResolveAddress(context, *it); + } + + Assembly::Assembler assembler; + assembler.Assemble(source); + + zasm::Program program(zasm::MachineMode::AMD64); + zasm::x86::Assembler a(program); + for (const zasm::Instruction& instruction : assembler.GetInstructions()) { + a.emit(instruction); + } + + zasm::Serializer serializer; + if (serializer.serialize(program, base).getCode() != zasm::ErrorCode::None) { + Fail("assembler failed to serialize the given source"); + } + + const uint8_t* code = serializer.getCode(); + size_t codeSize = serializer.getCodeSize(); + if (codeSize == 0) { + Fail("assembled to zero bytes; check the source"); + } + + std::string hex; + for (size_t i = 0; i < codeSize; ++i) { + hex += std::format("{:02X}", code[i]); + } + + nlohmann::json out { { "hex", hex }, { "size", codeSize } }; + + if (write) { + Process& process = RequireProcess(context); + if (!process.WriteBuffer(base, code, codeSize)) { + Fail(std::format("assembled ok but write failed at 0x{:X}", base)); + } + out["written"] = true; + out["address"] = std::format("0x{:X}", base); + } + return out; + }, + }); + + Register(Tool { + .Name = "dissect_struct", + .Description = "Interpret memory at an address as a struct: for each 8-byte slot report the raw qword and, if it points into readable memory, that it looks like a pointer.", + .InputSchema = { + { "type", "object" }, + { "properties", { + { "address", { { "description", "Numeric address or expression string" } } }, + { "size", { { "type", "integer" }, { "description", "Struct size in bytes (8-1024, default 64)" } } } } }, + { "required", { "address" } } }, + .Invoke = [](ToolContext& context, const nlohmann::json& args) { + Process& process = RequireProcess(context); + uintptr_t address = ResolveAddress(context, Require(args, "address")); + + size_t size = 64; + if (auto it = args.find("size"); it != args.end()) { + size = it->get(); + } + size = std::clamp(size, 8, 1024); + + nlohmann::json fields = nlohmann::json::array(); + for (size_t offset = 0; offset + 8 <= size; offset += 8) { + uintptr_t slot = address + offset; + uintptr_t qword = process.Read(slot); + + nlohmann::json field { + { "offset", std::format("0x{:X}", offset) }, + { "value", std::format("0x{:X}", qword) }, + }; + if (qword != 0 && process.IsAddressReadable(qword)) { + field["isPointer"] = true; + } + fields.push_back(field); + } + return nlohmann::json { + { "address", std::format("0x{:X}", address) }, + { "size", size }, + { "fields", fields }, + }; + }, + }); + + Register(Tool { + .Name = "generate_pattern", + .Description = "Generate a unique byte signature (IDA-style) for the instruction(s) at an address, wildcarding volatile operands.", + .InputSchema = { + { "type", "object" }, + { "properties", { { "address", { { "description", "Numeric address or expression string" } } } } }, + { "required", { "address" } } }, + .Invoke = [](ToolContext& context, const nlohmann::json& args) { + uintptr_t address = ResolveAddress(context, Require(args, "address")); + RequireProcess(context); + + PatternGenerator generator; + std::optional result = generator.Generate( + context.AppState.Process, address, 0x0, 0x7FFFFFFFFFFF); + if (!result) { + Fail(std::format("could not generate a pattern at 0x{:X}", address)); + } + return nlohmann::json { + { "address", std::format("0x{:X}", address) }, + { "pattern", result->pattern }, + { "byteCount", result->byteCount }, + { "instrCount", result->instrCount }, + { "wildcards", result->wildcards }, + }; + }, + }); + } + + void ToolRegistry::RegisterScanTools() + { + Register(Tool { + .Name = "pattern_scan", + .Description = "Scan the attached process for a byte pattern (\"48 8B ?? C3\"). Long-running: returns a jobId, then poll job_status.", + .InputSchema = { + { "type", "object" }, + { "properties", { + { "pattern", { { "type", "string" }, { "description", "Space-separated hex with ?? wildcards" } } }, + { "module", { { "type", "string" }, { "description", "Optional module to limit the scan to" } } } } }, + { "required", { "pattern" } } }, + .Invoke = [](ToolContext& context, const nlohmann::json& args) { + RequireProcess(context); + std::string pattern = Require(args, "pattern").get(); + std::string module = args.value("module", std::string {}); + + std::unique_ptr* processSlot = &context.AppState.Process; + std::string label = std::format("pattern_scan {}", pattern); + + uint64_t jobId = context.Jobs.Submit(label, [processSlot, pattern, module]() -> nlohmann::json { + PatternScanner scanner(*processSlot); + std::vector hits = module.empty() + ? scanner.PatternScan(pattern) + : scanner.PatternScan(pattern, module); + + nlohmann::json addresses = nlohmann::json::array(); + for (uintptr_t hit : hits) { + addresses.push_back(std::format("0x{:X}", hit)); + } + return nlohmann::json { { "matches", addresses }, { "count", addresses.size() } }; + }); + + return nlohmann::json { + { "jobId", jobId }, + { "status", "running" }, + { "hint", "poll job_status with this id" }, + }; + }, + }); + + Register(Tool { + .Name = "scan_value", + .Description = "Start a value scan over the attached process. Fire-and-poll: returns immediately, then use scan_results.", + .InputSchema = { + { "type", "object" }, + { "properties", { + { "valueType", { { "type", "string" }, { "description", "int8/16/32/64, uint8/16/32/64, float, double" } } }, + { "scanType", { { "type", "string" }, { "description", "exact, bigger, smaller, between, unknown" } } }, + { "value", { { "type", "string" }, { "description", "Target value for exact/bigger/smaller" } } }, + { "upper", { { "type", "string" }, { "description", "Upper bound for 'between'" } } }, + { "next", { { "type", "boolean" }, { "description", "Refine the previous scan instead of starting fresh" } } } } }, + { "required", { "valueType", "scanType" } } }, + .Invoke = [](ToolContext& context, const nlohmann::json& args) { + RequireProcess(context); + + ValueType valueType = ParseValueType(Require(args, "valueType").get()); + ScanType scanType = ParseScanType(Require(args, "scanType").get()); + std::string value = args.value("value", std::string {}); + std::string upper = args.value("upper", std::string {}); + bool next = args.value("next", false); + + if (next) { + if (!context.Scanner) { + Fail("no active scan to refine; run scan_value without next=true first"); + } + if (context.Scanner->IsScanning()) { + Fail("a scan is already in progress"); + } + if (!context.Scanner->NextScan(valueType, scanType, 0x0, 0x7FFFFFFFFFFF, value, upper)) { + Fail("failed to start scan refinement (invalid value or incompatible value type?)"); + } + } else { + if (context.Scanner && context.Scanner->IsScanning()) { + Fail("a scan is already in progress; poll scan_results until scanning=false"); + } + context.Scanner = std::make_unique(context.AppState.Process); + if (!context.Scanner->FirstScan(valueType, scanType, 0x0, 0x7FFFFFFFFFFF, value, upper)) { + context.Scanner.reset(); + Fail("failed to start scan (invalid value for the chosen type?)"); + } + } + + return nlohmann::json { + { "status", "scanning" }, + { "hint", "poll scan_results until scanning is false" }, + }; + }, + }); + + Register(Tool { + .Name = "scan_results", + .Description = "Report the current value-scan state: whether it's still scanning, the match count, and up to 100 result addresses.", + .InputSchema = { + { "type", "object" }, + { "properties", { { "limit", { { "type", "integer" }, { "description", "Max addresses to return (1-1000, default 100)" } } } } } }, + .Invoke = [](ToolContext& context, const nlohmann::json& args) { + if (!context.Scanner) { + return nlohmann::json { { "scanning", false }, { "count", 0 }, { "results", nlohmann::json::array() } }; + } + + size_t limit = 100; + if (auto it = args.find("limit"); it != args.end()) { + limit = std::clamp(it->get(), 1, 1000); + } + + std::lock_guard lock(context.Scanner->GetMutex()); + const std::vector& results = context.Scanner->GetResults(); + + nlohmann::json addresses = nlohmann::json::array(); + for (size_t i = 0; i < results.size() && i < limit; ++i) { + addresses.push_back(std::format("0x{:X}", results[i].Address)); + } + return nlohmann::json { + { "scanning", context.Scanner->IsScanning() }, + { "count", results.size() }, + { "results", addresses }, + }; + }, + }); + } + +} \ No newline at end of file diff --git a/CheatStengine/src/CheatStengine/Server/ToolRegistry.h b/CheatStengine/src/CheatStengine/Server/ToolRegistry.h new file mode 100644 index 0000000..ca40fea --- /dev/null +++ b/CheatStengine/src/CheatStengine/Server/ToolRegistry.h @@ -0,0 +1,58 @@ +#pragma once + +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include + +namespace Server { + + struct ToolContext { + State& AppState; + JobManager& Jobs; + std::unique_ptr& Scanner; + }; + + struct Tool { + using Handler = std::function; + + std::string Name; + std::string Description; + nlohmann::json InputSchema; + Handler Invoke; + }; + + class ToolRegistry { + public: + ToolRegistry(); + + [[nodiscard]] nlohmann::json ListTools() const; + [[nodiscard]] nlohmann::json CallTool(ToolContext& context, const std::string& name, const nlohmann::json& arguments) const; + [[nodiscard]] nlohmann::json DescribeAll() const; + + bool SetEnabled(const std::string& name, bool enabled); + [[nodiscard]] bool IsEnabled(const std::string& name) const; + + private: + void Register(Tool tool); + + void RegisterCoreTools(); // process list / attach / modules / job status + void RegisterMemoryTools(); // read / write / query / resolve + void RegisterAnalysisTools(); // disassemble / assemble / dissect / pattern gen + void RegisterScanTools(); // pattern scan / value scan + + std::vector m_Tools; + + mutable std::mutex m_Mutex; + std::unordered_map m_Disabled; + }; + +} diff --git a/CheatStengine/src/CheatStengine/Server/WebUI.h b/CheatStengine/src/CheatStengine/Server/WebUI.h new file mode 100644 index 0000000..1ff4ce8 --- /dev/null +++ b/CheatStengine/src/CheatStengine/Server/WebUI.h @@ -0,0 +1,367 @@ +#pragma once + +namespace Server { + + inline constexpr const char* kDashboardHtml = R"HTML( + + + + +Cheat Stengine — MCP + + + + +
+ +

Cheat Stengine — MCP

+ Configuration → +
+
+
+
Server
+
Attached process
+
Sessions
+
Active jobs
+
+ +
+

Sessions

+ +
IDClientVersionAge (s)
none
+
+ +
+

Jobs

+ +
IDLabelStatus
none
+
+ +
+

Tools

+
+ + +
+

loading…

+
+
+ + +)HTML"; + + inline constexpr const char* kConfigHtml = R"HTML( + + + + +Cheat Stengine — MCP Config + + + + +
+

Cheat Stengine — MCP Configuration

+ ← Dashboard +
+
+
+

Connection

+
+
Endpoint
+
Bearer token
+
+ Require auth + + +
+
+
+ +
+

Install targets

+

loading…

+
+
+
+ + +)HTML"; + +} diff --git a/CheatStengine/src/CheatStengine/Settings/EnumSetting.h b/CheatStengine/src/CheatStengine/Settings/EnumSetting.h index 6e4d428..b2663a9 100644 --- a/CheatStengine/src/CheatStengine/Settings/EnumSetting.h +++ b/CheatStengine/src/CheatStengine/Settings/EnumSetting.h @@ -103,6 +103,7 @@ class EnumSetting final : public Setting { void Apply() override { m_Value = m_TempValue; } [[nodiscard]] EnumType GetValue() const { return m_Value; } + [[nodiscard]] EnumType GetPendingValue() const { return m_TempValue; } [[nodiscard]] const char* GetCurrentItemName() const { return EnumTraits::GetName(m_Value); } [[nodiscard]] std::string GetName() const override { return m_Name; } @@ -120,4 +121,4 @@ class EnumSetting final : public Setting { std::string m_Description; EnumType m_Value; EnumType m_TempValue; -}; \ No newline at end of file +}; diff --git a/CheatStengine/src/CheatStengine/Settings/KernelSettings.cpp b/CheatStengine/src/CheatStengine/Settings/KernelSettings.cpp new file mode 100644 index 0000000..79598c1 --- /dev/null +++ b/CheatStengine/src/CheatStengine/Settings/KernelSettings.cpp @@ -0,0 +1,317 @@ +#include "KernelSettings.h" + +#include + +#include + +#include +#include +#include +#include + +namespace { + +const char* GetModeName(Protocol::LoadMode mode) +{ + switch (mode) { + case Protocol::LoadMode::Pnp: return "PnP"; + case Protocol::LoadMode::Service: return "Service"; + case Protocol::LoadMode::Mapped: return "Mapped"; + default: return "Unknown"; + } +} + +const char* GetStateName(Protocol::RuntimeState state) +{ + switch (state) { + case Protocol::RuntimeState::Starting: return "Starting"; + case Protocol::RuntimeState::Ready: return "Ready"; + case Protocol::RuntimeState::Idle: return "Idle"; + case Protocol::RuntimeState::Stopping: return "Stopping"; + case Protocol::RuntimeState::Removed: return "Removed"; + default: return "Unknown"; + } +} + +std::string GetErrorText(DWORD error) +{ + if (error == ERROR_SUCCESS) { + return "None"; + } + return std::format("{} ({})", std::system_category().message(error), error); +} + +void DrawRow(const char* name, const std::string& value) +{ + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::TextDisabled("%s", name); + ImGui::TableSetColumnIndex(1); + ImGui::TextUnformatted(value.c_str()); +} + +} + +KernelSettings::KernelSettings( + std::string name, + Monitor& monitor, + std::function visible) + : m_Name(std::move(name)) + , m_Monitor(monitor) + , m_Visible(std::move(visible)) +{ +} + +void KernelSettings::Draw() +{ + PollTasks(); + if (m_Visible && !m_Visible()) { + return; + } + const MonitorSnapshot monitor = m_Monitor.GetSnapshot(); + const bool connectionChanged = monitor.Connected + && monitor.Generation != m_LastPingGeneration; + if ((!m_LastPing || connectionChanged) && !m_PingTask.valid()) { + StartPing(); + } + + ImGui::PushID(m_Name.c_str()); + ImGui::Separator(); + ImGui::Spacing(); + ImGui::TextUnformatted("Kernel Mode"); + ImGui::Spacing(); + + DrawStatus(); + + const bool pinging = m_PingTask.valid(); + if (pinging) { + ImGui::BeginDisabled(); + } + if (ImGui::Button(pinging ? "Testing..." : "Ping Test")) { + StartPing(); + } + if (pinging) { + ImGui::EndDisabled(); + } + + int timeout = static_cast(m_TempValue); + ImGui::SetNextItemWidth(ImGui::GetWindowWidth() * 0.4f); + if (ImGui::SliderInt( + "Idle timeout", + &timeout, + static_cast(Protocol::MinimumIdleTimeoutSeconds), + static_cast(Protocol::MaximumIdleTimeoutSeconds), + "%d seconds")) { + m_TempValue = static_cast(timeout); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Time before the driver enters its idle state after the last client disconnects."); + } + + if (m_ConfigureTask.valid() || m_QueuedConfigure) { + ImGui::TextDisabled("Applying idle timeout..."); + } else if (m_LastConfigure) { + if (m_LastConfigure->Success) { + ImGui::TextDisabled("Idle timeout applied: %u seconds", m_LastConfigure->EffectiveSeconds); + } else { + const std::string error = GetErrorText(m_LastConfigure->Error); + ImGui::TextColored(ImVec4 { 1.0f, 0.4f, 0.4f, 1.0f }, "Apply failed: %s", error.c_str()); + } + } + + ImGui::PopID(); +} + +void KernelSettings::Restore() +{ + m_TempValue = m_Value; +} + +void KernelSettings::Apply() +{ + m_Value = m_TempValue; + if (!m_Visible || m_Visible()) { + QueueConfigure(m_Value); + } +} + +std::string KernelSettings::GetName() const +{ + return m_Name; +} + +std::string KernelSettings::GetDescription() const +{ + return {}; +} + +bool KernelSettings::HasValueChanged() const +{ + return m_Value != m_TempValue; +} + +void KernelSettings::PollTasks() +{ + if (m_PingTask.valid() + && m_PingTask.wait_for(std::chrono::seconds(0)) == std::future_status::ready) { + m_LastPing = m_PingTask.get(); + m_LastPingGeneration = m_PendingPingGeneration; + } + + if (m_ConfigureTask.valid() + && m_ConfigureTask.wait_for(std::chrono::seconds(0)) == std::future_status::ready) { + ConfigureResult result = m_ConfigureTask.get(); + if (result.Success) { + m_Value = result.EffectiveSeconds; + if (m_TempValue == result.RequestedSeconds) { + m_TempValue = result.EffectiveSeconds; + } + } + m_LastConfigure = result; + if (result.Success && !m_PingTask.valid()) { + StartPing(); + } + } + + StartConfigure(); +} + +void KernelSettings::StartPing() +{ + if (m_PingTask.valid()) { + return; + } + + const uint64_t nonce = m_NextNonce++; + const MonitorSnapshot monitor = m_Monitor.GetSnapshot(); + m_PendingPingGeneration = monitor.Connected ? monitor.Generation : 0; + m_PingTask = std::async(std::launch::async, [nonce] { + return RunPing(nonce); + }); +} + +void KernelSettings::QueueConfigure(uint32_t seconds) +{ + m_QueuedConfigure = seconds; + StartConfigure(); +} + +void KernelSettings::StartConfigure() +{ + if (m_ConfigureTask.valid() || !m_QueuedConfigure) { + return; + } + + const uint32_t seconds = *m_QueuedConfigure; + m_QueuedConfigure.reset(); + m_ConfigureTask = std::async(std::launch::async, [seconds] { + return RunConfigure(seconds); + }); +} + +void KernelSettings::DrawStatus() const +{ + const MonitorSnapshot monitor = m_Monitor.GetSnapshot(); + Protocol::LoadMode mode = monitor.Mode; + Protocol::RuntimeState state = monitor.State; + DWORD error = monitor.Error; + const bool currentPing = m_LastPing + && m_LastPing->Response + && (!monitor.Connected || m_LastPingGeneration == monitor.Generation); + + if (m_LastPing) { + if (!monitor.Connected) { + mode = m_LastPing->Mode; + } + if (!monitor.Connected || m_LastPingGeneration == monitor.Generation) { + error = m_LastPing->Error; + } + if (!monitor.Connected && m_LastPing->Response) { + state = static_cast(m_LastPing->Response->State); + } + } + + if (ImGui::BeginTable("Status", 2, ImGuiTableFlags_SizingStretchProp | ImGuiTableFlags_BordersInnerH)) { + DrawRow( + "Connection", + monitor.Connected + ? "Connected" + : currentPing + ? "Reachable" + : monitor.Running ? "Connecting" + : "Stopped"); + DrawRow("Load", GetModeName(mode)); + DrawRow("State", GetStateName(state)); + + if (currentPing) { + const Protocol::PingResponse& response = *m_LastPing->Response; + DrawRow("Driver", std::format( + "{}.{}.{}", + response.DriverMajorVersion, + response.DriverMinorVersion, + response.DriverPatchVersion)); + DrawRow("Protocol", std::format( + "{}.{}", + response.Header.MajorVersion, + response.Header.MinorVersion)); + DrawRow("Capabilities", std::format("0x{:016X}", response.Capabilities)); + DrawRow("Active sessions", std::to_string(response.ActiveSessions)); + DrawRow("Idle timeout", std::format("{} seconds", response.IdleTimeoutSeconds)); + DrawRow( + "Idle remaining", + std::format( + "{} seconds", + monitor.Connected ? monitor.RemainingIdleSeconds : response.RemainingIdleSeconds)); + DrawRow("RTT", std::format("{:.3f} ms", m_LastPing->Milliseconds)); + } else { + DrawRow("Heartbeat", monitor.Sequence == 0 ? "Waiting" : std::format("#{}", monitor.Sequence)); + DrawRow("Idle remaining", std::format("{} seconds", monitor.RemainingIdleSeconds)); + } + + DrawRow("Error", GetErrorText(error)); + ImGui::EndTable(); + } + + ImGui::Spacing(); +} + +KernelSettings::PingResult KernelSettings::RunPing(uint64_t nonce) +{ + PingResult result; + Device device; + result.Mode = device.GetMode(); + if (!device.IsOpen()) { + result.Error = device.GetError(); + return result; + } + + const auto started = std::chrono::steady_clock::now(); + result.Response = device.Ping(nonce); + const auto finished = std::chrono::steady_clock::now(); + result.Milliseconds = std::chrono::duration(finished - started).count(); + result.Error = device.GetError(); + if (result.Response) { + result.Mode = static_cast(result.Response->LoadMode); + } + return result; +} + +KernelSettings::ConfigureResult KernelSettings::RunConfigure(uint32_t seconds) +{ + ConfigureResult result; + result.RequestedSeconds = seconds; + + Device device; + if (!device.IsOpen()) { + result.Error = device.GetError(); + return result; + } + + const std::optional response = device.Configure(seconds); + result.Error = device.GetError(); + if (response) { + result.EffectiveSeconds = response->IdleTimeoutSeconds; + result.Success = true; + } + return result; +} diff --git a/CheatStengine/src/CheatStengine/Settings/KernelSettings.h b/CheatStengine/src/CheatStengine/Settings/KernelSettings.h new file mode 100644 index 0000000..16d1908 --- /dev/null +++ b/CheatStengine/src/CheatStengine/Settings/KernelSettings.h @@ -0,0 +1,69 @@ +#pragma once + +#include "Setting.h" + +#include + +#include + +#include + +#include +#include +#include +#include +#include + +class KernelSettings final : public Setting { +public: + KernelSettings(std::string name, Monitor& monitor, std::function visible); + + void Draw() override; + void Restore() override; + void Apply() override; + + [[nodiscard]] std::string GetName() const override; + [[nodiscard]] std::string GetDescription() const override; + [[nodiscard]] bool HasValueChanged() const override; + +private: + struct PingResult { + std::optional Response; + Protocol::LoadMode Mode = Protocol::LoadMode::Unknown; + DWORD Error = ERROR_SUCCESS; + double Milliseconds = 0.0; + }; + + struct ConfigureResult { + uint32_t RequestedSeconds = 0; + uint32_t EffectiveSeconds = 0; + DWORD Error = ERROR_SUCCESS; + bool Success = false; + }; + + void PollTasks(); + void StartPing(); + void QueueConfigure(uint32_t seconds); + void StartConfigure(); + void DrawStatus() const; + + [[nodiscard]] static PingResult RunPing(uint64_t nonce); + [[nodiscard]] static ConfigureResult RunConfigure(uint32_t seconds); + +private: + std::string m_Name; + Monitor& m_Monitor; + std::function m_Visible; + uint32_t m_Value = Protocol::DefaultIdleTimeoutSeconds; + uint32_t m_TempValue = Protocol::DefaultIdleTimeoutSeconds; + uint64_t m_NextNonce = 1; + uint64_t m_PendingPingGeneration = 0; + uint64_t m_LastPingGeneration = 0; + + std::future m_PingTask; + std::optional m_LastPing; + + std::future m_ConfigureTask; + std::optional m_QueuedConfigure; + std::optional m_LastConfigure; +}; diff --git a/CheatStengine/src/CheatStengine/Settings/McpControls.h b/CheatStengine/src/CheatStengine/Settings/McpControls.h new file mode 100644 index 0000000..1b86267 --- /dev/null +++ b/CheatStengine/src/CheatStengine/Settings/McpControls.h @@ -0,0 +1,33 @@ +#pragma once + +#include "Setting.h" + +#include +#include + +class McpControls final : public Setting { +public: + explicit McpControls(std::string name, std::function draw) + : m_Name(std::move(name)) + , m_Draw(std::move(draw)) + { + } + + void Draw() override + { + if (m_Draw) { + m_Draw(); + } + } + + void Restore() override { } + void Apply() override { } + + [[nodiscard]] std::string GetName() const override { return m_Name; } + [[nodiscard]] std::string GetDescription() const override { return {}; } + [[nodiscard]] bool HasValueChanged() const override { return false; } + +private: + std::string m_Name; + std::function m_Draw; +}; diff --git a/CheatStengine/src/CheatStengine/Tools/MemoryScanner.cpp b/CheatStengine/src/CheatStengine/Tools/MemoryScanner.cpp index 3b89e78..61f91a3 100644 --- a/CheatStengine/src/CheatStengine/Tools/MemoryScanner.cpp +++ b/CheatStengine/src/CheatStengine/Tools/MemoryScanner.cpp @@ -1,9 +1,230 @@ #include "MemoryScanner.h" +#include #include #include +#include +#include +#include #include +#include +#include +#include +#include + +namespace { + + constexpr size_t ScanReadChunkSize = 1024 * 1024; + constexpr size_t MaximumConsecutiveQueryFailures = 16; + + std::unique_ptr CloneProcess(const std::unique_ptr& process) + { + if (!process || !process->IsValid()) { + return {}; + } + + const ProcessMode mode = dynamic_cast(process.get()) + ? ProcessMode::Kernel + : ProcessMode::WinAPI; + try { + std::unique_ptr clone = Process::Create(process->GetPid(), mode); + if (!clone || !clone->IsValid() || !clone->Query(0)) { + WARN("Failed to open an independent process connection for the memory scan"); + return {}; + } + return clone; + } catch (const std::exception& error) { + WARN("Failed to open an independent process connection for the memory scan: {}", error.what()); + return {}; + } + } + + std::string_view Trim(std::string_view value) + { + const size_t first = value.find_first_not_of(" \t\r\n"); + if (first == std::string_view::npos) { + return {}; + } + + const size_t last = value.find_last_not_of(" \t\r\n"); + return value.substr(first, last - first + 1); + } + + template + T ParseInteger(std::string_view input) + { + static_assert(std::is_integral_v); + + const std::string_view trimmed = Trim(input); + if (trimmed.empty()) { + throw std::invalid_argument("value is empty"); + } + + const size_t prefixOffset = (trimmed.front() == '+' || trimmed.front() == '-') ? 1 : 0; + const bool isNegative = trimmed.front() == '-'; + const bool isHex = trimmed.size() >= prefixOffset + 2 + && trimmed[prefixOffset] == '0' + && (trimmed[prefixOffset + 1] == 'x' || trimmed[prefixOffset + 1] == 'X'); + const int base = isHex ? 16 : 10; + const std::string text(trimmed); + size_t parsedCharacters = 0; + + if constexpr (std::is_unsigned_v) { + if (isNegative) { + throw std::out_of_range("negative value cannot be stored in an unsigned type"); + } + + const unsigned long long parsed = std::stoull(text, &parsedCharacters, base); + if (parsedCharacters != text.size() || parsed > std::numeric_limits::max()) { + throw std::out_of_range("value is outside the selected type's range"); + } + return static_cast(parsed); + } else { + if (isHex && !isNegative) { + using UnsignedT = std::make_unsigned_t; + const unsigned long long parsed = std::stoull(text, &parsedCharacters, base); + if (parsedCharacters != text.size() || parsed > std::numeric_limits::max()) { + throw std::out_of_range("value is outside the selected type's range"); + } + + const UnsignedT bits = static_cast(parsed); + T value {}; + std::memcpy(&value, &bits, sizeof(value)); + return value; + } + + const long long parsed = std::stoll(text, &parsedCharacters, base); + if (parsedCharacters != text.size() + || parsed < static_cast(std::numeric_limits::min()) + || parsed > static_cast(std::numeric_limits::max())) { + throw std::out_of_range("value is outside the selected type's range"); + } + return static_cast(parsed); + } + } + + template + T ParseFloatingPoint(std::string_view input) + { + const std::string_view trimmed = Trim(input); + if (trimmed.empty()) { + throw std::invalid_argument("value is empty"); + } + + const std::string text(trimmed); + size_t parsedCharacters = 0; + T value {}; + if constexpr (std::is_same_v) { + value = std::stof(text, &parsedCharacters); + } else { + value = std::stod(text, &parsedCharacters); + } + + if (parsedCharacters != text.size()) { + throw std::invalid_argument("value contains invalid characters"); + } + return value; + } + + ScanValue ParseScanValue(std::string_view input, ValueType type) + { + switch (type) { + case ValueType::Int8: return ParseInteger(input); + case ValueType::Int16: return ParseInteger(input); + case ValueType::Int32: return ParseInteger(input); + case ValueType::Int64: return ParseInteger(input); + case ValueType::UInt8: return ParseInteger(input); + case ValueType::UInt16: return ParseInteger(input); + case ValueType::UInt32: return ParseInteger(input); + case ValueType::UInt64: return ParseInteger(input); + case ValueType::Float: return ParseFloatingPoint(input); + case ValueType::Double: return ParseFloatingPoint(input); + default: throw std::invalid_argument("invalid value type"); + } + } + + template + T ReadUnaligned(const uint8_t* data) + { + T value {}; + std::memcpy(&value, data, sizeof(value)); + return value; + } + + ScanValue ScanValueFromBytes(ValueType type, const uint8_t* data) + { + switch (type) { + case ValueType::Int8: return ReadUnaligned(data); + case ValueType::Int16: return ReadUnaligned(data); + case ValueType::Int32: return ReadUnaligned(data); + case ValueType::Int64: return ReadUnaligned(data); + case ValueType::UInt8: return ReadUnaligned(data); + case ValueType::UInt16: return ReadUnaligned(data); + case ValueType::UInt32: return ReadUnaligned(data); + case ValueType::UInt64: return ReadUnaligned(data); + case ValueType::Float: return ReadUnaligned(data); + case ValueType::Double: return ReadUnaligned(data); + default: throw std::invalid_argument("invalid value type"); + } + } + + bool TryReadScanValue(const Process& process, uintptr_t address, const ScanValue& previousValue, ScanValue& value) + { + return std::visit([&](auto previous) { + using T = decltype(previous); + T current {}; + if (!process.ReadBuffer(address, ¤t, sizeof(current))) { + return false; + } + value = current; + return true; + }, + previousValue); + } + + bool AreBoundsOrdered(const ScanValue& lower, const ScanValue& upper) + { + return std::visit([&](auto lowerValue) { + using T = decltype(lowerValue); + const T* upperValue = std::get_if(&upper); + return upperValue && lowerValue <= *upperValue; + }, + lower); + } + + template + bool CompareRawValues(ScanType scanType, const uint8_t* data, + const std::vector& target1, const std::vector& target2) + { + if (scanType == ScanType::UnknownInitialValue) { + return true; + } + if (target1.size() != sizeof(T)) { + return false; + } + + const T currentValue = ReadUnaligned(data); + const T lowerValue = ReadUnaligned(target1.data()); + switch (scanType) { + case ScanType::ExactValue: return currentValue == lowerValue; + case ScanType::BiggerThan: return currentValue > lowerValue; + case ScanType::SmallerThan: return currentValue < lowerValue; + case ScanType::ValueBetween: + return target2.size() == sizeof(T) + && currentValue >= lowerValue + && currentValue <= ReadUnaligned(target2.data()); + default: return false; + } + } + +} // namespace + +MemoryScanner::MemoryScanner(const std::unique_ptr& process) + : m_Process(CloneProcess(process)) +{ + m_CurrentResults.reserve(100000); +} std::string ValueTypeToString(ValueType type) { @@ -66,18 +287,15 @@ std::string MemoryAddress::ReadValue(const Process& proc) const bool MemoryAddress::WriteValue(const Process& proc, const std::string& value) const { - switch (Type) { - case ValueType::Int8: return proc.Write(Address, static_cast(std::stoi(value))); - case ValueType::Int16: return proc.Write(Address, static_cast(std::stoi(value))); - case ValueType::Int32: return proc.Write(Address, static_cast(std::stoi(value))); - case ValueType::Int64: return proc.Write(Address, static_cast(std::stoll(value))); - case ValueType::UInt8: return proc.Write(Address, static_cast(std::stoul(value))); - case ValueType::UInt16: return proc.Write(Address, static_cast(std::stoul(value))); - case ValueType::UInt32: return proc.Write(Address, static_cast(std::stoul(value))); - case ValueType::UInt64: return proc.Write(Address, static_cast(std::stoull(value))); - case ValueType::Float: return proc.Write(Address, std::stof(value)); - case ValueType::Double: return proc.Write(Address, std::stod(value)); - default: return false; + try { + const ScanValue parsedValue = ParseScanValue(value, Type); + return std::visit([&](auto typedValue) { + return proc.Write(Address, typedValue); + }, + parsedValue); + } catch (const std::exception& error) { + ERR("Invalid value '{}': {}", value, error.what()); + return false; } } @@ -100,325 +318,400 @@ std::string ScanValueToString(const ScanValue& value, bool isHex) bool MemoryScanner::FirstScan(ValueType valueType, ScanType scanType, uintptr_t minAddress, uintptr_t maxAddress, const std::string& lowerValue, const std::string& upperValue) { - if (m_Scanning) { + const size_t typeSize = GetTypeSize(valueType); + if (!m_Process || !m_Process->IsValid() || typeSize == 0 + || scanType < ScanType::ExactValue || scanType >= ScanType::COUNT + || minAddress >= maxAddress) { + ERR("Invalid memory scan configuration"); return false; } - std::vector targetValue1, targetValue2; - if (scanType != ScanType::UnknownInitialValue) { - if (lowerValue.empty()) { - return false; - } - targetValue1 = StringToBytes(lowerValue, valueType); - } - if (scanType == ScanType::ValueBetween) { - if (upperValue.empty()) { - return false; - } - targetValue2 = StringToBytes(upperValue, valueType); - } - std::thread([this](ValueType valueType, ScanType scanType, - uintptr_t minAddress, uintptr_t maxAddress, - const std::vector& targetValue1, const std::vector& targetValue2) { - auto start = std::chrono::high_resolution_clock::now(); - - { - std::lock_guard lock(m_Mutex); - m_CurrentResults.clear(); + std::vector targetValue1; + std::vector targetValue2; + try { + if (scanType != ScanType::UnknownInitialValue) { + if (lowerValue.empty()) { + ERR("A scan value is required"); + return false; + } + targetValue1 = StringToBytes(lowerValue, valueType); } - m_Scanning = true; - std::vector> regions; - regions.reserve(256); - uintptr_t currentAddress = minAddress; - while (currentAddress < maxAddress) { - std::optional mbi = m_Process->Query(currentAddress); - if (!mbi) { - currentAddress += 0x1000; - continue; + if (scanType == ScanType::ValueBetween) { + if (upperValue.empty()) { + ERR("An upper scan value is required"); + return false; } - - uintptr_t base = reinterpret_cast(mbi->BaseAddress); - if (!IsValidMemoryRegion(*mbi)) { - currentAddress = base + mbi->RegionSize; - continue; + targetValue2 = StringToBytes(upperValue, valueType); + if (!AreBoundsOrdered(StringToValue(lowerValue, valueType), StringToValue(upperValue, valueType))) { + ERR("The lower scan value must not exceed the upper scan value"); + return false; } - - regions.emplace_back(base, mbi->RegionSize); - currentAddress = base + mbi->RegionSize; } + } catch (const std::exception& error) { + ERR("Invalid scan value: {}", error.what()); + return false; + } + + std::scoped_lock threadLock(m_ThreadMutex); + if (m_Scanning.exchange(true, std::memory_order_acq_rel)) { + return false; + } + m_ValueType = valueType; + + try { + m_ScanThread = std::jthread([this, valueType, scanType, minAddress, maxAddress, + typeSize, targetValue1 = std::move(targetValue1), + targetValue2 = std::move(targetValue2)](std::stop_token stopToken) { + try { + auto startTime = std::chrono::high_resolution_clock::now(); + { + std::lock_guard lock(m_Mutex); + m_CurrentResults.clear(); + } + + std::vector> regions; + regions.reserve(256); + uintptr_t currentAddress = minAddress; + size_t consecutiveQueryFailures = 0; + while (currentAddress < maxAddress && !stopToken.stop_requested()) { + const std::optional mbi = m_Process->Query(currentAddress); + if (!mbi) { + ++consecutiveQueryFailures; + if (consecutiveQueryFailures >= MaximumConsecutiveQueryFailures) { + WARN("Stopping memory-region discovery after {} consecutive query failures at 0x{:X}", + consecutiveQueryFailures, currentAddress); + break; + } + currentAddress += std::min(0x1000, maxAddress - currentAddress); + continue; + } + consecutiveQueryFailures = 0; - INFO("Found {} regions to scan", regions.size()); - INFO("Took: {}ms", std::chrono::duration(std::chrono::high_resolution_clock::now() - start).count()); - start = std::chrono::high_resolution_clock::now(); - size_t typeSize = GetTypeSize(valueType); + const uintptr_t base = reinterpret_cast(mbi->BaseAddress); + uintptr_t regionEnd = std::numeric_limits::max(); + if (mbi->RegionSize <= std::numeric_limits::max() - base) { + regionEnd = base + static_cast(mbi->RegionSize); + } - std::vector>> futures; - futures.reserve(regions.size()); + if (regionEnd <= currentAddress) { + currentAddress += std::min(0x1000, maxAddress - currentAddress); + continue; + } - for (const auto& [address, size] : regions) { - futures.emplace_back(std::async(std::launch::async, [this, address, size, typeSize, scanType, valueType, &targetValue1, &targetValue2]() -> std::vector { - std::vector buffer(size); - if (!m_Process->ReadBuffer(address, buffer.data(), size)) { - return {}; + const uintptr_t scanStart = std::max({ minAddress, currentAddress, base }); + const uintptr_t scanEnd = std::min(maxAddress, regionEnd); + if (IsValidMemoryRegion(*mbi) + && scanEnd > scanStart + && scanEnd - scanStart >= typeSize) { + regions.emplace_back(scanStart, static_cast(scanEnd - scanStart)); + } + currentAddress = regionEnd; } - std::vector localResults; - for (size_t offset = 0; offset < size; offset += typeSize) { - uintptr_t currentAddress = address + offset; - - if (CompareByteArrays(scanType, valueType, buffer.data() + offset, targetValue1, targetValue2)) { - switch (valueType) { - case ValueType::Int8: localResults.emplace_back(currentAddress, *reinterpret_cast(buffer.data() + offset)); break; - case ValueType::Int16: localResults.emplace_back(currentAddress, *reinterpret_cast(buffer.data() + offset)); break; - case ValueType::Int32: localResults.emplace_back(currentAddress, *reinterpret_cast(buffer.data() + offset)); break; - case ValueType::Int64: localResults.emplace_back(currentAddress, *reinterpret_cast(buffer.data() + offset)); break; - case ValueType::UInt8: localResults.emplace_back(currentAddress, *reinterpret_cast(buffer.data() + offset)); break; - case ValueType::UInt16: localResults.emplace_back(currentAddress, *reinterpret_cast(buffer.data() + offset)); break; - case ValueType::UInt32: localResults.emplace_back(currentAddress, *reinterpret_cast(buffer.data() + offset)); break; - case ValueType::UInt64: localResults.emplace_back(currentAddress, *reinterpret_cast(buffer.data() + offset)); break; - case ValueType::Float: localResults.emplace_back(currentAddress, *reinterpret_cast(buffer.data() + offset)); break; - case ValueType::Double: localResults.emplace_back(currentAddress, *reinterpret_cast(buffer.data() + offset)); break; + INFO("Found {} regions to scan", regions.size()); + INFO("Region query took: {}ms", std::chrono::duration(std::chrono::high_resolution_clock::now() - startTime).count()); + startTime = std::chrono::high_resolution_clock::now(); + + std::vector newResults; + if (!regions.empty() && !stopToken.stop_requested()) { + const size_t hardwareThreads = std::max(1, std::thread::hardware_concurrency()); + const size_t workerCount = std::min(hardwareThreads, regions.size()); + const size_t regionsPerWorker = (regions.size() + workerCount - 1) / workerCount; + + std::vector>> futures; + futures.reserve(workerCount); + for (size_t worker = 0; worker < workerCount; ++worker) { + const size_t firstRegion = worker * regionsPerWorker; + const size_t lastRegion = std::min(firstRegion + regionsPerWorker, regions.size()); + if (firstRegion >= lastRegion) { + break; } + + futures.emplace_back(std::async(std::launch::async, + [this, ®ions, firstRegion, lastRegion, typeSize, scanType, valueType, + &targetValue1, &targetValue2, stopToken] { + std::vector localResults; + for (size_t regionIndex = firstRegion; + regionIndex < lastRegion && !stopToken.stop_requested(); + ++regionIndex) { + const auto [address, regionSize] = regions[regionIndex]; + std::vector buffer(std::min(ScanReadChunkSize, regionSize)); + + for (size_t regionOffset = 0; + regionOffset < regionSize && !stopToken.stop_requested();) { + const size_t readSize = std::min(buffer.size(), regionSize - regionOffset); + if (m_Process->ReadBuffer(address + regionOffset, buffer.data(), readSize)) { + for (size_t offset = 0; offset + typeSize <= readSize; offset += typeSize) { + const uint8_t* valueBytes = buffer.data() + offset; + if (CompareByteArrays(scanType, valueType, valueBytes, + targetValue1, targetValue2)) { + localResults.emplace_back(address + regionOffset + offset, + ScanValueFromBytes(valueType, valueBytes)); + } + } + } + regionOffset += readSize; + } + } + return localResults; + })); + } + + for (auto& future : futures) { + std::vector results = future.get(); + newResults.insert(newResults.end(), + std::make_move_iterator(results.begin()), + std::make_move_iterator(results.end())); } - // Additional scan types can be implemented here. } - return localResults; - })); - } - for (auto& future : futures) { - std::vector results = future.get(); - std::lock_guard lock(m_Mutex); - m_CurrentResults.insert(m_CurrentResults.end(), - std::make_move_iterator(results.begin()), - std::make_move_iterator(results.end())); - } + if (!stopToken.stop_requested()) { + const size_t resultCount = newResults.size(); + { + std::lock_guard lock(m_Mutex); + m_CurrentResults = std::move(newResults); + } + + INFO("Found {} results", resultCount); + INFO("Memory scan took: {}ms", std::chrono::duration(std::chrono::high_resolution_clock::now() - startTime).count()); + } + } catch (const std::exception& error) { + ERR("Memory scan failed: {}", error.what()); + } catch (...) { + ERR("Memory scan failed with an unknown error"); + } + m_Scanning.store(false, std::memory_order_release); + }); + } catch (const std::exception& error) { + m_ValueType = ValueType::COUNT; + m_Scanning.store(false, std::memory_order_release); + ERR("Failed to start memory scan: {}", error.what()); + return false; + } - INFO("Found {} results", m_CurrentResults.size()); - INFO("Took: {}ms", std::chrono::duration(std::chrono::high_resolution_clock::now() - start).count()); - m_Scanning = false; - }, - valueType, scanType, minAddress, maxAddress, targetValue1, targetValue2) - .detach(); return true; } -void MemoryScanner::NextScan(ValueType valueType, ScanType scanType, uintptr_t minAddress, uintptr_t maxAddress, const std::string& lowerValue, const std::string& upperValue) +bool MemoryScanner::NextScan(ValueType valueType, ScanType scanType, uintptr_t minAddress, uintptr_t maxAddress, const std::string& lowerValue, const std::string& upperValue) { - if (m_CurrentResults.empty()) { - ERR("Not previous scan results"); - return; + const size_t typeSize = GetTypeSize(valueType); + if (!m_Process || !m_Process->IsValid() || typeSize == 0 + || scanType < ScanType::ExactValue || scanType >= ScanType::COUNT + || minAddress >= maxAddress) { + ERR("Invalid memory scan configuration"); + return false; } - if (m_Scanning) { - return; + if (valueType != m_ValueType) { + ERR("The value type cannot be changed while refining a scan"); + return false; } - std::thread([this](ValueType valueType, ScanType scanType, uintptr_t minAddress, uintptr_t maxAddress, const std::string& lowerValue, const std::string& upperValue) { - INFO("Refining {} results...", m_CurrentResults.size()); + { + std::lock_guard lock(m_Mutex); + if (m_CurrentResults.empty()) { + ERR("No previous scan results"); + return false; + } + } - ScanValue targetValue1, targetValue2; - if (scanType != ScanType::UnknownInitialValue && !lowerValue.empty()) { + ScanValue targetValue1 = int8_t {}; + ScanValue targetValue2 = int8_t {}; + try { + if (scanType != ScanType::UnknownInitialValue) { + if (lowerValue.empty()) { + ERR("A scan value is required"); + return false; + } targetValue1 = StringToValue(lowerValue, valueType); } - if (scanType == ScanType::ValueBetween && !upperValue.empty()) { + if (scanType == ScanType::ValueBetween) { + if (upperValue.empty()) { + ERR("An upper scan value is required"); + return false; + } targetValue2 = StringToValue(upperValue, valueType); + if (!AreBoundsOrdered(targetValue1, targetValue2)) { + ERR("The lower scan value must not exceed the upper scan value"); + return false; + } } + } catch (const std::exception& error) { + ERR("Invalid scan value: {}", error.what()); + return false; + } - m_Scanning = true; - - uint32_t numThreads = std::thread::hardware_concurrency(); - if (numThreads == 0) { - numThreads = 4; - } - - size_t totalAddresses = m_CurrentResults.size(); - size_t chunkSize = (totalAddresses + numThreads - 1) / numThreads; + std::scoped_lock threadLock(m_ThreadMutex); + if (m_Scanning.exchange(true, std::memory_order_acq_rel)) { + return false; + } - std::vector>> futures; - for (size_t i = 0; i < numThreads; ++i) { - size_t start = i * chunkSize; - size_t end = std::min(start + chunkSize, totalAddresses); - if (start >= totalAddresses) { - break; - } + try { + m_ScanThread = std::jthread([this, scanType, minAddress, maxAddress, typeSize, + targetValue1 = std::move(targetValue1), + targetValue2 = std::move(targetValue2)](std::stop_token stopToken) { + try { + size_t totalAddresses = 0; + { + std::lock_guard lock(m_Mutex); + totalAddresses = m_CurrentResults.size(); + } + INFO("Refining {} results...", totalAddresses); + + const size_t hardwareThreads = std::max(1, std::thread::hardware_concurrency()); + const size_t workerCount = std::max(1, std::min(hardwareThreads, totalAddresses)); + const size_t addressesPerWorker = (totalAddresses + workerCount - 1) / workerCount; + + std::vector>> futures; + futures.reserve(workerCount); + for (size_t worker = 0; worker < workerCount; ++worker) { + const size_t firstAddress = worker * addressesPerWorker; + const size_t lastAddress = std::min(firstAddress + addressesPerWorker, totalAddresses); + if (firstAddress >= lastAddress) { + break; + } - futures.push_back(std::async(std::launch::async, [this, scanType, &targetValue1, &targetValue2, minAddress, maxAddress](size_t startIdx, size_t endIdx) { - std::vector threadResults; + futures.emplace_back(std::async(std::launch::async, + [this, scanType, &targetValue1, &targetValue2, minAddress, maxAddress, typeSize, + firstAddress, lastAddress, stopToken] { + std::vector localResults; + for (size_t index = firstAddress; + index < lastAddress && !stopToken.stop_requested(); + ++index) { + const ScannedAddress& address = m_CurrentResults[index]; + if (address.Address < minAddress || address.Address >= maxAddress + || typeSize > maxAddress - address.Address) { + continue; + } + + ScanValue value = address.PreviousValue; + if (!TryReadScanValue(*m_Process, address.Address, address.PreviousValue, value)) { + continue; + } + if (CompareValues(scanType, value, targetValue1, targetValue2)) { + localResults.push_back(address); + localResults.back().PreviousValue = value; + } + } + return localResults; + })); + } - for (size_t j = startIdx; j < endIdx; j++) { - ScannedAddress& addr = m_CurrentResults[j]; - if (addr.Address < minAddress || addr.Address >= maxAddress) { - continue; + std::vector newResults; + for (auto& future : futures) { + std::vector results = future.get(); + newResults.insert(newResults.end(), + std::make_move_iterator(results.begin()), + std::make_move_iterator(results.end())); } - ScanValue value = addr.ReadValue(*m_Process); - if (CompareValues(scanType, value, targetValue1, targetValue2)) { - threadResults.push_back(addr); - threadResults.back().PreviousValue = value; + + if (!stopToken.stop_requested()) { + const size_t resultCount = newResults.size(); + { + std::lock_guard lock(m_Mutex); + m_CurrentResults = std::move(newResults); + } + INFO("Found {} results", resultCount); } + } catch (const std::exception& error) { + ERR("Memory scan refinement failed: {}", error.what()); + } catch (...) { + ERR("Memory scan refinement failed with an unknown error"); } + m_Scanning.store(false, std::memory_order_release); + }); + } catch (const std::exception& error) { + m_Scanning.store(false, std::memory_order_release); + ERR("Failed to start memory scan refinement: {}", error.what()); + return false; + } - return threadResults; }, start, end)); - } - - std::vector newResults; - for (auto& future : futures) { - auto threadResult = future.get(); - newResults.insert(newResults.end(), - std::make_move_iterator(threadResult.begin()), - std::make_move_iterator(threadResult.end())); - } - - { - std::lock_guard lock(m_Mutex); - m_CurrentResults = std::move(newResults); - } - m_Scanning = false; - INFO("Found {} results", m_CurrentResults.size()); - }, - valueType, scanType, minAddress, maxAddress, lowerValue, upperValue) - .detach(); + return true; } -bool MemoryScanner::CompareValues(ScanType scanType, const ScanValue& value, const ScanValue& targetValue1, const ScanValue& targetValue2) +void MemoryScanner::Cancel() { - if (scanType == ScanType::ExactValue) { - return value == targetValue1; - } - - if (scanType == ScanType::BiggerThan) { - return std::visit(overloads { - [&](int8_t v) { return v > std::get(targetValue1); }, - [&](int16_t v) { return v > std::get(targetValue1); }, - [&](int32_t v) { return v > std::get(targetValue1); }, - [&](int64_t v) { return v > std::get(targetValue1); }, - [&](uint8_t v) { return v > std::get(targetValue1); }, - [&](uint16_t v) { return v > std::get(targetValue1); }, - [&](uint32_t v) { return v > std::get(targetValue1); }, - [&](uint64_t v) { return v > std::get(targetValue1); }, - [&](float v) { return v > std::get(targetValue1); }, - [&](double v) { return v > std::get(targetValue1); }, - }, - value); - } - - if (scanType == ScanType::SmallerThan) { - return std::visit(overloads { - [&](int8_t v) { return v < std::get(targetValue1); }, - [&](int16_t v) { return v < std::get(targetValue1); }, - [&](int32_t v) { return v < std::get(targetValue1); }, - [&](int64_t v) { return v < std::get(targetValue1); }, - [&](uint8_t v) { return v < std::get(targetValue1); }, - [&](uint16_t v) { return v < std::get(targetValue1); }, - [&](uint32_t v) { return v < std::get(targetValue1); }, - [&](uint64_t v) { return v < std::get(targetValue1); }, - [&](float v) { return v < std::get(targetValue1); }, - [&](double v) { return v < std::get(targetValue1); }, - }, - value); + std::scoped_lock lock(m_ThreadMutex); + if (m_ScanThread.joinable()) { + m_ScanThread.request_stop(); } - return false; } -bool MemoryScanner::CompareByteArrays(ScanType scanType, ValueType valueType, uint8_t* data, const std::vector& target1, const std::vector& target2) +bool MemoryScanner::CompareValues(ScanType scanType, const ScanValue& value, const ScanValue& targetValue1, const ScanValue& targetValue2) { - if (scanType == ScanType::ExactValue) { - return memcmp(data, target1.data(), target1.size()) == 0; + if (scanType == ScanType::UnknownInitialValue) { + return true; } - if (scanType == ScanType::BiggerThan) { - switch (valueType) { - case ValueType::Int8: return *(int8_t*)data > *(int8_t*)target1.data(); - case ValueType::Int16: return *(int16_t*)data > *(int16_t*)target1.data(); - case ValueType::Int32: return *(int32_t*)data > *(int32_t*)target1.data(); - case ValueType::Int64: return *(int64_t*)data > *(int64_t*)target1.data(); - case ValueType::UInt8: return *(uint8_t*)data > *(uint8_t*)target1.data(); - case ValueType::UInt16: return *(uint16_t*)data > *(uint16_t*)target1.data(); - case ValueType::UInt32: return *(uint32_t*)data > *(uint32_t*)target1.data(); - case ValueType::UInt64: return *(uint64_t*)data > *(uint64_t*)target1.data(); - case ValueType::Float: return *(float*)data > *(float*)target1.data(); - case ValueType::Double: return *(double*)data > *(double*)target1.data(); - default: return false; + return std::visit([&](auto currentValue) { + using T = decltype(currentValue); + const T* lowerValue = std::get_if(&targetValue1); + if (!lowerValue) { + return false; } - } - if (scanType == ScanType::SmallerThan) { - switch (valueType) { - case ValueType::Int8: return *(int8_t*)data < *(int8_t*)target1.data(); - case ValueType::Int16: return *(int16_t*)data < *(int16_t*)target1.data(); - case ValueType::Int32: return *(int32_t*)data < *(int32_t*)target1.data(); - case ValueType::Int64: return *(int64_t*)data < *(int64_t*)target1.data(); - case ValueType::UInt8: return *(uint8_t*)data < *(uint8_t*)target1.data(); - case ValueType::UInt16: return *(uint16_t*)data < *(uint16_t*)target1.data(); - case ValueType::UInt32: return *(uint32_t*)data < *(uint32_t*)target1.data(); - case ValueType::UInt64: return *(uint64_t*)data < *(uint64_t*)target1.data(); - case ValueType::Float: return *(float*)data < *(float*)target1.data(); - case ValueType::Double: return *(double*)data < *(double*)target1.data(); + switch (scanType) { + case ScanType::ExactValue: return currentValue == *lowerValue; + case ScanType::BiggerThan: return currentValue > *lowerValue; + case ScanType::SmallerThan: return currentValue < *lowerValue; + case ScanType::ValueBetween: { + const T* upperValue = std::get_if(&targetValue2); + return upperValue && currentValue >= *lowerValue && currentValue <= *upperValue; + } default: return false; } - } + }, + value); +} - if (scanType == ScanType::ValueBetween) { - switch (valueType) { - case ValueType::Int8: return *(int8_t*)data >= *(int8_t*)target1.data() && *(int8_t*)data <= *(int8_t*)target2.data(); - case ValueType::Int16: return *(int16_t*)data >= *(int16_t*)target1.data() && *(int16_t*)data <= *(int16_t*)target2.data(); - case ValueType::Int32: return *(int32_t*)data >= *(int32_t*)target1.data() && *(int32_t*)data <= *(int32_t*)target2.data(); - case ValueType::Int64: return *(int64_t*)data >= *(int64_t*)target1.data() && *(int64_t*)data <= *(int64_t*)target2.data(); - case ValueType::UInt8: return *(uint8_t*)data >= *(uint8_t*)target1.data() && *(uint8_t*)data <= *(uint8_t*)target2.data(); - case ValueType::UInt16: return *(uint16_t*)data >= *(uint16_t*)target1.data() && *(uint16_t*)data <= *(uint16_t*)target2.data(); - case ValueType::UInt32: return *(uint32_t*)data >= *(uint32_t*)target1.data() && *(uint32_t*)data <= *(uint32_t*)target2.data(); - case ValueType::UInt64: return *(uint64_t*)data >= *(uint64_t*)target1.data() && *(uint64_t*)data <= *(uint64_t*)target2.data(); - case ValueType::Float: return *(float*)data >= *(float*)target1.data() && *(float*)data <= *(float*)target2.data(); - case ValueType::Double: return *(double*)data >= *(double*)target1.data() && *(double*)data <= *(double*)target2.data(); - default: return false; - } +bool MemoryScanner::CompareByteArrays(ScanType scanType, ValueType valueType, const uint8_t* data, const std::vector& target1, const std::vector& target2) +{ + if (!data) { + return false; } - return true; + switch (valueType) { + case ValueType::Int8: return CompareRawValues(scanType, data, target1, target2); + case ValueType::Int16: return CompareRawValues(scanType, data, target1, target2); + case ValueType::Int32: return CompareRawValues(scanType, data, target1, target2); + case ValueType::Int64: return CompareRawValues(scanType, data, target1, target2); + case ValueType::UInt8: return CompareRawValues(scanType, data, target1, target2); + case ValueType::UInt16: return CompareRawValues(scanType, data, target1, target2); + case ValueType::UInt32: return CompareRawValues(scanType, data, target1, target2); + case ValueType::UInt64: return CompareRawValues(scanType, data, target1, target2); + case ValueType::Float: return CompareRawValues(scanType, data, target1, target2); + case ValueType::Double: return CompareRawValues(scanType, data, target1, target2); + default: return false; + } } std::vector MemoryScanner::StringToBytes(const std::string& str, ValueType type) { - std::string val = str.starts_with("0x") ? str.substr(2) : str; - switch (type) { - case ValueType::Int8: return ValueToBytes(std::stoi(val)); - case ValueType::UInt8: return ValueToBytes(std::stoul(val)); - case ValueType::Int16: return ValueToBytes(std::stoi(val)); - case ValueType::UInt16: return ValueToBytes(std::stoul(val)); - case ValueType::Int32: return ValueToBytes(std::stol(val)); - case ValueType::UInt32: return ValueToBytes(std::stoul(val)); - case ValueType::Int64: return ValueToBytes(std::stoll(val)); - case ValueType::UInt64: return ValueToBytes(std::stoull(val)); - case ValueType::Float: return ValueToBytes(std::stof(val)); - case ValueType::Double: return ValueToBytes(std::stod(val)); - default: return {}; - } + const ScanValue value = ParseScanValue(str, type); + return std::visit([](auto typedValue) { + return ValueToBytes(typedValue); + }, + value); } ScanValue MemoryScanner::StringToValue(const std::string& str, ValueType type) { - switch (type) { - case ValueType::Int8: return static_cast(std::stoi(str)); - case ValueType::UInt8: return static_cast(std::stoul(str)); - case ValueType::Int16: return static_cast(std::stoi(str)); - case ValueType::UInt16: return static_cast(std::stoul(str)); - case ValueType::Int32: return static_cast(std::stol(str)); - case ValueType::UInt32: return static_cast(std::stoul(str)); - case ValueType::Int64: return static_cast(std::stoll(str)); - case ValueType::UInt64: return static_cast(std::stoull(str)); - case ValueType::Float: return static_cast(std::stof(str)); - case ValueType::Double: return static_cast(std::stod(str)); - default: return {}; - } + return ParseScanValue(str, type); } bool MemoryScanner::IsValidMemoryRegion(const MEMORY_BASIC_INFORMATION& memInfo) { - return (memInfo.State == MEM_COMMIT) - && (memInfo.Protect & PAGE_GUARD) == 0 - && (memInfo.Protect != PAGE_NOACCESS) - && ((memInfo.Protect & PAGE_READWRITE) - || (memInfo.Protect & PAGE_READONLY) - || (memInfo.Protect & PAGE_EXECUTE_READ) - || (memInfo.Protect & PAGE_EXECUTE_READWRITE)); -} \ No newline at end of file + if (memInfo.State != MEM_COMMIT || (memInfo.Protect & PAGE_GUARD) != 0) { + return false; + } + + switch (memInfo.Protect & 0xFF) { + case PAGE_READONLY: + case PAGE_READWRITE: + case PAGE_WRITECOPY: + case PAGE_EXECUTE_READ: + case PAGE_EXECUTE_READWRITE: + case PAGE_EXECUTE_WRITECOPY: return true; + default: return false; + } +} diff --git a/CheatStengine/src/CheatStengine/Tools/MemoryScanner.h b/CheatStengine/src/CheatStengine/Tools/MemoryScanner.h index 3c0cf80..8ef205d 100644 --- a/CheatStengine/src/CheatStengine/Tools/MemoryScanner.h +++ b/CheatStengine/src/CheatStengine/Tools/MemoryScanner.h @@ -4,9 +4,12 @@ #include +#include #include +#include #include #include +#include #include #include @@ -67,26 +70,24 @@ enum class ScanType { class MemoryScanner { public: - explicit MemoryScanner(const std::unique_ptr& proc) - : m_Process(proc) - { - m_CurrentResults.reserve(100000); - } + explicit MemoryScanner(const std::unique_ptr& process); bool FirstScan(ValueType valueType, ScanType scanType, uintptr_t minAddress, uintptr_t maxAddress, const std::string& lowerValue = "", const std::string& upperValue = ""); - void NextScan(ValueType valueType, ScanType scanType, + bool NextScan(ValueType valueType, ScanType scanType, uintptr_t minAddress, uintptr_t maxAddress, const std::string& lowerValue = "", const std::string& upperValue = ""); + void Cancel(); [[nodiscard]] const std::vector& GetResults() const { return m_CurrentResults; } [[nodiscard]] std::mutex& GetMutex() { return m_Mutex; } [[nodiscard]] bool IsScanning() const { return m_Scanning; } + [[nodiscard]] uint32_t GetProcessId() const { return m_Process ? m_Process->GetPid() : 0; } private: template @@ -97,16 +98,19 @@ class MemoryScanner { return bytes; } static bool CompareValues(ScanType scanType, const ScanValue& value, const ScanValue& targetValue1, const ScanValue& targetValue2); - static bool CompareByteArrays(ScanType scanType, ValueType valueType, uint8_t* data, const std::vector& target1, const std::vector& target2); + static bool CompareByteArrays(ScanType scanType, ValueType valueType, const uint8_t* data, const std::vector& target1, const std::vector& target2); static std::vector StringToBytes(const std::string& str, ValueType type); static ScanValue StringToValue(const std::string& str, ValueType type); static bool IsValidMemoryRegion(const MEMORY_BASIC_INFORMATION& memInfo); private: - const std::unique_ptr& m_Process; + std::unique_ptr m_Process; std::vector m_CurrentResults; std::mutex m_Mutex; - bool m_Scanning = false; + std::mutex m_ThreadMutex; + std::atomic m_ValueType = ValueType::COUNT; + std::atomic m_Scanning = false; + std::jthread m_ScanThread; }; diff --git a/CheatStengine/src/CheatStengine/UI/ImGui/IconCache.cpp b/CheatStengine/src/CheatStengine/UI/ImGui/IconCache.cpp new file mode 100644 index 0000000..dc4cf22 --- /dev/null +++ b/CheatStengine/src/CheatStengine/UI/ImGui/IconCache.cpp @@ -0,0 +1,211 @@ +#include "IconCache.h" + +#include + +#include + +#include +#include + +#include +#include + +#pragma comment(lib, "Shell32.lib") + +IconCache::~IconCache() +{ + Shutdown(); +} + +void IconCache::Initialize(ID3D11Device* device) +{ + m_Device = device; +} + +void IconCache::Shutdown() +{ + for (auto& [pid, texture] : m_ProcessIcons) { + if (texture) { + reinterpret_cast(texture)->Release(); + } + } + m_ProcessIcons.clear(); + + if (m_FallbackIcon) { + reinterpret_cast(m_FallbackIcon)->Release(); + m_FallbackIcon = 0; + } +} + +uint64_t IconCache::GetProcessIcon(uint32_t pid) +{ + if (!m_Device) { + return 0; + } + + if (auto it = m_ProcessIcons.find(pid); it != m_ProcessIcons.end()) { + return it->second; + } + + std::wstring path = GetProcessPath(pid); + uint64_t texture = path.empty() ? 0 : CreateTextureFromPath(path); + + m_ProcessIcons.emplace(pid, texture); + return texture; +} + +uint64_t IconCache::GetWindowIcon(void* hwnd) +{ + DWORD pid = 0; + GetWindowThreadProcessId(static_cast(hwnd), &pid); + if (pid == 0) { + return 0; + } + return GetProcessIcon(pid); +} + +uint64_t IconCache::GetFallbackIcon() +{ + if (!m_Device || m_FallbackIcon) { + return m_FallbackIcon; + } + + SHFILEINFOW info {}; + if (SHGetFileInfoW(L"app.exe", FILE_ATTRIBUTE_NORMAL, &info, sizeof(info), + SHGFI_ICON | SHGFI_SMALLICON | SHGFI_USEFILEATTRIBUTES) + && info.hIcon) { + m_FallbackIcon = CreateTextureFromIcon(info.hIcon); + DestroyIcon(info.hIcon); + } + return m_FallbackIcon; +} + +uint64_t IconCache::CreateTextureFromPath(const std::wstring& path) +{ + SHFILEINFOW info {}; + if (!SHGetFileInfoW(path.c_str(), 0, &info, sizeof(info), SHGFI_ICON | SHGFI_SMALLICON) + || !info.hIcon) { + return 0; + } + + uint64_t texture = CreateTextureFromIcon(info.hIcon); + DestroyIcon(info.hIcon); + return texture; +} + +uint64_t IconCache::CreateTextureFromIcon(HICON icon) +{ + ICONINFO iconInfo {}; + if (!GetIconInfo(icon, &iconInfo)) { + return 0; + } + + BITMAP bmp {}; + if (GetObject(iconInfo.hbmColor, sizeof(BITMAP), &bmp) == 0) { + DeleteObject(iconInfo.hbmColor); + DeleteObject(iconInfo.hbmMask); + return 0; + } + + int width = bmp.bmWidth; + int height = bmp.bmHeight; + + BITMAPINFO bmi {}; + bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER); + bmi.bmiHeader.biWidth = width; + bmi.bmiHeader.biHeight = -height; + bmi.bmiHeader.biPlanes = 1; + bmi.bmiHeader.biBitCount = 32; + bmi.bmiHeader.biCompression = BI_RGB; + + std::vector pixels(static_cast(width) * height); + + HDC screenDc = GetDC(nullptr); + int scanlines = GetDIBits(screenDc, iconInfo.hbmColor, 0, height, pixels.data(), &bmi, DIB_RGB_COLORS); + + if (scanlines == 0) { + ReleaseDC(nullptr, screenDc); + DeleteObject(iconInfo.hbmColor); + DeleteObject(iconInfo.hbmMask); + return 0; + } + + bool hasAlpha = false; + for (uint32_t& pixel : pixels) { + if (pixel & 0xFF000000) { + hasAlpha = true; + } + uint32_t b = (pixel & 0x000000FF); + uint32_t r = (pixel & 0x00FF0000) >> 16; + pixel = (pixel & 0xFF00FF00) | (b << 16) | r; + } + if (!hasAlpha) { + std::vector mask(static_cast(width) * height); + if (GetDIBits(screenDc, iconInfo.hbmMask, 0, height, mask.data(), &bmi, DIB_RGB_COLORS) != 0) { + for (size_t i = 0; i < pixels.size(); ++i) { + bool transparent = (mask[i] & 0x00FFFFFF) != 0; + pixels[i] = transparent ? (pixels[i] & 0x00FFFFFF) : (pixels[i] | 0xFF000000); + } + } else { + for (uint32_t& pixel : pixels) { + pixel |= 0xFF000000; + } + } + } + + ReleaseDC(nullptr, screenDc); + DeleteObject(iconInfo.hbmColor); + DeleteObject(iconInfo.hbmMask); + + D3D11_TEXTURE2D_DESC desc {}; + desc.Width = width; + desc.Height = height; + desc.MipLevels = 1; + desc.ArraySize = 1; + desc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; + desc.SampleDesc.Count = 1; + desc.Usage = D3D11_USAGE_IMMUTABLE; + desc.BindFlags = D3D11_BIND_SHADER_RESOURCE; + + D3D11_SUBRESOURCE_DATA data {}; + data.pSysMem = pixels.data(); + data.SysMemPitch = width * 4; + + ID3D11Texture2D* tex = nullptr; + if (FAILED(m_Device->CreateTexture2D(&desc, &data, &tex)) || !tex) { + return 0; + } + + D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc {}; + srvDesc.Format = desc.Format; + srvDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D; + srvDesc.Texture2D.MipLevels = 1; + + ID3D11ShaderResourceView* srv = nullptr; + HRESULT hr = m_Device->CreateShaderResourceView(tex, &srvDesc, &srv); + tex->Release(); + + if (FAILED(hr) || !srv) { + return 0; + } + + return reinterpret_cast(srv); +} + +std::wstring IconCache::GetProcessPath(uint32_t pid) +{ + HANDLE handle = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid); + if (!handle) { + return {}; + } + + wchar_t buffer[MAX_PATH]; + DWORD size = MAX_PATH; + std::wstring path; + if (QueryFullProcessImageNameW(handle, 0, buffer, &size)) { + path.assign(buffer, size); + } + + CloseHandle(handle); + return path; +} diff --git a/CheatStengine/src/CheatStengine/UI/ImGui/IconCache.h b/CheatStengine/src/CheatStengine/UI/ImGui/IconCache.h new file mode 100644 index 0000000..c028688 --- /dev/null +++ b/CheatStengine/src/CheatStengine/UI/ImGui/IconCache.h @@ -0,0 +1,33 @@ +#pragma once + +#include +#include +#include + +struct ID3D11Device; +struct ID3D11ShaderResourceView; +struct HICON__; +using HICON = HICON__*; + +class IconCache { +public: + IconCache() = default; + ~IconCache(); + + void Initialize(ID3D11Device* device); + void Shutdown(); + + [[nodiscard]] uint64_t GetProcessIcon(uint32_t pid); + [[nodiscard]] uint64_t GetWindowIcon(void* hwnd); + [[nodiscard]] uint64_t GetFallbackIcon(); + +private: + [[nodiscard]] uint64_t CreateTextureFromPath(const std::wstring& path); + [[nodiscard]] uint64_t CreateTextureFromIcon(HICON icon); + [[nodiscard]] static std::wstring GetProcessPath(uint32_t pid); + + ID3D11Device* m_Device = nullptr; + + std::unordered_map m_ProcessIcons; + uint64_t m_FallbackIcon = 0; +}; diff --git a/CheatStengine/vendor/IconFontCppHeaders b/CheatStengine/vendor/IconFontCppHeaders new file mode 160000 index 0000000..210b5a3 --- /dev/null +++ b/CheatStengine/vendor/IconFontCppHeaders @@ -0,0 +1 @@ +Subproject commit 210b5a399a64270674560d633638952d1e8d804d diff --git a/CheatStengine/vendor/cpp-httplib b/CheatStengine/vendor/cpp-httplib new file mode 160000 index 0000000..a7bc00e --- /dev/null +++ b/CheatStengine/vendor/cpp-httplib @@ -0,0 +1 @@ +Subproject commit a7bc00e3307fecdb4d67545e93be7b88cfb1e186 diff --git a/CheatStengine/vendor/httplib.cmake b/CheatStengine/vendor/httplib.cmake new file mode 100644 index 0000000..103fa77 --- /dev/null +++ b/CheatStengine/vendor/httplib.cmake @@ -0,0 +1,14 @@ +set(CPP_HTTPLIB_DIR ${CMAKE_CURRENT_SOURCE_DIR}/vendor/cpp-httplib) + +if (NOT EXISTS ${CPP_HTTPLIB_DIR}) + include(FetchContent) + FetchContent_Declare( + httplib + GIT_REPOSITORY https://github.com/yhirose/cpp-httplib.git + GIT_TAG v0.18.3 + SOURCE_DIR ${CPP_HTTPLIB_DIR} + ) + FetchContent_MakeAvailable(httplib) +else () + add_subdirectory(${CPP_HTTPLIB_DIR}) +endif () diff --git a/CheatStengine/vendor/json b/CheatStengine/vendor/json new file mode 160000 index 0000000..9cca280 --- /dev/null +++ b/CheatStengine/vendor/json @@ -0,0 +1 @@ +Subproject commit 9cca280a4d0ccf0c08f47a99aa71d1b0e52f8d03 diff --git a/CheatStengine/vendor/json.cmake b/CheatStengine/vendor/json.cmake new file mode 100644 index 0000000..32ec554 --- /dev/null +++ b/CheatStengine/vendor/json.cmake @@ -0,0 +1,14 @@ +set(NLOHMANN_JSON_DIR ${CMAKE_CURRENT_SOURCE_DIR}/vendor/json) + +if (NOT EXISTS ${NLOHMANN_JSON_DIR}) + include(FetchContent) + FetchContent_Declare( + nlohmann_json + GIT_REPOSITORY https://github.com/nlohmann/json.git + GIT_TAG v3.11.3 + SOURCE_DIR ${NLOHMANN_JSON_DIR} + ) + FetchContent_MakeAvailable(nlohmann_json) +else () + add_subdirectory(${NLOHMANN_JSON_DIR}) +endif () diff --git a/CheatStengine/vendor/zasm b/CheatStengine/vendor/zasm new file mode 160000 index 0000000..813a187 --- /dev/null +++ b/CheatStengine/vendor/zasm @@ -0,0 +1 @@ +Subproject commit 813a1875d1f45d6c27d938eb10b5f96c05757a1b diff --git a/CheatStengineDriver/CMakeLists.txt b/CheatStengineDriver/CMakeLists.txt index ca615e5..fb3b33f 100644 --- a/CheatStengineDriver/CMakeLists.txt +++ b/CheatStengineDriver/CMakeLists.txt @@ -1,12 +1,63 @@ -project(CheatStengineDriver) +project(CheatStengineDriver LANGUAGES CXX) include(vendor/FindWDK.cmake) find_package(WDK REQUIRED) -file(GLOB_RECURSE SOURCE_FILES src/*.cpp) +set(DRIVER_WINVER 0x0A00) +set(DRIVER_NTDDI_VERSION 0x0A000008) + +if (MSVC) + string(REPLACE "/EHsc" "" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}") +endif () + wdk_add_driver(${PROJECT_NAME} - NTDDI_VERSION 0x0A000010 - ${SOURCE_FILES}) + WINVER ${DRIVER_WINVER} + NTDDI_VERSION ${DRIVER_NTDDI_VERSION} + src/CheatStengineDriver/Control.cpp + src/CheatStengineDriver/Control.h + src/CheatStengineDriver/Device.cpp + src/CheatStengineDriver/Device.h + src/CheatStengineDriver/Driver.cpp + src/CheatStengineDriver/Driver.h + src/CheatStengineDriver/Mapper.cpp + src/CheatStengineDriver/Mapper.h + src/CheatStengineDriver/Memory.cpp + src/CheatStengineDriver/Memory.h + src/CheatStengineDriver/Pnp.cpp + src/CheatStengineDriver/Pnp.h + src/CheatStengineDriver/Process.cpp + src/CheatStengineDriver/Process.h + Driver.inf + ../Shared/Protocol.h +) + +target_include_directories(${PROJECT_NAME} PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/../Shared +) + +target_compile_definitions(${PROJECT_NAME} PRIVATE + _KMODE + WINVER=${DRIVER_WINVER} +) + +target_compile_options(${PROJECT_NAME} PRIVATE + /W4 + /GR- + /EHs-c- +) + +target_link_libraries(${PROJECT_NAME} WDK::WDMSEC) + +get_target_property(DRIVER_LINK_FLAGS ${PROJECT_NAME} LINK_FLAGS) +string(REPLACE [=[/DRIVER ]=] [=[/DRIVER:WDM ]=] DRIVER_LINK_FLAGS ${DRIVER_LINK_FLAGS}) +string(REPLACE [=[/MERGE:_TEXT=.text;_PAGE=PAGE ]=] "" DRIVER_LINK_FLAGS ${DRIVER_LINK_FLAGS}) +string(REPLACE [=[/MERGE:_TEXT=.text_PAGE=PAGE ]=] "" DRIVER_LINK_FLAGS ${DRIVER_LINK_FLAGS}) +set_target_properties(${PROJECT_NAME} PROPERTIES LINK_FLAGS ${DRIVER_LINK_FLAGS}) +set_source_files_properties(Driver.inf PROPERTIES HEADER_FILE_ONLY TRUE) -target_compile_definitions(${PROJECT_NAME} PRIVATE _KMODE) +add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + ${CMAKE_CURRENT_SOURCE_DIR}/Driver.inf + $/Driver.inf +) diff --git a/CheatStengineDriver/Driver.inf b/CheatStengineDriver/Driver.inf new file mode 100644 index 0000000..de56726 --- /dev/null +++ b/CheatStengineDriver/Driver.inf @@ -0,0 +1,63 @@ +[Version] +Signature="$WINDOWS NT$" +Class=System +ClassGuid={4D36E97D-E325-11CE-BFC1-08002BE10318} +Provider=%ProviderName% +CatalogFile.NTamd64=Driver.cat +DriverVer=07/21/2026,1.0.0.0 +PnpLockdown=1 + +[Manufacturer] +%ManufacturerName%=Models,NTamd64.10.0...19041 + +[Models.NTamd64.10.0...19041] +%DeviceName%=Device_Install,Root\CheatStengine + +[DestinationDirs] +DriverCopyFiles=13 + +[SourceDisksNames] +1=%DiskName%,,, + +[SourceDisksFiles] +CheatStengineDriver.sys=1 + +[Device_Install.NT] +CopyFiles=DriverCopyFiles + +[Device_Install.NT.HW] +AddReg=DeviceSecurity + +[DeviceSecurity] +HKR,,DeviceCharacteristics,0x00010001,0x00000100 +HKR,,Security,0x00000000,"D:P(A;;GA;;;SY)(A;;GA;;;BA)" + +[Device_Install.NT.Interfaces] +AddInterface={8D3F3F2D-3D46-4A66-8F9B-46E6D5A67831},,Interface_Install + +[Interface_Install] +AddReg=InterfaceSecurity + +[InterfaceSecurity] +HKR,,Security,0x00000000,"D:P(A;;GA;;;SY)(A;;GA;;;BA)" + +[Device_Install.NT.Services] +AddService=CheatStengineDriver,0x00000002,Service_Install + +[Service_Install] +DisplayName=%ServiceName% +ServiceType=1 +StartType=3 +ErrorControl=1 +ServiceBinary=%13%\CheatStengineDriver.sys +Security="D:P(A;;GA;;;SY)(A;;GA;;;BA)" + +[DriverCopyFiles] +CheatStengineDriver.sys + +[Strings] +ProviderName="Cheat Stengine" +ManufacturerName="Cheat Stengine" +DeviceName="Cheat Stengine Device" +ServiceName="Cheat Stengine Driver" +DiskName="Cheat Stengine Installation Media" diff --git a/CheatStengineDriver/src/CheatStengineDriver/Control.cpp b/CheatStengineDriver/src/CheatStengineDriver/Control.cpp new file mode 100644 index 0000000..2c3079b --- /dev/null +++ b/CheatStengineDriver/src/CheatStengineDriver/Control.cpp @@ -0,0 +1,598 @@ +#include "Control.h" + +#include "Memory.h" +#include "Process.h" + +namespace { + +using Handler = NTSTATUS (*)( + DeviceExtension*, + PFILE_OBJECT, + PIRP, + PIO_STACK_LOCATION, + PULONG_PTR); + +struct Route { + ULONG Code; + Handler Function; + bool WakesDevice; +}; + +uint64_t GetCapabilities() +{ + uint64_t capabilities = Protocol::BindProcess + | Protocol::QueryMemory + | Protocol::AllocateMemory + | Protocol::FreeMemory + | Protocol::Ping + | Protocol::Heartbeat + | Protocol::Configure; + if (CanCopyProcessMemory()) { + capabilities |= Protocol::ReadMemory | Protocol::WriteMemory; + } + if (CanProtectProcessMemory()) { + capabilities |= Protocol::ProtectMemory; + } + return capabilities; +} + +template +NTSTATUS GetRequest(PIRP irp, ULONG inputLength, const T** request, bool negotiate = false) +{ + if (!request || !irp->AssociatedIrp.SystemBuffer || inputLength < sizeof(T)) { + return STATUS_BUFFER_TOO_SMALL; + } + + const T* packet = static_cast(irp->AssociatedIrp.SystemBuffer); + const Protocol::PacketHeader& header = packet->Header; + if (header.Size < sizeof(T) || header.Size > inputLength + || header.Flags != 0 || header.Reserved != 0) { + return STATUS_INVALID_PARAMETER; + } + if (header.MajorVersion != Protocol::VersionMajor + || (!negotiate && header.MinorVersion > Protocol::VersionMinor)) { + return STATUS_REVISION_MISMATCH; + } + + *request = packet; + return STATUS_SUCCESS; +} + +template +T* GetResponse(PIRP irp, ULONG outputLength, PULONG_PTR information) +{ + if (!irp->AssociatedIrp.SystemBuffer || outputLength < sizeof(T)) { + *information = sizeof(T); + return nullptr; + } + return static_cast(irp->AssociatedIrp.SystemBuffer); +} + +PVOID GetDirectBuffer(PIRP irp, ULONG length) +{ + if (!irp->MdlAddress || length == 0 || MmGetMdlByteCount(irp->MdlAddress) != length) { + return nullptr; + } + return MmGetSystemAddressForMdlSafe( + irp->MdlAddress, + static_cast(NormalPagePriority | MdlMappingNoExecute)); +} + +NTSTATUS HandleInformation( + DeviceExtension* extension, + PFILE_OBJECT, + PIRP irp, + PIO_STACK_LOCATION stack, + PULONG_PTR information) +{ + const Protocol::InformationRequest* request = nullptr; + NTSTATUS status = GetRequest( + irp, + stack->Parameters.DeviceIoControl.InputBufferLength, + &request, + true); + if (!NT_SUCCESS(status)) { + return status; + } + + Protocol::InformationResponse* response = GetResponse( + irp, + stack->Parameters.DeviceIoControl.OutputBufferLength, + information); + if (!response) { + return STATUS_BUFFER_TOO_SMALL; + } + + *response = {}; + response->Header = Protocol::CreateHeader(); + response->Capabilities = GetCapabilities(); + response->MaximumTransferSize = Protocol::MaximumTransferSize; + response->LoadMode = static_cast(extension->Mode); + *information = sizeof(*response); + return STATUS_SUCCESS; +} + +NTSTATUS HandlePing( + DeviceExtension* extension, + PFILE_OBJECT, + PIRP irp, + PIO_STACK_LOCATION stack, + PULONG_PTR information) +{ + const Protocol::PingRequest* requestPointer = nullptr; + NTSTATUS status = GetRequest( + irp, + stack->Parameters.DeviceIoControl.InputBufferLength, + &requestPointer); + if (!NT_SUCCESS(status)) { + return status; + } + const Protocol::PingRequest request = *requestPointer; + + Protocol::PingResponse* response = GetResponse( + irp, + stack->Parameters.DeviceIoControl.OutputBufferLength, + information); + if (!response) { + return STATUS_BUFFER_TOO_SMALL; + } + + *response = {}; + response->Header = Protocol::CreateHeader(); + response->Nonce = request.Nonce; + response->KernelTime = KeQueryInterruptTime(); + response->Capabilities = GetCapabilities(); + response->MaximumTransferSize = Protocol::MaximumTransferSize; + response->DriverMajorVersion = Protocol::DriverVersionMajor; + response->DriverMinorVersion = Protocol::DriverVersionMinor; + response->DriverPatchVersion = Protocol::DriverVersionPatch; + response->ActiveSessions = GetSessionCount(extension); + response->IdleTimeoutSeconds = GetIdleTimeout(extension); + response->RemainingIdleSeconds = GetRemainingIdleTime(extension); + response->LoadMode = static_cast(extension->Mode); + response->State = static_cast(GetRuntimeState(extension)); + *information = sizeof(*response); + return STATUS_SUCCESS; +} + +NTSTATUS HandleHeartbeat( + DeviceExtension* extension, + PFILE_OBJECT fileObject, + PIRP irp, + PIO_STACK_LOCATION stack, + PULONG_PTR information) +{ + const Protocol::HeartbeatRequest* requestPointer = nullptr; + NTSTATUS status = GetRequest( + irp, + stack->Parameters.DeviceIoControl.InputBufferLength, + &requestPointer); + if (!NT_SUCCESS(status)) { + return status; + } + const Protocol::HeartbeatRequest request = *requestPointer; + + Protocol::HeartbeatResponse* response = GetResponse( + irp, + stack->Parameters.DeviceIoControl.OutputBufferLength, + information); + if (!response) { + return STATUS_BUFFER_TOO_SMALL; + } + + Session* session = nullptr; + status = ReferenceSession(extension, fileObject, &session); + if (!NT_SUCCESS(status)) { + return status; + } + status = UpdateHeartbeat(session, request.Sequence); + ReleaseSession(session); + if (!NT_SUCCESS(status)) { + return status; + } + + TouchDevice(extension); + *response = {}; + response->Header = Protocol::CreateHeader(); + response->Sequence = request.Sequence; + response->KernelTime = KeQueryInterruptTime(); + response->RemainingIdleSeconds = GetRemainingIdleTime(extension); + response->State = static_cast(GetRuntimeState(extension)); + *information = sizeof(*response); + return STATUS_SUCCESS; +} + +NTSTATUS HandleConfigure( + DeviceExtension* extension, + PFILE_OBJECT, + PIRP irp, + PIO_STACK_LOCATION stack, + PULONG_PTR information) +{ + const Protocol::ConfigureRequest* requestPointer = nullptr; + NTSTATUS status = GetRequest( + irp, + stack->Parameters.DeviceIoControl.InputBufferLength, + &requestPointer); + if (!NT_SUCCESS(status) || requestPointer->Reserved != 0) { + return NT_SUCCESS(status) ? STATUS_INVALID_PARAMETER : status; + } + const Protocol::ConfigureRequest request = *requestPointer; + + Protocol::ConfigureResponse* response = GetResponse( + irp, + stack->Parameters.DeviceIoControl.OutputBufferLength, + information); + if (!response) { + return STATUS_BUFFER_TOO_SMALL; + } + + status = SetIdleTimeout(extension, request.IdleTimeoutSeconds); + if (!NT_SUCCESS(status)) { + return status; + } + + *response = {}; + response->Header = Protocol::CreateHeader(); + response->IdleTimeoutSeconds = GetIdleTimeout(extension); + *information = sizeof(*response); + return STATUS_SUCCESS; +} + +NTSTATUS HandleBind( + DeviceExtension* extension, + PFILE_OBJECT fileObject, + PIRP irp, + PIO_STACK_LOCATION stack, + PULONG_PTR) +{ + const Protocol::BindProcessRequest* request = nullptr; + NTSTATUS status = GetRequest( + irp, + stack->Parameters.DeviceIoControl.InputBufferLength, + &request); + if (!NT_SUCCESS(status)) { + return status; + } + if (request->ProcessId == 0 || request->Reserved != 0) { + return STATUS_INVALID_PARAMETER; + } + return BindSession(extension, fileObject, request->ProcessId); +} + +NTSTATUS HandleRead( + DeviceExtension* extension, + PFILE_OBJECT fileObject, + PIRP irp, + PIO_STACK_LOCATION stack, + PULONG_PTR information) +{ + const ULONG inputLength = stack->Parameters.DeviceIoControl.InputBufferLength; + const ULONG outputLength = stack->Parameters.DeviceIoControl.OutputBufferLength; + const Protocol::ReadMemoryRequest* request = nullptr; + NTSTATUS status = GetRequest(irp, inputLength, &request); + if (!NT_SUCCESS(status)) { + return status; + } + if (request->Size == 0 || request->Size > Protocol::MaximumTransferSize + || request->Size != outputLength) { + return STATUS_INVALID_BUFFER_SIZE; + } + + PVOID buffer = GetDirectBuffer(irp, outputLength); + if (!buffer) { + return STATUS_INSUFFICIENT_RESOURCES; + } + + Session* session = nullptr; + PEPROCESS process = nullptr; + status = ReferenceSessionProcess(extension, fileObject, &session, &process); + if (!NT_SUCCESS(status)) { + return status; + } + + SIZE_T transferred = 0; + status = ReadProcessMemory( + process, + request->Address, + buffer, + static_cast(request->Size), + &transferred); + ReleaseSession(session); + *information = transferred; + return status; +} + +NTSTATUS HandleWrite( + DeviceExtension* extension, + PFILE_OBJECT fileObject, + PIRP irp, + PIO_STACK_LOCATION stack, + PULONG_PTR information) +{ + const ULONG inputLength = stack->Parameters.DeviceIoControl.InputBufferLength; + const ULONG outputLength = stack->Parameters.DeviceIoControl.OutputBufferLength; + const Protocol::WriteMemoryRequest* request = nullptr; + NTSTATUS status = GetRequest(irp, inputLength, &request); + if (!NT_SUCCESS(status)) { + return status; + } + if (request->Size == 0 || request->Size > Protocol::MaximumTransferSize + || request->Size != outputLength) { + return STATUS_INVALID_BUFFER_SIZE; + } + + PVOID buffer = GetDirectBuffer(irp, outputLength); + if (!buffer) { + return STATUS_INSUFFICIENT_RESOURCES; + } + + Session* session = nullptr; + PEPROCESS process = nullptr; + status = ReferenceSessionProcess(extension, fileObject, &session, &process); + if (!NT_SUCCESS(status)) { + return status; + } + + SIZE_T transferred = 0; + status = WriteProcessMemory( + process, + request->Address, + buffer, + static_cast(request->Size), + &transferred); + ReleaseSession(session); + *information = transferred; + return status; +} + +NTSTATUS HandleQuery( + DeviceExtension* extension, + PFILE_OBJECT fileObject, + PIRP irp, + PIO_STACK_LOCATION stack, + PULONG_PTR information) +{ + const Protocol::QueryMemoryRequest* requestPointer = nullptr; + NTSTATUS status = GetRequest( + irp, + stack->Parameters.DeviceIoControl.InputBufferLength, + &requestPointer); + if (!NT_SUCCESS(status)) { + return status; + } + const Protocol::QueryMemoryRequest request = *requestPointer; + + Protocol::QueryMemoryResponse* response = GetResponse( + irp, + stack->Parameters.DeviceIoControl.OutputBufferLength, + information); + if (!response) { + return STATUS_BUFFER_TOO_SMALL; + } + + Session* session = nullptr; + PEPROCESS process = nullptr; + status = ReferenceSessionProcess(extension, fileObject, &session, &process); + if (!NT_SUCCESS(status)) { + return status; + } + + MEMORY_BASIC_INFORMATION memoryInformation {}; + status = QueryProcessMemory(process, request.Address, &memoryInformation); + ReleaseSession(session); + if (!NT_SUCCESS(status)) { + return status; + } + + *response = {}; + response->Header = Protocol::CreateHeader(); + response->BaseAddress = reinterpret_cast(memoryInformation.BaseAddress); + response->AllocationBase = reinterpret_cast(memoryInformation.AllocationBase); + response->RegionSize = memoryInformation.RegionSize; + response->AllocationProtect = memoryInformation.AllocationProtect; + response->State = memoryInformation.State; + response->Protect = memoryInformation.Protect; + response->Type = memoryInformation.Type; + *information = sizeof(*response); + return STATUS_SUCCESS; +} + +NTSTATUS HandleAllocate( + DeviceExtension* extension, + PFILE_OBJECT fileObject, + PIRP irp, + PIO_STACK_LOCATION stack, + PULONG_PTR information) +{ + const Protocol::AllocateMemoryRequest* requestPointer = nullptr; + NTSTATUS status = GetRequest( + irp, + stack->Parameters.DeviceIoControl.InputBufferLength, + &requestPointer); + if (!NT_SUCCESS(status)) { + return status; + } + const Protocol::AllocateMemoryRequest request = *requestPointer; + + Protocol::AllocateMemoryResponse* response = GetResponse( + irp, + stack->Parameters.DeviceIoControl.OutputBufferLength, + information); + if (!response) { + return STATUS_BUFFER_TOO_SMALL; + } + + Session* session = nullptr; + PEPROCESS process = nullptr; + status = ReferenceSessionProcess(extension, fileObject, &session, &process); + if (!NT_SUCCESS(status)) { + return status; + } + + UINT64 address = 0; + UINT64 size = 0; + status = AllocateProcessMemory( + process, + request.Address, + request.Size, + request.AllocationType, + request.Protect, + &address, + &size); + ReleaseSession(session); + if (!NT_SUCCESS(status)) { + return status; + } + + *response = {}; + response->Header = Protocol::CreateHeader(); + response->Address = address; + response->Size = size; + *information = sizeof(*response); + return STATUS_SUCCESS; +} + +NTSTATUS HandleFree( + DeviceExtension* extension, + PFILE_OBJECT fileObject, + PIRP irp, + PIO_STACK_LOCATION stack, + PULONG_PTR) +{ + const Protocol::FreeMemoryRequest* request = nullptr; + NTSTATUS status = GetRequest( + irp, + stack->Parameters.DeviceIoControl.InputBufferLength, + &request); + if (!NT_SUCCESS(status)) { + return status; + } + if (request->Reserved != 0) { + return STATUS_INVALID_PARAMETER; + } + + Session* session = nullptr; + PEPROCESS process = nullptr; + status = ReferenceSessionProcess(extension, fileObject, &session, &process); + if (!NT_SUCCESS(status)) { + return status; + } + status = FreeProcessMemory( + process, + request->Address, + request->Size, + request->FreeType); + ReleaseSession(session); + return status; +} + +NTSTATUS HandleProtect( + DeviceExtension* extension, + PFILE_OBJECT fileObject, + PIRP irp, + PIO_STACK_LOCATION stack, + PULONG_PTR information) +{ + if (!CanProtectProcessMemory()) { + return STATUS_NOT_SUPPORTED; + } + + const Protocol::ProtectMemoryRequest* requestPointer = nullptr; + NTSTATUS status = GetRequest( + irp, + stack->Parameters.DeviceIoControl.InputBufferLength, + &requestPointer); + if (!NT_SUCCESS(status) || requestPointer->Reserved != 0) { + return NT_SUCCESS(status) ? STATUS_INVALID_PARAMETER : status; + } + const Protocol::ProtectMemoryRequest request = *requestPointer; + + Protocol::ProtectMemoryResponse* response = GetResponse( + irp, + stack->Parameters.DeviceIoControl.OutputBufferLength, + information); + if (!response) { + return STATUS_BUFFER_TOO_SMALL; + } + + Session* session = nullptr; + PEPROCESS process = nullptr; + status = ReferenceSessionProcess(extension, fileObject, &session, &process); + if (!NT_SUCCESS(status)) { + return status; + } + + ULONG oldProtect = 0; + UINT64 address = 0; + UINT64 size = 0; + status = ProtectProcessMemory( + process, + request.Address, + request.Size, + request.Protect, + &oldProtect, + &address, + &size); + ReleaseSession(session); + if (!NT_SUCCESS(status)) { + return status; + } + + *response = {}; + response->Header = Protocol::CreateHeader(); + response->Address = address; + response->Size = size; + response->OldProtect = oldProtect; + *information = sizeof(*response); + return STATUS_SUCCESS; +} + +const Route Routes[] = { + { Protocol::InformationControlCode, HandleInformation, true }, + { Protocol::PingControlCode, HandlePing, true }, + { Protocol::HeartbeatControlCode, HandleHeartbeat, true }, + { Protocol::ConfigureControlCode, HandleConfigure, true }, + { Protocol::BindProcessControlCode, HandleBind, false }, + { Protocol::ReadMemoryControlCode, HandleRead, false }, + { Protocol::WriteMemoryControlCode, HandleWrite, false }, + { Protocol::QueryMemoryControlCode, HandleQuery, false }, + { Protocol::AllocateMemoryControlCode, HandleAllocate, false }, + { Protocol::FreeMemoryControlCode, HandleFree, false }, + { Protocol::ProtectMemoryControlCode, HandleProtect, false }, +}; + +} + +NTSTATUS DispatchControlRequest( + DeviceExtension* extension, + PFILE_OBJECT fileObject, + PIRP irp, + PIO_STACK_LOCATION stack, + ULONG_PTR* information) +{ + const ULONG code = stack->Parameters.DeviceIoControl.IoControlCode; + for (const Route& route : Routes) { + if (route.Code != code) { + continue; + } + if (GetRuntimeState(extension) == Protocol::RuntimeState::Idle + && !route.WakesDevice) { + return STATUS_DEVICE_NOT_READY; + } + if (route.WakesDevice) { + TouchDevice(extension); + } + const NTSTATUS status = route.Function( + extension, + fileObject, + irp, + stack, + information); + if (NT_SUCCESS(status)) { + TouchDevice(extension); + } + return status; + } + return STATUS_INVALID_DEVICE_REQUEST; +} diff --git a/CheatStengineDriver/src/CheatStengineDriver/Control.h b/CheatStengineDriver/src/CheatStengineDriver/Control.h new file mode 100644 index 0000000..07e7068 --- /dev/null +++ b/CheatStengineDriver/src/CheatStengineDriver/Control.h @@ -0,0 +1,10 @@ +#pragma once + +#include "Device.h" + +NTSTATUS DispatchControlRequest( + DeviceExtension* extension, + PFILE_OBJECT fileObject, + PIRP irp, + PIO_STACK_LOCATION stack, + ULONG_PTR* information); diff --git a/CheatStengineDriver/src/CheatStengineDriver/Device.cpp b/CheatStengineDriver/src/CheatStengineDriver/Device.cpp new file mode 100644 index 0000000..b6ae396 --- /dev/null +++ b/CheatStengineDriver/src/CheatStengineDriver/Device.cpp @@ -0,0 +1,478 @@ +#include "Device.h" + +#include "Control.h" + +namespace { + +constexpr ULONG PoolTag = 'sSeC'; +constexpr LONG64 TimeUnitsPerSecond = 10'000'000; + +VOID IdleTimerCallback(PEX_TIMER, PVOID context) +{ + DeviceExtension* extension = static_cast(context); + KIRQL oldIrql = PASSIVE_LEVEL; + KeAcquireSpinLock(&extension->IdleLock, &oldIrql); + const LONG64 deadline = InterlockedCompareExchange64(&extension->IdleDeadline, 0, 0); + if (deadline == 0 + || static_cast(KeQueryInterruptTime()) < deadline + || GetDeviceState(extension) != DeviceState::Started) { + KeReleaseSpinLock(&extension->IdleLock, oldIrql); + return; + } + + InterlockedCompareExchange( + &extension->Runtime, + static_cast(Protocol::RuntimeState::Idle), + static_cast(Protocol::RuntimeState::Ready)); + InterlockedCompareExchange64(&extension->IdleDeadline, 0, deadline); + KeReleaseSpinLock(&extension->IdleLock, oldIrql); +} + +Session* TakeSession(DeviceExtension* extension, PFILE_OBJECT fileObject) +{ + if (!fileObject) { + return nullptr; + } + + ExAcquireFastMutex(&extension->SessionLock); + Session* session = static_cast(InterlockedExchangePointer( + reinterpret_cast(&fileObject->FsContext), + nullptr)); + ExReleaseFastMutex(&extension->SessionLock); + return session; +} + +void DestroySession(Session* session) +{ + if (!session) { + return; + } + + ExWaitForRundownProtectionRelease(&session->Rundown); + PEPROCESS process = static_cast(InterlockedExchangePointer( + reinterpret_cast(&session->Process), + nullptr)); + if (process) { + ObDereferenceObject(process); + } + + DeviceExtension* extension = session->Device; + InterlockedDecrement(&extension->SessionCount); + TouchDevice(extension); + ExFreePoolWithTag(session, PoolTag); +} + +} + +DeviceExtension* GetExtension(PDEVICE_OBJECT deviceObject) +{ + return static_cast(deviceObject->DeviceExtension); +} + +DeviceState GetDeviceState(DeviceExtension* extension) +{ + return static_cast(InterlockedCompareExchange(&extension->State, 0, 0)); +} + +void SetDeviceState(DeviceExtension* extension, DeviceState state) +{ + InterlockedExchange(&extension->State, static_cast(state)); +} + +NTSTATUS InitializeDevice(PDEVICE_OBJECT deviceObject, Protocol::LoadMode mode, DeviceState state) +{ + DeviceExtension* extension = GetExtension(deviceObject); + RtlZeroMemory(extension, sizeof(*extension)); + extension->DeviceObject = deviceObject; + extension->Mode = mode; + extension->State = static_cast(state); + extension->Runtime = static_cast( + state == DeviceState::Started + ? Protocol::RuntimeState::Ready + : Protocol::RuntimeState::Starting); + extension->IdleTimeoutSeconds = Protocol::DefaultIdleTimeoutSeconds; + + IoInitializeRemoveLock(&extension->RemoveLock, PoolTag, 0, 0); + ExInitializeRundownProtection(&extension->IoRundown); + ExInitializeFastMutex(&extension->SessionLock); + KeInitializeSpinLock(&extension->IdleLock); + extension->IdleTimer = ExAllocateTimer(IdleTimerCallback, extension, EX_TIMER_NO_WAKE); + if (!extension->IdleTimer) { + return STATUS_INSUFFICIENT_RESOURCES; + } + + extension->LastActivity = static_cast(KeQueryInterruptTime()); + if (state == DeviceState::Started) { + TouchDevice(extension); + } + return STATUS_SUCCESS; +} + +void DestroyDevice(DeviceExtension* extension) +{ + SetDeviceState(extension, DeviceState::Removed); + KIRQL oldIrql = PASSIVE_LEVEL; + KeAcquireSpinLock(&extension->IdleLock, &oldIrql); + InterlockedExchange( + &extension->Runtime, + static_cast(Protocol::RuntimeState::Removed)); + InterlockedExchange64(&extension->IdleDeadline, 0); + PEX_TIMER timer = extension->IdleTimer; + extension->IdleTimer = nullptr; + KeReleaseSpinLock(&extension->IdleLock, oldIrql); + + if (timer) { + ExDeleteTimer(timer, TRUE, TRUE, nullptr); + } + + if (InterlockedCompareExchange(&extension->IoDrained, 1, 0) == 0) { + ExWaitForRundownProtectionRelease(&extension->IoRundown); + } +} + +void StartDevice(DeviceExtension* extension) +{ + if (InterlockedCompareExchange(&extension->IoDrained, 0, 1) == 1) { + ExReInitializeRundownProtection(&extension->IoRundown); + } + KIRQL oldIrql = PASSIVE_LEVEL; + KeAcquireSpinLock(&extension->IdleLock, &oldIrql); + InterlockedExchange( + &extension->Runtime, + static_cast(Protocol::RuntimeState::Ready)); + KeReleaseSpinLock(&extension->IdleLock, oldIrql); + TouchDevice(extension); +} + +void StopDevice(DeviceExtension* extension) +{ + KIRQL oldIrql = PASSIVE_LEVEL; + KeAcquireSpinLock(&extension->IdleLock, &oldIrql); + InterlockedExchange( + &extension->Runtime, + static_cast(Protocol::RuntimeState::Stopping)); + InterlockedExchange64(&extension->IdleDeadline, 0); + if (extension->IdleTimer) { + ExCancelTimer(extension->IdleTimer, nullptr); + } + KeReleaseSpinLock(&extension->IdleLock, oldIrql); + if (InterlockedCompareExchange(&extension->IoDrained, 1, 0) == 0) { + ExWaitForRundownProtectionRelease(&extension->IoRundown); + } +} + +bool AcquireDeviceIo(DeviceExtension* extension) +{ + if (!ExAcquireRundownProtection(&extension->IoRundown)) { + return false; + } + if (GetDeviceState(extension) != DeviceState::Started) { + ExReleaseRundownProtection(&extension->IoRundown); + return false; + } + return true; +} + +void ReleaseDeviceIo(DeviceExtension* extension) +{ + ExReleaseRundownProtection(&extension->IoRundown); +} + +void TouchDevice(DeviceExtension* extension) +{ + KIRQL oldIrql = PASSIVE_LEVEL; + KeAcquireSpinLock(&extension->IdleLock, &oldIrql); + if (GetDeviceState(extension) != DeviceState::Started || !extension->IdleTimer) { + KeReleaseSpinLock(&extension->IdleLock, oldIrql); + return; + } + + const ULONG seconds = static_cast(InterlockedCompareExchange( + &extension->IdleTimeoutSeconds, + 0, + 0)); + const LONG64 now = static_cast(KeQueryInterruptTime()); + InterlockedExchange64(&extension->LastActivity, now); + InterlockedExchange( + &extension->Runtime, + static_cast(Protocol::RuntimeState::Ready)); + + if (seconds == 0) { + InterlockedExchange64(&extension->IdleDeadline, 0); + ExCancelTimer(extension->IdleTimer, nullptr); + KeReleaseSpinLock(&extension->IdleLock, oldIrql); + return; + } + + const LONG64 duration = static_cast(seconds) * TimeUnitsPerSecond; + InterlockedExchange64(&extension->IdleDeadline, now + duration); + ExSetTimer(extension->IdleTimer, -duration, 0, nullptr); + KeReleaseSpinLock(&extension->IdleLock, oldIrql); +} + +NTSTATUS SetIdleTimeout(DeviceExtension* extension, ULONG seconds) +{ + if (seconds != 0 + && (seconds < Protocol::MinimumIdleTimeoutSeconds + || seconds > Protocol::MaximumIdleTimeoutSeconds)) { + return STATUS_INVALID_PARAMETER; + } + + InterlockedExchange(&extension->IdleTimeoutSeconds, static_cast(seconds)); + TouchDevice(extension); + return STATUS_SUCCESS; +} + +ULONG GetIdleTimeout(DeviceExtension* extension) +{ + return static_cast(InterlockedCompareExchange( + &extension->IdleTimeoutSeconds, + 0, + 0)); +} + +ULONG GetRemainingIdleTime(DeviceExtension* extension) +{ + const LONG64 deadline = InterlockedCompareExchange64(&extension->IdleDeadline, 0, 0); + if (deadline == 0) { + return 0; + } + + const LONG64 now = static_cast(KeQueryInterruptTime()); + if (now >= deadline) { + return 0; + } + + const ULONGLONG remaining = static_cast(deadline - now); + return static_cast((remaining + TimeUnitsPerSecond - 1) / TimeUnitsPerSecond); +} + +Protocol::RuntimeState GetRuntimeState(DeviceExtension* extension) +{ + const DeviceState state = GetDeviceState(extension); + if (state == DeviceState::Removed) { + return Protocol::RuntimeState::Removed; + } + if (state != DeviceState::Started) { + return state == DeviceState::Added + ? Protocol::RuntimeState::Starting + : Protocol::RuntimeState::Stopping; + } + return static_cast( + InterlockedCompareExchange(&extension->Runtime, 0, 0)); +} + +ULONG GetSessionCount(DeviceExtension* extension) +{ + return static_cast(InterlockedCompareExchange(&extension->SessionCount, 0, 0)); +} + +NTSTATUS ReferenceSession(DeviceExtension* extension, PFILE_OBJECT fileObject, Session** session) +{ + if (!extension || !fileObject || !session) { + return STATUS_INVALID_PARAMETER; + } + + ExAcquireFastMutex(&extension->SessionLock); + Session* context = static_cast(fileObject->FsContext); + const bool acquired = context && ExAcquireRundownProtection(&context->Rundown); + ExReleaseFastMutex(&extension->SessionLock); + if (!acquired) { + return STATUS_FILE_CLOSED; + } + + *session = context; + return STATUS_SUCCESS; +} + +void ReleaseSession(Session* session) +{ + ExReleaseRundownProtection(&session->Rundown); +} + +NTSTATUS BindSession( + DeviceExtension* extension, + PFILE_OBJECT fileObject, + ULONG processId) +{ + if (processId == 0) { + return STATUS_INVALID_PARAMETER; + } + + Session* session = nullptr; + NTSTATUS status = ReferenceSession(extension, fileObject, &session); + if (!NT_SUCCESS(status)) { + return status; + } + + PEPROCESS process = nullptr; + status = PsLookupProcessByProcessId( + reinterpret_cast(static_cast(processId)), + &process); + if (NT_SUCCESS(status)) { + PVOID previous = InterlockedCompareExchangePointer( + reinterpret_cast(&session->Process), + process, + nullptr); + if (previous) { + ObDereferenceObject(process); + status = STATUS_ALREADY_REGISTERED; + } + } + + ReleaseSession(session); + if (NT_SUCCESS(status)) { + TouchDevice(extension); + } + return status; +} + +NTSTATUS ReferenceSessionProcess( + DeviceExtension* extension, + PFILE_OBJECT fileObject, + Session** session, + PEPROCESS* process) +{ + if (!process) { + return STATUS_INVALID_PARAMETER; + } + + NTSTATUS status = ReferenceSession(extension, fileObject, session); + if (!NT_SUCCESS(status)) { + return status; + } + + PEPROCESS boundProcess = static_cast(InterlockedCompareExchangePointer( + reinterpret_cast(&(*session)->Process), + nullptr, + nullptr)); + if (!boundProcess) { + ReleaseSession(*session); + *session = nullptr; + return STATUS_INVALID_DEVICE_STATE; + } + + *process = boundProcess; + return STATUS_SUCCESS; +} + +NTSTATUS UpdateHeartbeat(Session* session, UINT64 sequence) +{ + if (sequence == 0 || sequence > MAXLONG64) { + return STATUS_INVALID_PARAMETER; + } + + LONG64 current = InterlockedCompareExchange64(&session->HeartbeatSequence, 0, 0); + while (sequence > static_cast(current)) { + const LONG64 previous = InterlockedCompareExchange64( + &session->HeartbeatSequence, + static_cast(sequence), + current); + if (previous == current) { + return STATUS_SUCCESS; + } + current = previous; + } + return STATUS_INVALID_PARAMETER; +} + +NTSTATUS DispatchCreate(PDEVICE_OBJECT deviceObject, PIRP irp) +{ + DeviceExtension* extension = GetExtension(deviceObject); + NTSTATUS status = IoAcquireRemoveLock(&extension->RemoveLock, irp); + if (!NT_SUCCESS(status)) { + return CompleteIrp(irp, status); + } + + const bool ioAcquired = AcquireDeviceIo(extension); + if (!ioAcquired) { + status = STATUS_INVALID_DEVICE_STATE; + } else { + PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(irp); + if (!stack->FileObject) { + status = STATUS_INVALID_PARAMETER; + } else { + ExAcquireFastMutex(&extension->SessionLock); + if (stack->FileObject->FsContext) { + status = STATUS_INVALID_DEVICE_STATE; + } else { + Session* session = static_cast(ExAllocatePool2( + POOL_FLAG_NON_PAGED, + sizeof(Session), + PoolTag)); + if (!session) { + status = STATUS_INSUFFICIENT_RESOURCES; + } else { + RtlZeroMemory(session, sizeof(*session)); + ExInitializeRundownProtection(&session->Rundown); + session->Device = extension; + stack->FileObject->FsContext = session; + InterlockedIncrement(&extension->SessionCount); + status = STATUS_SUCCESS; + } + } + ExReleaseFastMutex(&extension->SessionLock); + } + ReleaseDeviceIo(extension); + } + + if (NT_SUCCESS(status)) { + TouchDevice(extension); + } + IoReleaseRemoveLock(&extension->RemoveLock, irp); + return CompleteIrp(irp, status); +} + +NTSTATUS DispatchCleanup(PDEVICE_OBJECT deviceObject, PIRP irp) +{ + DeviceExtension* extension = GetExtension(deviceObject); + const NTSTATUS lockStatus = IoAcquireRemoveLock(&extension->RemoveLock, irp); + PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(irp); + DestroySession(TakeSession(extension, stack->FileObject)); + if (NT_SUCCESS(lockStatus)) { + IoReleaseRemoveLock(&extension->RemoveLock, irp); + } + return CompleteIrp(irp, STATUS_SUCCESS); +} + +NTSTATUS DispatchClose(PDEVICE_OBJECT deviceObject, PIRP irp) +{ + DeviceExtension* extension = GetExtension(deviceObject); + const NTSTATUS lockStatus = IoAcquireRemoveLock(&extension->RemoveLock, irp); + PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(irp); + DestroySession(TakeSession(extension, stack->FileObject)); + if (NT_SUCCESS(lockStatus)) { + IoReleaseRemoveLock(&extension->RemoveLock, irp); + } + return CompleteIrp(irp, STATUS_SUCCESS); +} + +NTSTATUS DispatchDeviceControl(PDEVICE_OBJECT deviceObject, PIRP irp) +{ + DeviceExtension* extension = GetExtension(deviceObject); + NTSTATUS status = IoAcquireRemoveLock(&extension->RemoveLock, irp); + if (!NT_SUCCESS(status)) { + return CompleteIrp(irp, status); + } + + ULONG_PTR information = 0; + if (!AcquireDeviceIo(extension)) { + status = STATUS_INVALID_DEVICE_STATE; + } else { + PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(irp); + if (!stack->FileObject) { + status = STATUS_INVALID_PARAMETER; + } else { + status = DispatchControlRequest( + extension, + stack->FileObject, + irp, + stack, + &information); + } + ReleaseDeviceIo(extension); + } + + IoReleaseRemoveLock(&extension->RemoveLock, irp); + return CompleteIrp(irp, status, information); +} diff --git a/CheatStengineDriver/src/CheatStengineDriver/Device.h b/CheatStengineDriver/src/CheatStengineDriver/Device.h new file mode 100644 index 0000000..b1a13d4 --- /dev/null +++ b/CheatStengineDriver/src/CheatStengineDriver/Device.h @@ -0,0 +1,82 @@ +#pragma once + +#include "Driver.h" + +#include + +enum class DeviceState : LONG { + Added, + Started, + StopPending, + Stopped, + RemovePending, + SurpriseRemoved, + Removed, +}; + +struct DeviceExtension { + PDEVICE_OBJECT DeviceObject; + PDEVICE_OBJECT LowerDeviceObject; + PDEVICE_OBJECT PhysicalDeviceObject; + IO_REMOVE_LOCK RemoveLock; + EX_RUNDOWN_REF IoRundown; + FAST_MUTEX SessionLock; + KSPIN_LOCK IdleLock; + UNICODE_STRING InterfaceName; + PEX_TIMER IdleTimer; + volatile LONG State; + volatile LONG IoDrained; + volatile LONG Runtime; + volatile LONG SessionCount; + volatile LONG IdleTimeoutSeconds; + volatile LONG64 LastActivity; + volatile LONG64 IdleDeadline; + Protocol::LoadMode Mode; + BOOLEAN InterfaceEnabled; +}; + +struct Session { + EX_RUNDOWN_REF Rundown; + DeviceExtension* Device; + PEPROCESS Process; + volatile LONG64 HeartbeatSequence; +}; + +DeviceExtension* GetExtension(PDEVICE_OBJECT deviceObject); +DeviceState GetDeviceState(DeviceExtension* extension); +void SetDeviceState(DeviceExtension* extension, DeviceState state); + +NTSTATUS InitializeDevice(PDEVICE_OBJECT deviceObject, Protocol::LoadMode mode, DeviceState state); +void DestroyDevice(DeviceExtension* extension); +void StartDevice(DeviceExtension* extension); +void StopDevice(DeviceExtension* extension); +bool AcquireDeviceIo(DeviceExtension* extension); +void ReleaseDeviceIo(DeviceExtension* extension); +void TouchDevice(DeviceExtension* extension); +NTSTATUS SetIdleTimeout(DeviceExtension* extension, ULONG seconds); +ULONG GetIdleTimeout(DeviceExtension* extension); +ULONG GetRemainingIdleTime(DeviceExtension* extension); +Protocol::RuntimeState GetRuntimeState(DeviceExtension* extension); +ULONG GetSessionCount(DeviceExtension* extension); + +NTSTATUS BindSession(DeviceExtension* extension, PFILE_OBJECT fileObject, ULONG processId); +NTSTATUS ReferenceSessionProcess( + DeviceExtension* extension, + PFILE_OBJECT fileObject, + Session** session, + PEPROCESS* process); +NTSTATUS ReferenceSession(DeviceExtension* extension, PFILE_OBJECT fileObject, Session** session); +void ReleaseSession(Session* session); +NTSTATUS UpdateHeartbeat(Session* session, UINT64 sequence); + +_Dispatch_type_(IRP_MJ_CREATE) +DRIVER_DISPATCH DispatchCreate; + +_Dispatch_type_(IRP_MJ_CLEANUP) +DRIVER_DISPATCH DispatchCleanup; + +_Dispatch_type_(IRP_MJ_CLOSE) +DRIVER_DISPATCH DispatchClose; + +_Dispatch_type_(IRP_MJ_DEVICE_CONTROL) +DRIVER_DISPATCH DispatchDeviceControl; diff --git a/CheatStengineDriver/src/CheatStengineDriver/Driver.cpp b/CheatStengineDriver/src/CheatStengineDriver/Driver.cpp index 929cce8..1214c9e 100644 --- a/CheatStengineDriver/src/CheatStengineDriver/Driver.cpp +++ b/CheatStengineDriver/src/CheatStengineDriver/Driver.cpp @@ -1,202 +1,70 @@ -#include "driver.h" -#include "process.h" +#include "Driver.h" -static PDEVICE_OBJECT g_DeviceObject = nullptr; -static UNICODE_STRING g_DevName {}; -static UNICODE_STRING g_SymLink {}; +#include "Device.h" +#include "Mapper.h" +#include "Memory.h" +#include "Pnp.h" +#include "Process.h" -static NTSTATUS CreateClose(PDEVICE_OBJECT deviceObject, PIRP irp) +namespace { + +NTSTATUS DispatchUnsupported(PDEVICE_OBJECT, PIRP irp) { - UNREFERENCED_PARAMETER(deviceObject); + return CompleteIrp(irp, STATUS_INVALID_DEVICE_REQUEST); +} - irp->IoStatus.Status = STATUS_SUCCESS; - irp->IoStatus.Information = 0; - IoCompleteRequest(irp, IO_NO_INCREMENT); - return STATUS_SUCCESS; } -static NTSTATUS DeviceControl(PDEVICE_OBJECT deviceObject, PIRP irp) +NTSTATUS CompleteIrp(PIRP irp, NTSTATUS status, ULONG_PTR information) { - UNREFERENCED_PARAMETER(deviceObject); - - PIO_STACK_LOCATION irpSp = IoGetCurrentIrpStackLocation(irp); - - const ULONG ioCode = irpSp->Parameters.DeviceIoControl.IoControlCode; - if (ioCode != IOCTL_CS_COMMAND) { - DbgPrint("[CheatStengine] Unknown IOCTL: 0x%08X\n", ioCode); - irp->IoStatus.Status = STATUS_INVALID_DEVICE_REQUEST; - irp->IoStatus.Information = 0; - IoCompleteRequest(irp, IO_NO_INCREMENT); - return STATUS_INVALID_DEVICE_REQUEST; - } - - const ULONG inputLen = irpSp->Parameters.DeviceIoControl.InputBufferLength; - const ULONG outputLen = irpSp->Parameters.DeviceIoControl.OutputBufferLength; - if (inputLen < sizeof(CommandHeader) || !irp->AssociatedIrp.SystemBuffer) { - DbgPrint("[CheatStengine] Buffer too small or null (%lu bytes)\n", inputLen); - irp->IoStatus.Status = STATUS_BUFFER_TOO_SMALL; - IoCompleteRequest(irp, IO_NO_INCREMENT); - return STATUS_BUFFER_TOO_SMALL; - } - - CommandHeader* cmd = static_cast(irp->AssociatedIrp.SystemBuffer); - DbgPrint("[CheatStengine] Command type=%d PID=%lu\n", cmd->Type, cmd->Pid); - - // Resolve target process. - PEPROCESS targetProcess = nullptr; - NTSTATUS status = PsLookupProcessByProcessId(reinterpret_cast(cmd->Pid), &targetProcess); - if (!NT_SUCCESS(status)) { - DbgPrint("[CheatStengine] PsLookupProcessByProcessId failed for PID %lu: 0x%08X\n", - cmd->Pid, status); - irp->IoStatus.Status = STATUS_INVALID_PARAMETER; - IoCompleteRequest(irp, IO_NO_INCREMENT); - return STATUS_INVALID_PARAMETER; - } - - ULONG bytesReturned = 0; - - // Dispatch command. - switch (cmd->Type) { - case CommandHeader::ReadMemory: - status = ReadProcessMemory( - targetProcess, - cmd->ReadMemoryData.Address, - cmd->ReadMemoryData.Buffer, - cmd->ReadMemoryData.Size); - break; - - case CommandHeader::WriteMemory: - status = WriteProcessMemory( - targetProcess, - cmd->WriteMemoryData.Address, - cmd->WriteMemoryData.Buffer, - cmd->WriteMemoryData.Size); - break; - - case CommandHeader::QueryMemory: - if (outputLen != sizeof(MEMORY_BASIC_INFORMATION)) { - DbgPrint("[CheatStengine] Buffer isn't the size of a MEMORY_BASIC_INFORMATION: %lu bytes\n", outputLen); - status = STATUS_BUFFER_TOO_SMALL; - break; - } - status = QueryProcessMemory( - targetProcess, - cmd->QueryMemoryData.Address, - static_cast(irp->AssociatedIrp.SystemBuffer)); - bytesReturned = sizeof(MEMORY_BASIC_INFORMATION); - break; - - case CommandHeader::AllocateMemory: - status = AllocateProcessMemory( - targetProcess, - cmd->AllocateMemoryData.Address, - cmd->AllocateMemoryData.Size, - cmd->AllocateMemoryData.AllocationType, - cmd->AllocateMemoryData.Protect, - cmd->AllocateMemoryData.AllocatedAddressPtr); - break; - - case CommandHeader::FreeMemory: - status = FreeProcessMemory( - targetProcess, - cmd->FreeMemoryData.Address, - cmd->FreeMemoryData.Size, - cmd->FreeMemoryData.FreeType); - break; - - case CommandHeader::ProtectMemory: - status = ProtectProcessMemory( - targetProcess, - cmd->ProtectMemoryData.Address, - cmd->ProtectMemoryData.Size, - cmd->ProtectMemoryData.NewProtect, - cmd->ProtectMemoryData.OldProtectPtr); - break; - - default: - DbgPrint("[CheatStengine] Unhandled command type: %d\n", cmd->Type); - status = STATUS_INVALID_PARAMETER; - break; - } - - ObDereferenceObject(targetProcess); - irp->IoStatus.Status = status; - irp->IoStatus.Information = bytesReturned; + irp->IoStatus.Information = information; IoCompleteRequest(irp, IO_NO_INCREMENT); return status; } -static void DriverUnload(PDRIVER_OBJECT driverObject) +void ConfigureDispatch(PDRIVER_OBJECT driverObject, bool pnp) { - UNREFERENCED_PARAMETER(driverObject); - - NTSTATUS status = IoDeleteSymbolicLink(&g_SymLink); - if (!NT_SUCCESS(status)) { - DbgPrint("[CheatStengine] IoDeleteSymbolicLink failed: 0x%08X\n", status); - return; + for (ULONG index = 0; index <= IRP_MJ_MAXIMUM_FUNCTION; ++index) { + driverObject->MajorFunction[index] = pnp ? DispatchPassThrough : DispatchUnsupported; } - IoDeleteDevice(driverObject->DeviceObject); - DbgPrint("[CheatStengine] Driver unloaded.\n"); + driverObject->MajorFunction[IRP_MJ_CREATE] = DispatchCreate; + driverObject->MajorFunction[IRP_MJ_CLEANUP] = DispatchCleanup; + driverObject->MajorFunction[IRP_MJ_CLOSE] = DispatchClose; + driverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = DispatchDeviceControl; + if (pnp) { + driverObject->MajorFunction[IRP_MJ_PNP] = DispatchPnp; + driverObject->MajorFunction[IRP_MJ_POWER] = DispatchPower; + driverObject->DriverExtension->AddDevice = AddDevice; + } + driverObject->DriverUnload = pnp ? DriverUnload : nullptr; } -NTSTATUS UnsupportedDispatch(PDEVICE_OBJECT driverObject, PIRP irp) +void DriverUnload(PDRIVER_OBJECT driverObject) { - UNREFERENCED_PARAMETER(driverObject); - irp->IoStatus.Status = STATUS_NOT_SUPPORTED; - IoCompleteRequest(irp, IO_NO_INCREMENT); - return irp->IoStatus.Status; + PDEVICE_OBJECT deviceObject = driverObject->DeviceObject; + while (deviceObject) { + PDEVICE_OBJECT nextDeviceObject = deviceObject->NextDevice; + DeviceExtension* extension = GetExtension(deviceObject); + if (extension->Mode == Protocol::LoadMode::Service) { + DestroyControlDevice(deviceObject); + } + deviceObject = nextDeviceObject; + } } -NTSTATUS InitializeDriver(PDRIVER_OBJECT driverObject, PUNICODE_STRING registryPath) +extern "C" NTSTATUS DriverEntry(PDRIVER_OBJECT driverObject, PUNICODE_STRING registryPath) { - DbgPrint("[CheatStengine] DriverEntry.\n"); - - RtlInitUnicodeString(&g_DevName, L"\\Device\\CheatStengineDriver"); - RtlInitUnicodeString(&g_SymLink, L"\\DosDevices\\CheatStengineDriver"); - - PDEVICE_OBJECT deviceObject = nullptr; - NTSTATUS status = IoCreateDevice( - driverObject, - 0, - &g_DevName, - FILE_DEVICE_UNKNOWN, - FILE_DEVICE_SECURE_OPEN, - FALSE, - &deviceObject); - - if (!NT_SUCCESS(status)) { - DbgPrint("[CheatStengine] IoCreateDevice failed: 0x%08X\n", status); - return status; - } - - status = IoCreateSymbolicLink(&g_SymLink, &g_DevName); - if (!NT_SUCCESS(status)) { - DbgPrint("[CheatStengine] IoCreateSymbolicLink failed: 0x%08X\n", status); - IoDeleteDevice(deviceObject); - return status; + if (!registryPath) { + return BootstrapMappedDriver(); } - - for (int i = 0; i <= IRP_MJ_MAXIMUM_FUNCTION; i++) { - driverObject->MajorFunction[i] = UnsupportedDispatch; + if (!driverObject || !driverObject->DriverExtension) { + return STATUS_INVALID_PARAMETER; } - driverObject->MajorFunction[IRP_MJ_CREATE] = CreateClose; - driverObject->MajorFunction[IRP_MJ_CLOSE] = CreateClose; - driverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = DeviceControl; - driverObject->DriverUnload = DriverUnload; - deviceObject->Flags |= DO_BUFFERED_IO; - deviceObject->Flags &= ~DO_DEVICE_INITIALIZING; - - DbgPrint("[CheatStengine] Driver loaded.\n"); - return STATUS_SUCCESS; + InitializeMemorySupport(); + InitializeProcessSupport(); + ConfigureDispatch(driverObject, true); + return CreateControlDevice(driverObject, Protocol::LoadMode::Service); } - -extern "C" NTSTATUS NTAPI IoCreateDriver(PUNICODE_STRING driverName, PDRIVER_INITIALIZE initializationFunction); -extern "C" NTSTATUS DriverEntry(PDRIVER_OBJECT driverObject, PUNICODE_STRING registryPath) -{ - UNREFERENCED_PARAMETER(registryPath); - // return InitializeDriver(driverObject, registryPath); - return IoCreateDriver(nullptr, InitializeDriver); -} \ No newline at end of file diff --git a/CheatStengineDriver/src/CheatStengineDriver/Driver.h b/CheatStengineDriver/src/CheatStengineDriver/Driver.h index 7da2453..01eee15 100644 --- a/CheatStengineDriver/src/CheatStengineDriver/Driver.h +++ b/CheatStengineDriver/src/CheatStengineDriver/Driver.h @@ -1,66 +1,9 @@ #pragma once -#include #include -#include -#define IOCTL_CS_COMMAND \ - CTL_CODE(FILE_DEVICE_UNKNOWN, 0x6969, METHOD_BUFFERED, FILE_ANY_ACCESS) +extern "C" DRIVER_INITIALIZE DriverEntry; +DRIVER_UNLOAD DriverUnload; -struct CommandHeader { - enum Type : uint32_t { - ReadMemory = 0, - WriteMemory = 1, - QueryMemory = 2, - AllocateMemory = 3, - FreeMemory = 4, - ProtectMemory = 5, - } Type; - - uint32_t Pid; - - union { - struct - { - uintptr_t Address; - size_t Size; - void* Buffer; - } ReadMemoryData; - - struct - { - uintptr_t Address; - size_t Size; - void* Buffer; - } WriteMemoryData; - - struct - { - uintptr_t Address; - } QueryMemoryData; - - struct - { - uintptr_t Address; - size_t Size; - uint32_t AllocationType; - uint32_t Protect; - uintptr_t* AllocatedAddressPtr; - } AllocateMemoryData; - - struct - { - uintptr_t Address; - size_t Size; - uint32_t FreeType; - } FreeMemoryData; - - struct - { - uintptr_t Address; - size_t Size; - uint32_t NewProtect; - uint32_t* OldProtectPtr; - } ProtectMemoryData; - }; -}; \ No newline at end of file +void ConfigureDispatch(PDRIVER_OBJECT driverObject, bool pnp); +NTSTATUS CompleteIrp(PIRP irp, NTSTATUS status, ULONG_PTR information = 0); \ No newline at end of file diff --git a/CheatStengineDriver/src/CheatStengineDriver/Mapper.cpp b/CheatStengineDriver/src/CheatStengineDriver/Mapper.cpp new file mode 100644 index 0000000..b661a9a --- /dev/null +++ b/CheatStengineDriver/src/CheatStengineDriver/Mapper.cpp @@ -0,0 +1,116 @@ +#include "Mapper.h" + +#include "Memory.h" +#include "Process.h" + +#include + +namespace { + +using CreateDriverRoutine = NTSTATUS (NTAPI*)(PUNICODE_STRING, PDRIVER_INITIALIZE); + +constexpr GUID DeviceClassGuid = { + 0xa7e1dd95, + 0x2cb7, + 0x46a5, + { 0x90, 0x1e, 0x23, 0x7a, 0x1f, 0xb7, 0x69, 0x5c } +}; + +UNICODE_STRING GetDeviceName() +{ + UNICODE_STRING name = RTL_CONSTANT_STRING(L"\\Device\\MemoryAccess"); + return name; +} + +UNICODE_STRING GetSymbolicLinkName() +{ + UNICODE_STRING name = RTL_CONSTANT_STRING(L"\\DosDevices\\MemoryAccess"); + return name; +} + +} + +NTSTATUS BootstrapMappedDriver() +{ + UNICODE_STRING routineName = RTL_CONSTANT_STRING(L"IoCreateDriver"); + const auto createDriver = reinterpret_cast( + MmGetSystemRoutineAddress(&routineName)); + if (!createDriver) { + return STATUS_NOT_SUPPORTED; + } + + UNICODE_STRING driverName = RTL_CONSTANT_STRING(L"\\Driver\\CheatStengineDriver"); + return createDriver(&driverName, InitializeMappedDriver); +} + +NTSTATUS InitializeMappedDriver(PDRIVER_OBJECT driverObject, PUNICODE_STRING registryPath) +{ + UNREFERENCED_PARAMETER(registryPath); + if (!driverObject || !driverObject->DriverExtension) { + return STATUS_INVALID_PARAMETER; + } + + InitializeMemorySupport(); + InitializeProcessSupport(); + ConfigureDispatch(driverObject, false); + + return CreateControlDevice(driverObject, Protocol::LoadMode::Mapped); +} + +NTSTATUS CreateControlDevice(PDRIVER_OBJECT driverObject, Protocol::LoadMode mode) +{ + if (!driverObject + || (mode != Protocol::LoadMode::Service && mode != Protocol::LoadMode::Mapped)) { + return STATUS_INVALID_PARAMETER; + } + + UNICODE_STRING deviceName = GetDeviceName(); + PDEVICE_OBJECT deviceObject = nullptr; + NTSTATUS status = IoCreateDeviceSecure( + driverObject, + sizeof(DeviceExtension), + &deviceName, + Protocol::DeviceType, + FILE_DEVICE_SECURE_OPEN, + FALSE, + &SDDL_DEVOBJ_SYS_ALL_ADM_ALL, + &DeviceClassGuid, + &deviceObject); + if (!NT_SUCCESS(status)) { + return status; + } + + status = InitializeDevice(deviceObject, mode, DeviceState::Started); + if (!NT_SUCCESS(status)) { + IoDeleteDevice(deviceObject); + return status; + } + + UNICODE_STRING symbolicLinkName = GetSymbolicLinkName(); + status = IoCreateSymbolicLink(&symbolicLinkName, &deviceName); + if (!NT_SUCCESS(status)) { + DestroyDevice(GetExtension(deviceObject)); + IoDeleteDevice(deviceObject); + return status; + } + + deviceObject->Flags |= DO_DIRECT_IO | DO_POWER_PAGABLE; + deviceObject->Flags &= ~DO_DEVICE_INITIALIZING; + return STATUS_SUCCESS; +} + +void DestroyControlDevice(PDEVICE_OBJECT deviceObject) +{ + DeviceExtension* extension = GetExtension(deviceObject); + const NTSTATUS lockStatus = IoAcquireRemoveLock(&extension->RemoveLock, deviceObject); + SetDeviceState(extension, DeviceState::Removed); + StopDevice(extension); + if (NT_SUCCESS(lockStatus)) { + IoReleaseRemoveLockAndWait(&extension->RemoveLock, deviceObject); + } + + UNICODE_STRING symbolicLinkName = GetSymbolicLinkName(); + IoDeleteSymbolicLink(&symbolicLinkName); + DestroyDevice(extension); + IoDeleteDevice(deviceObject); +} diff --git a/CheatStengineDriver/src/CheatStengineDriver/Mapper.h b/CheatStengineDriver/src/CheatStengineDriver/Mapper.h new file mode 100644 index 0000000..670db69 --- /dev/null +++ b/CheatStengineDriver/src/CheatStengineDriver/Mapper.h @@ -0,0 +1,8 @@ +#pragma once + +#include "Device.h" + +NTSTATUS InitializeMappedDriver(PDRIVER_OBJECT driverObject, PUNICODE_STRING registryPath); +NTSTATUS BootstrapMappedDriver(); +NTSTATUS CreateControlDevice(PDRIVER_OBJECT driverObject, Protocol::LoadMode mode); +void DestroyControlDevice(PDEVICE_OBJECT deviceObject); diff --git a/CheatStengineDriver/src/CheatStengineDriver/Memory.cpp b/CheatStengineDriver/src/CheatStengineDriver/Memory.cpp index 5f30a6e..70abdf2 100644 --- a/CheatStengineDriver/src/CheatStengineDriver/Memory.cpp +++ b/CheatStengineDriver/src/CheatStengineDriver/Memory.cpp @@ -1,209 +1,122 @@ -#include "memory.h" +#include "Memory.h" -#include +namespace { -// Credit to https://github.com/ryan-weil/ReadWriteDriver +using CopyRoutine = NTSTATUS (NTAPI*)( + PEPROCESS, + PVOID, + PEPROCESS, + PVOID, + SIZE_T, + KPROCESSOR_MODE, + PSIZE_T); -static constexpr UINT32 PAGE_OFFSET_BITS = 12; -static constexpr UINT64 PTE_PHYS_MASK = (~0xFULL << 8) & 0xFFF'FFFF'FFULL; -static constexpr UINT64 PTE_PHYS_MASK_WIDE = 0x000F'FFFF'FFFF'F000ULL; +PVOID volatile CopyAddress = nullptr; -namespace { - // RTL_OSVERSIONINFOW build numbers - enum WindowsBuild : ULONG { - Build_1803 = 17134, - Build_1809 = 17763, - Build_1903 = 18362, - Build_1909 = 18363, - Build_2004 = 19041, - Build_20H2 = 19569, - Build_21H1 = 20180, - Build_22H1 = 22000, - Build_22H2 = 22621, - Build_22H22 = 19045, - Build_Win11 = 24345, - }; - - INT32 UserDirBaseOffset() - { - RTL_OSVERSIONINFOW ver {}; - ver.dwOSVersionInfoSize = sizeof(ver); - RtlGetVersion(&ver); - - switch (ver.dwBuildNumber) { - case Build_1803: - case Build_1809: - return 0x0278; - - case Build_1903: - case Build_1909: - return 0x0280; - - case Build_2004: - case Build_20H2: - case Build_21H1: - case Build_22H1: - case Build_22H2: - case Build_22H22: - case Build_Win11: - default: - return 0x0388; - } +NTSTATUS TransferStatus(NTSTATUS status, SIZE_T transferred, SIZE_T requested) +{ + if (transferred == requested) { + return STATUS_SUCCESS; + } + if (transferred != 0) { + return STATUS_PARTIAL_COPY; } + return NT_SUCCESS(status) ? STATUS_PARTIAL_COPY : status; } -UINT64 GetProcessCr3(PEPROCESS process) +CopyRoutine GetCopyRoutine() { - PUCHAR processBytes = reinterpret_cast(process); - ULONG_PTR dirBase = *reinterpret_cast(processBytes + 0x28); - if (dirBase != 0) { - return dirBase; - } + return reinterpret_cast( + InterlockedCompareExchangePointer(&CopyAddress, nullptr, nullptr)); +} - INT32 offset = UserDirBaseOffset(); - return *reinterpret_cast(processBytes + offset); } -bool IsValidUserModeAddress(uintptr_t address, size_t size) +void InitializeMemorySupport() { - uintptr_t end = address + size; - if (end < address) { - // overflow check - return false; - } - - return address < MmUserProbeAddress && end <= MmUserProbeAddress; + UNICODE_STRING routineName = RTL_CONSTANT_STRING(L"MmCopyVirtualMemory"); + InterlockedExchangePointer( + &CopyAddress, + MmGetSystemRoutineAddress(&routineName)); } -NTSTATUS PhysRead(PVOID physAddress, PVOID buffer, SIZE_T size, SIZE_T* bytesRead) +bool CanCopyProcessMemory() { - MM_COPY_ADDRESS src {}; - src.PhysicalAddress.QuadPart = reinterpret_cast(physAddress); - return MmCopyMemory(buffer, src, size, MM_COPY_MEMORY_PHYSICAL, bytesRead); + return GetCopyRoutine() != nullptr; } -NTSTATUS PhysWrite(PVOID physAddress, PVOID buffer, SIZE_T size, SIZE_T* bytesWritten) +bool IsUserAddressRange(UINT64 address, UINT64 size) { - if (!physAddress) { - return STATUS_INVALID_PARAMETER; - } - - PHYSICAL_ADDRESS pa {}; - pa.QuadPart = reinterpret_cast(physAddress); - - PVOID mapped = MmMapIoSpaceEx(pa, size, PAGE_READWRITE); - if (!mapped) { - return STATUS_INSUFFICIENT_RESOURCES; + if (address == 0 || size == 0) { + return false; } - RtlCopyMemory(mapped, buffer, size); - *bytesWritten = size; - MmUnmapIoSpace(mapped, size); - return STATUS_SUCCESS; + const UINT64 maximumAddress = reinterpret_cast(MmHighestUserAddress); + return address <= maximumAddress && size - 1 <= maximumAddress - address; } -UINT64 TranslateLinear(UINT64 dtb, UINT64 va) +NTSTATUS ReadProcessAddressSpace( + PEPROCESS process, + UINT64 address, + PVOID buffer, + SIZE_T size, + PSIZE_T bytesTransferred) { - dtb &= ~0xFULL; - - const UINT64 pageOffset = va & ~(~0ULL << PAGE_OFFSET_BITS); - const UINT64 pteIndex = (va >> 12) & 0x1FFULL; - const UINT64 ptIndex = (va >> 21) & 0x1FFULL; - const UINT64 pdIndex = (va >> 30) & 0x1FFULL; - const UINT64 pdpIndex = (va >> 39) & 0x1FFULL; - - SIZE_T br = 0; - - // --- Level 4 -> PDPTE --- - UINT64 pdpe = 0; - PhysRead(reinterpret_cast(dtb + 8 * pdpIndex), &pdpe, sizeof(pdpe), &br); - if (!(pdpe & 1)) { - return 0; - } - - // --- Level 3 --> PDE --- - UINT64 pde = 0; - PhysRead(reinterpret_cast((pdpe & PTE_PHYS_MASK) + 8 * pdIndex), &pde, sizeof(pde), &br); - if (!(pde & 1)) { - return 0; - } - - if (pde & (1ULL << 7)) { - // 1 GiB huge page - return (pde & (~0ULL << 42 >> 12)) + (va & ~(~0ULL << 30)); - } - - // --- Level 2 -> PTE --- - UINT64 pteAddr = 0; - PhysRead(reinterpret_cast((pde & PTE_PHYS_MASK) + 8 * ptIndex), &pteAddr, sizeof(pteAddr), &br); - if (!(pteAddr & 1)) { - return 0; + if (!process || !buffer || !bytesTransferred || !IsUserAddressRange(address, size)) { + return STATUS_INVALID_PARAMETER; } - - if (pteAddr & (1ULL << 7)) { - // 2 MiB large page - return (pteAddr & PTE_PHYS_MASK) + (va & ~(~0ULL << 21)); + if (KeGetCurrentIrql() != PASSIVE_LEVEL) { + return STATUS_INVALID_DEVICE_STATE; } - // --- Level 1 -> physical page --- - UINT64 pte = 0; - PhysRead(reinterpret_cast((pteAddr & PTE_PHYS_MASK) + 8 * pteIndex), &pte, sizeof(pte), &br); - UINT64 physBase = pte & PTE_PHYS_MASK; - if (!physBase) { - return 0; + const CopyRoutine copyRoutine = GetCopyRoutine(); + if (!copyRoutine) { + return STATUS_NOT_SUPPORTED; } - return physBase + pageOffset; + *bytesTransferred = 0; + SIZE_T transferred = 0; + const NTSTATUS status = copyRoutine( + process, + reinterpret_cast(address), + PsGetCurrentProcess(), + buffer, + size, + KernelMode, + &transferred); + *bytesTransferred = transferred; + return TransferStatus(status, transferred, size); } -bool IsPageNoAccess(UINT64 dtb, UINT64 va) +NTSTATUS WriteProcessAddressSpace( + PEPROCESS process, + UINT64 address, + PVOID buffer, + SIZE_T size, + PSIZE_T bytesTransferred) { - const UINT64 pteIndex = (va >> 12) & 0x1FFULL; - const UINT64 ptIndex = (va >> 21) & 0x1FFULL; - const UINT64 pdIndex = (va >> 30) & 0x1FFULL; - const UINT64 pdpIndex = (va >> 39) & 0x1FFULL; - - SIZE_T br = 0; - UINT64 entry = 0; - - // PDPTE - PhysRead(reinterpret_cast((dtb & ~0xFFFULL) + pdpIndex * sizeof(UINT64)), - &entry, sizeof(entry), &br); - if (!(entry & 1)) { - return true; + if (!process || !buffer || !bytesTransferred || !IsUserAddressRange(address, size)) { + return STATUS_INVALID_PARAMETER; } - - // PDE - { - UINT64 base = entry & PTE_PHYS_MASK_WIDE; - PhysRead(reinterpret_cast(base + pdIndex * sizeof(UINT64)), - &entry, sizeof(entry), &br); - if (!(entry & 1)) { - return true; - } - if (entry & (1ULL << 7)) { - return false; // 1 GiB page – present - } + if (KeGetCurrentIrql() != PASSIVE_LEVEL) { + return STATUS_INVALID_DEVICE_STATE; } - // PT entry (2 MiB check) - { - UINT64 base = entry & PTE_PHYS_MASK_WIDE; - PhysRead(reinterpret_cast(base + ptIndex * sizeof(UINT64)), - &entry, sizeof(entry), &br); - if (!(entry & 1)) { - return true; - } - if (entry & (1ULL << 7)) { - return false; // 2 MiB page – present - } + const CopyRoutine copyRoutine = GetCopyRoutine(); + if (!copyRoutine) { + return STATUS_NOT_SUPPORTED; } - // PTE - { - UINT64 base = entry & PTE_PHYS_MASK_WIDE; - PhysRead(reinterpret_cast(base + pteIndex * sizeof(UINT64)), - &entry, sizeof(entry), &br); - return !(entry & 1); - } -} \ No newline at end of file + *bytesTransferred = 0; + SIZE_T transferred = 0; + const NTSTATUS status = copyRoutine( + PsGetCurrentProcess(), + buffer, + process, + reinterpret_cast(address), + size, + KernelMode, + &transferred); + *bytesTransferred = transferred; + return TransferStatus(status, transferred, size); +} diff --git a/CheatStengineDriver/src/CheatStengineDriver/Memory.h b/CheatStengineDriver/src/CheatStengineDriver/Memory.h index dd1265a..439386e 100644 --- a/CheatStengineDriver/src/CheatStengineDriver/Memory.h +++ b/CheatStengineDriver/src/CheatStengineDriver/Memory.h @@ -2,10 +2,21 @@ #include -NTSTATUS PhysRead(PVOID physAddress, PVOID buffer, SIZE_T size, SIZE_T* bytesRead); -NTSTATUS PhysWrite(PVOID physAddress, PVOID buffer, SIZE_T size, SIZE_T* bytesWritten); +void InitializeMemorySupport(); +bool CanCopyProcessMemory(); -UINT64 TranslateLinear(UINT64 dtb, UINT64 va); -bool IsPageNoAccess(UINT64 dtb, UINT64 va); -bool IsValidUserModeAddress(uintptr_t address, size_t size); -UINT64 GetProcessCr3(PEPROCESS process); \ No newline at end of file +bool IsUserAddressRange(UINT64 address, UINT64 size); + +NTSTATUS ReadProcessAddressSpace( + PEPROCESS process, + UINT64 address, + PVOID buffer, + SIZE_T size, + PSIZE_T bytesTransferred); + +NTSTATUS WriteProcessAddressSpace( + PEPROCESS process, + UINT64 address, + PVOID buffer, + SIZE_T size, + PSIZE_T bytesTransferred); diff --git a/CheatStengineDriver/src/CheatStengineDriver/Pnp.cpp b/CheatStengineDriver/src/CheatStengineDriver/Pnp.cpp new file mode 100644 index 0000000..8485c82 --- /dev/null +++ b/CheatStengineDriver/src/CheatStengineDriver/Pnp.cpp @@ -0,0 +1,270 @@ +#include "Pnp.h" + +namespace { + +NTSTATUS SetInterface(DeviceExtension* extension, BOOLEAN enabled) +{ + if (extension->InterfaceEnabled == enabled) { + return STATUS_SUCCESS; + } + + const NTSTATUS status = IoSetDeviceInterfaceState( + &extension->InterfaceName, + enabled ? TRUE : FALSE); + if (NT_SUCCESS(status)) { + extension->InterfaceEnabled = enabled; + } + return status; +} + +NTSTATUS ForwardSynchronously(PDEVICE_OBJECT deviceObject, PIRP irp) +{ + DeviceExtension* extension = GetExtension(deviceObject); + KEVENT event {}; + KeInitializeEvent(&event, NotificationEvent, FALSE); + + IoCopyCurrentIrpStackLocationToNext(irp); + IoSetCompletionRoutine( + irp, + CompleteSynchronousIrp, + &event, + TRUE, + TRUE, + TRUE); + + const NTSTATUS callStatus = IoCallDriver(extension->LowerDeviceObject, irp); + if (callStatus == STATUS_PENDING) { + KeWaitForSingleObject(&event, Executive, KernelMode, FALSE, nullptr); + } + return irp->IoStatus.Status; +} + +NTSTATUS ForwardWithRemoveLock(PDEVICE_OBJECT deviceObject, PIRP irp, bool power) +{ + DeviceExtension* extension = GetExtension(deviceObject); + IoCopyCurrentIrpStackLocationToNext(irp); + IoSetCompletionRoutine( + irp, + ReleaseRemoveLock, + extension, + TRUE, + TRUE, + TRUE); + return power + ? PoCallDriver(extension->LowerDeviceObject, irp) + : IoCallDriver(extension->LowerDeviceObject, irp); +} + +bool BeginPendingState(DeviceExtension* extension, DeviceState state) +{ + if (GetDeviceState(extension) != DeviceState::Started) { + return false; + } + + SetDeviceState(extension, state); + SetInterface(extension, FALSE); + StopDevice(extension); + return true; +} + +void RestorePendingState(DeviceExtension* extension, DeviceState state) +{ + if (GetDeviceState(extension) != state) { + return; + } + + StartDevice(extension); + SetInterface(extension, TRUE); + SetDeviceState(extension, DeviceState::Started); + TouchDevice(extension); +} + +} + +NTSTATUS CompleteSynchronousIrp(PDEVICE_OBJECT, PIRP, PVOID context) +{ + KeSetEvent(static_cast(context), IO_NO_INCREMENT, FALSE); + return STATUS_MORE_PROCESSING_REQUIRED; +} + +NTSTATUS ReleaseRemoveLock(PDEVICE_OBJECT, PIRP irp, PVOID context) +{ + if (irp->PendingReturned) { + IoMarkIrpPending(irp); + } + IoReleaseRemoveLock(&static_cast(context)->RemoveLock, irp); + return STATUS_CONTINUE_COMPLETION; +} + +NTSTATUS DispatchPnp(PDEVICE_OBJECT deviceObject, PIRP irp) +{ + DeviceExtension* extension = GetExtension(deviceObject); + if (extension->Mode != Protocol::LoadMode::Pnp) { + return CompleteIrp(irp, STATUS_INVALID_DEVICE_REQUEST); + } + NTSTATUS status = IoAcquireRemoveLock(&extension->RemoveLock, irp); + if (!NT_SUCCESS(status)) { + return CompleteIrp(irp, status); + } + + PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(irp); + switch (stack->MinorFunction) { + case IRP_MN_START_DEVICE: + status = ForwardSynchronously(deviceObject, irp); + if (NT_SUCCESS(status)) { + StartDevice(extension); + status = SetInterface(extension, TRUE); + if (NT_SUCCESS(status)) { + SetDeviceState(extension, DeviceState::Started); + TouchDevice(extension); + } else { + SetDeviceState(extension, DeviceState::Stopped); + StopDevice(extension); + } + } + IoReleaseRemoveLock(&extension->RemoveLock, irp); + return CompleteIrp(irp, status); + + case IRP_MN_QUERY_STOP_DEVICE: { + const bool transitioned = BeginPendingState(extension, DeviceState::StopPending); + status = ForwardSynchronously(deviceObject, irp); + if (!NT_SUCCESS(status) && transitioned) { + RestorePendingState(extension, DeviceState::StopPending); + } + IoReleaseRemoveLock(&extension->RemoveLock, irp); + return CompleteIrp(irp, status); + } + + case IRP_MN_QUERY_REMOVE_DEVICE: { + const bool transitioned = BeginPendingState(extension, DeviceState::RemovePending); + status = ForwardSynchronously(deviceObject, irp); + if (!NT_SUCCESS(status) && transitioned) { + RestorePendingState(extension, DeviceState::RemovePending); + } + IoReleaseRemoveLock(&extension->RemoveLock, irp); + return CompleteIrp(irp, status); + } + + case IRP_MN_CANCEL_STOP_DEVICE: + status = ForwardSynchronously(deviceObject, irp); + RestorePendingState(extension, DeviceState::StopPending); + IoReleaseRemoveLock(&extension->RemoveLock, irp); + return CompleteIrp(irp, status); + + case IRP_MN_CANCEL_REMOVE_DEVICE: + status = ForwardSynchronously(deviceObject, irp); + RestorePendingState(extension, DeviceState::RemovePending); + IoReleaseRemoveLock(&extension->RemoveLock, irp); + return CompleteIrp(irp, status); + + case IRP_MN_STOP_DEVICE: + SetDeviceState(extension, DeviceState::Stopped); + SetInterface(extension, FALSE); + StopDevice(extension); + status = ForwardSynchronously(deviceObject, irp); + IoReleaseRemoveLock(&extension->RemoveLock, irp); + return CompleteIrp(irp, status); + + case IRP_MN_SURPRISE_REMOVAL: + SetDeviceState(extension, DeviceState::SurpriseRemoved); + SetInterface(extension, FALSE); + StopDevice(extension); + return ForwardWithRemoveLock(deviceObject, irp, false); + + case IRP_MN_REMOVE_DEVICE: + SetDeviceState(extension, DeviceState::Removed); + SetInterface(extension, FALSE); + StopDevice(extension); + IoReleaseRemoveLockAndWait(&extension->RemoveLock, irp); + IoSkipCurrentIrpStackLocation(irp); + status = IoCallDriver(extension->LowerDeviceObject, irp); + IoDetachDevice(extension->LowerDeviceObject); + DestroyDevice(extension); + RtlFreeUnicodeString(&extension->InterfaceName); + IoDeleteDevice(deviceObject); + return status; + + default: + return ForwardWithRemoveLock(deviceObject, irp, false); + } +} + +NTSTATUS DispatchPower(PDEVICE_OBJECT deviceObject, PIRP irp) +{ + DeviceExtension* extension = GetExtension(deviceObject); + if (extension->Mode != Protocol::LoadMode::Pnp) { + return CompleteIrp(irp, STATUS_INVALID_DEVICE_REQUEST); + } + const NTSTATUS status = IoAcquireRemoveLock(&extension->RemoveLock, irp); + if (!NT_SUCCESS(status)) { + return CompleteIrp(irp, status); + } + return ForwardWithRemoveLock(deviceObject, irp, true); +} + +NTSTATUS DispatchPassThrough(PDEVICE_OBJECT deviceObject, PIRP irp) +{ + DeviceExtension* extension = GetExtension(deviceObject); + if (extension->Mode != Protocol::LoadMode::Pnp) { + return CompleteIrp(irp, STATUS_INVALID_DEVICE_REQUEST); + } + const NTSTATUS status = IoAcquireRemoveLock(&extension->RemoveLock, irp); + if (!NT_SUCCESS(status)) { + return CompleteIrp(irp, status); + } + return ForwardWithRemoveLock(deviceObject, irp, false); +} + +NTSTATUS AddDevice(PDRIVER_OBJECT driverObject, PDEVICE_OBJECT physicalDeviceObject) +{ + PDEVICE_OBJECT deviceObject = nullptr; + NTSTATUS status = IoCreateDevice( + driverObject, + sizeof(DeviceExtension), + nullptr, + Protocol::DeviceType, + FILE_DEVICE_SECURE_OPEN, + FALSE, + &deviceObject); + if (!NT_SUCCESS(status)) { + return status; + } + + status = InitializeDevice(deviceObject, Protocol::LoadMode::Pnp, DeviceState::Added); + if (!NT_SUCCESS(status)) { + IoDeleteDevice(deviceObject); + return status; + } + + DeviceExtension* extension = GetExtension(deviceObject); + extension->PhysicalDeviceObject = physicalDeviceObject; + status = IoAttachDeviceToDeviceStackSafe( + deviceObject, + physicalDeviceObject, + &extension->LowerDeviceObject); + if (!NT_SUCCESS(status)) { + DestroyDevice(extension); + IoDeleteDevice(deviceObject); + return status; + } + + GUID interfaceGuid = Protocol::DeviceInterfaceGuid; + status = IoRegisterDeviceInterface( + physicalDeviceObject, + &interfaceGuid, + nullptr, + &extension->InterfaceName); + if (!NT_SUCCESS(status)) { + IoDetachDevice(extension->LowerDeviceObject); + DestroyDevice(extension); + IoDeleteDevice(deviceObject); + return status; + } + + deviceObject->Flags |= DO_DIRECT_IO; + if ((extension->LowerDeviceObject->Flags & DO_POWER_PAGABLE) != 0) { + deviceObject->Flags |= DO_POWER_PAGABLE; + } + deviceObject->Flags &= ~DO_DEVICE_INITIALIZING; + return STATUS_SUCCESS; +} diff --git a/CheatStengineDriver/src/CheatStengineDriver/Pnp.h b/CheatStengineDriver/src/CheatStengineDriver/Pnp.h new file mode 100644 index 0000000..442267e --- /dev/null +++ b/CheatStengineDriver/src/CheatStengineDriver/Pnp.h @@ -0,0 +1,15 @@ +#pragma once + +#include "Device.h" + +DRIVER_ADD_DEVICE AddDevice; + +_Dispatch_type_(IRP_MJ_PNP) +DRIVER_DISPATCH DispatchPnp; + +_Dispatch_type_(IRP_MJ_POWER) +DRIVER_DISPATCH DispatchPower; + +DRIVER_DISPATCH DispatchPassThrough; +IO_COMPLETION_ROUTINE CompleteSynchronousIrp; +IO_COMPLETION_ROUTINE ReleaseRemoveLock; diff --git a/CheatStengineDriver/src/CheatStengineDriver/Process.cpp b/CheatStengineDriver/src/CheatStengineDriver/Process.cpp index 28528b3..7e11ad0 100644 --- a/CheatStengineDriver/src/CheatStengineDriver/Process.cpp +++ b/CheatStengineDriver/src/CheatStengineDriver/Process.cpp @@ -1,232 +1,201 @@ -#include "process.h" -#include "memory.h" +#include "Process.h" -#include +#include "Memory.h" -extern "C" NTSYSAPI NTSTATUS NTAPI ZwProtectVirtualMemory( - IN HANDLE ProcessHandle, - IN OUT PVOID* BaseAddress, - IN OUT PSIZE_T NumberOfBytesToProtect, - IN ULONG NewAccessProtection, - OUT PULONG OldAccessProtection); +namespace { -#define MIN(a, b) (((a) < (b)) ? (a) : (b)) +using ProtectRoutine = NTSTATUS (NTAPI*)( + HANDLE, + PVOID*, + PSIZE_T, + ULONG, + PULONG); -NTSTATUS ReadProcessMemory(PEPROCESS process, uintptr_t address, void* buffer, size_t size) -{ - DbgPrint("[CheatStengine] ReadProcessMemory addr=0x%p size=%zu\n", - reinterpret_cast(address), size); - - if (!buffer || size == 0) { - return STATUS_INVALID_PARAMETER; - } +PVOID volatile ProtectAddress = nullptr; - if (!IsValidUserModeAddress(address, size)) { - DbgPrint("[CheatStengine] ReadProcessMemory: Invalid user mode address 0x%p\n", - reinterpret_cast(address)); - return STATUS_ACCESS_VIOLATION; - } - - const UINT64 dtb = GetProcessCr3(process); - if (IsPageNoAccess(dtb, address)) { - DbgPrint("[CheatStengine] ReadProcessMemory: Address 0x%p is marked as PAGE_NOACCESS\n", - reinterpret_cast(address)); - return STATUS_ACCESS_DENIED; - } - - const UINT64 physAddr = TranslateLinear(dtb, address); - if (!physAddr) { - DbgPrint("[CheatStengine] ReadProcessMemory: Failed to translate linear address 0x%p\n", - reinterpret_cast(address)); - return STATUS_UNSUCCESSFUL; - } - - // Clamp to the end of the current physical page. - const ULONG64 chunk = MIN(PAGE_SIZE - static_cast(physAddr & 0xFFF), size); - SIZE_T bytesRead = 0; - NTSTATUS status = PhysRead(reinterpret_cast(physAddr), buffer, chunk, &bytesRead); - if (!NT_SUCCESS(status)) { - DbgPrint("[CheatStengine] PhysRead failed: 0x%08X\n", status); - } - return status; +bool IsNativeSize(UINT64 size) +{ + return size <= static_cast(MAXULONG_PTR); } -NTSTATUS WriteProcessMemory(PEPROCESS process, uintptr_t address, void* buffer, size_t size) +bool IsUserAddress(UINT64 address) { - DbgPrint("[CheatStengine] WriteProcessMemory addr=0x%p size=%zu\n", - reinterpret_cast(address), size); - - if (!buffer || size == 0) { - return STATUS_INVALID_PARAMETER; - } + return address <= reinterpret_cast(MmHighestUserAddress); +} - if (!IsValidUserModeAddress(address, size)) { - return STATUS_ACCESS_VIOLATION; - } +} - const UINT64 dtb = GetProcessCr3(process); - if (IsPageNoAccess(dtb, address)) { - return STATUS_ACCESS_DENIED; - } +void InitializeProcessSupport() +{ + UNICODE_STRING routineName = RTL_CONSTANT_STRING(L"ZwProtectVirtualMemory"); + PVOID routine = MmGetSystemRoutineAddress(&routineName); + InterlockedExchangePointer(&ProtectAddress, routine); +} - const UINT64 physAddr = TranslateLinear(dtb, address); - if (!physAddr) { - return STATUS_UNSUCCESSFUL; - } +bool CanProtectProcessMemory() +{ + return InterlockedCompareExchangePointer(&ProtectAddress, nullptr, nullptr) != nullptr; +} - const ULONG64 chunk = MIN(PAGE_SIZE - static_cast(physAddr & 0xFFF), size); - SIZE_T bytesWritten = 0; - NTSTATUS status = PhysWrite(reinterpret_cast(physAddr), buffer, chunk, &bytesWritten); - if (!NT_SUCCESS(status)) { - DbgPrint("[CheatStengine] PhysWrite failed: 0x%08X\n", status); - } - return status; +NTSTATUS ReadProcessMemory( + PEPROCESS process, + UINT64 address, + PVOID buffer, + SIZE_T size, + PSIZE_T bytesTransferred) +{ + return ReadProcessAddressSpace(process, address, buffer, size, bytesTransferred); } -NTSTATUS QueryProcessMemory(PEPROCESS process, uintptr_t address, MEMORY_BASIC_INFORMATION* mbi) +NTSTATUS WriteProcessMemory( + PEPROCESS process, + UINT64 address, + PVOID buffer, + SIZE_T size, + PSIZE_T bytesTransferred) { - DbgPrint("[CheatStengine] QueryProcessMemory addr=0x%p, mbi=0x%p\n", - reinterpret_cast(address), reinterpret_cast(mbi)); + return WriteProcessAddressSpace(process, address, buffer, size, bytesTransferred); +} - if (!process || !mbi) { +NTSTATUS QueryProcessMemory( + PEPROCESS process, + UINT64 address, + PMEMORY_BASIC_INFORMATION memoryInformation) +{ + if (!process || !memoryInformation || !IsUserAddress(address)) { return STATUS_INVALID_PARAMETER; } - - if (!IsValidUserModeAddress(address, 1)) { - DbgPrint("[CheatStengine] QueryProcessMemory: Invalid user mode address 0x%p\n", - reinterpret_cast(address)); - return STATUS_ACCESS_VIOLATION; + if (KeGetCurrentIrql() != PASSIVE_LEVEL) { + return STATUS_INVALID_DEVICE_STATE; } NTSTATUS status = STATUS_SUCCESS; - KAPC_STATE apcState {}; KeStackAttachProcess(process, &apcState); - __try { - SIZE_T returnLength = 0; - - status = ZwQueryVirtualMemory( - ZwCurrentProcess(), - reinterpret_cast(address), - MemoryBasicInformation, - mbi, - sizeof(MEMORY_BASIC_INFORMATION), - &returnLength); - } __except (EXCEPTION_EXECUTE_HANDLER) { - status = GetExceptionCode(); - DbgPrint("[CheatStengine] Exception in QueryProcessMemory: 0x%08X\n", status); - } + SIZE_T returnLength = 0; + status = ZwQueryVirtualMemory( + ZwCurrentProcess(), + reinterpret_cast(address), + MemoryBasicInformation, + memoryInformation, + sizeof(*memoryInformation), + &returnLength); KeUnstackDetachProcess(&apcState); - - DbgPrint("[CheatStengine] ZwQueryVirtualMemory returned 0x%08X, BaseAddress=0x%p, RegionSize=0x%p, State=0x%X, Protect=0x%X, Type=0x%X\n", - status, mbi->BaseAddress, reinterpret_cast(mbi->RegionSize), mbi->State, mbi->Protect, mbi->Type); - - if (!NT_SUCCESS(status)) { - DbgPrint("[CheatStengine] ZwQueryVirtualMemory failed: 0x%08X\n", status); - } return status; } -NTSTATUS AllocateProcessMemory(PEPROCESS process, uintptr_t address, size_t size, - uint32_t allocationType, uint32_t protect, - uintptr_t* outAddress) +NTSTATUS AllocateProcessMemory( + PEPROCESS process, + UINT64 address, + UINT64 size, + ULONG allocationType, + ULONG protect, + PUINT64 allocatedAddress, + PUINT64 allocatedSize) { - if (!process || !outAddress || size == 0) + if (!process || !allocatedAddress || !allocatedSize || size == 0 + || !IsNativeSize(size) + || (address != 0 && !IsUserAddressRange(address, size))) { return STATUS_INVALID_PARAMETER; + } + if (KeGetCurrentIrql() != PASSIVE_LEVEL) { + return STATUS_INVALID_DEVICE_STATE; + } NTSTATUS status = STATUS_SUCCESS; PVOID baseAddress = reinterpret_cast(address); - SIZE_T regionSize = size; - + SIZE_T regionSize = static_cast(size); KAPC_STATE apcState {}; KeStackAttachProcess(process, &apcState); - __try { - status = ZwAllocateVirtualMemory( - ZwCurrentProcess(), - &baseAddress, - 0, - ®ionSize, - allocationType, - protect); - } __except (EXCEPTION_EXECUTE_HANDLER) { - status = GetExceptionCode(); - DbgPrint("[CheatStengine] Exception in AllocateProcessMemory: 0x%08X\n", status); - } + status = ZwAllocateVirtualMemory( + ZwCurrentProcess(), + &baseAddress, + 0, + ®ionSize, + allocationType, + protect); KeUnstackDetachProcess(&apcState); if (NT_SUCCESS(status)) { - *outAddress = reinterpret_cast(baseAddress); - } else { - DbgPrint("[CheatStengine] ZwAllocateVirtualMemory failed: 0x%08X\n", status); + *allocatedAddress = reinterpret_cast(baseAddress); + *allocatedSize = regionSize; } return status; } -NTSTATUS FreeProcessMemory(PEPROCESS process, uintptr_t address, size_t size, uint32_t freeType) +NTSTATUS FreeProcessMemory( + PEPROCESS process, + UINT64 address, + UINT64 size, + ULONG freeType) { - if (!process || address == 0) { + const bool release = freeType == MEM_RELEASE && size == 0; + const bool decommit = freeType == MEM_DECOMMIT + && size != 0 + && IsUserAddressRange(address, size); + if (!process || address == 0 || !IsNativeSize(size) || (!release && !decommit) + || !IsUserAddress(address)) { return STATUS_INVALID_PARAMETER; } + if (KeGetCurrentIrql() != PASSIVE_LEVEL) { + return STATUS_INVALID_DEVICE_STATE; + } NTSTATUS status = STATUS_SUCCESS; PVOID baseAddress = reinterpret_cast(address); - SIZE_T regionSize = size; - + SIZE_T regionSize = static_cast(size); KAPC_STATE apcState {}; KeStackAttachProcess(process, &apcState); - __try { - status = ZwFreeVirtualMemory( - ZwCurrentProcess(), - &baseAddress, - ®ionSize, - freeType); - } __except (EXCEPTION_EXECUTE_HANDLER) { - status = GetExceptionCode(); - DbgPrint("[CheatStengine] Exception in FreeProcessMemory: 0x%08X\n", status); - } + status = ZwFreeVirtualMemory( + ZwCurrentProcess(), + &baseAddress, + ®ionSize, + freeType); KeUnstackDetachProcess(&apcState); - - if (!NT_SUCCESS(status)) { - DbgPrint("[CheatStengine] ZwFreeVirtualMemory failed: 0x%08X\n", status); - } return status; } -NTSTATUS ProtectProcessMemory(PEPROCESS process, uintptr_t address, size_t size, - uint32_t newProtect, uint32_t* oldProtect) +NTSTATUS ProtectProcessMemory( + PEPROCESS process, + UINT64 address, + UINT64 size, + ULONG newProtect, + PULONG oldProtect, + PUINT64 protectedAddress, + PUINT64 protectedSize) { - if (!process || address == 0 || size == 0 || !oldProtect) { + if (!process || !oldProtect || !protectedAddress || !protectedSize + || !IsNativeSize(size) || !IsUserAddressRange(address, size)) { return STATUS_INVALID_PARAMETER; } + if (KeGetCurrentIrql() != PASSIVE_LEVEL) { + return STATUS_INVALID_DEVICE_STATE; + } - if (!IsValidUserModeAddress(address, size)) { - return STATUS_ACCESS_VIOLATION; + const auto protectRoutine = reinterpret_cast( + InterlockedCompareExchangePointer(&ProtectAddress, nullptr, nullptr)); + if (!protectRoutine) { + return STATUS_NOT_SUPPORTED; } NTSTATUS status = STATUS_SUCCESS; - ULONG oldProtectValue = 0; - + PVOID baseAddress = reinterpret_cast(address); + SIZE_T regionSize = static_cast(size); + ULONG previousProtect = 0; KAPC_STATE apcState {}; KeStackAttachProcess(process, &apcState); - __try { - status = ZwProtectVirtualMemory( - ZwCurrentProcess(), - reinterpret_cast(&address), - &size, - newProtect, - &oldProtectValue); - - if (NT_SUCCESS(status)) { - *oldProtect = oldProtectValue; - } - } __except (EXCEPTION_EXECUTE_HANDLER) { - status = GetExceptionCode(); - DbgPrint("[CheatStengine] Exception in ProtectProcessMemory: 0x%08X\n", status); - } + status = protectRoutine( + ZwCurrentProcess(), + &baseAddress, + ®ionSize, + newProtect, + &previousProtect); KeUnstackDetachProcess(&apcState); - if (!NT_SUCCESS(status)) { - DbgPrint("[CheatStengine] ZwProtectVirtualMemory failed: 0x%08X\n", status); + if (NT_SUCCESS(status)) { + *oldProtect = previousProtect; + *protectedAddress = reinterpret_cast(baseAddress); + *protectedSize = regionSize; } return status; -} \ No newline at end of file +} diff --git a/CheatStengineDriver/src/CheatStengineDriver/Process.h b/CheatStengineDriver/src/CheatStengineDriver/Process.h index 97c4d03..c335df8 100644 --- a/CheatStengineDriver/src/CheatStengineDriver/Process.h +++ b/CheatStengineDriver/src/CheatStengineDriver/Process.h @@ -1,42 +1,49 @@ #pragma once -#include #include +void InitializeProcessSupport(); +bool CanProtectProcessMemory(); + NTSTATUS ReadProcessMemory( PEPROCESS process, - uintptr_t address, - void* buffer, - size_t size); + UINT64 address, + PVOID buffer, + SIZE_T size, + PSIZE_T bytesTransferred); NTSTATUS WriteProcessMemory( PEPROCESS process, - uintptr_t address, - void* buffer, - size_t size); + UINT64 address, + PVOID buffer, + SIZE_T size, + PSIZE_T bytesTransferred); NTSTATUS QueryProcessMemory( PEPROCESS process, - uintptr_t address, - PMEMORY_BASIC_INFORMATION mbi); + UINT64 address, + PMEMORY_BASIC_INFORMATION memoryInformation); NTSTATUS AllocateProcessMemory( PEPROCESS process, - uintptr_t address, - size_t size, - uint32_t allocationType, - uint32_t protect, - uintptr_t* outAddress); + UINT64 address, + UINT64 size, + ULONG allocationType, + ULONG protect, + PUINT64 allocatedAddress, + PUINT64 allocatedSize); NTSTATUS FreeProcessMemory( PEPROCESS process, - uintptr_t address, - size_t size, - uint32_t freeType); + UINT64 address, + UINT64 size, + ULONG freeType); NTSTATUS ProtectProcessMemory( PEPROCESS process, - uintptr_t address, - size_t size, - uint32_t newProtect, - uint32_t* oldProtect); \ No newline at end of file + UINT64 address, + UINT64 size, + ULONG newProtect, + PULONG oldProtect, + PUINT64 protectedAddress, + PUINT64 protectedSize); diff --git a/CheatStengineDriver/vendor/FindWDK b/CheatStengineDriver/vendor/FindWDK new file mode 160000 index 0000000..ff531a9 --- /dev/null +++ b/CheatStengineDriver/vendor/FindWDK @@ -0,0 +1 @@ +Subproject commit ff531a9533b6d38f0ab62e23da2205b0cfe0d51b diff --git a/Engine/CMakeLists.txt b/Engine/CMakeLists.txt index 41fe01a..27e69fa 100644 --- a/Engine/CMakeLists.txt +++ b/Engine/CMakeLists.txt @@ -3,15 +3,26 @@ project(Engine LANGUAGES CXX) include(vendor/imgui.cmake) include(vendor/spdlog.cmake) -file(GLOB_RECURSE SOURCE_FILES src/*.cpp) -add_library(${PROJECT_NAME} ${SOURCE_FILES}) +add_library(${PROJECT_NAME} + src/Engine/Core/Application.cpp + src/Engine/Core/Layers/Layer.cpp + src/Engine/Core/Layers/LayerStack.cpp + src/Engine/Core/Log.cpp + src/Engine/Core/Window.cpp + src/Engine/Renderer/GraphicsContext.cpp + src/Platform/DX11/DX11Context.cpp + src/Platform/Win32/Win32Window.cpp +) + +target_compile_definitions(imgui PUBLIC IMGUI_USE_WCHAR32) +target_compile_definitions(${PROJECT_NAME} PUBLIC ENGINE_IMGUI) if (WIN32) - add_definitions(-DPLATFORM_WINDOWS -DNOMINMAX) + target_compile_definitions(${PROJECT_NAME} PUBLIC PLATFORM_WINDOWS NOMINMAX) elseif (UNIX AND NOT APPLE) - add_definitions(-DPLATFORM_LINUX) + target_compile_definitions(${PROJECT_NAME} PUBLIC PLATFORM_LINUX) elseif (APPLE) - add_definitions(-DPLATFORM_MACOS) + target_compile_definitions(${PROJECT_NAME} PUBLIC PLATFORM_MACOS) endif () target_include_directories(${PROJECT_NAME} PUBLIC src) diff --git a/Engine/src/Platform/Win32/Win32Window.cpp b/Engine/src/Platform/Win32/Win32Window.cpp index 58f85c8..0f4d256 100644 --- a/Engine/src/Platform/Win32/Win32Window.cpp +++ b/Engine/src/Platform/Win32/Win32Window.cpp @@ -364,6 +364,19 @@ void Win32Window::Init(const WindowProps& props) SetVSync(false); } +HICON Win32Window::LoadAppIcon(int size) +{ + // The branded icon ships next to the executable under Resources/. Fall back + // to the stock application icon if it can't be loaded (e.g. running from a + // directory where Resources wasn't copied). + HICON icon = static_cast(::LoadImage( + nullptr, "Resources/favicon.ico", IMAGE_ICON, size, size, LR_LOADFROMFILE)); + if (!icon) { + icon = ::LoadIcon(nullptr, IDI_APPLICATION); + } + return icon; +} + void Win32Window::RegisterWindowClass() const { WNDCLASSEX wc = {}; @@ -371,10 +384,10 @@ void Win32Window::RegisterWindowClass() const wc.style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC; wc.lpfnWndProc = WindowProc; wc.hInstance = m_Instance; - wc.hIcon = ::LoadIcon(nullptr, IDI_APPLICATION); + wc.hIcon = LoadAppIcon(::GetSystemMetrics(SM_CXICON)); wc.hCursor = ::LoadCursor(nullptr, IDC_ARROW); wc.lpszClassName = "CECLASS"; - wc.hIconSm = ::LoadIcon(nullptr, IDI_APPLICATION); + wc.hIconSm = LoadAppIcon(::GetSystemMetrics(SM_CXSMICON)); if (!::RegisterClassEx(&wc)) { throw std::runtime_error("Failed to register window class"); @@ -399,6 +412,13 @@ void Win32Window::CreateWindow(const WindowProps& props) throw std::runtime_error("Failed to create window"); } + if (HICON largeIcon = LoadAppIcon(::GetSystemMetrics(SM_CXICON))) { + ::SendMessage(m_Hwnd, WM_SETICON, ICON_BIG, reinterpret_cast(largeIcon)); + } + if (HICON smallIcon = LoadAppIcon(::GetSystemMetrics(SM_CXSMICON))) { + ::SendMessage(m_Hwnd, WM_SETICON, ICON_SMALL, reinterpret_cast(smallIcon)); + } + ::ShowWindow(m_Hwnd, SW_SHOWNA); } diff --git a/Engine/src/Platform/Win32/Win32Window.h b/Engine/src/Platform/Win32/Win32Window.h index 823db46..14553b9 100644 --- a/Engine/src/Platform/Win32/Win32Window.h +++ b/Engine/src/Platform/Win32/Win32Window.h @@ -62,6 +62,8 @@ class Win32Window final : public Window { void CreateWindow(const WindowProps& props); void ProcessEvents(); + static HICON LoadAppIcon(int size); + private: HWND m_Hwnd = nullptr; HINSTANCE m_Instance = nullptr; diff --git a/Engine/vendor/imgui b/Engine/vendor/imgui new file mode 160000 index 0000000..2a1b69f --- /dev/null +++ b/Engine/vendor/imgui @@ -0,0 +1 @@ +Subproject commit 2a1b69f05748ad909f03acf4533447cac1331611 diff --git a/Engine/vendor/spdlog b/Engine/vendor/spdlog new file mode 160000 index 0000000..7e635fc --- /dev/null +++ b/Engine/vendor/spdlog @@ -0,0 +1 @@ +Subproject commit 7e635fca68d014934b4af8a1cf874f63989352b7 diff --git a/Images/128x128.png b/Images/128x128.png new file mode 100644 index 0000000..7f80d25 Binary files /dev/null and b/Images/128x128.png differ diff --git a/Images/64x64.png b/Images/64x64.png new file mode 100644 index 0000000..28694c7 Binary files /dev/null and b/Images/64x64.png differ diff --git a/Images/favicon.ico b/Images/favicon.ico new file mode 100644 index 0000000..d1e1dd1 Binary files /dev/null and b/Images/favicon.ico differ diff --git a/README.md b/README.md index e96e3b8..7fc5389 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,21 @@ -# Cheat Stengine +

+ Cheat Stengine Logo + Cheat Stengine +

-Cheat Stengine is a reverse engineering tool for Windows processes. +Cheat Stengine is a reverse engineering tool for Windows The goal is to create a tool that has a better user experience than Cheat Engine, while still providing powerful -features for reverse engineering and game hacking. +features for reverse engineering, debugging, memory analysis, and game hacking. > [!WARNING] > Cheat Stengine is still in the very early stages of development. Many features are not yet implemented, and the tool -> may be unstable. +> may be unstable. Expect bugs, crashes, and breaking changes between versions. +> +> If you're on an AMD, you might face some issues. If you do, please copy the generated dump file and create an issue +> with any relevant information that could help us reproduce the problem. +> +> We are not responsible for individual data loss, system instability, or other issues caused by using Cheat Stengine. +> Use it at your own risk. ## Preview @@ -16,32 +25,50 @@ features for reverse engineering and game hacking. ## Compiling from Source -```console +```bash cmake -S . -B build cmake --build build --config Release ``` +## MCP Server + +Cheat Stengine comes with a built-in MCP server so an AI agent can drive the +engine: list and attach processes, read/write and scan memory, disassemble, +assemble, dissect structs, and generate byte signatures + +The server starts automatically on launch +To configure open the **MCP** tab in Settings +(`Ctrl+,`) to: + +* toggle the server on or off +* require the bearer token (off by default) +* copy the endpoint and token +* reinstall the client configs for Claude Code, Cursor, Claude Desktop, Codex + and the skill(optional but saves you around 100 tokens by explaining the ai how tools work) + ## Features -- [x] Pattern Scanner -- [x] Disassembler -- [x] Assembler -- [x] Struct Dissector -- [x] Memory Scanner -- [x] Address Watcher -- [x] Module List -- [x] PE Viewer -- [x] Kernel Mode -- [x] Pattern Generator -- [ ] Process Dumper -- [ ] String Scanner/Viewer -- [ ] Code cave scanner -- [ ] Code injection -- [ ] Syscall Tracer -- [ ] Lua scripting -- [ ] Pointer Scanner -- [ ] Memory Viewer -- [ ] Debugger -- [ ] Thread Explorer -- [ ] Handle Viewer -- [ ] Plugin System +* [x] Pattern Scanner +* [x] MCP Support +* [x] Disassembler +* [x] Assembler +* [x] Struct Dissector +* [x] Memory Scanner +* [x] Address Watcher +* [x] Module List +* [x] PE Viewer +* [x] Kernel Mode +* [x] Pattern Generator +* [ ] Process Dumper +* [ ] String Scanner/Viewer +* [ ] Code Cave Scanner +* [ ] Code Injection +* [ ] Syscall Tracer +* [ ] Lua Scripting +* [ ] Pointer Scanner +* [ ] Memory Viewer +* [ ] Debugger +* [ ] Thread Explorer +* [ ] Handle Viewer +* [ ] Plugin System +* [ ] DBVM \ No newline at end of file diff --git a/SKILL.md b/SKILL.md new file mode 100644 index 0000000..f14c282 --- /dev/null +++ b/SKILL.md @@ -0,0 +1,29 @@ +--- +name: cheatstengine +description: Drive Cheat Stengine (a Windows reverse-engineering tool) over MCP: list/attach processes, read/write and scan memory, disassemble, assemble, dissect structs, and generate byte signatures. Use when inspecting or modifying a running process's memory on this machine. +--- + +# Cheat Stengine MCP + +This tool exposes a live reverse-engineering engine over MCP at `http://127.0.0.1:13777/mcp`. +Auth is off by default, so you can connect with just the URL. If the user turns auth on, send `Authorization: Bearer 2187c0e6df08f002e36a6303069998a5341e94ade9aec213cf81cfe083229c3a`. + +## Workflow + +1. `list_processes` - find the target's pid. +2. `open_process` { pid } - attach. Everything below needs an attached process. +3. Inspect: `list_modules`, `read_memory`, `query_memory`, `disassemble`, `dissect_struct`. +4. Resolve addresses symbolically with `resolve_address` { expression: "module.dll+0x1234" }; most tools also accept an expression string directly in place of a numeric address. +5. Modify: `write_memory` { address, hex }, or `assemble` { source, address, write: true }. + +## Async tools (poll, don't block) + +- `pattern_scan` { pattern } returns a `jobId`. Poll `job_status` { id } until status is `completed`, then read `result.matches`. +- `scan_value` { valueType, scanType, value } starts a value scan. Poll `scan_results` until `scanning` is false. Refine with `scan_value` { ..., next: true }. + +## Notes + +- Addresses in results are hex strings like `0x7FF6...`. +- Value types: int8/16/32/64, uint8/16/32/64, float, double. +- Scan types: exact, bigger, smaller, between, unknown. +- `pattern` is space-separated hex with `??` wildcards, e.g. `48 8B ?? C3`. diff --git a/SampleApp/CMakeLists.txt b/SampleApp/CMakeLists.txt index e128067..0fbabcd 100644 --- a/SampleApp/CMakeLists.txt +++ b/SampleApp/CMakeLists.txt @@ -1,9 +1,10 @@ project(SampleApp) -file(GLOB_RECURSE SOURCE_FILES src/*.cpp) -add_executable(${PROJECT_NAME} ${SOURCE_FILES}) +add_executable(${PROJECT_NAME} + src/SampleApp/main.cpp +) target_include_directories(${PROJECT_NAME} PUBLIC src) target_link_libraries(${PROJECT_NAME} PUBLIC Engine -) \ No newline at end of file +) diff --git a/Shared/Protocol.h b/Shared/Protocol.h new file mode 100644 index 0000000..c8ed1c3 --- /dev/null +++ b/Shared/Protocol.h @@ -0,0 +1,255 @@ +#pragma once + +#ifdef _KMODE +#include +#else +#include +#include +#endif + +#include + +namespace Protocol { + +inline constexpr uint16_t VersionMajor = 1; +inline constexpr uint16_t VersionMinor = 2; +inline constexpr uint16_t DriverVersionMajor = 1; +inline constexpr uint16_t DriverVersionMinor = 0; +inline constexpr uint32_t DriverVersionPatch = 0; +inline constexpr uint32_t DeviceType = 0x8000; +inline constexpr uint64_t MaximumTransferSize = 16ULL * 1024ULL * 1024ULL; +inline constexpr uint32_t DefaultIdleTimeoutSeconds = 60; +inline constexpr uint32_t MinimumIdleTimeoutSeconds = 15; +inline constexpr uint32_t MaximumIdleTimeoutSeconds = 3600; + +inline constexpr GUID DeviceInterfaceGuid = { + 0x8d3f3f2d, + 0x3d46, + 0x4a66, + { 0x8f, 0x9b, 0x46, 0xe6, 0xd5, 0xa6, 0x78, 0x31 } +}; + +enum Capability : uint64_t { + BindProcess = 1ULL << 0, + ReadMemory = 1ULL << 1, + WriteMemory = 1ULL << 2, + QueryMemory = 1ULL << 3, + AllocateMemory = 1ULL << 4, + FreeMemory = 1ULL << 5, + ProtectMemory = 1ULL << 6, + Ping = 1ULL << 7, + Heartbeat = 1ULL << 8, + Configure = 1ULL << 9, +}; + +inline constexpr uint64_t RequiredCapabilities = BindProcess + | ReadMemory + | WriteMemory + | QueryMemory + | AllocateMemory + | FreeMemory + | Ping + | Heartbeat + | Configure; + +inline constexpr uint64_t Capabilities = RequiredCapabilities | ProtectMemory; + +enum class LoadMode : uint32_t { + Unknown, + Pnp, + Service, + Mapped, +}; + +enum class RuntimeState : uint32_t { + Starting, + Ready, + Idle, + Stopping, + Removed, +}; + +struct PacketHeader { + uint32_t Size; + uint16_t MajorVersion; + uint16_t MinorVersion; + uint32_t Flags; + uint32_t Reserved; +}; + +struct InformationRequest { + PacketHeader Header; +}; + +struct InformationResponse { + PacketHeader Header; + uint64_t Capabilities; + uint64_t MaximumTransferSize; + uint32_t LoadMode; + uint32_t Reserved; +}; + +struct PingRequest { + PacketHeader Header; + uint64_t Nonce; +}; + +struct PingResponse { + PacketHeader Header; + uint64_t Nonce; + uint64_t KernelTime; + uint64_t Capabilities; + uint64_t MaximumTransferSize; + uint16_t DriverMajorVersion; + uint16_t DriverMinorVersion; + uint32_t DriverPatchVersion; + uint32_t ActiveSessions; + uint32_t IdleTimeoutSeconds; + uint32_t RemainingIdleSeconds; + uint32_t LoadMode; + uint32_t State; + uint32_t Reserved; +}; + +struct HeartbeatRequest { + PacketHeader Header; + uint64_t Sequence; +}; + +struct HeartbeatResponse { + PacketHeader Header; + uint64_t Sequence; + uint64_t KernelTime; + uint32_t RemainingIdleSeconds; + uint32_t State; +}; + +struct ConfigureRequest { + PacketHeader Header; + uint32_t IdleTimeoutSeconds; + uint32_t Reserved; +}; + +struct ConfigureResponse { + PacketHeader Header; + uint32_t IdleTimeoutSeconds; + uint32_t Reserved; +}; + +struct BindProcessRequest { + PacketHeader Header; + uint32_t ProcessId; + uint32_t Reserved; +}; + +struct ReadMemoryRequest { + PacketHeader Header; + uint64_t Address; + uint64_t Size; +}; + +struct WriteMemoryRequest { + PacketHeader Header; + uint64_t Address; + uint64_t Size; +}; + +struct QueryMemoryRequest { + PacketHeader Header; + uint64_t Address; +}; + +struct QueryMemoryResponse { + PacketHeader Header; + uint64_t BaseAddress; + uint64_t AllocationBase; + uint64_t RegionSize; + uint32_t AllocationProtect; + uint32_t State; + uint32_t Protect; + uint32_t Type; +}; + +struct AllocateMemoryRequest { + PacketHeader Header; + uint64_t Address; + uint64_t Size; + uint32_t AllocationType; + uint32_t Protect; +}; + +struct AllocateMemoryResponse { + PacketHeader Header; + uint64_t Address; + uint64_t Size; +}; + +struct FreeMemoryRequest { + PacketHeader Header; + uint64_t Address; + uint64_t Size; + uint32_t FreeType; + uint32_t Reserved; +}; + +struct ProtectMemoryRequest { + PacketHeader Header; + uint64_t Address; + uint64_t Size; + uint32_t Protect; + uint32_t Reserved; +}; + +struct ProtectMemoryResponse { + PacketHeader Header; + uint64_t Address; + uint64_t Size; + uint32_t OldProtect; + uint32_t Reserved; +}; + +inline constexpr uint32_t InformationControlCode = CTL_CODE(DeviceType, 0x800, METHOD_BUFFERED, FILE_READ_ACCESS); +inline constexpr uint32_t BindProcessControlCode = CTL_CODE(DeviceType, 0x801, METHOD_BUFFERED, FILE_READ_ACCESS | FILE_WRITE_ACCESS); +inline constexpr uint32_t ReadMemoryControlCode = CTL_CODE(DeviceType, 0x802, METHOD_OUT_DIRECT, FILE_READ_ACCESS); +inline constexpr uint32_t WriteMemoryControlCode = CTL_CODE(DeviceType, 0x803, METHOD_IN_DIRECT, FILE_WRITE_ACCESS); +inline constexpr uint32_t QueryMemoryControlCode = CTL_CODE(DeviceType, 0x804, METHOD_BUFFERED, FILE_READ_ACCESS); +inline constexpr uint32_t AllocateMemoryControlCode = CTL_CODE(DeviceType, 0x805, METHOD_BUFFERED, FILE_WRITE_ACCESS); +inline constexpr uint32_t FreeMemoryControlCode = CTL_CODE(DeviceType, 0x806, METHOD_BUFFERED, FILE_WRITE_ACCESS); +inline constexpr uint32_t ProtectMemoryControlCode = CTL_CODE(DeviceType, 0x807, METHOD_BUFFERED, FILE_WRITE_ACCESS); +inline constexpr uint32_t PingControlCode = CTL_CODE(DeviceType, 0x808, METHOD_BUFFERED, FILE_READ_ACCESS); +inline constexpr uint32_t HeartbeatControlCode = CTL_CODE(DeviceType, 0x809, METHOD_BUFFERED, FILE_READ_ACCESS | FILE_WRITE_ACCESS); +inline constexpr uint32_t ConfigureControlCode = CTL_CODE(DeviceType, 0x80A, METHOD_BUFFERED, FILE_WRITE_ACCESS); + +template +constexpr PacketHeader CreateHeader() +{ + return { + static_cast(sizeof(T)), + VersionMajor, + VersionMinor, + 0, + 0, + }; +} + +static_assert(sizeof(PacketHeader) == 16); +static_assert(sizeof(InformationRequest) == 16); +static_assert(sizeof(InformationResponse) == 40); +static_assert(sizeof(PingRequest) == 24); +static_assert(sizeof(PingResponse) == 80); +static_assert(sizeof(HeartbeatRequest) == 24); +static_assert(sizeof(HeartbeatResponse) == 40); +static_assert(sizeof(ConfigureRequest) == 24); +static_assert(sizeof(ConfigureResponse) == 24); +static_assert(sizeof(BindProcessRequest) == 24); +static_assert(sizeof(ReadMemoryRequest) == 32); +static_assert(sizeof(WriteMemoryRequest) == 32); +static_assert(sizeof(QueryMemoryRequest) == 24); +static_assert(sizeof(QueryMemoryResponse) == 56); +static_assert(sizeof(AllocateMemoryRequest) == 40); +static_assert(sizeof(AllocateMemoryResponse) == 32); +static_assert(sizeof(FreeMemoryRequest) == 40); +static_assert(sizeof(ProtectMemoryRequest) == 40); +static_assert(sizeof(ProtectMemoryResponse) == 40); + +}