From 048ad6420db3f8ceb7c31b5f8ca50dc79c6985cf Mon Sep 17 00:00:00 2001 From: Malfurious Date: Wed, 24 Jan 2024 22:07:35 -0500 Subject: symtbl: Refactor abstract IndexTbl interface There are some useful concepts expressed in the Symtbl class that can provide good value if applied elsewhere as well. In this particular case, I want to address the somewhat awkward relationship between Symtbl and the Payload class by providing an abstract base for both of them. I will go into more details in an upcoming commit for Payload. This patch shouldn't change any behavior for Symtbl barring perhaps its new preference of the new IndexEntry type described below. Some characteristics of Symtbl are refactored into two new interface types: IndexEntry provides "base" and implements logic supporting the use of instance objects as integers. The intent is to extend from this class when creating special types to be used in IndexTbls, Symtbls, etc. IndexTbl (extends IndexEntry) provides a unified system for attribute / element access, and acts as an abstract container where storage and lookup semantics are up to the specific implementation. Symtbl (extends IndexTbl) is now better described as an Index table, where indices represent numeric addresses. The nominal data type is int, however IndexEntries (which are int-like) may be nested to record the addresses of ROP gadgets, sub-symtbls, and perhaps more in the future. Signed-off-by: Malfurious --- sploit/types/__init__.py | 2 + sploit/types/index_entry.py | 44 +++++++++++++++++++ sploit/types/indextbl.py | 103 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 149 insertions(+) create mode 100644 sploit/types/__init__.py create mode 100644 sploit/types/index_entry.py create mode 100644 sploit/types/indextbl.py (limited to 'sploit/types') diff --git a/sploit/types/__init__.py b/sploit/types/__init__.py new file mode 100644 index 0000000..1316dad --- /dev/null +++ b/sploit/types/__init__.py @@ -0,0 +1,2 @@ +from .indextbl import * +from .index_entry 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) -- cgit v1.2.3 From 8338e1baec2f2e5714863aacda44177e616b6dfc Mon Sep 17 00:00:00 2001 From: Malfurious Date: Fri, 26 Jan 2024 02:34:02 -0500 Subject: lict: Add new list-dictionary hybrid type Lict is a fairly fully-featured data structure which stores elements in a well ordered list, while offering opt-in support for per-element dictionary keys. This type is intended to be the new back-end storage for Payload data, but may have other use-cases as well. An OrderedDict is not a suitable replacement, as they do not permit unkeyed elements. Signed-off-by: Malfurious --- sploit/types/__init__.py | 1 + sploit/types/lict.py | 202 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 203 insertions(+) create mode 100644 sploit/types/lict.py (limited to 'sploit/types') diff --git a/sploit/types/__init__.py b/sploit/types/__init__.py index 1316dad..a618162 100644 --- a/sploit/types/__init__.py +++ b/sploit/types/__init__.py @@ -1,2 +1,3 @@ from .indextbl import * from .index_entry import * +from .lict import * 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 -- cgit v1.2.3