Skip to content

Commit

Permalink
replace udis with iced-x86 (#41)
Browse files Browse the repository at this point in the history
* replace udis with iced-x86

#36

* Updates for code review
  • Loading branch information
LunNova authored Nov 22, 2023
1 parent a5be6c2 commit 19f2132
Show file tree
Hide file tree
Showing 3 changed files with 93 additions and 148 deletions.
7 changes: 5 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,11 @@ crate-type = ["cdylib"]
name = "kernel32_detour"
crate-type = ["cdylib"]

[target."cfg(any(target_arch = \"x86\", target_arch = \"x86_64\"))".dependencies]
udis = { package = "libudis86-sys", version = "0.2.1" }
[target."cfg(any(target_arch = \"x86\", target_arch = \"x86_64\"))".dependencies.iced-x86]
version = "1.20"
default-features = false
# https://github.com/icedland/iced/blob/master/src/rust/iced-x86/README.md#crate-feature-flags
features = ["std", "decoder", "fast_fmt"]

[target."cfg(windows)".dev-dependencies.windows]
version = "0.48"
Expand Down
137 changes: 34 additions & 103 deletions src/arch/x86/trampoline/disasm.rs
Original file line number Diff line number Diff line change
@@ -1,126 +1,57 @@
//! The underlying disassembler should be opaque to the outside.
use std::slice;
use iced_x86::{Instruction, Mnemonic, OpKind, Register};

/// A x86/x64 disassembler.
pub struct Disassembler(udis::ud);

impl Disassembler {
/// Creates a default x86 disassembler.
pub fn new(target: *const ()) -> Disassembler {
unsafe {
let mut ud = ::std::mem::zeroed();
udis::ud_init(&mut ud);
udis::ud_set_user_opaque_data(&mut ud, target as *mut _);
udis::ud_set_input_hook(&mut ud, Some(Self::udis_read_address));
udis::ud_set_mode(&mut ud, (::std::mem::size_of::<usize>() * 8) as u8);
Disassembler(ud)
}
}

/// Reads one byte from a pointer and advances it.
unsafe extern "C" fn udis_read_address(ud: *mut udis::ud) -> libc::c_int {
let pointer = udis::ud_get_user_opaque_data(ud) as *mut u8;
let result = *pointer;
udis::ud_set_user_opaque_data(ud, pointer.offset(1) as *mut _);
libc::c_int::from(result)
}
}

/// Safe wrapper around an instruction.
pub struct Instruction {
address: usize,
mnemonic: udis::ud_mnemonic_code,
operands: Vec<udis::ud_operand>,
bytes: &'static [u8],
pub trait InstructionExt {
/// Returns the instructions relative branch offset, if applicable.
fn relative_branch_target(&self) -> Option<u64>;
/// Returns the instructions RIP operand displacement if applicable.
fn rip_operand_target(&self) -> Option<u64>;
/// Returns true if this instruction any type of a loop.
fn is_loop(&self) -> bool;
/// Returns true if this instruction is an unconditional jump.
fn is_unconditional_jump(&self) -> bool;
/// Returns true if this instruction is a function call.
fn is_call(&self) -> bool;
/// Returns true if this instruction is a return.
fn is_return(&self) -> bool;
}

impl Instruction {
/// Disassembles a new instruction at the specified address.
pub unsafe fn new(disasm: &mut Disassembler, address: *const ()) -> Option<Self> {
let instruction_bytes = udis::ud_disassemble(&mut disasm.0) as usize;
if instruction_bytes > 0 {
Some(Instruction {
address: address as usize,
mnemonic: udis::ud_insn_mnemonic(&disasm.0),
operands: disasm.0.operand.to_vec(),
bytes: slice::from_raw_parts(address as *const _, instruction_bytes),
})
} else {
None
}
}

/// Returns the instruction's address.
pub fn address(&self) -> usize {
self.address
}

/// Returns the next instruction's address.
pub fn next_instruction_address(&self) -> usize {
self.address() + self.len()
}

impl InstructionExt for Instruction {
/// Returns the instructions relative branch offset, if applicable.
pub fn relative_branch_displacement(&self) -> Option<isize> {
unsafe {
self
.operands
.iter()
.find(|op| op.otype == udis::ud_type::UD_OP_JIMM)
.map(|op| match op.size {
8 => op.lval.sbyte as isize,
32 => op.lval.sdword as isize,
_ => unreachable!("Operand size: {}", op.size),
})
fn relative_branch_target(&self) -> Option<u64> {
use OpKind::*;
match self.op0_kind() {
NearBranch16 | NearBranch32 | NearBranch64 => Some(self.near_branch_target()),
_ => None,
}
}

/// Returns the instructions RIP operand displacement if applicable.
pub fn rip_operand_displacement(&self) -> Option<isize> {
unsafe {
// The operands displacement (e.g `mov eax, [rip+0x10]` ⟶ 0x10)
self
.operands
.iter()
.find(|op| op.otype == udis::ud_type::UD_OP_MEM && op.base == udis::ud_type::UD_R_RIP)
.map(|op| op.lval.sdword as isize)
}
fn rip_operand_target(&self) -> Option<u64> {
self
.op_kinds()
.find(|op| *op == OpKind::Memory && self.memory_base() == Register::RIP)
.map(|_| self.memory_displacement64())
}

/// Returns true if this instruction any type of a loop.
pub fn is_loop(&self) -> bool {
matches!(
self.mnemonic,
udis::ud_mnemonic_code::UD_Iloop
| udis::ud_mnemonic_code::UD_Iloope
| udis::ud_mnemonic_code::UD_Iloopne
| udis::ud_mnemonic_code::UD_Ijecxz
| udis::ud_mnemonic_code::UD_Ijcxz
)
fn is_loop(&self) -> bool {
use Mnemonic::*;
matches!(self.mnemonic(), Loop | Loope | Loopne | Jecxz | Jcxz)
}

/// Returns true if this instruction is an unconditional jump.
pub fn is_unconditional_jump(&self) -> bool {
self.mnemonic == udis::ud_mnemonic_code::UD_Ijmp
fn is_unconditional_jump(&self) -> bool {
self.mnemonic() == Mnemonic::Jmp
}

/// Returns true if this instruction is a function call.
pub fn is_call(&self) -> bool {
self.mnemonic == udis::ud_mnemonic_code::UD_Icall
fn is_call(&self) -> bool {
self.mnemonic() == Mnemonic::Call
}

/// Returns true if this instruction is a return.
pub fn is_return(&self) -> bool {
self.mnemonic == udis::ud_mnemonic_code::UD_Iret
}

/// Returns the instruction's bytes.
pub unsafe fn as_slice(&self) -> &[u8] {
self.bytes
}

/// Returns the size of the instruction in bytes.
pub fn len(&self) -> usize {
self.bytes.len()
fn is_return(&self) -> bool {
self.mnemonic() == Mnemonic::Ret
}
}
97 changes: 54 additions & 43 deletions src/arch/x86/trampoline/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ use self::disasm::*;
use crate::arch::x86::thunk;
use crate::error::{Error, Result};
use crate::pic;
use std::mem;
use iced_x86::{Decoder, DecoderOptions, Instruction};
use std::ptr::slice_from_raw_parts;
use std::{mem, slice};

mod disasm;

Expand Down Expand Up @@ -31,8 +33,6 @@ impl Trampoline {

/// A trampoline builder.
struct Builder {
/// Disassembler for x86/x64.
disassembler: Disassembler,
/// Target destination for a potential internal branch.
branch_address: Option<usize>,
/// Total amount of bytes disassembled.
Expand All @@ -49,7 +49,6 @@ impl Builder {
/// Returns a trampoline builder.
pub fn new(target: *const (), margin: usize) -> Self {
Builder {
disassembler: Disassembler::new(target),
branch_address: None,
total_bytes_disassembled: 0,
finished: false,
Expand All @@ -60,13 +59,36 @@ impl Builder {

/// Creates a trampoline with the supplied settings.
///
/// Margins larger than five bytes may lead to undefined behavior.
/// # Safety
///
/// target..target+margin+15 must be valid to read as a u8 slice or behavior
/// may be undefined
pub unsafe fn build(mut self) -> Result<Trampoline> {
let mut emitter = pic::CodeEmitter::new();

while !self.finished {
let instruction = self.next_instruction()?;
let thunk = self.process_instruction(&instruction)?;
// 15 = max size of x64 instruction
// safety: we don't know the end address of a function so this could be too far
// if the function is right at the end of the code section iced_x86 decoder
// doesn't have a way to read one byte at a time without creating a slice in
// advance and it's invalid to make a slice that's too long we could make a
// new Decoder before reading every individual instruction? but it'd still need
// to be given a 15 byte slice to handle any valid x64 instruction
let target: *const u8 = self.target.cast();
let slice = unsafe { slice::from_raw_parts(std::hint::black_box(target), self.margin + 15) };
let decoder = Decoder::with_ip(
(mem::size_of::<usize>() * 8) as u32,
slice,
self.target as u64,
DecoderOptions::NONE,
);
for instruction in decoder {
if instruction.is_invalid() {
break;
}
self.total_bytes_disassembled += instruction.len();
let instr_offset = instruction.ip() as usize - (self.target as usize);
let instruction_bytes = &slice[instr_offset..instr_offset + instruction.len()];
let thunk = self.process_instruction(&instruction, instruction_bytes)?;

// If the trampoline displacement is larger than the target
// function, all instructions will be displaced, and if there is
Expand All @@ -80,9 +102,13 @@ impl Builder {
// Determine whether enough bytes for the margin has been disassembled
if self.total_bytes_disassembled >= self.margin && !self.finished {
// Add a jump to the first instruction after the prolog
emitter.add_thunk(thunk::jmp(instruction.next_instruction_address()));
emitter.add_thunk(thunk::jmp(instruction.next_ip() as usize));
self.finished = true;
}

if self.finished {
break;
}
}

Ok(Trampoline {
Expand All @@ -91,30 +117,16 @@ impl Builder {
})
}

/// Disassembles the next instruction and returns its properties.
unsafe fn next_instruction(&mut self) -> Result<Instruction> {
let instruction_address = self.target as usize + self.total_bytes_disassembled;

// Disassemble the next instruction
match Instruction::new(&mut self.disassembler, instruction_address as *const _) {
None => Err(Error::InvalidCode)?,
Some(instruction) => {
// Keep track of the total amount of bytes
self.total_bytes_disassembled += instruction.len();
Ok(instruction)
},
}
}

/// Returns an instruction after analysing and potentially modifies it.
unsafe fn process_instruction(
&mut self,
instruction: &Instruction,
instruction_bytes: &[u8],
) -> Result<Box<dyn pic::Thunkable>> {
if let Some(displacement) = instruction.rip_operand_displacement() {
return self.handle_rip_relative_instruction(instruction, displacement);
} else if let Some(displacement) = instruction.relative_branch_displacement() {
return self.handle_relative_branch(instruction, displacement);
if let Some(target) = instruction.rip_operand_target() {
return self.handle_rip_relative_instruction(instruction, instruction_bytes, target as usize);
} else if let Some(target) = instruction.relative_branch_target() {
return self.handle_relative_branch(instruction, instruction_bytes, target as usize);
} else if instruction.is_return() {
// In case the operand is not placed in a branch, the function
// returns unconditionally (i.e it terminates here).
Expand All @@ -123,7 +135,7 @@ impl Builder {

// The instruction does not use any position-dependant operands,
// therefore the bytes can be copied directly from source.
Ok(Box::new(instruction.as_slice().to_vec()))
Ok(Box::new(instruction_bytes.to_vec()))
}

/// Adjusts the offsets for RIP relative operands. They are only available
Expand All @@ -137,19 +149,23 @@ impl Builder {
unsafe fn handle_rip_relative_instruction(
&mut self,
instruction: &Instruction,
displacement: isize,
instruction_bytes: &[u8],
target: usize,
) -> Result<Box<dyn pic::Thunkable>> {
let displacement = target
.wrapping_sub(instruction.ip() as usize)
.wrapping_sub(instruction.len()) as isize;
// If the instruction is an unconditional jump, processing stops here
self.finished = instruction.is_unconditional_jump();

// Nothing should be done if `displacement` is within the prolog.
if (-(self.total_bytes_disassembled as isize)..0).contains(&displacement) {
return Ok(Box::new(instruction.as_slice().to_vec()));
return Ok(Box::new(instruction_bytes.to_vec()));
}

// These need to be captured by the closure
let instruction_address = instruction.address() as isize;
let instruction_bytes = instruction.as_slice().to_vec();
let instruction_address = instruction.ip() as isize;
let instruction_bytes = instruction_bytes.to_vec();

Ok(Box::new(pic::UnsafeThunk::new(
move |offset| {
Expand Down Expand Up @@ -179,13 +195,9 @@ impl Builder {
unsafe fn handle_relative_branch(
&mut self,
instruction: &Instruction,
displacement: isize,
instruction_bytes: &[u8],
destination_address_abs: usize,
) -> Result<Box<dyn pic::Thunkable>> {
// Calculate the absolute address of the target destination
let destination_address_abs = instruction
.next_instruction_address()
.wrapping_add(displacement as usize);

if instruction.is_call() {
// Calls are not an issue since they return to the original address
return Ok(thunk::call(destination_address_abs));
Expand All @@ -199,7 +211,7 @@ impl Builder {
if prolog_range.contains(&destination_address_abs) {
// Keep track of the jump's destination address
self.branch_address = Some(destination_address_abs);
Ok(Box::new(instruction.as_slice().to_vec()))
Ok(Box::new(instruction_bytes.to_vec()))
} else if instruction.is_loop() {
// Loops (e.g 'loopnz', 'jecxz') to the outside are not supported
Err(Error::UnsupportedInstruction)
Expand All @@ -212,8 +224,7 @@ impl Builder {
// Conditional jumps (Jcc)
// To extract the condition, the primary opcode is required. Short
// jumps are only one byte, but long jccs are prefixed with 0x0F.
let primary_opcode = instruction
.as_slice()
let primary_opcode = instruction_bytes
.iter()
.find(|op| **op != 0x0F)
.expect("retrieving conditional jump primary op code");
Expand All @@ -228,6 +239,6 @@ impl Builder {
fn is_instruction_in_branch(&self, instruction: &Instruction) -> bool {
self
.branch_address
.map_or(false, |offset| instruction.address() < offset)
.map_or(false, |offset| instruction.ip() < offset as u64)
}
}

0 comments on commit 19f2132

Please sign in to comment.