getenv
doesn't store the env var's value on the stack. It's already on the stack from process startup, and getenv
obtains a pointer to it.
See the i386 System V ABI's description of where argv[] and envp[] are located at process startup: above [esp]
.
_start
doesn't copy them before calling main
, just calculates pointers to them to pass as args to main
. (Links to the latest version at https://github.com/hjl-tools/x86-psABI/wiki/X86-psABI, where the official current version is maintained.)
Your code is casting a pointer to stack memory (containing the value of an env var) into a function pointer and calling through it. Look at the compiler-generated asm (e.g. on https://godbolt.org/): it'll be something like call getenv
/ call eax
.
-zexecstack
in your kernel version1 makes all your pages executable, not just the stack. It also applies to .data
, .bss
, and .rodata
sections, and memory allocated with malloc
/ new
.
The exact mechanism on GNU/Linux was a "read-implies-exec" process-wide flag that affects all future allocations, including manual use of mmap
. See Unexpected exec permission from mmap when assembly files included in the project for more about the GNU_STACK
ELF header stuff.
Footnote 1: Linux after 5.4 or so only makes the stack itself executable, not READ_IMPLIES_EXEC: Linux default behavior of executable .data section changed between 5.4 and 5.9?
Fun fact: taking the address of a nested function that accesses its parents local variables gets gcc to enable -zexecstack
. It stores code for an executable "trampoline" onto the stack that passes a "static chain" pointer to the actual nested function, allowing it to reference its parent's stack-frame.
If you wanted to exec data as code without -zexecstack
, you'd use mprotect(PROT_EXEC|PROT_READ|PROT_WRITE)
on the page containing that env var. (It's part of your stack so you shouldn't remove write permission; it could be in the same page as main's stack frame for example.)
Related:
With GNU/Linux ld
from binutils before late 2018 or so, the .rodata
section is linked into the same ELF segment as the .text
section, and thus const char code[] = {0xc3}
or string literals are executable.
Current ld
gives .rodata
its own segment that's mapped read without exec, so finding ROP / Spectre "gadgets" in read-only data is no longer possible, unless you use -zexecstack
. And even that doesn't work on current kernels; char code[] = ...;
as a local inside a function will put data on the stack where it's actually executable. See How to get c code to execute hex machine code? for details.
gdb
and/proc/<pid>/maps
(to check permissions etc on the sections) – Craigcraighead