Linux default behavior of executable .data section changed between 5.4 and 5.9?
Asked Answered
O

2

10

Story

Case 1

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.

Case 2:

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.

Code

[Linux 5.4.0-53-generic]

Run (normal)

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$ 

Check Permission (00400000-00402000 rwxp), so it is executable.

## 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➤

[Linux 5.9.0-050900rc5-generic]

Run (Segfault)

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# 

Check Permission (00400000-00402000 rw-p), so it is NOT executable.

## 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➤  

objdump -p

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-

Questions

  1. Where is the configuration on Linux that manages default ELF sections permission?
  2. Are my observations on permissions correct?

Summary

  • Default permission for .data section on Linux 5.4.0-53-generic is executable.
  • Default permission for .data section on Linux 5.9.0-050900rc5-generic is NOT executable.
Oram answered 14/11, 2020 at 12:7 Comment(6)
It's more likely to be binutils version difference, not a kernel as you seem to have done your tests on different machines with different binaries. Copy the binary from one system to the other and check like that.Labile
@Labile I just verified that the ld version on those machines are the same GNU ld (GNU Binutils for Ubuntu) 2.34. I also checked the md5sum $(which ld) and got 702d6893f3910cae8f155bc8aec5a10d /usr/bin/ld which is the same on those machines. Could you give more specific advice on binary file should I check?Oram
Oh okay I somehow missed the md5sum of the binary is the same. Sorry. Back to reading the kernel git log then :DLabile
Please post the output of objdump -p on the binary.Inaction
@Inaction Ok, edited.Oram
Related: Why data and stack segments are executable? has the GAS and NASM syntax for adding a PT_GNU_STACK segment with the standard noexec setting.Diseuse
A
9

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).

