I accidentally wrote my Assembly code in the .data
section. I compiled it and executed it. The program ran normally under Linux 5.4.0-53-generic
even though I didn't specify a flag like execstack
.
After that, I executed the program under Linux 5.9.0-050900rc5-generic
. The program got SIGSEGV
. I inspected the virtual memory permission by reading /proc/$pid/maps
. It turned out that the section is not executable.
I think there is a configuration on Linux that manages that permission. But I don't know where to find.
ammarfaizi2@integral:/tmp$ uname -r
5.4.0-53-generic
ammarfaizi2@integral:/tmp$ cat test.asm
[section .data]
global _start
_start:
mov eax, 60
xor edi, edi
syscall
ammarfaizi2@integral:/tmp$ nasm --version
NASM version 2.14.02
ammarfaizi2@integral:/tmp$ nasm -felf64 test.asm -o test.o
ammarfaizi2@integral:/tmp$ ld test.o -o test
ammarfaizi2@integral:/tmp$ ./test
ammarfaizi2@integral:/tmp$ echo $?
0
ammarfaizi2@integral:/tmp$ md5sum test
7ffff5fd44e6ff0a278e881732fba525 test
ammarfaizi2@integral:/tmp$
## Debug
gef➤ shell cat /proc/`pgrep test`/maps
00400000-00402000 rwxp 00000000 08:03 7471589 /tmp/test
7ffff7ffb000-7ffff7ffe000 r--p 00000000 00:00 0 [vvar]
7ffff7ffe000-7ffff7fff000 r-xp 00000000 00:00 0 [vdso]
7ffffffde000-7ffffffff000 rwxp 00000000 00:00 0 [stack]
ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0 [vsyscall]
gef➤
root@esteh:/tmp# uname -r
5.9.0-050900rc5-generic
root@esteh:/tmp# cat test.asm
[section .data]
global _start
_start:
mov eax, 60
xor edi, edi
syscall
root@esteh:/tmp# nasm --version
NASM version 2.14.02
root@esteh:/tmp# nasm -felf64 test.asm -o test.o
root@esteh:/tmp# ld test.o -o test
root@esteh:/tmp# ./test
Segmentation fault (core dumped)
root@esteh:/tmp# echo $?
139
root@esteh:/tmp# md5sum test
7ffff5fd44e6ff0a278e881732fba525 test
root@esteh:/tmp#
## Debug
gef➤ shell cat /proc/`pgrep test`/maps
00400000-00402000 rw-p 00000000 fc:01 2412 /tmp/test
7ffff7ff9000-7ffff7ffd000 r--p 00000000 00:00 0 [vvar]
7ffff7ffd000-7ffff7fff000 r-xp 00000000 00:00 0 [vdso]
7ffffffde000-7ffffffff000 rw-p 00000000 00:00 0 [stack]
ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0 [vsyscall]
gef➤
root@esteh:/tmp# objdump -p test
test: file format elf64-x86-64
Program Header:
LOAD off 0x0000000000000000 vaddr 0x0000000000400000 paddr 0x0000000000400000 align 2**12
filesz 0x0000000000001009 memsz 0x0000000000001009 flags rw-
.data
section on Linux 5.4.0-53-generic
is executable..data
section on Linux 5.9.0-050900rc5-generic
is NOT executable.This is only a guess: I think the culprit is the READ_IMPLIES_EXEC
personality that was being set automatically in the absence of a PT_GNU_STACK
segment.
In the 5.4 kernel source we can find this piece of code:
SET_PERSONALITY2(loc->elf_ex, &arch_state);
if (elf_read_implies_exec(loc->elf_ex, executable_stack))
current->personality |= READ_IMPLIES_EXEC;
That's the only thing that can transform an RW section into an RWX one. Any other use of PROC_EXEC
didn't seem to be changed or relevant to this question, to me.
The executable_stack
is set here:
for (i = 0; i < loc->elf_ex.e_phnum; i++, elf_ppnt++)
switch (elf_ppnt->p_type) {
case PT_GNU_STACK:
if (elf_ppnt->p_flags & PF_X)
executable_stack = EXSTACK_ENABLE_X;
else
executable_stack = EXSTACK_DISABLE_X;
break;
But if the PT_GNU_STACK
segment is not present, that variable retains its default value:
int executable_stack = EXSTACK_DEFAULT;
Now this workflow is identical in both 5.4 and the latest kernel source, what changed is the definition of elf_read_implies_exec
:
Linux 5.4:
/*
* An executable for which elf_read_implies_exec() returns TRUE will
* have the READ_IMPLIES_EXEC personality flag set automatically.
*/
#define elf_read_implies_exec(ex, executable_stack) \
(executable_stack != EXSTACK_DISABLE_X)
Latest Linux:
/*
* An executable for which elf_read_implies_exec() returns TRUE will
* have the READ_IMPLIES_EXEC personality flag set automatically.
*
* The decision process for determining the results are:
*
* CPU: | lacks NX* | has NX, ia32 | has NX, x86_64 |
* ELF: | | | |
* ---------------------|------------|------------------|----------------|
* missing PT_GNU_STACK | exec-all | exec-all | exec-none |
* PT_GNU_STACK == RWX | exec-stack | exec-stack | exec-stack |
* PT_GNU_STACK == RW | exec-none | exec-none | exec-none |
*
* exec-all : all PROT_READ user mappings are executable, except when
* backed by files on a noexec-filesystem.
* exec-none : only PROT_EXEC user mappings are executable.
* exec-stack: only the stack and PROT_EXEC user mappings are executable.
*
* *this column has no architectural effect: NX markings are ignored by
* hardware, but may have behavioral effects when "wants X" collides with
* "cannot be X" constraints in memory permission flags, as in
* https://lkml.kernel.org/r/[email protected]
*
*/
#define elf_read_implies_exec(ex, executable_stack) \
(mmap_is_ia32() && executable_stack == EXSTACK_DEFAULT)
Note how in the 5.4 version the elf_read_implies_exec
returned a true value if the stack was not explicitly marked as not executable (via the PT_GNU_STACK
segment).
In the latest source, the check is now more defensive: the elf_read_implies_exec
is true only on 32-bit executable, in the case where no PT_GNU_STACK
segment was found in the ELF binary.
I assembled your program, linked it, and found no PT_GNU_STACK
segment, so this may be the reason.
If this is indeed the issue and if I followed the code correctly, if you set the stack as not executable in the binary, its data section should not be mapped executable anymore (not even on Linux 5.4).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With