| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240 |
- #include "detour_thread_transaction.h"
- #include <Windows.h>
- #include <TlHelp32.h>
- #include <detours.h>
- #include "../../../process/freeze/client_process_freeze.h"
- namespace sunrise::client::hooking::detour::transaction {
- namespace {
- /** 4 protected functions per hook bound the fixed range storage, so no heap is used. */
- constexpr std::size_t kProtectedCodeLimit = 64;
- /** Exact executable range described by one x64 unwind record. */
- struct CodeRange {
- DWORD64 begin{};
- DWORD64 end{};
- };
- /**
- * Closes every held thread handle after Detours resumes the threads.
- * @param threads Thread handles to close and clear.
- */
- void close_threads(Threads& threads) noexcept {
- for (std::size_t index = 0; index < threads.count; ++index) {
- CloseHandle(threads.handles[index]);
- }
- threads = {};
- }
- /**
- * Checks whether one thread id was already enlisted by an earlier snapshot.
- * @param threads Threads kept suspended by the active transaction.
- * @param threadId Candidate process thread id.
- * @return True when the thread is already enlisted.
- */
- [[nodiscard]] bool contains(const Threads& threads, DWORD threadId) noexcept {
- for (std::size_t index = 0; index < threads.count; ++index) {
- if (threads.ids[index] == threadId) {
- return true;
- }
- }
- return false;
- }
- /**
- * Enlists every unseen thread present in one process-wide snapshot.
- * @param threads Receives handles that stay suspended until the transaction ends.
- * @param foundUnseen Receives true when this pass saw any new thread id.
- * @return True when the whole snapshot was inspected without a hard failure.
- */
- [[nodiscard]] bool enlist_snapshot(Threads& threads, bool& foundUnseen) noexcept {
- foundUnseen = false;
- const HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
- if (snapshot == INVALID_HANDLE_VALUE) {
- return false;
- }
- THREADENTRY32 entry{};
- entry.dwSize = sizeof(entry);
- BOOL available = Thread32First(snapshot, &entry);
- const DWORD processId = GetCurrentProcessId();
- const DWORD currentThreadId = GetCurrentThreadId();
- bool succeeded = true;
- while (available != FALSE && succeeded) {
- const bool belongsToProcess = entry.th32OwnerProcessID == processId;
- const bool needsEnlistment =
- entry.th32ThreadID != currentThreadId && !contains(threads, entry.th32ThreadID);
- if (belongsToProcess && needsEnlistment) {
- foundUnseen = true;
- if (threads.count == threads.handles.size()) {
- succeeded = false;
- break;
- }
- const HANDLE thread =
- OpenThread(THREAD_SUSPEND_RESUME | THREAD_GET_CONTEXT | THREAD_SET_CONTEXT,
- FALSE,
- entry.th32ThreadID);
- if (thread == nullptr) {
- // A disappearing thread is absent from the next stable snapshot.
- if (GetLastError() != ERROR_INVALID_PARAMETER) {
- succeeded = false;
- }
- } else if (DetourUpdateThread(thread) != NO_ERROR) {
- CloseHandle(thread);
- succeeded = false;
- } else {
- threads.handles[threads.count] = thread;
- threads.ids[threads.count] = entry.th32ThreadID;
- ++threads.count;
- }
- }
- available = Thread32Next(snapshot, &entry);
- }
- if (succeeded && available == FALSE && GetLastError() != ERROR_NO_MORE_FILES) {
- succeeded = false;
- }
- CloseHandle(snapshot);
- return succeeded;
- }
- /**
- * Enlists new process threads until a full snapshot finds no unseen thread id.
- * @param threads Receives every handle the transaction holds.
- * @return True when a full pass found no new thread.
- */
- [[nodiscard]] bool enlist_until_stable(Threads& threads) noexcept {
- bool foundUnseen{};
- do {
- if (!enlist_snapshot(threads, foundUnseen)) {
- return false;
- }
- // Earlier handles stay suspended while a later pass finds newly created threads.
- } while (foundUnseen);
- return true;
- }
- /** @param protection Windows page protection. @return True for executable page types. */
- [[nodiscard]] bool is_executable(DWORD protection) noexcept {
- /** The low byte stores PAGE_* type while higher bits store modifiers. */
- constexpr DWORD kPageTypeMask = 0xFF;
- switch (protection & kPageTypeMask) {
- case PAGE_EXECUTE:
- case PAGE_EXECUTE_READ:
- case PAGE_EXECUTE_READWRITE:
- case PAGE_EXECUTE_WRITECOPY:
- return true;
- default:
- return false;
- }
- }
- /**
- * Finds the canonical unwind-backed function range of one protected entry.
- * @param entry Protected function entry given by the hook owner.
- * @param range Receives the exact executable range.
- * @return True when both the entry and canonical code have a valid x64 unwind record.
- */
- [[nodiscard]] bool resolve_range(const ProtectedCodeEntry& entry, CodeRange& range) noexcept {
- range = {};
- if (entry.address == nullptr) {
- return false;
- }
- MEMORY_BASIC_INFORMATION entryMemory{};
- if (VirtualQuery(entry.address, &entryMemory, sizeof(entryMemory)) != sizeof(entryMemory)
- || entryMemory.State != MEM_COMMIT || !is_executable(entryMemory.Protect)) {
- return false;
- }
- void* const code = DetourCodeFromPointer(entry.address, nullptr);
- MEMORY_BASIC_INFORMATION codeMemory{};
- if (code == nullptr || VirtualQuery(code, &codeMemory, sizeof(codeMemory)) != sizeof(codeMemory)
- || codeMemory.State != MEM_COMMIT || !is_executable(codeMemory.Protect)) {
- return false;
- }
- const DWORD64 codeAddress = reinterpret_cast<DWORD64>(code);
- DWORD64 imageBase{};
- const RUNTIME_FUNCTION* function = RtlLookupFunctionEntry(codeAddress, &imageBase, nullptr);
- if (function == nullptr) {
- return false;
- }
- range = {imageBase + function->BeginAddress, imageBase + function->EndAddress};
- return range.begin < range.end && codeAddress >= range.begin && codeAddress < range.end;
- }
- } // namespace
- /** Starts a Detours transaction and enlists process threads to a stable snapshot. */
- bool begin(Threads& threads) noexcept {
- threads = {};
- // Detours suspends these threads at commit. Another suspender running at the same time
- // would freeze this thread, and then neither side can finish.
- process::freeze::enter_exclusive();
- if (DetourTransactionBegin() != NO_ERROR) {
- process::freeze::leave_exclusive();
- return false;
- }
- if (DetourUpdateThread(GetCurrentThread()) != NO_ERROR || !enlist_until_stable(threads)) {
- (void)DetourTransactionAbort();
- close_threads(threads);
- process::freeze::leave_exclusive();
- return false;
- }
- return true;
- }
- /** Aborts the active Detours transaction before releasing enlisted thread handles. */
- bool abort(Threads& threads) noexcept {
- const bool aborted = DetourTransactionAbort() == NO_ERROR;
- close_threads(threads);
- process::freeze::leave_exclusive();
- return aborted;
- }
- /** Commits the active Detours transaction before releasing enlisted thread handles. */
- bool commit(Threads& threads) noexcept {
- const bool committed = DetourTransactionCommit() == NO_ERROR;
- close_threads(threads);
- process::freeze::leave_exclusive();
- return committed;
- }
- /** Finds the protected function ranges and checks every suspended instruction pointer. */
- InspectionResult inspect(const Threads& threads,
- std::span<const ProtectedCodeEntry> entries) noexcept {
- if (entries.empty() || entries.size() > kProtectedCodeLimit) {
- return InspectionResult::failed;
- }
- std::array<CodeRange, kProtectedCodeLimit> ranges{};
- for (std::size_t index = 0; index < entries.size(); ++index) {
- if (!resolve_range(entries[index], ranges[index])) {
- return InspectionResult::failed;
- }
- }
- for (std::size_t threadIndex = 0; threadIndex < threads.count; ++threadIndex) {
- CONTEXT context{};
- context.ContextFlags = CONTEXT_CONTROL;
- if (GetThreadContext(threads.handles[threadIndex], &context) == FALSE) {
- return InspectionResult::failed;
- }
- for (std::size_t rangeIndex = 0; rangeIndex < entries.size(); ++rangeIndex) {
- const CodeRange range = ranges[rangeIndex];
- if (context.Rip >= range.begin && context.Rip < range.end) {
- return InspectionResult::protectedCodeActive;
- }
- }
- }
- return InspectionResult::clear;
- }
- } // namespace sunrise::client::hooking::detour::transaction
|