activity_sdk_decode_plans_image.cpp 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311
  1. #include <algorithm>
  2. #include <array>
  3. #include <cstring>
  4. #include <limits>
  5. #include <string_view>
  6. #include "../../patterns/registry.h"
  7. #include "../../patterns/signature_text.h"
  8. #include "activity_sdk_decode_plans_internal.h"
  9. namespace sunrise::client::content::activity::sdk_generation::decode_plans::internal {
  10. namespace {
  11. // Matches the codec walk's primitive size lookup: movsx rcx, r9b; lea rdx, [table];
  12. // mov edx, [rdx+rcx*4]. The table address comes from the wildcarded lea operand.
  13. constexpr std::string_view kPrimitiveSizeLoadText =
  14. "49 0F BE C9 48 8D 15 ? ? ? ? 8B 14 8A 03 D3 3B D6";
  15. /** Compiled pattern bytes of the signature text above. */
  16. constexpr auto kPrimitiveSizeLoad =
  17. patterns::signature<patterns::signature_length(kPrimitiveSizeLoadText)>(kPrimitiveSizeLoadText);
  18. /** The lea operand and the end of the lea, from the start of the match. */
  19. constexpr std::size_t kPrimitiveSizeOperand = 7;
  20. constexpr std::size_t kPrimitiveSizeNext = 11;
  21. /** PE32+ header layout the slot index depends on. */
  22. constexpr std::size_t kNewHeaderOffsetAt = 0x3C;
  23. constexpr std::size_t kSectionCountAt = 6;
  24. constexpr std::size_t kOptionalSizeAt = 20;
  25. constexpr std::size_t kOptionalHeaderAt = 24;
  26. constexpr std::uint16_t kOptionalMagic64 = 0x20B;
  27. constexpr std::size_t kImageBaseAt = 24;
  28. constexpr std::size_t kSizeOfImageAt = 56;
  29. /** The headers and section table of a mapped module fit in its first page. */
  30. constexpr std::size_t kPeHeaderSpan = 4096;
  31. constexpr std::size_t kSectionRowSize = 40;
  32. constexpr std::size_t kSectionCharacteristicsAt = 36;
  33. constexpr std::size_t kSectionLimit = 96;
  34. /** A tagdefs slot is 32 bytes: handle, zero pad, record pointer, 16 more bytes. */
  35. constexpr std::size_t kSlotSize = 32;
  36. constexpr std::size_t kSlotStep = 8;
  37. /** Every bound handle sits in the 0x8080xxxx space. */
  38. constexpr std::uint32_t kHandleSpaceMask = 0xFFFF0000U;
  39. constexpr std::uint32_t kHandleSpace = 0x80800000U;
  40. } // namespace
  41. /** Releases the view, the mapping and the file in that order. */
  42. Image::~Image() noexcept {
  43. if (view_ != nullptr) {
  44. UnmapViewOfFile(view_);
  45. }
  46. if (mapping_ != nullptr) {
  47. CloseHandle(mapping_);
  48. }
  49. if (file_ != INVALID_HANDLE_VALUE) {
  50. CloseHandle(file_);
  51. }
  52. }
  53. /**
  54. * Maps the executable file read-only and indexes the reflection slots.
  55. * @param path Null-terminated executable path, always mapped for the evidence digest.
  56. * @param module Base of the mapped module to read from, or null to read the file.
  57. * @return Status::ready when the headers parse and the slot index is complete.
  58. */
  59. Status Image::open(const wchar_t* path, const void* module) noexcept {
  60. if (path == nullptr || path[0] == L'\0' || view_ != nullptr) {
  61. return Status::invalidInput;
  62. }
  63. file_ = CreateFileW(path,
  64. GENERIC_READ,
  65. FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
  66. nullptr,
  67. OPEN_EXISTING,
  68. FILE_ATTRIBUTE_NORMAL,
  69. nullptr);
  70. if (file_ == INVALID_HANDLE_VALUE) {
  71. return Status::executableUnavailable;
  72. }
  73. LARGE_INTEGER size{};
  74. if (GetFileSizeEx(file_, &size) == FALSE || size.QuadPart <= 0
  75. || static_cast<std::uint64_t>(size.QuadPart) > (std::numeric_limits<std::size_t>::max)()) {
  76. return Status::executableUnavailable;
  77. }
  78. mapping_ = CreateFileMappingW(file_, nullptr, PAGE_READONLY, 0, 0, nullptr);
  79. if (mapping_ == nullptr) {
  80. return Status::executableUnavailable;
  81. }
  82. view_ = static_cast<const std::byte*>(MapViewOfFile(mapping_, FILE_MAP_READ, 0, 0, 0));
  83. if (view_ == nullptr) {
  84. return Status::executableUnavailable;
  85. }
  86. viewSize_ = static_cast<std::size_t>(size.QuadPart);
  87. data_ = module == nullptr ? view_ : static_cast<const std::byte*>(module);
  88. size_ = module == nullptr ? viewSize_ : kPeHeaderSpan;
  89. try {
  90. if (!parse_headers(module != nullptr) || !index_slots() || !resolve_primitive_sizes()) {
  91. return Status::executableInvalid;
  92. }
  93. } catch (...) {
  94. return Status::allocation;
  95. }
  96. return Status::ready;
  97. }
  98. std::span<const std::byte> Image::file() const noexcept {
  99. return view_ == nullptr ? std::span<const std::byte>{} : std::span(view_, viewSize_);
  100. }
  101. std::span<const std::byte> Image::bytes() const noexcept {
  102. return data_ == nullptr ? std::span<const std::byte>{} : std::span(data_, size_);
  103. }
  104. /**
  105. * Reads the PE32+ section table and the image base the slot pointers are relative to.
  106. * A mapped module keeps every section at its virtual address, so raw and virtual coincide there.
  107. * @param mapped True when the bytes are the module in memory.
  108. * @return False when the bytes are not a PE32+ image with a .vmp0 section.
  109. */
  110. bool Image::parse_headers(bool mapped) noexcept {
  111. const std::span<const std::byte> image = bytes();
  112. std::uint32_t headerOffset = 0;
  113. std::uint16_t sectionCount = 0;
  114. std::uint16_t optionalSize = 0;
  115. std::uint16_t magic = 0;
  116. if (image.size() < 2 || image[0] != std::byte{'M'} || image[1] != std::byte{'Z'}
  117. || !read_at(image, kNewHeaderOffsetAt, headerOffset) || headerOffset > image.size() - 4
  118. || std::memcmp(image.data() + headerOffset, "PE\0\0", 4)
  119. || !read_at(image, headerOffset + kSectionCountAt, sectionCount)
  120. || !read_at(image, headerOffset + kOptionalSizeAt, optionalSize)
  121. || !read_at(image, headerOffset + kOptionalHeaderAt, magic) || magic != kOptionalMagic64
  122. || sectionCount == 0 || sectionCount > kSectionLimit
  123. || !read_at(image, headerOffset + kOptionalHeaderAt + kImageBaseAt, imageBase_)) {
  124. return false;
  125. }
  126. if (mapped) {
  127. std::uint32_t sizeOfImage = 0;
  128. if (!read_at(image, headerOffset + kOptionalHeaderAt + kSizeOfImageAt, sizeOfImage)) {
  129. return false;
  130. }
  131. // Slot pointers in memory are relocated, so the module base replaces the header's.
  132. imageBase_ = reinterpret_cast<std::uint64_t>(data_);
  133. size_ = sizeOfImage;
  134. }
  135. const std::size_t table = headerOffset + kOptionalHeaderAt + optionalSize;
  136. bool vmp0 = false;
  137. sections_.clear();
  138. for (std::size_t index = 0; index < sectionCount; ++index) {
  139. const std::size_t row = table + index * kSectionRowSize;
  140. std::uint32_t virtualSize = 0;
  141. std::uint32_t virtualAddress = 0;
  142. std::uint32_t rawSize = 0;
  143. std::uint32_t rawOffset = 0;
  144. std::uint32_t characteristics = 0;
  145. if (row > image.size() || image.size() - row < kSectionRowSize
  146. || !read_at(image, row + 8, virtualSize) || !read_at(image, row + 12, virtualAddress)
  147. || !read_at(image, row + 16, rawSize) || !read_at(image, row + 20, rawOffset)
  148. || !read_at(image, row + kSectionCharacteristicsAt, characteristics)) {
  149. return false;
  150. }
  151. const std::string_view name(reinterpret_cast<const char*>(image.data() + row), 8);
  152. const std::size_t nameLength = name.find('\0');
  153. const std::string_view trimmed = name.substr(0, nameLength);
  154. Section section{virtualAddress,
  155. virtualSize,
  156. mapped ? virtualAddress : rawOffset,
  157. mapped ? virtualSize : rawSize,
  158. trimmed == ".data",
  159. (characteristics & IMAGE_SCN_MEM_EXECUTE) != 0};
  160. if (trimmed == ".vmp0" && !vmp0) {
  161. vmp0 = true;
  162. vmp0Low_ = imageBase_ + virtualAddress;
  163. vmp0High_ = vmp0Low_ + virtualSize;
  164. }
  165. sections_.push_back(section);
  166. }
  167. return vmp0;
  168. }
  169. /**
  170. * Scans every .data section for 32-byte slots {handle, 0, record pointer into .vmp0}.
  171. * @return True; a section that runs past the file is clipped, not refused.
  172. */
  173. bool Image::index_slots() noexcept {
  174. const std::span<const std::byte> image = bytes();
  175. slots_.clear();
  176. for (const Section& section : sections_) {
  177. if (!section.data || section.rawOffset >= image.size()) {
  178. continue;
  179. }
  180. const std::size_t length =
  181. static_cast<std::size_t>((std::min)(section.rawSize, image.size() - section.rawOffset));
  182. const std::span<const std::byte> data = image.subspan(section.rawOffset, length);
  183. for (std::size_t at = 0; at + kSlotSize < length; at += kSlotStep) {
  184. std::uint32_t handle = 0;
  185. std::uint32_t pad = 0;
  186. std::uint64_t record = 0;
  187. if (!read_at(data, at, handle) || !read_at(data, at + 4, pad)
  188. || !read_at(data, at + 8, record)) {
  189. break;
  190. }
  191. if (pad != 0 || (handle & kHandleSpaceMask) != kHandleSpace || record < vmp0Low_
  192. || record >= vmp0High_) {
  193. continue;
  194. }
  195. slots_.try_emplace(handle, record);
  196. }
  197. }
  198. return true;
  199. }
  200. /**
  201. * Finds the primitive size table through the one code site that indexes it.
  202. * @return False when the site is missing or not unique in the executable sections.
  203. */
  204. bool Image::resolve_primitive_sizes() noexcept {
  205. const std::span<const std::byte> image = bytes();
  206. std::array<patterns::ImageRange, kSectionLimit> ranges{};
  207. std::array<const Section*, kSectionLimit> owners{};
  208. std::size_t count = 0;
  209. for (const Section& section : sections_) {
  210. if (!section.code || section.rawSize == 0 || section.rawOffset >= image.size()) {
  211. continue;
  212. }
  213. const std::size_t length =
  214. static_cast<std::size_t>((std::min)(section.rawSize, image.size() - section.rawOffset));
  215. // The scan only reads; the range type is mutable because hook targets share it.
  216. auto* const start = const_cast<std::byte*>(image.data() + section.rawOffset);
  217. ranges[count] = patterns::ImageRange{std::span(start, length)};
  218. owners[count++] = &section;
  219. }
  220. const std::array definitions{patterns::Pattern{"primitive_size_load", kPrimitiveSizeLoad}};
  221. std::array<patterns::Match, 1> matches{};
  222. if (!patterns::resolve_all(std::span(ranges.data(), count), definitions, matches)
  223. || matches[0].status != patterns::MatchStatus::unique) {
  224. return false;
  225. }
  226. for (std::size_t index = 0; index < count; ++index) {
  227. const std::span<std::byte> range = ranges[index].bytes;
  228. if (matches[0].address < range.data()
  229. || matches[0].address >= range.data() + range.size()) {
  230. continue;
  231. }
  232. const auto inside = static_cast<std::uint64_t>(matches[0].address - range.data());
  233. std::int32_t displacement = 0;
  234. std::memcpy(&displacement, matches[0].address + kPrimitiveSizeOperand, sizeof displacement);
  235. const std::uint64_t site = imageBase_ + owners[index]->virtualAddress + inside;
  236. primitiveSizes_ = site + kPrimitiveSizeNext
  237. + static_cast<std::uint64_t>(static_cast<std::int64_t>(displacement));
  238. return true;
  239. }
  240. return false;
  241. }
  242. /**
  243. * Maps one virtual address to a file offset through the section table.
  244. * @param address Virtual address relative to the image base in the header.
  245. * @param size Bytes the caller reads there.
  246. * @param at Receives the file offset.
  247. * @return False when no section holds the address or the read leaves the file.
  248. */
  249. bool Image::offset(std::uint64_t address, std::size_t size, std::size_t& at) const noexcept {
  250. if (address < imageBase_) {
  251. return false;
  252. }
  253. const std::uint64_t relative = address - imageBase_;
  254. for (const Section& section : sections_) {
  255. if (relative < section.virtualAddress
  256. || relative - section.virtualAddress >= section.virtualSize) {
  257. continue;
  258. }
  259. const std::uint64_t inside = relative - section.virtualAddress;
  260. if (inside >= section.rawSize) {
  261. continue;
  262. }
  263. const std::uint64_t file = section.rawOffset + inside;
  264. if (file > size_ || size_ - file < size) {
  265. return false;
  266. }
  267. at = static_cast<std::size_t>(file);
  268. return true;
  269. }
  270. return false;
  271. }
  272. bool Image::u8(std::uint64_t address, std::uint8_t& value) const noexcept {
  273. std::size_t at = 0;
  274. return offset(address, sizeof value, at) && read_at(bytes(), at, value);
  275. }
  276. bool Image::u16(std::uint64_t address, std::uint16_t& value) const noexcept {
  277. std::size_t at = 0;
  278. return offset(address, sizeof value, at) && read_at(bytes(), at, value);
  279. }
  280. bool Image::u32(std::uint64_t address, std::uint32_t& value) const noexcept {
  281. std::size_t at = 0;
  282. return offset(address, sizeof value, at) && read_at(bytes(), at, value);
  283. }
  284. bool Image::i32(std::uint64_t address, std::int32_t& value) const noexcept {
  285. std::size_t at = 0;
  286. return offset(address, sizeof value, at) && read_at(bytes(), at, value);
  287. }
  288. bool Image::f32(std::uint64_t address, float& value) const noexcept {
  289. std::size_t at = 0;
  290. return offset(address, sizeof value, at) && read_at(bytes(), at, value);
  291. }
  292. } // namespace sunrise::client::content::activity::sdk_generation::decode_plans::internal