|
| 1 | +use crate::bindings::{ |
| 2 | + Elf64_Dyn, Elf64_Rela, Elf64_Sym, Elf64_Xword, DT_JMPREL, DT_NULL, DT_PLTRELSZ, DT_STRTAB, |
| 3 | + DT_SYMTAB, PT_DYNAMIC, R_AARCH64_JUMP_SLOT, R_X86_64_JUMP_SLOT, |
| 4 | +}; |
| 5 | +use libc::{c_char, c_int, c_void, dl_phdr_info}; |
| 6 | +use log::{error, trace}; |
| 7 | +use std::ffi::CStr; |
| 8 | +use std::ptr; |
| 9 | + |
| 10 | +fn elf64_r_type(info: Elf64_Xword) -> u32 { |
| 11 | + (info & 0xffffffff) as u32 |
| 12 | +} |
| 13 | + |
| 14 | +fn elf64_r_sym(info: Elf64_Xword) -> u32 { |
| 15 | + (info >> 32) as u32 |
| 16 | +} |
| 17 | + |
| 18 | +pub struct GotSymbolOverwrite { |
| 19 | + pub symbol_name: &'static str, |
| 20 | + pub new_func: *mut (), |
| 21 | + pub orig_func: *mut *mut (), |
| 22 | +} |
| 23 | + |
| 24 | +/// Override the GOT entry for symbols specified in `overwrites`. |
| 25 | +/// |
| 26 | +/// See: https://cs4401.walls.ninja/notes/lecture/basics_global_offset_table.html |
| 27 | +/// See: https://bottomupcs.com/ch09s03.html |
| 28 | +/// See: https://www.codeproject.com/articles/1032231/what-is-the-symbol-table-and-what-is-the-global-of |
| 29 | +/// |
| 30 | +/// Safety: Why is anything happening in in here safe? Well generally we can say all of the pointer |
| 31 | +/// arithmetics are safe because the dynamic library the `info` is pointing to was loaded by the |
| 32 | +/// dynamic linker prior to us messing with the global offset table. If the dynamic library would |
| 33 | +/// not be a valid ELF64, the dynamic linker would have not loaded it. |
| 34 | +unsafe fn override_got_entry( |
| 35 | + info: *mut dl_phdr_info, |
| 36 | + overwrites: *mut Vec<GotSymbolOverwrite>, |
| 37 | +) -> bool { |
| 38 | + let phdr = (*info).dlpi_phdr; |
| 39 | + |
| 40 | + // Locate the dynamic programm header (`PT_DYNAMIC`) |
| 41 | + let mut dyn_ptr: *const Elf64_Dyn = ptr::null(); |
| 42 | + for i in 0..(*info).dlpi_phnum { |
| 43 | + let phdr_i = phdr.offset(i as isize); |
| 44 | + if (*phdr_i).p_type == PT_DYNAMIC { |
| 45 | + dyn_ptr = ((*info).dlpi_addr as usize + (*phdr_i).p_vaddr as usize) as *const Elf64_Dyn; |
| 46 | + break; |
| 47 | + } |
| 48 | + } |
| 49 | + if dyn_ptr.is_null() { |
| 50 | + trace!("Failed to locate dynamic section"); |
| 51 | + return false; |
| 52 | + } |
| 53 | + |
| 54 | + let mut rel_plt: *mut Elf64_Rela = ptr::null_mut(); |
| 55 | + let mut rel_plt_size: usize = 0; |
| 56 | + let mut symtab: *mut Elf64_Sym = ptr::null_mut(); |
| 57 | + let mut strtab: *const c_char = ptr::null(); |
| 58 | + |
| 59 | + // The dynamic programm header (`PT_DYNAMIC`) has different sections. We are interessted in the |
| 60 | + // procedure linkage table (PLT in `DT_JMPREL`), the size of the PLT (`DT_PLTRELSZ`), the |
| 61 | + // symbol table (`DT_SYMTAB`) and the the string table for the symbol names (`DT_STRTAB`). |
| 62 | + // |
| 63 | + // Addresses in here are sometimes relative, sometimes absolute |
| 64 | + // - on musl, addresses are relative |
| 65 | + // - on glibc, addresses are absolutes |
| 66 | + // https://elixir.bootlin.com/glibc/glibc-2.36/source/elf/get-dynamic-info.h#L84 |
| 67 | + let mut dyn_iter = dyn_ptr; |
| 68 | + loop { |
| 69 | + let d_tag = (*dyn_iter).d_tag as u32; |
| 70 | + if d_tag == DT_NULL { |
| 71 | + break; |
| 72 | + } |
| 73 | + match d_tag { |
| 74 | + DT_JMPREL => { |
| 75 | + // Relocation entries for the PLT (Procedure Linkage Table) |
| 76 | + if ((*dyn_iter).d_un.d_ptr as usize) < ((*info).dlpi_addr as usize) { |
| 77 | + rel_plt = ((*info).dlpi_addr as usize + (*dyn_iter).d_un.d_ptr as usize) |
| 78 | + as *mut Elf64_Rela; |
| 79 | + } else { |
| 80 | + rel_plt = (*dyn_iter).d_un.d_ptr as *mut Elf64_Rela; |
| 81 | + } |
| 82 | + } |
| 83 | + DT_PLTRELSZ => { |
| 84 | + // Size of the PLT relocation entries |
| 85 | + rel_plt_size = (*dyn_iter).d_un.d_val as usize; |
| 86 | + } |
| 87 | + DT_SYMTAB => { |
| 88 | + // The symbol table |
| 89 | + if ((*dyn_iter).d_un.d_ptr as usize) < ((*info).dlpi_addr as usize) { |
| 90 | + symtab = ((*info).dlpi_addr as usize + (*dyn_iter).d_un.d_ptr as usize) |
| 91 | + as *mut Elf64_Sym; |
| 92 | + } else { |
| 93 | + symtab = (*dyn_iter).d_un.d_ptr as *mut Elf64_Sym; |
| 94 | + } |
| 95 | + } |
| 96 | + DT_STRTAB => { |
| 97 | + // The string table for the symbol names |
| 98 | + if ((*dyn_iter).d_un.d_ptr as usize) < ((*info).dlpi_addr as usize) { |
| 99 | + strtab = ((*info).dlpi_addr as usize + (*dyn_iter).d_un.d_ptr as usize) |
| 100 | + as *const c_char; |
| 101 | + } else { |
| 102 | + strtab = (*dyn_iter).d_un.d_ptr as *const c_char; |
| 103 | + } |
| 104 | + } |
| 105 | + _ => {} |
| 106 | + } |
| 107 | + dyn_iter = dyn_iter.offset(1); |
| 108 | + } |
| 109 | + |
| 110 | + if rel_plt.is_null() || rel_plt_size == 0 || symtab.is_null() || strtab.is_null() { |
| 111 | + trace!("Failed to locate required ELF sections (`DT_JMPREL`, `DT_PLTRELSZ`, `DT_SYMTAB` and `DT_STRTAB`)"); |
| 112 | + return false; |
| 113 | + } |
| 114 | + |
| 115 | + let num_relocs = rel_plt_size / std::mem::size_of::<Elf64_Rela>(); |
| 116 | + |
| 117 | + // For each symbol we want to overwrite (from `overwrites`), we scan the relocation entries. |
| 118 | + // Once the matching symbol name is found, patch its GOT entry to point to our new function. |
| 119 | + for overwrite in &mut *overwrites { |
| 120 | + for i in 0..num_relocs { |
| 121 | + let rel = rel_plt.add(i); |
| 122 | + let r_type = elf64_r_type((*rel).r_info); |
| 123 | + |
| 124 | + // Only handle JUMP_SLOT relocations |
| 125 | + if r_type != R_AARCH64_JUMP_SLOT && r_type != R_X86_64_JUMP_SLOT { |
| 126 | + continue; |
| 127 | + } |
| 128 | + |
| 129 | + // Get the symbol index for this relocation, then the symbol struct |
| 130 | + let sym_index = elf64_r_sym((*rel).r_info) as usize; |
| 131 | + let sym = symtab.add(sym_index); |
| 132 | + |
| 133 | + // Access the symbol name via the string table |
| 134 | + let name_offset = (*sym).st_name as isize; |
| 135 | + let name_ptr = strtab.offset(name_offset); |
| 136 | + let name = CStr::from_ptr(name_ptr).to_str().unwrap_or(""); |
| 137 | + |
| 138 | + if name == overwrite.symbol_name { |
| 139 | + // Calculate the GOT entry address. Per the ELF spec, `r_offset` for pointer-sized |
| 140 | + // relocations (such as GOT entries) is guaranteed to be pointer-aligned, see: |
| 141 | + // https://github.com/ARM-software/abi-aa/blob/main/aaelf64/aaelf64.rst#5733relocation-operations |
| 142 | + let got_entry = |
| 143 | + ((*info).dlpi_addr as usize + (*rel).r_offset as usize) as *mut *mut (); |
| 144 | + |
| 145 | + // Change memory protection so we can write to the GOT entry |
| 146 | + let page_size = libc::sysconf(libc::_SC_PAGESIZE) as usize; |
| 147 | + let aligned_addr = (got_entry as usize) & !(page_size - 1); |
| 148 | + if libc::mprotect( |
| 149 | + aligned_addr as *mut c_void, |
| 150 | + page_size, |
| 151 | + libc::PROT_READ | libc::PROT_WRITE, |
| 152 | + ) != 0 |
| 153 | + { |
| 154 | + let err = *libc::__errno_location(); |
| 155 | + trace!("mprotect failed: {}", err); |
| 156 | + return false; |
| 157 | + } |
| 158 | + |
| 159 | + trace!( |
| 160 | + "Overriding GOT entry for {} at offset {:?} (abs: {:p}) pointing to {:p} (orig function at {:p})", |
| 161 | + overwrite.symbol_name, |
| 162 | + (*rel).r_offset, |
| 163 | + got_entry, |
| 164 | + *got_entry, |
| 165 | + *overwrite.orig_func |
| 166 | + ); |
| 167 | + |
| 168 | + // This works for musl based linux distros, but not for libc once |
| 169 | + *overwrite.orig_func = libc::dlsym(libc::RTLD_NEXT, name_ptr) as *mut (); |
| 170 | + if (*overwrite.orig_func).is_null() { |
| 171 | + // libc linux fallback |
| 172 | + *overwrite.orig_func = *got_entry; |
| 173 | + } |
| 174 | + *got_entry = overwrite.new_func; |
| 175 | + continue; |
| 176 | + } |
| 177 | + } |
| 178 | + } |
| 179 | + true |
| 180 | +} |
| 181 | + |
| 182 | +/// Callback function that should be passed to `libc::dl_iterate_phdr()` and gets called for every |
| 183 | +/// shared object. |
| 184 | +pub unsafe extern "C" fn callback(info: *mut dl_phdr_info, _size: usize, data: *mut c_void) -> c_int { |
| 185 | + let overwrites = &mut *(data as *mut Vec<GotSymbolOverwrite>); |
| 186 | + |
| 187 | + // detect myself ... |
| 188 | + let mut my_info: libc::Dl_info = std::mem::zeroed(); |
| 189 | + if libc::dladdr(callback as *const c_void, &mut my_info) == 0 { |
| 190 | + error!("Did not find my own `dladdr` and therefore can't hook into the GOT."); |
| 191 | + return 0; |
| 192 | + } |
| 193 | + let my_base_addr = my_info.dli_fbase as usize; |
| 194 | + let module_base_addr = (*info).dlpi_addr as usize; |
| 195 | + if module_base_addr == my_base_addr { |
| 196 | + // "this" lib is actually me: skipping GOT hooking for myself |
| 197 | + return 0; |
| 198 | + } |
| 199 | + |
| 200 | + let name = if (*info).dlpi_name.is_null() || *(*info).dlpi_name == 0 { |
| 201 | + "[Executable]" |
| 202 | + } else { |
| 203 | + CStr::from_ptr((*info).dlpi_name) |
| 204 | + .to_str() |
| 205 | + .unwrap_or("[Unknown]") |
| 206 | + }; |
| 207 | + |
| 208 | + // I guess if we try to hook into GOT from `linux-vdso` or `ld-linux` our best outcome will be |
| 209 | + // that nothing happens, but most likely we'll crash and we should avoid that. |
| 210 | + if name.contains("linux-vdso") || name.contains("ld-linux") { |
| 211 | + return 0; |
| 212 | + } |
| 213 | + |
| 214 | + if override_got_entry(info, overwrites) { |
| 215 | + trace!("Hooked into {name}"); |
| 216 | + } else { |
| 217 | + trace!("Hooking {name} failed"); |
| 218 | + } |
| 219 | + |
| 220 | + 0 |
| 221 | +} |
0 commit comments