I'm dabbling in Cyber Security, particularly the topic of buffer overflows. To this end, I'm considering an example in which a buffer overflow allows an attacker to execute arbitrary code from within a statically allocated message buffer (i.e. somewhere in the data segment). Regrettably, my efforts fail with segmentation faults, near guaranteed because of missing execution rights on that data segment (my attack does run properly on an executable stack).
I am well aware that there are options to mark a program as requiring an executable stack, which will be heeded by the kernel when loading the binary. Namely, you can create such a binary with
gcc -z execstack <source> -o <binary>
In the past, this actually sufficed to also receive an executable heap in Linux, since apparently all readable pages were then treated as executable pages. But with more modern kernels, pretty much only the stack becomes executable with this option.
I'd also like to mention two previous questions from the Stackexchange network relating to this topic:
mprotect
systemcall which allows manually redefining the traits of pages from within the program. Unfortunately, this approach is not applicable for my purpose, as I don't have any such control over the program and am trying to establish the ability to execute code in the first place.-z execstack
linker option when compiling the binary. Unfortunately, the question itself (what lead to this change) remains unanswered.So then: Is there any way in modern Linux to run a program without executability protections on the segments, without having to change the program code itself? Failing that, is there a way to at least make the heap / data segment executable? Or can it be constituted that only the stack can be made executable in general?
There isn't really a way to do this nicely. Modern Linux parted ways with read-implies-exec years ago in v5.8 (see patch here). Without touching the binary (either its ELF headers or its code) there isn't a general way to restore the old behavior. Certain tricks may still work on e.g. 32-bit binaries, old CPUs or weird architectures but that's about it. An example is x86 32-bit ELFs lacking explicit stack protection flags, which will still have read-implies-exec today (see table here), though modern compilers usually shouldn't omit stack protection flags by default.
I have been teaching and writing challenges for cybersecurity courses focused on binary exploitations at different levels for a while, and this change definitely warranted an update to the whole teaching setup for entry level binary exploitation. Nowadays, it is not realistic to go with the good ol' "everything is executable" assumption that would permit to jump to shellcode anywhere. Of course, there are still situations where read-implies-exec or full RWX/unprotected memory are a thing, like embedded environments, but in general this cannot be assumed anymore. Exploitation (thankfully) got harder.
At the end of the day, anything is possible if you have root privileges and the ability to load custom kernel modules. After all, it isn't unthinkable to want to play around like in the old days on a toy VM (I would definitely not recommend doing this on your actual machine).
If you want to have fun, here's a kernel module that I just wrote for x86-64 that re-enables read-implies-exec via kprobes for the whole system when inserted (you need a kernel with the default CONFIG_KPROBES=y
for this to work):
// SPDX-License-Identifier: (GPL-2.0 OR MIT)
/**
* Restore old read-implies-exec kernel behavior via a kprobes hack: hook into
* setup_new_exec() to set the READ_IMPLIES_EXEC personality flag for the
* current task and into setup_arg_pages() to force executable_stack=EXSTACK_ENABLE_X.
*/
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/kprobes.h>
#include <linux/binfmts.h>
#ifndef CONFIG_X86_64
#error "This module only supports x86-64"
#endif
#ifdef pr_fmt
#undef pr_fmt
#endif
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
static int kp_setup_new_exec_pre(struct kprobe *kp, struct pt_regs *regs)
{
current->personality |= READ_IMPLIES_EXEC;
return 0;
}
static int kp_setup_arg_pages_pre(struct kprobe *kp, struct pt_regs *regs)
{
regs->dx = EXSTACK_ENABLE_X;
return 0;
}
static struct kprobe kps[] = {
{ .pre_handler = kp_setup_arg_pages_pre, .symbol_name = "setup_arg_pages" },
{ .pre_handler = kp_setup_new_exec_pre, .symbol_name = "setup_new_exec" },
};
static int __init modinit(void)
{
int ret;
for (unsigned i = 0; i < ARRAY_SIZE(kps); i++) {
ret = register_kprobe(&kps[i]);
if (ret < 0) {
pr_err("Failed to register kprobe for %s: %d\n",
kps[i].symbol_name, ret);
return -1;
}
pr_info("Registered kprobe for %s\n", kps[i].symbol_name);
}
pr_warn("Your system now runs with old read-implies-exec semantics!\n");
return 0;
}
static void __exit modexit(void)
{
for (unsigned i = 0; i < ARRAY_SIZE(kps); i++) {
unregister_kprobe(&kps[i]);
pr_info("Unregistered kprobe for %s\n", kps[i].symbol_name);
}
}
module_init(modinit);
module_exit(modexit);
MODULE_VERSION("0.1");
MODULE_DESCRIPTION("Restore old read-implies-exec behavior via a kprobes hack");
MODULE_AUTHOR("Marco Bonelli");
MODULE_LICENSE("Dual MIT/GPL");
Test on Linux v6.12 on a x86-64 QEMU VM with busybox:
/ # cat /proc/self/maps
00400000-00401000 r--p 00000000 00:02 6 /bin/busybox
00401000-005bc000 r-xp 00001000 00:02 6 /bin/busybox
005bc000-0067d000 r--p 001bc000 00:02 6 /bin/busybox
0067e000-00688000 rw-p 0027d000 00:02 6 /bin/busybox
00688000-0068b000 rw-p 00000000 00:00 0
30b03000-30b26000 rw-p 00000000 00:00 0 [heap]
7f845aeed000-7f845aef1000 r--p 00000000 00:00 0 [vvar]
7f845aef1000-7f845aef3000 r-xp 00000000 00:00 0 [vdso]
7fffe91a8000-7fffe91c9000 rw-p 00000000 00:00 0 [stack]
ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0 [vsyscall]
/ # insmod read_implies_exec.ko
[ 8.195897] read_implies_exec: loading out-of-tree module taints kernel.
[ 8.200370] read_implies_exec: Registered kprobe for setup_arg_pages
[ 8.201108] read_implies_exec: Registered kprobe for setup_new_exec
[ 8.201497] read_implies_exec: Your system now runs with old read-implies-exec semantics!READ_IMPLIES_EXEC personality.
/ # cat /proc/self/maps
00400000-0067d000 r-xp 00000000 00:02 6 /bin/busybox
0067e000-00688000 rwxp 0027d000 00:02 6 /bin/busybox
00688000-0068b000 rwxp 00000000 00:00 0
26519000-2653c000 rwxp 00000000 00:00 0 [heap]
7f75b7d91000-7f75b7d95000 r--p 00000000 00:00 0 [vvar]
7f75b7d95000-7f75b7d97000 r-xp 00000000 00:00 0 [vdso]
7fff29ba3000-7fff29bc4000 rwxp 00000000 00:00 0 [stack]
ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0 [vsyscall]