wraith/manipulation/inline_hook/arch/
x64.rs

1//! x86_64 architecture implementation
2
3use super::{Architecture, DecodedInstruction};
4
5/// x86_64 (64-bit) architecture
6pub struct X64;
7
8impl Architecture for X64 {
9    // E9 rel32 - 5 bytes
10    const JMP_REL_SIZE: usize = 5;
11
12    // FF 25 00 00 00 00 + 8-byte addr = 14 bytes
13    const JMP_ABS_SIZE: usize = 14;
14
15    const PTR_SIZE: usize = 8;
16    const CODE_ALIGNMENT: usize = 16;
17
18    // we can use 5-byte jmp rel32 if within ±2GB, otherwise need 14 bytes
19    const MIN_HOOK_SIZE: usize = 5;
20
21    fn encode_jmp_rel(source: usize, target: usize) -> Option<Vec<u8>> {
22        // calculate relative offset (accounting for instruction length)
23        let offset = (target as i64) - (source as i64) - 5;
24
25        // check if within 32-bit signed range
26        if offset < i32::MIN as i64 || offset > i32::MAX as i64 {
27            return None;
28        }
29
30        let mut bytes = Vec::with_capacity(5);
31        bytes.push(0xE9); // jmp rel32
32        bytes.extend_from_slice(&(offset as i32).to_le_bytes());
33        Some(bytes)
34    }
35
36    fn encode_jmp_abs(target: usize) -> Vec<u8> {
37        // FF 25 00 00 00 00 = jmp qword ptr [rip+0]
38        // followed by 8-byte absolute address
39        let mut bytes = Vec::with_capacity(14);
40        bytes.extend_from_slice(&[0xFF, 0x25, 0x00, 0x00, 0x00, 0x00]);
41        bytes.extend_from_slice(&(target as u64).to_le_bytes());
42        bytes
43    }
44
45    fn encode_call_rel(source: usize, target: usize) -> Option<Vec<u8>> {
46        let offset = (target as i64) - (source as i64) - 5;
47
48        if offset < i32::MIN as i64 || offset > i32::MAX as i64 {
49            return None;
50        }
51
52        let mut bytes = Vec::with_capacity(5);
53        bytes.push(0xE8); // call rel32
54        bytes.extend_from_slice(&(offset as i32).to_le_bytes());
55        Some(bytes)
56    }
57
58    fn encode_nop_sled(size: usize) -> Vec<u8> {
59        // use multi-byte NOPs for efficiency
60        let mut bytes = Vec::with_capacity(size);
61        let mut remaining = size;
62
63        while remaining > 0 {
64            match remaining {
65                1 => {
66                    bytes.push(0x90); // NOP
67                    remaining -= 1;
68                }
69                2 => {
70                    bytes.extend_from_slice(&[0x66, 0x90]); // 66 NOP
71                    remaining -= 2;
72                }
73                3 => {
74                    bytes.extend_from_slice(&[0x0F, 0x1F, 0x00]); // NOP dword ptr [rax]
75                    remaining -= 3;
76                }
77                4 => {
78                    bytes.extend_from_slice(&[0x0F, 0x1F, 0x40, 0x00]); // NOP dword ptr [rax+0]
79                    remaining -= 4;
80                }
81                5 => {
82                    bytes.extend_from_slice(&[0x0F, 0x1F, 0x44, 0x00, 0x00]); // NOP dword ptr [rax+rax*1+0]
83                    remaining -= 5;
84                }
85                6 => {
86                    bytes.extend_from_slice(&[0x66, 0x0F, 0x1F, 0x44, 0x00, 0x00]); // 66 NOP dword ptr [rax+rax*1+0]
87                    remaining -= 6;
88                }
89                7 => {
90                    bytes.extend_from_slice(&[0x0F, 0x1F, 0x80, 0x00, 0x00, 0x00, 0x00]); // NOP dword ptr [rax+0]
91                    remaining -= 7;
92                }
93                _ => {
94                    // 8+ byte NOP
95                    bytes.extend_from_slice(&[0x0F, 0x1F, 0x84, 0x00, 0x00, 0x00, 0x00, 0x00]);
96                    remaining -= 8;
97                }
98            }
99        }
100
101        bytes
102    }
103
104    fn find_instruction_boundary(code: &[u8], required_size: usize) -> Option<usize> {
105        let mut offset = 0;
106
107        while offset < required_size && offset < code.len() {
108            let insn = decode_instruction_x64(&code[offset..])?;
109            offset += insn.length;
110        }
111
112        if offset >= required_size {
113            Some(offset)
114        } else {
115            None
116        }
117    }
118
119    fn relocate_instruction(
120        instruction: &[u8],
121        old_address: usize,
122        new_address: usize,
123    ) -> Option<Vec<u8>> {
124        if instruction.is_empty() {
125            return None;
126        }
127
128        let decoded = decode_instruction_x64(instruction)?;
129
130        // if not relative, just copy as-is
131        if !decoded.is_relative {
132            return Some(instruction[..decoded.length].to_vec());
133        }
134
135        // handle different relative instruction types
136        match instruction[0] {
137            // E8 - call rel32
138            0xE8 => {
139                if instruction.len() < 5 {
140                    return None;
141                }
142                let orig_offset = i32::from_le_bytes(instruction[1..5].try_into().ok()?);
143                let orig_target = (old_address as i64 + 5 + orig_offset as i64) as usize;
144                let new_offset = (orig_target as i64 - new_address as i64 - 5) as i32;
145
146                let mut bytes = vec![0xE8];
147                bytes.extend_from_slice(&new_offset.to_le_bytes());
148                Some(bytes)
149            }
150
151            // E9 - jmp rel32
152            0xE9 => {
153                if instruction.len() < 5 {
154                    return None;
155                }
156                let orig_offset = i32::from_le_bytes(instruction[1..5].try_into().ok()?);
157                let orig_target = (old_address as i64 + 5 + orig_offset as i64) as usize;
158                let new_offset = (orig_target as i64 - new_address as i64 - 5) as i32;
159
160                let mut bytes = vec![0xE9];
161                bytes.extend_from_slice(&new_offset.to_le_bytes());
162                Some(bytes)
163            }
164
165            // EB - jmp rel8
166            0xEB => {
167                if instruction.len() < 2 {
168                    return None;
169                }
170                let orig_offset = instruction[1] as i8;
171                let orig_target = (old_address as i64 + 2 + orig_offset as i64) as usize;
172
173                // try to keep as short jump if possible
174                let new_offset = (orig_target as i64 - new_address as i64 - 2) as i64;
175                if new_offset >= i8::MIN as i64 && new_offset <= i8::MAX as i64 {
176                    Some(vec![0xEB, new_offset as u8])
177                } else {
178                    // expand to jmp rel32
179                    let new_offset = (orig_target as i64 - new_address as i64 - 5) as i32;
180                    let mut bytes = vec![0xE9];
181                    bytes.extend_from_slice(&new_offset.to_le_bytes());
182                    Some(bytes)
183                }
184            }
185
186            // 0F 80-8F - conditional jumps rel32
187            0x0F if instruction.len() >= 2 && (0x80..=0x8F).contains(&instruction[1]) => {
188                if instruction.len() < 6 {
189                    return None;
190                }
191                let orig_offset = i32::from_le_bytes(instruction[2..6].try_into().ok()?);
192                let orig_target = (old_address as i64 + 6 + orig_offset as i64) as usize;
193                let new_offset = (orig_target as i64 - new_address as i64 - 6) as i32;
194
195                let mut bytes = vec![0x0F, instruction[1]];
196                bytes.extend_from_slice(&new_offset.to_le_bytes());
197                Some(bytes)
198            }
199
200            // 70-7F - short conditional jumps
201            b if (0x70..=0x7F).contains(&b) => {
202                if instruction.len() < 2 {
203                    return None;
204                }
205                let orig_offset = instruction[1] as i8;
206                let orig_target = (old_address as i64 + 2 + orig_offset as i64) as usize;
207
208                // expand to long conditional jump
209                let new_offset = (orig_target as i64 - new_address as i64 - 6) as i32;
210                let long_opcode = 0x80 + (b - 0x70);
211                let mut bytes = vec![0x0F, long_opcode];
212                bytes.extend_from_slice(&new_offset.to_le_bytes());
213                Some(bytes)
214            }
215
216            // handle RIP-relative addressing in other instructions
217            _ => relocate_rip_relative_x64(instruction, old_address, new_address),
218        }
219    }
220
221    fn needs_relocation(instruction: &[u8]) -> bool {
222        if instruction.is_empty() {
223            return false;
224        }
225
226        match instruction[0] {
227            // relative jumps and calls
228            0xE8 | 0xE9 | 0xEB => true,
229            // short conditional jumps
230            0x70..=0x7F => true,
231            // long conditional jumps
232            0x0F if instruction.len() >= 2 && (0x80..=0x8F).contains(&instruction[1]) => true,
233            // check for RIP-relative addressing (ModR/M with mod=00, r/m=101)
234            _ => has_rip_relative_addressing(instruction),
235        }
236    }
237}
238
239/// decode a single x64 instruction and return its properties
240fn decode_instruction_x64(code: &[u8]) -> Option<DecodedInstruction> {
241    if code.is_empty() {
242        return None;
243    }
244
245    let mut offset = 0;
246
247    // skip legacy prefixes
248    while offset < code.len() {
249        match code[offset] {
250            0x26 | 0x2E | 0x36 | 0x3E | 0x64 | 0x65 | 0x66 | 0x67 | 0xF0 | 0xF2 | 0xF3 => {
251                offset += 1;
252            }
253            _ => break,
254        }
255    }
256
257    if offset >= code.len() {
258        return None;
259    }
260
261    // check for REX prefix
262    let has_rex = (0x40..=0x4F).contains(&code[offset]);
263    if has_rex {
264        offset += 1;
265    }
266
267    if offset >= code.len() {
268        return None;
269    }
270
271    let opcode = code[offset];
272    offset += 1;
273
274    // decode based on opcode
275    let (length, is_relative, relative_target) = match opcode {
276        // single-byte instructions
277        0x50..=0x5F | 0x90..=0x9F | 0xC3 | 0xCC | 0xCB | 0xCF => {
278            (offset, false, None)
279        }
280
281        // push/pop with immediate
282        0x68 => (offset + 4, false, None), // push imm32
283        0x6A => (offset + 1, false, None), // push imm8
284
285        // ret with immediate
286        0xC2 => (offset + 2, false, None),
287
288        // jmp/call rel32
289        0xE8 | 0xE9 => {
290            if code.len() < offset + 4 {
291                return None;
292            }
293            let rel = i32::from_le_bytes(code[offset..offset + 4].try_into().ok()?);
294            let target = (code.as_ptr() as usize + offset + 4).wrapping_add(rel as usize);
295            (offset + 4, true, Some(target))
296        }
297
298        // jmp rel8
299        0xEB => {
300            if code.len() < offset + 1 {
301                return None;
302            }
303            let rel = code[offset] as i8;
304            let target = (code.as_ptr() as usize + offset + 1).wrapping_add(rel as usize);
305            (offset + 1, true, Some(target))
306        }
307
308        // short conditional jumps
309        0x70..=0x7F => {
310            if code.len() < offset + 1 {
311                return None;
312            }
313            let rel = code[offset] as i8;
314            let target = (code.as_ptr() as usize + offset + 1).wrapping_add(rel as usize);
315            (offset + 1, true, Some(target))
316        }
317
318        // mov r64, imm64 (REX.W + B8+rd)
319        0xB8..=0xBF if has_rex && (code[offset - 2] & 0x08) != 0 => {
320            (offset + 8, false, None)
321        }
322
323        // mov r32, imm32
324        0xB8..=0xBF => (offset + 4, false, None),
325
326        // mov r8, imm8
327        0xB0..=0xB7 => (offset + 1, false, None),
328
329        // two-byte opcodes (0F xx)
330        0x0F => {
331            if offset >= code.len() {
332                return None;
333            }
334            let op2 = code[offset];
335            offset += 1;
336
337            match op2 {
338                // long conditional jumps
339                0x80..=0x8F => {
340                    if code.len() < offset + 4 {
341                        return None;
342                    }
343                    let rel = i32::from_le_bytes(code[offset..offset + 4].try_into().ok()?);
344                    let target = (code.as_ptr() as usize + offset + 4).wrapping_add(rel as usize);
345                    (offset + 4, true, Some(target))
346                }
347                // setcc
348                0x90..=0x9F => decode_modrm_instruction(code, offset, has_rex),
349                // cmovcc
350                0x40..=0x4F => decode_modrm_instruction(code, offset, has_rex),
351                // movzx/movsx
352                0xB6 | 0xB7 | 0xBE | 0xBF => decode_modrm_instruction(code, offset, has_rex),
353                // other two-byte with ModR/M
354                _ => decode_modrm_instruction(code, offset, has_rex),
355            }
356        }
357
358        // immediate group instructions: ModR/M + imm8
359        0x80 | 0x83 | 0xC0 | 0xC1 => {
360            let (len, is_rel, target) = decode_modrm_instruction(code, offset, has_rex);
361            (len + 1, is_rel, target) // +1 for imm8
362        }
363
364        // immediate group instructions: ModR/M + imm32
365        0x81 | 0xC7 => {
366            let (len, is_rel, target) = decode_modrm_instruction(code, offset, has_rex);
367            (len + 4, is_rel, target) // +4 for imm32
368        }
369
370        // imul r, r/m, imm8
371        0x6B => {
372            let (len, is_rel, target) = decode_modrm_instruction(code, offset, has_rex);
373            (len + 1, is_rel, target)
374        }
375
376        // imul r, r/m, imm32
377        0x69 => {
378            let (len, is_rel, target) = decode_modrm_instruction(code, offset, has_rex);
379            (len + 4, is_rel, target)
380        }
381
382        // mov r/m8, imm8
383        0xC6 => {
384            let (len, is_rel, target) = decode_modrm_instruction(code, offset, has_rex);
385            (len + 1, is_rel, target)
386        }
387
388        // instructions with ModR/M byte only (no immediate)
389        0x00..=0x3F | 0x84..=0x8F | 0xD0..=0xD3 | 0xF6..=0xF7
390        | 0xFE..=0xFF | 0x63 | 0x8D => {
391            decode_modrm_instruction(code, offset, has_rex)
392        }
393
394        // int3
395        0xCC => (offset, false, None),
396
397        // int imm8
398        0xCD => (offset + 1, false, None),
399
400        // instructions with immediate
401        0x04 | 0x0C | 0x14 | 0x1C | 0x24 | 0x2C | 0x34 | 0x3C => (offset + 1, false, None), // AL, imm8
402        0x05 | 0x0D | 0x15 | 0x1D | 0x25 | 0x2D | 0x35 | 0x3D => (offset + 4, false, None), // EAX, imm32
403
404        // default: try ModR/M decode
405        _ => decode_modrm_instruction(code, offset, has_rex),
406    };
407
408    Some(DecodedInstruction {
409        length,
410        is_relative,
411        relative_target,
412    })
413}
414
415/// decode ModR/M-based instruction length
416fn decode_modrm_instruction(code: &[u8], offset: usize, _has_rex: bool) -> (usize, bool, Option<usize>) {
417    if offset >= code.len() {
418        return (offset, false, None);
419    }
420
421    let modrm = code[offset];
422    let mod_field = (modrm >> 6) & 0x03;
423    let rm = modrm & 0x07;
424
425    let mut len = offset + 1; // +1 for ModR/M
426
427    // check for RIP-relative (mod=00, rm=101)
428    let is_rip_relative = mod_field == 0 && rm == 5;
429
430    match mod_field {
431        0b00 => {
432            if rm == 4 {
433                // SIB byte follows
434                len += 1;
435                if len <= code.len() && (code[len - 1] & 0x07) == 5 {
436                    len += 4; // disp32 with SIB base=5
437                }
438            } else if rm == 5 {
439                len += 4; // RIP-relative disp32
440            }
441        }
442        0b01 => {
443            if rm == 4 {
444                len += 1; // SIB
445            }
446            len += 1; // disp8
447        }
448        0b10 => {
449            if rm == 4 {
450                len += 1; // SIB
451            }
452            len += 4; // disp32
453        }
454        0b11 => {
455            // register-direct, no additional bytes
456        }
457        _ => {}
458    }
459
460    (len.min(code.len()), is_rip_relative, None)
461}
462
463/// check if instruction uses RIP-relative addressing
464fn has_rip_relative_addressing(instruction: &[u8]) -> bool {
465    if instruction.is_empty() {
466        return false;
467    }
468
469    let mut offset = 0;
470
471    // skip prefixes
472    while offset < instruction.len() {
473        match instruction[offset] {
474            0x26 | 0x2E | 0x36 | 0x3E | 0x64 | 0x65 | 0x66 | 0x67 | 0xF0 | 0xF2 | 0xF3 => {
475                offset += 1;
476            }
477            _ => break,
478        }
479    }
480
481    // skip REX
482    if offset < instruction.len() && (0x40..=0x4F).contains(&instruction[offset]) {
483        offset += 1;
484    }
485
486    if offset >= instruction.len() {
487        return false;
488    }
489
490    // skip opcode(s)
491    let opcode = instruction[offset];
492    offset += 1;
493
494    if opcode == 0x0F && offset < instruction.len() {
495        offset += 1; // two-byte opcode
496    }
497
498    // check ModR/M
499    if offset >= instruction.len() {
500        return false;
501    }
502
503    let modrm = instruction[offset];
504    let mod_field = (modrm >> 6) & 0x03;
505    let rm = modrm & 0x07;
506
507    // RIP-relative: mod=00, rm=101
508    mod_field == 0 && rm == 5
509}
510
511/// relocate RIP-relative instructions
512fn relocate_rip_relative_x64(
513    instruction: &[u8],
514    old_address: usize,
515    new_address: usize,
516) -> Option<Vec<u8>> {
517    if !has_rip_relative_addressing(instruction) {
518        // not RIP-relative, copy as-is
519        return Some(instruction.to_vec());
520    }
521
522    let decoded = decode_instruction_x64(instruction)?;
523    let mut result = instruction[..decoded.length].to_vec();
524
525    // find ModR/M offset
526    let mut modrm_offset = 0;
527
528    // skip prefixes
529    while modrm_offset < result.len() {
530        match result[modrm_offset] {
531            0x26 | 0x2E | 0x36 | 0x3E | 0x64 | 0x65 | 0x66 | 0x67 | 0xF0 | 0xF2 | 0xF3 => {
532                modrm_offset += 1;
533            }
534            _ => break,
535        }
536    }
537
538    // skip REX
539    if modrm_offset < result.len() && (0x40..=0x4F).contains(&result[modrm_offset]) {
540        modrm_offset += 1;
541    }
542
543    // skip opcode(s)
544    if modrm_offset < result.len() {
545        modrm_offset += 1;
546        if result[modrm_offset - 1] == 0x0F && modrm_offset < result.len() {
547            modrm_offset += 1;
548        }
549    }
550
551    // modrm_offset now points to ModR/M byte
552    if modrm_offset >= result.len() {
553        return Some(result);
554    }
555
556    // disp32 follows ModR/M for RIP-relative
557    let disp_offset = modrm_offset + 1;
558    if disp_offset + 4 > result.len() {
559        return Some(result);
560    }
561
562    // read original displacement
563    let orig_disp = i32::from_le_bytes(result[disp_offset..disp_offset + 4].try_into().ok()?);
564
565    // calculate original absolute target
566    let insn_end = old_address + decoded.length;
567    let orig_target = (insn_end as i64 + orig_disp as i64) as usize;
568
569    // calculate new displacement
570    let new_insn_end = new_address + decoded.length;
571    let new_disp = (orig_target as i64 - new_insn_end as i64) as i32;
572
573    // check if new displacement fits in 32 bits
574    let target_diff = orig_target as i64 - new_insn_end as i64;
575    if target_diff < i32::MIN as i64 || target_diff > i32::MAX as i64 {
576        // can't relocate - target too far
577        return None;
578    }
579
580    // write new displacement
581    result[disp_offset..disp_offset + 4].copy_from_slice(&new_disp.to_le_bytes());
582
583    Some(result)
584}
585
586#[cfg(test)]
587mod tests {
588    use super::*;
589
590    #[test]
591    fn test_encode_jmp_rel_near() {
592        // source at 0x1000, target at 0x1100 (within range)
593        let bytes = X64::encode_jmp_rel(0x1000, 0x1100).unwrap();
594        assert_eq!(bytes.len(), 5);
595        assert_eq!(bytes[0], 0xE9);
596        // offset should be 0x100 - 5 = 0xFB
597        let offset = i32::from_le_bytes(bytes[1..5].try_into().unwrap());
598        assert_eq!(offset, 0xFB);
599    }
600
601    #[test]
602    fn test_encode_jmp_rel_far() {
603        // source and target more than 2GB apart - should fail
604        let result = X64::encode_jmp_rel(0x0000_0000_0000_1000, 0x0000_0001_0000_0000);
605        assert!(result.is_none());
606    }
607
608    #[test]
609    fn test_encode_jmp_abs() {
610        let bytes = X64::encode_jmp_abs(0xDEADBEEF12345678);
611        assert_eq!(bytes.len(), 14);
612        assert_eq!(&bytes[0..6], &[0xFF, 0x25, 0x00, 0x00, 0x00, 0x00]);
613        let addr = u64::from_le_bytes(bytes[6..14].try_into().unwrap());
614        assert_eq!(addr, 0xDEADBEEF12345678);
615    }
616
617    #[test]
618    fn test_nop_sled() {
619        for size in 1..=16 {
620            let bytes = X64::encode_nop_sled(size);
621            assert_eq!(bytes.len(), size);
622        }
623    }
624
625    #[test]
626    fn test_decode_push_rbp() {
627        // push rbp = 55
628        let code = [0x55];
629        let decoded = decode_instruction_x64(&code).unwrap();
630        assert_eq!(decoded.length, 1);
631        assert!(!decoded.is_relative);
632    }
633
634    #[test]
635    fn test_decode_mov_rbp_rsp() {
636        // mov rbp, rsp = 48 89 E5
637        let code = [0x48, 0x89, 0xE5];
638        let decoded = decode_instruction_x64(&code).unwrap();
639        assert_eq!(decoded.length, 3);
640        assert!(!decoded.is_relative);
641    }
642
643    #[test]
644    fn test_decode_sub_rsp_imm8() {
645        // sub rsp, 0x28 = 48 83 EC 28
646        let code = [0x48, 0x83, 0xEC, 0x28];
647        let decoded = decode_instruction_x64(&code).unwrap();
648        assert_eq!(decoded.length, 4);
649        assert!(!decoded.is_relative);
650    }
651
652    #[test]
653    fn test_find_instruction_boundary() {
654        // typical prologue: push rbp; mov rbp, rsp; sub rsp, 0x28
655        let code = [0x55, 0x48, 0x89, 0xE5, 0x48, 0x83, 0xEC, 0x28];
656
657        // need at least 5 bytes for hook
658        let boundary = X64::find_instruction_boundary(&code, 5).unwrap();
659        assert!(boundary >= 5);
660        assert!(boundary <= 8);
661    }
662}