diff --git a/src/client/component/arxan.cpp b/src/client/component/arxan.cpp index f2722454..2040326f 100644 --- a/src/client/component/arxan.cpp +++ b/src/client/component/arxan.cpp @@ -3,9 +3,11 @@ #include "game/game.hpp" +#include "game_module.hpp" #include "scheduler.hpp" #include +#include namespace arxan { @@ -18,6 +20,11 @@ namespace arxan #define ProcessDebugObjectHandle 30 // WinXP source says 31? #define ProcessDebugFlags 31 // WinXP source says 32? + HANDLE process_id_to_handle(const DWORD pid) + { + return reinterpret_cast(static_cast(pid)); + } + NTSTATUS WINAPI nt_query_information_process_stub(const HANDLE handle, const PROCESSINFOCLASS info_class, const PVOID info, const ULONG info_length, const PULONG ret_length) @@ -71,6 +78,13 @@ namespace arxan return STATUS_INVALID_HANDLE; } + void hide_being_debugged() + { + auto* const peb = PPEB(__readgsqword(0x60)); + peb->BeingDebugged = false; + *reinterpret_cast(LPSTR(peb) + 0xBC) &= ~0x70; + } + LONG WINAPI exception_filter(const LPEXCEPTION_POINTERS info) { if (info->ExceptionRecord->ExceptionCode == STATUS_INVALID_HANDLE) @@ -78,19 +92,185 @@ namespace arxan return EXCEPTION_CONTINUE_EXECUTION; } - if (info->ExceptionRecord->ExceptionCode == STATUS_ACCESS_VIOLATION) - { - //MessageBoxA(0, 0, "AV", 0); - } - return EXCEPTION_CONTINUE_SEARCH; } - void hide_being_debugged() + struct integrity_handler_context { - auto* const peb = PPEB(__readgsqword(0x60)); - peb->BeingDebugged = false; - *reinterpret_cast(LPSTR(peb) + 0xBC) &= ~0x70; + uint32_t* computed_checksum; + uint32_t* original_checksum; + }; + + // Pretty trashy, but working, heuristic to search integrity the handler context + bool is_handler_context(uint8_t* stack_frame, const uint32_t computed_checksum, const uint32_t frame_offset) + { + auto* potential_address = *reinterpret_cast(stack_frame + frame_offset); + + int64_t diff = reinterpret_cast(stack_frame) - reinterpret_cast(potential_address); + diff = std::abs(diff); + + return diff < 0x1000 && *potential_address == computed_checksum; + } + + integrity_handler_context* search_handler_context(uint8_t* stack_frame, const uint32_t computed_checksum) + { + for (uint32_t frame_offset = 24; frame_offset < 64; frame_offset += 8) + { + if (is_handler_context(stack_frame, computed_checksum, frame_offset)) + { + return reinterpret_cast(stack_frame + frame_offset); + } + } + + return nullptr; + } + + uint32_t adjust_integrity_checksum(const uint64_t return_address, uint8_t* stack_frame, + const uint32_t current_checksum) + { + const auto handler_address = return_address - 5; + const auto* context = search_handler_context(stack_frame, current_checksum); + + if (!context) + { + OutputDebugStringA(utils::string::va("Unable to find frame offset for: %llX", return_address)); + return current_checksum; + } + + const auto correct_checksum = *context->original_checksum; + *context->computed_checksum = correct_checksum; + + if (current_checksum != correct_checksum) + { +#ifdef _DEBUG + OutputDebugStringA(utils::string::va("Adjusting checksum (%llX): %X -> %X", handler_address, + current_checksum, correct_checksum)); +#endif + } + + return correct_checksum; + } + + void patch_intact_basic_block_integrity_check(void* address) + { + const auto game_address = reinterpret_cast(address); + constexpr auto inst_len = 3; + + const auto next_inst_addr = game_address + inst_len; + const auto next_inst = *reinterpret_cast(next_inst_addr); + + if ((next_inst & 0xFF00FFFF) != 0xFF004583) + { + throw std::runtime_error(utils::string::va("Unable to patch intact basic block: %llX", game_address)); + } + + const auto other_frame_offset = static_cast(next_inst >> 16); + static const auto stub = utils::hook::assemble([](utils::hook::assembler& a) + { + a.push(rax); + + a.mov(rax, qword_ptr(rsp, 8)); + a.sub(rax, 2); // Skip the push we inserted + + a.push(rax); + a.pushad64(); + + a.mov(r8, qword_ptr(rsp, 0x88)); + a.mov(rcx, rax); + a.mov(rdx, rbp); + a.call_aligned(adjust_integrity_checksum); + + a.mov(qword_ptr(rsp, 0x80), rax); + + a.popad64(); + a.pop(rax); + + a.add(rsp, 8); + + a.mov(dword_ptr(rdx, rcx, 4), eax); + + a.pop(rax); // return addr + a.xchg(rax, qword_ptr(rsp)); // switch with push + + a.add(dword_ptr(rbp, rax), 0xFFFFFFFF); + + a.mov(rax, dword_ptr(rdx, rcx, 4)); // restore rax + + a.ret(); + }); + + // push other_frame_offset + utils::hook::set(game_address, static_cast(0x6A | (other_frame_offset << 8))); + utils::hook::call(game_address + 2, stub); + } + + void patch_split_basic_block_integrity_check(void* address) + { + const auto game_address = reinterpret_cast(address); + constexpr auto inst_len = 3; + + const auto next_inst_addr = game_address + inst_len; + + if (*reinterpret_cast(next_inst_addr) != 0xE9) + { + throw std::runtime_error(utils::string::va("Unable to patch split basic block: %llX", game_address)); + } + + const auto jump_target = utils::hook::extract(reinterpret_cast(next_inst_addr + 1)); + const auto stub = utils::hook::assemble([jump_target](utils::hook::assembler& a) + { + a.push(rax); + + a.mov(rax, qword_ptr(rsp, 8)); + a.push(rax); + + a.pushad64(); + + a.mov(r8, qword_ptr(rsp, 0x88)); + a.mov(rcx, rax); + a.mov(rdx, rbp); + a.call_aligned(adjust_integrity_checksum); + + a.mov(qword_ptr(rsp, 0x80), rax); + + a.popad64(); + a.pop(rax); + + a.add(rsp, 8); + + a.mov(dword_ptr(rdx, rcx, 4), eax); + + a.add(rsp, 8); + + a.jmp(jump_target); + }); + + utils::hook::call(game_address, stub); + } + + void search_and_patch_integrity_checks() + { + // There seem to be 670 results. + // Searching them is quite slow. + // Maybe precomputing that might be better? + const auto intact_results = "89 04 8A 83 45 ? FF"_sig; + const auto split_results = "89 04 8A E9"_sig; + + int results = 0; + + for (auto* i : intact_results) + { + patch_intact_basic_block_integrity_check(i); + results++; + } + + for (auto* i : split_results) + { + patch_split_basic_block_integrity_check(i); + results++; + } + + OutputDebugStringA(utils::string::va("integrity check amount: %d\n", results)); } } @@ -103,12 +283,10 @@ namespace arxan scheduler::loop(hide_being_debugged, scheduler::pipeline::async); const utils::nt::library ntdll("ntdll.dll"); - nt_close_hook.create(ntdll.get_proc("NtClose"), nt_close_stub); const auto nt_query_information_process = ntdll.get_proc("NtQueryInformationProcess"); - nt_query_information_process_hook.create(nt_query_information_process, - nt_query_information_process_stub); + nt_query_information_process_hook.create(nt_query_information_process, nt_query_information_process_stub); nt_query_information_process_hook.move(); AddVectoredExceptionHandler(1, exception_filter); @@ -116,7 +294,12 @@ namespace arxan void post_unpack() override { + search_and_patch_integrity_checks(); + } + int priority() override + { + return COMPONENT_MAX_PRIORITY; } }; } diff --git a/src/common/utils/hook.cpp b/src/common/utils/hook.cpp index 0c150a32..7168722b 100644 --- a/src/common/utils/hook.cpp +++ b/src/common/utils/hook.cpp @@ -1,12 +1,171 @@ #include "hook.hpp" -#include "string.hpp" +#include #include +#include "concurrency.hpp" +#include "string.hpp" +#include "nt.hpp" + +#ifdef max +#undef max +#endif + +#ifdef min +#undef min +#endif + namespace utils::hook { namespace { + uint8_t* allocate_somewhere_near(const void* base_address, const size_t size) + { + size_t offset = 0; + while (true) + { + offset += size; + auto* target_address = static_cast(base_address) - offset; + if (is_relatively_far(base_address, target_address)) + { + return nullptr; + } + + const auto res = VirtualAlloc(const_cast(target_address), size, MEM_RESERVE | MEM_COMMIT, + PAGE_EXECUTE_READWRITE); + if (res) + { + if (is_relatively_far(base_address, target_address)) + { + VirtualFree(res, 0, MEM_RELEASE); + return nullptr; + } + + return static_cast(res); + } + } + } + + class memory + { + public: + memory() = default; + + memory(const void* ptr) + : memory() + { + this->length_ = 0x1000; + this->buffer_ = allocate_somewhere_near(ptr, this->length_); + if (!this->buffer_) + { + throw std::runtime_error("Failed to allocate"); + } + } + + ~memory() + { + if (this->buffer_) + { + VirtualFree(this->buffer_, 0, MEM_RELEASE); + } + } + + memory(memory&& obj) noexcept + : memory() + { + this->operator=(std::move(obj)); + } + + memory& operator=(memory&& obj) noexcept + { + if (this != &obj) + { + this->~memory(); + this->buffer_ = obj.buffer_; + this->length_ = obj.length_; + this->offset_ = obj.offset_; + + obj.buffer_ = nullptr; + obj.length_ = 0; + obj.offset_ = 0; + } + + return *this; + } + + void* allocate(const size_t length) + { + if (!this->buffer_) + { + return nullptr; + } + + if (this->offset_ + length > this->length_) + { + return nullptr; + } + + const auto ptr = this->get_ptr(); + this->offset_ += length; + return ptr; + } + + void* get_ptr() const + { + return this->buffer_ + this->offset_; + } + + private: + uint8_t* buffer_{}; + size_t length_{}; + size_t offset_{}; + }; + + void* get_memory_near(const void* address, const size_t size) + { + static concurrency::container> memory_container{}; + + return memory_container.access([&](std::vector& memories) + { + for (auto& memory : memories) + { + if (!is_relatively_far(address, memory.get_ptr())) + { + const auto buffer = memory.allocate(size); + if (buffer) + { + return buffer; + } + } + } + + memories.emplace_back(address); + return memories.back().allocate(size); + }); + } + + concurrency::container>& get_original_data_map() + { + static concurrency::container> og_data{}; + return og_data; + } + + void store_original_data(const void* /*data*/, size_t /*length*/) + { + /*get_original_data_map().access([data, length](std::map& og_map) + { + const auto data_ptr = static_cast(data); + for (size_t i = 0; i < length; ++i) + { + const auto pos = data_ptr + i; + if (!og_map.contains(pos)) + { + og_map[pos] = *pos; + } + } + });*/ + } + void* initialize_min_hook() { static class min_hook_init @@ -140,6 +299,7 @@ namespace utils::hook { this->clear(); this->place_ = place; + store_original_data(place, 14); if (MH_CreateHook(this->place_, target, &this->original_) != MH_OK) { @@ -192,6 +352,8 @@ namespace utils::hook auto* const ptr = library.get_iat_entry(target_library, process); if (!ptr) return false; + store_original_data(ptr, sizeof(*ptr)); + DWORD protect; VirtualProtect(ptr, sizeof(*ptr), PAGE_EXECUTE_READWRITE, &protect); @@ -203,6 +365,8 @@ namespace utils::hook void nop(void* place, const size_t length) { + store_original_data(place, length); + DWORD old_protect{}; VirtualProtect(place, length, PAGE_EXECUTE_READWRITE, &old_protect); @@ -219,6 +383,8 @@ namespace utils::hook void copy(void* place, const void* data, const size_t length) { + store_original_data(place, length); + DWORD old_protect{}; VirtualProtect(place, length, PAGE_EXECUTE_READWRITE, &old_protect); @@ -233,6 +399,16 @@ namespace utils::hook copy(reinterpret_cast(place), data, length); } + void copy_string(void* place, const char* str) + { + copy(reinterpret_cast(place), str, strlen(str) + 1); + } + + void copy_string(const size_t place, const char* str) + { + copy_string(reinterpret_cast(place), str); + } + bool is_relatively_far(const void* pointer, const void* data, const int offset) { const int64_t diff = size_t(data) - (size_t(pointer) + offset); @@ -244,12 +420,23 @@ namespace utils::hook { if (is_relatively_far(pointer, data)) { - throw std::runtime_error("Too far away to create 32bit relative branch"); + auto* trampoline = get_memory_near(pointer, 14); + if (!trampoline) + { + throw std::runtime_error("Too far away to create 32bit relative branch"); + } + + call(pointer, trampoline); + jump(trampoline, data, true, true); + return; } + uint8_t copy_data[5]; + copy_data[0] = 0xE8; + *reinterpret_cast(©_data[1]) = int32_t(size_t(data) - (size_t(pointer) + 5)); + auto* patch_pointer = PBYTE(pointer); - set(patch_pointer, 0xE8); - set(patch_pointer + 1, int32_t(size_t(data) - (size_t(pointer) + 5))); + copy(patch_pointer, copy_data, sizeof(copy_data)); } void call(const size_t pointer, void* data) @@ -274,7 +461,14 @@ namespace utils::hook if (!use_far && is_relatively_far(pointer, data)) { - throw std::runtime_error("Too far away to create 32bit relative branch"); + auto* trampoline = get_memory_near(pointer, 14); + if (!trampoline) + { + throw std::runtime_error("Too far away to create 32bit relative branch"); + } + jump(pointer, trampoline, false, false); + jump(trampoline, data, true, true); + return; } auto* patch_pointer = PBYTE(pointer); @@ -283,19 +477,28 @@ namespace utils::hook { if (use_safe) { - copy(patch_pointer, jump_data_safe, sizeof(jump_data_safe)); - copy(patch_pointer + sizeof(jump_data_safe), &data, sizeof(data)); + uint8_t copy_data[sizeof(jump_data_safe) + sizeof(data)]; + memcpy(copy_data, jump_data_safe, sizeof(jump_data_safe)); + memcpy(copy_data + sizeof(jump_data_safe), &data, sizeof(data)); + + copy(patch_pointer, copy_data, sizeof(copy_data)); } else { - copy(patch_pointer, jump_data, sizeof(jump_data)); - copy(patch_pointer + 2, &data, sizeof(data)); + uint8_t copy_data[sizeof(jump_data)]; + memcpy(copy_data, jump_data, sizeof(jump_data)); + memcpy(copy_data + 2, &data, sizeof(data)); + + copy(patch_pointer, copy_data, sizeof(copy_data)); } } else { - set(patch_pointer, 0xE9); - set(patch_pointer + 1, int32_t(size_t(data) - (size_t(pointer) + 5))); + uint8_t copy_data[5]; + copy_data[0] = 0xE9; + *reinterpret_cast(©_data[1]) = int32_t(size_t(data) - (size_t(pointer) + 5)); + + copy(patch_pointer, copy_data, sizeof(copy_data)); } } @@ -386,4 +589,26 @@ namespace utils::hook return extract(data + 1); } -} \ No newline at end of file + + std::vector query_original_data(const void* data, const size_t length) + { + std::vector og_data{}; + og_data.resize(length); + memcpy(og_data.data(), data, length); + + get_original_data_map().access([data, length, &og_data](const std::map& og_map) + { + auto* ptr = static_cast(data); + for (size_t i = 0; i < length; ++i) + { + auto entry = og_map.find(ptr + i); + if (entry != og_map.end()) + { + og_data[i] = entry->second; + } + } + }); + + return og_data; + } +} diff --git a/src/common/utils/hook.hpp b/src/common/utils/hook.hpp index 53dcc8d7..83962cd9 100644 --- a/src/common/utils/hook.hpp +++ b/src/common/utils/hook.hpp @@ -22,9 +22,9 @@ namespace utils::hook { auto functions = get_iota_functions(); functions.emplace_back([]() - { - return Entries - 1; - }); + { + return Entries - 1; + }); return functions; } } @@ -155,6 +155,9 @@ namespace utils::hook void copy(void* place, const void* data, size_t length); void copy(size_t place, const void* data, size_t length); + void copy_string(void* place, const char* str); + void copy_string(size_t place, const char* str); + bool is_relatively_far(const void* pointer, const void* data, int offset = 5); void call(void* pointer, void* data); @@ -184,19 +187,13 @@ namespace utils::hook void* follow_branch(void* address); template - static void set(void* place, T value) + static void set(void* place, T value = false) { - DWORD old_protect; - VirtualProtect(place, sizeof(T), PAGE_EXECUTE_READWRITE, &old_protect); - - *static_cast(place) = value; - - VirtualProtect(place, sizeof(T), old_protect, &old_protect); - FlushInstructionCache(GetCurrentProcess(), place, sizeof(T)); + copy(place, &value, sizeof(value)); } template - static void set(const size_t place, T value) + static void set(const size_t place, T value = false) { return set(reinterpret_cast(place), value); } @@ -212,4 +209,6 @@ namespace utils::hook { return static_cast(func)(args...); } -} \ No newline at end of file + + std::vector query_original_data(const void* data, size_t length); +} diff --git a/src/common/utils/signature.cpp b/src/common/utils/signature.cpp index 9bb8c621..6a9933c3 100644 --- a/src/common/utils/signature.cpp +++ b/src/common/utils/signature.cpp @@ -4,6 +4,14 @@ #include +#ifdef max +#undef max +#endif + +#ifdef min +#undef min +#endif + namespace utils::hook { void signature::load_pattern(const std::string& pattern) @@ -29,7 +37,7 @@ namespace utils::hook throw std::runtime_error("Invalid pattern"); } - char str[] = {val, 0}; + char str[] = { val, 0 }; const auto current_nibble = static_cast(strtol(str, nullptr, 16)); if (!has_nibble) @@ -68,15 +76,15 @@ namespace utils::hook } } - std::vector signature::process_range(uint8_t* start, const size_t length) const + signature::signature_result signature::process_range(uint8_t* start, const size_t length) const { if (this->has_sse_support()) return this->process_range_vectorized(start, length); return this->process_range_linear(start, length); } - std::vector signature::process_range_linear(uint8_t* start, const size_t length) const + signature::signature_result signature::process_range_linear(uint8_t* start, const size_t length) const { - std::vector result; + std::vector result; for (size_t i = 0; i < length; ++i) { @@ -93,17 +101,17 @@ namespace utils::hook if (j == this->mask_.size()) { - result.push_back(size_t(address)); + result.push_back(address); } } return result; } - std::vector signature::process_range_vectorized(uint8_t* start, const size_t length) const + signature::signature_result signature::process_range_vectorized(uint8_t* start, const size_t length) const { - std::vector result; - __declspec(align(16)) char desired_mask[16] = {0}; + std::vector result; + __declspec(align(16)) char desired_mask[16] = { 0 }; for (size_t i = 0; i < this->mask_.size(); i++) { @@ -118,14 +126,14 @@ namespace utils::hook const auto address = start + i; const auto value = _mm_loadu_si128(reinterpret_cast(address)); const auto comparison = _mm_cmpestrm(value, 16, comparand, static_cast(this->mask_.size()), - _SIDD_CMP_EQUAL_EACH); + _SIDD_CMP_EQUAL_EACH); const auto matches = _mm_and_si128(mask, comparison); const auto equivalence = _mm_xor_si128(mask, matches); if (_mm_test_all_zeros(equivalence, equivalence)) { - result.push_back(size_t(address)); + result.push_back(address); } } @@ -144,7 +152,7 @@ namespace utils::hook signature::signature_result signature::process_serial() const { const auto sub = this->has_sse_support() ? 16 : this->mask_.size(); - return {this->process_range(this->start_, this->length_ - sub)}; + return { this->process_range(this->start_, this->length_ - sub) }; } signature::signature_result signature::process_parallel() const @@ -156,7 +164,7 @@ namespace utils::hook const auto grid = range / cores; std::mutex mutex; - std::vector result; + std::vector result; std::vector threads; for (auto i = 0u; i < cores; ++i) @@ -165,7 +173,7 @@ namespace utils::hook const auto length = (i + 1 == cores) ? (this->start_ + this->length_ - sub) - start : grid; threads.emplace_back([&, start, length]() { - auto local_result = this->process_range(start, length); + const auto local_result = this->process_range(start, length); if (local_result.empty()) return; std::lock_guard _(mutex); @@ -185,7 +193,7 @@ namespace utils::hook } std::sort(result.begin(), result.end()); - return {std::move(result)}; + return { std::move(result) }; } bool signature::has_sse_support() const diff --git a/src/common/utils/signature.hpp b/src/common/utils/signature.hpp index a3728327..054e6b45 100644 --- a/src/common/utils/signature.hpp +++ b/src/common/utils/signature.hpp @@ -7,33 +7,9 @@ namespace utils::hook class signature final { public: - class signature_result - { - public: - signature_result(std::vector&& matches) : matches_(std::move(matches)) - { - } + using signature_result = std::vector; - [[nodiscard]] uint8_t* get(const size_t index) const - { - if (index >= this->count()) - { - throw std::runtime_error("Invalid index"); - } - - return reinterpret_cast(this->matches_[index]); - } - - [[nodiscard]] size_t count() const - { - return this->matches_.size(); - } - - private: - std::vector matches_; - }; - - explicit signature(const std::string& pattern, const nt::library library = {}) + explicit signature(const std::string& pattern, const nt::library& library = {}) : signature(pattern, library.get_ptr(), library.get_optional_header()->SizeOfImage) { } @@ -62,9 +38,9 @@ namespace utils::hook signature_result process_parallel() const; signature_result process_serial() const; - std::vector process_range(uint8_t* start, size_t length) const; - std::vector process_range_linear(uint8_t* start, size_t length) const; - std::vector process_range_vectorized(uint8_t* start, size_t length) const; + signature_result process_range(uint8_t* start, size_t length) const; + signature_result process_range_linear(uint8_t* start, size_t length) const; + signature_result process_range_vectorized(uint8_t* start, size_t length) const; bool has_sse_support() const; };