├── .gitignore ├── Cross.toml ├── .github ├── dependabot.yml └── workflows │ ├── i686-linux-android.yml │ ├── x86_64-linux-android.yml │ ├── aarch64-linux-android.yml │ ├── arm-linux-androideabi.yml │ ├── i686-unknown-linux-gnu.yml │ ├── armv7-linux-androideabi.yml │ ├── aarch64-unknown-linux-gnu.yml │ ├── arm-unknown-linux-gnueabi.yml │ └── x86_64-unknown-linux-gnu.yml ├── Cargo.toml ├── LICENSE ├── Cargo.lock ├── src ├── elf32.rs ├── elf64.rs └── lib.rs ├── examples ├── dump_plt.rs └── hook_getpid.rs ├── README.md └── tests └── integration_tests.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | */target 3 | /bin -------------------------------------------------------------------------------- /Cross.toml: -------------------------------------------------------------------------------- 1 | # Cross.toml -> https://github.com/cross-rs/cross/wiki/Configuration#config-file 2 | [target.aarch64-linux-android] 3 | image = "ghcr.io/cross-rs/aarch64-linux-android:main" 4 | [target.i686-linux-android] 5 | image = "ghcr.io/cross-rs/i686-linux-android:main" 6 | [target.x86_64-linux-android] 7 | image = "ghcr.io/cross-rs/x86_64-linux-android:main" 8 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | # Check for updates every Monday 6 | schedule: 7 | interval: "weekly" 8 | open-pull-requests-limit: 10 9 | - package-ecosystem: cargo 10 | directory: "/" 11 | schedule: 12 | interval: "daily" 13 | open-pull-requests-limit: 10 14 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "plt-rs" 3 | version = "0.3.1" 4 | edition = "2021" 5 | authors = ["ohchase"] 6 | license = "MIT" 7 | description = "Library for inspecting, analyzing, and instrumenting linux and android applications runtime symbol linkage" 8 | documentation = "https://docs.rs/plt-rs" 9 | readme = "README.md" 10 | repository = "https://github.com/ohchase/plt-rs/" 11 | homepage = "https://github.com/ohchase/plt-rs/" 12 | keywords = ["plt", "elf", "linker", "hook", "symbols"] 13 | exclude = ["/examples"] 14 | 15 | [lib] 16 | crate-type = ["lib"] 17 | 18 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 19 | 20 | [dependencies] 21 | libc = "0.2" 22 | thiserror = "2.0" 23 | 24 | [dev-dependencies] 25 | anyhow = "1.0.100" 26 | -------------------------------------------------------------------------------- /.github/workflows/i686-linux-android.yml: -------------------------------------------------------------------------------- 1 | # We could use `@actions-rs/cargo` Action ability to automatically install `cross` tool 2 | # in order to compile our application for some unusual targets. 3 | 4 | on: [push, pull_request] 5 | 6 | name: i686-linux-android 7 | 8 | jobs: 9 | build: 10 | name: Build 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | target: 15 | - i686-linux-android 16 | 17 | continue-on-error: true 18 | 19 | steps: 20 | - uses: actions/checkout@v6 21 | - uses: actions-rs/toolchain@v1 22 | with: 23 | toolchain: stable 24 | target: ${{ matrix.target }} 25 | override: true 26 | - uses: actions-rs/cargo@v1 27 | with: 28 | use-cross: true 29 | command: build 30 | args: --target=${{ matrix.target }} 31 | - uses: actions-rs/cargo@v1 32 | with: 33 | use-cross: true 34 | command: test 35 | args: --target=${{ matrix.target }} 36 | -------------------------------------------------------------------------------- /.github/workflows/x86_64-linux-android.yml: -------------------------------------------------------------------------------- 1 | # We could use `@actions-rs/cargo` Action ability to automatically install `cross` tool 2 | # in order to compile our application for some unusual targets. 3 | 4 | on: [push, pull_request] 5 | 6 | name: x86_64-linux-android 7 | 8 | jobs: 9 | build: 10 | name: Build 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | target: 15 | - x86_64-linux-android 16 | 17 | continue-on-error: true 18 | 19 | steps: 20 | - uses: actions/checkout@v6 21 | - uses: actions-rs/toolchain@v1 22 | with: 23 | toolchain: stable 24 | target: ${{ matrix.target }} 25 | override: true 26 | - uses: actions-rs/cargo@v1 27 | with: 28 | use-cross: true 29 | command: build 30 | args: --target=${{ matrix.target }} 31 | - uses: actions-rs/cargo@v1 32 | with: 33 | use-cross: true 34 | command: test 35 | args: --target=${{ matrix.target }} 36 | -------------------------------------------------------------------------------- /.github/workflows/aarch64-linux-android.yml: -------------------------------------------------------------------------------- 1 | # We could use `@actions-rs/cargo` Action ability to automatically install `cross` tool 2 | # in order to compile our application for some unusual targets. 3 | 4 | on: [push, pull_request] 5 | 6 | name: aarch64-linux-android 7 | 8 | jobs: 9 | build: 10 | name: Build 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | target: 15 | - aarch64-linux-android 16 | 17 | continue-on-error: true 18 | 19 | steps: 20 | - uses: actions/checkout@v6 21 | - uses: actions-rs/toolchain@v1 22 | with: 23 | toolchain: stable 24 | target: ${{ matrix.target }} 25 | override: true 26 | - uses: actions-rs/cargo@v1 27 | with: 28 | use-cross: true 29 | command: build 30 | args: --target=${{ matrix.target }} 31 | - uses: actions-rs/cargo@v1 32 | with: 33 | use-cross: true 34 | command: test 35 | args: --target=${{ matrix.target }} 36 | -------------------------------------------------------------------------------- /.github/workflows/arm-linux-androideabi.yml: -------------------------------------------------------------------------------- 1 | # We could use `@actions-rs/cargo` Action ability to automatically install `cross` tool 2 | # in order to compile our application for some unusual targets. 3 | 4 | on: [push, pull_request] 5 | 6 | name: arm-linux-androideabi 7 | 8 | jobs: 9 | build: 10 | name: Build 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | target: 15 | - arm-linux-androideabi 16 | 17 | continue-on-error: true 18 | 19 | steps: 20 | - uses: actions/checkout@v6 21 | - uses: actions-rs/toolchain@v1 22 | with: 23 | toolchain: stable 24 | target: ${{ matrix.target }} 25 | override: true 26 | - uses: actions-rs/cargo@v1 27 | with: 28 | use-cross: true 29 | command: build 30 | args: --target=${{ matrix.target }} 31 | - uses: actions-rs/cargo@v1 32 | with: 33 | use-cross: true 34 | command: test 35 | args: --target=${{ matrix.target }} 36 | -------------------------------------------------------------------------------- /.github/workflows/i686-unknown-linux-gnu.yml: -------------------------------------------------------------------------------- 1 | # We could use `@actions-rs/cargo` Action ability to automatically install `cross` tool 2 | # in order to compile our application for some unusual targets. 3 | 4 | on: [push, pull_request] 5 | 6 | name: i686-unknown-linux-gnu 7 | 8 | jobs: 9 | build: 10 | name: Build 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | target: 15 | - i686-unknown-linux-gnu 16 | 17 | continue-on-error: true 18 | 19 | steps: 20 | - uses: actions/checkout@v6 21 | - uses: actions-rs/toolchain@v1 22 | with: 23 | toolchain: stable 24 | target: ${{ matrix.target }} 25 | override: true 26 | - uses: actions-rs/cargo@v1 27 | with: 28 | use-cross: true 29 | command: build 30 | args: --target=${{ matrix.target }} 31 | - uses: actions-rs/cargo@v1 32 | with: 33 | use-cross: true 34 | command: test 35 | args: --target=${{ matrix.target }} 36 | -------------------------------------------------------------------------------- /.github/workflows/armv7-linux-androideabi.yml: -------------------------------------------------------------------------------- 1 | # We could use `@actions-rs/cargo` Action ability to automatically install `cross` tool 2 | # in order to compile our application for some unusual targets. 3 | 4 | on: [push, pull_request] 5 | 6 | name: armv7-linux-androideabi 7 | 8 | jobs: 9 | build: 10 | name: Build 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | target: 15 | - armv7-linux-androideabi 16 | 17 | continue-on-error: true 18 | 19 | steps: 20 | - uses: actions/checkout@v6 21 | - uses: actions-rs/toolchain@v1 22 | with: 23 | toolchain: stable 24 | target: ${{ matrix.target }} 25 | override: true 26 | - uses: actions-rs/cargo@v1 27 | with: 28 | use-cross: true 29 | command: build 30 | args: --target=${{ matrix.target }} 31 | - uses: actions-rs/cargo@v1 32 | with: 33 | use-cross: true 34 | command: test 35 | args: --target=${{ matrix.target }} 36 | -------------------------------------------------------------------------------- /.github/workflows/aarch64-unknown-linux-gnu.yml: -------------------------------------------------------------------------------- 1 | # We could use `@actions-rs/cargo` Action ability to automatically install `cross` tool 2 | # in order to compile our application for some unusual targets. 3 | 4 | on: [push, pull_request] 5 | 6 | name: aarch64-unknown-linux-gnu 7 | 8 | jobs: 9 | build: 10 | name: Build 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | target: 15 | - aarch64-unknown-linux-gnu 16 | 17 | continue-on-error: true 18 | 19 | steps: 20 | - uses: actions/checkout@v6 21 | - uses: actions-rs/toolchain@v1 22 | with: 23 | toolchain: stable 24 | target: ${{ matrix.target }} 25 | override: true 26 | - uses: actions-rs/cargo@v1 27 | with: 28 | use-cross: true 29 | command: build 30 | args: --target=${{ matrix.target }} 31 | - uses: actions-rs/cargo@v1 32 | with: 33 | use-cross: true 34 | command: test 35 | args: --target=${{ matrix.target }} 36 | -------------------------------------------------------------------------------- /.github/workflows/arm-unknown-linux-gnueabi.yml: -------------------------------------------------------------------------------- 1 | # We could use `@actions-rs/cargo` Action ability to automatically install `cross` tool 2 | # in order to compile our application for some unusual targets. 3 | 4 | on: [push, pull_request] 5 | 6 | name: arm-unknown-linux-gnueabi 7 | 8 | jobs: 9 | build: 10 | name: Build 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | target: 15 | - arm-unknown-linux-gnueabi 16 | 17 | continue-on-error: true 18 | 19 | steps: 20 | - uses: actions/checkout@v6 21 | - uses: actions-rs/toolchain@v1 22 | with: 23 | toolchain: stable 24 | target: ${{ matrix.target }} 25 | override: true 26 | - uses: actions-rs/cargo@v1 27 | with: 28 | use-cross: true 29 | command: build 30 | args: --target=${{ matrix.target }} 31 | - uses: actions-rs/cargo@v1 32 | with: 33 | use-cross: true 34 | command: test 35 | args: --target=${{ matrix.target }} 36 | -------------------------------------------------------------------------------- /.github/workflows/x86_64-unknown-linux-gnu.yml: -------------------------------------------------------------------------------- 1 | # We could use `@actions-rs/cargo` Action ability to automatically install `cross` tool 2 | # in order to compile our application for some unusual targets. 3 | 4 | on: [push, pull_request] 5 | 6 | name: x86_64-unknown-linux-gnu 7 | 8 | jobs: 9 | build: 10 | name: Build 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | target: 15 | - x86_64-unknown-linux-gnu 16 | 17 | continue-on-error: true 18 | 19 | steps: 20 | - uses: actions/checkout@v6 21 | - uses: actions-rs/toolchain@v1 22 | with: 23 | toolchain: stable 24 | target: ${{ matrix.target }} 25 | override: true 26 | - uses: actions-rs/cargo@v1 27 | with: 28 | use-cross: true 29 | command: build 30 | args: --target=${{ matrix.target }} 31 | - uses: actions-rs/cargo@v1 32 | with: 33 | use-cross: true 34 | command: test 35 | args: --target=${{ matrix.target }} 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 ohchase 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "anyhow" 7 | version = "1.0.100" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" 10 | 11 | [[package]] 12 | name = "libc" 13 | version = "0.2.178" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" 16 | 17 | [[package]] 18 | name = "plt-rs" 19 | version = "0.3.1" 20 | dependencies = [ 21 | "anyhow", 22 | "libc", 23 | "thiserror", 24 | ] 25 | 26 | [[package]] 27 | name = "proc-macro2" 28 | version = "1.0.86" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" 31 | dependencies = [ 32 | "unicode-ident", 33 | ] 34 | 35 | [[package]] 36 | name = "quote" 37 | version = "1.0.36" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" 40 | dependencies = [ 41 | "proc-macro2", 42 | ] 43 | 44 | [[package]] 45 | name = "syn" 46 | version = "2.0.87" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" 49 | dependencies = [ 50 | "proc-macro2", 51 | "quote", 52 | "unicode-ident", 53 | ] 54 | 55 | [[package]] 56 | name = "thiserror" 57 | version = "2.0.17" 58 | source = "registry+https://github.com/rust-lang/crates.io-index" 59 | checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" 60 | dependencies = [ 61 | "thiserror-impl", 62 | ] 63 | 64 | [[package]] 65 | name = "thiserror-impl" 66 | version = "2.0.17" 67 | source = "registry+https://github.com/rust-lang/crates.io-index" 68 | checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" 69 | dependencies = [ 70 | "proc-macro2", 71 | "quote", 72 | "syn", 73 | ] 74 | 75 | [[package]] 76 | name = "unicode-ident" 77 | version = "1.0.12" 78 | source = "registry+https://github.com/rust-lang/crates.io-index" 79 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 80 | -------------------------------------------------------------------------------- /src/elf32.rs: -------------------------------------------------------------------------------- 1 | use super::DynamicSectionType; 2 | 3 | pub type Word = libc::Elf32_Word; 4 | // manual impl, signed word is i32; 5 | pub type SignedWord = i32; 6 | pub type Half = libc::Elf32_Half; 7 | pub type Addr = libc::Elf32_Addr; 8 | pub type ProgramHeader = libc::Elf32_Phdr; 9 | 10 | #[repr(C)] 11 | #[derive(Debug)] 12 | pub struct DynEntry { 13 | pub d_tag: self::Word, 14 | /// Either a value (Elf64_Xword) or an address (Elf64_Addr) 15 | pub d_val_ptr: self::Word, 16 | } 17 | 18 | #[repr(C)] 19 | #[derive(Debug)] 20 | pub struct DynSym { 21 | pub st_name: self::Word, 22 | pub st_value: self::Addr, 23 | pub st_size: self::Word, 24 | pub st_info: u8, 25 | pub st_other: u8, 26 | pub st_shndx: self::Half, 27 | } 28 | 29 | #[repr(C)] 30 | #[derive(Debug)] 31 | pub struct DynRel { 32 | pub r_offset: self::Addr, 33 | pub r_info: self::Word, 34 | } 35 | 36 | impl DynRel { 37 | pub fn symbol_index(&self) -> self::Word { 38 | (self.r_info >> 8) as self::Word 39 | } 40 | pub fn symbol_type(&self) -> self::Word { 41 | (self.r_info & 0x0ff) as self::Word 42 | } 43 | } 44 | 45 | #[repr(C)] 46 | #[derive(Debug)] 47 | pub struct DynRela { 48 | pub r_offset: self::Addr, 49 | pub r_info: self::Word, 50 | pub r_addend: self::SignedWord, 51 | } 52 | 53 | impl DynRela { 54 | pub fn symbol_index(&self) -> self::Word { 55 | (self.r_info >> 8) as self::Word 56 | } 57 | pub fn symbol_type(&self) -> self::Word { 58 | (self.r_info & 0x0ff) as self::Word 59 | } 60 | } 61 | 62 | /// An unknown Dynamic Section Type was observed 63 | #[derive(Debug, thiserror::Error)] 64 | #[error("Unknown Dynamic section type witnessed: `{0}`")] 65 | pub struct DynTypeError(self::Word); 66 | 67 | impl TryFrom for DynamicSectionType { 68 | type Error = DynTypeError; 69 | fn try_from(value: self::Word) -> Result { 70 | use DynamicSectionType::*; 71 | Ok(match value { 72 | 0 => DT_NULL, 73 | 74 | 2 => DT_PLTRELSZ, 75 | 3 => DT_PLTGOT, 76 | 20 => DT_PLTREL, 77 | 78 | 5 => DT_STRTAB, 79 | 6 => DT_SYMTAB, 80 | 11 => DT_SYMENT, 81 | 82 | 17 => DT_REL, 83 | 18 => DT_RELSZ, 84 | 19 => DT_RELENT, 85 | 86 | 7 => DT_RELA, 87 | 8 => DT_RELASZ, 88 | 9 => DT_RELAENT, 89 | 90 | 10 => DT_STRSZ, 91 | 23 => DT_JMPREL, 92 | 93 | tag => return Err(DynTypeError(tag)), 94 | }) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/elf64.rs: -------------------------------------------------------------------------------- 1 | use super::DynamicSectionType; 2 | 3 | pub type Word = libc::Elf64_Word; 4 | pub type Half = libc::Elf64_Half; 5 | pub type Addr = libc::Elf64_Addr; 6 | pub type ExtendedWord = libc::Elf64_Xword; 7 | 8 | // manual impl doesn't exist everywhere 9 | pub type ExtendedSignedWord = i64; 10 | 11 | pub type ProgramHeader = libc::Elf64_Phdr; 12 | 13 | #[repr(C)] 14 | #[derive(Debug)] 15 | pub struct DynEntry { 16 | pub d_tag: libc::Elf64_Xword, 17 | /// Either a value (Elf64_Xword) or an address (Elf64_Addr) 18 | pub d_val_ptr: libc::Elf64_Xword, 19 | } 20 | 21 | #[repr(C)] 22 | #[derive(Debug)] 23 | pub struct DynSym { 24 | pub st_name: self::Word, 25 | pub st_info: u8, 26 | pub st_other: u8, 27 | pub st_shndx: self::Half, 28 | pub st_value: self::Addr, 29 | pub st_size: self::ExtendedWord, 30 | } 31 | 32 | #[repr(C)] 33 | #[derive(Debug)] 34 | pub struct DynRel { 35 | pub r_offset: self::Addr, 36 | pub r_info: self::ExtendedWord, 37 | } 38 | 39 | impl DynRel { 40 | pub fn symbol_index(&self) -> self::Word { 41 | (self.r_info >> 32) as self::Word 42 | } 43 | pub fn symbol_type(&self) -> self::Word { 44 | (self.r_info & 0xffffffff) as self::Word 45 | } 46 | } 47 | 48 | #[repr(C)] 49 | #[derive(Debug)] 50 | pub struct DynRela { 51 | pub r_offset: self::Addr, 52 | pub r_info: self::ExtendedWord, 53 | pub r_addend: self::ExtendedSignedWord, 54 | } 55 | 56 | impl DynRela { 57 | pub fn symbol_index(&self) -> self::Word { 58 | (self.r_info >> 32) as self::Word 59 | } 60 | pub fn symbol_type(&self) -> self::Word { 61 | (self.r_info & 0xffffffff) as self::Word 62 | } 63 | } 64 | 65 | /// An unknown Dynamic Section Type was observed 66 | #[derive(Debug, thiserror::Error)] 67 | #[error("Unknown Dynamic section type witnessed: `{0}`")] 68 | pub struct DynTypeError(self::ExtendedWord); 69 | 70 | impl TryFrom for DynamicSectionType { 71 | type Error = DynTypeError; 72 | fn try_from(value: self::ExtendedWord) -> Result { 73 | use DynamicSectionType::*; 74 | Ok(match value { 75 | 0 => DT_NULL, 76 | 77 | 2 => DT_PLTRELSZ, 78 | 3 => DT_PLTGOT, 79 | 20 => DT_PLTREL, 80 | 81 | 5 => DT_STRTAB, 82 | 6 => DT_SYMTAB, 83 | 11 => DT_SYMENT, 84 | 85 | 17 => DT_REL, 86 | 18 => DT_RELSZ, 87 | 19 => DT_RELENT, 88 | 89 | 7 => DT_RELA, 90 | 8 => DT_RELASZ, 91 | 9 => DT_RELAENT, 92 | 93 | 10 => DT_STRSZ, 94 | 23 => DT_JMPREL, 95 | 96 | tag => return Err(DynTypeError(tag)), 97 | }) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /examples/dump_plt.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use plt_rs::{collect_modules, DynamicLibrary, RelocationTable}; 3 | 4 | fn main() -> Result<()> { 5 | let entries = collect_modules(); 6 | println!("collected {} modules", entries.len()); 7 | 8 | for entry in entries.into_iter() { 9 | let entry_name = entry.name().to_owned(); 10 | println!("[{}] Addr: {:#X?}", entry_name, entry.addr()); 11 | 12 | let Ok(dynamic_lib) = DynamicLibrary::initialize(entry) else { 13 | println!( 14 | "failed to parse {} as dynamic library, skipping...", 15 | entry_name 16 | ); 17 | continue; 18 | }; 19 | 20 | let Some(dynamic_symbols) = dynamic_lib.symbols() else { 21 | println!("failed to retrieve dynamic symbols, skipping..."); 22 | continue; 23 | }; 24 | let string_table = dynamic_lib.string_table(); 25 | 26 | println!("dynamic addend relocations:"); 27 | if let Some(dyn_relas) = dynamic_lib.addend_relocs() { 28 | dyn_relas 29 | .entries() 30 | .iter() 31 | .flat_map(|e| dynamic_symbols.resolve_name(e.symbol_index() as usize, string_table)) 32 | .filter(|s| !s.is_empty()) 33 | .for_each(|s| println!("\t{}", s)); 34 | } 35 | 36 | println!("dynamic relocations:"); 37 | if let Some(dyn_relocs) = dynamic_lib.relocs() { 38 | dyn_relocs 39 | .entries() 40 | .iter() 41 | .flat_map(|e| dynamic_symbols.resolve_name(e.symbol_index() as usize, string_table)) 42 | .filter(|s| !s.is_empty()) 43 | .for_each(|s| println!("\t{}", s)); 44 | } 45 | 46 | println!("plt:"); 47 | if let Some(plt) = dynamic_lib.plt() { 48 | match plt { 49 | RelocationTable::WithAddend(rel) => { 50 | rel.entries() 51 | .iter() 52 | .flat_map(|e| { 53 | dynamic_symbols.resolve_name(e.symbol_index() as usize, string_table) 54 | }) 55 | .filter(|s| !s.is_empty()) 56 | .for_each(|s| println!("\t{}", s)); 57 | } 58 | RelocationTable::WithoutAddend(rel) => { 59 | rel.entries() 60 | .iter() 61 | .flat_map(|e| { 62 | dynamic_symbols.resolve_name(e.symbol_index() as usize, string_table) 63 | }) 64 | .filter(|s| !s.is_empty()) 65 | .for_each(|s| println!("\t{}", s)); 66 | } 67 | } 68 | } 69 | println!(); 70 | } 71 | 72 | Ok(()) 73 | } 74 | -------------------------------------------------------------------------------- /examples/hook_getpid.rs: -------------------------------------------------------------------------------- 1 | use anyhow::anyhow; 2 | use anyhow::Result; 3 | use libc::c_void; 4 | use plt_rs::{collect_modules, DynamicLibrary}; 5 | 6 | unsafe fn getpid() -> u32 { 7 | 999 8 | } 9 | 10 | /// Finding executable target differs on unix and android 11 | #[cfg(target_os = "linux")] 12 | fn find_executable<'a>() -> Option> { 13 | let loaded_modules = collect_modules(); 14 | loaded_modules.into_iter().next() 15 | } 16 | 17 | /// Finding executable target differs on unix and android 18 | #[cfg(target_os = "android")] 19 | fn find_executable<'a>() -> Option> { 20 | let executable = std::env::current_exe().expect("current exe"); 21 | let file_stem = executable.file_stem()?; 22 | let file_stem = file_stem.to_str()?; 23 | let loaded_modules = collect_modules(); 24 | loaded_modules 25 | .into_iter() 26 | .filter(|lib| lib.name().contains(file_stem)) 27 | .next() 28 | } 29 | 30 | fn main() -> Result<()> { 31 | let my_pid = unsafe { libc::getpid() }; 32 | println!("application pid is {my_pid}"); 33 | 34 | let executable_entry = find_executable().ok_or(anyhow!("unable to find target executable"))?; 35 | println!("successfully identified executable"); 36 | 37 | let dyn_lib = DynamicLibrary::initialize(executable_entry)?; 38 | println!("successfully initialied dynamic library for instrumentation"); 39 | 40 | let target_function = dyn_lib 41 | .try_find_function("getpid") 42 | .ok_or(anyhow!("unable to find getpid symbol"))?; 43 | println!( 44 | "successfully identified libc getpid offset: {:#X?}", 45 | target_function.r_offset 46 | ); 47 | 48 | let base_addr = dyn_lib.library().addr(); 49 | let plt_fn_ptr = (base_addr + target_function.r_offset as usize) as *mut *mut c_void; 50 | let page_size = unsafe { libc::sysconf(libc::_SC_PAGE_SIZE) as usize }; 51 | let plt_page = ((plt_fn_ptr as usize / page_size) * page_size) as *mut c_void; 52 | println!("page start for function is {plt_page:#X?}"); 53 | 54 | let _stored_address = unsafe { 55 | // Set the memory page to read, write 56 | let prot_res = libc::mprotect(plt_page, page_size, libc::PROT_WRITE | libc::PROT_READ); 57 | if prot_res != 0 { 58 | println!("protection res: {prot_res}"); 59 | return Err(anyhow!("mprotect to rw")); 60 | } 61 | 62 | // Replace the function address 63 | let previous_address = std::ptr::replace(plt_fn_ptr, getpid as *mut _); 64 | 65 | // Set the memory page protection back to read only 66 | let prot_res = libc::mprotect(plt_page, page_size, libc::PROT_READ); 67 | if prot_res != 0 { 68 | return Err(anyhow!("mprotect to r")); 69 | } 70 | 71 | previous_address as *const c_void 72 | }; 73 | 74 | let get_pid = unsafe { libc::getpid() }; 75 | println!("new pid is: {get_pid}"); 76 | 77 | Ok(()) 78 | } 79 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Plt-rs 2 | ![Crates.io](https://img.shields.io/crates/v/plt-rs) 3 | ![Crates.io License](https://img.shields.io/crates/l/plt-rs) 4 | 5 | ## Overview 6 | By crawling the dynamically loaded objects of an executable we can hook exported functions. 7 | Generally, PLT hooking is an ideal solution for hooking given you can guarantee a Unix Like environment. 8 | This library does not do any inline hooking, so there is no architecture (i686, arm, etc.) specific assembly magic going on, 9 | making cross compatibility very easy. There IS architecture specific constants, but its very minimal. 10 | 11 | ## Why 12 | Video game modding, reverse engineering, etc 13 | - Can hook networking calls: recv / send 14 | - Rendering calls: eglSwapBuffers / video game mods and overlays 15 | - Application hardening and monitoring 16 | - Defensive and Offensive usages 17 | 18 | ## Supports and tests against many targets 19 | highlighted builds 20 | - ![i686-unknown-linux-gnu](https://github.com/ohchase/plt-rs/actions/workflows/i686-unknown-linux-gnu.yml/badge.svg) 21 | - ![x86_64-unknown-linux-gnu](https://github.com/ohchase/plt-rs/actions/workflows/x86_64-unknown-linux-gnu.yml/badge.svg) 22 | - ![aarch64-unknown-linux-gnu](https://github.com/ohchase/plt-rs/actions/workflows/aarch64-unknown-linux-gnu.yml/badge.svg) 23 | - ![aarch64-linux-android](https://github.com/ohchase/plt-rs/actions/workflows/aarch64-linux-android.yml/badge.svg) 24 | - ![armv7-linux-androideabi](https://github.com/ohchase/plt-rs/actions/workflows/armv7-linux-androideabi.yml/badge.svg) 25 | 26 | ## Worked Example hooking `getpid` 27 | Here we are hooking our own executables usages of libc getpid. 28 | Refer to `examples/hook_getpid.rs` for the full example, supporting android and 32 bit. 29 | A good chunk of the code is for the actual pointer replacement to hook the function! 30 | 31 | ```rust 32 | 33 | /// our own get pid function 34 | unsafe fn getpid() -> u32 { 35 | 999 36 | } 37 | 38 | fn main() -> Result<()> { 39 | let my_pid = unsafe { libc::getpid() }; 40 | println!("application pid is {my_pid}"); 41 | 42 | let executable_entry = find_executable().ok_or(anyhow!("unable to find target executable"))?; 43 | println!("successfully identified executable"); 44 | 45 | let dyn_lib = DynamicLibrary::initialize(executable_entry)?; 46 | println!("successfully initialied dynamic library for instrumentation"); 47 | 48 | let target_function = 49 | dyn_lib.try_find_function("getpid").ok_or(anyhow!("unable to find getpid symbol"))?; 50 | println!( 51 | "successfully identified libc getpid offset: {:#X?}", 52 | target_function.r_offset 53 | ); 54 | 55 | let base_addr = dyn_lib.library().addr(); 56 | let plt_fn_ptr = (base_addr + target_function.r_offset as usize) as *mut *mut c_void; 57 | let page_size = unsafe { libc::sysconf(libc::_SC_PAGE_SIZE) as usize }; 58 | let plt_page = ((plt_fn_ptr as usize / page_size) * page_size) as *mut c_void; 59 | println!("page start for function is {plt_page:#X?}"); 60 | 61 | let _stored_address = unsafe { 62 | // Set the memory page to read, write 63 | let prot_res = libc::mprotect(plt_page, page_size, libc::PROT_WRITE | libc::PROT_READ); 64 | if prot_res != 0 { 65 | println!("protection res: {prot_res}"); 66 | return Err(anyhow!("mprotect to rw")); 67 | } 68 | 69 | // Replace the function address 70 | let previous_address = std::ptr::replace(plt_fn_ptr, getpid as *mut _); 71 | 72 | // Set the memory page protection back to read only 73 | let prot_res = libc::mprotect(plt_page, page_size, libc::PROT_READ); 74 | if prot_res != 0 { 75 | return Err(anyhow!("mprotect to r")); 76 | } 77 | 78 | previous_address as *const c_void 79 | }; 80 | 81 | let get_pid = unsafe { libc::getpid() }; 82 | println!("new pid is: {get_pid}"); 83 | 84 | Ok(()) 85 | } 86 | ``` 87 | 88 | ```terminal 89 | application pid is 127765 90 | successfully identified executable 91 | successfully initialied dynamic library for instrumentation 92 | successfully identified libc getpid offset: 0x7E460 93 | page start for function is 0x000061019c41b000 94 | new pid is: 999 95 | ``` 96 | 97 | ## References / Inspirations 98 | Projects I referenced and was heavily inspired by while working on this. 99 | - [Plthook by Kubo] https://github.com/kubo/plthook 100 | - [Bhook by bytedance] https://github.com/bytedance/bhook 101 | -------------------------------------------------------------------------------- /tests/integration_tests.rs: -------------------------------------------------------------------------------- 1 | use libc::c_void; 2 | use plt_rs::{collect_modules, DynamicLibrary, RelocationTable}; 3 | 4 | /// Make sure we can load all the modules we load ourselves 5 | /// A simple sanity check, we are not checking the modules contents in any meaningful way. 6 | /// But this works great to catch issues, because realistically we should never run into a issue parsing libraries. 7 | #[test] 8 | fn can_load_own_link_map() { 9 | let entries = collect_modules(); 10 | 11 | for entry in entries.into_iter() { 12 | if let Ok(dynamic_lib) = DynamicLibrary::initialize(entry) { 13 | let dynamic_symbols = dynamic_lib.symbols().expect("symbols..."); 14 | let string_table = dynamic_lib.string_table(); 15 | if let Some(dyn_relas) = dynamic_lib.addend_relocs() { 16 | dyn_relas 17 | .entries() 18 | .iter() 19 | .flat_map(|e| { 20 | dynamic_symbols.resolve_name(e.symbol_index() as usize, string_table) 21 | }) 22 | .filter(|s| !s.is_empty()) 23 | .for_each(|s| println!("\t{}", s)); 24 | } 25 | 26 | if let Some(dyn_relocs) = dynamic_lib.relocs() { 27 | dyn_relocs 28 | .entries() 29 | .iter() 30 | .flat_map(|e| { 31 | dynamic_symbols.resolve_name(e.symbol_index() as usize, string_table) 32 | }) 33 | .filter(|s| !s.is_empty()) 34 | .for_each(|s| println!("\t{}", s)); 35 | } 36 | 37 | if let Some(plt) = dynamic_lib.plt() { 38 | match plt { 39 | RelocationTable::WithAddend(rel) => { 40 | rel.entries() 41 | .iter() 42 | .flat_map(|e| { 43 | dynamic_symbols 44 | .resolve_name(e.symbol_index() as usize, string_table) 45 | }) 46 | .filter(|s| !s.is_empty()) 47 | .for_each(|s| println!("\t{}", s)); 48 | } 49 | RelocationTable::WithoutAddend(rel) => { 50 | rel.entries() 51 | .iter() 52 | .flat_map(|e| { 53 | dynamic_symbols 54 | .resolve_name(e.symbol_index() as usize, string_table) 55 | }) 56 | .filter(|s| !s.is_empty()) 57 | .for_each(|s| println!("\t{}", s)); 58 | } 59 | } 60 | } 61 | } 62 | } 63 | } 64 | 65 | unsafe fn getpid() -> u32 { 66 | 999 67 | } 68 | 69 | /// Finding executable target differs on unix and android 70 | #[cfg(target_os = "linux")] 71 | fn find_executable<'a>() -> Option> { 72 | let loaded_modules = collect_modules(); 73 | loaded_modules.into_iter().next() 74 | } 75 | 76 | /// Finding executable target differs on unix and android 77 | #[cfg(target_os = "android")] 78 | fn find_executable<'a>() -> Option> { 79 | let executable = std::env::current_exe().expect("current exe"); 80 | let file_stem = executable.file_stem()?; 81 | let file_stem = file_stem.to_str()?; 82 | let loaded_modules = collect_modules(); 83 | loaded_modules 84 | .into_iter() 85 | .filter(|lib| lib.name().contains(file_stem)) 86 | .next() 87 | } 88 | #[test] 89 | fn can_hook_getpid() { 90 | let my_pid = unsafe { libc::getpid() }; 91 | println!("application pid is {my_pid}"); 92 | 93 | let executable_entry = find_executable().expect("can find executable"); 94 | println!("successfully identified executable"); 95 | 96 | let dyn_lib = DynamicLibrary::initialize(executable_entry).expect("can load"); 97 | println!("successfully initialied dynamic library for instrumentation"); 98 | 99 | let target_function = dyn_lib 100 | .try_find_function("getpid") 101 | .expect("executable should link getpid"); 102 | println!( 103 | "successfully identified libc getpid offset: {:#X?}", 104 | target_function.r_offset 105 | ); 106 | 107 | let base_addr = dyn_lib.library().addr(); 108 | let plt_fn_ptr = (base_addr + target_function.r_offset as usize) as *mut *mut libc::c_void; 109 | let page_size = unsafe { libc::sysconf(libc::_SC_PAGE_SIZE) as usize }; 110 | let plt_page = ((plt_fn_ptr as usize / page_size) * page_size) as *mut libc::c_void; 111 | println!("page start for function is {plt_page:#X?}"); 112 | 113 | let _stored_address = unsafe { 114 | // Set the memory page to read, write 115 | let prot_res = libc::mprotect(plt_page, page_size, libc::PROT_WRITE | libc::PROT_READ); 116 | if prot_res != 0 { 117 | panic!("failed to set prot res"); 118 | } 119 | 120 | // Replace the function address 121 | let previous_address = std::ptr::replace(plt_fn_ptr, getpid as *mut _); 122 | 123 | // Set the memory page protection back to read only 124 | let prot_res = libc::mprotect(plt_page, page_size, libc::PROT_READ); 125 | if prot_res != 0 { 126 | panic!("failed to set prot res"); 127 | } 128 | 129 | previous_address as *const c_void 130 | }; 131 | 132 | let get_pid = unsafe { libc::getpid() }; 133 | assert_eq!(get_pid, 999) 134 | } 135 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | use thiserror::Error; 3 | 4 | #[cfg(target_pointer_width = "64")] 5 | pub mod elf64; 6 | #[cfg(target_pointer_width = "64")] 7 | use elf64 as elf; 8 | 9 | #[cfg(target_pointer_width = "32")] 10 | pub mod elf32; 11 | #[cfg(target_pointer_width = "32")] 12 | use elf32 as elf; 13 | 14 | /// Errors related to dynamic libraries 15 | #[derive(Debug, Error)] 16 | pub enum DynamicError { 17 | /// Tried to cast from a raw type section and was unmapped 18 | #[error("Unknown type witnessed: `{0}`")] 19 | TypeCast(#[from] elf::DynTypeError), 20 | 21 | /// Given prescence of `0` section type, `1` section type would be required 22 | #[error("Given the prescence of `{0:#?}`, expected prescence of `{1:#?}`")] 23 | DependentSection(DynamicSectionType, DynamicSectionType), 24 | 25 | #[error("Failed to parse, required section missing `{0:#?}`")] 26 | RequiredSection(DynamicSectionType), 27 | 28 | #[error("No dynamic program header available")] 29 | ProgramHeader, 30 | } 31 | 32 | /// Section type enumeration 33 | #[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] 34 | #[allow(non_camel_case_types)] 35 | pub enum DynamicSectionType { 36 | DT_NULL, 37 | DT_PLTRELSZ, 38 | DT_PLTGOT, 39 | DT_PLTREL, 40 | 41 | DT_STRTAB, 42 | DT_SYMTAB, 43 | DT_SYMENT, 44 | 45 | DT_RELA, 46 | DT_RELASZ, 47 | DT_RELAENT, 48 | 49 | DT_REL, 50 | DT_RELSZ, 51 | DT_RELENT, 52 | 53 | DT_STRSZ, 54 | DT_JMPREL, 55 | } 56 | 57 | /// Container of Dynamic Relocations 58 | pub struct DynamicRelocations<'a> { 59 | inner: &'a [elf::DynRel], 60 | } 61 | 62 | impl DynamicRelocations<'_> { 63 | /// Extract string from table starting at carrot position 64 | pub fn read_at(&self, index: usize) -> Option<&elf::DynRel> { 65 | self.inner.get(index) 66 | } 67 | 68 | /// Dynamic relocations internal slice 69 | pub fn entries(&self) -> &[elf::DynRel] { 70 | self.inner 71 | } 72 | } 73 | 74 | /// Container of Dynamic Addend Relocations 75 | pub struct DynamicAddendRelocations<'a> { 76 | inner: &'a [elf::DynRela], 77 | } 78 | 79 | impl DynamicAddendRelocations<'_> { 80 | /// Extract string from table starting at carrot position 81 | pub fn read_at(&self, index: usize) -> Option<&elf::DynRela> { 82 | self.inner.get(index) 83 | } 84 | 85 | /// Dynamic addend relocations internal slice 86 | pub fn entries(&self) -> &[elf::DynRela] { 87 | self.inner 88 | } 89 | } 90 | 91 | /// Container of Dynamic Symbols 92 | pub struct DynamicSymbols<'a> { 93 | inner: &'a elf::DynSym, 94 | } 95 | 96 | impl DynamicSymbols<'_> { 97 | /// gets the dynamic symbol at this index 98 | fn get(&self, index: usize) -> Option<&elf::DynSym> { 99 | unsafe { (self.inner as *const elf::DynSym).add(index).as_ref() } 100 | } 101 | 102 | /// resolves the name of the dynamic symbol at `index` 103 | pub fn resolve_name<'b>( 104 | &'b self, 105 | index: usize, 106 | string_table: &'b StringTable<'b>, 107 | ) -> Option> { 108 | let entry = self.get(index)?; 109 | string_table.read_at(entry.st_name as usize) 110 | } 111 | } 112 | 113 | /// Container of Dynamic Entries 114 | pub struct DynamicSection<'a> { 115 | inner: &'a elf::DynEntry, 116 | } 117 | 118 | #[derive(Debug)] 119 | /// A view of the Library's String table 120 | /// The inner `raw` reference refers to a continguous array of zero terminated strings. 121 | /// The string table view is needed to arbitrarily access into the data and pull out the null terminated strings. 122 | pub struct StringTable<'a> { 123 | raw: &'a [libc::c_char], 124 | } 125 | 126 | impl<'a> StringTable<'a> { 127 | /// Extract string from table starting at carrot position 128 | pub fn read_at(&self, carrot: usize) -> Option> { 129 | match carrot >= self.raw.len() { 130 | true => None, 131 | false => unsafe { Some(std::ffi::CStr::from_ptr(&self.raw[carrot]).to_string_lossy()) }, 132 | } 133 | } 134 | 135 | /// total size of the string table in memory. 136 | /// This does not reflect how many strings 137 | pub fn total_size(&self) -> usize { 138 | self.raw.len() 139 | } 140 | } 141 | 142 | impl DynamicSection<'_> { 143 | /// Iterate dynamic section's DynEntry link list attempting to find section with target section type 144 | fn find_section(&self, tag: DynamicSectionType) -> Option<&elf::DynEntry> { 145 | let mut current = Some(self.inner); 146 | while let Some(inner) = current { 147 | match DynamicSectionType::try_from(inner.d_tag) { 148 | Ok(DynamicSectionType::DT_NULL) => return None, 149 | Ok(this_tag) if this_tag == tag => return Some(inner), 150 | Ok(_) => { 151 | // nothing to do. 152 | } 153 | Err(_err) => { 154 | // continue for now...; 155 | } 156 | } 157 | 158 | current = unsafe { (inner as *const elf::DynEntry).offset(1).as_ref() }; 159 | } 160 | 161 | None 162 | } 163 | } 164 | 165 | /// An Elf Program Header 166 | /// Primary examples are PT_LOAD and PT_DYNAMIC 167 | pub struct ProgramHeader<'a> { 168 | inner: &'a elf::ProgramHeader, 169 | } 170 | 171 | impl ProgramHeader<'_> { 172 | /// Access the program headers type 173 | pub fn header_type(&self) -> elf::Word { 174 | self.inner.p_type 175 | } 176 | 177 | /// Access the program headers virtual address 178 | pub fn virtual_addr(&self) -> usize { 179 | self.inner.p_vaddr as usize 180 | } 181 | 182 | /// Total size in memory 183 | pub fn memory_size(&self) -> usize { 184 | self.inner.p_memsz as usize 185 | } 186 | 187 | /// File size 188 | pub fn file_size(&self) -> usize { 189 | self.inner.p_filesz as usize 190 | } 191 | 192 | /// Program headers absolute address 193 | pub fn program_addr(&self) -> usize { 194 | self.inner.p_paddr as usize 195 | } 196 | 197 | /// Program headers offset 198 | pub fn offset(&self) -> usize { 199 | self.inner.p_offset as usize 200 | } 201 | } 202 | 203 | /// A dynamic libraries plt maybe be addend entries or non addend entries 204 | pub enum RelocationTable<'a> { 205 | WithAddend(DynamicAddendRelocations<'a>), 206 | WithoutAddend(DynamicRelocations<'a>), 207 | } 208 | 209 | /// Dynamic Library Entry 210 | /// An 'upgraded' LibraryEntry with the dynamic section resolved. 211 | pub struct DynamicLibrary<'a> { 212 | library: LoadedLibrary<'a>, 213 | dyn_section: DynamicSection<'a>, 214 | dyn_string_table: StringTable<'a>, 215 | 216 | dyn_symbols: Option>, 217 | dyn_relocs: Option>, 218 | dyn_addend_relocs: Option>, 219 | dyn_plt: Option>, 220 | } 221 | 222 | /// Access the libraries dynamic symbols through the library's dynamic section 223 | fn extract_dyn_symbols<'a, 'b>( 224 | lib: &'a LoadedLibrary<'a>, 225 | dynamic_section: &'a DynamicSection<'a>, 226 | ) -> std::result::Result>, DynamicError> { 227 | // No actual requirement for dynamic symbols. 228 | let Some(dyn_symbol_table) = dynamic_section.find_section(DynamicSectionType::DT_SYMTAB) else { 229 | return Ok(None); 230 | }; 231 | 232 | // We use explicit Elf Dynamic entry structs. 233 | // The SYMENT size doesn't seem relevant anymore, so we can assert it the same size as the dyn entry to combat any egregious misusages 234 | let table_size = dynamic_section 235 | .find_section(DynamicSectionType::DT_SYMENT) 236 | .ok_or(DynamicError::DependentSection( 237 | DynamicSectionType::DT_SYMTAB, 238 | DynamicSectionType::DT_SYMENT, 239 | ))? 240 | .d_val_ptr as usize; 241 | assert_eq!(table_size, std::mem::size_of::()); 242 | 243 | // We don't have enough information to tell if this elf represents an Object Mapped or Shared Library / Executable mapped entry 244 | // For object mapped the ptr's are relative. So we have to rebase by the virtual address from dl_info 245 | let dyn_sym_ptr = match dyn_symbol_table.d_val_ptr as usize <= lib.addr() { 246 | false => dyn_symbol_table.d_val_ptr as usize, 247 | true => dyn_symbol_table.d_val_ptr as usize + lib.addr(), 248 | } as *const elf::DynSym; 249 | 250 | Ok(Some(DynamicSymbols { 251 | inner: unsafe { dyn_sym_ptr.as_ref().unwrap() }, 252 | })) 253 | } 254 | 255 | /// Access the libraries dynamic section by dereferencing the PT_DYN program header's virtual address value 256 | fn extract_dyn_section<'a, 'b>( 257 | lib: &'a LoadedLibrary<'a>, 258 | ) -> std::result::Result, DynamicError> { 259 | let dynamic_header = lib 260 | .program_headers() 261 | .find(|p_h| p_h.header_type() == 0x02) 262 | .ok_or(DynamicError::ProgramHeader)?; 263 | 264 | let dynamic_sections = lib.addr() + dynamic_header.virtual_addr(); 265 | let dynamic_sections = dynamic_sections as *const elf::DynEntry; 266 | Ok(DynamicSection { 267 | inner: unsafe { dynamic_sections.as_ref().unwrap() }, 268 | }) 269 | } 270 | 271 | /// Access the libraries string table through the library's dynamic section 272 | fn extract_dyn_string_table<'a, 'b>( 273 | lib: &'a LoadedLibrary<'a>, 274 | dynamic_section: &'a DynamicSection<'a>, 275 | ) -> std::result::Result, DynamicError> { 276 | let str_table_entry = dynamic_section 277 | .find_section(DynamicSectionType::DT_STRTAB) 278 | .ok_or(DynamicError::RequiredSection(DynamicSectionType::DT_STRTAB))?; 279 | let table_size = dynamic_section 280 | .find_section(DynamicSectionType::DT_STRSZ) 281 | .ok_or(DynamicError::DependentSection( 282 | DynamicSectionType::DT_STRTAB, 283 | DynamicSectionType::DT_STRSZ, 284 | ))? 285 | .d_val_ptr as usize; 286 | 287 | // We don't have enough information to tell if this elf represents an Object Mapped or Shared Library / Executable mapped entry 288 | // For object mapped the ptr's are relative. So we have to rebase by the virtual address from dl_info 289 | let str_table_ptr = match str_table_entry.d_val_ptr as usize <= lib.addr() { 290 | false => str_table_entry.d_val_ptr as usize, 291 | true => str_table_entry.d_val_ptr as usize + lib.addr(), 292 | } as *const libc::c_char; 293 | 294 | Ok(StringTable { 295 | raw: unsafe { std::slice::from_raw_parts(str_table_ptr, table_size) }, 296 | }) 297 | } 298 | 299 | /// Access the libaries dynamic relocations through the dynamic program header 300 | fn extract_dyn_relocs<'a, 'b>( 301 | lib: &'a LoadedLibrary<'a>, 302 | dynamic_section: &'a DynamicSection<'a>, 303 | ) -> std::result::Result>, DynamicError> { 304 | let Some(dyn_rel_entry) = dynamic_section.find_section(DynamicSectionType::DT_REL) else { 305 | return Ok(None); 306 | }; 307 | 308 | let total_size = dynamic_section 309 | .find_section(DynamicSectionType::DT_RELSZ) 310 | .ok_or(DynamicError::DependentSection( 311 | DynamicSectionType::DT_REL, 312 | DynamicSectionType::DT_RELSZ, 313 | ))? 314 | .d_val_ptr as usize; 315 | let entry_size = dynamic_section 316 | .find_section(DynamicSectionType::DT_RELENT) 317 | .ok_or(DynamicError::DependentSection( 318 | DynamicSectionType::DT_REL, 319 | DynamicSectionType::DT_RELENT, 320 | ))? 321 | .d_val_ptr as usize; 322 | 323 | assert_eq!(entry_size, std::mem::size_of::()); 324 | 325 | let entry_count = total_size / entry_size; 326 | // We don't have enough information to tell if this elf represents an Object Mapped or Shared Library / Executable mapped entry 327 | // For object mapped the ptr's are relative. So we have to rebase by the virtual address from dl_info 328 | let dyn_rel_entry = match dyn_rel_entry.d_val_ptr as usize <= lib.addr() { 329 | false => dyn_rel_entry.d_val_ptr as usize, 330 | true => dyn_rel_entry.d_val_ptr as usize + lib.addr(), 331 | } as *const elf::DynRel; 332 | 333 | Ok(Some(DynamicRelocations { 334 | inner: unsafe { std::slice::from_raw_parts(dyn_rel_entry, entry_count) }, 335 | })) 336 | } 337 | 338 | /// Access the libaries dynamic addend relocations through the dynamic program header 339 | fn extract_dyn_addend_relocs<'a, 'b>( 340 | lib: &'a LoadedLibrary<'a>, 341 | dynamic_section: &'a DynamicSection<'a>, 342 | ) -> std::result::Result>, DynamicError> { 343 | let Some(dyn_rel_entry) = dynamic_section.find_section(DynamicSectionType::DT_RELA) else { 344 | return Ok(None); 345 | }; 346 | 347 | let total_size = dynamic_section 348 | .find_section(DynamicSectionType::DT_RELASZ) 349 | .ok_or(DynamicError::DependentSection( 350 | DynamicSectionType::DT_RELA, 351 | DynamicSectionType::DT_RELASZ, 352 | ))? 353 | .d_val_ptr as usize; 354 | let entry_size = dynamic_section 355 | .find_section(DynamicSectionType::DT_RELAENT) 356 | .ok_or(DynamicError::DependentSection( 357 | DynamicSectionType::DT_RELA, 358 | DynamicSectionType::DT_RELAENT, 359 | ))? 360 | .d_val_ptr as usize; 361 | 362 | assert_eq!(entry_size, std::mem::size_of::()); 363 | 364 | let entry_count = total_size / entry_size; 365 | // We don't have enough information to tell if this elf represents an Object Mapped or Shared Library / Executable mapped entry 366 | // For object mapped the ptr's are relative. So we have to rebase by the virtual address from dl_info 367 | let dyn_rel_entry = match dyn_rel_entry.d_val_ptr as usize <= lib.addr() { 368 | false => dyn_rel_entry.d_val_ptr as usize, 369 | true => dyn_rel_entry.d_val_ptr as usize + lib.addr(), 370 | } as *const elf::DynRela; 371 | 372 | Ok(Some(DynamicAddendRelocations { 373 | inner: unsafe { std::slice::from_raw_parts(dyn_rel_entry, entry_count) }, 374 | })) 375 | } 376 | 377 | /// Access the libraries plt relocations 378 | fn extract_dyn_plt<'a, 'b>( 379 | lib: &'a LoadedLibrary<'a>, 380 | dynamic_section: &'a DynamicSection<'a>, 381 | ) -> std::result::Result>, DynamicError> { 382 | // decipher if its rel or rela relocation entries 383 | // if this isn't present we can't have a plt 384 | let Some(dyn_type) = dynamic_section.find_section(DynamicSectionType::DT_PLTREL) else { 385 | return Ok(None); 386 | }; 387 | 388 | let relocation_type = DynamicSectionType::try_from(dyn_type.d_val_ptr)?; 389 | 390 | let dyn_plt_entry = dynamic_section 391 | .find_section(DynamicSectionType::DT_JMPREL) 392 | .ok_or(DynamicError::DependentSection( 393 | DynamicSectionType::DT_PLTREL, 394 | DynamicSectionType::DT_JMPREL, 395 | ))?; 396 | let total_size = dynamic_section 397 | .find_section(DynamicSectionType::DT_PLTRELSZ) 398 | .ok_or(DynamicError::DependentSection( 399 | DynamicSectionType::DT_PLTREL, 400 | DynamicSectionType::DT_PLTRELSZ, 401 | ))? 402 | .d_val_ptr as usize; 403 | 404 | let entry_addr = match dyn_plt_entry.d_val_ptr as usize <= lib.addr() { 405 | false => dyn_plt_entry.d_val_ptr as usize, 406 | true => dyn_plt_entry.d_val_ptr as usize + lib.addr(), 407 | }; 408 | 409 | Ok(match relocation_type { 410 | DynamicSectionType::DT_REL => { 411 | let entry_count = total_size / std::mem::size_of::(); 412 | Some(RelocationTable::WithoutAddend(DynamicRelocations { 413 | inner: unsafe { 414 | std::slice::from_raw_parts(entry_addr as *const elf::DynRel, entry_count) 415 | }, 416 | })) 417 | } 418 | DynamicSectionType::DT_RELA => { 419 | let entry_count = total_size / std::mem::size_of::(); 420 | Some(RelocationTable::WithAddend(DynamicAddendRelocations { 421 | inner: unsafe { 422 | std::slice::from_raw_parts(entry_addr as *const elf::DynRela, entry_count) 423 | }, 424 | })) 425 | } 426 | _ => None, 427 | }) 428 | } 429 | 430 | impl<'a> DynamicLibrary<'a> { 431 | /// Try to consume a LoadedLibrary and create a resolved Dynamic view 432 | /// The Dynamic Library will take ownership of the load library as well as store 433 | /// all relevant dynamic sections for easy access and symbol resolution 434 | pub fn initialize(lib: LoadedLibrary<'a>) -> std::result::Result { 435 | let dyn_section = extract_dyn_section(&lib)?; 436 | let dyn_string_table = extract_dyn_string_table(&lib, &dyn_section)?; 437 | let dyn_symbols = extract_dyn_symbols(&lib, &dyn_section)?; 438 | let dyn_relocs = extract_dyn_relocs(&lib, &dyn_section)?; 439 | let dyn_addend_relocs = extract_dyn_addend_relocs(&lib, &dyn_section)?; 440 | let dyn_plt = extract_dyn_plt(&lib, &dyn_section)?; 441 | 442 | Ok(Self { 443 | library: lib, 444 | dyn_section, 445 | dyn_string_table, 446 | dyn_symbols, 447 | dyn_relocs, 448 | dyn_addend_relocs, 449 | dyn_plt, 450 | }) 451 | } 452 | 453 | /// Finding target function differs on 32 bit and 64 bit. 454 | /// On 32 bit we want to check the relocations table only, opposed to the addend relocations table. 455 | /// Additionally, we will fall back to the plt given it is an addendless relocation table. 456 | #[cfg(target_pointer_width = "32")] 457 | pub fn try_find_function(&self, symbol_name: &str) -> Option<&'_ elf32::DynRel> { 458 | let string_table = self.string_table(); 459 | let dyn_symbols = self.symbols()?; 460 | if let Some(dyn_relas) = self.relocs() { 461 | let dyn_relas = dyn_relas.entries().iter(); 462 | if let Some(symbol) = dyn_relas 463 | .flat_map(|e| { 464 | dyn_symbols 465 | .resolve_name(e.symbol_index() as usize, string_table) 466 | .map(|s| (e, s)) 467 | }) 468 | .filter(|(_, s)| s.eq(symbol_name)) 469 | .next() 470 | .map(|(target_function, _)| target_function) 471 | { 472 | return Some(symbol); 473 | } 474 | } 475 | 476 | if let Some(dyn_relas) = self.plt_rel() { 477 | let dyn_relas = dyn_relas.entries().iter(); 478 | if let Some(symbol) = dyn_relas 479 | .flat_map(|e| { 480 | dyn_symbols 481 | .resolve_name(e.symbol_index() as usize, string_table) 482 | .map(|s| (e, s)) 483 | }) 484 | .filter(|(_, s)| s.eq(symbol_name)) 485 | .next() 486 | .map(|(target_function, _)| target_function) 487 | { 488 | return Some(symbol); 489 | } 490 | } 491 | None 492 | } 493 | 494 | /// Finding target function differs on 32 bit and 64 bit. 495 | /// On 64 bit we want to check the addended relocations table only, opposed to the addendless relocations table. 496 | /// Additionally, we will fall back to the plt given it is an addended relocation table. 497 | #[cfg(target_pointer_width = "64")] 498 | pub fn try_find_function(&self, symbol_name: &str) -> Option<&'_ elf64::DynRela> { 499 | let string_table = self.string_table(); 500 | let symbols = self.symbols()?; 501 | if let Some(dyn_relas) = self.addend_relocs() { 502 | let dyn_relas = dyn_relas.entries().iter(); 503 | if let Some(symbol) = dyn_relas 504 | .flat_map(|e| { 505 | symbols 506 | .resolve_name(e.symbol_index() as usize, string_table) 507 | .map(|s| (e, s)) 508 | }) 509 | .find(|(_, s)| s.eq(symbol_name)) 510 | .map(|(target_function, _)| target_function) 511 | { 512 | return Some(symbol); 513 | } 514 | } 515 | 516 | if let Some(dyn_relas) = self.plt_rela() { 517 | let dyn_relas = dyn_relas.entries().iter(); 518 | if let Some(symbol) = dyn_relas 519 | .flat_map(|e| { 520 | symbols 521 | .resolve_name(e.symbol_index() as usize, string_table) 522 | .map(|s| (e, s)) 523 | }) 524 | .find(|(_, s)| s.eq(symbol_name)) 525 | .map(|(target_function, _)| target_function) 526 | { 527 | return Some(symbol); 528 | } 529 | } 530 | None 531 | } 532 | /// Access the plt as a dynamic relocation table if possible 533 | /// can fail if the plt is not available or the plt is with addend 534 | pub fn plt_rel(&self) -> Option<&DynamicRelocations<'_>> { 535 | match self.plt() { 536 | Some(RelocationTable::WithoutAddend(relocs)) => Some(relocs), 537 | _ => None, 538 | } 539 | } 540 | 541 | /// Access the plt as a dynamic addend relocation table if possible 542 | /// can fail if the plt is not available or the plt is without addend 543 | pub fn plt_rela(&self) -> Option<&DynamicAddendRelocations<'_>> { 544 | match self.plt() { 545 | Some(RelocationTable::WithAddend(relocs)) => Some(relocs), 546 | _ => None, 547 | } 548 | } 549 | /// Access the dynamic libraries plt if available 550 | /// Can be either a DynamicRelocations or DynamicAddendRelocations 551 | pub fn plt(&self) -> Option<&RelocationTable<'_>> { 552 | self.dyn_plt.as_ref() 553 | } 554 | 555 | /// Access the dynamic libraries relocations if available 556 | pub fn relocs(&self) -> Option<&DynamicRelocations<'_>> { 557 | self.dyn_relocs.as_ref() 558 | } 559 | 560 | /// Access the dynamic libraries addend relocations if available 561 | pub fn addend_relocs(&self) -> Option<&DynamicAddendRelocations<'_>> { 562 | self.dyn_addend_relocs.as_ref() 563 | } 564 | 565 | /// Access the dynamic libraries symbol table if available 566 | pub fn symbols(&self) -> Option<&DynamicSymbols<'_>> { 567 | self.dyn_symbols.as_ref() 568 | } 569 | 570 | /// Access the dynamic libraries dynamic section 571 | pub fn dyn_section(&self) -> &DynamicSection<'_> { 572 | &self.dyn_section 573 | } 574 | 575 | /// Access the dynamic libraries backing general loaded library structure 576 | /// capable of providing the name and base address of the in memory 577 | pub fn library(&self) -> &LoadedLibrary<'_> { 578 | &self.library 579 | } 580 | 581 | /// Access the dynamic string table 582 | pub fn string_table(&self) -> &StringTable<'_> { 583 | &self.dyn_string_table 584 | } 585 | } 586 | 587 | /// A library loaded in the process 588 | pub struct LoadedLibrary<'a> { 589 | addr: usize, 590 | name: Cow<'a, str>, 591 | program_headers: &'a [elf::ProgramHeader], 592 | } 593 | 594 | impl LoadedLibrary<'_> { 595 | /// Access the libraries string name 596 | /// This is more the libraries `path` than the name per say 597 | pub fn name(&self) -> &str { 598 | &self.name 599 | } 600 | 601 | /// Access the libraries virtual address 602 | pub fn addr(&self) -> usize { 603 | self.addr 604 | } 605 | 606 | /// Iterate the libraries program headers 607 | pub fn program_headers(&self) -> impl Iterator> { 608 | self.program_headers 609 | .iter() 610 | .map(|header| ProgramHeader { inner: header }) 611 | } 612 | 613 | /// Access the libraries PT_INTERP program headers 614 | pub fn interpreter_header(&self) -> Option> { 615 | self.program_headers().find(|p_h| p_h.header_type() == 0x03) 616 | } 617 | 618 | /// Access the libraries PT_LOAD program headers 619 | pub fn load_headers(&self) -> impl Iterator> { 620 | self.program_headers() 621 | .filter(|p_h| p_h.header_type() == 0x01) 622 | } 623 | } 624 | 625 | /// Returns a `Vec` of objects loaded into the current address space. 626 | pub fn collect_modules<'a>() -> Vec> { 627 | let mut ret = Vec::new(); 628 | 629 | // Pushes an `Object` into the result vector on the behalf of C. 630 | extern "C" fn push_object(objs: &mut Vec, dl_info: &libc::dl_phdr_info) { 631 | let name = unsafe { std::ffi::CStr::from_ptr(dl_info.dlpi_name) }.to_string_lossy(); 632 | // We have to copy sthe `dl_phdr_info` struct out, as the same memory buffer is used for 633 | // each entry during the iteration process. Otherwise we could have used a vector of 634 | // pointers. 635 | println!("{} {}", dl_info.dlpi_addr, dl_info.dlpi_phnum); 636 | 637 | if dl_info.dlpi_phnum == 0 { 638 | return; 639 | } 640 | 641 | let program_headers = 642 | unsafe { std::slice::from_raw_parts(dl_info.dlpi_phdr, dl_info.dlpi_phnum as usize) }; 643 | objs.push(LoadedLibrary { 644 | addr: dl_info.dlpi_addr as usize, 645 | name, 646 | program_headers, 647 | }); 648 | } 649 | 650 | // Callback for `dl_iterate_phdr(3)`. 651 | unsafe extern "C" fn collect_objs( 652 | info: *mut libc::dl_phdr_info, 653 | _sz: usize, 654 | data: *mut libc::c_void, 655 | ) -> libc::c_int { 656 | if let Some(info) = unsafe { info.as_ref() } { 657 | push_object(&mut *(data as *mut Vec), info); // Get Rust to push the object. 658 | }; 659 | 660 | 0 661 | } 662 | 663 | let ret_void_p = &mut ret as *mut Vec as *mut libc::c_void; 664 | unsafe { libc::dl_iterate_phdr(Some(collect_objs), ret_void_p) }; 665 | 666 | ret 667 | } 668 | --------------------------------------------------------------------------------