mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-17 16:17:06 +02:00
crash fix + launch arg
This commit is contained in:
1 parent
fa07f912f4
commit
2fd92ba989
8 files changed
+298
-251
No files matched your search
@@ -3,15 +3,23 @@
|
||||
#include <cstdint>
|
||||
|
||||
namespace dingosdk::game::build::v20260929::startup_interventions {
|
||||
// Interpreter frame clear (REP STOSB) that overlaps the protected frame state.
|
||||
inline constexpr std::uintptr_t frame = 0x0880d7d5;
|
||||
inline constexpr std::array<std::uint8_t, 2> frame_instruction{0xf3, 0xaa};
|
||||
// Handler and data expected in R9/R14 at the frame clear.
|
||||
inline constexpr std::uintptr_t frame_handler = 0x0880d710;
|
||||
inline constexpr std::uintptr_t frame_data = 0x086772fc;
|
||||
// Exit thunk (JMP [rip+...]) and the return address it is expected to be called from.
|
||||
// Build 25414733 clears the saved native frame through the VM's byte store,
|
||||
// not the 20260908 REP STOSB handler. Capture before its first zero byte.
|
||||
inline constexpr std::uintptr_t frame = 0x08ae2785;
|
||||
inline constexpr std::array<std::uint8_t, 3> frame_instruction{0x40, 0x88, 0x32}; // MOV [RDX], SIL
|
||||
inline constexpr std::array<std::uint8_t, 8> frame_fingerprint{
|
||||
0x40, 0x88, 0x32, 0x5e, 0x66, 0x8b, 0x41, 0x08};
|
||||
inline constexpr std::uintptr_t frame_data = 0x08b7bf3e; // R11: clear bytecode
|
||||
inline constexpr std::uintptr_t frame_cursor = 0x08b7bf72; // [RCX + 0xd8]
|
||||
inline constexpr std::uintptr_t frame_size = 0xc0;
|
||||
inline constexpr std::uintptr_t frame_stack_offset = 0xb70;
|
||||
inline constexpr std::uintptr_t frame_vm_offset = 0xb40;
|
||||
inline constexpr std::uintptr_t frame_saved_registers_offset = 0x1b0;
|
||||
inline constexpr std::uintptr_t frame_cursor_offset = 0xd8;
|
||||
inline constexpr std::uint64_t frame_count = 0xdaf400c0;
|
||||
inline constexpr std::uintptr_t exit_stack_offset = 0x128;
|
||||
inline constexpr std::uintptr_t exit_thunk = 0x08a06ff3;
|
||||
inline constexpr std::uintptr_t exit_return = 0x087755f2;
|
||||
inline constexpr std::uintptr_t exit_return = 0x087fe7fb;
|
||||
inline constexpr std::array<std::uint8_t, 6> exit_instruction{
|
||||
0xff, 0x25, 0x7f, 0x43, 0x5e, 0xfd};
|
||||
}
|
||||
@@ -9,195 +9,201 @@
|
||||
|
||||
namespace dingosdk {
|
||||
namespace {
|
||||
|
||||
// Re-observed on the supported image: the interpreter frame is based on RBP,
|
||||
// and its 0x630-byte clear overlaps that frame. These are not Season 4 offsets.
|
||||
constexpr std::uint64_t protected_state = 0x343cfc26;
|
||||
constexpr std::uint32_t direction_flag = 0x400;
|
||||
namespace pins = addr::startup_interventions;
|
||||
constexpr std::uint8_t breakpoint_opcode = 0xcc;
|
||||
|
||||
StartupInterventionMachine machine;
|
||||
std::atomic<std::uintptr_t> installed_base{};
|
||||
// The frame site sits in protector code that is regenerated for every game
|
||||
// build; when the table has no verified site for this build only the exit
|
||||
// intervention is armed.
|
||||
std::atomic_bool frame_armed{};
|
||||
PVOID handler_handle{};
|
||||
SRWLOCK handler_lock = SRWLOCK_INIT;
|
||||
std::array<std::uint8_t, startup_frame_size> saved_frame{};
|
||||
std::uintptr_t saved_frame_address{};
|
||||
|
||||
struct HandlerGuard {
|
||||
DWORD last_error{GetLastError()};
|
||||
HandlerGuard() noexcept { AcquireSRWLockExclusive(&handler_lock); }
|
||||
~HandlerGuard() { ReleaseSRWLockExclusive(&handler_lock); SetLastError(last_error); }
|
||||
};
|
||||
|
||||
template<class T>
|
||||
bool read_local(std::uintptr_t address, T& output) noexcept {
|
||||
SIZE_T count{};
|
||||
return address && ReadProcessMemory(GetCurrentProcess(),
|
||||
reinterpret_cast<const void*>(address), &output, sizeof(output), &count) &&
|
||||
count == sizeof(output);
|
||||
reinterpret_cast<const void*>(address), &output, sizeof(output), &count) && count == sizeof(output);
|
||||
}
|
||||
|
||||
bool executable_image_range(std::uintptr_t image_base, std::uintptr_t address,
|
||||
std::size_t size) noexcept {
|
||||
bool writable_range(std::uintptr_t address, std::size_t size) noexcept {
|
||||
MEMORY_BASIC_INFORMATION memory{};
|
||||
if (!address || !size || !VirtualQuery(reinterpret_cast<const void*>(address), &memory, sizeof(memory)) ||
|
||||
memory.State != MEM_COMMIT || (memory.Protect & (PAGE_GUARD | PAGE_NOACCESS))) return false;
|
||||
switch (memory.Protect & 0xff) {
|
||||
case PAGE_READWRITE: case PAGE_WRITECOPY: case PAGE_EXECUTE_READWRITE: case PAGE_EXECUTE_WRITECOPY: break;
|
||||
default: return false;
|
||||
}
|
||||
const auto region = reinterpret_cast<std::uintptr_t>(memory.BaseAddress);
|
||||
return address >= region && address - region < memory.RegionSize && size <= memory.RegionSize - (address - region);
|
||||
}
|
||||
|
||||
bool write_local(std::uintptr_t address, const void* data, std::size_t size) noexcept {
|
||||
SIZE_T count{};
|
||||
return writable_range(address, size) && WriteProcessMemory(GetCurrentProcess(),
|
||||
reinterpret_cast<void*>(address), data, size, &count) && count == size;
|
||||
}
|
||||
|
||||
bool executable_image_range(std::uintptr_t image_base, std::uintptr_t address, std::size_t size) noexcept {
|
||||
MEMORY_BASIC_INFORMATION memory{};
|
||||
if (!image_base || !address || !size ||
|
||||
!VirtualQuery(reinterpret_cast<const void*>(address), &memory, sizeof(memory))) return false;
|
||||
const auto executable = [](DWORD protection) {
|
||||
if (protection & (PAGE_GUARD | PAGE_NOACCESS)) return false;
|
||||
switch (protection & 0xff) {
|
||||
case PAGE_EXECUTE:
|
||||
case PAGE_EXECUTE_READ:
|
||||
case PAGE_EXECUTE_READWRITE:
|
||||
case PAGE_EXECUTE_WRITECOPY:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
};
|
||||
const auto end = reinterpret_cast<std::uintptr_t>(memory.BaseAddress) + memory.RegionSize;
|
||||
!VirtualQuery(reinterpret_cast<const void*>(address), &memory, sizeof(memory)) ||
|
||||
(memory.Protect & (PAGE_GUARD | PAGE_NOACCESS))) return false;
|
||||
switch (memory.Protect & 0xff) {
|
||||
case PAGE_EXECUTE: case PAGE_EXECUTE_READ: case PAGE_EXECUTE_READWRITE: case PAGE_EXECUTE_WRITECOPY: break;
|
||||
default: return false;
|
||||
}
|
||||
const auto region = reinterpret_cast<std::uintptr_t>(memory.BaseAddress);
|
||||
return memory.State == MEM_COMMIT && memory.Type == MEM_IMAGE &&
|
||||
reinterpret_cast<std::uintptr_t>(memory.AllocationBase) == image_base &&
|
||||
executable(memory.Protect) && address <= end && size <= end - address;
|
||||
address >= region && address - region < memory.RegionSize && size <= memory.RegionSize - (address - region);
|
||||
}
|
||||
|
||||
bool write_opcode(std::uintptr_t address, std::uint8_t opcode) noexcept {
|
||||
DWORD old_protection{};
|
||||
if (!VirtualProtect(reinterpret_cast<void*>(address), 1, PAGE_EXECUTE_READWRITE,
|
||||
&old_protection)) return false;
|
||||
if (!VirtualProtect(reinterpret_cast<void*>(address), 1, PAGE_EXECUTE_READWRITE, &old_protection)) return false;
|
||||
SIZE_T count{};
|
||||
const auto wrote = WriteProcessMemory(GetCurrentProcess(), reinterpret_cast<void*>(address),
|
||||
&opcode, sizeof(opcode), &count) && count == sizeof(opcode);
|
||||
const auto flushed = wrote && FlushInstructionCache(
|
||||
GetCurrentProcess(), reinterpret_cast<void*>(address), 1) != FALSE;
|
||||
const auto wrote = WriteProcessMemory(GetCurrentProcess(), reinterpret_cast<void*>(address), &opcode, 1, &count) && count == 1;
|
||||
const auto flushed = wrote && FlushInstructionCache(GetCurrentProcess(), reinterpret_cast<void*>(address), 1) != FALSE;
|
||||
DWORD ignored{};
|
||||
const auto protected_again = VirtualProtect(
|
||||
reinterpret_cast<void*>(address), 1, old_protection, &ignored) != FALSE;
|
||||
const auto protected_again = VirtualProtect(reinterpret_cast<void*>(address), 1, old_protection, &ignored) != FALSE;
|
||||
return wrote && flushed && protected_again;
|
||||
}
|
||||
|
||||
StartupInterventionRegisters capture_registers(const CONTEXT& context,
|
||||
std::uintptr_t exception_address) noexcept {
|
||||
return {exception_address, context.Rsp, context.Rax, context.Rbx, context.Rcx,
|
||||
context.Rdx, context.Rbp, context.Rsi, context.Rdi, context.R8,
|
||||
context.R9, context.R14, context.EFlags};
|
||||
StartupInterventionRegisters capture_registers(const CONTEXT& c, std::uintptr_t address) noexcept {
|
||||
return {address, c.Rsp, c.Rax, c.Rbx, c.Rcx, c.Rdx, c.Rbp, c.Rsi, c.Rdi,
|
||||
c.R8, c.R9, c.R10, c.R11, c.R14, c.R15, c.EFlags};
|
||||
}
|
||||
|
||||
void apply_registers(CONTEXT& context, const StartupInterventionRegisters& registers) noexcept {
|
||||
context.Rip = registers.rip;
|
||||
context.Rsp = registers.rsp;
|
||||
context.Rax = registers.rax;
|
||||
context.Rcx = registers.rcx;
|
||||
context.Rdi = registers.rdi;
|
||||
bool frame_shape_matches(std::uintptr_t base, const StartupInterventionRegisters& r) noexcept {
|
||||
return base == supported_build::preferred_image_base && r.rip == base + startup_frame_rva &&
|
||||
r.rbp >= pins::frame_stack_offset && r.rsp == r.rbp - pins::frame_stack_offset &&
|
||||
r.rcx == r.rbp - pins::frame_vm_offset && r.rdx == r.rbp - startup_frame_size &&
|
||||
r.r15 == r.rbp - pins::frame_saved_registers_offset &&
|
||||
r.r11 == base + startup_frame_data_rva && r.r9 == pins::frame_count && r.r10 == r.r9 &&
|
||||
(r.rsi & 0xff) == 0;
|
||||
}
|
||||
|
||||
LONG CALLBACK intervention_handler(EXCEPTION_POINTERS* pointers) {
|
||||
if (!pointers || !pointers->ExceptionRecord || !pointers->ContextRecord ||
|
||||
pointers->ExceptionRecord->ExceptionCode != EXCEPTION_BREAKPOINT)
|
||||
pointers->ExceptionRecord->ExceptionCode != EXCEPTION_BREAKPOINT) return EXCEPTION_CONTINUE_SEARCH;
|
||||
const auto base = installed_base.load(std::memory_order_acquire);
|
||||
const auto address = reinterpret_cast<std::uintptr_t>(pointers->ExceptionRecord->ExceptionAddress);
|
||||
if (!base || (address != base + startup_frame_rva && address != base + startup_exit_rva))
|
||||
return EXCEPTION_CONTINUE_SEARCH;
|
||||
const auto image_base = installed_base.load(std::memory_order_acquire);
|
||||
if (!image_base) return EXCEPTION_CONTINUE_SEARCH;
|
||||
const auto address = reinterpret_cast<std::uintptr_t>(
|
||||
pointers->ExceptionRecord->ExceptionAddress);
|
||||
auto registers = capture_registers(*pointers->ContextRecord, address);
|
||||
HandlerGuard guard;
|
||||
auto& context = *pointers->ContextRecord;
|
||||
auto registers = capture_registers(context, address);
|
||||
const auto thread = GetCurrentThreadId();
|
||||
|
||||
if (address == image_base + startup_frame_rva) {
|
||||
std::array<std::uint8_t, 2> patched{};
|
||||
std::uint64_t state{};
|
||||
if (!read_local(address, patched) || patched[0] != breakpoint_opcode ||
|
||||
registers.rbp > std::numeric_limits<std::uintptr_t>::max() - startup_frame_state_offset ||
|
||||
!read_local(registers.rbp + startup_frame_state_offset, state)) {
|
||||
machine.fail();
|
||||
return EXCEPTION_CONTINUE_SEARCH;
|
||||
if (address == base + startup_frame_rva) {
|
||||
std::array<std::uint8_t, 3> instruction{};
|
||||
if (!read_local(address, instruction) ||
|
||||
(instruction[0] != breakpoint_opcode && instruction[0] != startup_frame_instruction[0])) return EXCEPTION_CONTINUE_SEARCH;
|
||||
const bool patched = instruction[0] == breakpoint_opcode;
|
||||
instruction[0] = startup_frame_instruction[0];
|
||||
if (instruction != startup_frame_instruction) { machine.fail(); return EXCEPTION_CONTINUE_SEARCH; }
|
||||
// Another thread may have fetched INT3 before capture restored the site.
|
||||
if (!patched) { context.Rip = address; return EXCEPTION_CONTINUE_EXECUTION; }
|
||||
if (machine.phase() == StartupInterventionPhase::armed && frame_shape_matches(base, registers)) {
|
||||
StartupFrameMetadata metadata{};
|
||||
if (!read_local(registers.rcx + startup_frame_cursor_offset, metadata.cursor)) {
|
||||
machine.fail(); return EXCEPTION_CONTINUE_SEARCH;
|
||||
}
|
||||
if (startup_frame_clear_matches(base, registers, instruction, metadata)) {
|
||||
const auto frame_address = registers.rbp - startup_frame_size;
|
||||
if (!read_local(frame_address, saved_frame) ||
|
||||
!machine.capture_frame(base, registers, instruction, metadata, thread) ||
|
||||
!write_opcode(address, startup_frame_instruction[0])) {
|
||||
machine.fail(); return EXCEPTION_CONTINUE_SEARCH;
|
||||
}
|
||||
saved_frame_address = frame_address;
|
||||
// Let the original clear run; restore this exact frame only if the
|
||||
// guarded exit arrives on the same thread and native stack frame.
|
||||
context.Rip = address;
|
||||
logging::log(logging::Level::info, logging::Channel::runtime,
|
||||
"Startup frame captured: {} bytes, site {:#x}, bytecode {:#x}",
|
||||
saved_frame.size(), startup_frame_rva, startup_frame_data_rva);
|
||||
return EXCEPTION_CONTINUE_EXECUTION;
|
||||
}
|
||||
}
|
||||
const std::array<std::uint8_t, 2> original{startup_frame_instruction[0], patched[1]};
|
||||
if (!machine.preserve_frame(image_base, registers, original, state) ||
|
||||
!write_opcode(address, startup_frame_instruction[0])) {
|
||||
// This VM opcode also performs ordinary byte stores before the target
|
||||
// clear. Reproduce MOV [RDX], SIL without changing flags or registers.
|
||||
const auto byte = static_cast<std::uint8_t>(registers.rsi);
|
||||
if (!write_local(registers.rdx, &byte, sizeof(byte))) {
|
||||
machine.fail();
|
||||
return EXCEPTION_CONTINUE_SEARCH;
|
||||
logging::write(logging::Level::error, logging::Channel::runtime,
|
||||
"Startup byte-store intervention could not replay an ordinary store");
|
||||
if (!write_opcode(address, startup_frame_instruction[0])) return EXCEPTION_CONTINUE_SEARCH;
|
||||
context.Rip = address;
|
||||
return EXCEPTION_CONTINUE_EXECUTION;
|
||||
}
|
||||
apply_registers(*pointers->ContextRecord, registers);
|
||||
context.Rip = address + startup_frame_instruction.size();
|
||||
return EXCEPTION_CONTINUE_EXECUTION;
|
||||
}
|
||||
|
||||
if (address == image_base + startup_exit_rva) {
|
||||
std::array<std::uint8_t, 6> patched{};
|
||||
std::uint64_t return_address{};
|
||||
if (!read_local(address, patched) || patched[0] != breakpoint_opcode ||
|
||||
registers.rsp > std::numeric_limits<std::uintptr_t>::max() - sizeof(return_address) ||
|
||||
!read_local(registers.rsp, return_address)) {
|
||||
machine.fail();
|
||||
return EXCEPTION_CONTINUE_SEARCH;
|
||||
}
|
||||
patched[0] = startup_exit_instruction[0];
|
||||
logging::log(logging::Level::info, logging::Channel::runtime,
|
||||
"Startup exit intervention hit: return {:#x}, rcx {:#x}, rdx {:#x}, frame site {}",
|
||||
return_address >= image_base ? return_address - image_base : return_address, registers.rcx,
|
||||
registers.rdx, frame_armed.load() ? "armed" : "not armed");
|
||||
if (!machine.return_from_exit(image_base, registers, patched, return_address) ||
|
||||
!write_opcode(address, startup_exit_instruction[0])) {
|
||||
machine.fail();
|
||||
return EXCEPTION_CONTINUE_SEARCH;
|
||||
}
|
||||
apply_registers(*pointers->ContextRecord, registers);
|
||||
return EXCEPTION_CONTINUE_EXECUTION;
|
||||
std::array<std::uint8_t, 6> instruction{};
|
||||
std::uint64_t return_address{};
|
||||
if (!read_local(address, instruction) || instruction[0] != breakpoint_opcode ||
|
||||
!read_local(registers.rsp, return_address)) { machine.fail(); return EXCEPTION_CONTINUE_SEARCH; }
|
||||
instruction[0] = startup_exit_instruction[0];
|
||||
logging::log(logging::Level::info, logging::Channel::runtime,
|
||||
"Startup exit intervention hit: return {:#x}, rcx {:#x}, rdx {:#x}, frame captured {}",
|
||||
return_address >= base ? return_address - base : return_address, registers.rcx, registers.rdx,
|
||||
machine.phase() == StartupInterventionPhase::frame_captured);
|
||||
if (!machine.return_from_exit(base, registers, instruction, return_address, thread) ||
|
||||
!write_local(saved_frame_address, saved_frame.data(), saved_frame.size()) ||
|
||||
!write_opcode(address, startup_exit_instruction[0])) {
|
||||
machine.fail();
|
||||
logging::write(logging::Level::error, logging::Channel::runtime, "Startup exit/frame restoration guard failed");
|
||||
return EXCEPTION_CONTINUE_SEARCH;
|
||||
}
|
||||
return EXCEPTION_CONTINUE_SEARCH;
|
||||
context.Rax = registers.rax;
|
||||
context.Rip = registers.rip;
|
||||
context.Rsp = registers.rsp;
|
||||
logging::log(logging::Level::info, logging::Channel::runtime,
|
||||
"Startup frame restored: {} bytes; exit intervention completed", saved_frame.size());
|
||||
return EXCEPTION_CONTINUE_EXECUTION;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool startup_frame_clear_matches(std::uintptr_t base, const StartupInterventionRegisters& registers,
|
||||
const std::array<std::uint8_t, 3>& instruction, const StartupFrameMetadata& metadata) noexcept {
|
||||
return frame_shape_matches(base, registers) && instruction == startup_frame_instruction &&
|
||||
metadata.cursor == base + startup_frame_cursor_rva;
|
||||
}
|
||||
|
||||
bool StartupInterventionMachine::arm() noexcept {
|
||||
auto expected = StartupInterventionPhase::uninitialized;
|
||||
return phase_.compare_exchange_strong(expected, StartupInterventionPhase::armed,
|
||||
std::memory_order_acq_rel);
|
||||
return phase_.compare_exchange_strong(expected, StartupInterventionPhase::armed, std::memory_order_acq_rel);
|
||||
}
|
||||
|
||||
bool StartupInterventionMachine::preserve_frame(
|
||||
std::uintptr_t image_base, StartupInterventionRegisters& registers,
|
||||
const std::array<std::uint8_t, 2>& instruction,
|
||||
std::uint64_t state) noexcept {
|
||||
bool StartupInterventionMachine::capture_frame(std::uintptr_t base, const StartupInterventionRegisters& registers,
|
||||
const std::array<std::uint8_t, 3>& instruction, const StartupFrameMetadata& metadata, std::uint32_t thread) noexcept {
|
||||
auto expected = StartupInterventionPhase::armed;
|
||||
if (!phase_.compare_exchange_strong(expected, StartupInterventionPhase::handling_frame,
|
||||
std::memory_order_acq_rel)) return false;
|
||||
const auto matches = image_base == supported_build::preferred_image_base &&
|
||||
registers.rip == image_base + startup_frame_rva && registers.rsp == registers.rbp &&
|
||||
registers.rcx == 0x630 &&
|
||||
registers.rdi <= std::numeric_limits<std::uint64_t>::max() - 0x210 &&
|
||||
registers.rdi + 0x210 == registers.rsp && registers.r8 == registers.rdi &&
|
||||
registers.r9 == image_base + startup_frame_handler_rva &&
|
||||
registers.r14 == image_base + startup_frame_data_rva &&
|
||||
(registers.rax & 0xff) == 0 && !(registers.eflags & direction_flag) &&
|
||||
instruction == startup_frame_instruction && state == protected_state &&
|
||||
registers.rdi <= std::numeric_limits<std::uint64_t>::max() - registers.rcx;
|
||||
if (!matches) {
|
||||
phase_.store(StartupInterventionPhase::failed, std::memory_order_release);
|
||||
return false;
|
||||
}
|
||||
registers.rdi += registers.rcx;
|
||||
registers.rcx = 0;
|
||||
registers.rip += instruction.size();
|
||||
phase_.store(StartupInterventionPhase::frame_preserved, std::memory_order_release);
|
||||
if (!phase_.compare_exchange_strong(expected, StartupInterventionPhase::handling_frame, std::memory_order_acq_rel)) return false;
|
||||
if (!thread || !startup_frame_clear_matches(base, registers, instruction, metadata)) { fail(); return false; }
|
||||
frame_base_ = registers.rbp;
|
||||
frame_thread_ = thread;
|
||||
phase_.store(StartupInterventionPhase::frame_captured, std::memory_order_release);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool StartupInterventionMachine::return_from_exit(
|
||||
std::uintptr_t image_base, StartupInterventionRegisters& registers,
|
||||
const std::array<std::uint8_t, 6>& instruction,
|
||||
std::uint64_t return_address) noexcept {
|
||||
const bool frame = frame_armed.load(std::memory_order_acquire);
|
||||
auto expected = frame ? StartupInterventionPhase::frame_preserved : StartupInterventionPhase::armed;
|
||||
if (!phase_.compare_exchange_strong(expected, StartupInterventionPhase::handling_exit,
|
||||
std::memory_order_acq_rel)) return false;
|
||||
// With the frame site verified the exact protector return address is pinned
|
||||
// too; without it, TerminateProcess(current process, 0x32) from this build's
|
||||
// protector thunk back into the image is the guard.
|
||||
const bool return_matches = return_address == image_base + startup_exit_return_rva ||
|
||||
(!frame && return_address >= image_base && return_address - image_base < startup_expected_image_size);
|
||||
const auto matches = image_base == supported_build::preferred_image_base &&
|
||||
registers.rip == image_base + startup_exit_rva &&
|
||||
registers.rcx == std::numeric_limits<std::uint64_t>::max() &&
|
||||
registers.rdx == 0x32 && return_matches &&
|
||||
instruction == startup_exit_instruction &&
|
||||
registers.rsp <= std::numeric_limits<std::uint64_t>::max() - sizeof(return_address);
|
||||
if (!matches) {
|
||||
phase_.store(StartupInterventionPhase::failed, std::memory_order_release);
|
||||
return false;
|
||||
}
|
||||
bool StartupInterventionMachine::return_from_exit(std::uintptr_t base, StartupInterventionRegisters& registers,
|
||||
const std::array<std::uint8_t, 6>& instruction, std::uint64_t return_address, std::uint32_t thread) noexcept {
|
||||
auto expected = StartupInterventionPhase::frame_captured;
|
||||
if (!phase_.compare_exchange_strong(expected, StartupInterventionPhase::handling_exit, std::memory_order_acq_rel)) return false;
|
||||
const bool matches = base == supported_build::preferred_image_base && registers.rip == base + startup_exit_rva &&
|
||||
registers.rcx == std::numeric_limits<std::uint64_t>::max() && registers.rdx == 0x32 &&
|
||||
instruction == startup_exit_instruction && return_address == base + startup_exit_return_rva &&
|
||||
thread == frame_thread_ && registers.rbp == frame_base_ &&
|
||||
frame_base_ >= pins::exit_stack_offset && registers.rsp == frame_base_ - pins::exit_stack_offset;
|
||||
if (!matches) { fail(); return false; }
|
||||
registers.rax = FALSE;
|
||||
registers.rip = return_address;
|
||||
registers.rsp += sizeof(return_address);
|
||||
@@ -205,84 +211,60 @@ bool StartupInterventionMachine::return_from_exit(
|
||||
return true;
|
||||
}
|
||||
|
||||
void StartupInterventionMachine::fail() noexcept {
|
||||
phase_.store(StartupInterventionPhase::failed, std::memory_order_release);
|
||||
void StartupInterventionMachine::fail() noexcept { phase_.store(StartupInterventionPhase::failed, std::memory_order_release); }
|
||||
StartupInterventionPhase StartupInterventionMachine::phase() const noexcept { return phase_.load(std::memory_order_acquire); }
|
||||
|
||||
bool startup_intervention_fingerprints_match(std::uintptr_t base, std::uint32_t size,
|
||||
const std::array<std::uint8_t, 8>& frame, const std::array<std::uint8_t, 6>& exit) noexcept {
|
||||
return base == supported_build::preferred_image_base && size == startup_expected_image_size &&
|
||||
frame == startup_frame_fingerprint && exit == startup_exit_instruction;
|
||||
}
|
||||
|
||||
StartupInterventionPhase StartupInterventionMachine::phase() const noexcept {
|
||||
return phase_.load(std::memory_order_acquire);
|
||||
}
|
||||
|
||||
bool startup_intervention_fingerprints_match(
|
||||
std::uintptr_t image_base, std::uint32_t image_size,
|
||||
const std::array<std::uint8_t, 6>& exit_instruction) noexcept {
|
||||
return image_base == supported_build::preferred_image_base &&
|
||||
image_size == startup_expected_image_size &&
|
||||
exit_instruction == startup_exit_instruction;
|
||||
}
|
||||
|
||||
bool start_startup_interventions(std::uintptr_t image_base, std::string& error) noexcept {
|
||||
bool start_startup_interventions(std::uintptr_t base, std::string& error) noexcept {
|
||||
error.clear();
|
||||
try {
|
||||
if (handler_handle || installed_base.load())
|
||||
throw std::runtime_error("Startup interventions are already installed");
|
||||
if (handler_handle || installed_base.load()) throw std::runtime_error("Startup interventions are already installed");
|
||||
IMAGE_DOS_HEADER dos{};
|
||||
if (!read_local(image_base, dos) || dos.e_magic != IMAGE_DOS_SIGNATURE ||
|
||||
dos.e_lfanew <= 0 || dos.e_lfanew > 0x100000)
|
||||
if (!read_local(base, dos) || dos.e_magic != IMAGE_DOS_SIGNATURE || dos.e_lfanew <= 0 || dos.e_lfanew > 0x100000)
|
||||
throw std::runtime_error("Startup intervention image has an invalid DOS header");
|
||||
IMAGE_NT_HEADERS64 nt{};
|
||||
if (!read_local(image_base + static_cast<std::uintptr_t>(dos.e_lfanew), nt) ||
|
||||
nt.Signature != IMAGE_NT_SIGNATURE ||
|
||||
nt.FileHeader.Machine != IMAGE_FILE_MACHINE_AMD64 ||
|
||||
nt.OptionalHeader.Magic != IMAGE_NT_OPTIONAL_HDR64_MAGIC)
|
||||
if (!read_local(base + static_cast<std::uintptr_t>(dos.e_lfanew), nt) || nt.Signature != IMAGE_NT_SIGNATURE ||
|
||||
nt.FileHeader.Machine != IMAGE_FILE_MACHINE_AMD64 || nt.OptionalHeader.Magic != IMAGE_NT_OPTIONAL_HDR64_MAGIC)
|
||||
throw std::runtime_error("Startup intervention image has an invalid PE header");
|
||||
const auto frame_address = image_base + startup_frame_rva;
|
||||
const auto exit_address = image_base + startup_exit_rva;
|
||||
if (exit_address < image_base ||
|
||||
!executable_image_range(image_base, exit_address, startup_exit_instruction.size()))
|
||||
const auto frame_address = base + startup_frame_rva;
|
||||
const auto exit_address = base + startup_exit_rva;
|
||||
if (!executable_image_range(base, frame_address, startup_frame_fingerprint.size()) ||
|
||||
!executable_image_range(base, exit_address, startup_exit_instruction.size()))
|
||||
throw std::runtime_error("Startup intervention sites are outside executable image pages");
|
||||
std::array<std::uint8_t, 2> frame{};
|
||||
std::array<std::uint8_t, 8> frame{};
|
||||
std::array<std::uint8_t, 6> exit{};
|
||||
if (!read_local(exit_address, exit) ||
|
||||
!startup_intervention_fingerprints_match(image_base,
|
||||
nt.OptionalHeader.SizeOfImage, exit))
|
||||
if (!read_local(frame_address, frame) || !read_local(exit_address, exit) ||
|
||||
!startup_intervention_fingerprints_match(base, nt.OptionalHeader.SizeOfImage, frame, exit))
|
||||
throw std::runtime_error("Startup intervention image fingerprint does not match");
|
||||
const bool arm_frame = frame_address >= image_base &&
|
||||
executable_image_range(image_base, frame_address, startup_frame_instruction.size()) &&
|
||||
read_local(frame_address, frame) && frame == startup_frame_instruction;
|
||||
frame_armed.store(arm_frame, std::memory_order_release);
|
||||
|
||||
installed_base.store(image_base, std::memory_order_release);
|
||||
installed_base.store(base, std::memory_order_release);
|
||||
handler_handle = AddVectoredExceptionHandler(1, intervention_handler);
|
||||
if (!handler_handle) {
|
||||
installed_base.store(0, std::memory_order_release);
|
||||
throw std::runtime_error("Cannot install startup intervention exception handler");
|
||||
if (!handler_handle) { installed_base.store(0); throw std::runtime_error("Cannot install startup intervention exception handler"); }
|
||||
bool frame_attempted = false;
|
||||
if (!write_opcode(exit_address, breakpoint_opcode))
|
||||
error = "Cannot arm the guarded startup-exit intervention";
|
||||
if (error.empty()) {
|
||||
frame_attempted = true;
|
||||
if (!write_opcode(frame_address, breakpoint_opcode))
|
||||
error = "Cannot arm the guarded frame-capture intervention";
|
||||
}
|
||||
bool frame_patched = false;
|
||||
bool exit_patched = false;
|
||||
if (write_opcode(exit_address, breakpoint_opcode)) exit_patched = true;
|
||||
else error = "Cannot arm the guarded startup-exit intervention";
|
||||
if (error.empty() && arm_frame) {
|
||||
if (write_opcode(frame_address, breakpoint_opcode)) frame_patched = true;
|
||||
else error = "Cannot arm the guarded frame-preservation intervention";
|
||||
}
|
||||
if (error.empty() && !machine.arm())
|
||||
error = "Cannot arm the startup intervention state machine";
|
||||
if (error.empty() && !machine.arm()) error = "Cannot arm the startup intervention state machine";
|
||||
if (!error.empty()) {
|
||||
if (frame_patched) write_opcode(frame_address, startup_frame_instruction[0]);
|
||||
if (exit_patched) write_opcode(exit_address, startup_exit_instruction[0]);
|
||||
if (frame_attempted) write_opcode(frame_address, startup_frame_instruction[0]);
|
||||
write_opcode(exit_address, startup_exit_instruction[0]);
|
||||
RemoveVectoredExceptionHandler(handler_handle);
|
||||
handler_handle = nullptr;
|
||||
installed_base.store(0, std::memory_order_release);
|
||||
installed_base.store(0);
|
||||
machine.fail();
|
||||
return false;
|
||||
}
|
||||
logging::log(logging::Level::info, logging::Channel::runtime,
|
||||
"Startup interventions armed: frame {:#x}, exit {:#x}", startup_frame_rva, startup_exit_rva);
|
||||
return true;
|
||||
} catch (const std::exception& exception) {
|
||||
error = exception.what();
|
||||
machine.fail();
|
||||
return false;
|
||||
}
|
||||
} catch (const std::exception& exception) { error = exception.what(); machine.fail(); return false; }
|
||||
}
|
||||
|
||||
} // namespace dingosdk
|
||||
@@ -1,6 +1,5 @@
|
||||
#pragma once
|
||||
#include "Engine/Game/Build/image_identity.h"
|
||||
|
||||
#include "Engine/Game/Build/supported_build.h"
|
||||
#include "Engine/Game/Build/addresses.h"
|
||||
#include "Engine/Game/Build/20260929/startup_interventions.h"
|
||||
@@ -14,67 +13,61 @@ namespace dingosdk {
|
||||
|
||||
inline constexpr std::uint32_t startup_expected_image_size = supported_build::game_image_size;
|
||||
inline constexpr std::uintptr_t startup_frame_rva = addr::startup_interventions::frame;
|
||||
inline constexpr std::uintptr_t startup_frame_handler_rva = addr::startup_interventions::frame_handler;
|
||||
inline constexpr std::uintptr_t startup_frame_data_rva = addr::startup_interventions::frame_data;
|
||||
inline constexpr std::uintptr_t startup_frame_state_offset = 0x70;
|
||||
inline constexpr std::uintptr_t startup_frame_cursor_rva = addr::startup_interventions::frame_cursor;
|
||||
inline constexpr std::uintptr_t startup_frame_size = addr::startup_interventions::frame_size;
|
||||
inline constexpr std::uintptr_t startup_frame_cursor_offset = addr::startup_interventions::frame_cursor_offset;
|
||||
inline constexpr std::uintptr_t startup_exit_rva = addr::startup_interventions::exit_thunk;
|
||||
inline constexpr std::uintptr_t startup_exit_return_rva = addr::startup_interventions::exit_return;
|
||||
inline constexpr auto startup_frame_instruction = addr::startup_interventions::frame_instruction;
|
||||
inline constexpr auto startup_frame_fingerprint = addr::startup_interventions::frame_fingerprint;
|
||||
inline constexpr auto startup_exit_instruction = addr::startup_interventions::exit_instruction;
|
||||
|
||||
enum class StartupInterventionPhase : std::uint32_t {
|
||||
uninitialized,
|
||||
armed,
|
||||
handling_frame,
|
||||
frame_preserved,
|
||||
handling_exit,
|
||||
complete,
|
||||
failed,
|
||||
uninitialized, armed, handling_frame, frame_captured, handling_exit, complete, failed,
|
||||
};
|
||||
|
||||
struct StartupInterventionRegisters {
|
||||
std::uint64_t rip{};
|
||||
std::uint64_t rsp{};
|
||||
std::uint64_t rax{};
|
||||
std::uint64_t rbx{};
|
||||
std::uint64_t rcx{};
|
||||
std::uint64_t rdx{};
|
||||
std::uint64_t rbp{};
|
||||
std::uint64_t rsi{};
|
||||
std::uint64_t rdi{};
|
||||
std::uint64_t r8{};
|
||||
std::uint64_t r9{};
|
||||
std::uint64_t r14{};
|
||||
std::uint64_t rip{}, rsp{}, rax{}, rbx{}, rcx{}, rdx{}, rbp{}, rsi{}, rdi{};
|
||||
std::uint64_t r8{}, r9{}, r10{}, r11{}, r14{}, r15{};
|
||||
std::uint32_t eflags{};
|
||||
};
|
||||
|
||||
// Pure guarded state machine shared by the runtime exception handler and its
|
||||
// focused tests. A matching transition is accepted once; a guard mismatch at
|
||||
// an armed site permanently fails the sequence.
|
||||
struct StartupFrameMetadata {
|
||||
std::uint64_t cursor{};
|
||||
};
|
||||
|
||||
bool startup_frame_clear_matches(std::uintptr_t image_base,
|
||||
const StartupInterventionRegisters& registers,
|
||||
const std::array<std::uint8_t, 3>& instruction,
|
||||
const StartupFrameMetadata& metadata) noexcept;
|
||||
|
||||
// One-shot capture/exit sequence. The runtime serializes transitions and owns
|
||||
// the frame bytes; ordinary VM byte stores do not advance this state machine.
|
||||
class StartupInterventionMachine {
|
||||
public:
|
||||
bool arm() noexcept;
|
||||
bool preserve_frame(std::uintptr_t image_base, StartupInterventionRegisters& registers,
|
||||
const std::array<std::uint8_t, 2>& instruction,
|
||||
std::uint64_t protected_state) noexcept;
|
||||
bool capture_frame(std::uintptr_t image_base, const StartupInterventionRegisters& registers,
|
||||
const std::array<std::uint8_t, 3>& instruction,
|
||||
const StartupFrameMetadata& metadata, std::uint32_t thread_id) noexcept;
|
||||
bool return_from_exit(std::uintptr_t image_base, StartupInterventionRegisters& registers,
|
||||
const std::array<std::uint8_t, 6>& instruction,
|
||||
std::uint64_t return_address) noexcept;
|
||||
std::uint64_t return_address, std::uint32_t thread_id) noexcept;
|
||||
void fail() noexcept;
|
||||
StartupInterventionPhase phase() const noexcept;
|
||||
|
||||
private:
|
||||
std::atomic<StartupInterventionPhase> phase_{StartupInterventionPhase::uninitialized};
|
||||
std::uint64_t frame_base_{};
|
||||
std::uint32_t frame_thread_{};
|
||||
};
|
||||
|
||||
// The frame site is checked separately: it is optional (see start_startup_interventions).
|
||||
bool startup_intervention_fingerprints_match(
|
||||
std::uintptr_t image_base, std::uint32_t image_size,
|
||||
const std::array<std::uint8_t, 6>& exit_instruction) noexcept;
|
||||
bool startup_intervention_fingerprints_match(std::uintptr_t image_base, std::uint32_t image_size,
|
||||
const std::array<std::uint8_t, 8>& frame, const std::array<std::uint8_t, 6>& exit) noexcept;
|
||||
|
||||
// Installs two process-wide, one-shot breakpoint interventions for the exact
|
||||
// inspected Skate image. The caller keeps all existing game threads suspended
|
||||
// during installation; future worker threads are covered by the code sites.
|
||||
// Installs both verified sites while the caller holds existing game threads
|
||||
// suspended. Capture retires the byte-store breakpoint; exit restores the
|
||||
// captured native frame and retires the exit breakpoint before returning FALSE.
|
||||
bool start_startup_interventions(std::uintptr_t image_base, std::string& error) noexcept;
|
||||
|
||||
} // namespace dingosdk
|
||||
+52
-6
@@ -173,6 +173,25 @@ void apply_style() {
|
||||
}
|
||||
|
||||
|
||||
// True once `process` shows its startup splash (ReSkateStartupWindow) or, if the
|
||||
// splash is disabled, the game's own window.
|
||||
bool game_window_shown(DWORD process) {
|
||||
struct Search { DWORD process; bool found; } search{process, false};
|
||||
EnumWindows([](HWND window, LPARAM parameter) -> BOOL {
|
||||
auto& target = *reinterpret_cast<Search*>(parameter);
|
||||
DWORD owner{};
|
||||
GetWindowThreadProcessId(window, &owner);
|
||||
if (owner != target.process || !IsWindowVisible(window)) return TRUE;
|
||||
std::array<wchar_t, 64> name{};
|
||||
GetClassNameW(window, name.data(), static_cast<int>(name.size()));
|
||||
const std::wstring_view kind(name.data());
|
||||
if (kind == L"ReSkateStartupWindow" || kind == L"Skate") { target.found = true; return FALSE; }
|
||||
return TRUE;
|
||||
}, reinterpret_cast<LPARAM>(&search));
|
||||
return search.found;
|
||||
}
|
||||
|
||||
|
||||
} // namespace
|
||||
} // namespace detail
|
||||
|
||||
@@ -258,6 +277,9 @@ int run(const launcher_app::Session& session, const std::vector<std::wstring>& a
|
||||
auto& ui = *ui_storage;
|
||||
ModsPanel mods_panel;
|
||||
bool running = true;
|
||||
HANDLE game{};
|
||||
DWORD game_id{};
|
||||
bool hidden{};
|
||||
while (running) {
|
||||
MSG message;
|
||||
while (PeekMessageW(&message, nullptr, 0, 0, PM_REMOVE)) {
|
||||
@@ -270,12 +292,35 @@ int run(const launcher_app::Session& session, const std::vector<std::wstring>& a
|
||||
launcher.restart();
|
||||
break;
|
||||
}
|
||||
// The launcher is done the moment injection succeeded and Skate is running
|
||||
// on its own, so close immediately instead of staying beside the game. This
|
||||
// is the earliest safe point: a launch that fails never sets launched(), so
|
||||
// it still gets to report itself in the window.
|
||||
if (launcher.launched()) break;
|
||||
if (IsIconic(window)) { Sleep(50); continue; }
|
||||
// A successful launch closes immediately unless the user keeps the
|
||||
// launcher alive. In that mode it sleeps hidden while the game runs.
|
||||
if (launcher.launched() && !launcher.settings().keep_open_after_launch) break;
|
||||
if (!game && launcher.game()) {
|
||||
game_id = launcher.game();
|
||||
game = OpenProcess(SYNCHRONIZE, FALSE, game_id);
|
||||
}
|
||||
if (game) {
|
||||
if (!hidden && game_window_shown(game_id)) {
|
||||
ShowWindow(window, SW_HIDE);
|
||||
hidden = true;
|
||||
}
|
||||
if (hidden && !launcher.busy() && launcher.snapshot().phase == Phase::failed) {
|
||||
ShowWindow(window, SW_SHOWNORMAL);
|
||||
SetForegroundWindow(window);
|
||||
hidden = false;
|
||||
}
|
||||
if (WaitForSingleObject(game, 0) == WAIT_OBJECT_0) {
|
||||
CloseHandle(game);
|
||||
game = nullptr;
|
||||
launcher.game_exited(hidden);
|
||||
if (hidden) {
|
||||
ShowWindow(window, SW_SHOWNORMAL);
|
||||
SetForegroundWindow(window);
|
||||
hidden = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (hidden || IsIconic(window)) { Sleep(hidden ? 100 : 50); continue; }
|
||||
ImGui_ImplDX12_NewFrame();
|
||||
ImGui_ImplWin32_NewFrame();
|
||||
ImGui::NewFrame();
|
||||
@@ -284,6 +329,7 @@ int run(const launcher_app::Session& session, const std::vector<std::wstring>& a
|
||||
renderer.render();
|
||||
}
|
||||
ShowWindow(window, SW_HIDE);
|
||||
if (game) CloseHandle(game);
|
||||
launcher.cancel();
|
||||
// Closing the window with Settings still open must not lose changes.
|
||||
try { launcher.save(); } catch (...) {}
|
||||
|
||||
+2
-1
@@ -7,7 +7,8 @@
|
||||
|
||||
namespace dingosdk::launcher_gui {
|
||||
|
||||
// Runs the windowed launcher until the user closes it or Skate starts.
|
||||
// Runs the launcher until it is closed. The saved setting can retain it hidden
|
||||
// while Skate runs and show it again after the game exits.
|
||||
// `arguments` are the launcher's own command-line arguments, kept for a
|
||||
// restart after a self-update.
|
||||
int run(const launcher_app::Session& session, const std::vector<std::wstring>& arguments);
|
||||
|
||||
@@ -122,6 +122,7 @@ struct Settings {
|
||||
int console_key{static_cast<int>(launcher::default_console_key)};
|
||||
int log_level{2};
|
||||
std::string arguments;
|
||||
bool keep_open_after_launch{};
|
||||
// Off: never replace ReSkate.dll or the launcher (keeps a test build someone handed out).
|
||||
bool updates{true};
|
||||
// Off: a crash uploads nothing (RESKATE_CRASH_REPORTING=0 for the launcher and the game).
|
||||
@@ -159,9 +160,10 @@ public:
|
||||
bool restart_requested() const { return restart_; }
|
||||
// Process id of the Skate this launcher started, once it exists.
|
||||
DWORD game() const { return game_; }
|
||||
// Injection finished and the game is running on its own; the launcher closes
|
||||
// as soon as this is true.
|
||||
// Injection finished and the game is running on its own.
|
||||
bool launched() const { return launched_; }
|
||||
// Resets the launch state after the retained game process exits.
|
||||
void game_exited(bool seen);
|
||||
Settings& settings() { return settings_; }
|
||||
const launcher_app::Session& session() const { return session_; }
|
||||
void save();
|
||||
|
||||
@@ -56,6 +56,8 @@ Settings load_settings(const fs::path& path) {
|
||||
for (std::size_t index = 0; index < log_levels.size(); ++index)
|
||||
if (level == log_levels[index]) settings.log_level = static_cast<int>(index);
|
||||
settings.arguments = json.value("arguments", std::string());
|
||||
// Keep the original settings key so existing launcher preferences survive.
|
||||
settings.keep_open_after_launch = !json.value("close_on_launch", !settings.keep_open_after_launch);
|
||||
settings.updates = json.value("updates", settings.updates);
|
||||
settings.crash_reports = json.value("crash_reports", settings.crash_reports);
|
||||
settings.steam_username = json.value("steam_username", std::string());
|
||||
@@ -79,6 +81,7 @@ void save_settings(const fs::path& path, const Settings& settings) {
|
||||
json["console_key"] = settings.console_key;
|
||||
json["log_level"] = log_levels[static_cast<std::size_t>(settings.log_level)];
|
||||
json["arguments"] = settings.arguments;
|
||||
json["close_on_launch"] = !settings.keep_open_after_launch;
|
||||
json["updates"] = settings.updates;
|
||||
json["crash_reports"] = settings.crash_reports;
|
||||
json["steam_username"] = settings.steam_username;
|
||||
@@ -131,6 +134,14 @@ State Launcher::snapshot() {
|
||||
return state_;
|
||||
}
|
||||
|
||||
void Launcher::game_exited(bool seen) {
|
||||
game_ = 0;
|
||||
launched_ = false;
|
||||
if (busy_ || snapshot().phase == Phase::failed) return;
|
||||
if (seen) check();
|
||||
else fail("Skate closed while it was starting.");
|
||||
}
|
||||
|
||||
void Launcher::save() {
|
||||
save_settings(settings_path(session_), settings_);
|
||||
// The game inherits the launcher's environment, so this reaches the next launch; the
|
||||
@@ -390,6 +401,7 @@ void Launcher::run_play() {
|
||||
launched_ = true;
|
||||
set(Phase::launching, "Skate is starting", options.offline || !launcher_app::steam_signed_in() ?
|
||||
"Offline mode: playing without Steam as Unknown Player; multiplayer is hidden." :
|
||||
settings_.keep_open_after_launch ? "The launcher hides while you play and returns when Skate closes." :
|
||||
"The launcher closes now; Skate keeps starting on its own.");
|
||||
}
|
||||
|
||||
|
||||
@@ -183,6 +183,9 @@ void settings_window(Launcher& launcher, const Fonts& fonts, ImVec2 size, Ui& ui
|
||||
break;
|
||||
default:
|
||||
section_caption(fonts, "LAUNCHER");
|
||||
setting_check("Keep the launcher open after launch",
|
||||
"Hide while you play and return when Skate closes. Turn off to close after a successful launch.",
|
||||
settings.keep_open_after_launch);
|
||||
setting_check("Install ReSkate updates",
|
||||
"Turn off to keep the ReSkate.dll and launcher you have, such as a test build someone gave you. "
|
||||
"Skate's own Steam updates are not affected.",
|
||||
|
||||
Reference in new issue
Block a user