update arxan component
includes anti anti debug
This commit is contained in:
parent
24a8c68abf
commit
820aa6788e
@ -1,151 +0,0 @@
|
||||
#include <std_include.hpp>
|
||||
#include "loader/component_loader.hpp"
|
||||
|
||||
#include "arxan.hpp"
|
||||
#include "scheduler.hpp"
|
||||
|
||||
#include "game/game.hpp"
|
||||
|
||||
#include <utils/hook.hpp>
|
||||
|
||||
namespace arxan
|
||||
{
|
||||
namespace
|
||||
{
|
||||
utils::hook::detour nt_close_hook;
|
||||
utils::hook::detour nt_query_information_process_hook;
|
||||
|
||||
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)
|
||||
{
|
||||
auto* orig = static_cast<decltype(NtQueryInformationProcess)*>(nt_query_information_process_hook.
|
||||
get_original());
|
||||
const auto status = orig(handle, info_class, info, info_length, ret_length);
|
||||
|
||||
if (NT_SUCCESS(status))
|
||||
{
|
||||
if (info_class == ProcessBasicInformation)
|
||||
{
|
||||
static DWORD explorer_pid = 0;
|
||||
if (!explorer_pid)
|
||||
{
|
||||
auto* const shell_window = GetShellWindow();
|
||||
GetWindowThreadProcessId(shell_window, &explorer_pid);
|
||||
}
|
||||
|
||||
static_cast<PPROCESS_BASIC_INFORMATION>(info)->Reserved3 = PVOID(DWORD64(explorer_pid));
|
||||
}
|
||||
else if (info_class == 30) // ProcessDebugObjectHandle
|
||||
{
|
||||
*static_cast<HANDLE*>(info) = nullptr;
|
||||
|
||||
return 0xC0000353;
|
||||
}
|
||||
else if (info_class == 7) // ProcessDebugPort
|
||||
{
|
||||
*static_cast<HANDLE*>(info) = nullptr;
|
||||
}
|
||||
else if (info_class == 31)
|
||||
{
|
||||
*static_cast<ULONG*>(info) = 1;
|
||||
}
|
||||
|
||||
//https://docs.microsoft.com/en-us/windows/win32/api/winternl/nf-winternl-ntqueryinformationprocess
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
NTSTATUS NTAPI nt_close_stub(const HANDLE handle)
|
||||
{
|
||||
char info[16];
|
||||
if (NtQueryObject(handle, OBJECT_INFORMATION_CLASS(4), &info, 2, nullptr) >= 0 && size_t(handle) != 0x12345)
|
||||
{
|
||||
auto* orig = static_cast<decltype(NtClose)*>(nt_close_hook.get_original());
|
||||
return orig(handle);
|
||||
}
|
||||
|
||||
return STATUS_INVALID_HANDLE;
|
||||
}
|
||||
|
||||
LONG WINAPI exception_filter(const LPEXCEPTION_POINTERS info)
|
||||
{
|
||||
if (info->ExceptionRecord->ExceptionCode == STATUS_INVALID_HANDLE)
|
||||
{
|
||||
return EXCEPTION_CONTINUE_EXECUTION;
|
||||
}
|
||||
|
||||
return EXCEPTION_CONTINUE_SEARCH;
|
||||
}
|
||||
|
||||
void hide_being_debugged()
|
||||
{
|
||||
auto* const peb = PPEB(__readgsqword(0x60));
|
||||
peb->BeingDebugged = false;
|
||||
*reinterpret_cast<PDWORD>(LPSTR(peb) + 0xBC) &= ~0x70;
|
||||
}
|
||||
|
||||
void remove_hardware_breakpoints()
|
||||
{
|
||||
CONTEXT context;
|
||||
ZeroMemory(&context, sizeof(context));
|
||||
context.ContextFlags = CONTEXT_DEBUG_REGISTERS;
|
||||
|
||||
auto* const thread = GetCurrentThread();
|
||||
GetThreadContext(thread, &context);
|
||||
|
||||
context.Dr0 = 0;
|
||||
context.Dr1 = 0;
|
||||
context.Dr2 = 0;
|
||||
context.Dr3 = 0;
|
||||
context.Dr6 = 0;
|
||||
context.Dr7 = 0;
|
||||
|
||||
SetThreadContext(thread, &context);
|
||||
}
|
||||
|
||||
BOOL WINAPI set_thread_context_stub(const HANDLE thread, CONTEXT* context)
|
||||
{
|
||||
return SetThreadContext(thread, context);
|
||||
}
|
||||
}
|
||||
|
||||
class component final : public component_interface
|
||||
{
|
||||
public:
|
||||
void* load_import(const std::string& library, const std::string& function) override
|
||||
{
|
||||
if (function == "SetThreadContext")
|
||||
{
|
||||
//return set_thread_context_stub;
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void post_load() override
|
||||
{
|
||||
hide_being_debugged();
|
||||
scheduler::loop(hide_being_debugged, scheduler::pipeline::async);
|
||||
|
||||
const utils::nt::library ntdll("ntdll.dll");
|
||||
nt_close_hook.create(ntdll.get_proc<void*>("NtClose"), nt_close_stub);
|
||||
nt_query_information_process_hook.create(ntdll.get_proc<void*>("NtQueryInformationProcess"),
|
||||
nt_query_information_process_stub);
|
||||
// https://www.geoffchappell.com/studies/windows/win32/ntdll/api/index.htm
|
||||
AddVectoredExceptionHandler(1, exception_filter);
|
||||
}
|
||||
|
||||
void post_unpack() override
|
||||
{
|
||||
// cba to implement sp, not sure if it's even needed
|
||||
if (game::environment::is_sp())
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
REGISTER_COMPONENT(arxan::component)
|
587
src/client/component/arxan/arxan.cpp
Normal file
587
src/client/component/arxan/arxan.cpp
Normal file
@ -0,0 +1,587 @@
|
||||
#include <std_include.hpp>
|
||||
#include "loader/component_loader.hpp"
|
||||
|
||||
#include "game/game.hpp"
|
||||
|
||||
#include "component/game_module.hpp"
|
||||
#include "component/scheduler.hpp"
|
||||
|
||||
#include <utils/hook.hpp>
|
||||
#include <utils/string.hpp>
|
||||
|
||||
#include "integrity.hpp"
|
||||
#include "breakpoints.hpp"
|
||||
|
||||
#define PRECOMPUTED_INTEGRITY_CHECKS
|
||||
#define PRECOMPUTED_BREAKPOINTS
|
||||
|
||||
#define ProcessDebugPort 7
|
||||
#define ProcessDebugObjectHandle 30
|
||||
#define ProcessDebugFlags 31
|
||||
|
||||
namespace arxan
|
||||
{
|
||||
namespace integrity
|
||||
{
|
||||
const std::vector<std::pair<uint8_t*, size_t>>& get_text_sections()
|
||||
{
|
||||
static const std::vector<std::pair<uint8_t*, size_t>> text = []
|
||||
{
|
||||
std::vector<std::pair<uint8_t*, size_t>> texts{};
|
||||
|
||||
const utils::nt::library game{ game_module::get_game_module() };
|
||||
for (const auto& section : game.get_section_headers())
|
||||
{
|
||||
if (section->Characteristics & IMAGE_SCN_MEM_EXECUTE)
|
||||
{
|
||||
texts.emplace_back(game.get_ptr() + section->VirtualAddress, section->Misc.VirtualSize);
|
||||
}
|
||||
}
|
||||
|
||||
return texts;
|
||||
}();
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
bool is_in_texts(const uint64_t addr)
|
||||
{
|
||||
const auto& texts = get_text_sections();
|
||||
for (const auto& text : texts)
|
||||
{
|
||||
const auto start = reinterpret_cast<ULONG_PTR>(text.first);
|
||||
if (addr >= start && addr <= (start + text.second))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool is_in_texts(const void* addr)
|
||||
{
|
||||
return is_in_texts(reinterpret_cast<uint64_t>(addr));
|
||||
}
|
||||
|
||||
struct integrity_handler_context
|
||||
{
|
||||
uint32_t* computed_checksum;
|
||||
uint32_t* original_checksum;
|
||||
};
|
||||
|
||||
bool is_on_stack(uint8_t* stack_frame, const void* pointer)
|
||||
{
|
||||
const auto stack_value = reinterpret_cast<uint64_t>(stack_frame);
|
||||
const auto pointer_value = reinterpret_cast<uint64_t>(pointer);
|
||||
|
||||
const auto diff = static_cast<int64_t>(stack_value - pointer_value);
|
||||
return std::abs(diff) < 0x1000;
|
||||
}
|
||||
|
||||
// Pretty trashy, but working, heuristic to search the integrity handler context
|
||||
bool is_handler_context(uint8_t* stack_frame, const uint32_t computed_checksum, const uint32_t frame_offset)
|
||||
{
|
||||
const auto* potential_context = reinterpret_cast<integrity_handler_context*>(stack_frame + frame_offset);
|
||||
return is_on_stack(stack_frame, potential_context->computed_checksum)
|
||||
&& *potential_context->computed_checksum == computed_checksum
|
||||
&& is_in_texts(potential_context->original_checksum);
|
||||
}
|
||||
|
||||
integrity_handler_context* search_handler_context(uint8_t* stack_frame, const uint32_t computed_checksum)
|
||||
{
|
||||
for (uint32_t frame_offset = 0; frame_offset < 0x90; frame_offset += 8)
|
||||
{
|
||||
if (is_handler_context(stack_frame, computed_checksum, frame_offset))
|
||||
{
|
||||
return reinterpret_cast<integrity_handler_context*>(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)
|
||||
{
|
||||
[[maybe_unused]] 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 DEV_BUILD
|
||||
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<uint64_t>(address);
|
||||
constexpr auto inst_len = 3;
|
||||
|
||||
const auto next_inst_addr = game_address + inst_len;
|
||||
const auto next_inst = *reinterpret_cast<uint32_t*>(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<uint8_t>(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<uint16_t>(game_address, static_cast<uint16_t>(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<uint64_t>(address);
|
||||
constexpr auto inst_len = 3;
|
||||
|
||||
const auto next_inst_addr = game_address + inst_len;
|
||||
|
||||
if (*reinterpret_cast<uint8_t*>(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<void*>(reinterpret_cast<void*>(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);
|
||||
}
|
||||
|
||||
#ifdef PRECOMPUTED_INTEGRITY_CHECKS
|
||||
void search_and_patch_integrity_checks_precomputed()
|
||||
{
|
||||
if (game::environment::is_sp())
|
||||
{
|
||||
for (const auto i : sp::intact_integrity_check_blocks)
|
||||
{
|
||||
patch_intact_basic_block_integrity_check(reinterpret_cast<void*>(i));
|
||||
}
|
||||
for (const auto i : sp::split_integrity_check_blocks)
|
||||
{
|
||||
patch_split_basic_block_integrity_check(reinterpret_cast<void*>(i));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for (const auto i : mp::intact_integrity_check_blocks)
|
||||
{
|
||||
patch_intact_basic_block_integrity_check(reinterpret_cast<void*>(i));
|
||||
}
|
||||
for (const auto i : mp::split_integrity_check_blocks)
|
||||
{
|
||||
patch_split_basic_block_integrity_check(reinterpret_cast<void*>(i));
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
void search_and_patch_integrity_checks()
|
||||
{
|
||||
#ifdef PRECOMPUTED_INTEGRITY_CHECKS
|
||||
assert(game::base_address == 0x140000000);
|
||||
search_and_patch_integrity_checks_precomputed();
|
||||
#else
|
||||
const auto intact_results = "89 04 8A 83 45 ? FF"_sig;
|
||||
const auto split_results = "89 04 8A E9"_sig;
|
||||
|
||||
for (auto* i : intact_results)
|
||||
{
|
||||
patch_intact_basic_block_integrity_check(i);
|
||||
}
|
||||
|
||||
for (auto* i : split_results)
|
||||
{
|
||||
patch_split_basic_block_integrity_check(i);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
using namespace integrity;
|
||||
|
||||
namespace anti_debug
|
||||
{
|
||||
utils::hook::detour nt_close_hook;
|
||||
utils::hook::detour nt_query_information_process_hook;
|
||||
|
||||
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)
|
||||
{
|
||||
auto* orig = static_cast<decltype(NtQueryInformationProcess)*>(nt_query_information_process_hook.get_original());
|
||||
auto status = orig(handle, info_class, info, info_length, ret_length);
|
||||
|
||||
if (NT_SUCCESS(status))
|
||||
{
|
||||
if (info_class == ProcessDebugObjectHandle)
|
||||
{
|
||||
*static_cast<HANDLE*>(info) = nullptr;
|
||||
return static_cast<LONG>(0xC0000353);
|
||||
}
|
||||
else if (info_class == ProcessDebugPort)
|
||||
{
|
||||
*static_cast<HANDLE*>(info) = nullptr;
|
||||
}
|
||||
else if (info_class == ProcessDebugFlags)
|
||||
{
|
||||
*static_cast<ULONG*>(info) = 1;
|
||||
}
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
NTSTATUS NTAPI nt_close_stub(const HANDLE handle)
|
||||
{
|
||||
char info[16];
|
||||
if (NtQueryObject(handle, OBJECT_INFORMATION_CLASS(4), &info, 2, nullptr) >= 0 && size_t(handle) != 0x12345)
|
||||
{
|
||||
auto* orig = static_cast<decltype(NtClose)*>(nt_close_hook.get_original());
|
||||
return orig(handle);
|
||||
}
|
||||
|
||||
return STATUS_INVALID_HANDLE;
|
||||
}
|
||||
|
||||
void hide_being_debugged()
|
||||
{
|
||||
auto* const peb = PPEB(__readgsqword(0x60));
|
||||
peb->BeingDebugged = false;
|
||||
*reinterpret_cast<PDWORD>(LPSTR(peb) + 0xBC) &= ~0x70; // NtGlobalFlag
|
||||
}
|
||||
|
||||
void remove_hardware_breakpoints()
|
||||
{
|
||||
CONTEXT context;
|
||||
ZeroMemory(&context, sizeof(context));
|
||||
context.ContextFlags = CONTEXT_DEBUG_REGISTERS;
|
||||
|
||||
auto* const thread = GetCurrentThread();
|
||||
GetThreadContext(thread, &context);
|
||||
|
||||
context.Dr0 = 0;
|
||||
context.Dr1 = 0;
|
||||
context.Dr2 = 0;
|
||||
context.Dr3 = 0;
|
||||
context.Dr6 = 0;
|
||||
context.Dr7 = 0;
|
||||
|
||||
SetThreadContext(thread, &context);
|
||||
}
|
||||
|
||||
LONG WINAPI exception_filter(const LPEXCEPTION_POINTERS info)
|
||||
{
|
||||
if (info->ExceptionRecord->ExceptionCode == STATUS_INVALID_HANDLE)
|
||||
{
|
||||
return EXCEPTION_CONTINUE_EXECUTION;
|
||||
}
|
||||
|
||||
return EXCEPTION_CONTINUE_SEARCH;
|
||||
}
|
||||
|
||||
BOOL WINAPI set_thread_context_stub(const HANDLE thread, CONTEXT* context)
|
||||
{
|
||||
if (context->ContextFlags == CONTEXT_DEBUG_REGISTERS)
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
return SetThreadContext(thread, context);
|
||||
}
|
||||
|
||||
enum dbg_funcs_e
|
||||
{
|
||||
DbgBreakPoint,
|
||||
DbgUserBreakPoint,
|
||||
DbgUiConnectToDbg,
|
||||
DbgUiContinue,
|
||||
DbgUiConvertStateChangeStructure,
|
||||
DbgUiDebugActiveProcess,
|
||||
DbgUiGetThreadDebugObject,
|
||||
DbgUiIssueRemoteBreakin,
|
||||
DbgUiRemoteBreakin,
|
||||
DbgUiSetThreadDebugObject,
|
||||
DbgUiStopDebugging,
|
||||
DbgUiWaitStateChange,
|
||||
DbgPrintReturnControlC,
|
||||
DbgPrompt,
|
||||
DBG_FUNCS_COUNT,
|
||||
};
|
||||
const char* dbg_funcs_names[] =
|
||||
{
|
||||
"DbgBreakPoint",
|
||||
"DbgUserBreakPoint",
|
||||
"DbgUiConnectToDbg",
|
||||
"DbgUiContinue",
|
||||
"DbgUiConvertStateChangeStructure",
|
||||
"DbgUiDebugActiveProcess",
|
||||
"DbgUiGetThreadDebugObject",
|
||||
"DbgUiIssueRemoteBreakin",
|
||||
"DbgUiRemoteBreakin",
|
||||
"DbgUiSetThreadDebugObject",
|
||||
"DbgUiStopDebugging",
|
||||
"DbgUiWaitStateChange",
|
||||
"DbgPrintReturnControlC",
|
||||
"DbgPrompt",
|
||||
};
|
||||
struct dbg_func_bytes_s
|
||||
{
|
||||
std::uint8_t buffer[15];
|
||||
};
|
||||
dbg_func_bytes_s dbg_func_bytes[DBG_FUNCS_COUNT];
|
||||
void* dbg_func_procs[DBG_FUNCS_COUNT]{};
|
||||
|
||||
void store_debug_functions()
|
||||
{
|
||||
const utils::nt::library ntdll("ntdll.dll");
|
||||
|
||||
for (auto i = 0; i < DBG_FUNCS_COUNT; i++)
|
||||
{
|
||||
dbg_func_procs[i] = ntdll.get_proc<void*>(dbg_funcs_names[i]);
|
||||
memcpy(dbg_func_bytes[i].buffer, dbg_func_procs[i], sizeof(dbg_func_bytes[i].buffer));
|
||||
}
|
||||
}
|
||||
|
||||
void restore_debug_functions()
|
||||
{
|
||||
for (auto i = 0; i < DBG_FUNCS_COUNT; i++)
|
||||
{
|
||||
utils::hook::copy(dbg_func_procs[i], dbg_func_bytes[i].buffer, sizeof(dbg_func_bytes[i].buffer));
|
||||
}
|
||||
}
|
||||
|
||||
namespace breakpoints
|
||||
{
|
||||
std::unordered_map<PVOID, void*> handle_handler;
|
||||
|
||||
void fake_breakpoint_trigger(void* address, _CONTEXT* fake_context)
|
||||
{
|
||||
_EXCEPTION_POINTERS fake_info{};
|
||||
_EXCEPTION_RECORD fake_record{};
|
||||
fake_info.ExceptionRecord = &fake_record;
|
||||
fake_info.ContextRecord = fake_context;
|
||||
|
||||
fake_record.ExceptionAddress = reinterpret_cast<void*>(reinterpret_cast<std::uint64_t>(address) + 3);
|
||||
fake_record.ExceptionCode = EXCEPTION_BREAKPOINT;
|
||||
|
||||
for (auto handler : handle_handler)
|
||||
{
|
||||
if (handler.second)
|
||||
{
|
||||
auto result = utils::hook::invoke<LONG>(handler.second, &fake_info);
|
||||
if (result)
|
||||
{
|
||||
memset(fake_context, 0, sizeof(_CONTEXT));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void patch_int2d_trap(void* address)
|
||||
{
|
||||
const auto game_address = reinterpret_cast<std::uint64_t>(address);
|
||||
|
||||
const auto jump_target = utils::hook::extract<void*>(reinterpret_cast<void*>(game_address + 3));
|
||||
|
||||
_CONTEXT* fake_context = new _CONTEXT{};
|
||||
const auto stub = utils::hook::assemble([address, jump_target, fake_context](utils::hook::assembler& a)
|
||||
{
|
||||
a.push(rcx);
|
||||
a.mov(rcx, fake_context);
|
||||
a.call_aligned(RtlCaptureContext);
|
||||
a.pop(rcx);
|
||||
|
||||
a.pushad64();
|
||||
a.mov(rcx, address);
|
||||
a.mov(rdx, fake_context);
|
||||
a.call_aligned(fake_breakpoint_trigger);
|
||||
a.popad64();
|
||||
|
||||
a.jmp(jump_target);
|
||||
});
|
||||
|
||||
utils::hook::nop(game_address, 7);
|
||||
utils::hook::jump(game_address, stub, false);
|
||||
}
|
||||
|
||||
#ifdef PRECOMPUTED_BREAKPOINTS
|
||||
void patch_breakpoints_precomputed()
|
||||
{
|
||||
for (const auto i : mp::int2d_breakpoint_addresses)
|
||||
{
|
||||
patch_int2d_trap(reinterpret_cast<void*>(i));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
void patch_breakpoints()
|
||||
{
|
||||
static bool once = false;
|
||||
if (once)
|
||||
{
|
||||
return;
|
||||
}
|
||||
once = true;
|
||||
|
||||
// sp has no breakpoints
|
||||
if (game::environment::is_sp())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
#ifdef PRECOMPUTED_BREAKPOINTS
|
||||
assert(game::base_address == 0x140000000);
|
||||
patch_breakpoints_precomputed();
|
||||
#else
|
||||
const auto int2d_results = utils::hook::signature("CD 2D E9 ? ? ? ?", game_module::get_game_module()).process();
|
||||
for (auto* i : int2d_results)
|
||||
{
|
||||
patch_int2d_trap(i);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
PVOID WINAPI add_vectored_exception_handler_stub(ULONG first, PVECTORED_EXCEPTION_HANDLER handler)
|
||||
{
|
||||
breakpoints::patch_breakpoints();
|
||||
|
||||
auto handle = AddVectoredExceptionHandler(first, handler);
|
||||
handle_handler[handle] = handler;
|
||||
|
||||
return handle;
|
||||
}
|
||||
|
||||
ULONG WINAPI remove_vectored_exception_handler_stub(PVOID handle)
|
||||
{
|
||||
handle_handler[handle] = nullptr;
|
||||
return RemoveVectoredExceptionHandler(handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
using namespace anti_debug;
|
||||
|
||||
class component final : public component_interface
|
||||
{
|
||||
public:
|
||||
void* load_import(const std::string& library, const std::string& function) override
|
||||
{
|
||||
if (function == "SetThreadContext")
|
||||
{
|
||||
return set_thread_context_stub;
|
||||
}
|
||||
else if (function == "AddVectoredExceptionHandler")
|
||||
{
|
||||
return breakpoints::add_vectored_exception_handler_stub;
|
||||
}
|
||||
else if (function == "RemoveVectoredExceptionHandler")
|
||||
{
|
||||
return breakpoints::remove_vectored_exception_handler_stub;
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void post_load() override
|
||||
{
|
||||
remove_hardware_breakpoints();
|
||||
hide_being_debugged();
|
||||
scheduler::loop(hide_being_debugged, scheduler::pipeline::async);
|
||||
store_debug_functions();
|
||||
|
||||
const utils::nt::library ntdll("ntdll.dll");
|
||||
nt_close_hook.create(ntdll.get_proc<void*>("NtClose"), nt_close_stub);
|
||||
|
||||
const auto nt_query_information_process = ntdll.get_proc<void*>("NtQueryInformationProcess");
|
||||
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);
|
||||
}
|
||||
|
||||
void post_unpack() override
|
||||
{
|
||||
remove_hardware_breakpoints();
|
||||
search_and_patch_integrity_checks();
|
||||
restore_debug_functions();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
REGISTER_COMPONENT(arxan::component)
|
165
src/client/component/arxan/breakpoints.hpp
Normal file
165
src/client/component/arxan/breakpoints.hpp
Normal file
@ -0,0 +1,165 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace mp
|
||||
{
|
||||
constexpr uint64_t int2d_breakpoint_addresses[] =
|
||||
{
|
||||
0x1400837C9,
|
||||
0x140088C28,
|
||||
0x140089CB9,
|
||||
0x1400922E9,
|
||||
0x140119A39,
|
||||
0x140199679,
|
||||
0x1401CBB59,
|
||||
0x140205DD9,
|
||||
0x140244A39,
|
||||
0x1402BCFC9,
|
||||
0x140346C39,
|
||||
0x14037E2A9,
|
||||
0x1404EB459,
|
||||
0x1404EB7C9,
|
||||
0x1405FCD09,
|
||||
0x140612AB3,
|
||||
0x140622D2E,
|
||||
0x1407D82DB,
|
||||
0x1407DC18C,
|
||||
0x140847869,
|
||||
0x140867E53,
|
||||
0x15143E836,
|
||||
0x15144B18C,
|
||||
0x15144BE27,
|
||||
0x1515E11E0,
|
||||
0x1515F18C9,
|
||||
0x1515F2ADE,
|
||||
0x15161E477,
|
||||
0x15162189F,
|
||||
0x151623FC5,
|
||||
0x151637C0D,
|
||||
0x151640170,
|
||||
0x151641309,
|
||||
0x1516514DA,
|
||||
0x1516528BC,
|
||||
0x151653351,
|
||||
0x15165627D,
|
||||
0x151656436,
|
||||
0x151683F6C,
|
||||
0x151686F24,
|
||||
0x1516881C5,
|
||||
0x151695BAD,
|
||||
0x15169714D,
|
||||
0x1516977E9,
|
||||
0x1516B6978,
|
||||
0x1516B933F,
|
||||
0x1516D75C7,
|
||||
0x1516D8B61,
|
||||
0x1516DDA42,
|
||||
0x1517064EC,
|
||||
0x151706519,
|
||||
0x151712445,
|
||||
0x151735FFA,
|
||||
0x151737872,
|
||||
0x151738660,
|
||||
0x151748404,
|
||||
0x151748DB3,
|
||||
0x15174D408,
|
||||
0x15174F7D2,
|
||||
0x151763BDE,
|
||||
0x151778988,
|
||||
0x1517886DB,
|
||||
0x15178DDAB,
|
||||
0x151793BB6,
|
||||
0x15179DC17,
|
||||
0x1517A6BC5,
|
||||
0x1517ACBA3,
|
||||
0x1517B3502,
|
||||
0x1517BAE0E,
|
||||
0x1517BB039,
|
||||
0x1517BB049,
|
||||
0x1517BC58E,
|
||||
0x1517BC7A0,
|
||||
0x1517BCF51,
|
||||
0x1517BFF71,
|
||||
0x1517C0761,
|
||||
0x1517C33F1,
|
||||
0x1517C44C4,
|
||||
0x1517C75AE,
|
||||
0x1517CDB3B,
|
||||
0x1517CE48A,
|
||||
0x1517D0559,
|
||||
0x1517D8E0F,
|
||||
0x1517D95C0,
|
||||
0x1517DA7FF,
|
||||
0x1517DD407,
|
||||
0x1517E11D2,
|
||||
0x1517E5C08,
|
||||
0x1517ED03C,
|
||||
0x1517EE9CC,
|
||||
0x1517EFB56,
|
||||
0x1517F31AC,
|
||||
0x1517F73D1,
|
||||
0x1517FB1CA,
|
||||
0x1518050D4,
|
||||
0x15180EFA2,
|
||||
0x15181A3BE,
|
||||
0x15181A5C4,
|
||||
0x151823361,
|
||||
0x1518233AC,
|
||||
0x151862C5B,
|
||||
0x151863913,
|
||||
0x15188E396,
|
||||
0x15188EA8D,
|
||||
0x15189760A,
|
||||
0x1518977B0,
|
||||
0x1518B14C9,
|
||||
0x1518B7227,
|
||||
0x1518B73AF,
|
||||
0x1518B84F9,
|
||||
0x1518C562A,
|
||||
0x1518EA89B,
|
||||
0x1518EFC20,
|
||||
0x1518F10A0,
|
||||
0x1518F4D00,
|
||||
0x1518FD8B3,
|
||||
0x1519033DA,
|
||||
0x151904C1E,
|
||||
0x151908F69,
|
||||
0x15190D0DB,
|
||||
0x151988E27,
|
||||
0x15198FDD5,
|
||||
0x151994C76,
|
||||
0x151995C6B,
|
||||
0x1519A66F1,
|
||||
0x1519A7438,
|
||||
0x1519A9B02,
|
||||
0x1519C4269,
|
||||
0x1519C4C75,
|
||||
0x1519EE7BB,
|
||||
0x1519EFFA8,
|
||||
0x1519F0368,
|
||||
0x1519F286A,
|
||||
0x151A04D7E,
|
||||
0x151A35E78,
|
||||
0x151A38F2F,
|
||||
0x151A450E4,
|
||||
0x151A76754,
|
||||
0x151A89BD1,
|
||||
0x151A89C0A,
|
||||
0x151AAB0EA,
|
||||
0x151AB3E09,
|
||||
0x151ABB764,
|
||||
0x151ABCC16,
|
||||
0x151ABF3F0,
|
||||
0x151AC0659,
|
||||
0x151AC5C1B,
|
||||
0x151AC8705,
|
||||
0x151AC88E2,
|
||||
0x151ACA788,
|
||||
0x151ACED96,
|
||||
0x151AD7F8A,
|
||||
0x151ADA318,
|
||||
0x151ADC9BE,
|
||||
0x151AE1BAF,
|
||||
};
|
||||
}
|
92
src/client/component/arxan/integrity.hpp
Normal file
92
src/client/component/arxan/integrity.hpp
Normal file
@ -0,0 +1,92 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace mp
|
||||
{
|
||||
constexpr uint64_t intact_integrity_check_blocks[] =
|
||||
{
|
||||
0x140000519,
|
||||
0x1407DB02E,
|
||||
0x140AFF08D,
|
||||
0x15143E06E,
|
||||
0x151440DC4,
|
||||
0x151455F3B,
|
||||
0x151489363,
|
||||
0x1515F10AB,
|
||||
0x1515F66D0,
|
||||
0x15161DB84,
|
||||
0x15161EAA8,
|
||||
0x151622530,
|
||||
0x151687D47,
|
||||
0x151690107,
|
||||
0x1516979D7,
|
||||
0x1516D3ED0,
|
||||
0x1516E2FC2,
|
||||
0x15173F3E9,
|
||||
0x15174B9FA,
|
||||
0x15175EAE1,
|
||||
0x15176E778,
|
||||
0x151787948,
|
||||
0x15179956D,
|
||||
0x1517B36C8,
|
||||
0x1517DE653,
|
||||
0x1517E2CE0,
|
||||
0x1517EEAE2,
|
||||
0x1517F92E1,
|
||||
0x1517FB858,
|
||||
0x151809719,
|
||||
0x151868985,
|
||||
0x1518BB832,
|
||||
0x1518EE362,
|
||||
0x1518FE162,
|
||||
0x1519024C3,
|
||||
0x151908639,
|
||||
0x151985713,
|
||||
0x15199565C,
|
||||
0x1519C6C5B,
|
||||
0x1519EB05E,
|
||||
0x1519F0EE1,
|
||||
0x1519F4A25,
|
||||
0x151A0503F,
|
||||
0x151A45752,
|
||||
0x151A7E83E,
|
||||
0x151A87C67,
|
||||
0x151AAF804,
|
||||
0x151AB3E72,
|
||||
0x151AC3482,
|
||||
0x151AC5B0F,
|
||||
0x151ACBE3C,
|
||||
};
|
||||
constexpr uint64_t split_integrity_check_blocks[] =
|
||||
{
|
||||
0x1514B9FC8,
|
||||
0x15161D235,
|
||||
0x1516DE9FF,
|
||||
0x151754691,
|
||||
0x151772B7F,
|
||||
0x1517B3CA9,
|
||||
0x1517DB0D0,
|
||||
0x1517E65DF,
|
||||
0x15181407C,
|
||||
0x151832847,
|
||||
0x15183B664,
|
||||
0x15199ABAA,
|
||||
0x1519AA163,
|
||||
0x1519C5852,
|
||||
0x1519C7B0E,
|
||||
0x1519C7F63,
|
||||
0x1519F3291,
|
||||
};
|
||||
}
|
||||
namespace sp
|
||||
{
|
||||
constexpr uint64_t intact_integrity_check_blocks[] =
|
||||
{
|
||||
0x15263308B,
|
||||
};
|
||||
constexpr uint64_t split_integrity_check_blocks[] =
|
||||
{
|
||||
0x152630C3F,
|
||||
};
|
||||
}
|
@ -1,8 +1,20 @@
|
||||
#include "hook.hpp"
|
||||
#include "string.hpp"
|
||||
|
||||
#include <map>
|
||||
#include <MinHook.h>
|
||||
|
||||
#include "concurrency.hpp"
|
||||
#include "string.hpp"
|
||||
#include "nt.hpp"
|
||||
|
||||
#ifdef max
|
||||
#undef max
|
||||
#endif
|
||||
|
||||
#ifdef min
|
||||
#undef min
|
||||
#endif
|
||||
|
||||
Mem seg_ptr(const SReg& segment, const uint64_t off)
|
||||
{
|
||||
auto mem = ptr_abs(off);
|
||||
@ -14,22 +26,164 @@ namespace utils::hook
|
||||
{
|
||||
namespace
|
||||
{
|
||||
[[maybe_unused]] class _
|
||||
size_t get_allocation_granularity()
|
||||
{
|
||||
SYSTEM_INFO info{};
|
||||
GetSystemInfo(&info);
|
||||
|
||||
return info.dwAllocationGranularity;
|
||||
}
|
||||
|
||||
uint8_t* allocate_somewhere_near(const void* base_address, const size_t granularity, const size_t size)
|
||||
{
|
||||
size_t target_address = reinterpret_cast<size_t>(base_address) - (1ull << 31);
|
||||
target_address &= ~(granularity - 1);
|
||||
|
||||
while (true)
|
||||
{
|
||||
target_address += granularity;
|
||||
|
||||
auto* target_ptr = reinterpret_cast<uint8_t*>(target_address);
|
||||
if (is_relatively_far(base_address, target_ptr))
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const auto res = VirtualAlloc(target_ptr, size, MEM_RESERVE | MEM_COMMIT,
|
||||
PAGE_EXECUTE_READWRITE);
|
||||
if (res)
|
||||
{
|
||||
if (is_relatively_far(base_address, target_ptr))
|
||||
{
|
||||
VirtualFree(res, 0, MEM_RELEASE);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return static_cast<uint8_t*>(res);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class memory
|
||||
{
|
||||
public:
|
||||
_()
|
||||
memory() = default;
|
||||
|
||||
memory(const void* ptr)
|
||||
: memory()
|
||||
{
|
||||
if (MH_Initialize() != MH_OK)
|
||||
static const auto allocation_granularity = get_allocation_granularity();
|
||||
this->length_ = allocation_granularity;
|
||||
|
||||
this->buffer_ = allocate_somewhere_near(ptr, allocation_granularity, this->length_);
|
||||
if (!this->buffer_)
|
||||
{
|
||||
throw std::runtime_error("Failed to initialize MinHook");
|
||||
throw std::runtime_error("Failed to allocate");
|
||||
}
|
||||
}
|
||||
|
||||
~_()
|
||||
~memory()
|
||||
{
|
||||
MH_Uninitialize();
|
||||
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<std::vector<memory>> memory_container{};
|
||||
|
||||
return memory_container.access<void*>([&](std::vector<memory>& 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);
|
||||
});
|
||||
}
|
||||
|
||||
void* initialize_min_hook()
|
||||
{
|
||||
static class min_hook_init
|
||||
{
|
||||
public:
|
||||
min_hook_init()
|
||||
{
|
||||
if (MH_Initialize() != MH_OK)
|
||||
{
|
||||
throw std::runtime_error("Failed to initialize MinHook");
|
||||
}
|
||||
}
|
||||
|
||||
~min_hook_init()
|
||||
{
|
||||
MH_Uninitialize();
|
||||
}
|
||||
} min_hook_init;
|
||||
return &min_hook_init;
|
||||
}
|
||||
}
|
||||
|
||||
void assembler::pushad64()
|
||||
@ -43,12 +197,26 @@ namespace utils::hook
|
||||
this->push(rsi);
|
||||
this->push(rdi);
|
||||
|
||||
this->sub(rsp, 0x40);
|
||||
this->push(r8);
|
||||
this->push(r9);
|
||||
this->push(r10);
|
||||
this->push(r11);
|
||||
this->push(r12);
|
||||
this->push(r13);
|
||||
this->push(r14);
|
||||
this->push(r15);
|
||||
}
|
||||
|
||||
void assembler::popad64()
|
||||
{
|
||||
this->add(rsp, 0x40);
|
||||
this->pop(r15);
|
||||
this->pop(r14);
|
||||
this->pop(r13);
|
||||
this->pop(r12);
|
||||
this->pop(r11);
|
||||
this->pop(r10);
|
||||
this->pop(r9);
|
||||
this->pop(r8);
|
||||
|
||||
this->pop(rdi);
|
||||
this->pop(rsi);
|
||||
@ -94,19 +262,26 @@ namespace utils::hook
|
||||
|
||||
asmjit::Error assembler::call(void* target)
|
||||
{
|
||||
return Assembler::call(size_t(target));
|
||||
return Assembler::call(reinterpret_cast<size_t>(target));
|
||||
}
|
||||
|
||||
asmjit::Error assembler::jmp(void* target)
|
||||
{
|
||||
return Assembler::jmp(size_t(target));
|
||||
return Assembler::jmp(reinterpret_cast<size_t>(target));
|
||||
}
|
||||
|
||||
detour::detour(const size_t place, void* target) : detour(reinterpret_cast<void*>(place), target)
|
||||
detour::detour()
|
||||
{
|
||||
(void)initialize_min_hook();
|
||||
}
|
||||
|
||||
detour::detour(const size_t place, void* target)
|
||||
: detour(reinterpret_cast<void*>(place), target)
|
||||
{
|
||||
}
|
||||
|
||||
detour::detour(void* place, void* target)
|
||||
: detour()
|
||||
{
|
||||
this->create(place, target);
|
||||
}
|
||||
@ -116,13 +291,19 @@ namespace utils::hook
|
||||
this->clear();
|
||||
}
|
||||
|
||||
void detour::enable() const
|
||||
void detour::enable()
|
||||
{
|
||||
MH_EnableHook(this->place_);
|
||||
|
||||
if (!this->moved_data_.empty())
|
||||
{
|
||||
this->move();
|
||||
}
|
||||
}
|
||||
|
||||
void detour::disable() const
|
||||
void detour::disable()
|
||||
{
|
||||
this->un_move();
|
||||
MH_DisableHook(this->place_);
|
||||
}
|
||||
|
||||
@ -148,11 +329,23 @@ namespace utils::hook
|
||||
{
|
||||
if (this->place_)
|
||||
{
|
||||
this->un_move();
|
||||
MH_RemoveHook(this->place_);
|
||||
}
|
||||
|
||||
this->place_ = nullptr;
|
||||
this->original_ = nullptr;
|
||||
this->moved_data_ = {};
|
||||
}
|
||||
|
||||
void detour::move()
|
||||
{
|
||||
this->moved_data_ = move_hook(this->place_);
|
||||
}
|
||||
|
||||
void* detour::get_place() const
|
||||
{
|
||||
return this->place_;
|
||||
}
|
||||
|
||||
void* detour::get_original() const
|
||||
@ -160,20 +353,29 @@ namespace utils::hook
|
||||
return this->original_;
|
||||
}
|
||||
|
||||
bool iat(const nt::library& library, const std::string& target_library, const std::string& process, void* stub)
|
||||
void detour::un_move()
|
||||
{
|
||||
if (!library.is_valid()) return false;
|
||||
if (!this->moved_data_.empty())
|
||||
{
|
||||
copy(this->place_, this->moved_data_.data(), this->moved_data_.size());
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<std::pair<void*, void*>> iat(const nt::library& library, const std::string& target_library,
|
||||
const std::string& process, void* stub)
|
||||
{
|
||||
if (!library.is_valid()) return {};
|
||||
|
||||
auto* const ptr = library.get_iat_entry(target_library, process);
|
||||
if (!ptr) return false;
|
||||
if (!ptr) return {};
|
||||
|
||||
DWORD protect;
|
||||
VirtualProtect(ptr, sizeof(*ptr), PAGE_EXECUTE_READWRITE, &protect);
|
||||
|
||||
*ptr = stub;
|
||||
std::swap(*ptr, stub);
|
||||
|
||||
VirtualProtect(ptr, sizeof(*ptr), protect, &protect);
|
||||
return true;
|
||||
return { {ptr, stub} };
|
||||
}
|
||||
|
||||
void nop(void* place, const size_t length)
|
||||
@ -208,23 +410,50 @@ namespace utils::hook
|
||||
copy(reinterpret_cast<void*>(place), data, length);
|
||||
}
|
||||
|
||||
void copy_string(void* place, const char* str)
|
||||
{
|
||||
copy(reinterpret_cast<void*>(place), str, strlen(str) + 1);
|
||||
}
|
||||
|
||||
void copy_string(const size_t place, const char* str)
|
||||
{
|
||||
copy_string(reinterpret_cast<void*>(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);
|
||||
const auto small_diff = int32_t(diff);
|
||||
return diff != int64_t(small_diff);
|
||||
return is_relatively_far(reinterpret_cast<size_t>(pointer), reinterpret_cast<size_t>(data), offset);
|
||||
}
|
||||
|
||||
bool is_relatively_far(const size_t pointer, const size_t data, const int offset)
|
||||
{
|
||||
const auto diff = static_cast<int64_t>(data - (pointer + offset));
|
||||
const auto small_diff = static_cast<int32_t>(diff);
|
||||
return diff != static_cast<int64_t>(small_diff);
|
||||
}
|
||||
|
||||
void call(void* pointer, void* data)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
auto* patch_pointer = PBYTE(pointer);
|
||||
set<uint8_t>(patch_pointer, 0xE8);
|
||||
set<int32_t>(patch_pointer + 1, int32_t(size_t(data) - (size_t(pointer) + 5)));
|
||||
uint8_t copy_data[5];
|
||||
copy_data[0] = 0xE8;
|
||||
*reinterpret_cast<int32_t*>(©_data[1]) = static_cast<int32_t>(reinterpret_cast<size_t>(data) - (
|
||||
reinterpret_cast<size_t>(pointer) + 5));
|
||||
|
||||
auto* patch_pointer = static_cast<PBYTE>(pointer);
|
||||
copy(patch_pointer, copy_data, sizeof(copy_data));
|
||||
}
|
||||
|
||||
void call(const size_t pointer, void* data)
|
||||
@ -237,39 +466,67 @@ namespace utils::hook
|
||||
return call(pointer, reinterpret_cast<void*>(data));
|
||||
}
|
||||
|
||||
void jump(void* pointer, void* data, const bool use_far)
|
||||
void jump(void* pointer, void* data, const bool use_far, const bool use_safe)
|
||||
{
|
||||
static const unsigned char jump_data[] = {
|
||||
0x48, 0xb8, 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11, 0xff, 0xe0
|
||||
};
|
||||
|
||||
static const unsigned char jump_data_safe[] = {
|
||||
0xFF, 0x25, 0x00, 0x00, 0x00, 0x00
|
||||
};
|
||||
|
||||
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);
|
||||
|
||||
if (use_far)
|
||||
{
|
||||
copy(patch_pointer, jump_data, sizeof(jump_data));
|
||||
copy(patch_pointer + 2, &data, sizeof(data));
|
||||
if (use_safe)
|
||||
{
|
||||
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
|
||||
{
|
||||
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<uint8_t>(patch_pointer, 0xE9);
|
||||
set<int32_t>(patch_pointer + 1, int32_t(size_t(data) - (size_t(pointer) + 5)));
|
||||
uint8_t copy_data[5];
|
||||
copy_data[0] = 0xE9;
|
||||
*reinterpret_cast<int32_t*>(©_data[1]) = int32_t(size_t(data) - (size_t(pointer) + 5));
|
||||
|
||||
copy(patch_pointer, copy_data, sizeof(copy_data));
|
||||
}
|
||||
}
|
||||
|
||||
void jump(const size_t pointer, void* data, const bool use_far)
|
||||
void jump(const size_t pointer, void* data, const bool use_far, const bool use_safe)
|
||||
{
|
||||
return jump(reinterpret_cast<void*>(pointer), data, use_far);
|
||||
return jump(reinterpret_cast<void*>(pointer), data, use_far, use_safe);
|
||||
}
|
||||
|
||||
void jump(const size_t pointer, const size_t data, const bool use_far)
|
||||
void jump(const size_t pointer, const size_t data, const bool use_far, const bool use_safe)
|
||||
{
|
||||
return jump(pointer, reinterpret_cast<void*>(data), use_far);
|
||||
return jump(pointer, reinterpret_cast<void*>(data), use_far, use_safe);
|
||||
}
|
||||
|
||||
void* assemble(const std::function<void(assembler&)>& asm_function)
|
||||
@ -284,24 +541,70 @@ namespace utils::hook
|
||||
asm_function(a);
|
||||
|
||||
void* result = nullptr;
|
||||
runtime.add(&result, &code);
|
||||
auto err_result = runtime.add(&result, &code);
|
||||
|
||||
if (err_result != asmjit::ErrorCode::kErrorOk)
|
||||
{
|
||||
printf("ASMJIT ERROR: %s\n", asmjit::DebugUtils::errorAsString(err_result));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void inject(void* pointer, const void* data)
|
||||
void inject(size_t pointer, size_t data)
|
||||
{
|
||||
if (is_relatively_far(pointer, data, 4))
|
||||
{
|
||||
throw std::runtime_error("Too far away to create 32bit relative branch");
|
||||
}
|
||||
|
||||
set<int32_t>(pointer, int32_t(size_t(data) - (size_t(pointer) + 4)));
|
||||
set<int32_t>(
|
||||
pointer, static_cast<int32_t>(data - (pointer + 4)));
|
||||
}
|
||||
|
||||
void inject(void* pointer, const void* data)
|
||||
{
|
||||
return inject(reinterpret_cast<size_t>(pointer), reinterpret_cast<size_t>(data));
|
||||
}
|
||||
|
||||
void inject(const size_t pointer, const void* data)
|
||||
{
|
||||
return inject(reinterpret_cast<void*>(pointer), data);
|
||||
return inject(pointer, reinterpret_cast<size_t>(data));
|
||||
}
|
||||
|
||||
std::vector<uint8_t> move_hook(void* pointer)
|
||||
{
|
||||
std::vector<uint8_t> original_data{};
|
||||
|
||||
auto* data_ptr = static_cast<uint8_t*>(pointer);
|
||||
if (data_ptr[0] == 0xE9)
|
||||
{
|
||||
original_data.resize(6);
|
||||
memmove(original_data.data(), pointer, original_data.size());
|
||||
|
||||
auto* target = follow_branch(data_ptr);
|
||||
nop(data_ptr, 1);
|
||||
jump(data_ptr + 1, target);
|
||||
}
|
||||
else if (data_ptr[0] == 0xFF && data_ptr[1] == 0x25)
|
||||
{
|
||||
original_data.resize(15);
|
||||
memmove(original_data.data(), pointer, original_data.size());
|
||||
|
||||
copy(data_ptr + 1, data_ptr, 14);
|
||||
nop(data_ptr, 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw std::runtime_error("No branch instruction found");
|
||||
}
|
||||
|
||||
return original_data;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> move_hook(const size_t pointer)
|
||||
{
|
||||
return move_hook(reinterpret_cast<void*>(pointer));
|
||||
}
|
||||
|
||||
void* follow_branch(void* address)
|
||||
@ -314,31 +617,4 @@ namespace utils::hook
|
||||
|
||||
return extract<void*>(data + 1);
|
||||
}
|
||||
|
||||
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<const uint8_t*>(base_address) - offset;
|
||||
if (utils::hook::is_relatively_far(base_address, target_address))
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const auto res = VirtualAlloc(const_cast<uint8_t*>(target_address), size, MEM_RESERVE | MEM_COMMIT,
|
||||
PAGE_EXECUTE_READWRITE);
|
||||
if (res)
|
||||
{
|
||||
if (utils::hook::is_relatively_far(base_address, target_address))
|
||||
{
|
||||
VirtualFree(res, 0, MEM_RELEASE);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return static_cast<uint8_t*>(res);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -13,20 +13,20 @@ namespace utils::hook
|
||||
{
|
||||
namespace detail
|
||||
{
|
||||
template<size_t entries>
|
||||
template <size_t Entries>
|
||||
std::vector<size_t(*)()> get_iota_functions()
|
||||
{
|
||||
if constexpr (entries == 0)
|
||||
if constexpr (Entries == 0)
|
||||
{
|
||||
std::vector<size_t(*)()> functions;
|
||||
return functions;
|
||||
}
|
||||
else
|
||||
{
|
||||
auto functions = get_iota_functions<entries - 1>();
|
||||
auto functions = get_iota_functions<Entries - 1>();
|
||||
functions.emplace_back([]()
|
||||
{
|
||||
return entries - 1;
|
||||
return Entries - 1;
|
||||
});
|
||||
return functions;
|
||||
}
|
||||
@ -39,8 +39,8 @@ namespace utils::hook
|
||||
// Example:
|
||||
// ID3D11Device* device = ...
|
||||
// auto entry = get_vtable_entry(device, &ID3D11Device::CreateTexture2D);
|
||||
template <size_t entries = 100, typename Class, typename T, typename... Args>
|
||||
void** get_vtable_entry(Class* obj, T (Class::* entry)(Args ...))
|
||||
template <size_t Entries = 100, typename Class, typename T, typename... Args>
|
||||
void** get_vtable_entry(Class* obj, T(Class::* entry)(Args ...))
|
||||
{
|
||||
union
|
||||
{
|
||||
@ -50,11 +50,11 @@ namespace utils::hook
|
||||
|
||||
func = entry;
|
||||
|
||||
auto iota_functions = detail::get_iota_functions<entries>();
|
||||
auto iota_functions = detail::get_iota_functions<Entries>();
|
||||
auto* object = iota_functions.data();
|
||||
|
||||
using FakeFunc = size_t(__thiscall*)(void* self);
|
||||
auto index = static_cast<FakeFunc>(pointer)(&object);
|
||||
using fake_func = size_t(__thiscall*)(void* self);
|
||||
auto index = static_cast<fake_func>(pointer)(&object);
|
||||
|
||||
void** obj_v_table = *reinterpret_cast<void***>(obj);
|
||||
return &obj_v_table[index];
|
||||
@ -88,7 +88,7 @@ namespace utils::hook
|
||||
class detour
|
||||
{
|
||||
public:
|
||||
detour() = default;
|
||||
detour();
|
||||
detour(void* place, void* target);
|
||||
detour(size_t place, void* target);
|
||||
~detour();
|
||||
@ -102,13 +102,15 @@ namespace utils::hook
|
||||
{
|
||||
if (this != &other)
|
||||
{
|
||||
this->~detour();
|
||||
this->clear();
|
||||
|
||||
this->place_ = other.place_;
|
||||
this->original_ = other.original_;
|
||||
this->moved_data_ = other.moved_data_;
|
||||
|
||||
other.place_ = nullptr;
|
||||
other.original_ = nullptr;
|
||||
other.moved_data_ = {};
|
||||
}
|
||||
|
||||
return *this;
|
||||
@ -117,13 +119,17 @@ namespace utils::hook
|
||||
detour(const detour&) = delete;
|
||||
detour& operator=(const detour&) = delete;
|
||||
|
||||
void enable() const;
|
||||
void disable() const;
|
||||
void enable();
|
||||
void disable();
|
||||
|
||||
void create(void* place, void* target);
|
||||
void create(size_t place, void* target);
|
||||
void clear();
|
||||
|
||||
void move();
|
||||
|
||||
void* get_place() const;
|
||||
|
||||
template <typename T>
|
||||
T* get() const
|
||||
{
|
||||
@ -139,11 +145,15 @@ namespace utils::hook
|
||||
[[nodiscard]] void* get_original() const;
|
||||
|
||||
private:
|
||||
std::vector<uint8_t> moved_data_{};
|
||||
void* place_{};
|
||||
void* original_{};
|
||||
|
||||
void un_move();
|
||||
};
|
||||
|
||||
bool iat(const nt::library& library, const std::string& target_library, const std::string& process, void* stub);
|
||||
std::optional<std::pair<void*, void*>> iat(const nt::library& library, const std::string& target_library,
|
||||
const std::string& process, void* stub);
|
||||
|
||||
void nop(void* place, size_t length);
|
||||
void nop(size_t place, size_t length);
|
||||
@ -151,20 +161,28 @@ 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);
|
||||
bool is_relatively_far(size_t pointer, size_t data, int offset = 5);
|
||||
|
||||
void call(void* pointer, void* data);
|
||||
void call(size_t pointer, void* data);
|
||||
void call(size_t pointer, size_t data);
|
||||
|
||||
void jump(void* pointer, void* data, bool use_far = false);
|
||||
void jump(size_t pointer, void* data, bool use_far = false);
|
||||
void jump(size_t pointer, size_t data, bool use_far = false);
|
||||
void jump(void* pointer, void* data, bool use_far = false, bool use_safe = false);
|
||||
void jump(size_t pointer, void* data, bool use_far = false, bool use_safe = false);
|
||||
void jump(size_t pointer, size_t data, bool use_far = false, bool use_safe = false);
|
||||
|
||||
void* assemble(const std::function<void(assembler&)>& asm_function);
|
||||
|
||||
void inject(void* pointer, const void* data);
|
||||
void inject(size_t pointer, const void* data);
|
||||
void inject(size_t pointer, size_t data);
|
||||
|
||||
std::vector<uint8_t> move_hook(void* pointer);
|
||||
std::vector<uint8_t> move_hook(size_t pointer);
|
||||
|
||||
template <typename T>
|
||||
T extract(void* address)
|
||||
@ -177,19 +195,13 @@ namespace utils::hook
|
||||
void* follow_branch(void* address);
|
||||
|
||||
template <typename T>
|
||||
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<T*>(place) = value;
|
||||
|
||||
VirtualProtect(place, sizeof(T), old_protect, &old_protect);
|
||||
FlushInstructionCache(GetCurrentProcess(), place, sizeof(T));
|
||||
copy(place, &value, sizeof(value));
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
static void set(const size_t place, T value)
|
||||
static void set(const size_t place, T value = false)
|
||||
{
|
||||
return set<T>(reinterpret_cast<void*>(place), value);
|
||||
}
|
||||
@ -206,8 +218,6 @@ namespace utils::hook
|
||||
return static_cast<T(*)(Args ...)>(func)(args...);
|
||||
}
|
||||
|
||||
uint8_t* allocate_somewhere_near(const void* base_address, const size_t size);
|
||||
|
||||
template <size_t Base>
|
||||
void* allocate_far_jump()
|
||||
{
|
||||
@ -255,11 +265,4 @@ namespace utils::hook
|
||||
const auto pos = create_far_jump<Base>(dest);
|
||||
jump(address, pos, false);
|
||||
}
|
||||
|
||||
template <size_t Base, typename T>
|
||||
void far_call(const size_t address, const T dest)
|
||||
{
|
||||
const auto pos = create_far_jump<Base>(dest);
|
||||
call(address, pos);
|
||||
}
|
||||
}
|
@ -4,6 +4,14 @@
|
||||
|
||||
#include <intrin.h>
|
||||
|
||||
#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<uint8_t>(strtol(str, nullptr, 16));
|
||||
|
||||
if (!has_nibble)
|
||||
@ -68,15 +76,15 @@ namespace utils::hook
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<size_t> 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<size_t> 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<size_t> result;
|
||||
std::vector<uint8_t*> 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<size_t> 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<size_t> result;
|
||||
__declspec(align(16)) char desired_mask[16] = {0};
|
||||
std::vector<uint8_t*> 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<const __m128i*>(address));
|
||||
const auto comparison = _mm_cmpestrm(value, 16, comparand, static_cast<int>(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<size_t> result;
|
||||
std::vector<uint8_t*> result;
|
||||
std::vector<std::thread> 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
|
||||
|
@ -7,33 +7,9 @@ namespace utils::hook
|
||||
class signature final
|
||||
{
|
||||
public:
|
||||
class signature_result
|
||||
{
|
||||
public:
|
||||
signature_result(std::vector<size_t>&& matches) : matches_(std::move(matches))
|
||||
{
|
||||
}
|
||||
using signature_result = std::vector<uint8_t*>;
|
||||
|
||||
[[nodiscard]] uint8_t* get(const size_t index) const
|
||||
{
|
||||
if (index >= this->count())
|
||||
{
|
||||
throw std::runtime_error("Invalid index");
|
||||
}
|
||||
|
||||
return reinterpret_cast<uint8_t*>(this->matches_[index]);
|
||||
}
|
||||
|
||||
[[nodiscard]] size_t count() const
|
||||
{
|
||||
return this->matches_.size();
|
||||
}
|
||||
|
||||
private:
|
||||
std::vector<size_t> 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<size_t> process_range(uint8_t* start, size_t length) const;
|
||||
std::vector<size_t> process_range_linear(uint8_t* start, size_t length) const;
|
||||
std::vector<size_t> 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;
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user