Skip to content

Instantly share code, notes, and snippets.

@rusty-snake
Last active October 20, 2024 13:36
Show Gist options
  • Save rusty-snake/481ad71892fb587a6056326686e55bf7 to your computer and use it in GitHub Desktop.
Save rusty-snake/481ad71892fb587a6056326686e55bf7 to your computer and use it in GitHub Desktop.
Bypass seccomp-bpf based memory-deny-write-execute with `READ_IMPLIES_EXEC`

Bypass seccomp-bpf based memory-deny-write-execute with READ_IMPLIES_EXEC

The most seccomp-bpf based memory-deny-write-execute implementations can be bypassed. Well known ways to bypass them are

  1. mapping a file from a filesystem that is not mounted noexec (e.g. /dev/shm).
  2. memfd_create
  • systemd recommends to deny memfd_create with SystemCallFilter=~memfd_create.
  • firejail's memory-deny-write-execute implementation denies memfd_create. This is more secure but makes memory-deny-write-execute unusable for programs that need (non-exec) memfd_create like the most GNOME programs.
  • syd emulates memfd_create by using MFD_NOEXEC_SEAL by default.
  • crablock allows to deny executable memfds with --seccomp-memfd-noexec or deny memfd_create entirely with --seccomp-syscall-filter.
  1. shmat on architectures that multiplex it through ipc.
  • firejail can block secondary architectures with seccomp.block-secondary.
  • systemd recommends to set SystemCallArchitectures=native.
  • crablock does not allow secondary architectures in its seccomp filters.
  • If the primary architecture multiplexes it, nothing can be done.

However there is a fourth bypass that is neither mentioned in the documentation of firejail or systemd nor blocked by their seccomp-bpf based mdwe implementations. The READ_IMPLIES_EXEC personality flag that can be used to create a mapping that is executable without requesting PROT_EXEC in the mmap call.

PROT_WRITE | PROT_EXEC: FAILED
PROT_READ | PROT_WRITE: SUCCESS: 7f48d8515000-7f48d8516000 rw-p 00000000 00:00 0
Set personality READ_IMPLIES_EXEC.
PROT_WRITE | PROT_EXEC: FAILED
PROT_READ | PROT_WRITE: SUCCESS: 7f48d8514000-7f48d8515000 rwxp 00000000 00:00 0
Mapping is W&X! 🧨
  • systemd's MemoryDenyWriteExecute=yes uses PR_MDWE_REFUSE_EXEC_GAIN if available which can not be bypassed that way. Otherwise you can (and should always) use LockPersonality=yes.
  • firejail denyies personality with seccomp unless allow-debuggers is used.
  • crablock's --seccomp-memory-deny-write-execute denies calls to personality that try to set READ_IMPLIES_EXEC. crablock also implements --mdwe-refuse-exec-gain which can not be bypassed that way.

Copying and distribution of this file, with or without modification, are permitted in any medium without royalty provided the copyright notice and this notice are preserved. This file is offered as-is, without any warranty.
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "libc"
version = "0.2.158"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439"
[[package]]
name = "mmap_wx"
version = "0.1.0"
dependencies = [
"libc",
]
[package]
name = "mmap_wx"
version = "0.1.0"
edition = "2021"
[[bin]]
name = "mmap_wx"
path = "./main.rs"
[dependencies]
libc = "0.2"
// SPDX-License-Identifier: 0BSD
/*
* Copyright © 2024 rusty-snake
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
* REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
* INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
* LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
* OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
* PERFORMANCE OF THIS SOFTWARE.
*/
use std::fs;
use std::ptr;
fn main() {
mmap_wx();
mmap_rw();
personality_read_implies_exec();
mmap_wx();
mmap_rw();
}
fn personality_read_implies_exec() {
println!("Set personality READ_IMPLIES_EXEC.");
assert!(unsafe { libc::personality(libc::READ_IMPLIES_EXEC as u64) } == 0);
}
fn mmap_wx() {
let r = unsafe {
libc::mmap(
ptr::null_mut(),
4096,
libc::PROT_WRITE | libc::PROT_EXEC,
libc::MAP_PRIVATE | libc::MAP_ANONYMOUS,
-1,
0,
)
};
if r == libc::MAP_FAILED {
println!("PROT_WRITE | PROT_EXEC: FAILED");
} else {
let addr = format!("{:x}", r as usize);
let maps_entry = fs::read_to_string("/proc/self/maps")
.unwrap()
.lines()
.find(|line| line.starts_with(&addr))
.unwrap()
.to_string();
println!("PROT_WRITE | PROT_EXEC: SUCCESS: {maps_entry}");
}
}
fn mmap_rw() {
let r = unsafe {
libc::mmap(
ptr::null_mut(),
4096,
libc::PROT_READ | libc::PROT_WRITE,
libc::MAP_PRIVATE | libc::MAP_ANONYMOUS,
-1,
0,
)
};
if r == libc::MAP_FAILED {
println!("PROT_READ | PROT_WRITE: FAILED");
} else {
let addr = format!("{:x}", r as usize);
let maps_entry = fs::read_to_string("/proc/self/maps")
.unwrap()
.lines()
.find(|line| line.starts_with(&addr))
.unwrap()
.to_string();
println!("PROT_READ | PROT_WRITE: SUCCESS: {maps_entry}");
if maps_entry.contains("rwxp") {
println!("Mapping is W&X! 🧨")
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment