detour_thread_transaction.cpp 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  1. #include "detour_thread_transaction.h"
  2. #include <Windows.h>
  3. #include <TlHelp32.h>
  4. #include <detours.h>
  5. #include "../../../process/freeze/client_process_freeze.h"
  6. namespace sunrise::client::hooking::detour::transaction {
  7. namespace {
  8. /** 4 protected functions per hook bound the fixed range storage, so no heap is used. */
  9. constexpr std::size_t kProtectedCodeLimit = 64;
  10. /** Exact executable range described by one x64 unwind record. */
  11. struct CodeRange {
  12. DWORD64 begin{};
  13. DWORD64 end{};
  14. };
  15. /**
  16. * Closes every held thread handle after Detours resumes the threads.
  17. * @param threads Thread handles to close and clear.
  18. */
  19. void close_threads(Threads& threads) noexcept {
  20. for (std::size_t index = 0; index < threads.count; ++index) {
  21. CloseHandle(threads.handles[index]);
  22. }
  23. threads = {};
  24. }
  25. /**
  26. * Checks whether one thread id was already enlisted by an earlier snapshot.
  27. * @param threads Threads kept suspended by the active transaction.
  28. * @param threadId Candidate process thread id.
  29. * @return True when the thread is already enlisted.
  30. */
  31. [[nodiscard]] bool contains(const Threads& threads, DWORD threadId) noexcept {
  32. for (std::size_t index = 0; index < threads.count; ++index) {
  33. if (threads.ids[index] == threadId) {
  34. return true;
  35. }
  36. }
  37. return false;
  38. }
  39. /**
  40. * Enlists every unseen thread present in one process-wide snapshot.
  41. * @param threads Receives handles that stay suspended until the transaction ends.
  42. * @param foundUnseen Receives true when this pass saw any new thread id.
  43. * @return True when the whole snapshot was inspected without a hard failure.
  44. */
  45. [[nodiscard]] bool enlist_snapshot(Threads& threads, bool& foundUnseen) noexcept {
  46. foundUnseen = false;
  47. const HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
  48. if (snapshot == INVALID_HANDLE_VALUE) {
  49. return false;
  50. }
  51. THREADENTRY32 entry{};
  52. entry.dwSize = sizeof(entry);
  53. BOOL available = Thread32First(snapshot, &entry);
  54. const DWORD processId = GetCurrentProcessId();
  55. const DWORD currentThreadId = GetCurrentThreadId();
  56. bool succeeded = true;
  57. while (available != FALSE && succeeded) {
  58. const bool belongsToProcess = entry.th32OwnerProcessID == processId;
  59. const bool needsEnlistment =
  60. entry.th32ThreadID != currentThreadId && !contains(threads, entry.th32ThreadID);
  61. if (belongsToProcess && needsEnlistment) {
  62. foundUnseen = true;
  63. if (threads.count == threads.handles.size()) {
  64. succeeded = false;
  65. break;
  66. }
  67. const HANDLE thread =
  68. OpenThread(THREAD_SUSPEND_RESUME | THREAD_GET_CONTEXT | THREAD_SET_CONTEXT,
  69. FALSE,
  70. entry.th32ThreadID);
  71. if (thread == nullptr) {
  72. // A disappearing thread is absent from the next stable snapshot.
  73. if (GetLastError() != ERROR_INVALID_PARAMETER) {
  74. succeeded = false;
  75. }
  76. } else if (DetourUpdateThread(thread) != NO_ERROR) {
  77. CloseHandle(thread);
  78. succeeded = false;
  79. } else {
  80. threads.handles[threads.count] = thread;
  81. threads.ids[threads.count] = entry.th32ThreadID;
  82. ++threads.count;
  83. }
  84. }
  85. available = Thread32Next(snapshot, &entry);
  86. }
  87. if (succeeded && available == FALSE && GetLastError() != ERROR_NO_MORE_FILES) {
  88. succeeded = false;
  89. }
  90. CloseHandle(snapshot);
  91. return succeeded;
  92. }
  93. /**
  94. * Enlists new process threads until a full snapshot finds no unseen thread id.
  95. * @param threads Receives every handle the transaction holds.
  96. * @return True when a full pass found no new thread.
  97. */
  98. [[nodiscard]] bool enlist_until_stable(Threads& threads) noexcept {
  99. bool foundUnseen{};
  100. do {
  101. if (!enlist_snapshot(threads, foundUnseen)) {
  102. return false;
  103. }
  104. // Earlier handles stay suspended while a later pass finds newly created threads.
  105. } while (foundUnseen);
  106. return true;
  107. }
  108. /** @param protection Windows page protection. @return True for executable page types. */
  109. [[nodiscard]] bool is_executable(DWORD protection) noexcept {
  110. /** The low byte stores PAGE_* type while higher bits store modifiers. */
  111. constexpr DWORD kPageTypeMask = 0xFF;
  112. switch (protection & kPageTypeMask) {
  113. case PAGE_EXECUTE:
  114. case PAGE_EXECUTE_READ:
  115. case PAGE_EXECUTE_READWRITE:
  116. case PAGE_EXECUTE_WRITECOPY:
  117. return true;
  118. default:
  119. return false;
  120. }
  121. }
  122. /**
  123. * Finds the canonical unwind-backed function range of one protected entry.
  124. * @param entry Protected function entry given by the hook owner.
  125. * @param range Receives the exact executable range.
  126. * @return True when both the entry and canonical code have a valid x64 unwind record.
  127. */
  128. [[nodiscard]] bool resolve_range(const ProtectedCodeEntry& entry, CodeRange& range) noexcept {
  129. range = {};
  130. if (entry.address == nullptr) {
  131. return false;
  132. }
  133. MEMORY_BASIC_INFORMATION entryMemory{};
  134. if (VirtualQuery(entry.address, &entryMemory, sizeof(entryMemory)) != sizeof(entryMemory)
  135. || entryMemory.State != MEM_COMMIT || !is_executable(entryMemory.Protect)) {
  136. return false;
  137. }
  138. void* const code = DetourCodeFromPointer(entry.address, nullptr);
  139. MEMORY_BASIC_INFORMATION codeMemory{};
  140. if (code == nullptr || VirtualQuery(code, &codeMemory, sizeof(codeMemory)) != sizeof(codeMemory)
  141. || codeMemory.State != MEM_COMMIT || !is_executable(codeMemory.Protect)) {
  142. return false;
  143. }
  144. const DWORD64 codeAddress = reinterpret_cast<DWORD64>(code);
  145. DWORD64 imageBase{};
  146. const RUNTIME_FUNCTION* function = RtlLookupFunctionEntry(codeAddress, &imageBase, nullptr);
  147. if (function == nullptr) {
  148. return false;
  149. }
  150. range = {imageBase + function->BeginAddress, imageBase + function->EndAddress};
  151. return range.begin < range.end && codeAddress >= range.begin && codeAddress < range.end;
  152. }
  153. } // namespace
  154. /** Starts a Detours transaction and enlists process threads to a stable snapshot. */
  155. bool begin(Threads& threads) noexcept {
  156. threads = {};
  157. // Detours suspends these threads at commit. Another suspender running at the same time
  158. // would freeze this thread, and then neither side can finish.
  159. process::freeze::enter_exclusive();
  160. if (DetourTransactionBegin() != NO_ERROR) {
  161. process::freeze::leave_exclusive();
  162. return false;
  163. }
  164. if (DetourUpdateThread(GetCurrentThread()) != NO_ERROR || !enlist_until_stable(threads)) {
  165. (void)DetourTransactionAbort();
  166. close_threads(threads);
  167. process::freeze::leave_exclusive();
  168. return false;
  169. }
  170. return true;
  171. }
  172. /** Aborts the active Detours transaction before releasing enlisted thread handles. */
  173. bool abort(Threads& threads) noexcept {
  174. const bool aborted = DetourTransactionAbort() == NO_ERROR;
  175. close_threads(threads);
  176. process::freeze::leave_exclusive();
  177. return aborted;
  178. }
  179. /** Commits the active Detours transaction before releasing enlisted thread handles. */
  180. bool commit(Threads& threads) noexcept {
  181. const bool committed = DetourTransactionCommit() == NO_ERROR;
  182. close_threads(threads);
  183. process::freeze::leave_exclusive();
  184. return committed;
  185. }
  186. /** Finds the protected function ranges and checks every suspended instruction pointer. */
  187. InspectionResult inspect(const Threads& threads,
  188. std::span<const ProtectedCodeEntry> entries) noexcept {
  189. if (entries.empty() || entries.size() > kProtectedCodeLimit) {
  190. return InspectionResult::failed;
  191. }
  192. std::array<CodeRange, kProtectedCodeLimit> ranges{};
  193. for (std::size_t index = 0; index < entries.size(); ++index) {
  194. if (!resolve_range(entries[index], ranges[index])) {
  195. return InspectionResult::failed;
  196. }
  197. }
  198. for (std::size_t threadIndex = 0; threadIndex < threads.count; ++threadIndex) {
  199. CONTEXT context{};
  200. context.ContextFlags = CONTEXT_CONTROL;
  201. if (GetThreadContext(threads.handles[threadIndex], &context) == FALSE) {
  202. return InspectionResult::failed;
  203. }
  204. for (std::size_t rangeIndex = 0; rangeIndex < entries.size(); ++rangeIndex) {
  205. const CodeRange range = ranges[rangeIndex];
  206. if (context.Rip >= range.begin && context.Rip < range.end) {
  207. return InspectionResult::protectedCodeActive;
  208. }
  209. }
  210. }
  211. return InspectionResult::clear;
  212. }
  213. } // namespace sunrise::client::hooking::detour::transaction