Repository: Gallopsled/pwntools

Issue:
CET Shadow stack reporting in checksec.
It would be nice if `checksec` could display if the binary use shadow stack & IBT.

according to this kernel doc:  [Control-flow Enforcement Technology (CET) Shadow Stack](https://kernel.org/doc/html/next/arch/x86/shstk.html)

![](https://imgur.com/PBz7SAj.png)

that can be verified on a binary compiled with `gcc  -mshstk -fcf-protection=full` 
with readelf -n prog:
```sh
root@locahost:/host# readelf -n libc.so.6 

Displaying notes found in: .note.gnu.property
  Owner                Data size 	Description
  GNU                  0x00000040	NT_GNU_PROPERTY_TYPE_0
      Properties: x86 feature: IBT, SHSTK
	x86 ISA needed: x86-64-baseline
```

so checksec could report IBT & SHSTK flags presence in elf file.






Discussion:
I would love to see this too. Would you mind contributing a PR for that?

Resolving patch:
diff --git a/pwnlib/elf/elf.py b/pwnlib/elf/elf.py
--- a/pwnlib/elf/elf.py
+++ b/pwnlib/elf/elf.py
@@ -53,6 +53,7 @@
 from elftools.elf.constants import SHN_INDICES
 from elftools.elf.descriptions import describe_e_type
 from elftools.elf.elffile import ELFFile
+from elftools.elf.enums import ENUM_GNU_PROPERTY_X86_FEATURE_1_FLAGS
 from elftools.elf.gnuversions import GNUVerDefSection
 from elftools.elf.relocation import RelocationSection
 from elftools.elf.sections import SymbolTableSection
@@ -510,6 +511,29 @@ def iter_segments_by_type(self, t):
             if t == seg.header.p_type or t in str(seg.header.p_type):
                 yield seg
 
+    def iter_notes(self):
+        """ 
+        Yields:
+            All the notes in the PT_NOTE segments.  Each result is a dictionary-
+            like object with ``n_name``, ``n_type``, and ``n_desc`` fields, amongst
+            others.
+        """
+        for seg in self.iter_segments_by_type('PT_NOTE'):
+            for note in seg.iter_notes():
+                yield note
+
+    def iter_properties(self):
+        """
+        Yields:
+            All the GNU properties in the PT_NOTE segments.  Each result is a dictionary-
+            like object with ``pr_type``, ``pr_datasz``, and ``pr_data`` fields.
+        """
+        for note in self.iter_notes():
+            if note.n_type != 'NT_GNU_PROPERTY_TYPE_0':
+                continue
+            for prop in note.n_desc:
+                yield prop
+                
     def get_segment_for_address(self, address, size=1):
         """get_segment_for_address(address, size=1) -> Segment
 
@@ -2060,6 +2084,12 @@ def checksec(self, banner=True, color=True):
 
         if self.ubsan:
             res.append("UBSAN:".ljust(10) + green("Enabled"))
+        
+        if self.shadowstack:
+            res.append("SHSTK:".ljust(10) + green("Enabled"))
+        
+        if self.ibt:
+            res.append("IBT:".ljust(10) + green("Enabled"))
 
         # Check for Linux configuration, it must contain more than
         # just the version.
@@ -2117,6 +2147,31 @@ def ubsan(self):
         """:class:`bool`: Whether the current binary was built with
         Undefined Behavior Sanitizer (``UBSAN``)."""
         return any(s.startswith('__ubsan_') for s in self.symbols)
+    
+    @property
+    def shadowstack(self):
+        """:class:`bool`: Whether the current binary was built with	
+        Shadow Stack (``SHSTK``)"""
+        if self.arch not in ['i386', 'amd64']:
+            return False
+        for prop in self.iter_properties():
+            if prop.pr_type != 'GNU_PROPERTY_X86_FEATURE_1_AND':
+                continue
+            return prop.pr_data & ENUM_GNU_PROPERTY_X86_FEATURE_1_FLAGS['GNU_PROPERTY_X86_FEATURE_1_SHSTK'] > 0
+        return False
+
+    @property
+    def ibt(self):
+        """:class:`bool`: Whether the current binary was built with
+        Indirect Branch Tracking (``IBT``)"""
+        if self.arch not in ['i386', 'amd64']:
+            return False
+        for prop in self.iter_properties():
+            if prop.pr_type != 'GNU_PROPERTY_X86_FEATURE_1_AND':
+                continue
+            return prop.pr_data & ENUM_GNU_PROPERTY_X86_FEATURE_1_FLAGS['GNU_PROPERTY_X86_FEATURE_1_IBT'] > 0
+        return False
+
 
     def _update_args(self, kw):
         kw.setdefault('arch', self.arch)
diff --git a/pwnlib/tubes/ssh.py b/pwnlib/tubes/ssh.py
--- a/pwnlib/tubes/ssh.py
+++ b/pwnlib/tubes/ssh.py
@@ -17,6 +17,7 @@
 
 from pwnlib import term
 from pwnlib.context import context, LocalContext
+from pwnlib.exception import PwnlibException
 from pwnlib.log import Logger
 from pwnlib.log import getLogger
 from pwnlib.term import text
@@ -613,6 +614,9 @@ def __init__(self, user=None, host=None, port=22, password=None, key=None,
         self._platform_info = {}
         self._aslr = None
         self._aslr_ulimit = None
+        self._cpuinfo_cache = None
+        self._user_shstk = None
+        self._ibt = None
 
         misc.mkdir_p(self._cachedir)
 
@@ -2144,6 +2148,57 @@ def preexec():
 
         return self._aslr_ulimit
 
+    def _cpuinfo(self):
+        if self._cpuinfo_cache is None:
+            with context.quiet:
+                try:
+                    self._cpuinfo_cache = self.read('/proc/cpuinfo')
+                except PwnlibException:
+                    self._cpuinfo_cache = b''
+        return self._cpuinfo_cache
+
+    @property
+    def user_shstk(self):
+        """:class:`bool`: Whether userspace shadow stack is supported on the system.
+
+        Example:
+
+            >>> s = ssh("travis", "example.pwnme")
+            >>> s.user_shstk
+            False
+        """
+        if self._user_shstk is None:
+            if self.os != 'linux':
+                self.warn_once("Only Linux is supported for userspace shadow stack checks.")
+                self._user_shstk = False
+
+            else:
+                cpuinfo = self._cpuinfo()
+
+                self._user_shstk = b' user_shstk' in cpuinfo
+        return self._user_shstk
+
+    @property
+    def ibt(self):
+        """:class:`bool`: Whether kernel indirect branch tracking is supported on the system.
+
+        Example:
+
+            >>> s = ssh("travis", "example.pwnme")
+            >>> s.ibt
+            False
+        """
+        if self._ibt is None:
+            if self.os != 'linux':
+                self.warn_once("Only Linux is supported for kernel indirect branch tracking checks.")
+                self._ibt = False
+
+            else:
+                cpuinfo = self._cpuinfo()
+
+                self._ibt = b' ibt ' in cpuinfo or b' ibt\n' in cpuinfo
+        return self._ibt
+
     def _checksec_cache(self, value=None):
         path = self._get_cachefile('%s-%s' % (self.host, self.port))
 
@@ -2180,7 +2235,15 @@ def checksec(self, banner=True):
             "ASLR:".ljust(10) + {
                 True: green("Enabled"),
                 False: red("Disabled")
-            }[self.aslr]
+            }[self.aslr],
+            "SHSTK:".ljust(10) + {
+                True: green("Enabled"),
+                False: red("Disabled")
+            }[self.user_shstk],
+            "IBT:".ljust(10) + {
+                True: green("Enabled"),
+                False: red("Disabled")
+            }[self.ibt],
         ]
 
         if self.aslr_ulimit:


Test patch: