Repository: Gallopsled/pwntools

Issue:
rwx checksec in response to recent linux kernel update
https://github.com/torvalds/linux/commit/9fccc5c0c99f238aa1b0460fccbdb30a887e7036

![image](https://user-images.githubusercontent.com/76640319/122515722-6e2f2580-d040-11eb-9e44-189d1cf574d0.png)

With the recently implemented Linux Kernel update being released onto the most recent Linux distros i.e. Ubuntu 20.04.2 LTS and Kali 2021.1, it appears that the default linux behavior to x86_64 binaries has been to turn off all **rwx** segments by default. 

This means that even if binaries do not disable the rwx, the linux kernel will do it for them. This has been implemented in the 5.8.0-55-generic kernel version. However, **checksec** appears to still mention **RWX: Has RWX semgents** when doing **checksec**. 

Perhaps this could be patched and pwntools could instead throw a separate warning 

**RWX may be present but has been disabled by the kernel.** 

Any opinion?


Discussion:
The fix for this is ultimately in `ELF.nx` which is currently a boolean.  To know whether this patch is applied, we have to know the kernel version.  This works nicely for "local" exploits, but not well for knowing what the remote system is running on.

It'll take some re-architecting to fix this and add a `warn_once` somewhere.

Not sure if the warning should be emitted on `ELF.nx` access (which is ultimately used by e.g. `rwx_segments`) or just on `ELF.checksec`.

If we have an `ssh` session we can trivially check the server kernel version, but for something that requires a TCP connection to a server (e.g. `remote()`) there's no way to know.

Need to think about how to handle this optimally.

Ideas / Problems:
* Add an `ssh=` optarg to the ELF constructor
* Add an e.g. `context.uname` or `context.kernel_version` (we already have `context.kernel`)
    * Different vendors may have kernel versions which *should* have this patch, but may not have adopted it (or back ported it)
* Just ALWAYS warn about it (and suggest setting `context.kernel_version`)
* Warn about it in `ssh.checksec`
* How should `ELF.rwx_segments` behave?

Resolving patch:
diff --git a/pwnlib/elf/elf.py b/pwnlib/elf/elf.py
--- a/pwnlib/elf/elf.py
+++ b/pwnlib/elf/elf.py
@@ -49,7 +49,6 @@
 
 from collections import namedtuple
 
-from elftools.elf.constants import E_FLAGS
 from elftools.elf.constants import P_FLAGS
 from elftools.elf.constants import SHN_INDICES
 from elftools.elf.descriptions import describe_e_type
@@ -1676,61 +1675,244 @@ def nx(self):
         Specifically, we are checking for ``READ_IMPLIES_EXEC`` being set
         by the kernel, as a result of honoring ``PT_GNU_STACK`` in the kernel.
 
-        The **Linux kernel** directly honors ``PT_GNU_STACK`` to `mark the
-        stack as executable.`__
+        ``READ_IMPLIES_EXEC`` is set, according to a set of architecture specific
+        rules, that depend on the CPU features, and the presence of ``PT_GNU_STACK``.
+
+        Unfortunately, :class:`ELF` is not context-aware, so it's not always possible
+        to determine whether the process of a binary that's missing ``PT_GNU_STACK``
+        will have NX or not.
+        
+        The rules are as follows:
+
+            +-----------+--------------+---------------------------+------------------------------------------------+----------+
+            | ELF arch  | linux        | GNU_STACK                 | other                                          | NX       |
+            +===========+==============+===========================+================================================+==========+
+            | i386      | < 5.8        | non-exec                  |                                                | enabled  |
+            |           | [#x86_5.7]_  +---------------------------+------------------------------------------------+----------+
+            |           |              | exec / missing            |                                                | disabled |
+            |           +--------------+---------------------------+------------------------------------------------+----------+
+            |           | >= 5.8       | exec / non-exec           |                                                | enabled  |
+            |           | [#x86_5.8]_  +---------------------------+------------------------------------------------+----------+
+            |           |              | missing                   |                                                | disabled |
+            +-----------+--------------+---------------------------+------------------------------------------------+----------+
+            | amd64     | < 5.8        | non-exec                  |                                                | enabled  |
+            |           | [#x86_5.7]_  +---------------------------+------------------------------------------------+----------+
+            |           |              | exec / missing            |                                                | disabled |
+            |           +--------------+---------------------------+------------------------------------------------+----------+
+            |           | >= 5.8       | exec / non-exec / missing |                                                | enabled  |
+            |           | [#x86_5.8]_  |                           |                                                |          |
+            +-----------+--------------+---------------------------+------------------------------------------------+----------+
+            | arm       | < 5.8        | non-exec*                 |                                                | enabled  |
+            |           | [#arm_5.7]_  +---------------------------+------------------------------------------------+----------+
+            |           |              | exec / missing            |                                                | disabled |
+            |           +--------------+---------------------------+------------------------------------------------+----------+
+            |           | >= 5.8       | exec / non-exec*          |                                                | enabled  |
+            |           | [#arm_5.8]_  +---------------------------+------------------------------------------------+----------+
+            |           |              | missing                   |                                                | disabled |
+            +-----------+--------------+---------------------------+------------------------------------------------+----------+
+            | mips      | < 5.18       | non-exec*                 |                                                | enabled  |
+            |           | [#mips_5.17]_+---------------------------+------------------------------------------------+----------+
+            |           |              | exec / missing            |                                                | disabled |
+            |           +--------------+---------------------------+------------------------------------------------+----------+
+            |           | >= 5.18      | exec / non-exec*          |                                                | enabled  |
+            |           | [#mips_5.18]_+---------------------------+------------------------------------------------+----------+
+            |           |              | missing                   |                                                | disabled |
+            +-----------+--------------+---------------------------+------------------------------------------------+----------+
+            | powerpc   | [#powerpc]_  | non-exec / exec           |                                                | enabled  |
+            |           |              +---------------------------+------------------------------------------------+----------+
+            |           |              | missing                   |                                                | disabled |
+            +-----------+--------------+---------------------------+------------------------------------------------+----------+
+            | powerpc64 | [#powerpc]_  | exec / non-exec / missing |                                                | enabled  |
+            +-----------+--------------+---------------------------+------------------------------------------------+----------+
+            | ia64      | [#ia64]_     | non-exec                  |                                                | enabled  |
+            |           |              +---------------------------+------------------------------------------------+----------+
+            |           |              | exec / missing            | e_flags & EF_IA_64_LINUX_EXECUTABLE_STACK == 0 | enabled  |
+            |           |              +                           +------------------------------------------------+----------+
+            |           |              |                           | e_flags & EF_IA_64_LINUX_EXECUTABLE_STACK != 0 | disabled |
+            +-----------+--------------+---------------------------+------------------------------------------------+----------+
+            | the rest  | [#the_rest]_ | exec / non-exec / missing |                                                | enabled  |
+            +-----------+--------------+---------------------------+------------------------------------------------+----------+
+
+            \\* Hardware limitations are ignored.
+
+        If ``READ_IMPLIES_EXEC`` is set, then `all readable pages are executable`__.
+            .. __: https://github.com/torvalds/linux/blob/v6.3/fs/binfmt_elf.c#L1008-L1009
+            .. code-block:: c
+
+                if (elf_read_implies_exec(loc->elf_ex, executable_stack))
+                    current->personality |= READ_IMPLIES_EXEC;
+
+        .. [#x86_5.7]
+            `source <https://github.com/torvalds/linux/blob/v5.7/arch/x86/include/asm/elf.h#L285-L286>`__
+
+            .. code-block:: c
+
+                #define elf_read_implies_exec(ex, executable_stack)	\\
+                    (executable_stack != EXSTACK_DISABLE_X)
+
+        .. [#x86_5.8]
+            `source <https://github.com/torvalds/linux/blob/v5.8/arch/x86/include/asm/elf.h#L305-L306>`__
+
+            .. code-block:: c
+
+                #define elf_read_implies_exec(ex, executable_stack)	\\
+                    (mmap_is_ia32() && executable_stack == EXSTACK_DEFAULT)
+
+            `mmap_is_ia32()`__:
+                .. __: https://github.com/torvalds/linux/blob/v5.8/arch/x86/include/asm/elf.h#L318-L321
+                .. code-block:: c
+
+                    /*
+                     * True on X86_32 or when emulating IA32 on X86_64
+                     */
+                    static inline int mmap_is_ia32(void)
+
+        .. [#arm_5.7]
+            `source <https://github.com/torvalds/linux/blob/v5.7/arch/arm/kernel/elf.c#L85-L92>`__
+
+            .. code-block:: c
+
+                int arm_elf_read_implies_exec(int executable_stack)
+                {
+                    if (executable_stack != EXSTACK_DISABLE_X)
+                        return 1;
+                    if (cpu_architecture() < CPU_ARCH_ARMv6)
+                        return 1;
+                    return 0;
+                }
+
+        .. [#arm_5.8]
+            `source <https://github.com/torvalds/linux/blob/v5.8/arch/arm/kernel/elf.c#L104-L111>`__
+
+            .. code-block:: c
+
+                int arm_elf_read_implies_exec(int executable_stack)
+                {
+                    if (executable_stack == EXSTACK_DEFAULT)
+                        return 1;
+                    if (cpu_architecture() < CPU_ARCH_ARMv6)
+                        return 1;
+                    return 0;
+                }
+
+        .. [#mips_5.17]
+            `source <https://github.com/torvalds/linux/blob/v5.17/arch/mips/kernel/elf.c#L329-L342>`__
+
+            .. code-block:: c
+
+                int mips_elf_read_implies_exec(void *elf_ex, int exstack)
+                {
+                    if (exstack != EXSTACK_DISABLE_X) {
+                        /* The binary doesn't request a non-executable stack */
+                        return 1;
+                    }
+                    if (!cpu_has_rixi) {
+                        /* The CPU doesn't support non-executable memory */
+                        return 1;
+                    }
+                    return 0;
+                }
+
+        .. [#mips_5.18]
+            `source <https://github.com/torvalds/linux/blob/v5.18/arch/mips/kernel/elf.c#L329-L336>`__
+
+            .. code-block:: c
+
+                int mips_elf_read_implies_exec(void *elf_ex, int exstack)
+                {
+                    /*
+                     * Set READ_IMPLIES_EXEC only on non-NX systems that
+                     * do not request a specific state via PT_GNU_STACK.
+                     */
+                    return (!cpu_has_rixi && exstack == EXSTACK_DEFAULT);
+                }
 
-        .. __: https://github.com/torvalds/linux/blob/v4.9/fs/binfmt_elf.c#L784-L789
+        .. [#powerpc]
+            `source <https://github.com/torvalds/linux/blob/v6.3/arch/powerpc/include/asm/elf.h#L82-L108>`__
 
-        .. code-block:: c
+            .. code-block:: c
 
-            case PT_GNU_STACK:
-                if (elf_ppnt->p_flags & PF_X)
-                    executable_stack = EXSTACK_ENABLE_X;
-                else
-                    executable_stack = EXSTACK_DISABLE_X;
-                break;
+                #ifdef __powerpc64__
+                /* stripped */
+                # define elf_read_implies_exec(ex, exec_stk) (is_32bit_task() ? \\
+                        (exec_stk == EXSTACK_DEFAULT) : 0)
+                #else 
+                # define elf_read_implies_exec(ex, exec_stk) (exec_stk == EXSTACK_DEFAULT)
+                #endif /* __powerpc64__ */
 
-        Additionally, it then sets ``read_implies_exec``, so that `all readable pages
-        are executable`__.
+        .. [#ia64]
+            `source <https://github.com/torvalds/linux/blob/v6.3/arch/ia64/include/asm/elf.h#L203-L204>`__
 
-        .. __: https://github.com/torvalds/linux/blob/v4.9/fs/binfmt_elf.c#L849-L850
+            .. code-block:: c
 
-        .. code-block:: c
+                #define elf_read_implies_exec(ex, executable_stack)					\\
+                    ((executable_stack!=EXSTACK_DISABLE_X) && ((ex).e_flags & EF_IA_64_LINUX_EXECUTABLE_STACK) != 0)
+
+            EF_IA_64_LINUX_EXECUTABLE_STACK__:
+                .. __: https://github.com/torvalds/linux/blob/v6.3/arch/ia64/include/asm/elf.h#L33
+
+                .. code-block:: c
+
+                    #define EF_IA_64_LINUX_EXECUTABLE_STACK	0x1	/* is stack (& heap) executable by default? */
+
+        .. [#the_rest]
+            `source <https://github.com/torvalds/linux/blob/v6.3/include/linux/elf.h#L13>`__
 
-            if (elf_read_implies_exec(loc->elf_ex, executable_stack))
-                current->personality |= READ_IMPLIES_EXEC;
+            .. code-block:: c
+
+                # define elf_read_implies_exec(ex, have_pt_gnu_stack)	0
         """
         if not self.executable:
             return True
-
+        
+        exec_bit = None
         for seg in self.iter_segments_by_type('GNU_STACK'):
-            return not bool(seg.header.p_flags & P_FLAGS.PF_X)
+            exec_bit = bool(seg.header.p_flags & P_FLAGS.PF_X)
 
-        # If you NULL out the PT_GNU_STACK section via ELF.disable_nx(),
-        # everything is executable.
-        return False
+        non_exec = exec_bit is False
+        missing = exec_bit is None
+        EF_IA_64_LINUX_EXECUTABLE_STACK = 1
+
+        if self.arch in ['i386', 'arm', 'aarch64', 'mips', 'mips64']:
+            if non_exec:
+                return True
+            elif missing:
+                return False
+            return None
+        elif self.arch == 'amd64':
+            return True if non_exec else None
+        elif self.arch == 'powerpc':
+            return not missing
+        elif self.arch == 'powerpc64':
+            return True
+        elif self.arch == 'ia64':
+            if non_exec:
+                return True
+            return not bool(self['e_flags'] & EF_IA_64_LINUX_EXECUTABLE_STACK)
+
+        return True
 
     @property
     def execstack(self):
-        """:class:`bool`: Whether the current binary uses an executable stack.
+        """:class:`bool`: Whether dynamically loading the current binary will make the stack executable.
+
+        This is based on the presence of a program header ``PT_GNU_STACK``,
+        its setting, and the default stack permissions for the architecture.
 
-        This is based on the presence of a program header PT_GNU_STACK_
-        being present, and its setting.
+        If ``PT_GNU_STACK`` is persent, the stack permissions are `set according to it`__:
 
-            ``PT_GNU_STACK``
+        .. __: https://github.com/bminor/glibc/blob/glibc-2.37/elf/dl-load.c#L1218-L1220
 
-            The p_flags member specifies the permissions on the segment
-            containing the stack and is used to indicate whether the stack
-            should be executable. The absense of this header indicates
-            that the stack will be executable.
+        .. code-block:: c
 
-        In particular, if the header is missing the stack is executable.
-        If the header is present, it may **explicitly** mark that the stack is
-        executable.
+            case PT_GNU_STACK:
+              stack_flags = ph->p_flags;
+              break;
 
-        This is only somewhat accurate.  When using the GNU Linker, it usees
-        DEFAULT_STACK_PERMS_ to decide whether a lack of ``PT_GNU_STACK``
-        should mark the stack as executable:
+        Else, the stack permissions are set according to the architecture defaults
+        as `defined by`__ ``DEFAULT_STACK_PERMS``:
+
+        .. __: https://github.com/bminor/glibc/blob/glibc-2.37/elf/dl-load.c#L1093-L1096
 
         .. code-block:: c
 
@@ -1745,29 +1927,25 @@ def execstack(self):
         ::
 
             $ git grep '#define DEFAULT_STACK_PERMS' | grep -v PF_X
-            sysdeps/aarch64/stackinfo.h:31:#define DEFAULT_STACK_PERMS (PF_R|PF_W)
-            sysdeps/nios2/stackinfo.h:31:#define DEFAULT_STACK_PERMS (PF_R|PF_W)
-            sysdeps/tile/stackinfo.h:31:#define DEFAULT_STACK_PERMS (PF_R|PF_W)
-
-        .. _PT_GNU_STACK: https://refspecs.linuxbase.org/LSB_3.0.0/LSB-PDA/LSB-PDA/progheader.html
-        .. _DEFAULT_STACK_PERMS: https://github.com/bminor/glibc/blob/glibc-2.25/elf/dl-load.c#L1036-L1038
+            sysdeps/aarch64/stackinfo.h:    #define DEFAULT_STACK_PERMS (PF_R|PF_W)
+            sysdeps/arc/stackinfo.h:        #define DEFAULT_STACK_PERMS (PF_R|PF_W)
+            sysdeps/csky/stackinfo.h:       #define DEFAULT_STACK_PERMS (PF_R|PF_W)
+            sysdeps/ia64/stackinfo.h:       #define DEFAULT_STACK_PERMS (PF_R|PF_W)
+            sysdeps/loongarch/stackinfo.h:  #define DEFAULT_STACK_PERMS (PF_R | PF_W)
+            sysdeps/nios2/stackinfo.h:      #define DEFAULT_STACK_PERMS (PF_R|PF_W)
+            sysdeps/riscv/stackinfo.h:      #define DEFAULT_STACK_PERMS (PF_R | PF_W)
         """
-        # Dynamic objects do not have the ability to change the executable state of the stack.
         if not self.executable:
             return False
 
-        # If NX is completely off for the process, the stack is executable.
-        if not self.nx:
-            return True
-
+        # If the ``PT_GNU_STACK`` program header is preset, use it's premissions.
+        for seg in self.iter_segments_by_type('GNU_STACK'):
+            return bool(seg.header.p_flags & P_FLAGS.PF_X)
+        
         # If the ``PT_GNU_STACK`` program header is missing, then use the
-        # default rules.  Only AArch64 gets a non-executable stack by default.
-        for _ in self.iter_segments_by_type('GNU_STACK'):
-            break
-        else:
-            return self.arch != 'aarch64'
-
-        return False
+        # default rules. Out of the supported architectures, only AArch64,
+        # IA-64, and RISC-V get a non-executable stack by default.
+        return self.arch not in ['aarch64', 'ia64', 'riscv32', 'riscv64']
 
     @property
     def canary(self):
@@ -1842,6 +2020,7 @@ def checksec(self, banner=True, color=True):
             "NX:".ljust(10) + {
                 True:  green("NX enabled"),
                 False: red("NX disabled"),
+                None: yellow("NX unknown - GNU_STACK missing"),
             }[self.nx],
             "PIE:".ljust(10) + {
                 True: green("PIE enabled"),
@@ -1850,7 +2029,7 @@ def checksec(self, banner=True, color=True):
         ])
 
         # Execstack may be a thing, even with NX enabled, because of glibc
-        if self.execstack and self.nx:
+        if self.execstack and self.nx is not False:
             res.append("Stack:".ljust(10) + red("Executable"))
 
         # Are there any RWX areas in the binary?
@@ -2062,6 +2241,8 @@ def disable_nx(self):
             if self.mmap[offset:offset+4] == PT_GNU_STACK:
                 self.mmap[offset:offset+4] = b'\x00' * 4
                 self.save()
+                # Invalidate the cached segments, ``PT_GNU_STACK`` was removed.
+                self._segments = None
                 return
 
         log.error("Could not find PT_GNU_STACK, stack should already be executable")


Test patch: