wraith/manipulation/inline_hook/arch/
x64.rs

1//! x86_64 architecture implementation
2
3#[cfg(all(not(feature = "std"), feature = "alloc"))]
4use alloc::vec::Vec;
5
6#[cfg(feature = "std")]
7use std::vec::Vec;
8
9use super::Architecture;
10use crate::manipulation::inline_hook::asm::{
11    iced_decoder::InstructionDecoder,
12    iced_relocator::InstructionRelocator,
13};
14
15/// x86_64 (64-bit) architecture
16pub struct X64;
17
18impl Architecture for X64 {
19    // E9 rel32 - 5 bytes
20    const JMP_REL_SIZE: usize = 5;
21
22    // FF 25 00 00 00 00 + 8-byte addr = 14 bytes
23    const JMP_ABS_SIZE: usize = 14;
24
25    const PTR_SIZE: usize = 8;
26    const CODE_ALIGNMENT: usize = 16;
27
28    // we can use 5-byte jmp rel32 if within ±2GB, otherwise need 14 bytes
29    const MIN_HOOK_SIZE: usize = 5;
30
31    fn encode_jmp_rel(source: usize, target: usize) -> Option<Vec<u8>> {
32        // calculate relative offset (accounting for instruction length)
33        let offset = (target as i64) - (source as i64) - 5;
34
35        // check if within 32-bit signed range
36        if offset < i32::MIN as i64 || offset > i32::MAX as i64 {
37            return None;
38        }
39
40        let mut bytes = Vec::with_capacity(5);
41        bytes.push(0xE9); // jmp rel32
42        bytes.extend_from_slice(&(offset as i32).to_le_bytes());
43        Some(bytes)
44    }
45
46    fn encode_jmp_abs(target: usize) -> Vec<u8> {
47        // FF 25 00 00 00 00 = jmp qword ptr [rip+0]
48        // followed by 8-byte absolute address
49        let mut bytes = Vec::with_capacity(14);
50        bytes.extend_from_slice(&[0xFF, 0x25, 0x00, 0x00, 0x00, 0x00]);
51        bytes.extend_from_slice(&(target as u64).to_le_bytes());
52        bytes
53    }
54
55    fn encode_call_rel(source: usize, target: usize) -> Option<Vec<u8>> {
56        let offset = (target as i64) - (source as i64) - 5;
57
58        if offset < i32::MIN as i64 || offset > i32::MAX as i64 {
59            return None;
60        }
61
62        let mut bytes = Vec::with_capacity(5);
63        bytes.push(0xE8); // call rel32
64        bytes.extend_from_slice(&(offset as i32).to_le_bytes());
65        Some(bytes)
66    }
67
68    fn encode_nop_sled(size: usize) -> Vec<u8> {
69        // use multi-byte NOPs for efficiency
70        let mut bytes = Vec::with_capacity(size);
71        let mut remaining = size;
72
73        while remaining > 0 {
74            match remaining {
75                1 => {
76                    bytes.push(0x90); // NOP
77                    remaining -= 1;
78                }
79                2 => {
80                    bytes.extend_from_slice(&[0x66, 0x90]); // 66 NOP
81                    remaining -= 2;
82                }
83                3 => {
84                    bytes.extend_from_slice(&[0x0F, 0x1F, 0x00]); // NOP dword ptr [rax]
85                    remaining -= 3;
86                }
87                4 => {
88                    bytes.extend_from_slice(&[0x0F, 0x1F, 0x40, 0x00]); // NOP dword ptr [rax+0]
89                    remaining -= 4;
90                }
91                5 => {
92                    bytes.extend_from_slice(&[0x0F, 0x1F, 0x44, 0x00, 0x00]); // NOP dword ptr [rax+rax*1+0]
93                    remaining -= 5;
94                }
95                6 => {
96                    bytes.extend_from_slice(&[0x66, 0x0F, 0x1F, 0x44, 0x00, 0x00]); // 66 NOP dword ptr [rax+rax*1+0]
97                    remaining -= 6;
98                }
99                7 => {
100                    bytes.extend_from_slice(&[0x0F, 0x1F, 0x80, 0x00, 0x00, 0x00, 0x00]); // NOP dword ptr [rax+0]
101                    remaining -= 7;
102                }
103                _ => {
104                    // 8+ byte NOP
105                    bytes.extend_from_slice(&[0x0F, 0x1F, 0x84, 0x00, 0x00, 0x00, 0x00, 0x00]);
106                    remaining -= 8;
107                }
108            }
109        }
110
111        bytes
112    }
113
114    fn find_instruction_boundary(code: &[u8], required_size: usize) -> Option<usize> {
115        // use iced-x86 for accurate instruction boundary detection
116        let decoder = InstructionDecoder::x64();
117        decoder.find_boundary(0, code, required_size)
118    }
119
120    fn relocate_instruction(
121        instruction: &[u8],
122        old_address: usize,
123        new_address: usize,
124    ) -> Option<Vec<u8>> {
125        if instruction.is_empty() {
126            return None;
127        }
128
129        // use iced-x86 for accurate instruction relocation
130        let relocator = InstructionRelocator::x64();
131        let result = relocator.relocate_instruction(
132            instruction,
133            old_address as u64,
134            new_address as u64,
135        );
136
137        if result.success {
138            Some(result.bytes)
139        } else {
140            None
141        }
142    }
143
144    fn needs_relocation(instruction: &[u8]) -> bool {
145        if instruction.is_empty() {
146            return false;
147        }
148
149        // use iced-x86 to check if instruction uses relative addressing
150        crate::manipulation::inline_hook::asm::iced_relocator::instruction_needs_relocation(
151            instruction,
152            0,
153        )
154    }
155}
156
157#[cfg(test)]
158mod tests {
159    use super::*;
160
161    #[test]
162    fn test_encode_jmp_rel_near() {
163        // source at 0x1000, target at 0x1100 (within range)
164        let bytes = X64::encode_jmp_rel(0x1000, 0x1100).unwrap();
165        assert_eq!(bytes.len(), 5);
166        assert_eq!(bytes[0], 0xE9);
167        // offset should be 0x100 - 5 = 0xFB
168        let offset = i32::from_le_bytes(bytes[1..5].try_into().unwrap());
169        assert_eq!(offset, 0xFB);
170    }
171
172    #[test]
173    fn test_encode_jmp_rel_far() {
174        // source and target more than 2GB apart - should fail
175        let result = X64::encode_jmp_rel(0x0000_0000_0000_1000, 0x0000_0001_0000_0000);
176        assert!(result.is_none());
177    }
178
179    #[test]
180    fn test_encode_jmp_abs() {
181        let bytes = X64::encode_jmp_abs(0xDEADBEEF12345678);
182        assert_eq!(bytes.len(), 14);
183        assert_eq!(&bytes[0..6], &[0xFF, 0x25, 0x00, 0x00, 0x00, 0x00]);
184        let addr = u64::from_le_bytes(bytes[6..14].try_into().unwrap());
185        assert_eq!(addr, 0xDEADBEEF12345678);
186    }
187
188    #[test]
189    fn test_nop_sled() {
190        for size in 1..=16 {
191            let bytes = X64::encode_nop_sled(size);
192            assert_eq!(bytes.len(), size);
193        }
194    }
195
196    #[test]
197    fn test_find_instruction_boundary() {
198        // typical prologue: push rbp; mov rbp, rsp; sub rsp, 0x28
199        let code = [0x55, 0x48, 0x89, 0xE5, 0x48, 0x83, 0xEC, 0x28];
200
201        // need at least 5 bytes for hook
202        let boundary = X64::find_instruction_boundary(&code, 5).unwrap();
203        assert!(boundary >= 5);
204        assert!(boundary <= 8);
205    }
206
207    #[test]
208    fn test_relocate_jmp_rel32() {
209        // jmp +0x100 from 0x1000 (target: 0x1105)
210        let jmp = [0xE9, 0x00, 0x01, 0x00, 0x00];
211
212        // relocate to 0x2000, target should still be 0x1105
213        let result = X64::relocate_instruction(&jmp, 0x1000, 0x2000).unwrap();
214        assert_eq!(result.len(), 5);
215        assert_eq!(result[0], 0xE9);
216
217        // verify new offset: 0x1105 - 0x2000 - 5 = -0xF00
218        let new_offset = i32::from_le_bytes(result[1..5].try_into().unwrap());
219        assert_eq!(new_offset, -0xF00);
220    }
221
222    #[test]
223    fn test_relocate_non_relative() {
224        // push rbp - not relative, should copy as-is
225        let push = [0x55];
226        let result = X64::relocate_instruction(&push, 0x1000, 0x2000).unwrap();
227        assert_eq!(result, vec![0x55]);
228    }
229
230    #[test]
231    fn test_needs_relocation() {
232        // JMP rel32 - needs relocation
233        assert!(X64::needs_relocation(&[0xE9, 0x00, 0x00, 0x00, 0x00]));
234
235        // CALL rel32 - needs relocation
236        assert!(X64::needs_relocation(&[0xE8, 0x00, 0x00, 0x00, 0x00]));
237
238        // PUSH RBP - doesn't need relocation
239        assert!(!X64::needs_relocation(&[0x55]));
240
241        // NOP - doesn't need relocation
242        assert!(!X64::needs_relocation(&[0x90]));
243    }
244}