diff options
| -rw-r--r-- | sploit/payload/__init__.py | 1 | ||||
| -rw-r--r-- | sploit/payload/gadhint.py | 43 | ||||
| -rw-r--r-- | sploit/payload/payload.py | 281 | ||||
| -rw-r--r-- | sploit/payload/payload_entry.py | 99 | ||||
| -rw-r--r-- | sploit/payload/rop.py | 245 | ||||
| -rw-r--r-- | sploit/rev/gadget.py | 23 | ||||
| -rw-r--r-- | sploit/symtbl.py | 112 | ||||
| -rw-r--r-- | sploit/types/__init__.py | 3 | ||||
| -rw-r--r-- | sploit/types/index_entry.py | 44 | ||||
| -rw-r--r-- | sploit/types/indextbl.py | 103 | ||||
| -rw-r--r-- | sploit/types/lict.py | 202 | 
11 files changed, 826 insertions, 330 deletions
| diff --git a/sploit/payload/__init__.py b/sploit/payload/__init__.py index 78769b4..d65b0f0 100644 --- a/sploit/payload/__init__.py +++ b/sploit/payload/__init__.py @@ -1,3 +1,4 @@  from .gadhint import *  from .payload import * +from .payload_entry import *  from .rop import * diff --git a/sploit/payload/gadhint.py b/sploit/payload/gadhint.py index 9b077fe..1bef9f0 100644 --- a/sploit/payload/gadhint.py +++ b/sploit/payload/gadhint.py @@ -1,12 +1,15 @@ +import copy  from dataclasses import dataclass, field +  from sploit.rev.gadget import Gadget +from sploit.types.index_entry import IndexEntry  @dataclass -class GadHint: +class GadHint(IndexEntry):      """      User-annotated gadget description object -    gadget (Gadget|int): The gadget being annotated.  May be a Gadget object or +    base (Gadget|int): The gadget being annotated.  May be a Gadget object or      an offset as an int.      pops (list[str]): The registers popped by this gadget, in order of @@ -20,10 +23,10 @@ class GadHint:      Keys are destination register names, values are immediate values.  The order      given is insignificant. -    writes (dict{str:str}): The register-to-memory moves (stores) made by this -    gadget.  Keys are destination register names (expected to hold memory -    locations), values are source register names (expected to hold direct -    values).  The order given is insignificant. +    writes (dict{str:str}): The register-to-memory stores made by this gadget. +    Keys are the destination register names (which hold memory addresses), +    values are source register names (which hold values to-be-stored).  The +    order given is insignificant.      requirements (dict{str:int}): The register state that is required before      this gadget should be executed.  Keys are register names, values are the @@ -43,7 +46,7 @@ class GadHint:      should not be accounted for.  A value of zero is taken as "unspecified".      """ -    gadget: int = 0 +    base: int = 0      pops: list = field(default_factory=list)      movs: dict = field(default_factory=dict)      imms: dict = field(default_factory=dict) @@ -57,21 +60,7 @@ class GadHint:      @property      def offset(self):          """Return gadget offset as an integer.""" -        return int(self.gadget) - -    def __index__(self): -        """Convert object to integer using offset value.""" -        return self.offset - -    def __add__(self, x): -        """Return new object with adjusted offset.""" -        return GadHint(self.gadget + x, self.pops, self.movs, self.imms, -                       self.writes, self.requirements, self.stack, self.align, -                       self.syscall, self.spm) - -    def __sub__(self, x): -        """Return new object with adjusted offset.""" -        return self + (-x) +        return int(self.base)      def with_requirements(self, reqs):          """Return new object with additional requirements.""" @@ -81,9 +70,9 @@ class GadHint:                      f"GadHint: Conflicting gadget requirements: "                      f"{self.requirements}, {reqs}") -        return GadHint(self.gadget, self.pops, self.movs, self.imms, -                       self.writes, self.requirements | reqs, self.stack, -                       self.align, self.syscall, self.spm) +        new = copy.deepcopy(self) +        new.requirements |= reqs +        return new      def __repr__(self):          """Return human-readable GadHint.""" @@ -92,8 +81,8 @@ class GadHint:                  return f", {name}={prop}"              return "" -        s = hex(self.gadget) -        s = f"Gadget({s})" if type(self.gadget) is Gadget else s +        s = hex(self.base) +        s = f"Gadget({s})" if isinstance(self.base, Gadget) else s          s += fmt("pops", self.pops)          s += fmt("movs", self.movs)          s += fmt("imms", self.imms) diff --git a/sploit/payload/payload.py b/sploit/payload/payload.py index cf105c6..a59eed2 100644 --- a/sploit/payload/payload.py +++ b/sploit/payload/payload.py @@ -1,94 +1,203 @@ -from sploit.arch import arch, itob -from sploit.symtbl import Symtbl +from sploit.arch import itob +from sploit.payload.payload_entry import PayloadEntry +from sploit.types.indextbl import IndexTbl +from sploit.types.index_entry import IndexEntry +from sploit.types.lict import Lict + +_REPR_DATA_LEN = 64 + +class Payload(IndexTbl): +    """ +    Binary payload builder + +    This class provides an API for fluently specifying structured payloads from +    an assortment of input data.  Payload "indices" are any bytes-like data, +    which includes some supported IndexEntry types as well as nested Payloads. + +    Payload is an IndexTbl based on a Lict and features two main use-cases or +    syntaxes for interacting with data. + +    The first method (the formal method) is through the use of normal index +    access via attributes or subscripts.  In this case, element keys are usually +    given.  When a new index is defined, it is inserted at the end of the +    payload.  Modifications to existing indices change the data in-place, and +    this causes the content of the payload to shift around if the replaced data +    is of a different length. + +    The second method (the quick method) is through the use of Payload's __call__ +    method.  This is a general purpose "quick action" method that, among other +    things, will insert data to the payload.  If the Payload object is called +    with 1 or more arguments, the values of these arguments are appended to the +    payload in the order given.  There is no way to specify keys using this +    option, so the data simply occupies unkeyed elements in the underlying Lict. + +    In either case, the data inserted must be bytes-like.  In some common cases, +    the data will be coerced into bytes.  See the method __prep_insertion for +    details on how this is handled.  See the PayloadEntry module for some +    additional features. + +    When retrieving indices from the payload, instead of the element's value, +    either the element offset or (for IndexEntries) the value based at that +    offset is returned to you.  If the payload has a non-zero base, this is +    interpreted as the element's address in memory.  This is useful for any +    exploit that requires pointers to other crafted data. + +    The binary output of a payload is simply the binary output of each of its +    elements, concatenated together - there are no gaps.  If you need to space +    or separate two elements, you need to insert padding bytes between them. +    The element binary content is either the object itself (for bytes elements), +    the output from `payload_bytes()` (for PayloadEntries), or the output from +    `bytes(obj)` for everything else. + +    The following is a simple example using the Payload module to perform a +    hypothetical stack buffer overrun "ret2win" with both of the build syntaxes: + +        # 100 bytes from the start of the buffer to the saved frame pointer +        # call (return into) the function "pwned", given by an ELF Symtbl +        # 3 arguments, which are given on the stack + +        # formal method +        p = Payload() +        p.smash = padlen(100) +        p.fp    = placeholder() +        p.ret   = elf.sym.pwned +        p.ret2  = placeholder() +        p.arg1  = 0 +        p.arg2  = 1 +        p.arg3  = 2 +        io.write(bytes(p)) + +        # quick method +        p = Payload()(padlen(100), placeholder()) +        p(elf.sym.pwned, placeholder(), 0, 1, 2) +        io.write(p()) +    """ + +    def __init__(self, base=0, entries=None): +        """Construct new Payload with optional base and content.""" +        super().__init__(base) +        if not isinstance(entries, Lict): +            entries = Lict(entries) +        object.__setattr__(self, "__entries__", entries) + +    def __repr__(self): +        """Return human-readable Payload.""" +        FMT = "\n{:<20} {:<20} {:<20}" +        s = f"{len(self.__entries__)} items, {len(self)} bytes @ {hex(self)}" + +        if len(self.__entries__) > 0: +            s += FMT.format("ADDRESS", "SYMBOL", "DATA") + +        for i, value in enumerate(self.__entries__): +            key = self.__entries__.idx2key(i) +            key = "(unkeyed)" if key is None else str(key) +            key = f"[{key}]" if isinstance(value, IndexEntry) else key + +            addr = self.__addrof(i) +            data = str(self.__bytesof(i)) +            if len(data) > _REPR_DATA_LEN: +                data = data[:_REPR_DATA_LEN] + " ..." + +            s += FMT.format(hex(addr), key, data) + +        return s + +    def __bytes__(self): +        """Return calculated payload bytes.""" +        x = [ self.__bytesof(i) for i in range(len(self.__entries__)) ] +        return b"".join(x) + +    def __call__(self, *args): +        """ +        Payload quick-action call operator. + +        If called with arguments, append these values to the payload in the +        order given.  The payload (self) is returned for easily chaining calls. + +        If called without arguments, return the rendered payload content as if +        `bytes(payload)` was called. +        """ +        if len(args) == 0: +            return bytes(self) + +        for value in args: +            value = self.__prep_insertion(value, self.end()) +            self.__entries__.append(value) -class Payload: -    MAGIC = b'\xef' - -    def __init__(self, **kwargs): -        self.payload = b'' -        self.sym = Symtbl(**kwargs) -        self.ctrs = {} - -    def __len__(self): -        return len(self.payload) - -    def __call__(self, badbytes=b''): -        found = [ hex(x) for x in set(self.payload).intersection(badbytes) ] -        if len(found) > 0: -            raise Exception(f'Payload: bad bytes in content: {found}') -        return self.payload - -    def _name(self, kind, sym): -        if sym is not None: return sym -        try: ctr = self.ctrs[kind] -        except: ctr = 0 -        self.ctrs[kind] = ctr + 1 -        return f'{kind}_{ctr}' - -    def _append(self, value, sym): -        (self.sym @ 0)[sym] = len(self) -        self.payload += value -        return self - -    def _prepend(self, value, sym): -        self.sym >>= len(value) -        (self.sym @ 0)[sym] = 0 -        self.payload = value + self.payload          return self      def end(self): -        return self.sym.base + len(self) - -    def bin(self, *values, sym=None): -        return self._append(b''.join(values), sym=self._name('bin', sym)) - -    def str(self, *values, sym=None): -        values = [ v.encode() + b'\x00' for v in values ] -        return self.bin(*values, sym=self._name('str', sym)) - -    def int(self, *values, sym=None): -        values = [ itob(v) for v in values ] -        return self.bin(*values, sym=self._name('int', sym)) +        """Return the offset or address of the end of the payload.""" +        return self.base + len(self) -    def int8(self, *values, sym=None): -        values = [ itob(v, 1) for v in values ] -        return self.bin(*values, sym=self._name('int', sym)) +    # IndexTbl abstract methods -    def int16(self, *values, sym=None): -        values = [ itob(v, 2) for v in values ] -        return self.bin(*values, sym=self._name('int', sym)) +    def __copy__(self): +        """Return copy of object with shared data entries.""" +        return Payload(self.base, self.__entries__) -    def int32(self, *values, sym=None): -        values = [ itob(v, 4) for v in values ] -        return self.bin(*values, sym=self._name('int', sym)) +    def __iter__(self): +        """Iterate over data entries.""" +        return iter(self.__entries__) -    def int64(self, *values, sym=None): -        values = [ itob(v, 8) for v in values ] -        return self.bin(*values, sym=self._name('int', sym)) - -    def ret(self, *values, sym=None): -        return self.int(*values, sym=self._name('ret', sym)) - -    def sbp(self, *values, sym=None): -        if len(values) == 0: -            return self.rep(self.MAGIC, arch.wordsize, sym=self._name('sbp', sym)) -        return self.int(*values, sym=self._name('sbp', sym)) - -    def rep(self, value, size, sym=None): -        return self.bin(self._rep_helper(value, size), sym=self._name('rep', sym)) - -    def pad(self, size, value=None, sym=None): -        return self.bin(self._pad_helper(size, value), sym=self._name('pad', sym)) - -    def pad_front(self, size, value=None, sym=None): -        return self._prepend(self._pad_helper(size, value), sym=self._name('pad', sym)) - -    def _rep_helper(self, value, size, *, explain=''): -        if size < 0: -            raise Exception(f'Payload: {explain}rep: available space is negative') -        if (size := size / len(value)) != int(size): -            raise Exception(f'Payload: {explain}rep: element does not divide the space evenly') -        return value * int(size) - -    def _pad_helper(self, size, value): -        return self._rep_helper(value or arch.nopcode, size - len(self), explain='pad: ') +    def __len__(self): +        """Return the size of the payload content in bytes.""" +        return len(bytes(self)) + +    def __getindex__(self, index): +        """Return payload index value or address.""" +        value, _ = self.__valueof(index) +        return value + +    def __setindex__(self, index, value): +        """Set payload index value.""" +        try: +            addr = self.__addrof(index) +        except KeyError: +            addr = self.end() +        value = self.__prep_insertion(value, addr) +        self.__entries__[index] = value + +    def __delindex__(self, index): +        """Delete payload index.""" +        del self.__entries__[index] + +    # Payload helpers + +    def __valueof(self, index): +        """Return a tuple: (addr of value, literal value) for index.""" +        value = self.__entries__[index] +        addr = self.__addrof(index) +        if isinstance(value, IndexEntry): +            value @= addr +            return value, value +        return addr, value + +    def __addrof(self, index): +        """Return address (base + offset) for index.""" +        index = self.__entries__.key2idx(index) +        sizes = [ len(self.__bytesof(i)) for i in range(index) ] +        return self.base + sum(sizes) + +    def __bytesof(self, index): +        """Return byte output for index.""" +        _, value = self.__valueof(index) +        if isinstance(value, PayloadEntry): +            return value.payload_bytes(self) +        return bytes(value) + +    def __prep_insertion(self, value, addr): +        """Initialize or type coerce input value for payload insert.""" +        if isinstance(value, PayloadEntry): +            value @= addr +            value.payload_insert(self) +            return value + +        if type(value) is str: +            value = value.encode() + b"\x00" +        elif type(value) is int: +            value = itob(value) + +        # Confirm value has a functional conversion to bytes +        bytes(value) +        return value diff --git a/sploit/payload/payload_entry.py b/sploit/payload/payload_entry.py new file mode 100644 index 0000000..7088f83 --- /dev/null +++ b/sploit/payload/payload_entry.py @@ -0,0 +1,99 @@ +from sploit.arch import arch, itob +from sploit.types.index_entry import IndexEntry + +_PLACEHOLDER_MAGIC = b"\xef" + +class PayloadEntry(IndexEntry): +    """Base class for dynamic Payload entries""" + +    def __repr__(self): +        """Return human-readable entry description.""" +        return f"{self.__class__.__name__}{self.__dict__}" + +    def payload_insert(self, payload): +        """ +        Called on insert into a payload object. + +        Override this method to perform any initialization which requires a +        reference to the payload object.  self.base is set to the insertion +        location. +        """ +        pass + +    def payload_bytes(self, payload): +        """ +        Called to generate bytes for this entry. + +        Override this method to generate and return the binary output for this +        dynamic payload entry.  self.base is set to the current entry address +        or offset. +        """ +        return b"" + +# Concrete payload entry definitions + +class pointer(PayloadEntry): +    """Generate an integer which is always a fixed offset from self.base.""" + +    def __init__(self, target=None): +        self.target = target + +    def payload_insert(self, payload): +        if self.target is None: +            self.target = self.base +        self.target -= self.base + +    def payload_bytes(self, payload): +        return itob(self.target + self.base) + +class padlen(PayloadEntry): +    """Generate padding to reach a target payload length.""" + +    def __init__(self, size, data=None): +        self.size = size +        self.data = data + +    def _gen_padding(self, size): +        data = self.data or arch.nopcode +        if size < 0: +            raise ValueError("padding: Available space is negative") +        if (size := size / len(data)) != int(size): +            raise ValueError("padding: Element does not divide the space evenly") +        return data * int(size) + +    def payload_bytes(self, payload): +        return self._gen_padding(self.size - (self.base - payload.base)) + +class padabs(padlen): +    """Generate padding to reach a target absolute address.""" + +    def payload_bytes(self, payload): +        return self._gen_padding(self.size - self.base) + +class padrel(padlen): +    """Generate a fixed length of padding (aka: length relative to self).""" + +    def payload_bytes(self, payload): +        return self._gen_padding(self.size) + +class padalign(padlen): +    """Generate padding to reach next aligned address.""" + +    def __init__(self, size=None, data=None): +        self.size = size +        self.data = data + +    def payload_bytes(self, payload): +        size = self.size or arch.alignment +        return self._gen_padding(-self.base % size) + +class placeholder(padlen): +    """Generate fixed length of magic bytes, one word length by default.""" + +    def __init__(self, size=None): +        self.size = size +        self.data = _PLACEHOLDER_MAGIC + +    def payload_bytes(self, payload): +        size = self.size or arch.wordsize +        return self._gen_padding(size) diff --git a/sploit/payload/rop.py b/sploit/payload/rop.py index 54226b4..30467de 100644 --- a/sploit/payload/rop.py +++ b/sploit/payload/rop.py @@ -1,78 +1,68 @@ -""" -ROP chain generation utilities - -This module contains tools for automating basic return-oriented-programming -workloads, such as loading register values and calling into arbitrary functions -or syscalls.  The tools are currently designed to work on x86 (32 or 64 bit) -and ARM (32 bit only). - -The main appeal of the ROP class is the ability to abstract away the manual -construction of ROP chain data, and instead make declarative statements like -"call this function with these arguments."  The ROP class will also utilize its -supplied binary objects to automatically find and use trivial gadgets. - -The user is able to provide annotations for more complicated gadgets, which help -instruct the class how to incorporate them into a ROP chain.  This is done with -the GadHint dataclass.  GadHint objects are provided to a ROP instance by -including them in the Symtbl of one of the binary objects it is constructed with. -If applicable, a user-supplied gadget will take precedence over automatic gadget -searching. - -See the GadHint class to learn more about the descriptive attributes that are -supported. -""" -  from graphlib import TopologicalSorter  from sploit.arch import arch, btoi, itob  from sploit.payload.gadhint import GadHint  from sploit.payload.payload import Payload +from sploit.payload.payload_entry import padalign, padlen -class ROP(Payload): -    """ -    ROP-enabled payload builder - -    POP_MAGIC (int): Magic value used for pop instructions where no specific -    value is required by the user. +_POP_MAGIC = 0xdead +_SPM_MAGIC = b"\x69" +_ERROR_MAGIC = 0xbaadc0de -    SPM_MAGIC (bytes): Magic value to fill the stack with when the best -    available cleaning gadget is larger than is necessary. - -    objects (list[ELF]): The binary objects this ROP instance will consider -    for gadget searching. +class ROP: +    """ +    ROP chain generation tool + +    This class contains methods for automating basic return-oriented programming +    workloads, such as loading register values and calling into arbitrary +    functions or syscalls.  The tools are currently designed to work on x86 +    (32 or 64 bit) and ARM (32 bit only). + +    The main appeal of the ROP class is the ability to abstract away the manual +    construction of ROP chain data, and instead make declarative statements +    like "call this function with these arguments".  The ROP class will also +    utilize its supplied binary objects to automatically find and use trivial +    gadgets. + +    The user is able to provide annotations for more complicated gadgets, which +    help instruct the class how to incorporate them into a ROP chain.  This is +    done with the GadHint dataclass.  GadHint objects are provided to a ROP +    instance by including them in the Symtbl of one of the binary objects it is +    constructed with.  If applicable, a user-supplied gadget will take +    precedence over automatic gadget searching.  See the GadHint module to learn +    more about the descriptive attributes that are supported. + +    objects (list[ELF]): The binary objects this ROP instance will consider for +    gadget searching.  If one of these is the target executable binary, it +    should appear first in the list.      safe_syscalls (bool): If True, require that automatically found syscall      instructions are immediately followed by a return instruction.      align_calls (bool): If True, ensure that the stack return address into      function calls is aligned according to the architecture alignment property. -    Knowledge of alignment is taken from the instance Symtbl's base value.      clean_stack (bool): If True, attempt to locate a cleaning gadget to "pop"      stack data that is leftover from a function call.  Required if attempting      to make multiple calls that involve stack-based arguments.      """ -    POP_MAGIC = 0xdead -    SPM_MAGIC = b'\x69' -      def __init__(self, *objects, safe_syscalls=True, align_calls=True, -                 clean_stack=True, **symbols): -        """Initialize new ROP builder instance.""" -        super().__init__(**symbols) +                 clean_stack=True): +        """Construct new ROP builder."""          self.objects = objects          self.safe_syscalls = safe_syscalls          self.align_calls = align_calls          self.clean_stack = clean_stack -    def gadgets(self, *regexes, cont=False): +    def search_gadgets(self, *regexes, cont=False):          """Return a list of matching gadgets, considering all objects."""          results = []          for obj in self.objects:              results += obj.gadgets(*regexes, cont=cont)          return results -    def gadget(self, *regexes): +    def search_gadget(self, *regexes):          """Return the first matching gadget, considering all objects."""          for obj in self.objects:              try: @@ -82,75 +72,70 @@ class ROP(Payload):          raise LookupError(              f"ROP: Need to define gadget symbol for {'; '.join(regexes)}") -    def assign(self, *, sym=None, **sets): +    def gadget(self, gadget):          """ -        Insert a ROP chain to control given registers. +        Return a generic ROP payload. + +        gadget (GadHint): Annotated gadget to prepare a chain from. +        """ +        return self.__build_chain(gadget, {}) + +    def assign(self, **sets): +        """ +        Return a ROP payload to control given registers.          **sets (str:int): Keyword arguments specify register assignments to          perform with this ROP chain.  Argument names correspond to register          names. - -        sym (str): If given, sym is the symbol name used to refer to the -        inserted data.          """ -        gadget = GadHint(0, requirements=sets) -        return self._start_chain(gadget, sym=self._name("assign", sym)) +        return self.gadget(GadHint(requirements=sets)) -    def call(self, func, *params, sym=None): +    def call(self, func, *params):          """ -        Insert a ROP chain to call function. +        Return a ROP payload to call function.          func (int): Entry address of function to call.          *params (int): Remaining positional args are passed to func. - -        sym (str): If given, sym is the symbol name used to refer to the -        inserted data.          """          register_params = dict(zip(arch.funcargs, params))          stack_params = params[len(register_params):]          gadget = GadHint(func, requirements=register_params, stack=stack_params,                           align=self.align_calls) -        return self._start_chain(gadget, sym=self._name("call", sym)) +        return self.gadget(gadget) -    def syscall(self, *params, sym=None): +    def syscall(self, *params):          """ -        Insert a ROP chain to call kernel. +        Return a ROP payload to call kernel.          *params (int): The first argument is the syscall number.  Remaining          positional arguments are passed to the syscall. - -        sym (str): If given, sym is the symbol name used to refer to the -        inserted data.          """          if len(params) > len(arch.kernargs):              raise TypeError("ROP: Too many arguments passed to syscall. " -                "Target architecture supports up to {len(arch.kernargs)-1}.") +                f"Target architecture supports up to {len(arch.kernargs)-1}.")          register_params = dict(zip(arch.kernargs, params)) -        gadget = self._get_gadget("syscall", {}).with_requirements(register_params) -        return self._start_chain(gadget, sym=self._name("syscall", sym)) +        sc = self.__get_gadget("syscall", {}) +        return self.gadget(sc.with_requirements(register_params)) -    def memcpy(self, dst, src, *, sym=None): +    def memcpy(self, dst, src):          """ -        Insert a ROP chain to write data into memory. +        Return a ROP payload to write data into memory.          dst (int): The destination memory address.          src (bytes): The content to write. - -        sym (str): If given, sym is the symbol name used to refer to the -        inserted data.          """ -        gadgets = [] +        data = Payload()          for idx in range(0, len(src), arch.wordsize): -            g = self._get_write(dst + idx, btoi(src[idx:idx+arch.wordsize])) -            gadgets.append(g) -        return self._start_chain(*gadgets, sym=self._name("memcpy", sym)) +            word = btoi(src[idx:idx+arch.wordsize]) +            data(self.gadget(self.__get_write(dst+idx, word))) +        return data -    def _get_hints(self): +    def __get_hints(self):          """Return all user-supplied gadget hints."""          return [h for obj in self.objects for _,h in obj.sym if type(h) is GadHint] -    def _discover_requirements(self, seen, graph, current): +    def __discover_requirements(self, seen, graph, current):          """          Populate gadget dependency graph. @@ -170,13 +155,13 @@ class ROP(Payload):              # Requiring a register to store different values may require the              # use of multiple gadgets.              if (r, v) not in seen: -                gadget = self._get_gadget(r, current.requirements) +                gadget = self.__get_gadget(r, current.requirements)                  # Add gadget's requirements to the dependency graph.                  # We say that each requirement is a 'successor' to this                  # current gadget 'r', so that the chain builder will satisfy                  # 'r' first.  This prevents the fulfillment of 'r' from -                # colbbering targets it requires, as the builder will satisfy +                # clobbering targets it requires, as the builder will satisfy                  # them afterward.                  for x in gadget.requirements:                      graph.add(x, r) @@ -189,9 +174,9 @@ class ROP(Payload):                  # Mark node as visited                  seen.add((r, v)) -                self._discover_requirements(seen, graph, gadget) +                self.__discover_requirements(seen, graph, gadget) -    def _get_gadget(self, target, sets): +    def __get_gadget(self, target, sets):          """          Get context-specific gadget. @@ -204,7 +189,7 @@ class ROP(Payload):          """          # First, consider user-provided hints before automatically locating          # gadgets. -        for hint in self._get_hints(): +        for hint in self.__get_hints():              # Setup additional requirements based on hint's register moves.              # If a mov target is in sets, require to set the src to the 'sets'              # value. @@ -229,17 +214,17 @@ class ROP(Payload):          # Automatically locate simple gadgets          if target == "ret": -            return GadHint(self.gadget(arch.ret)) +            return GadHint(self.search_gadget(arch.ret))          if target == "syscall":              insns = [arch.syscall, arch.ret] if self.safe_syscalls else [arch.syscall] -            return GadHint(self.gadget(*insns), syscall=True) +            return GadHint(self.search_gadget(*insns), syscall=True)          # target == register          insns = [ i.format(target) for i in arch.popgad ] -        return GadHint(self.gadget(*insns), pops=[target], spm=arch.wordsize) +        return GadHint(self.search_gadget(*insns), pops=[target]) -    def _get_clean(self, size): +    def __get_clean(self, size):          """          Get a stack cleaning gadget that moves sp by _at least_ size. @@ -249,11 +234,11 @@ class ROP(Payload):          # the user likely hasn't annotated the GadHint properly.  Returning a          # larger move than requested is fine, since the chain builder can insert          # junk to be popped. -        for hint in self._get_hints(): +        for hint in self.__get_hints():              if hint.spm >= size and hint.spm > 0:                  return hint -        results = self.gadgets(*arch.cleangad) +        results = self.search_gadgets(*arch.cleangad)          table = { int(g.asm[0].group(1), 0): g for g in results }          sizes = sorted([ x for x in table.keys() if x >= size ]) @@ -263,7 +248,7 @@ class ROP(Payload):          raise LookupError(              f"ROP: Need to define a stack move gadget of at least {size}") -    def _get_write(self, dst, src): +    def __get_write(self, dst, src):          """          Get a memory write gadget, injected with requirements for user data. @@ -273,7 +258,7 @@ class ROP(Payload):          # If any exist, take the first write provided by user hints, assuming          # the user's intent to specifically use _this_ write.  Follow-on gadgets          # to setup the dst and src registers must be findable. -        for hint in self._get_hints(): +        for hint in self.__get_hints():              if hint.writes:                  d, s = list(hint.writes.items())[0]                  return hint.with_requirements({d:dst, s:src}) @@ -281,7 +266,7 @@ class ROP(Payload):          # Only take an automatic write gadget if we can prove up front that its          # requirements can be met, otherwise move on.  A later search result may          # pass the test. -        results = self.gadgets(*arch.writegad) +        results = self.search_gadgets(*arch.writegad)          for gad in results:              d = gad.asm[0].group("dst") @@ -290,52 +275,45 @@ class ROP(Payload):              try:                  # Assert requirements are met.                  gadget = GadHint(gad, writes={d: s}, requirements={d:dst, s:src}) -                self._discover_requirements(set(), TopologicalSorter(), gadget) +                self.__discover_requirements(set(), TopologicalSorter(), gadget)                  return gadget              except:                  pass          raise LookupError("ROP: Need to define gadgets for memory write / deps") -    def _start_chain(self, *gadgets, sym=None): -        """ -        Insert a generic ROP chain. - -        *gadgets (GadHint): Annotated gadgets to prepare a chain from. - -        sym (str): If given, sym is the symbol name used to refer to the -        inserted data. +    def __build_chain(self, gadget, sets):          """ -        stack = Payload(base=self.end()) -        for g in gadgets: -            self._build_chain(stack, g, {}) -        return self.bin(stack(), sym=self._name("gadget", sym)) - -    def _build_chain(self, stack, gadget, sets): -        """ -        Generate chain data for a given ROP gadget. +        Generate ROP chain data for a given gadget.          This function recursively builds a ROP chain for the given gadget and -        its requirements, storing data in the 'stack' object. +        its requirements, returning the result as a Payload. -        stack (Payload): Stack data being constructed. -        gadget (GadHint): Current gadget we are processing. +        gadget (GadHint): Current gadget to process.          sets (dict{str:int}): The set of other register requirements we are          trying to fulfill in parallel.          """          # Form a to-do-list of registers from our immediate requirements,          # attempting to order them such that we avoid overwriting/conflicting -        # values (this may not be possible). +        # values.  This may not be possible, in which case graph.static_order() +        # will raise an exception.          reqs = gadget.requirements          graph = TopologicalSorter({ r:set() for r in reqs }) -        self._discover_requirements(set(), graph, gadget) +        self.__discover_requirements(set(), graph, gadget)          to_do_list = [ x for x in graph.static_order() if x in reqs ] +        chain = Payload() +          # Start chain by satisfying to-do-list requirements. +        if len(to_do_list) > 0: +            chain.requirements = Payload() +          while len(to_do_list) > 0: -            g = self._get_gadget(to_do_list[0], reqs) -            self._build_chain(stack, g, reqs) +            r = to_do_list[0] +            g = self.__get_gadget(r, reqs) +            c = self.__build_chain(g, reqs) +            chain.requirements[f"{r}_{reqs[r]}"] = c              # This gadget may satisfy multiple items in the to-do-list.              # Specifically, all of its pop and mov targets, and any load @@ -351,33 +329,36 @@ class ROP(Payload):          if gadget.offset != 0:              # Stack alignment if required.              if gadget.align: -                align = -stack.end() % arch.alignment -                stack.rep(itob(self._get_gadget("ret", {})), align) +                ret = self.__get_gadget("ret", {}) +                chain.alignment = padalign(0, itob(ret))              # "Return address" entry into this gadget. -            stack.ret(gadget.offset) +            chain.gadget = gadget.offset -            # The gadget's "inner stack data" will be values to be popped -            # and additional junk data to be deallocated by the gadget itself. -            sp_dest = len(stack) + gadget.spm -            stack.int(*[ sets.get(p, self.POP_MAGIC) for p in gadget.pops ]) -            if gadget.spm > 0: -                stack.pad(sp_dest, self.SPM_MAGIC) +            # The gadget's "inner stack data" will be values to be popped and +            # additional junk data to be deallocated by the gadget itself. +            if gadget.pops or gadget.spm > 0: +                chain.inner = Payload() +                chain.inner(*[ sets.get(p, _POP_MAGIC) for p in gadget.pops ]) +                if gadget.spm > 0: +                    chain.inner.pad = padlen(gadget.spm, _SPM_MAGIC)              # The gadget's "outer stack data" will be the additional values              # explicitly specified by the gadget.  Append a separate gadget              # to clean up these values. -            if len(gadget.stack) > 0: +            if gadget.stack:                  size = len(gadget.stack) * arch.wordsize                  if self.clean_stack: -                    clean = self._get_clean(size) -                    stack.ret(clean) -                    sp_dest = len(stack) + clean.spm +                    clean = self.__get_clean(size) +                    chain.cleanup = clean.offset +                    pad = padlen(clean.spm, _SPM_MAGIC)                  else: -                    ret = self._get_gadget("ret", {}) -                    stack.ret(ret) -                    sp_dest = len(stack) + size +                    chain.cleanup = _ERROR_MAGIC +                    pad = None + +                chain.outer = Payload() +                chain.outer(*gadget.stack) +                if pad: chain.outer.pad = pad -                stack.int(*gadget.stack) -                stack.pad(sp_dest, self.SPM_MAGIC) +        return chain diff --git a/sploit/rev/gadget.py b/sploit/rev/gadget.py index a2564c0..cc69723 100644 --- a/sploit/rev/gadget.py +++ b/sploit/rev/gadget.py @@ -1,35 +1,24 @@  from dataclasses import dataclass, field +from sploit.types.index_entry import IndexEntry  @dataclass -class Gadget: +class Gadget(IndexEntry):      """      Basic gadget description object -    offset (int): The location this gadget is found at.  What `offset` is -    relative to depends on context. +    base (int): The location this gadget is found at.  What `base` is relative +    to depends on context.      asm (list[re.Match]): A list of assembly instructions matched by the gadget      search query.      """ -    offset: int = 0 +    base: int = 0      asm: list = field(default_factory=list) -    def __index__(self): -        """Convert object to integer using offset value.""" -        return self.offset - -    def __add__(self, x): -        """Return new object with adjusted offset.""" -        return Gadget(self.offset + x, self.asm) - -    def __sub__(self, x): -        """Return new object with adjusted offset.""" -        return self + (-x) -      def __repr__(self):          """Return human-readable Gadget.""" -        s = hex(self.offset) +        s = hex(self.base)          if len(self.asm) > 0:              asm = "; ".join([ m.string for m in self.asm ])              s += f", '{asm}'" diff --git a/sploit/symtbl.py b/sploit/symtbl.py index a471958..86800f5 100644 --- a/sploit/symtbl.py +++ b/sploit/symtbl.py @@ -73,112 +73,88 @@ with the Symtbl base address.      print(s.a, s.b, s.c, s.d)   # "998 999 1000 1001"  """ +from sploit.types.indextbl import IndexTbl +from sploit.types.index_entry import IndexEntry +  def Symtbl(*, base=0, **symbols):      """      Create a new Symtbl object.      Return an empty Symtbl or, optionally, one initialized with the given -    symbol values.  Arguments _must_ be keyword arguments. +    symbol values.  Arguments must be keyword arguments.      Users should call this function instead of attempting to construct the      Symtbl class.  Construction is implemented via a normal function to prevent      any argument name from conflicting with __init__'s bound instance parameter.      """ -    self = SymtblImpl({}, 0, base) +    self = SymtblImpl(base, 0, dict())      for k, v in symbols.items():          self[k] = v      return self -class SymtblImpl: +class SymtblImpl(IndexTbl):      """Symtbl implementation class""" -    def __init__(self, entries, adjust, base): +    def __init__(self, base, adjust, entries):          """Construct Symtbl from instance data.""" +        super().__init__(base) +        object.__setattr__(self, "__adjust__", adjust)          object.__setattr__(self, "__entries__", entries) -        object.__setattr__(self, "__adjust__",  adjust) -        object.__setattr__(self, "base", base) -    def __index__(self): -        """Convert object to integer using base value.""" -        return self.base +    def __repr__(self): +        """Return human-readable Symtbl.""" +        FMT = "\n{:<20} {:<20}" +        s = f"{len(self)} symbols @ {hex(self)}" -    def __matmul__(self, base): -        """Create remapped version of object at absolute base.""" -        return SymtblImpl(self.__entries__, self.__adjust__, int(base)) +        if len(self) > 0: +            s += FMT.format("ADDRESS", "SYMBOL") -    def __add__(self, offset): -        """Create remapped version of object at relative base.""" -        return self @ (self.base + offset) +        for key, value in self: +            key = f"[{key}]" if isinstance(value, IndexEntry) else key +            s += FMT.format(hex(value), key) -    def __sub__(self, offset): -        """Create remapped version of object at relative base.""" -        return self @ (self.base - offset) +        return s      def __rshift__(self, offset): -        """Create symbol adjusted version of object.""" -        return SymtblImpl(self.__entries__, self.__adjust__ + int(offset), self.base) +        """Return symbol-adjusted version of object.""" +        adjust = self.__adjust__ + int(offset) +        return SymtblImpl(self.base, adjust, self.__entries__)      def __lshift__(self, offset): -        """Create symbol adjusted version of object.""" +        """Return symbol-adjusted version of object."""          return self >> (-offset)      def __mod__(self, offset): -        """Create symbol rebased version of object.""" +        """Return symbol-rebased version of object."""          return self >> (self.base - offset) -    def __getattr__(self, symbol): -        """Return symbol offset or subtable via pseudo-attribute.""" -        return self[symbol] +    # IndexTbl abstract methods -    def __setattr__(self, symbol, value): -        """Set symbol offset or subtable via pseudo-attribute.""" -        self[symbol] = value +    def __copy__(self): +        """Return copy of object with shared symbol entries.""" +        return SymtblImpl(self.base, self.__adjust__, self.__entries__) -    def __delattr__(self, symbol): -        """Unset symbol via pseudo-attribute.""" -        del self[symbol] +    def __iter__(self): +        """Iterate over table items, sorted by offsets.""" +        it = { k: self[k] for k in self.__entries__}.items() +        return iter(sorted(it, key=lambda x: int(x[1])))      def __len__(self):          """Return number of defined symbols."""          return len(self.__entries__) -    def __getitem__(self, symbol): -        """Return symbol offset, subtable, or translated offset via subscript.""" -        if symbol == "base": -            return self.base -        offset = self.__entries__[symbol] if type(symbol) is str else symbol +    def __getindex__(self, index): +        """Return symbol value or translated offset.""" +        if isinstance(index, (int, IndexEntry)): offset = index +        else: offset = self.__entries__[index]          return offset + (self.base + self.__adjust__) -    def __setitem__(self, symbol, value): -        """Set symbol offset or subtable via subscript.""" -        if symbol == "base": -            object.__setattr__(self, "base", int(value)) -        elif symbol in dir(self): -            raise KeyError(f"Symtbl: key is reserved: {symbol}") -        elif type(symbol) is not str: -            raise TypeError(f"Symtbl: key must be a string: {symbol}") -        else: -            self.__entries__[symbol] = value - (self.base + self.__adjust__) - -    def __delitem__(self, symbol): -        """Unset symbol via subscript.""" -        del self.__entries__[symbol] +    def __setindex__(self, index, value): +        """Set symbol value.""" +        if isinstance(index, (int, IndexEntry)): +            raise TypeError(f"Symtbl: Unsupported key type: {type(index)}") +        self.__entries__[index] = value - (self.base + self.__adjust__) -    def __iter__(self): -        """Iterate over table entries as key:value tuples, like dict.items().""" -        return iter(sorted({ k: self[k] for k in self.__entries__ }.items(), key=lambda v: int(v[1]))) - -    def __contains__(self, symbol): -        """Test symbol name membership in table.""" -        return symbol in self.__entries__ - -    def __repr__(self): -        """Return human-readable Symtbl.""" -        FMT = "\n{:<20} {:<20}" -        s = f"{len(self)} symbols @ {hex(self)}" -        if len(self) > 0: -            s += FMT.format("ADDRESS", "SYMBOL") -        for symbol, offset in self: -            disp = f"[{symbol}]" if type(offset) is not int else symbol -            s += FMT.format(hex(offset), disp) -        return s +    def __delindex__(self, index): +        """Delete symbol.""" +        del self.__entries__[index] diff --git a/sploit/types/__init__.py b/sploit/types/__init__.py new file mode 100644 index 0000000..a618162 --- /dev/null +++ b/sploit/types/__init__.py @@ -0,0 +1,3 @@ +from .indextbl import * +from .index_entry import * +from .lict import * diff --git a/sploit/types/index_entry.py b/sploit/types/index_entry.py new file mode 100644 index 0000000..a03ab92 --- /dev/null +++ b/sploit/types/index_entry.py @@ -0,0 +1,44 @@ +import copy + +class IndexEntry: +    """ +    Generic IndexTbl entry object + +    IndexEntry is intended to be subclassed to create interesting types that are +    compatible with IndexTbl directories.  IndexEntry gives objects a baseline +    int-like personality. + +    IndexEntry objects are convertable to int via int(), bin(), hex(), etc.  This +    integer value is manipulated via the object's "base" property, and a few +    operators are implemented to provide nicer syntax for this as well.  The use +    of operators generally yield distinct copies of the original object. + +    The property name "base" is used since it has semantic meaning for the +    IndexTbl class, which is itself an extension of this class. + +    base (int): Index integer value +    """ + +    base = 0 + +    def __init__(self, base=0): +        """Construct index with the given base value.""" +        self.base = base + +    def __index__(self): +        """Convert index to base integer value.""" +        return int(self.base) + +    def __matmul__(self, base): +        """Create new object with the given base value.""" +        new = copy.copy(self) +        new.base = base +        return new + +    def __add__(self, add): +        """Create new object with the given relative base value.""" +        return self @ (self.base + add) + +    def __sub__(self, sub): +        """Create new object with the given relative base value.""" +        return self @ (self.base - sub) diff --git a/sploit/types/indextbl.py b/sploit/types/indextbl.py new file mode 100644 index 0000000..4f57a59 --- /dev/null +++ b/sploit/types/indextbl.py @@ -0,0 +1,103 @@ +from abc import abstractmethod +from collections.abc import Collection + +from sploit.types.index_entry import IndexEntry + +class IndexTbl(IndexEntry, Collection): +    """ +    Abstract Index Table + +    IndexTbl is a common interface to an abstracted key-value store.  The +    storage mechanism as well as lookup semantics are defined by concrete +    implementations.  "Index" in this case is more akin to a directory index or +    book index than a strictly numeric array index. + +    In general, concrete tables may store values of any or multiple different +    types.  In particular, tables should give special accommodation for values +    of type IndexEntry.  These objects usually represent "rich" versions of the +    nominal data types the table expects to contain.  Implementation repr() +    methods usually also annotate which members are IndexEntries. + +    IndexTbl extends from IndexEntry, and so has a base value which represents +    the "base index" of the table.  The meaning of this depends on the +    implementation.  This inheritance also means that tables are generally +    expected to be nestable. + +    IndexTbls allow indices to be accessed via attribute or subscript notation. +    This is probably the key characteristic feature of the class.  The class +    namespace is kept as clean as possible to make for the fewest collisions +    between index names and other (real) class attributes.  Note that there are +    abstract methods, required to be overridden, which implement the index +    access.  These methods are only called for normal indices, not the table +    base. + +    Because this class overrides attribute access, normal automatic object +    copying is broken.  Because of this, implementations must also provide a +    definition for the __copy__() method as well. +    """ + +    @abstractmethod +    def __getindex__(self, index): +        """Lookup and retrieve index value.""" +        raise NotImplementedError + +    @abstractmethod +    def __setindex__(self, index, value): +        """Lookup and set index value.""" +        raise NotImplementedError + +    @abstractmethod +    def __delindex__(self, index): +        """Lookup and delete index value.""" +        raise NotImplementedError + +    @abstractmethod +    def __copy__(self): +        """Create a copy of this IndexTbl object.""" +        raise NotImplementedError + +    def __contains__(self, index): +        """Test the existence of the given index.""" +        try: +            self.__getindex__(index) +        except KeyError: +            return False +        else: +            return True + +    # Attribute access methods + +    def __getattr__(self, index): +        """Get value via attribute.""" +        return self[index] + +    def __setattr__(self, index, value): +        """Set value via attribute.""" +        self[index] = value + +    def __delattr__(self, index): +        """Delete value via attribute.""" +        del self[index] + +    # Subscript/item access methods + +    def __getitem__(self, index): +        """Get value via subscript.""" +        if index == "base": +            return self.base +        return self.__getindex__(index) + +    def __setitem__(self, index, value): +        """Set value via subscript.""" +        if index == "base": +            object.__setattr__(self, "base", value) +        elif index in dir(self): +            raise KeyError(f"IndexTbl: Index is reserved: {index}") +        else: +            self.__setindex__(index, value) + +    def __delitem__(self, index): +        """Delete value via subscript.""" +        if index == "base": +            raise KeyError("IndexTbl: May not delete index: base") +        self.__delindex__(index) diff --git a/sploit/types/lict.py b/sploit/types/lict.py new file mode 100644 index 0000000..ab6cb1f --- /dev/null +++ b/sploit/types/lict.py @@ -0,0 +1,202 @@ +from collections.abc import MutableSequence, MutableMapping + +class Lict(MutableSequence, MutableMapping): +    """ +    List / dictionary hybrid container + +    Lict attempts to provide an API for a list which supports optional element +    keys in addition to normal positional indices.  For Lict, index types are +    int and slice.  Keys may be any other type except for None, as None +    indicates an unkeyed value. + +    Nearly all of the operations you'd except to perform on list or dict are +    implemented here.  However, in cases of conflict, the list behavior is +    usually preferred (for example: iter(Lict) iterates over values instead of +    keys).  In general, Licts are slightly more list-like, since keys are +    optional, but sequence is required. + +    Licts can be constructed from any iterable, including other Licts.  When +    constructing from mappings, similar heuristics as dict's are used to parse +    key values. + +    In addition to keys and indices, slices may be used to return, modify, or +    delete a portion of a Lict.  The start and end fields of a slice may be +    either keys or indices, however the step field must be an integer as usual. + +    When assigning to a non-existent key, the new element is sequentially +    inserted at the end of the Lict. +    """ + +    def __init__(self, values=None): +        """Construct new Lict, optionally populated by the given iterable.""" +        self.__keys = [] +        self.__vals = [] +        if values is not None: +            self.extend(values) + +    def __repr__(self): +        """Return human-readable Lict.""" +        s = "" +        for i in range(len(self)): +            if self.__keys[i] is not None: +                s += f"{repr(self.__keys[i])}: " +            s += f"{repr(self.__vals[i])}, " +        return f"{{[{s[:-2]}]}}" + +    def __copy__(self): +        """Return shallow copy of object.""" +        return self.copy() + +    def copy(self): +        """Return shallow copy of object.""" +        return Lict(self) + +    def key2idx(self, arg): +        """ +        Return value of index type for the given input. + +        For keys, return the corresponding index, or raise KeyError. +        For slices, return a slice with components converted to index type. + +        If arg is already an index (or None) it is returned as-is with no +        assertions made. +        """ +        if isinstance(arg, slice): +            return slice(self.key2idx(arg.start), self.key2idx(arg.stop), arg.step) +        if isinstance(arg, int) or arg is None: +            return arg +        try: +            return self.__keys.index(arg) +        except ValueError as ex: +            raise KeyError(f"Lict: Key does not exist: {arg}") from ex + +    def idx2key(self, arg): +        """ +        Return value of key type for the given input. + +        For indices, return the corresponding key, None, or raise IndexError. +        For slices, return a slice with components converted to key type. + +        If arg is already a key type (or None) it is returned as-is with no +        assertions made. +        """ +        if isinstance(arg, slice): +            return slice(self.idx2key(arg.start), self.idx2key(arg.stop), arg.step) +        if not isinstance(arg, int) or arg is None: +            return arg +        return self.__keys[arg] + +    def haskey(self, arg): +        """ +        Test existence of key in Lict object. + +        `x in Lict` only tests for the _value_ x in Lict.  This is consistent +        with list behavior.  This method is provided to test keys as well. +        Raises TypeError on any index type. +        """ +        if arg is None: +            return False +        if isinstance(arg, (int, slice)): +            raise TypeError(f"Lict: Unsupported key type: {type(arg)}") +        return arg in self.__keys + +    def __assign_slice(self, i, value): +        """Update Lict values according to element slice.""" +        value = Lict(value) +        tmp = self.copy() +        tmp.__keys[i] = value.__keys +        tmp.__vals[i] = value.__vals + +        check_keys = [ x for x in tmp.__keys if x is not None ] +        if len(check_keys) != len(set(check_keys)): +            raise ValueError("Lict: Slice assignment results in duplicate keys") + +        self.__keys = tmp.__keys +        self.__vals = tmp.__vals + +    # collections.abc abstract methods + +    def __len__(self): +        """Return number of elements in Lict.""" +        assert len(self.__keys) == len(self.__vals) +        return len(self.__keys) + +    def __getitem__(self, arg): +        """Return value for given index, key, or slice.""" +        i = self.key2idx(arg) +        if isinstance(i, slice): +            return Lict(zip(self.__keys[i], self.__vals[i])) +        return self.__vals[i] + +    def __setitem__(self, arg, value): +        """Set value for given index, key, or slice.""" +        try: +            i = self.key2idx(arg) +        except KeyError: +            self.append(value, arg) +        else: +            if isinstance(i, slice): +                self.__assign_slice(i, value) +            else: +                self.__vals[i] = value + +    def __delitem__(self, arg): +        """Delete value for given index, key, or slice.""" +        i = self.key2idx(arg) +        del self.__keys[i] +        del self.__vals[i] + +    def insert(self, where, value, key=None): +        """Insert value into Lict.  Optionally apply the given key.""" +        if self.haskey(key): +            raise KeyError(f"Lict: Key is not unique: {key}") +        i = self.key2idx(where) +        self.__keys.insert(i, key) +        self.__vals.insert(i, value) + +    # Sequence overrides + +    def append(self, value, key=None): +        """Append value to Lict.  Optionally apply the given key.""" +        self.insert(len(self), value, key) + +    def extend(self, values): +        """Append all values in given iterable to the Lict.""" +        try: values = [ [k, v] for k, v in values.items() ] +        except: +            try: values = [ [k, v] for k, v in iter(values) ] +            except: values = [ [None, v] for v in iter(values) ] +        for k, v in values: +            self.append(v, k) + +    def reverse(self): +        """Reverse the sequence of Lict in-place.  Keys follow values.""" +        self.__keys.reverse() +        self.__vals.reverse() + +    # Mapping overrides + +    def get(self, key, default=None): +        """Return value for given key, or default if unable.""" +        try: return self[key] +        except: return default + +    def popitem(self): +        """Pop a key-value pair from the Lict and return it.""" +        return (self.__keys.pop(), self.__vals.pop()) + +    def items(self): +        """Return an iterable of key-value pairs.""" +        return list(zip(self.__keys, self.__vals)) + +    def keys(self): +        """Return an iterable of the Lict's keys.""" +        return [ x for x in self.__keys if x is not None ] + +    def values(self): +        """Return an iterable of the Lict's values.""" +        return list(self.__vals) + +    def update(self, values): +        """Method is unsupported.""" +        raise NotImplementedError | 
