diff --git a/docs/dev/design/assembly-hooks/overview.md b/docs/dev/design/assembly-hooks/overview.md index 96b49dd..bbef021 100644 --- a/docs/dev/design/assembly-hooks/overview.md +++ b/docs/dev/design/assembly-hooks/overview.md @@ -119,13 +119,13 @@ The following table below shows common hook lengths, for: - [Targeted Memory Allocation (TMA)](../../platform/overview.md#recommended-targeted-memory-allocation) (expected best case) when above `Relative Jump` range. - Worst case scenario. -| Architecture | Relative | TMA | Worst Case | -|----------------|---------------------|--------------|-----------------| -| x86^[1]^ | 5 bytes (+- 2GiB) | 5 bytes | 5 bytes | -| x86_64 | 5 bytes (+- 2GiB) | 6 bytes^[2]^ | 13 bytes^[3]^ | -| x86_64 (macOS) | 5 bytes (+- 2GiB) | 13 bytes^[4]^| 13 bytes^[3]^ | -| ARM64 | 4 bytes (+- 128MiB) | 12 bytes^[6]^| 20 bytes^[5]^ | -| ARM64 (macOS) | 4 bytes (+- 128MiB) | 12 bytes^[6]^| 20 bytes^[5]^ | +| Architecture | Relative | TMA | Worst Case | +| -------------- | ------------------- | ------------- | ------------- | +| x86^[1]^ | 5 bytes (+- 2GiB) | 5 bytes | 5 bytes | +| x86_64 | 5 bytes (+- 2GiB) | 6 bytes^[2]^ | 13 bytes^[3]^ | +| x86_64 (macOS) | 5 bytes (+- 2GiB) | 13 bytes^[4]^ | 13 bytes^[3]^ | +| ARM64 | 4 bytes (+- 128MiB) | 12 bytes^[6]^ | 20 bytes^[5]^ | +| ARM64 (macOS) | 4 bytes (+- 128MiB) | 12 bytes^[6]^ | 20 bytes^[5]^ | ^[1]^: x86 can reach any address from any address with relative branch due to integer overflow/wraparound. ^[2]^: [`jmp [
]`, with <Address> at < 2GiB](../../arch/operations.md#jumpabsoluteindirect). @@ -136,6 +136,8 @@ The following table below shows common hook lengths, for: ## Thread Safety & Memory Layout +!!! info "Extra: [Thread Safety on x86](../common.md#thread-safety-on-x86)" + In order to support thread safety, while retaining maximum runtime performance, the buffers where the original and hook code are contained have a very specific memory layout (shown below) @@ -187,69 +189,7 @@ hook: ; Backup (Hook) !!! info "When transitioning between Enabled/Disabled state, we place a temporary branch at `entry`, this allows us to manipulate the remaining code safely." -```asm -entry: ; Currently Applied (Hook) - b original ; Temp branch to original - mov x0, x2 - b back_to_code - -original: ; Backup (Original) - mov x0, x1 - add x0, x2 - b back_to_code - -hook: ; Backup (Hook) - add x1, x1 - mov x0, x2 - b back_to_code -``` - -!!! note "Don't forget to clear instruction cache on non-x86 architectures which need it." - -This ensures we can safely overwrite the remaining code... - -Then we overwrite `entry` code with `hook` code, except the branch: - -```asm -entry: ; Currently Applied (Hook) - b original ; Branch to original - add x0, x2 ; overwritten with 'original' code. - b back_to_code ; overwritten with 'original' code. - -original: ; Backup (Original) - mov x0, x1 - add x0, x2 - b back_to_code - -hook: ; Backup (Hook) - add x1, x1 - mov x0, x2 - b back_to_code -``` - -And lastly, overwrite the branch. - -To do this, read the original `sizeof(nint)` bytes at `entry`, replace branch bytes with original bytes -and do an atomic write. This way, the remaining instruction is safely replaced. - -```asm -entry: ; Currently Applied (Hook) - add x1, x1 ; 'original' code. - add x0, x2 ; 'original' code. - b back_to_code ; 'original' code. - -original: ; Backup (Original) - mov x0, x1 - add x0, x2 - b back_to_code - -hook: ; Backup (Hook) - add x1, x1 - mov x0, x2 - b back_to_code -``` - -This way we achieve zero overhead CPU-wise, at expense of some memory. +!!! info "Read [Thread Safe Enable/Disable of Hooks](../common.md#thread-safe-enabledisable-of-hooks) for more info." ## Legacy Compatibility Considerations @@ -266,4 +206,14 @@ This means a few functionalities must be supported here: - Supporting Assembly via FASM. - As this is only possible in Windows (FASM can't be recompiled on other OSes as library), this feature will be getting dropped. - - The `Reloaded.Hooks` wrapper will continue to ship FASM for backwards compatibility, however mods are expected to migrate to the new library in the future. \ No newline at end of file + - The `Reloaded.Hooks` wrapper will continue to ship FASM for backwards compatibility, however mods are expected to migrate to the new library in the future. + +## Limits + +Assembly hook info is packed by default to save on memory space. By default, the following limits apply: + +| Property | 4 Byte Instruction (e.g. ARM) | x86 | Unknown | +| -------------------- | ----------------------------- | ------ | ------- | +| Max Branch Length | 4 | 5 | 8 | +| Max Orig Code Length | 16KiB | 4KiB | 128MiB | +| Max Hook Code Length | 2MiB | 128KiB | 1GiB | diff --git a/docs/dev/design/branch-hooks/overview.md b/docs/dev/design/branch-hooks/overview.md index 76b34a9..365422b 100644 --- a/docs/dev/design/branch-hooks/overview.md +++ b/docs/dev/design/branch-hooks/overview.md @@ -45,7 +45,7 @@ Notably it differs in the following ways: ```mermaid flowchart TD CF[Caller Function] - RW[ReverseWrapper] + RW[Stub] HK["<Your Function>"] OM[Original Method] @@ -65,7 +65,7 @@ flowchart TD HK["<Your Function>"] OM[Original Method] - CF -- "call <> instead of original" --> HK + CF -- "call 'Your Function' instead of original" --> HK HK -. "Calls <Optionally>" .-> OM OM -. "Returns" .-> HK ``` @@ -74,6 +74,24 @@ This option allows for a small performance improvement, saving 1 instruction and This is on by default (can be disabled), and will take into effect when no conversion between calling conventions is needed and target is within 'Relative Jump' range for your CPU architecture. +### When Activated (with Calling Convention Conversion) + +```mermaid +flowchart TD + CF[Caller Function] + RW[ReverseWrapper] + HK["<Your Function>"] + W[Wrapper] + OM[Original Method] + + CF -- "call wrapper" --> RW + RW -- jump to your code --> HK + HK -. "Calls <Optionally>" .-> W + W -- "call original (wrapped)" --> OM + OM -. "Returns" .-> W + W -. "Returns" .-> HK +``` + ### When Deactivated ```mermaid @@ -92,39 +110,53 @@ When the hook is deactivated, the stub is replaced with a direct jump back to th By bypassing your code entirely, it is safe for your dynamic library (`.dll`/`.so`/`.dylib`) to unload from the process. -## Example +## Thread Safety & Memory Layout + +!!! info "Extra: [Thread Safety on x86](../common.md#thread-safety-on-x86)" + +Emplacing the jump to the stub and patching within the stub are atomic operations on all supported platforms. + +The 'branch hook' stub uses the following memory layout: + +```text +- ( [ReverseWrapper] OR [Branch to Custom Function] ) OR [Branch to Original Function] +- Branch to Original Function +- [Wrapper] (If Calling Convention Conversion is needed) +``` + +!!! tip "The library is optimised to not use redundant memory" -### Before + For example, in x86 (32-bit), a `jmp` instruction can reach any address from any address. In that situation, + we don't write `Branch to Original Function` to the buffer at all, provided a `ReverseWrapper` is not needed, + as it is not necessary. + +### Examples + +!!! info "Using x86 Assembly." + +#### Before ```asm -; x86 Assembly originalCaller: ; Some code... call originalFunction ; More code... - -originalFunction: - ; Function implementation... ``` -### After (Fast Mode) +#### After (Fast Mode) ```asm -; x86 Assembly originalCaller: ; Some code... - call newFunction + call userFunction ; To user method ; More code... -newFunction: +userFunction: ; New function implementation... call originalFunction ; Optional. - -originalFunction: - ; Original function implementation... ``` -### After +#### After ```asm ; x86 Assembly @@ -134,46 +166,73 @@ originalCaller: ; More code... stub: + ; == BranchToCustom == jmp newFunction - ; nop padding to 8 bytes (if needed) + ; == BranchToCustom == + + ; == BranchToOriginal == + jmp originalFunction + ; == BranchToOriginal == newFunction: ; New function implementation... call originalFunction ; Optional. +``` -originalFunction: - ; Original function implementation... +#### After (with Calling Convention Conversion) + +```asm +; x86 Assembly +originalCaller: + ; Some code... + call stub + ; More code... + +stub: + ; == ReverseWrapper == + ; implementation.. + call userFunction + ; ..implementation + ; == ReverseWrapper == + + ; == Wrapper == + ; implementation .. + jmp originalFunction + ; .. implementation + ; == Wrapper == + + ; == BranchToOriginal == + jmp originalFunction ; Whenever disabled :wink: + ; == BranchToOriginal == + +userFunction: + ; New function implementation... + call wrapper; (See Above) ``` -### After (with Calling Convention Conversion) +#### After (Disabled) ```asm ; x86 Assembly originalCaller: ; Some code... - call wrapper + call stub ; More code... -wrapper: - ; call convention conversion implementation - call newFunction - ; call convention conversion implementation - ret +stub: + ; We disable the hook by branching to instruction that branches to original + jmp originalFunction ; Whenever disabled :wink: newFunction: ; New function implementation... - call reverseWrapper ; Optional. - -reverseWrapper: - ; call convention conversion implementation - call originalFunction - ; call convention conversion implementation - ret + call originalFunction ; Optional. originalFunction: ; Original function implementation... ``` -## Thread Safety & Memory Layout +### Switching State + +!!! info "When transitioning between Enabled/Disabled state, we place a temporary branch at `stub`, this allows us to manipulate the remaining code safely." -Emplacing the jump to the stub and patching within the stub are atomic operations on all supported platforms. \ No newline at end of file +!!! info "Read [Thread Safe Enable/Disable of Hooks](../common.md#thread-safe-enabledisable-of-hooks) for more info." diff --git a/docs/dev/design/common.md b/docs/dev/design/common.md new file mode 100644 index 0000000..a0b53ad --- /dev/null +++ b/docs/dev/design/common.md @@ -0,0 +1,343 @@ +# Common Design Notes + +!!! info "Design notes common to all hooking strategies." + +## Thread Safe Enable/Disable of Hooks + +!!! info "Using ARM64 [Assembly Hook](./assembly-hooks/overview.md) as an example." + +!!! info "When transitioning between Enabled/Disabled state, we place a temporary branch at `entry`, this allows us to manipulate the remaining code safely." + +We start the 'disable' process with: + +```asm +entry: ; Currently Applied (Hook) + b original ; Temp branch to original + mov x0, x2 + b back_to_code + +original: ; Backup (Original) + mov x0, x1 + add x0, x2 + b back_to_code + +hook: ; Backup (Hook) + add x1, x1 + mov x0, x2 + b back_to_code +``` + +!!! note "Don't forget to clear instruction cache on non-x86 architectures which need it." + +This ensures we can safely overwrite the remaining code... + +Then we overwrite `entry` code with `hook` code, except the branch: + +```asm +entry: ; Currently Applied (Hook) + b original ; Branch to original + add x0, x2 ; overwritten with 'original' code. + b back_to_code ; overwritten with 'original' code. + +original: ; Backup (Original) + mov x0, x1 + add x0, x2 + b back_to_code + +hook: ; Backup (Hook) + add x1, x1 + mov x0, x2 + b back_to_code +``` + +And lastly, overwrite the branch. + +To do this, read the original `sizeof(nint)` bytes at `entry`, replace branch bytes with original bytes +and do an atomic write. This way, the remaining instruction is safely replaced. + +```asm +entry: ; Currently Applied (Hook) + add x1, x1 ; 'original' code. + add x0, x2 ; 'original' code. + b back_to_code ; 'original' code. + +original: ; Backup (Original) + mov x0, x1 + add x0, x2 + b back_to_code + +hook: ; Backup (Hook) + add x1, x1 + mov x0, x2 + b back_to_code +``` + +This way we achieve zero overhead CPU-wise, at expense of some memory. + +## Hook Length Mismatch Problem + +!!! info "When a hook is already present, and you wish to stack that hook over the existing hook, certain problems might arise." + +### When your hook is shorter than original. + +!!! tip "This is notably an issue when a hook entry composes of more than 1 instruction; i.e. on RISC architectures." + +!!! info "There is a potential register allocation caveat in this scenario." + +Pretend you have the following ARM64 function: + +=== "ARM64" + + ```asm + ADD x1, #5 + ADD x2, #10 + ADD x0, x1, x2 + ADD x0, x0, x0 + RET + ``` + +=== "C" + + ```asm + x1 = x1 + 5; + x2 = x2 + 10; + int x0 = x1 + x2; + x0 = x0 + x0; + return x0; + ``` + +And then, a large hook using an [absolute jump](../arch/operations.md#jumpabsolute) with register is applied: + +```asm +# Original instructions here replaced +MOVZ x0, A +MOVK x0, B, LSL #16 +MOVK x0, C, LSL #32 +MOVK x0, D, LSL #48 +B x0 +# <= branch returns here +``` + +If you then try to apply a smaller hook after applying the large hook, you might run into the following situation: + +```asm +# The 3 instructions here are an absolute jump using pointer. +adrp x9, [0] +ldr x9, [x9, 0x200] +br x9 +# Call to original function returns here, back to then branch to previous hook +MOVK x0, D, LSL #48 +B x0 +``` + +This is problematic, with respect to register allocation. +Absolute jumps on some RISC platforms like ARM will always require the use of a scratch register. + +But there is a risk the scratch register used is the same register (`x0`) as the register used by the +previous hook as the scratch register. In which case, the jump target becomes invalid. + +#### Resolution Strategy + +- Prefer absolute jumps without scratch registers (if possible). +- Detect `mov` + `branch` combinations for each target architecture. + - And *extend* the function's stolen bytes to cover the entirety. + - This avoids the scratch register duplication issue, as original hook code will branch to its own + code before we end up using the same scratch register. + +### When your hook is longer than original. + +!!! note "Only applies to architectures with variable length instructions. (x86)" + +!!! info "Some hooking libraries don't clean up remaining stolen bytes after installing a hook." + +!!! note "Very notably Steam does this for rendering (overlay) and input (controller support)." + +Consider the original function having the following instructions: + +``` +48 8B C4 mov rax, rsp +48 89 58 08 mov [rax + 08], rbx +``` + +After Steam hooks, it will leave the function like this + +``` +E9 XX XX XX XX jmp 'somewhere' +58 08 +``` + +If you're not able to install a relative hook, e.g. need to use an absolute jump + +``` +FF 25 XX XX XX XX jmp ['addr'] +``` + +The invalid instructions will now become part of the 'stolen' bytes, [when you call the original](./function-hooks/overview.md#when-activated); +and invalid instructions may be executed. + +#### Resolution Strategy + +This library must do the following: + +- Prefer shorter hooks (`relative jump` over `absolute jump`) when possible. +- Leave nop(s) after placing any branches, to avoid leaving invalid instructions. + - Don't contribute to the problem. + +There unfortunately isn't much we can do to detect invalid instructions generated by other hooking libraries +reliably, best we can do is try to avoid it by using shorter hooks. Thankfully this is not a common issue +given most people use the 'popular' libraries. + +## Thread Safety on x86 + +!!! note "Thread safety is ***'theoretically'*** not guaranteed for every possible x86 processor, however is satisfied for all modern CPUs." + +!!! tip "The information below is x86 specific but applies to all architectures with a non-fixed instruction size. Architectures with fixed instruction sizes (e.g. ARM) are thread safe in this library by default." + +### The Theory + +> If the `jmp` instruction emplaced when [switching state](./assembly-hooks/overview.md#switching-state) overwrites what originally + were multiple instructions, it is *theoretically* possible that the placing the `jmp` will make the + instruction about to be executed invalid. + +For example if the previous instruction sequence was: + +```asm +0x0: push ebp +0x1: mov ebp, esp ; 2 bytes +``` + +And inserting a jmp produces: + +```asm +0x0: jmp disabled ; 2 bytes +``` + +It's possible that the CPU's Instruction Pointer was at `0x1`` at the time of the overwrite, making the +`mov ebp, esp` instruction invalid. + +### What Happens in Practice + +In practice, modern x86 CPUs (1990 onwards) from Intel, AMD and VIA prefetch instruction in batches +of 16 bytes. We place our stubs generated by the various hooks on 16-byte boundaries for this +(and optimisation) reasons. + +So, by the time we change the code, the CPU has already prefetched the instructions we are atomically +overwriting. + +In other words, it is simply not possible to perfectly time a write such that a thread at `0x1` +(`mov ebp, esp`) would read an invalid instruction, as that instruction was prefetched and is being +executed from local thread cache. + +### What is Safe + +Here is a thread safety table for x86, taking the above into account: + +| Safe? | Hook | Notes | +| ----- | -------- | ---------------------------------------------------------------------------------------------- | +| ✅ | Function | Functions start on multiples of 16 on pretty much all compilers, per Intel Optimisation Guide. | +| ✅ | Branch | Stubs are 16 aligned. | +| ✅ | Assembly | Stubs are 16 aligned. | +| ✅ | VTable | VTable entries are `usize` aligned, and don't cross cache boundaries. | + +## Fallback Strategies + +### Return Address Patching + +!!! warning "This feature will not be ported over from legacy `Reloaded.Hooks`, until an edge case is found that requires this." + +!!! note "This section explains how Reloaded handles an edge case within an already super rare case." + +!!! note "This topic is a bit more complex, so we will use x86 as example here." + +For any of this to be necessary, the following conditions must be true: + +- An existing [relative jump](../arch/operations.md#jumprelative) hook exists. +- Reloaded can't find free memory within [relative jump](../arch/operations.md#jumprelative) range. + - The existing hook was somehow able to find free memory in this range, but we can't... (<= main reason this is improbable!!) +- [Free Space from Function Alignment Strategy](#free-space-from-function-alignment) fails. +- The instructions at beginning of the hooked function happened to just perfectly align such that our hook + jump is longer than the existing one. + +The low probability of this happening, at least on Windows and/or Linux is rather insane. It cannot +be estimated, but if I were to have a guess, maybe 1 in 1 billion. You'd be more likely to die +from a shark attack. + +------------------------------ + +In any case, when this happens, Reloaded performs *return address patching*. + +Suppose a foreign hooking library hooks a function with the following prologue: + +```asm +55 push ebp +89 e5 mov ebp, esp +00 00 add [eax], al +83 ec 20 sub esp, 32 +... +``` + +After hooking, this code would look like: + +```asm +E9 XX XX XX XX jmp 'somewhere' +<= existing hook jumps back here when calling original (this) function +83 ec 20 sub esp, 32 +... +``` + +When the prologue is set up 'just right', such that the existing instrucions divide perfectly +into 5 bytes, and we need to insert a 6 byte absolute jmp `FF 25`, Reloaded must patch the return address. + +Reloaded has a built in patcher for this super rare scenario, which detects and attempts to patch return +addresses of the following patterns: + +``` +Where nop* represents 0 or more nops. + +1. Relative immediate jumps. + + nop* + jmp 0x123456 + nop* + +2. Push + Return + + nop* + push 0x612403 + ret + nop* + +3. RIP Relative Addressing (X64) + + nop* + JMP [RIP+0] + nop* +``` + +This patching mechanism is rather complicated, relies on disassembling code at runtime and thus won't be explained here. + +!!! danger "Different hooking libraries use different logic for storing callbacks. In some cases alignment of code (or rather lack thereof) can also make this operation unreliable, since we rely on disassembling the code at runtime to find jumps back to end of hook. ***The success rate of this operation is NOT 100%***" + +## Requirements for External Libraries to Interoperate + +!!! note "While I haven't studied the source code of other hooking libraries before, I've had no issues in the past with the common [Detours][detours] and [minhook][minhook] libraries that are commonly used" + +### Hooking Over Reloaded Hooks + +!!! info "Libraries which can safely interoperate (stack hooks ontop) of Reloaded Hooks Hooks' must satisfy the following." + +- Must be able to patch (re-adjust) [relative jumps](../arch/operations.md#jumprelative). + - In some cases when assembling call to original function, relative jump target may be out of range, + compatible hooking software must handle this edge case. + +- Must be able to automatically determine number of bytes to steal from original function. + - This makes it possible to interoperate with the rare times we do a [absolute jump](../arch/operations.md#jumpabsolute) when + it may not be possible to do a relative jump (i.e.) as we cannot allocate memory in close + enough proximity. + +### Reloaded Hooks hooking over Existing Hooks + +!!! info "See: [Code Relocation](../arch/overview.md#code-relocation)" + +[detours]: https://github.com/microsoft/Detours +[minhook]: https://github.com/TsudaKageyu/minhook.git \ No newline at end of file diff --git a/docs/dev/design/function-hooks/hooking-strategy.md b/docs/dev/design/function-hooks/hooking-strategy.md index 48e7e99..6f8277e 100644 --- a/docs/dev/design/function-hooks/hooking-strategy.md +++ b/docs/dev/design/function-hooks/hooking-strategy.md @@ -49,119 +49,6 @@ If this is not possible, `reloaded-hooks` will start rewriting [relative jump(s) from the original function to [absolute jump(s)](../../arch/operations.md#jumpabsolute) in the presence of recognised patterns; if the code rewriter supports this. -## Hook Length Mismatch Problem - -!!! info "When a hook is already present, and you wish to stack that hook over the existing hook, certain problems might arise." - -### When your hook is shorter than original. - -!!! tip "This is notably an issue when a hook entry composes of more than 1 instruction; i.e. on RISC architectures." - -!!! info "There is a potential register allocation caveat in this scenario." - -Pretend you have the following ARM64 function: - -=== "ARM64" - - ```asm - ADD x1, #5 - ADD x2, #10 - ADD x0, x1, x2 - ADD x0, x0, x0 - RET - ``` - -=== "C" - - ```asm - x1 = x1 + 5; - x2 = x2 + 10; - int x0 = x1 + x2; - x0 = x0 + x0; - return x0; - ``` - -And then, a large hook using an [absolute jump](../../arch/operations.md#jumpabsolute) with register is applied: - -```asm -# Original instructions here replaced -MOVZ x0, A -MOVK x0, B, LSL #16 -MOVK x0, C, LSL #32 -MOVK x0, D, LSL #48 -B x0 -# <= branch returns here -``` - -If you then try to apply a smaller hook after applying the large hook, you might run into the following situation: - -```asm -# The 3 instructions here are an absolute jump using pointer. -adrp x9, [0] -ldr x9, [x9, 0x200] -br x9 -# Call to original function returns here, back to then branch to previous hook -MOVK x0, D, LSL #48 -B x0 -``` - -This is problematic, with respect to register allocation. -Absolute jumps on some RISC platforms like ARM will always require the use of a scratch register. - -But there is a risk the scratch register used is the same register (`x0`) as the register used by the -previous hook as the scratch register. In which case, the jump target becomes invalid. - -#### Resolution Strategy - -- Prefer absolute jumps without scratch registers (if possible). -- Detect `mov` + `branch` combinations for each target architecture. - - And *extend* the function's stolen bytes to cover the entirety. - - This avoids the scratch register duplication issue, as original hook code will branch to its own - code before we end up using the same scratch register. - -### When your hook is longer than original. - -!!! note "Only applies to architectures with variable length instructions. (x86)" - -!!! info "Some hooking libraries don't clean up remaining stolen bytes after installing a hook." - -!!! note "Very notably Steam does this for rendering (overlay) and input (controller support)." - -Consider the original function having the following instructions: - -``` -48 8B C4 mov rax, rsp -48 89 58 08 mov [rax + 08], rbx -``` - -After Steam hooks, it will leave the function like this - -``` -E9 XX XX XX XX jmp 'somewhere' -58 08 -``` - -If you're not able to install a relative hook, e.g. need to use an absolute jump - -``` -FF 25 XX XX XX XX jmp ['addr'] -``` - -The invalid instructions will now become part of the 'stolen' bytes, [when you call the original](./overview.md#when-activated); -and invalid instructions may be executed. - -#### Resolution Strategy - -This library must do the following: - -- Prefer shorter hooks (`relative jump` over `absolute jump`) when possible. -- Leave nop(s) after placing any branches, to avoid leaving invalid instructions. - - Don't contribute to the problem. - -There unfortunately isn't much we can do to detect invalid instructions generated by other hooking libraries -reliably, best we can do is try to avoid it by using shorter hooks. Thankfully this is not a common issue -given most people use the 'popular' libraries. - ## Fallback Strategies !!! info "Strategies used for improving interoperability with other hooks." @@ -179,105 +66,4 @@ given most people use the 'popular' libraries. If there's sufficient padding before the function, we can: - Insert our absolute jump there, and branch to it. or -- Insert jump target there, and branch using that jump target. - -### Return Address Patching - -!!! warning "This feature will not be ported over from legacy `Reloaded.Hooks`, until an edge case is found that requires this." - -!!! note "This section explains how Reloaded handles an edge case within an already super rare case." - -!!! note "This topic is a bit more complex, so we will use x86 as example here." - -For any of this to be necessary, the following conditions must be true: - -- An existing [relative jump](../../arch/operations.md#jumprelative) hook exists. -- Reloaded can't find free memory within [relative jump](../../arch/operations.md#jumprelative) range. - - The existing hook was somehow able to find free memory in this range, but we can't... (<= main reason this is improbable!!) -- [Free Space from Function Alignment Strategy](#free-space-from-function-alignment) fails. -- The instructions at beginning of the hooked function happened to just perfectly align such that our hook - jump is longer than the existing one. - -The low probability of this happening, at least on Windows and/or Linux is rather insane. It cannot -be estimated, but if I were to have a guess, maybe 1 in 1 billion. You'd be more likely to die -from a shark attack. - ------------------------------- - -In any case, when this happens, Reloaded performs *return address patching*. - -Suppose a foreign hooking library hooks a function with the following prologue: - -```asm -55 push ebp -89 e5 mov ebp, esp -00 00 add [eax], al -83 ec 20 sub esp, 32 -... -``` - -After hooking, this code would look like: - -```asm -E9 XX XX XX XX jmp 'somewhere' -<= existing hook jumps back here when calling original (this) function -83 ec 20 sub esp, 32 -... -``` - -When the prologue is set up 'just right', such that the existing instrucions divide perfectly -into 5 bytes, and we need to insert a 6 byte absolute jmp `FF 25`, Reloaded must patch the return address. - -Reloaded has a built in patcher for this super rare scenario, which detects and attempts to patch return -addresses of the following patterns: - -``` -Where nop* represents 0 or more nops. - -1. Relative immediate jumps. - - nop* - jmp 0x123456 - nop* - -2. Push + Return - - nop* - push 0x612403 - ret - nop* - -3. RIP Relative Addressing (X64) - - nop* - JMP [RIP+0] - nop* -``` - -This patching mechanism is rather complicated, relies on disassembling code at runtime and thus won't be explained here. - -!!! danger "Different hooking libraries use different logic for storing callbacks. In some cases alignment of code (or rather lack thereof) can also make this operation unreliable, since we rely on disassembling the code at runtime to find jumps back to end of hook. ***The success rate of this operation is NOT 100%***" - -## Requirements for External Libraries to Interoperate - -!!! note "While I haven't studied the source code of other hooking libraries before, I've had no issues in the past with the common [Detours][detours] and [minhook][minhook] libraries that are commonly used" - -### Hooking Over Reloaded Hooks - -!!! info "Libraries which can safely interoperate (stack hooks ontop) of Reloaded Hooks Hooks' must satisfy the following." - -- Must be able to patch (re-adjust) [relative jumps](../../arch/operations.md#jumprelative). - - In some cases when assembling call to original function, relative jump target may be out of range, - compatible hooking software must handle this edge case. - -- Must be able to automatically determine number of bytes to steal from original function. - - This makes it possible to interoperate with the rare times we do a [absolute jump](../../arch/operations.md#jumpabsolute) when - it may not be possible to do a relative jump (i.e.) as we cannot allocate memory in close - enough proximity. - -### Reloaded Hooks hooking over Existing Hooks - -!!! info "See: [Code Relocation](../../arch/overview.md#code-relocation)" - -[detours]: https://github.com/microsoft/Detours -[minhook]: https://github.com/TsudaKageyu/minhook.git \ No newline at end of file +- Insert jump target there, and branch using that jump target. \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index cb7a46f..cba4a3e 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -53,6 +53,7 @@ nav: - Home: index.md - Library Internals: - Design Docs: + - Common Notes: dev/design/common.md - Function Hooks: - Overview: dev/design/function-hooks/overview.md - Hooking Strategy: diff --git a/projects/reloaded-hooks-portable/src/api/hooks/assembly/assembly_hook.rs b/projects/reloaded-hooks-portable/src/api/hooks/assembly/assembly_hook.rs index 3472250..430ea18 100644 --- a/projects/reloaded-hooks-portable/src/api/hooks/assembly/assembly_hook.rs +++ b/projects/reloaded-hooks-portable/src/api/hooks/assembly/assembly_hook.rs @@ -2,49 +2,28 @@ extern crate alloc; use crate::{ api::{ buffers::buffer_abstractions::{Buffer, BufferFactory}, - errors::assembly_hook_error::{ArrayTooShortKind, AssemblyHookError}, + errors::assembly_hook_error::AssemblyHookError, jit::compiler::Jit, length_disassembler::LengthDisassembler, rewriter::code_rewriter::CodeRewriter, settings::assembly_hook_settings::AssemblyHookSettings, traits::register_info::RegisterInfo, }, - helpers::{ - atomic_write_masked::atomic_write_masked, - make_inline_rel_branch::{make_inline_branch, INLINE_BRANCH_LEN}, - }, + helpers::atomic_write_masked::atomic_write_masked, internal::assembly_hook::create_assembly_hook, }; -use alloc::boxed::Box; -use bitfield::bitfield; -use core::marker::PhantomData; -bitfield! { - /// `AddImmediate` represents the bitfields of the ADD (immediate) instruction - /// in AArch64 architecture. - pub struct AssemblyHookPackedProps(u8); - impl Debug; +use core::marker::PhantomData; +use core::ptr::NonNull; - /// Length of the 'branch to orig' inline array. - branch_to_orig_len, set_branch_to_orig_len: 6, 4; +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +use super::assembly_hook_props_x86::*; - /// Length of the 'branch to hook' inline array. - branch_to_hook_len, set_branch_to_hook_len: 3, 1; +#[cfg(target_arch = "aarch64")] +use super::assembly_hook_props_4byteins::*; - /// True if the hook is enabled, else false. - is_enabled, set_is_enabled: 0; -} - -impl AssemblyHookPackedProps { - /// Creates a new `AssemblyHookPackedProps` with specified properties. - pub fn new(is_enabled: bool, branch_to_orig_len: u8, branch_to_hook_len: u8) -> Self { - let mut props = AssemblyHookPackedProps(0); - props.set_is_enabled(is_enabled); - props.set_branch_to_orig_len(branch_to_orig_len); - props.set_branch_to_hook_len(branch_to_hook_len); - props - } -} +#[cfg(not(any(target_arch = "aarch64", target_arch = "x86", target_arch = "x86_64")))] +use super::assembly_hook_props_unknown::*; /// Represents an assembly hook. #[repr(C)] // Not 'packed' because this is not in array and malloc in practice will align this. @@ -58,26 +37,13 @@ where TBufferFactory: BufferFactory, { // docs/dev/design/assembly-hooks/overview.md - /// The code placed at the hook function when the hook is enabled. - enabled_code: Box<[u8]>, // 0 - - /// The code placed at the hook function when the hook is disabled. - disabled_code: Box<[u8]>, // 4/8 - /// The address of the stub containing custom code. - stub_address: usize, // 8/16 - - /// Stores sizes of 'branch_to_orig_opcode' and 'branch_to_hook_opcode'. - props: AssemblyHookPackedProps, // 12/24 - - /// The code to branch to 'orig' segment in the buffer, when disabling the hook. - branch_to_orig_opcode: [u8; INLINE_BRANCH_LEN], // 13/25 + stub_address: usize, // 0 - /// The code to branch to 'hook' segment in the buffer, when enabling the hook. - branch_to_hook_opcode: [u8; INLINE_BRANCH_LEN], // 18/30 (-1 on AArch64) + /// Address of 'props' structure + props: NonNull, // 4/8 - // End: 40 (AArch64) [no pad: 33] - // End: 24/40 (x86) [no pad: 23/35] + // Struct size: 8/16 bytes. // Dummy type parameters for Rust compiler to comply. _unused_buf: PhantomData, @@ -99,29 +65,11 @@ where TBufferFactory: BufferFactory, { pub fn new( - is_enabled: bool, - branch_to_orig: Box<[u8]>, - branch_to_hook: Box<[u8]>, - enabled_code: Box<[u8]>, - disabled_code: Box<[u8]>, + props: NonNull, stub_address: usize, ) -> Result> { - let branch_to_orig_opcode = - Self::inline_branch(&branch_to_orig, ArrayTooShortKind::ToOrig)?; - let branch_to_hook_opcode = - Self::inline_branch(&branch_to_hook, ArrayTooShortKind::ToHook)?; - let props = AssemblyHookPackedProps::new( - is_enabled, - branch_to_orig_opcode.len() as u8, - branch_to_hook_opcode.len() as u8, - ); - Ok(Self { props, - branch_to_orig_opcode, - branch_to_hook_opcode, - enabled_code, - disabled_code, stub_address, _unused_buf: PhantomData, _unused_tj: PhantomData, @@ -215,8 +163,15 @@ where /// If the hook is already enabled, this function does nothing. /// If the hook is disabled, this function will write the hook to memory. pub fn enable(&self) { - let num_bytes = self.props.branch_to_hook_len() as usize; - self.write_hook(&self.branch_to_hook_opcode, &self.enabled_code, num_bytes); + unsafe { + let props = self.props.as_ref(); + let num_bytes = props.get_branch_to_hook_len(); + self.write_hook( + props.get_branch_to_hook_slice(), + props.get_enabled_code(), + num_bytes, + ); + } } /// Disables the hook. @@ -224,19 +179,36 @@ where /// If the hook is already disabled, this function does nothing. /// If the hook is enabled, this function will no-op the hook. pub fn disable(&self) { - let num_bytes = self.props.branch_to_orig_len() as usize; - self.write_hook(&self.branch_to_orig_opcode, &self.disabled_code, num_bytes); + unsafe { + let props = self.props.as_ref(); + let num_bytes = props.get_branch_to_orig_len(); + self.write_hook( + props.get_branch_to_orig_slice(), + props.get_disabled_code(), + num_bytes, + ); + } } /// Returns true if the hook is enabled, else false. pub fn get_is_enabled(&self) -> bool { - self.props.is_enabled() + unsafe { self.props.as_ref().is_enabled() } } +} - fn inline_branch( - rc: &[u8], - kind: ArrayTooShortKind, - ) -> Result<[u8; INLINE_BRANCH_LEN], AssemblyHookError> { - make_inline_branch(rc).map_err(|e| AssemblyHookError::InlineBranchError(e, kind)) +impl Drop + for AssemblyHook +where + TBuffer: Buffer, + TJit: Jit, + TRegister: RegisterInfo + Clone + Default, + TDisassembler: LengthDisassembler, + TRewriter: CodeRewriter, + TBufferFactory: BufferFactory, +{ + fn drop(&mut self) { + unsafe { + self.props.as_mut().free(); + } } } diff --git a/projects/reloaded-hooks-portable/src/api/hooks/assembly/assembly_hook_props_4byteins.rs b/projects/reloaded-hooks-portable/src/api/hooks/assembly/assembly_hook_props_4byteins.rs new file mode 100644 index 0000000..4345c5f --- /dev/null +++ b/projects/reloaded-hooks-portable/src/api/hooks/assembly/assembly_hook_props_4byteins.rs @@ -0,0 +1,104 @@ +use bitfield::bitfield; + +bitfield! { + /// Defines the data layout of the Assembly Hook data. + /// For architectures which use 4 byte instructions. + pub struct AssemblyHookPackedProps(u32); + impl Debug; + + /// True if the hook is enabled, else false. + pub is_enabled, set_is_enabled: 0; + + /// Length of the 'disabled code' array. + u16, disabled_code_len, set_disabled_code_len_impl: 12, 1; // Max 16KiB. + + /// Length of the 'enabled code' array. + u32, enabled_code_len, set_enabled_code_len_impl: 31, 13; // Max 2MiB. +} + +impl AssemblyHookPackedProps { + /// Gets the length of the 'enabled code' array. + pub fn get_enabled_code_len(&self) -> usize { + self.enabled_code_len() as usize * 4 + } + + /// Gets the length of the 'disabled code' array. + pub fn get_disabled_code_len(&self) -> usize { + self.disabled_code_len() as usize * 4 + } + + /// Sets the length of the 'enabled code' array with a minimum value of 4. + pub fn set_enabled_code_len(&mut self, len: usize) { + debug_assert!( + len >= 4 && len % 4 == 0, + "Length must be a multiple of 4 and at least 4" + ); + self.set_enabled_code_len_impl((len / 4) as u32); + } + + /// Sets the length of the 'disabled code' array with a minimum value of 4. + pub fn set_disabled_code_len(&mut self, len: usize) { + debug_assert!( + len >= 4 && len % 4 == 0, + "Length must be a multiple of 4 and at least 4" + ); + self.set_disabled_code_len_impl((len / 4) as u16); + } + + /// Sets the 'branch to orig' length field based on the provided length. + pub fn set_branch_to_hook_len(&mut self, _len: usize) { + // no-op, for API compatibility + } + + /// Gets the length of the 'branch to hook' array. Always 4 for AArch64. + pub fn get_branch_to_hook_len(&self) -> usize { + 4 + } + + /// Sets the 'branch to orig' length field based on the provided length. + pub fn set_branch_to_orig_len(&mut self, _len: usize) { + // no-op, for API compatibility + } + + /// Gets the length of the 'branch to orig' array. Always 4 for AArch64. + pub fn get_branch_to_orig_len(&self) -> usize { + 4 + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_enabled_and_disabled_code_lengths() { + let mut props = AssemblyHookPackedProps(0); + + // Test setting and getting enabled code length + props.set_enabled_code_len(123 * 4); // Multiples of 4 + assert_eq!(props.get_enabled_code_len(), 123 * 4); + + // Test setting and getting disabled code length + props.set_disabled_code_len(456 * 4); // Multiples of 4 + assert_eq!(props.get_disabled_code_len(), 456 * 4); + } + + #[test] + #[should_panic(expected = "Length must be a multiple of 4 and at least 4")] + fn test_invalid_code_length() { + let mut props = AssemblyHookPackedProps(0); + props.set_enabled_code_len(5); // Should panic, not a multiple of 4 + } + + #[test] + fn test_branch_lengths() { + let mut props = AssemblyHookPackedProps(0); + + // Setting and getting branch lengths, always 4 for AArch64 + props.set_branch_to_hook_len(4); + assert_eq!(props.get_branch_to_hook_len(), 4); + + props.set_branch_to_orig_len(4); + assert_eq!(props.get_branch_to_orig_len(), 4); + } +} diff --git a/projects/reloaded-hooks-portable/src/api/hooks/assembly/assembly_hook_props_common.rs b/projects/reloaded-hooks-portable/src/api/hooks/assembly/assembly_hook_props_common.rs new file mode 100644 index 0000000..089daea --- /dev/null +++ b/projects/reloaded-hooks-portable/src/api/hooks/assembly/assembly_hook_props_common.rs @@ -0,0 +1,89 @@ +extern crate alloc; +use alloc::vec::Vec; +use core::{ + mem::size_of, + ptr::{copy_nonoverlapping, NonNull}, + slice::from_raw_parts, +}; + +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +use super::assembly_hook_props_x86::AssemblyHookPackedProps; + +#[cfg(target_arch = "aarch64")] +use super::assembly_hook_props_4byteins::AssemblyHookPackedProps; + +#[cfg(not(any(target_arch = "aarch64", target_arch = "x86", target_arch = "x86_64")))] +use super::assembly_hook_props_unknown::AssemblyHookPackedProps; + +/* + Memory Layout: + - AssemblyHookPackedProps + - disabled_code_instructions + - enabled_code_instructions + - branch_to_orig_instructions + - branch_to_hook_instructions +*/ + +// Implement common methods on AssemblyHookPackedProps +impl AssemblyHookPackedProps { + pub fn get_disabled_code<'a>(&self) -> &'a [u8] { + let start_addr = self as *const Self as *const u8; + let offset = size_of::(); + unsafe { from_raw_parts(start_addr.add(offset), self.get_disabled_code_len()) } + } + + pub fn get_enabled_code<'a>(&self) -> &'a [u8] { + let start_addr = self as *const Self as *const u8; + let offset = size_of::() + self.get_disabled_code_len(); + unsafe { from_raw_parts(start_addr.add(offset), self.get_enabled_code_len()) } + } + + pub fn get_branch_to_orig_slice<'a>(&self) -> &'a [u8] { + let start_addr = self as *const Self as *const u8; + let offset = size_of::() + self.get_disabled_code_len() + self.get_enabled_code_len(); + unsafe { from_raw_parts(start_addr.add(offset), self.get_branch_to_hook_len()) } + } + + pub fn get_branch_to_hook_slice<'a>(&self) -> &'a [u8] { + let start_addr = self as *const Self as *const u8; + let offset = size_of::() + + self.get_disabled_code_len() + + self.get_enabled_code_len() + + self.get_branch_to_orig_len(); + unsafe { from_raw_parts(start_addr.add(offset), self.get_branch_to_orig_len()) } + } + + /// Frees the memory allocated for this instance using libc's free. + /// # Safety + /// + /// It's safe. + pub unsafe fn free(&mut self) { + libc::free(self as *mut Self as *mut libc::c_void); + } +} + +/// Allocates memory and copies data from a Vec into it. +/// +/// # Arguments +/// +/// * `data` - The data to be copied into the allocated memory. +/// +/// # Returns +/// +/// A pointer to the newly allocated memory containing the copied data. +/// +/// # Safety +/// +/// The caller is responsible for ensuring that the allocated memory is freed +/// when no longer needed. This function uses `libc::malloc`, so the memory +/// must be freed with `libc::free`. +pub unsafe fn alloc_and_copy_packed_props(data: &Vec) -> NonNull { + let size = data.len(); + let ptr = libc::malloc(size) as *mut u8; + + if !ptr.is_null() { + copy_nonoverlapping(data.as_ptr(), ptr, size); + } + + NonNull::new(ptr as *mut AssemblyHookPackedProps).unwrap() +} diff --git a/projects/reloaded-hooks-portable/src/api/hooks/assembly/assembly_hook_props_unknown.rs b/projects/reloaded-hooks-portable/src/api/hooks/assembly/assembly_hook_props_unknown.rs new file mode 100644 index 0000000..0f0055e --- /dev/null +++ b/projects/reloaded-hooks-portable/src/api/hooks/assembly/assembly_hook_props_unknown.rs @@ -0,0 +1,111 @@ +use bitfield::bitfield; + +bitfield! { + /// Defines the data layout of the Assembly Hook data for unknown architectures. + pub struct AssemblyHookPackedProps(u64); + impl Debug; + + /// True if the hook is enabled, else false. + pub is_enabled, set_is_enabled: 0; + + /// Length of 'branch to hook' array. + u8, branch_to_hook_len, set_branch_to_hook_len_impl: 3, 1; + + /// Length of 'branch to hook' array. + u8, branch_to_orig_len, set_branch_to_orig_len_impl: 6, 4; + + /// Length of the 'disabled code' array. + u32, disabled_code_len, set_disabled_code_len_impl: 33, 7; // Max 128MiB. + + /// Length of the 'enabled code' array. + u32, enabled_code_len, set_enabled_code_len_impl: 63, 34; // Max 1GiB. +} + +impl AssemblyHookPackedProps { + /// Gets the length of the 'enabled code' array. + pub fn get_enabled_code_len(&self) -> usize { + self.enabled_code_len() as usize + } + + /// Gets the length of the 'disabled code' array. + pub fn get_disabled_code_len(&self) -> usize { + self.disabled_code_len() as usize + } + + /// Sets the length of the 'enabled code' array with a minimum value of 1. + pub fn set_enabled_code_len(&mut self, len: usize) { + debug_assert!(len >= 1, "Length must be at least 1"); + self.set_enabled_code_len_impl(len as u32); // Adjust for 'unknown' architecture + } + + /// Sets the length of the 'disabled code' array with a minimum value of 1. + pub fn set_disabled_code_len(&mut self, len: usize) { + debug_assert!(len >= 1, "Length must be at least 1"); + self.set_disabled_code_len_impl(len as u32); // Adjust for 'unknown' architecture + } + + /// Sets the length of the 'branch to hook' array. + pub fn set_branch_to_hook_len(&mut self, len: usize) { + self.set_branch_to_hook_len_impl(len as u8); + } + + /// Gets the length of the 'branch to hook' array. + pub fn get_branch_to_hook_len(&self) -> usize { + self.branch_to_hook_len() as usize + } + + /// Sets the length of the 'branch to orig' array. + pub fn set_branch_to_orig_len(&mut self, len: usize) { + self.set_branch_to_orig_len_impl(len as u8); + } + + /// Gets the length of the 'branch to orig' array. + pub fn get_branch_to_orig_len(&self) -> usize { + self.branch_to_orig_len() as usize + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_enabled_and_disabled_code_lengths() { + let mut props = AssemblyHookPackedProps(0); + + // Test setting and getting enabled code length + props.set_enabled_code_len(123); + assert_eq!(props.get_enabled_code_len(), 123); + + // Test setting and getting disabled code length + props.set_disabled_code_len(456); + assert_eq!(props.get_disabled_code_len(), 456); + } + + #[test] + #[should_panic(expected = "Length must be at least 1")] + fn test_enabled_code_length_minimum() { + let mut props = AssemblyHookPackedProps(0); + props.set_enabled_code_len(0); // Should panic + } + + #[test] + #[should_panic(expected = "Length must be at least 1")] + fn test_disabled_code_length_minimum() { + let mut props = AssemblyHookPackedProps(0); + props.set_disabled_code_len(0); // Should panic + } + + #[test] + fn test_branch_lengths() { + let mut props = AssemblyHookPackedProps(0); + + // Test setting and getting branch to hook length + props.set_branch_to_hook_len(3); + assert_eq!(props.get_branch_to_hook_len(), 3); + + // Test setting and getting branch to orig length + props.set_branch_to_orig_len(4); + assert_eq!(props.get_branch_to_orig_len(), 4); + } +} diff --git a/projects/reloaded-hooks-portable/src/api/hooks/assembly/assembly_hook_props_x86.rs b/projects/reloaded-hooks-portable/src/api/hooks/assembly/assembly_hook_props_x86.rs new file mode 100644 index 0000000..35ccf20 --- /dev/null +++ b/projects/reloaded-hooks-portable/src/api/hooks/assembly/assembly_hook_props_x86.rs @@ -0,0 +1,145 @@ +use bitfield::bitfield; + +bitfield! { + /// Defines the data layout of the Assembly Hook data for x86. + pub struct AssemblyHookPackedProps(u32); + impl Debug; + + /// True if the hook is enabled, else false. + pub is_enabled, set_is_enabled: 0; + + /// If true, 'branch to hook' array is 5 bytes instead of 2 bytes on x86. + is_long_branch_to_hook_len, set_is_long_branch_to_hook_len: 1; + + /// If true, 'branch to orig' array is 5 bytes instead of 2 bytes on x86. + is_long_branch_to_orig_len, set_is_long_branch_to_orig_len: 2; + + /// Length of the 'disabled code' array. + u16, disabled_code_len, set_disabled_code_len_impl: 14, 3; // Max 4KiB. + + /// Length of the 'enabled code' array. + u32, enabled_code_len, set_enabled_code_len_impl: 31, 15; // Max 128KiB. +} + +impl AssemblyHookPackedProps { + /// Gets the length of the 'enabled code' array. + pub fn get_enabled_code_len(&self) -> usize { + self.enabled_code_len() as usize + } + + /// Gets the length of the 'disabled code' array. + pub fn get_disabled_code_len(&self) -> usize { + self.disabled_code_len() as usize + } + + /// Sets the length of the 'enabled code' array with a minimum value of 1. + pub fn set_enabled_code_len(&mut self, len: usize) { + debug_assert!(len >= 1, "Length must be at least 1"); + self.set_enabled_code_len_impl(len as u32); + } + + /// Sets the length of the 'disabled code' array with a minimum value of 1. + pub fn set_disabled_code_len(&mut self, len: usize) { + debug_assert!(len >= 1, "Length must be at least 1"); + self.set_disabled_code_len_impl(len as u16); + } + + /// Sets the 'branch to orig' length field based on the provided length. + pub fn set_branch_to_hook_len(&mut self, len: usize) { + debug_assert!(len == 2 || len == 5, "Length must be either 2 or 5"); + self.set_is_long_branch_to_hook_len(len == 5); + } + + /// Gets the length of the 'branch to hook' array. + pub fn get_branch_to_hook_len(&self) -> usize { + if self.is_long_branch_to_hook_len() { + 5 + } else { + 2 + } + } + + /// Sets the 'branch to orig' length field based on the provided length. + pub fn set_branch_to_orig_len(&mut self, len: usize) { + debug_assert!(len == 2 || len == 5, "Length must be either 2 or 5"); + self.set_is_long_branch_to_orig_len(len == 5); + } + + /// Gets the length of the 'branch to orig' array. + pub fn get_branch_to_orig_len(&self) -> usize { + if self.is_long_branch_to_orig_len() { + 5 + } else { + 2 + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_enabled_and_disabled_code_lengths() { + let mut props = AssemblyHookPackedProps(0); + + // Test setting and getting enabled code length + props.set_enabled_code_len(123); + assert_eq!(props.get_enabled_code_len(), 123); + + // Test setting and getting disabled code length + props.set_disabled_code_len(456); + assert_eq!(props.get_disabled_code_len(), 456); + + // Test minimum length enforcement + props.set_enabled_code_len(1); + props.set_disabled_code_len(1); + assert_eq!(props.enabled_code_len(), 1); + assert_eq!(props.disabled_code_len(), 1); + } + + #[test] + #[should_panic(expected = "Length must be at least 1")] + fn test_enabled_code_length_minimum() { + let mut props = AssemblyHookPackedProps(0); + props.set_enabled_code_len(0); // Should panic + } + + #[test] + #[should_panic(expected = "Length must be at least 1")] + fn test_disabled_code_length_minimum() { + let mut props = AssemblyHookPackedProps(0); + props.set_disabled_code_len(0); // Should panic + } + + #[test] + fn test_branch_lengths() { + let mut props = AssemblyHookPackedProps(0); + + // Test setting and getting branch to hook length + props.set_branch_to_hook_len(2); + assert_eq!(props.get_branch_to_hook_len(), 2); + props.set_branch_to_hook_len(5); + assert_eq!(props.get_branch_to_hook_len(), 5); + + // Test setting and getting branch to orig length + props.set_branch_to_orig_len(2); + assert_eq!(props.get_branch_to_orig_len(), 2); + props.set_branch_to_orig_len(5); + assert_eq!(props.get_branch_to_orig_len(), 5); + } + + #[test] + #[should_panic(expected = "Length must be either 2 or 5")] + fn test_invalid_branch_to_hook_length() { + let mut props = AssemblyHookPackedProps(0); + props.set_branch_to_hook_len(3); // Should panic + } + + #[test] + #[should_panic(expected = "Length must be either 2 or 5")] + fn test_invalid_branch_to_orig_length() { + let mut props = AssemblyHookPackedProps(0); + props.set_branch_to_orig_len(4); // Should panic + } +} diff --git a/projects/reloaded-hooks-portable/src/internal/assembly_hook.rs b/projects/reloaded-hooks-portable/src/internal/assembly_hook.rs index 0e22654..feb5009 100644 --- a/projects/reloaded-hooks-portable/src/internal/assembly_hook.rs +++ b/projects/reloaded-hooks-portable/src/internal/assembly_hook.rs @@ -6,7 +6,9 @@ use crate::{ AssemblyHookError, RewriteErrorDetails, RewriteErrorSource::{self, *}, }, - hooks::assembly::assembly_hook::AssemblyHook, + hooks::assembly::{ + assembly_hook::AssemblyHook, assembly_hook_props_common::alloc_and_copy_packed_props, + }, jit::compiler::Jit, length_disassembler::LengthDisassembler, platforms::platform_functions::MUTUAL_EXCLUSOR, @@ -16,17 +18,28 @@ use crate::{ }, helpers::{ allocate_with_proximity::allocate_with_proximity, - jit_jump_operation::create_jump_operation, overwrite_code::overwrite_code, + jit_jump_operation::create_jump_operation, make_inline_rel_branch::INLINE_BRANCH_LEN, + overwrite_code::overwrite_code, }, }; use alloc::vec::Vec; use alloca::with_alloca; use core::{ cmp::max, - mem::{transmute, MaybeUninit}, + mem::{size_of, transmute, MaybeUninit}, ops::{Add, Sub}, + slice::from_raw_parts, }; +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +use crate::api::hooks::assembly::assembly_hook_props_x86::*; + +#[cfg(target_arch = "aarch64")] +use crate::api::hooks::assembly::assembly_hook_props_4byteins::*; + +#[cfg(not(any(target_arch = "aarch64", target_arch = "x86", target_arch = "x86_64")))] +use crate::api::hooks::assembly::assembly_hook_props_unknown::*; + /// Creates an assembly hook at a specified location in memory. /// /// # Overview @@ -140,9 +153,21 @@ where // Reusable code buffers. let max_vec_len = max(hook_code_max_length, hook_orig_max_length); - let mut code_buf_1 = Vec::::with_capacity(max_vec_len); - let mut code_buf_2 = Vec::::with_capacity(max_vec_len); + let mut props_buf = Vec::::with_capacity( + max_vec_len + + size_of::() + + hook_code_max_length + + hook_orig_max_length + + INLINE_BRANCH_LEN * 2, + ); + + // Reserve space for AssemblyHookPackedProps, and get a pointer to it. + #[allow(clippy::uninit_vec)] + props_buf.set_len(size_of::()); + let props: &mut AssemblyHookPackedProps = + unsafe { &mut *(props_buf.as_mut_ptr() as *mut AssemblyHookPackedProps) }; + let mut code_buf = Vec::::with_capacity(max_vec_len); let hook_params = HookFunctionCommonParams { behaviour: settings.behaviour, asm_code: settings.asm_code, @@ -159,46 +184,59 @@ where // - orig: Original Code // 'Original Code' @ entry + let orig_at_entry_start = props_buf.as_ptr().add(props_buf.len()) as usize; TRewriter::rewrite_code_with_buffer( settings.hook_address as *const u8, orig_code_length, settings.hook_address, buf_addr, settings.scratch_register.clone(), - &mut code_buf_1, + &mut props_buf, ) .map_err(|e| new_rewrite_error(OriginalCode, settings.hook_address, buf_addr, e))?; + let rewritten_len = props_buf + .as_ptr() + .add(props_buf.len()) + .sub(orig_at_entry_start) as usize; + create_jump_operation::( - buf_addr.wrapping_add(code_buf_1.len()), + buf_addr.wrapping_add(rewritten_len as usize), alloc_result.0, jump_back_address, settings.scratch_register.clone(), - &mut code_buf_1, + &mut props_buf, ) .map_err(|e| AssemblyHookError::JitError(e))?; + let orig_at_entry_end = props_buf.as_ptr().add(props_buf.len()) as usize; // 'Hook Function' @ entry construct_hook_function::( &hook_params, buf_addr, - &mut code_buf_2, + &mut props_buf, )?; + let hook_at_entry_end = props_buf.as_ptr().add(props_buf.len()) as usize; + // Write the default code. - let enabled_code = code_buf_2.as_slice().to_vec().into_boxed_slice(); - let disabled_code = code_buf_1.as_slice().to_vec().into_boxed_slice(); - code_buf_1.clear(); - code_buf_2.clear(); + let enabled_len = hook_at_entry_end - orig_at_entry_end; + let enabled_code = from_raw_parts(orig_at_entry_end as *const u8, enabled_len); + let disabled_len = orig_at_entry_end - orig_at_entry_start; + let disabled_code = from_raw_parts(orig_at_entry_start as *const u8, disabled_len); TBuffer::overwrite( buf_addr, if settings.auto_activate { - &enabled_code + enabled_code } else { - &disabled_code + disabled_code }, ); + props.set_is_enabled(settings.auto_activate); + props.set_enabled_code_len(enabled_len); + props.set_disabled_code_len(disabled_len); + // Write the other 2 stubs. let entry_end_ptr = buf_addr + max(enabled_code.len(), disabled_code.len()); @@ -206,12 +244,12 @@ where construct_hook_function::( &hook_params, entry_end_ptr, - &mut code_buf_1, + &mut code_buf, )?; - TBuffer::overwrite(entry_end_ptr, &code_buf_1); - let hook_at_hook_end = entry_end_ptr + code_buf_1.len(); - code_buf_1.clear(); + TBuffer::overwrite(entry_end_ptr, &code_buf); + let hook_at_hook_end = entry_end_ptr + code_buf.len(); + code_buf.clear(); // 'Original Code' @ orig TRewriter::rewrite_code_with_buffer( @@ -220,40 +258,44 @@ where settings.hook_address, hook_at_hook_end, settings.scratch_register.clone(), - &mut code_buf_1, + &mut code_buf, ) .map_err(|e| new_rewrite_error(OrigCodeAtOrig, settings.hook_address, hook_at_hook_end, e))?; create_jump_operation::( - hook_at_hook_end.wrapping_add(code_buf_1.len()), + hook_at_hook_end.wrapping_add(code_buf.len()), alloc_result.0, jump_back_address, settings.scratch_register.clone(), - &mut code_buf_1, + &mut code_buf, ) .map_err(|e| AssemblyHookError::JitError(e))?; - TBuffer::overwrite(hook_at_hook_end, &code_buf_1); - let orig_at_orig_len = code_buf_1.len(); - code_buf_1.clear(); + TBuffer::overwrite(hook_at_hook_end, &code_buf); + let orig_at_orig_len = code_buf.len(); // Branch `entry -> orig` // Branch `entry -> hook` + let branch_to_orig_start = props_buf.as_ptr().add(props_buf.len()); create_jump_operation::( buf_addr, true, entry_end_ptr, settings.scratch_register.clone(), - &mut code_buf_1, + &mut props_buf, ) .map_err(|e| AssemblyHookError::JitError(e))?; + let branch_to_hook_start = props_buf.as_ptr().add(props_buf.len()); + props.set_branch_to_orig_len(branch_to_hook_start.sub(branch_to_orig_start as usize) as usize); create_jump_operation::( buf_addr, true, hook_at_hook_end, settings.scratch_register.clone(), - &mut code_buf_2, + &mut props_buf, ) .map_err(|e| AssemblyHookError::JitError(e))?; + let branch_to_hook_end = props_buf.as_ptr().add(props_buf.len()); + props.set_branch_to_hook_len(branch_to_hook_end.sub(branch_to_hook_start as usize) as usize); // Now JIT a jump to the original code. overwrite_code(settings.hook_address, &code); @@ -276,14 +318,8 @@ where buf.advance(hook_at_hook_end.add(orig_at_orig_len).sub(buf_addr)); // Populate remaining fields - AssemblyHook::new( - settings.auto_activate, - code_buf_1.into_boxed_slice(), - code_buf_2.into_boxed_slice(), - enabled_code, - disabled_code, - buf_addr, - ) + let props = alloc_and_copy_packed_props(&props_buf); + AssemblyHook::new(props, buf_addr) } fn new_rewrite_error( diff --git a/projects/reloaded-hooks-portable/src/lib.rs b/projects/reloaded-hooks-portable/src/lib.rs index 24664bd..89528cd 100644 --- a/projects/reloaded-hooks-portable/src/lib.rs +++ b/projects/reloaded-hooks-portable/src/lib.rs @@ -29,6 +29,10 @@ pub mod api { pub mod hooks { pub mod assembly { pub mod assembly_hook; + pub mod assembly_hook_props_4byteins; + pub mod assembly_hook_props_common; + pub mod assembly_hook_props_unknown; + pub mod assembly_hook_props_x86; } }