diff --git a/docs/source/encoders.rst b/docs/source/encoders.rst index e36ed86d4..94b865c88 100644 --- a/docs/source/encoders.rst +++ b/docs/source/encoders.rst @@ -5,25 +5,8 @@ :mod:`pwnlib.encoders` --- Encoding Shellcode =============================================== -.. automodule:: pwnlib.encoders.encoder - :members: - -.. automodule:: pwnlib.encoders.i386.ascii_shellcode - :members: - :special-members: - :exclude-members: __init__ - -.. automodule:: pwnlib.encoders.i386.xor - :members: - -.. automodule:: pwnlib.encoders.i386.delta - :members: - -.. automodule:: pwnlib.encoders.amd64.delta +.. automodule:: pwnlib.encoders :members: -.. automodule:: pwnlib.encoders.arm.xor - :members: - -.. automodule:: pwnlib.encoders.mips.xor +.. automodule:: pwnlib.encoders.encoder :members: diff --git a/examples/encoder.py b/examples/encoder.py new file mode 100644 index 000000000..039e2b0ad --- /dev/null +++ b/examples/encoder.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python3 +from pwn import * + +context.randomize = False +context.log_level = 'error' + +def go(): + info('========== %s ==========', context.arch) + with context.silent: + sc = asm(shellcraft.sh()) + avoid=b'\x00\n\t ' + enc = encode(sc, avoid=avoid, force=1) + + assert not (byteset(avoid) & byteset(enc)) + assert enc != sc + + with context.silent: + io = ELF.from_bytes(enc).process() + io.sendline(b'whoami') + + try: + info('%r', io.recvline() == b'pwntools\n') + except EOFError: + info('EOFError') + + with context.silent: + io.close() + + +context.clear(arch='i386') +go() + +context.clear(arch='amd64') +go() + +context.clear(arch='arm') +go() + +context.clear(arch='mips') +go() \ No newline at end of file diff --git a/pwnlib/encoders/__init__.py b/pwnlib/encoders/__init__.py index 7276f3d57..e41d86861 100644 --- a/pwnlib/encoders/__init__.py +++ b/pwnlib/encoders/__init__.py @@ -1,6 +1,12 @@ # -*- coding:utf-8 -*- """ Encode shellcode to avoid input filtering and impress your friends! + +Note +---- + +Some of these methods will fail on various architectures. +Your best bet is to use :func:`.encoder.encode` with an ``avoid=...``. """ from __future__ import absolute_import @@ -8,7 +14,6 @@ from pwnlib.encoders import arm from pwnlib.encoders import i386 from pwnlib.encoders import mips -from pwnlib.encoders.encoder import Encoder from pwnlib.encoders.encoder import alphanumeric from pwnlib.encoders.encoder import encode from pwnlib.encoders.encoder import line diff --git a/pwnlib/encoders/amd64/delta.py b/pwnlib/encoders/amd64/delta.py index 51482ff3e..2e467c9fe 100644 --- a/pwnlib/encoders/amd64/delta.py +++ b/pwnlib/encoders/amd64/delta.py @@ -2,7 +2,7 @@ from __future__ import division from pwnlib.encoders.i386.delta import i386DeltaEncoder - +from pwnlib.util.misc import byteset class amd64DeltaEncoder(i386DeltaEncoder): r""" @@ -41,7 +41,7 @@ class amd64DeltaEncoder(i386DeltaEncoder): ''' arch = 'amd64' raw = b'H\x8d5\xf9\xff\xff\xffH\x83\xc6\x1a\xfcH\x89\xf7\xac\x93\xac(\xd8\xaa\x80\xeb\xacu\xf5' - blacklist = set(raw) + blacklist = byteset(raw) encode = amd64DeltaEncoder() __all__ = ['encode'] diff --git a/pwnlib/encoders/arm/__init__.py b/pwnlib/encoders/arm/__init__.py index b52d6a041..6964e4003 100644 --- a/pwnlib/encoders/arm/__init__.py +++ b/pwnlib/encoders/arm/__init__.py @@ -1,4 +1,4 @@ from __future__ import absolute_import -from pwnlib.encoders.arm import alphanumeric +# from pwnlib.encoders.arm import alphanumeric from pwnlib.encoders.arm import xor diff --git a/pwnlib/encoders/arm/alphanumeric/__init__.py b/pwnlib/encoders/arm/alphanumeric/__init__.py index ffe1bb489..f59acdf76 100644 --- a/pwnlib/encoders/arm/alphanumeric/__init__.py +++ b/pwnlib/encoders/arm/alphanumeric/__init__.py @@ -10,13 +10,13 @@ from pwnlib.context import context from . import builder -from pwnlib.encoders.encoder import Encoder +from pwnlib.encoders.encoder_class import Encoder class ArmEncoder(Encoder): arch = 'arm' - blacklist = {chr(c) for c in range(256) if chr(c) in (string.ascii_letters + string.digits)} + blacklist = {bytes([c]) for c in range(256) if chr(c) in (string.ascii_letters + string.digits)} icache_flush = 1 def __call__(self, input, avoid, pcreg=None): diff --git a/pwnlib/encoders/arm/xor.py b/pwnlib/encoders/arm/xor.py index 331d06133..aef0ec350 100644 --- a/pwnlib/encoders/arm/xor.py +++ b/pwnlib/encoders/arm/xor.py @@ -4,9 +4,10 @@ from pwnlib import shellcraft from pwnlib.asm import asm from pwnlib.context import context -from pwnlib.encoders.encoder import Encoder +from pwnlib.encoders.encoder_class import Encoder from pwnlib.util.fiddling import xor_key from pwnlib.util.lists import group +from pwnlib.util.misc import byteset from pwnlib.util.packing import u8 @@ -45,7 +46,7 @@ class ArmXorEncoder(Encoder): payload: """ - blacklist = set("\x01\x80\x03\x85\x04\x07\x87\x0c\x8f\x0f\x16\x1c\x9f\x84\xa0%$'-/\xb0\xbd\x81A@\xc2DG\xc6\xc8OPT\xd8_\xe1`\xe3\xe2\xe5\xe7\xe9\xe8\xea\xe0p\xf7") + blacklist = byteset(b"\x01\x80\x03\x85\x04\x07\x87\x0c\x8f\x0f\x16\x1c\x9f\x84\xa0%$'-/\xb0\xbd\x81A@\xc2DG\xc6\xc8OPT\xd8_\xe1`\xe3\xe2\xe5\xe7\xe9\xe8\xea\xe0p\xf7") def __call__(self, raw_bytes, avoid, pcreg=''): key, xordata = xor_key(raw_bytes, avoid, size=1) diff --git a/pwnlib/encoders/encoder.py b/pwnlib/encoders/encoder.py index 8dc5af130..047c20693 100644 --- a/pwnlib/encoders/encoder.py +++ b/pwnlib/encoders/encoder.py @@ -2,49 +2,18 @@ from __future__ import absolute_import from __future__ import division -import collections import random import re -import string from pwnlib.context import LocalContext from pwnlib.context import context +from pwnlib.encoders.encoder_class import Encoder from pwnlib.log import getLogger from pwnlib.util.fiddling import hexdump +from pwnlib.util.misc import byteset log = getLogger(__name__) -class Encoder(object): - _encoders = collections.defaultdict(lambda: []) - - #: Architecture which this encoder works on - arch = None - - #: Blacklist of bytes which are known not to be supported - blacklist = set() - - def __init__(self): - """Shellcode encoder class - - Implements an architecture-specific shellcode encoder - """ - Encoder._encoders[self.arch].append(self) - - def __call__(self, raw_bytes, avoid, pcreg): - """avoid(raw_bytes, avoid) - - Arguments: - raw_bytes(str): - String of bytes to encode - avoid(set): - Set of bytes to avoid - pcreg(str): - Register which contains the address of the shellcode. - May be necessary for some shellcode. - """ - raise NotImplementedError() - - @LocalContext def encode(raw_bytes, avoid=None, expr=None, force=0, pcreg=''): """encode(raw_bytes, avoid, expr, force) -> str @@ -54,37 +23,43 @@ def encode(raw_bytes, avoid=None, expr=None, force=0, pcreg=''): Arguments: - raw_bytes(str): Sequence of shellcode bytes to encode. - avoid(str): Bytes to avoid - expr(str): Regular expression which matches bad characters. - force(bool): Force re-encoding of the shellcode, even if it - doesn't contain any bytes in ``avoid``. + raw_bytes(bytes): Sequence of shellcode bytes to encode. + avoid(bytes): Bytes to avoid + expr(bytes): Regular expression which matches bad characters. + force(bool): Force re-encoding of the shellcode, even if it + doesn't contain any bytes in ``avoid``. """ orig_avoid = avoid - avoid = set(avoid or '') + avoid = byteset(avoid or b'') if expr: for char in all_chars: if re.search(expr, char): avoid.add(char) - if not (force or avoid & set(raw_bytes)): + if not (force or avoid & byteset(raw_bytes)): return raw_bytes encoders = Encoder._encoders[context.arch] - random.shuffle(encoders) + + if context.randomize: + random.shuffle(encoders) for encoder in encoders: if encoder.blacklist & avoid: continue + log.debug('Selected encoder %r', encoder) + + bytes_avoid = b''.join(avoid) + try: - v = encoder(raw_bytes, bytes(avoid), pcreg) + v = encoder(raw_bytes, bytes_avoid, pcreg) except NotImplementedError: continue - if avoid & set(v): + if avoid & byteset(v): log.warning_once("Encoder %s did not succeed" % encoder) continue @@ -97,23 +72,23 @@ def encode(raw_bytes, avoid=None, expr=None, force=0, pcreg=''): elif expr: avoid_errmsg = repr(expr) else: - avoid_errmsg = repr(bytes(avoid)) + avoid_errmsg = repr(b''.join(avoid)) args = (context.arch, avoid_errmsg, hexdump(raw_bytes)) msg = "No encoders for %s which can avoid %s for\n%s" % args msg = msg.replace('%', '%%') log.error(msg) -all_chars = list(chr(i) for i in range(256)) -re_alphanumeric = r'[^A-Za-z0-9]' -re_printable = r'[^\x21-\x7e]' -re_whitespace = r'\s' -re_null = r'\x00' -re_line = r'[\s\x00]' +all_chars = list(bytes([i]) for i in range(256)) +re_alphanumeric = br'[^A-Za-z0-9]' +re_printable = br'[^\x21-\x7e]' +re_whitespace = br'\s' +re_null = br'\x00' +re_line = br'[\s\x00]' @LocalContext def null(raw_bytes, *a, **kw): - """null(raw_bytes) -> str + """null(raw_bytes) -> bytes Encode the shellcode ``raw_bytes`` such that it does not contain any NULL bytes. @@ -124,7 +99,7 @@ def null(raw_bytes, *a, **kw): @LocalContext def line(raw_bytes, *a, **kw): - """line(raw_bytes) -> str + """line(raw_bytes) -> bytes Encode the shellcode ``raw_bytes`` such that it does not contain any NULL bytes or whitespace. @@ -135,7 +110,7 @@ def line(raw_bytes, *a, **kw): @LocalContext def alphanumeric(raw_bytes, *a, **kw): - """alphanumeric(raw_bytes) -> str + """alphanumeric(raw_bytes) -> bytes Encode the shellcode ``raw_bytes`` such that it does not contain any bytes except for [A-Za-z0-9]. @@ -146,7 +121,7 @@ def alphanumeric(raw_bytes, *a, **kw): @LocalContext def printable(raw_bytes, *a, **kw): - """printable(raw_bytes) -> str + """printable(raw_bytes) -> bytes Encode the shellcode ``raw_bytes`` such that it only contains non-space printable bytes. @@ -157,7 +132,7 @@ def printable(raw_bytes, *a, **kw): @LocalContext def scramble(raw_bytes, *a, **kw): - """scramble(raw_bytes) -> str + """scramble(raw_bytes) -> bytes Encodes the input data with a random encoder. diff --git a/pwnlib/encoders/encoder_class.py b/pwnlib/encoders/encoder_class.py new file mode 100644 index 000000000..cd106cdc3 --- /dev/null +++ b/pwnlib/encoders/encoder_class.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import +from __future__ import division + +import collections + +from pwnlib.util.misc import byteset + +class Encoder(object): + _encoders = collections.defaultdict(lambda: []) + + #: Architecture which this encoder works on + arch = None + + #: Blacklist of bytes which are known not to be supported + blacklist = byteset() + + def __init__(self): + """Shellcode encoder class + + Implements an architecture-specific shellcode encoder + """ + Encoder._encoders[self.arch].append(self) + + def __call__(self, raw_bytes, avoid, pcreg): + """avoid(raw_bytes, avoid) + + Arguments: + raw_bytes(bytes): + Bytes to encode + avoid(bytes): + Bytes to avoid + pcreg(str): + Register which contains the address of the shellcode. + May be necessary for some shellcode. + """ + raise NotImplementedError() diff --git a/pwnlib/encoders/i386/ascii_shellcode.py b/pwnlib/encoders/i386/ascii_shellcode.py index 0b61ca730..a9f372444 100644 --- a/pwnlib/encoders/i386/ascii_shellcode.py +++ b/pwnlib/encoders/i386/ascii_shellcode.py @@ -10,9 +10,10 @@ from pwnlib.context import LocalContext from pwnlib.context import context -from pwnlib.encoders.encoder import Encoder +from pwnlib.encoders.encoder_class import Encoder from pwnlib.encoders.encoder import all_chars from pwnlib.util.iters import group +from pwnlib.util.misc import byteset from pwnlib.util.packing import * @@ -94,13 +95,14 @@ def __call__(self, raw_bytes, avoid=None, pcreg=None): vocab = bytearray( b"!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~") else: - required_chars = set('\\-%TXP') + required_chars = byteset(b'\\-%TXP') allowed = set(all_chars) + avoid = byteset(avoid) if avoid.intersection(required_chars): raise RuntimeError( '''These characters ({}) are required because they assemble into instructions used to unpack the shellcode'''.format( - str(required_chars, 'ascii'))) + str(b''.join(required_chars), 'ascii'))) allowed.difference_update(avoid) vocab = bytearray(map(ord, allowed)) diff --git a/pwnlib/encoders/i386/delta.py b/pwnlib/encoders/i386/delta.py index e3d272917..6c243d964 100644 --- a/pwnlib/encoders/i386/delta.py +++ b/pwnlib/encoders/i386/delta.py @@ -9,8 +9,9 @@ from pwnlib.asm import asm from pwnlib.asm import disasm from pwnlib.context import context -from pwnlib.encoders.encoder import Encoder +from pwnlib.encoders.encoder_class import Encoder from pwnlib.util.fiddling import hexdump +from pwnlib.util.misc import byteset ''' @@ -53,7 +54,7 @@ class i386DeltaEncoder(Encoder): terminator = 0xac raw = b'\xd9\xd0\xfc\xd9t$\xf4^\x83\xc6\x18\x89\xf7\xac\x93\xac(\xd8\xaa\x80\xeb\xacu\xf5' - blacklist = set(raw) + blacklist = byteset(raw) def __call__(self, raw_bytes, avoid, pcreg=''): table = collections.defaultdict(lambda: []) diff --git a/pwnlib/encoders/i386/xor.py b/pwnlib/encoders/i386/xor.py index e28b00db7..32f98eaa7 100644 --- a/pwnlib/encoders/i386/xor.py +++ b/pwnlib/encoders/i386/xor.py @@ -15,10 +15,10 @@ from pwnlib import shellcraft from pwnlib.asm import asm from pwnlib.context import context -from pwnlib.encoders.encoder import Encoder +from pwnlib.encoders.encoder_class import Encoder from pwnlib.util.fiddling import xor_pair from pwnlib.util.lists import group - +from pwnlib.util.misc import byteset # Note shellcode assumes it's based at ecx @@ -63,7 +63,7 @@ class i386XorEncoder(Encoder): end: ''' - blacklist = set('\x14$1I^tu\x83\x89\x93\xab\xad\xc6\xd8\xd9\xf4\xf7\xfc') + blacklist = byteset(b'\x14$1I^tu\x83\x89\x93\xab\xad\xc6\xd8\xd9\xf4\xf7\xfc') def __call__(self, raw_bytes, avoid, pcreg=''): while len(raw_bytes) % context.bytes: diff --git a/pwnlib/encoders/mips/xor.py b/pwnlib/encoders/mips/xor.py index 6d14f9c33..a65adf64c 100644 --- a/pwnlib/encoders/mips/xor.py +++ b/pwnlib/encoders/mips/xor.py @@ -29,8 +29,9 @@ from pwnlib import asm from pwnlib import shellcraft from pwnlib.context import context -from pwnlib.encoders.encoder import Encoder +from pwnlib.encoders.encoder_class import Encoder from pwnlib.util.fiddling import xor_key +from pwnlib.util.misc import byteset decoders = { 'little': b''.join([ @@ -114,7 +115,7 @@ class MipsXorEncoder(Encoder): """ arch = 'mips' - blacklist = cannot_avoid = set(b''.join(v for v in decoders.values())) + blacklist = cannot_avoid = byteset(b''.join(v for v in decoders.values())) def __call__(self, raw_bytes, avoid, pcreg=''): diff --git a/pwnlib/util/misc.py b/pwnlib/util/misc.py index 7fbf479ca..c2f4fc4cd 100644 --- a/pwnlib/util/misc.py +++ b/pwnlib/util/misc.py @@ -612,3 +612,28 @@ def python_2_bytes_compatible(klass): if '__str__' not in klass.__dict__: klass.__str__ = klass.__bytes__ return klass + +def byteset(b=b''): + # type: (bytes|bytearray) -> set[bytes] + """ + Converts a ``bytes`` object into a ``set`` of ``bytes``. + + This is necessary because of differences between Python2 and Python3. + + In Python2, a ``set`` of ``bytes`` contains individual objects of ``bytes`` class. + + In Python3, a ``set`` of ``bytes`` contains individual objects of ``int`` class. + + This class normalizes the behavior, so that the ``set`` return always contains + objects of the ``bytes`` class. + + >>> sorted(byteset(b'asdfasdf')) + [b'a', b'd', b'f', b's'] + """ + if not isinstance(b, (bytes, bytearray)): + log.error("byteset only accepts type bytes") + + if six.PY2: + return set(b) + + return set([b[i:i+1] for i in range(len(b))]) \ No newline at end of file