diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 400d577..4910b91 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -6,3 +6,8 @@ updates: schedule: interval: "weekly" open-pull-requests-limit: 10 + - package-ecosystem: cargo + directory: "/" + schedule: + interval: "daily" + open-pull-requests-limit: 10 diff --git a/Cargo.lock b/Cargo.lock index e165fd6..902da49 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,19 +4,19 @@ version = 3 [[package]] name = "anyhow" -version = "1.0.80" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ad32ce52e4161730f7098c077cd2ed6229b5804ccf99e5366be1ab72a98b4e1" +checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" [[package]] name = "libc" -version = "0.2.153" +version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" [[package]] name = "plt-rs" -version = "0.2.0" +version = "0.3.0" dependencies = [ "anyhow", "libc", @@ -25,27 +25,27 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.78" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.35" +version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" dependencies = [ "proc-macro2", ] [[package]] name = "syn" -version = "2.0.52" +version = "2.0.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b699d15b36d1f02c3e7c69f8ffef53de37aefae075d8488d4ba1a7788d574a07" +checksum = "837a7e8026c6ce912ff01cefbe8cafc2f8010ac49682e2a3d9decc3bce1ecaaf" dependencies = [ "proc-macro2", "quote", @@ -54,18 +54,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.57" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e45bcbe8ed29775f228095caf2cd67af7a4ccf756ebff23a306bf3e8b47b24b" +checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.57" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81" +checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" dependencies = [ "proc-macro2", "quote", @@ -74,6 +74,6 @@ dependencies = [ [[package]] name = "unicode-ident" -version = "1.0.8" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" diff --git a/Cargo.toml b/Cargo.toml index e911170..0c89bb7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "plt-rs" -version = "0.2.0" +version = "0.3.0" edition = "2021" authors = ["ohchase"] license = "MIT" @@ -9,7 +9,7 @@ documentation = "https://docs.rs/plt-rs" readme = "README.md" repository = "https://github.com/ohchase/plt-rs/" homepage = "https://github.com/ohchase/plt-rs/" -keywords = ["plt", "elf", "bionic", "linker", "hooking"] +keywords = ["plt", "elf", "linker", "hook", "symbols"] exclude = ["/examples"] [lib] @@ -18,8 +18,8 @@ crate-type = ["lib"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -libc = "0.2.149" -thiserror = "1.0.49" +libc = "0.2" +thiserror = "1.0" [dev-dependencies] anyhow = "1.0.75" diff --git a/Cross.toml b/Cross.toml index f7229f8..32cb1da 100644 --- a/Cross.toml +++ b/Cross.toml @@ -4,4 +4,4 @@ image = "ghcr.io/cross-rs/aarch64-linux-android:main" [target.i686-linux-android] image = "ghcr.io/cross-rs/i686-linux-android:main" [target.x86_64-linux-android] -image = "ghcr.io/cross-rs/x86_64-linux-android:main" \ No newline at end of file +image = "ghcr.io/cross-rs/x86_64-linux-android:main" diff --git a/README.md b/README.md index 8f74122..981bc84 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# PLT-RS +# Plt-rs ## Change Notes ### 0.1.0 initial release @@ -6,9 +6,14 @@ - removed hooking functionality - reduced linux/android bloat - documented and generally made more ergonomic - -## Inspirations / Sources utilized -Projects I referenced while working on this. +### 0.3.0 usability +- promote finding function in dynamic library functionality to public +- added tests +- don't use patch version in dependencies +- ci/cd dependency updates + +## Inspired +Projects I referenced and was heavily inspired by while working on this. - [Plthook by Kubo] https://github.com/kubo/plthook - [Bhook by bytedance] https://github.com/bytedance/bhook @@ -18,26 +23,19 @@ Generally, PLT hooking is an ideal solution for hooking given you can guarantee This library does not do any inline hooking, so there is no architecture (i686, arm, etc.) specific assembly magic going on, making cross compatibility very easy. There IS architecture specific constants, but its very minimal. -## ... Unix Like? -Ye this library supports two target operating systems: Linux and Android; -So, android is unix like but has a separate linker implemention called Bionic. -At the ground floor, both are still linking shared objects in elf format; but -the library has to change how it crawls the dynamially loaded objects. - ## Why Video game modding, reverse engineering, etc - Can hook networking calls: recv / send -- Rendering calls: eglSwapBuffers / video game overlays +- Rendering calls: eglSwapBuffers / video game mods and overlays +- Application hardening and monitoring +- Defensive and Offensive usages ## Supports and tests against many targets +highlighted builds - ![i686-unknown-linux-gnu](https://github.com/ohchase/plt-rs/actions/workflows/i686-unknown-linux-gnu.yml/badge.svg) - ![x86_64-unknown-linux-gnu](https://github.com/ohchase/plt-rs/actions/workflows/x86_64-unknown-linux-gnu.yml/badge.svg) - ![aarch64-unknown-linux-gnu](https://github.com/ohchase/plt-rs/actions/workflows/aarch64-unknown-linux-gnu.yml/badge.svg) -- ![arm-unknown-linux-gnueabi](https://github.com/ohchase/plt-rs/actions/workflows/arm-unknown-linux-gnueabi.yml/badge.svg) -- ![i686-linux-android](https://github.com/ohchase/plt-rs/actions/workflows/i686-linux-android.yml/badge.svg) -- ![x86_64-linux-android](https://github.com/ohchase/plt-rs/actions/workflows/x86_64-linux-android.yml/badge.svg) - ![aarch64-linux-android](https://github.com/ohchase/plt-rs/actions/workflows/aarch64-linux-android.yml/badge.svg) -- ![arm-linux-androideabi](https://github.com/ohchase/plt-rs/actions/workflows/arm-linux-androideabi.yml/badge.svg) - ![armv7-linux-androideabi](https://github.com/ohchase/plt-rs/actions/workflows/armv7-linux-androideabi.yml/badge.svg) ## Show me da code @@ -52,49 +50,6 @@ unsafe fn getpid() -> u32 { 999 } -/// Finding target function differs on 32 bit and 64 bit. -/// On 64 bit we want to check the addended relocations table only, opposed to the addendless relocations table. -/// Additionally, we will fall back to the plt given it is an addended relocation table. -#[cfg(target_pointer_width = "64")] -fn try_find_function<'a>( - dyn_lib: &'a DynamicLibrary, - dyn_symbols: &'a DynamicSymbols, -) -> Option<&'a plt_rs::elf64::DynRela> { - let string_table = dyn_lib.string_table(); - if let Some(dyn_relas) = dyn_lib.addend_relocs() { - let dyn_relas = dyn_relas.entries().iter(); - if let Some(symbol) = dyn_relas - .flat_map(|e| { - dyn_symbols - .resolve_name(e.symbol_index() as usize, string_table) - .map(|s| (e, s)) - }) - .filter(|(_, s)| s.eq("getpid")) - .next() - .map(|(target_function, _)| target_function) - { - return Some(symbol); - } - } - - if let Some(dyn_relas) = dyn_lib.plt_rela() { - let dyn_relas = dyn_relas.entries().iter(); - if let Some(symbol) = dyn_relas - .flat_map(|e| { - dyn_symbols - .resolve_name(e.symbol_index() as usize, string_table) - .map(|s| (e, s)) - }) - .filter(|(_, s)| s.eq("getpid")) - .next() - .map(|(target_function, _)| target_function) - { - return Some(symbol); - } - } - return None; -} - fn main() -> Result<()> { let my_pid = unsafe { libc::getpid() }; println!("application pid is {my_pid}"); @@ -105,11 +60,8 @@ fn main() -> Result<()> { let dyn_lib = DynamicLibrary::initialize(executable_entry)?; println!("successfully initialied dynamic library for instrumentation"); - let dyn_symbols = dyn_lib - .symbols() - .ok_or(anyhow!("dynamic lib should have symbols"))?; let target_function = - try_find_function(&dyn_lib, &dyn_symbols).ok_or(anyhow!("unable to find getpid symbol"))?; + dyn_lib.try_find_function("getpid").ok_or(anyhow!("unable to find getpid symbol"))?; println!( "successfully identified libc getpid offset: {:#X?}", target_function.r_offset @@ -156,3 +108,8 @@ successfully identified libc getpid offset: 0x7E460 page start for function is 0x000061019c41b000 new pid is: 999 ``` + +## References / Inspirations +Projects I referenced and was heavily inspired by while working on this. +- [Plthook by Kubo] https://github.com/kubo/plthook +- [Bhook by bytedance] https://github.com/bytedance/bhook diff --git a/examples/dump_plt.rs b/examples/dump_plt.rs index 1fbb6f7..fd88ae4 100644 --- a/examples/dump_plt.rs +++ b/examples/dump_plt.rs @@ -3,6 +3,7 @@ use plt_rs::{collect_modules, DynamicLibrary, RelocationTable}; fn main() -> Result<()> { let entries = collect_modules(); + println!("collected modules"); for entry in entries.into_iter() { println!("[{:?}] Addr: {:#X?}", entry.name(), entry.addr()); diff --git a/examples/hook_getpid.rs b/examples/hook_getpid.rs index 76d1186..ad1673a 100644 --- a/examples/hook_getpid.rs +++ b/examples/hook_getpid.rs @@ -1,7 +1,7 @@ use anyhow::anyhow; use anyhow::Result; use libc::c_void; -use plt_rs::{collect_modules, DynamicLibrary, DynamicSymbols}; +use plt_rs::{collect_modules, DynamicLibrary}; unsafe fn getpid() -> u32 { 999 @@ -17,99 +17,16 @@ fn find_executable<'a>() -> Option> { /// Finding executable target differs on unix and android #[cfg(target_os = "android")] fn find_executable<'a>() -> Option> { + let executable = std::env::current_exe().expect("current exe"); + let file_stem = executable.file_stem()?; + let file_stem = file_stem.to_str()?; let loaded_modules = collect_modules(); loaded_modules .into_iter() - .filter(|lib| lib.name().contains("hook_getpid")) + .filter(|lib| lib.name().contains(file_stem)) .next() } -/// Finding target function differs on 32 bit and 64 bit. -/// On 32 bit we want to check the relocations table only, opposed to the addend relocations table. -/// Additionally, we will fall back to the plt given it is an addendless relocation table. -#[cfg(target_pointer_width = "32")] -fn try_find_function<'a>( - dyn_lib: &'a DynamicLibrary, - dyn_symbols: &'a DynamicSymbols, -) -> Option<&'a plt_rs::elf32::DynRel> { - let string_table = dyn_lib.string_table(); - if let Some(dyn_relas) = dyn_lib.relocs() { - let dyn_relas = dyn_relas.entries().iter(); - if let Some(symbol) = dyn_relas - .flat_map(|e| { - dyn_symbols - .resolve_name(e.symbol_index() as usize, string_table) - .map(|s| (e, s)) - }) - .filter(|(_, s)| s.eq("getpid")) - .next() - .map(|(target_function, _)| target_function) - { - return Some(symbol); - } - } - - if let Some(dyn_relas) = dyn_lib.plt_rel() { - let dyn_relas = dyn_relas.entries().iter(); - if let Some(symbol) = dyn_relas - .flat_map(|e| { - dyn_symbols - .resolve_name(e.symbol_index() as usize, string_table) - .map(|s| (e, s)) - }) - .filter(|(_, s)| s.eq("getpid")) - .next() - .map(|(target_function, _)| target_function) - { - return Some(symbol); - } - } - return None; -} - -/// Finding target function differs on 32 bit and 64 bit. -/// On 64 bit we want to check the addended relocations table only, opposed to the addendless relocations table. -/// Additionally, we will fall back to the plt given it is an addended relocation table. -#[cfg(target_pointer_width = "64")] -fn try_find_function<'a>( - dyn_lib: &'a DynamicLibrary, - dyn_symbols: &'a DynamicSymbols, -) -> Option<&'a plt_rs::elf64::DynRela> { - let string_table = dyn_lib.string_table(); - if let Some(dyn_relas) = dyn_lib.addend_relocs() { - let dyn_relas = dyn_relas.entries().iter(); - if let Some(symbol) = dyn_relas - .flat_map(|e| { - dyn_symbols - .resolve_name(e.symbol_index() as usize, string_table) - .map(|s| (e, s)) - }) - .filter(|(_, s)| s.eq("getpid")) - .next() - .map(|(target_function, _)| target_function) - { - return Some(symbol); - } - } - - if let Some(dyn_relas) = dyn_lib.plt_rela() { - let dyn_relas = dyn_relas.entries().iter(); - if let Some(symbol) = dyn_relas - .flat_map(|e| { - dyn_symbols - .resolve_name(e.symbol_index() as usize, string_table) - .map(|s| (e, s)) - }) - .filter(|(_, s)| s.eq("getpid")) - .next() - .map(|(target_function, _)| target_function) - { - return Some(symbol); - } - } - return None; -} - fn main() -> Result<()> { let my_pid = unsafe { libc::getpid() }; println!("application pid is {my_pid}"); @@ -120,11 +37,9 @@ fn main() -> Result<()> { let dyn_lib = DynamicLibrary::initialize(executable_entry)?; println!("successfully initialied dynamic library for instrumentation"); - let dyn_symbols = dyn_lib - .symbols() - .ok_or(anyhow!("dynamic lib should have symbols"))?; - let target_function = - try_find_function(&dyn_lib, &dyn_symbols).ok_or(anyhow!("unable to find getpid symbol"))?; + let target_function = dyn_lib + .try_find_function("getpid") + .ok_or(anyhow!("unable to find getpid symbol"))?; println!( "successfully identified libc getpid offset: {:#X?}", target_function.r_offset diff --git a/src/lib.rs b/src/lib.rs index 5c68ff3..f6d32a2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -450,6 +450,85 @@ impl<'a> DynamicLibrary<'a> { }) } + /// Finding target function differs on 32 bit and 64 bit. + /// On 32 bit we want to check the relocations table only, opposed to the addend relocations table. + /// Additionally, we will fall back to the plt given it is an addendless relocation table. + #[cfg(target_pointer_width = "32")] + pub fn try_find_function(&self, symbol_name: &str) -> Option<&'_ elf32::DynRel> { + let string_table = self.string_table(); + let dyn_symbols = self.symbols()?; + if let Some(dyn_relas) = self.relocs() { + let dyn_relas = dyn_relas.entries().iter(); + if let Some(symbol) = dyn_relas + .flat_map(|e| { + dyn_symbols + .resolve_name(e.symbol_index() as usize, string_table) + .map(|s| (e, s)) + }) + .filter(|(_, s)| s.eq(symbol_name)) + .next() + .map(|(target_function, _)| target_function) + { + return Some(symbol); + } + } + + if let Some(dyn_relas) = self.plt_rel() { + let dyn_relas = dyn_relas.entries().iter(); + if let Some(symbol) = dyn_relas + .flat_map(|e| { + dyn_symbols + .resolve_name(e.symbol_index() as usize, string_table) + .map(|s| (e, s)) + }) + .filter(|(_, s)| s.eq(symbol_name)) + .next() + .map(|(target_function, _)| target_function) + { + return Some(symbol); + } + } + None + } + + /// Finding target function differs on 32 bit and 64 bit. + /// On 64 bit we want to check the addended relocations table only, opposed to the addendless relocations table. + /// Additionally, we will fall back to the plt given it is an addended relocation table. + #[cfg(target_pointer_width = "64")] + pub fn try_find_function(&self, symbol_name: &str) -> Option<&'_ elf64::DynRela> { + let string_table = self.string_table(); + let symbols = self.symbols()?; + if let Some(dyn_relas) = self.addend_relocs() { + let dyn_relas = dyn_relas.entries().iter(); + if let Some(symbol) = dyn_relas + .flat_map(|e| { + symbols + .resolve_name(e.symbol_index() as usize, string_table) + .map(|s| (e, s)) + }) + .find(|(_, s)| s.eq(symbol_name)) + .map(|(target_function, _)| target_function) + { + return Some(symbol); + } + } + + if let Some(dyn_relas) = self.plt_rela() { + let dyn_relas = dyn_relas.entries().iter(); + if let Some(symbol) = dyn_relas + .flat_map(|e| { + symbols + .resolve_name(e.symbol_index() as usize, string_table) + .map(|s| (e, s)) + }) + .find(|(_, s)| s.eq(symbol_name)) + .map(|(target_function, _)| target_function) + { + return Some(symbol); + } + } + None + } /// Access the plt as a dynamic relocation table if possible /// can fail if the plt is not available or the plt is with addend pub fn plt_rel(&self) -> Option<&DynamicRelocations<'_>> { @@ -553,6 +632,12 @@ pub fn collect_modules<'a>() -> Vec> { // We have to copy sthe `dl_phdr_info` struct out, as the same memory buffer is used for // each entry during the iteration process. Otherwise we could have used a vector of // pointers. + println!("{} {}", dl_info.dlpi_addr, dl_info.dlpi_phnum); + + if dl_info.dlpi_phnum == 0 { + return; + } + let program_headers = unsafe { std::slice::from_raw_parts(dl_info.dlpi_phdr, dl_info.dlpi_phnum as usize) }; objs.push(LoadedLibrary { diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index baa1672..c6db566 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -1,5 +1,134 @@ +use libc::c_void; +use plt_rs::{collect_modules, DynamicLibrary, RelocationTable}; + +/// Make sure we can load all the modules we load ourselves +/// A simple sanity check, we are not checking the modules contents in any meaningful way. +/// But this works great to catch issues, because realistically we should never run into a issue parsing libraries. #[test] -fn can_load_own_link_map() {} +fn can_load_own_link_map() { + let entries = collect_modules(); + + for entry in entries.into_iter() { + if let Ok(dynamic_lib) = DynamicLibrary::initialize(entry) { + let dynamic_symbols = dynamic_lib.symbols().expect("symbols..."); + let string_table = dynamic_lib.string_table(); + if let Some(dyn_relas) = dynamic_lib.addend_relocs() { + dyn_relas + .entries() + .iter() + .flat_map(|e| { + dynamic_symbols.resolve_name(e.symbol_index() as usize, string_table) + }) + .filter(|s| !s.is_empty()) + .for_each(|s| println!("\t{}", s)); + } + + if let Some(dyn_relocs) = dynamic_lib.relocs() { + dyn_relocs + .entries() + .iter() + .flat_map(|e| { + dynamic_symbols.resolve_name(e.symbol_index() as usize, string_table) + }) + .filter(|s| !s.is_empty()) + .for_each(|s| println!("\t{}", s)); + } + + if let Some(plt) = dynamic_lib.plt() { + match plt { + RelocationTable::WithAddend(rel) => { + rel.entries() + .iter() + .flat_map(|e| { + dynamic_symbols + .resolve_name(e.symbol_index() as usize, string_table) + }) + .filter(|s| !s.is_empty()) + .for_each(|s| println!("\t{}", s)); + } + RelocationTable::WithoutAddend(rel) => { + rel.entries() + .iter() + .flat_map(|e| { + dynamic_symbols + .resolve_name(e.symbol_index() as usize, string_table) + }) + .filter(|s| !s.is_empty()) + .for_each(|s| println!("\t{}", s)); + } + } + } + } + } +} +unsafe fn getpid() -> u32 { + 999 +} + +/// Finding executable target differs on unix and android +#[cfg(target_os = "linux")] +fn find_executable<'a>() -> Option> { + let loaded_modules = collect_modules(); + loaded_modules.into_iter().next() +} + +/// Finding executable target differs on unix and android +#[cfg(target_os = "android")] +fn find_executable<'a>() -> Option> { + let executable = std::env::current_exe().expect("current exe"); + let file_stem = executable.file_stem()?; + let file_stem = file_stem.to_str()?; + let loaded_modules = collect_modules(); + loaded_modules + .into_iter() + .filter(|lib| lib.name().contains(file_stem)) + .next() +} #[test] -fn can_hook_getpid() {} +fn can_hook_getpid() { + let my_pid = unsafe { libc::getpid() }; + println!("application pid is {my_pid}"); + + let executable_entry = find_executable().expect("can find executable"); + println!("successfully identified executable"); + + let dyn_lib = DynamicLibrary::initialize(executable_entry).expect("can load"); + println!("successfully initialied dynamic library for instrumentation"); + + let target_function = dyn_lib + .try_find_function("getpid") + .expect("executable should link getpid"); + println!( + "successfully identified libc getpid offset: {:#X?}", + target_function.r_offset + ); + + let base_addr = dyn_lib.library().addr(); + let plt_fn_ptr = (base_addr + target_function.r_offset as usize) as *mut *mut libc::c_void; + let page_size = unsafe { libc::sysconf(libc::_SC_PAGE_SIZE) as usize }; + let plt_page = ((plt_fn_ptr as usize / page_size) * page_size) as *mut libc::c_void; + println!("page start for function is {plt_page:#X?}"); + + let _stored_address = unsafe { + // Set the memory page to read, write + let prot_res = libc::mprotect(plt_page, page_size, libc::PROT_WRITE | libc::PROT_READ); + if prot_res != 0 { + panic!("failed to set prot res"); + } + + // Replace the function address + let previous_address = std::ptr::replace(plt_fn_ptr, getpid as *mut _); + + // Set the memory page protection back to read only + let prot_res = libc::mprotect(plt_page, page_size, libc::PROT_READ); + if prot_res != 0 { + panic!("failed to set prot res"); + } + + previous_address as *const c_void + }; + + let get_pid = unsafe { libc::getpid() }; + assert_eq!(get_pid, 999) +}