summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--sploit/payload/__init__.py1
-rw-r--r--sploit/payload/gadhint.py43
-rw-r--r--sploit/payload/payload.py281
-rw-r--r--sploit/payload/payload_entry.py99
-rw-r--r--sploit/payload/rop.py245
-rw-r--r--sploit/rev/gadget.py23
-rw-r--r--sploit/symtbl.py112
-rw-r--r--sploit/types/__init__.py3
-rw-r--r--sploit/types/index_entry.py44
-rw-r--r--sploit/types/indextbl.py103
-rw-r--r--sploit/types/lict.py202
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