Skip to content

Commit

Permalink
Add x86 CET status to checksec output (#2293)
Browse files Browse the repository at this point in the history
* Add x86 CET status to checksec output

The ELF class now features two properties:
- ibt for indirect branch tracking `-fcf-protection=branch`
- shstk for shadow stack `-fcf-protection=return`

Both checking the presence of the respective feature bit in the
note property x86 feature segment.

* Check if user shadowstack is enabled on SSH target

Look for "user_shstk" in /proc/cpuinfo for ssh.checksec()

* Add ELF.iter_notes and ELF.iter_properties helpers

Allows to easily iterate the PT_NOTE segment and the properties in "GNU"
notes.

* Check for ibt on remote ssh machine

* Update CHANGELOG

* Suppress error when /proc/cpuinfo cannot be read
  • Loading branch information
peace-maker authored Nov 14, 2023
1 parent 0bf914e commit b53d476
Show file tree
Hide file tree
Showing 4 changed files with 123 additions and 3 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,10 @@ The table below shows which release corresponds to each branch, and what date th
## 4.13.0 (`dev`)

- [#2281][2281] FIX: Getting right amount of data for search fix
- [#2293][2293] Add x86 CET status to checksec output

[2281]: https://github.com/Gallopsled/pwntools/pull/2281
[2293]: https://github.com/Gallopsled/pwntools/pull/2293

## 4.12.0 (`beta`)

Expand Down
55 changes: 55 additions & 0 deletions pwnlib/elf/elf.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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)
Expand Down
65 changes: 64 additions & 1 deletion pwnlib/tubes/ssh.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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))

Expand Down Expand Up @@ -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:
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ requires-python = ">=2.7"
dependencies = [
"paramiko>=1.15.2",
"mako>=1.0.0",
"pyelftools>=0.24, <0.30; python_version < '3'",
"pyelftools>=0.24; python_version >= '3'",
"pyelftools>=0.28, <0.30; python_version < '3'",
"pyelftools>=0.28; python_version >= '3'",
"capstone>=3.0.5rc2", # see Gallopsled/pwntools#971, Gallopsled/pwntools#1160
"ropgadget>=5.3",
"pyserial>=2.7",
Expand Down

0 comments on commit b53d476

Please sign in to comment.