mmap, munmap
Memory protection
Although brk is a nice little tool to allocate memory there quite a lot of other things we can do with memory.
To write self modifying code we can use mmap to allocate a memory which has all the read-write-exec flags enabled
Let’s create an executable which can read byte stream from standard out and it tries to execute it.
#![no_std]
#![no_main]
extern crate linux;
use linux::syscall::*;
#[no_mangle]
fn main() -> u8 {
let ptr = unsafe {
mmap(
core::ptr::null_mut(), 1024,
PROT_READ|PROT_WRITE|PROT_EXEC,
MAP_PRIVATE|MAP_ANONYMOUS,
0, 0
).unwrap()
};
let mut buf = unsafe { core::slice::from_raw_parts_mut(ptr, 1024) };
if read(0, &mut buf).unwrap() > 0 {
unsafe { core::arch::asm!("jmp {0}", in(reg) ptr) }
}
0
}
Let’s break our program down: First we need to allocate a buffer which we can fill with data
#![allow(unused)]
fn main() {
let ptr = unsafe {
mmap(
core::ptr::null_mut(), 1024,
PROT_READ|PROT_WRITE|PROT_EXEC,
MAP_PRIVATE|MAP_ANONYMOUS,
0, 0
).unwrap()
};
}
After that we create a slice to make sure that we avoid any memory safety issues…
#![allow(unused)]
fn main() {
let mut buf = unsafe { core::slice::from_raw_parts_mut(ptr, 1024) };
}
Once we’ve done with that we can read data from stdin into this buffer and if there were some data we can try to execute it.
#![allow(unused)]
fn main() {
if read(0, &mut buf).unwrap() > 0 {
unsafe { core::arch::asm!("jmp {0}", in(reg) ptr) }
}
}
If there is no data present, we simply exit with process with return code 0. Let’s test our program like this:
> ./cargo.sh build
> cat /dev/null | ./target/bin; echo $?
0
> echo "hello world" | ./target/bin; echo $?
Segmentation fault (core dumped)
139
It seems to be working, so let’s write some code which is able to rewrite itself:
global exploit
.text:
exploit:
mov rdi,0x1
inc byte [rel exploit + 0x1]
cmp rdi,0xa
jb exploit
mov rax,0x3c
syscall
This code initializes rdi with 0x1 and increments the constant value of 0x1 by one.
After that it checks if rdi is already equals to 0xa and if not it jumps back to exploit but this
time we put 0x2 into rdi. Once the rdi reaches 0xa it calls the exit system call so the return
code of our process will be 10.
Let’s build that code and see how it looks after the compilation:
> nasm -f elf64 -o obj asm.s
> objdump --disassemble=exploit -M intel ./obj
0000000000000000 <exploit>:
0: bf 01 00 00 00 mov edi,0x1
5: fe 05 f6 ff ff ff inc BYTE PTR [rip+0xfffffffffffffff6] # 1 <exploit+0x1>
b: 48 83 ff 0a cmp rdi,0xa
f: 72 ef jb 0 <exploit>
11: b8 3c 00 00 00 mov eax,0x3c
16: 0f 05 syscall
We can dump our exploit function as a binary blob so we can use it against our rust program like this:
> objcopy -O binary --only-section=.text obj exploit
> cat ./exploit | ./target/bin; echo $?
10
File mappings
As we’ve seen in the brk section there are always some files mapped into the virtual address space of a process.
At least there is the binary which is being executed. In many times there are mapped here too. (Check out the mappings
of the cat command with cat /proc/self/maps or of your shell with cat /proc/$$/maps)
We can also map a regular file to the address space and use it like a permanent buffer for our program.
#![no_std]
#![no_main]
extern crate linux;
use linux::syscall::*;
use linux::constants::*;
#[no_mangle]
fn main() -> u8 {
let fd = open("/tmp/data", O_CREAT|O_APPEND|O_RDWR, S_IRUSR|S_IWUSR).unwrap();
fallocate(fd, 0, 0, 1024).unwrap();
let ptr = unsafe {
mmap(
core::ptr::null_mut(), 1024,
PROT_READ|PROT_WRITE,
MAP_SHARED_VALIDATE,
fd, 0
).unwrap()
};
let mut buf = unsafe { core::slice::from_raw_parts_mut(ptr, 1024) };
let _ = write(1, buf).unwrap();
let _ = read(0, &mut buf).unwrap();
0
}
This way we can use it:
> echo "Hello old world" | ./target/bin
> cat /tmp/data
Hello old world
> echo "Hello new world" | ./target/bin
Hello old world
> cat /tmp/data
Hello new world
Feel free to reimplement the exploit above by mapping it into the virtual address space instead of reading from stdin.
Shared memory
#![no_std]
#![no_main]
#[macro_use]
extern crate linux;
use linux::syscall::*;
use linux::constants::*;
#[no_mangle]
fn main() -> u8 {
let fd = open("/tmp/data", O_CREAT|O_TRUNC|O_RDWR, 0).unwrap();
fallocate(fd, 0, 0, 1024).unwrap();
let p1 = unsafe {
mmap(
core::ptr::null_mut(), 1024,
PROT_READ|PROT_WRITE,
MAP_SHARED_VALIDATE,
fd, 0
).unwrap()
};
let p2 = unsafe {
mmap(
core::ptr::null_mut(), 1024,
PROT_READ|PROT_WRITE,
MAP_SHARED_VALIDATE,
fd, 0
).unwrap()
};
let mut b1 = unsafe { core::slice::from_raw_parts_mut(p1, 1024) };
let mut b2 = unsafe { core::slice::from_raw_parts_mut(p2, 1024) };
b1[0] = 13;
println!("b1[0] = {}", b1[0]);
println!("b2[0] = {}", b2[0]);
0
}
> ./cargo.sh run
b1[0] = 13
b2[0] = 13
Overmap section with different protection
#![no_std]
#![no_main]
#[macro_use]
extern crate linux;
use linux::syscall::*;
use linux::constants::*;
#[no_mangle]
fn main() -> u8 {
let p1 = unsafe {
mmap(
core::ptr::null_mut(), 4096 * 3,
PROT_READ,
MAP_ANONYMOUS|MAP_PRIVATE,
0, 0
).unwrap()
};
read(0, &mut [0u8]);
let p2 = unsafe {
mmap(
p1.offset(4096), 4096,
PROT_READ|PROT_WRITE,
MAP_ANONYMOUS|MAP_PRIVATE|MAP_FIXED,
0, 0
).unwrap()
};
read(0, &mut [0u8]);
unsafe { munmap(p1, 4096 * 3).unwrap() };
pause();
0
}