From af6cf372122604a11d27fd33a10c0aa754e724a2 Mon Sep 17 00:00:00 2001 From: nexusverypro Date: Sun, 5 Jul 2026 19:31:46 +0100 Subject: [PATCH] fix: add support for "E9 rel32" for shorter method lengths --- include/radhook/api.h | 8 +- include/radhook/private/disassemble.h | 31 +++++--- src/radhook.cpp | 104 +++++++++++++++++++++----- src/x64disasm.cpp | 38 ++++++++-- 4 files changed, 144 insertions(+), 37 deletions(-) diff --git a/include/radhook/api.h b/include/radhook/api.h index 9051f64..8d82b32 100644 --- a/include/radhook/api.h +++ b/include/radhook/api.h @@ -13,7 +13,7 @@ // versioning #define RADHOOK_VERSION_MAJOR 1 #define RADHOOK_VERSION_MINOR 1 -#define RADHOOK_VERSION_PATCH 0 +#define RADHOOK_VERSION_PATCH 1 #if _MSC_VER # include // for source code annotations @@ -41,8 +41,12 @@ enum class RadHookResult : int { InvalidDetour, InvalidHandle, TrampolineTooFar, - TargetTooSmall, UnknownTarget, + BadDecode, + RelativeControlFlow, + TooManyRipFixups, + ExceedsMaxStolen, + InsufficientLength, Unknown, }; diff --git a/include/radhook/private/disassemble.h b/include/radhook/private/disassemble.h index 3c6e7a1..170e50a 100644 --- a/include/radhook/private/disassemble.h +++ b/include/radhook/private/disassemble.h @@ -15,13 +15,16 @@ #if defined(RADHOOK_ARCH_X64) struct X64Insn { - size_t length = 0; - bool ripRelative = false; - size_t dispOffset = 0; - bool relativeControlFlow = false; - bool isReturn = false; - bool isPadding = false; - bool valid = false; + size_t length = 0; + bool ripRelative = false; + size_t dispOffset = 0; + bool relativeControlFlow = false; + bool isReturn = false; + bool isPadding = false; + bool branchDisp = false; + size_t branchDispOffset = 0; + uint8_t branchDispSize = 0; + bool valid = false; }; X64Insn @@ -30,9 +33,11 @@ DecodeX64( size_t avail ); -constexpr size_t kX64StubLen = 14; // FF25 00000000 + imm64 +constexpr size_t kX64StubLen = 14; // FF25 00000000 + imm64 +constexpr size_t kX64StubLenShort = 5; // E9 rel32 constexpr size_t kX64MaxStolen = 32; constexpr size_t kX64MaxRipFixups = 8; +constexpr size_t kX64MaxBranchFixups = 8; enum class StolenBytesError { None, @@ -43,6 +48,12 @@ enum class StolenBytesError { InsufficientLength, }; +struct BranchFixup { + size_t offset; + size_t dispOffset; + uint8_t dispSize; +}; + StolenBytesError BuildStolenBytesX64( const uint8_t* target, @@ -50,7 +61,9 @@ BuildStolenBytesX64( uint8_t* outBytes, size_t& outLen, size_t* outRipOffsets, - size_t& outRipCount + size_t& outRipCount, + BranchFixup* outBranches, + size_t& outBranchCount ); #endif // defined(RADHOOK_ARCH_X64) diff --git a/src/radhook.cpp b/src/radhook.cpp index 60ed5db..23548e8 100644 --- a/src/radhook.cpp +++ b/src/radhook.cpp @@ -40,8 +40,12 @@ const char* RadHookResultToString(RadHookResult result) { case RadHookResult::InvalidDetour: return "InvalidDetour"; case RadHookResult::InvalidHandle: return "InvalidHandle"; case RadHookResult::TrampolineTooFar: return "TrampolineTooFar"; - case RadHookResult::TargetTooSmall: return "TargetTooSmall"; case RadHookResult::UnknownTarget: return "UnknownTarget"; + case RadHookResult::BadDecode: return "BadDecode"; + case RadHookResult::RelativeControlFlow:return "RelativeControlFlow"; + case RadHookResult::TooManyRipFixups: return "TooManyRipFixups"; + case RadHookResult::ExceedsMaxStolen: return "ExceedsMaxStolen"; + case RadHookResult::InsufficientLength: return "InsufficientLength"; default: return "Unknown"; } } @@ -130,28 +134,41 @@ RadHookResult InstallHook(RadHookOpaque* h, void* target, void* detour) { if (!detour) return RadHookResult::InvalidDetour; #if defined(RADHOOK_ARCH_X64) + auto fitsRel32 = [](intptr_t delta) { + return delta >= INT32_MIN && delta <= INT32_MAX; + }; + + uintptr_t targetAddr = reinterpret_cast(target); + uintptr_t detourAddr = reinterpret_cast(detour); + bool hookStubShort = fitsRel32(static_cast(detourAddr) - static_cast(targetAddr + kX64StubLenShort)); + size_t hookStubLen = hookStubShort ? kX64StubLenShort : kX64StubLen; + uint8_t stolen[kX64MaxStolen]; size_t stolenLen = 0; size_t ripOffsets[kX64MaxRipFixups]; size_t ripCount = 0; + BranchFixup branches[kX64MaxBranchFixups]; + size_t branchCount = 0; StolenBytesError sberr = BuildStolenBytesX64( - reinterpret_cast(target), kX64StubLen, - stolen, stolenLen, ripOffsets, ripCount); + reinterpret_cast(target), hookStubLen, + stolen, stolenLen, ripOffsets, ripCount, branches, branchCount); if (sberr != StolenBytesError::None) { - return (sberr == StolenBytesError::InsufficientLength) - ? RadHookResult::TargetTooSmall - : RadHookResult::DisassemblyFailed; + return static_cast( + static_cast(RadHookResult::UnknownTarget) + static_cast(sberr) + ); } + // trampoline = stolen bytes + jump back to target size_t allocSize = stolenLen + kX64StubLen; void* trampolineAlloc = RadAllocExecNear(target, allocSize); - if (!trampolineAlloc) trampolineAlloc = RadAllocExec(allocSize); // best-effort fallback + if (!trampolineAlloc) trampolineAlloc = RadAllocExec(allocSize); if (!trampolineAlloc) return RadHookResult::MemoryAllocFailed; std::memcpy(trampolineAlloc, stolen, stolenLen); + // rewrite rip-relative operands for (size_t k = 0; k < ripCount; ++k) { size_t off = ripOffsets[k]; int32_t originalDisp = 0; @@ -166,18 +183,60 @@ RadHookResult InstallHook(RadHookOpaque* h, void* target, void* detour) { std::memcpy(reinterpret_cast(trampolineAlloc) + off, &newDisp32, 4); } + // rewrite relative branch displacements + for (size_t k = 0; k < branchCount; ++k) { + size_t insnOff = branches[k].offset; + size_t dispOff = branches[k].dispOffset; + uint8_t dispSize = branches[k].dispSize; + + int32_t originalDisp = 0; + std::memcpy(&originalDisp, reinterpret_cast(target) + dispOff, dispSize); + if (dispSize == 1) originalDisp = static_cast(originalDisp); // sign-extend rel8 + + size_t insnLenAfterDisp = dispOff - insnOff + dispSize; + uintptr_t absTarget = reinterpret_cast(target) + insnOff + insnLenAfterDisp + static_cast(originalDisp); + + intptr_t newDisp = static_cast(absTarget) - + (reinterpret_cast(trampolineAlloc) + dispOff + dispSize); + + if (dispSize == 1) { + // rel8 + if (newDisp < INT8_MIN || newDisp > INT8_MAX) { + RadFreeExec(trampolineAlloc, allocSize); + return RadHookResult::TrampolineTooFar; + } + int8_t d8 = static_cast(newDisp); + std::memcpy(reinterpret_cast(trampolineAlloc) + dispOff, &d8, 1); + } else { + if (newDisp < INT32_MIN || newDisp > INT32_MAX) { + RadFreeExec(trampolineAlloc, allocSize); + return RadHookResult::TrampolineTooFar; + } + int32_t d32 = static_cast(newDisp); + std::memcpy(reinterpret_cast(trampolineAlloc) + dispOff, &d32, 4); + } + } + uint8_t* jumpBack = reinterpret_cast(trampolineAlloc) + stolenLen; uintptr_t backAddr = reinterpret_cast(target) + stolenLen; - { - jumpBack[0] = 0xFF; + intptr_t jumpBackDeltaShort = static_cast(backAddr) - + (reinterpret_cast(jumpBack) + kX64StubLenShort); + + if (fitsRel32(jumpBackDeltaShort)) { + jumpBack[0] = 0xE9; + int32_t d32 = static_cast(jumpBackDeltaShort); + std::memcpy(jumpBack + 1, &d32, 4); + } else { + jumpBack[0] = 0xFF; jumpBack[1] = 0x25; - jumpBack[2] = 0; jumpBack[3] = 0; + jumpBack[2] = 0; jumpBack[3] = 0; jumpBack[4] = 0; jumpBack[5] = 0; + std::memcpy(jumpBack + 6, &backAddr, 8); } - std::memcpy(jumpBack + 6, &backAddr, 8); RadFlushICache(trampolineAlloc, allocSize); + // patch target in place with a jump to the detour unsigned long oldProtect = 0; if (!RadMakeWritableExecutable(target, stolenLen, &oldProtect)) { RadFreeExec(trampolineAlloc, allocSize); @@ -188,19 +247,22 @@ RadHookResult InstallHook(RadHookOpaque* h, void* target, void* detour) { h->originalLength = stolenLen; uint8_t hookStub[kX64StubLen]; - { - hookStub[0] = 0xFF; + if (hookStubShort) { + hookStub[0] = 0xE9; + int32_t d32 = static_cast(detourAddr - (targetAddr + kX64StubLenShort)); + std::memcpy(hookStub + 1, &d32, 4); + } else { + hookStub[0] = 0xFF; hookStub[1] = 0x25; - hookStub[2] = 0; hookStub[3] = 0; + hookStub[2] = 0; hookStub[3] = 0; hookStub[4] = 0; hookStub[5] = 0; + std::memcpy(hookStub + 6, &detourAddr, 8); } - uintptr_t detourAddr = reinterpret_cast(detour); - std::memcpy(hookStub + 6, &detourAddr, 8); - std::memcpy(h->stubBytes, hookStub, kX64StubLen); - h->stubLength = kX64StubLen; + std::memcpy(h->stubBytes, hookStub, hookStubLen); + h->stubLength = hookStubLen; - std::memcpy(target, hookStub, kX64StubLen); + std::memcpy(target, hookStub, hookStubLen); RadRestoreMemoryProtection(target, stolenLen, oldProtect); RadFlushICache(target, stolenLen); @@ -213,6 +275,8 @@ RadHookResult InstallHook(RadHookOpaque* h, void* target, void* detour) { return RadHookResult::Success; #elif defined(RADHOOK_ARCH_ARM64) + // bail if any of the first N instructions use pc-relative addressing + // we dont fix these up on arm64 yet const uint32_t* src = reinterpret_cast(target); for (size_t k = 0; k < kArm64StubLen / 4; ++k) { if (IsArm64PcRelative(src[k])) { @@ -220,6 +284,7 @@ RadHookResult InstallHook(RadHookOpaque* h, void* target, void* detour) { } } + // trampoline = original instructions + absolute branch back to target size_t allocSize = kArm64StubLen + kArm64StubLen; void* trampolineAlloc = RadAllocExec(allocSize); if (!trampolineAlloc) return RadHookResult::MemoryAllocFailed; @@ -230,6 +295,7 @@ RadHookResult InstallHook(RadHookOpaque* h, void* target, void* detour) { RadFlushICache(trampolineAlloc, allocSize); + // patch target in place with an absolute branch to the detour unsigned long oldProtect = 0; if (!RadMakeWritableExecutable(target, kArm64StubLen, &oldProtect)) { RadFreeExec(trampolineAlloc, allocSize); diff --git a/src/x64disasm.cpp b/src/x64disasm.cpp index e9abb23..e332528 100644 --- a/src/x64disasm.cpp +++ b/src/x64disasm.cpp @@ -57,7 +57,10 @@ X64Insn DecodeX64(const uint8_t* p, size_t avail) { else if (opcode == 0x6A) { immSize = 1; } else if (opcode == 0x69) { hasModRM = true; immSize = opSize16 ? 2 : 4; } else if (opcode == 0x6B) { hasModRM = true; immSize = 1; } - else if (opcode >= 0x70 && opcode <= 0x7F) { immSize = 1; relBranch = true; } + else if (opcode >= 0x70 && opcode <= 0x7F) { + immSize = 1; relBranch = true; + insn.branchDisp = true; insn.branchDispSize = 1; + } else if (opcode == 0x80) { hasModRM = true; immSize = 1; } else if (opcode == 0x81) { hasModRM = true; immSize = opSize16 ? 2 : 4; } else if (opcode == 0x83) { hasModRM = true; immSize = 1; } @@ -77,9 +80,18 @@ X64Insn DecodeX64(const uint8_t* p, size_t avail) { else if (opcode == 0xCC) { /* int3 */ insn.isPadding = true; } else if (opcode == 0xCD) { immSize = 1; } else if (opcode == 0xD0 || opcode == 0xD1 || opcode == 0xD2 || opcode == 0xD3) { hasModRM = true; } - else if (opcode == 0xE8) { immSize = 4; relBranch = true; } - else if (opcode == 0xE9) { immSize = 4; relBranch = true; } - else if (opcode == 0xEB) { immSize = 1; relBranch = true; } + else if (opcode == 0xE8) { + immSize = 4; relBranch = true; + insn.branchDisp = true; insn.branchDispSize = 4; + } + else if (opcode == 0xE9) { + immSize = 4; relBranch = true; + insn.branchDisp = true; insn.branchDispSize = 4; + } + else if (opcode == 0xEB) { + immSize = 1; relBranch = true; + insn.branchDisp = true; insn.branchDispSize = 1; + } else if (opcode == 0xF6) { hasModRM = true; immSize = 1; } else if (opcode == 0xF7) { hasModRM = true; immSize = opSize16 ? 2 : 4; } else if (opcode == 0xFE || opcode == 0xFF) { hasModRM = true; } @@ -87,7 +99,10 @@ X64Insn DecodeX64(const uint8_t* p, size_t avail) { return insn; // unrecognized opcode } } else { - if (opcode >= 0x80 && opcode <= 0x8F) { immSize = 4; relBranch = true; } + if (opcode >= 0x80 && opcode <= 0x8F) { + immSize = 4; relBranch = true; + insn.branchDisp = true; insn.branchDispSize = 4; + } else if (opcode == 0x1E) { immSize = 1; } else if (opcode == 0x1F) { hasModRM = true; } else if (opcode == 0x05 || opcode == 0x31 || opcode == 0x34 || opcode == 0x35 || opcode == 0xA2) { /* syscall/rdtsc/cpuid etc */ } @@ -129,6 +144,8 @@ X64Insn DecodeX64(const uint8_t* p, size_t avail) { } } + if (insn.branchDisp) insn.branchDispOffset = i; // i is at the start of the displacement bytes + i += static_cast(immSize); if (i > avail) return insn; @@ -140,19 +157,26 @@ X64Insn DecodeX64(const uint8_t* p, size_t avail) { StolenBytesError BuildStolenBytesX64(const uint8_t* target, size_t minLen, uint8_t* outBytes, size_t& outLen, - size_t* outRipOffsets, size_t& outRipCount) { + size_t* outRipOffsets, size_t& outRipCount, + BranchFixup* outBranches, size_t& outBranchCount) { size_t len = 0; outRipCount = 0; + outBranchCount = 0; while (len < minLen) { if (len >= kX64MaxStolen) return StolenBytesError::ExceedsMaxStolen; X64Insn insn = DecodeX64(target + len, kX64MaxStolen - len); if (!insn.valid) return StolenBytesError::BadDecode; if (insn.isReturn || insn.isPadding) return StolenBytesError::InsufficientLength; - if (insn.relativeControlFlow) return StolenBytesError::RelativeControlFlow; + if (insn.ripRelative) { if (outRipCount >= kX64MaxRipFixups) return StolenBytesError::TooManyRipFixups; outRipOffsets[outRipCount++] = len + insn.dispOffset; } + if (insn.branchDisp) { + if (outBranchCount >= kX64MaxBranchFixups) return StolenBytesError::TooManyRipFixups; + outBranches[outBranchCount++] = { len, len + insn.branchDispOffset, insn.branchDispSize }; + } + len += insn.length; } std::memcpy(outBytes, target, len);