Apanage answered 14/11, 2020 at 19:4 Comment(4)
In even older kernels (some time before 5.4), PT_GNU_STACK == RWX (like a modern build with gcc -z execstack, not missing entirely) also resulted in the READ_IMPLIES_EXEC personality, making .data executable. How to get c code to execute hex machine code? shows some ways that used to work to run shellcode, and various old shellcode tutorials have used global non-const arrays with gcc -z execstack. (IIRC, I tested this myself to be sure that a binary with PT_GNU_STACK = RWX has write+exec .data on an older kernel)Diseuse
Yeah, Unexpected exec permission from mmap when assembly files included in the project has details of what I tested on x86-64 Linux 4.5 with gcc -z execstack from a C program, not NASM + ld, and verified that there was a PT_GNU_STACK = RWE (Exec) in the binary. (Although even ld includes a RWE .note section if there isn't one in the .o)Diseuse
@PeterCordes Based on what you've written here and in the questions you link, I think you're missing an important point in Margaret's answer. AFAICT, already in Linux 5.4 we have that PT_GNU_STACK == RWX gives READ_IMPLIES_EXEC. This behavior doesn't change until Linux 5.8-rc1. The three relevant commits were made April 20, 2020, as can be seen in this commit history of the relevant file.Pejoration
The first commit documents the then current behavior, the second commit ensures PT_GNU_STACK == RWX only gives an executable stack, while the third commit ensures a missing PT_GNU_STACK no longer gives READ_IMPLIES_EXEC for x86-64.Pejoration
D
10

Your binary is missing PT_GNU_STACK. As such, this change appears to have been caused by commit 9fccc5c0c99f238aa1b0460fccbdb30a887e7036:

From 9fccc5c0c99f238aa1b0460fccbdb30a887e7036 Mon Sep 17 00:00:00 2001
From: Kees Cook <[email protected]>
Date: Thu, 26 Mar 2020 23:48:17 -0700
Subject: x86/elf: Disable automatic READ_IMPLIES_EXEC on 64-bit

With modern x86 64-bit environments, there should never be a need for
automatic READ_IMPLIES_EXEC, as the architecture is intended to always
be execute-bit aware (as in, the default memory protection should be NX
unless a region explicitly requests to be executable).

There were very old x86_64 systems that lacked the NX bit, but for those,
the NX bit is, obviously, unenforceable, so these changes should have
no impact on them.

Suggested-by: Hector Marco-Gisbert <[email protected]>
Signed-off-by: Kees Cook <[email protected]>
Signed-off-by: Borislav Petkov <[email protected]>
Reviewed-by: Jason Gunthorpe <[email protected]>
Link: https://lkml.kernel.org/r/[email protected]
---
 arch/x86/include/asm/elf.h | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/arch/x86/include/asm/elf.h b/arch/x86/include/asm/elf.h
index 397a1c74433ec..452beed7892bb 100644
--- a/arch/x86/include/asm/elf.h
+++ b/arch/x86/include/asm/elf.h
@@ -287,7 +287,7 @@ extern u32 elf_hwcap2;
  *                 CPU: | lacks NX*  | has NX, ia32     | has NX, x86_64 |
  * ELF:                 |            |                  |                |
  * ---------------------|------------|------------------|----------------|
- * missing PT_GNU_STACK | exec-all   | exec-all         | exec-all       |
+ * 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      |
  *
@@ -303,7 +303,7 @@ extern u32 elf_hwcap2;
  *
  */
 #define elf_read_implies_exec(ex, executable_stack)    \
-   (executable_stack == EXSTACK_DEFAULT)
+   (mmap_is_ia32() && executable_stack == EXSTACK_DEFAULT)
 
 struct task_struct;
 
-- 
cgit 1.2.3-1.el7

This was first present in the 5.8 series. See also Unexpected exec permission from mmap when assembly files included in the project.

Denbrook answered 14/11, 2020 at 18:58 Comment(4)
Oh snap, I didn't see your answer while I was writing mine. Good finding!Apanage
There were very old x86_64 systems that lacked the NX bit -- wait what? I thought the x86-64 page-table format has always been the PAE-derived one with space for an exec-permission bit in the PTE. I could believe that some old kernel version that doesn't take advantage of the hardware capability might have existed, although 32-bit PAE (and NX support for it in Linux) preceded x86-64 so probably not. And it wouldn't make sense to mention that in the context of a kernel change, unless talking about backporting the patch to ancient kernels?Diseuse
Possibly virtual hardware with broken NX bits for the guest page tables? IDK, sounds weird. But I guess it's plausible that some x86-64 hardware failed to implement exec permissions separate from read, especially if it was "bolted on" to an existing microarchitecture, like maybe P4 (netburst)? Maybe I'll google tomorrow if nobody else finds anything.Diseuse
Update: Wikipedia claims that very early Intel64 (presumably P4 Nocona) had x86-64 but without a working NX bit. en.wikipedia.org/wiki/X86-64#History_2 says the first non-Xeon Prescott cores shipped with Intel64 disabled, and the E0 stepping enabled both Intel64 and the XD (aka NX) bit. So it's possible an earlier stepping of the uarch was in Nocona Xeon with Intel64 enabled did have Intel64 which didn't respect the NX bit in the page-table format. /sigh. Given that changelog, presumably it's real.Diseuse
A
9

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).

Apanage answered 14/11, 2020 at 19:4 Comment(4)
In even older kernels (some time before 5.4), PT_GNU_STACK == RWX (like a modern build with gcc -z execstack, not missing entirely) also resulted in the READ_IMPLIES_EXEC personality, making .data executable. How to get c code to execute hex machine code? shows some ways that used to work to run shellcode, and various old shellcode tutorials have used global non-const arrays with gcc -z execstack. (IIRC, I tested this myself to be sure that a binary with PT_GNU_STACK = RWX has write+exec .data on an older kernel)Diseuse
Yeah, Unexpected exec permission from mmap when assembly files included in the project has details of what I tested on x86-64 Linux 4.5 with gcc -z execstack from a C program, not NASM + ld, and verified that there was a PT_GNU_STACK = RWE (Exec) in the binary. (Although even ld includes a RWE .note section if there isn't one in the .o)Diseuse
@PeterCordes Based on what you've written here and in the questions you link, I think you're missing an important point in Margaret's answer. AFAICT, already in Linux 5.4 we have that PT_GNU_STACK == RWX gives READ_IMPLIES_EXEC. This behavior doesn't change until Linux 5.8-rc1. The three relevant commits were made April 20, 2020, as can be seen in this commit history of the relevant file.Pejoration
The first commit documents the then current behavior, the second commit ensures PT_GNU_STACK == RWX only gives an executable stack, while the third commit ensures a missing PT_GNU_STACK no longer gives READ_IMPLIES_EXEC for x86-64.Pejoration

© 2022 - 2024 — McMap. All rights reserved.