Skip to content

Commit 7dab3a0

Browse files
extract got code into it's own file
1 parent 9298487 commit 7dab3a0

File tree

2 files changed

+226
-221
lines changed

2 files changed

+226
-221
lines changed

profiling/src/io/got.rs

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
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

Comments
 (0)