-
-
Notifications
You must be signed in to change notification settings - Fork 369
librasan: Support patching Thumb functions #3176
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
I made an attempt to write tests for this, but unfortunately, this is probably not possible, since calling a Thumb function with invalid instructions leads to a SIGABRT or SIGILL, which cannot easily be caught in a unit test. Compiling a function with Thumb instructions requires a nightly feature in Rust, but that on its own isn't a problem because we're already using nightly for tests. #![cfg_attr(target_arch = "arm", feature(arm_target_feature))]
#[cfg(test)]
#[cfg(feature = "libc")]
#[cfg(target_arch = "arm")]
mod tests {
use asan::{
GuestAddr, expect_panic,
mmap::{Mmap, MmapProt, linux::LinuxMmap},
patch::{Patch, raw::RawPatch},
};
use log::info;
#[unsafe(no_mangle)]
#[target_feature(enable = "thumb-mode")]
extern "C" fn test1(a1: usize, a2: usize, a3: usize, a4: usize, a5: usize, a6: usize) -> usize {
assert_eq!(a1, 1);
assert_eq!(a2, 2);
assert_eq!(a3, 3);
assert_eq!(a4, 4);
assert_eq!(a5, 5);
assert_eq!(a6, 6);
return 0xdeadface;
}
#[unsafe(no_mangle)]
extern "C" fn test2(a1: usize, a2: usize, a3: usize, a4: usize, a5: usize, a6: usize) -> usize {
assert_eq!(a1, 1);
assert_eq!(a2, 2);
assert_eq!(a3, 3);
assert_eq!(a4, 4);
assert_eq!(a5, 5);
assert_eq!(a6, 6);
return 0xd00df00d;
}
#[test]
fn test_patch() {
let ret1 = unsafe { test1(1, 2, 3, 4, 5, 6) };
assert_eq!(ret1, 0xdeadface);
let ret2 = test2(1, 2, 3, 4, 5, 6);
assert_eq!(ret2, 0xd00df00d);
let ptest1 = test1 as *const () as GuestAddr;
let ptest2 = test2 as *const () as GuestAddr;
info!("pfn: {:#x}", ptest1);
let aligned_pfn = ptest1 & !0xfff;
info!("aligned_pfn: {:#x}", aligned_pfn);
LinuxMmap::protect(
aligned_pfn,
0x4096,
MmapProt::READ | MmapProt::WRITE | MmapProt::EXEC,
)
.unwrap();
RawPatch::patch(ptest1, ptest2).unwrap();
// This will raise SIGABRT or SIGILL
unsafe { test1(1, 2, 3, 4, 5, 6) };
}
} |
Cool. Sounds good. Let's ignore the negative tests (ones we expect to crash) and just make sure we have tests to show we can hook both ARM and THUMB functions successfully. |
81946e1
to
42a4660
Compare
I was so focused on getting a failing test case that I completely forgot about actually writing a test case for the more important working cases. I have used some macros to avoid repetitiveness. |
@WorksButNotTested looks good? |
I think the changes in fn patch(target: GuestAddr, destination: GuestAddr) -> Result<(), Self::Error> {
debug!("patch - addr: {:#x}, target: {:#x}", target, destination);
if target == destination {
Err(RawPatchError::IdentityPatch(target))?;
}
let patch = Self::get_patch(destination)?;
trace!("patch: {:02x?}", patch);
trace!("patch: {:02x?}", patch);
/* Mask the thumb mode indicator bit */
#[cfg(target_arch = "arm")]
let target = target & !1;
let dest = unsafe { from_raw_parts_mut(target as *mut u8, patch.len()) };
dest.copy_from_slice(&patch);
Ok(())
}
#[cfg(target_arch = "arm")]
fn get_patch(destination: GuestAddr) -> Result<Vec<u8>, RawPatchError> {
// ldr ip, [pc, #2]
// bx ip
// .long 0xdeadface
/* If our target is in thumb mode */
if target & 1 == 1 {
let insns = [
[0xdf, 0xf8, 0x02, 0xc0].to_vec(),
[0x60, 0x47].to_vec(),
[0xce, 0xfa, 0xad, 0xde].to_vec(),
];
let addr = destination.to_ne_bytes().to_vec();
let insns_mod = [&insns[0], &insns[1], &addr];
Ok(insns_mod.into_iter().flatten().cloned().collect())
} else {
let insns = [
[0x00, 0xc0, 0x9f, 0xe5].to_vec(),
[0x1c, 0xff, 0x2f, 0xe1].to_vec(),
[0xce, 0xfa, 0xad, 0xde].to_vec(),
];
let addr = destination.to_ne_bytes().to_vec();
let insns_mod = [&insns[0], &insns[1], &addr];
Ok(insns_mod.into_iter().flatten().cloned().collect())
}
} |
Think I've got my head around the macros in the tests now. They look good. Only thing I'd change on that is that its probably worth resetting the page permissions after and probably worth modifying two pages rather than one in case the function ends up spanning a page boundary. But I made those some mistakes in my original implementation, so I can understand why they got copied! |
This makes sense. I'll also update the |
Should be good to go now |
Also, the page size was set to |
Are you happy to make these changes too? |
I like it better, but it does require passing the |
// bx ip | ||
// .long 0xdeadface | ||
let insns = [ | ||
[0xdf, 0xf8, 0x02, 0xc0].to_vec(), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do these things have to be vecs? I feel like it could be some static values
Description
ARMv7 has two instruction sets: Thumb2 and ARM. ARM CPUs switch to Thumb state when they jump to a target address if the least significant bit of the address is set to 1. While
libc
can be compiled with ARM or Thumb instructions entirely, mixed instruction sets are also possible. This can happen whenlibc
is compiled for ARM but has some handwritten Thumb assembly implementations for certain functions for performance reasons.The following command shows a
libc.so.6
where two symbols have an address with the least significant bit set to 1.This poses a problem with the current live-patching implementation, which assumes that all
libc
functions are always ARM functions. In the current situation, the live patcher writes the assembly trampoline consisting of ARM instructions into a function where Thumb instructions are expected, offset by a single byte. This obviously leads to incorrect behavior once such a function is called.This PR fixes that by checking whether the target address points to a Thumb functions and generates a Thumb trampoline which it writes to the correct address (i.e. address - 1).
The ARMv7-A manual also says the following.
Which means we have to use
bx ip
instead ofmov pc, ip
in Thumb state. For consistency and following the recommendation, I've also changed the ARM trampoline to usebx ip
.Checklist
./scripts/precommit.sh
and addressed all comments