composition_check.cpp 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150
  1. #include <array>
  2. #include <atomic>
  3. #include <cstddef>
  4. #include <cstdint>
  5. #include <cstdio>
  6. #include <cstring>
  7. #include <string_view>
  8. #include "../../../core/logging/log.h"
  9. #include "../../hooking/detour.h"
  10. #include "internal.h"
  11. namespace sunrise::client::hooks::bootflow {
  12. namespace {
  13. /**
  14. * The matchmaking composition check. Its prologue repeats across the image, so the pattern runs
  15. * on to the properties byte read, which is unique to this function. Every branch displacement is
  16. * wildcarded.
  17. */
  18. constexpr std::string_view kCheckSignatureText =
  19. "48 89 5C 24 ? 57 48 83 EC ? 48 8B DA 48 8B F9 48 85 C9 0F 84 ? ? ? ? 48 85 D2 0F 84 ? ? ? ? "
  20. "0F B6 82 A2 02 00 00";
  21. /** Compiled pattern bytes of the signature text above. */
  22. constexpr auto kCheckSignature =
  23. signature<signature_length(kCheckSignatureText)>(kCheckSignatureText);
  24. /** Fields of the composition properties this hook touches, as byte offsets from their base. */
  25. struct PropertiesLayout {
  26. /**
  27. * Big fireteam count, signed 32-bit. The check fails the composition when it is over the set
  28. * cap. Both operands are local, so only this field can change the result.
  29. */
  30. static constexpr std::size_t bigFireteamCount = 20;
  31. };
  32. /** Count that clears the cap comparison for every configured cap. */
  33. constexpr std::int32_t kSolo = 0;
  34. /** Check result meaning an argument was null; also returned when the trampoline is gone. */
  35. constexpr std::int64_t kNullArgument = 3;
  36. /**
  37. * Lines allowed per run. The check runs on every composition test, so an uncapped report buries
  38. * the rest of the log. This budget still shows the count the boot started with.
  39. */
  40. constexpr unsigned kMaxReports = 4;
  41. /** Size of one zeroing line, set by its stage and count fields. */
  42. constexpr std::size_t kLineCapacity = 96;
  43. using Check = std::int64_t(__fastcall*)(void*, std::byte*);
  44. hooking::detour::Handle g_handle{};
  45. std::atomic<Check> g_original{nullptr};
  46. std::atomic<unsigned> g_reported{0};
  47. /**
  48. * Emits one zeroing event while the per-run budget lasts.
  49. * @param count The count that was replaced.
  50. */
  51. void report(std::int32_t count) noexcept {
  52. // One atomic claim per line, so a concurrent check cannot reuse a budget slot.
  53. if (g_reported.fetch_add(1, std::memory_order_relaxed) >= kMaxReports) {
  54. return;
  55. }
  56. std::array<char, kLineCapacity> line{};
  57. const int written = std::snprintf(line.data(),
  58. line.size(),
  59. "ev=bootflow stage=composition result=zeroed count=%d",
  60. static_cast<int>(count));
  61. if (written > 0) {
  62. core::log::write(core::log::Channel::client,
  63. core::log::Level::info,
  64. {line.data(), static_cast<std::size_t>(written)});
  65. }
  66. }
  67. /**
  68. * Clears the big fireteam count so the composition check passes.
  69. * The write repeats on every call because the field's producer rewrites it each run.
  70. * Nothing between entry and the compare rebuilds it, so the entry write is the one compared.
  71. * @param config Borrowed composition config, passed through untouched.
  72. * @param props Borrowed composition properties whose count is cleared.
  73. * @return The check's own result, or the null-argument result when the trampoline is gone.
  74. */
  75. __declspec(noinline) std::int64_t __fastcall check(void* config, std::byte* props) noexcept {
  76. const Check original = g_original.load(std::memory_order_acquire);
  77. if (original == nullptr) {
  78. return kNullArgument;
  79. }
  80. if (props == nullptr) {
  81. return original(config, props);
  82. }
  83. std::int32_t count = 0;
  84. std::memcpy(&count, props + PropertiesLayout::bigFireteamCount, sizeof count);
  85. std::memcpy(props + PropertiesLayout::bigFireteamCount, &kSolo, sizeof kSolo);
  86. if (count != kSolo) {
  87. report(count);
  88. }
  89. return original(config, props);
  90. }
  91. } // namespace
  92. /**
  93. * Stages the solo composition fix.
  94. * @param spec Receives the target and replacement.
  95. * @return staged when the target is found, unavailable on a miss.
  96. */
  97. StageResult stage_composition_check(hooking::detour::Spec& spec) noexcept {
  98. if (g_handle.attached) {
  99. return StageResult::attached;
  100. }
  101. std::byte* const target = scan_main_image_unique(kCheckSignature, "matchmaking_composition");
  102. if (target == nullptr) {
  103. core::log::write(core::log::Channel::client,
  104. core::log::Level::warn,
  105. "ev=bootflow stage=composition result=fail reason=target");
  106. return StageResult::unavailable;
  107. }
  108. spec = hooking::detour::Spec{target, reinterpret_cast<void*>(&check)};
  109. return StageResult::staged;
  110. }
  111. /** Takes the solo composition fix's attached handle, or a detached one. */
  112. void publish_composition_check(const hooking::detour::Handle& handle) noexcept {
  113. if (!handle.attached) {
  114. core::log::write(core::log::Channel::client,
  115. core::log::Level::warn,
  116. "ev=bootflow stage=composition result=fail reason=attach");
  117. return;
  118. }
  119. g_handle = handle;
  120. g_original.store(reinterpret_cast<Check>(g_handle.original), std::memory_order_release);
  121. core::log::write(core::log::Channel::client,
  122. core::log::Level::info,
  123. "ev=bootflow stage=composition result=ok");
  124. }
  125. /** Detaches the solo composition fix. */
  126. void uninstall_composition_check() noexcept {
  127. if (g_handle.attached) {
  128. (void)hooking::detour::uninstall(g_handle);
  129. }
  130. g_original.store(nullptr, std::memory_order_release);
  131. g_reported.store(0, std::memory_order_release);
  132. }
  133. } // namespace sunrise::client::hooks::bootflow