It’s a binary compiled by C. I wanted to run some code on heap,but I got a segment fault. Then I use gdb to check the binary. It showed the NX was disabled (complied with -z execstack
option) and I had RWX access on stack segment while I could only have RW access on heap segment.This happened on a linux 5.10 kernel debian. Then I ran this binary on a linxu 5.10 kernel Alpine Docker on my Macbook, it happened,too.
I remembered that I could execute code in heap a year before. And I tried to run this binary again on an old Ubuntu18 with linux 5.0 kernel. It returned successfully. The codes in stack were executed without error.
In all, my question is if there is any new feature updated from linux 5.0 to linux 5.10 which leads to the heap not executable anymore.
2
Answers
When ELF binaries are linked with the
execstack
linker option, the permissions for the ELF segmentGNU_STACK
are changed fromRW
(read and write) toRWE
(read, write, and execute).When the Linux kernel loads an ELF binary executable, it looks at the GNU_STACK segment to see if the binary wants an executable stack. See fs/binfmt_elf.c:load_elf_binary() for details, especially the
executable_stack
variable, and theEXSTACK_DEFAULT
macro. Depending on the kernel configuration, it may or may not make the stack executable.This is an often forgotten backwards compatibility support feature, which can also affect other memory mappings beyond just stacks. Here is an example program one can compile and run, to verify:
If you save the above as say
check.c
, you can compile it using for exampleRunning an Ubuntu 5.4.0-74-generic kernel on x86-64, the
.default
versions (without execstack linker option) report:except that because this kernel has address space randomization enabled, the exact addresses above will vary from execution to execution (and this is what we normally want, too); and GCC and Clang tend to use slightly different address ranges, but that too is fine.
As you can see, no data (stack, heap, allocations, or anonymous memory maps without
PROT_EXEC
) is executable.Running the
.execstack
binaries, however, report:This means that the ELF GNU_STACK segment protections are not applied to the stack only, but basically all allocations the process can make. Even requesting read-write, non-executable anonymous memory, gives the process executable memory.
The question is, does the original asker really see different output running the
.execstack
/-z execstack
binaries on different kernel versions, or have they fallen foul of Clang’s helpfulness?You see, if you copy one of the many "here’s how you can prove you can run code on stack in Linux by default" code snippets which include things like
volatile unsigned char injected[] = { 0xf3, 0xc3 };
(the two bytes being the minimal implementation for a Cvoid nothing(void) { return; }
function), Clang is helpful and instead of puttinginjected[]
on the heap or stack, it will try and put it in the code section instead. In other words, when creating such code, one must always examine the generated machine code to see where it actually puts the code bytes to be executed, and whether it executes those or just a copy in a code segment.I admit I was a bit peeved to see the initial claim, because it looked very much like someone trying to prove Linux does not default to proper non-executable stack and data regions, by using silly code that does not do what they think they do –– this is what well over half of such code "examples" you find on the internet are: garbage, mistakes, and sheer lies.
However, given the crucial tidbit that all this is related to the
execstack
linker option (implemented for backwards compatibility, so that users who want to run programs that only work if they have executable stack, can do so), that changes the question from being related to stack smashing/hacking, into a possible backwards compatibility issue. We do care about those, you see.Hopefully, OP will report whether they do really see different output of the above test program on different kernel versions with
execstack
linker option enabled.If they do, the next step is to pinpoint the change. It must be later than 5.4.0 (roughly November 2019), and is almost certainly in either fs/exec.c or fs/binfmt_elf.c –– these being links to their modification history. There are a lot of changes done to these in the last two years, so installing additional kernel versions to bracket the version which introduced the change in behaviour would probably be faster than poring over those changesets.
I’m doing self-study handout on CS:APP’s 4th lab and may get the same problem. Challenge level2 requires to change a global vriable. Most of previous writeups inject some shellcode which executes on the RWX stack, and the stack is actually a heap malloced by programmer. Previous writeups use the buffer to write shellcode and let return address point to shellcode in buffer. However, when I tried the same way on my Ubuntu 22.04 (linux 5.15.0-46-generic) today, a SIGSEGV appeared. The weird thing is that it works on my Archlinux successfully, but that happened in March, 2022. I have no idea if it would work now.