From 0734bc42d70dfc6da05e946c5eb07d043e788e68 Mon Sep 17 00:00:00 2001 From: Malfurious Date: Fri, 7 Mar 2025 03:44:06 -0500 Subject: rop: Move to new package tech Move the ROP modules into a new nsploit subpackage called "tech". This new package is designated for exploit technique implementations. In general, its contents should not be depended upon by the rest of the library. Signed-off-by: Malfurious --- nsploit/payload/__init__.py | 2 - nsploit/payload/gadhint.py | 98 ----------- nsploit/payload/ret2dlresolve.py | 4 +- nsploit/payload/rop.py | 364 --------------------------------------- nsploit/tech/__init__.py | 2 + nsploit/tech/gadhint.py | 98 +++++++++++ nsploit/tech/rop.py | 364 +++++++++++++++++++++++++++++++++++++++ 7 files changed, 466 insertions(+), 466 deletions(-) delete mode 100644 nsploit/payload/gadhint.py delete mode 100644 nsploit/payload/rop.py create mode 100644 nsploit/tech/__init__.py create mode 100644 nsploit/tech/gadhint.py create mode 100644 nsploit/tech/rop.py diff --git a/nsploit/payload/__init__.py b/nsploit/payload/__init__.py index da47cc1..9066a05 100644 --- a/nsploit/payload/__init__.py +++ b/nsploit/payload/__init__.py @@ -1,6 +1,4 @@ from .fmtstring import * -from .gadhint import * from .payload import * from .payload_entry import * from .ret2dlresolve import * -from .rop import * diff --git a/nsploit/payload/gadhint.py b/nsploit/payload/gadhint.py deleted file mode 100644 index 1918a79..0000000 --- a/nsploit/payload/gadhint.py +++ /dev/null @@ -1,98 +0,0 @@ -import copy -from dataclasses import dataclass, field - -from nsploit.rev.gadget import Gadget -from nsploit.types.index_entry import IndexEntry - -@dataclass -class GadHint(IndexEntry): - """ - User-annotated gadget description object - - 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 - occurrence. - - movs (dict{str:str}): The register-to-register moves made by this gadget. - Keys are destination register names, values are source register names. The - order given is insignificant. - - imms (dict{str:int}): The immediate-to-register loads made by this gadget. - Keys are destination register names, values are immediate 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 - required register values. - - stack (list[int]): A list of words to append to the stack following this - gadget. The first element given is nearest to the top of the stack and the - rest follow in order. - - align (bool): If True, this gadget expects the stack to be aligned prior - to entry. - - syscall (bool): If True, this gadget contains a syscall instruction. - - spm (int): "Stack pointer move" - The amount the stack pointer is adjusted - by this gadget. The effect of executing a terminating "return" instruction - should not be accounted for. A value of zero is taken as "unspecified". - """ - - base: int = 0 - pops: list = field(default_factory=list) - movs: dict = field(default_factory=dict) - imms: dict = field(default_factory=dict) - writes: dict = field(default_factory=dict) - requirements: dict = field(default_factory=dict) - stack: list = field(default_factory=list) - align: bool = False - syscall: bool = False - spm: int = 0 - - @property - def offset(self): - """Return gadget offset as an integer.""" - return int(self.base) - - def with_requirements(self, reqs): - """Return new object with additional requirements.""" - for k, v in reqs.items(): - if self.requirements.get(k, v) != v: - raise ValueError( - f"GadHint: Conflicting gadget requirements: " - f"{self.requirements}, {reqs}") - - new = copy.deepcopy(self) - new.requirements |= reqs - return new - - def __repr__(self): - """Return human-readable GadHint.""" - def fmt(name, prop): - if len(prop) > 0: - return f", {name}={prop}" - return "" - - 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) - s += fmt("writes", self.writes) - s += fmt("requirements", self.requirements) - s += fmt("stack", self.stack) - if self.align: - s += ", align" - if self.syscall: - s += ", syscall" - if self.spm > 0: - s += f", spm={self.spm}" - return f"GadHint({s})" diff --git a/nsploit/payload/ret2dlresolve.py b/nsploit/payload/ret2dlresolve.py index 3dbd2ed..8446ead 100644 --- a/nsploit/payload/ret2dlresolve.py +++ b/nsploit/payload/ret2dlresolve.py @@ -73,11 +73,11 @@ Elf64_Rel.r_info = 0xAAAAAAAABBBBBBBB """ from nsploit.arch import arch, itob -from nsploit.payload.gadhint import GadHint from nsploit.payload.payload import Payload from nsploit.payload.payload_entry import padalign, padlen, pointer -from nsploit.payload.rop import ROP from nsploit.rev.r2 import run_cmd +from nsploit.tech.gadhint import GadHint +from nsploit.tech.rop import ROP _JMP_SLOT = 0x07 diff --git a/nsploit/payload/rop.py b/nsploit/payload/rop.py deleted file mode 100644 index 78f9950..0000000 --- a/nsploit/payload/rop.py +++ /dev/null @@ -1,364 +0,0 @@ -from graphlib import TopologicalSorter - -from nsploit.arch import arch, btoi, itob -from nsploit.payload.gadhint import GadHint -from nsploit.payload.payload import Payload -from nsploit.payload.payload_entry import padalign, padlen - -_POP_MAGIC = 0xdead -_SPM_MAGIC = b"\x69" -_ERROR_MAGIC = 0xbaadc0de - -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. - - 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. - """ - - def __init__(self, *objects, safe_syscalls=True, align_calls=True, - 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 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 search_gadget(self, *regexes): - """Return the first matching gadget, considering all objects.""" - for obj in self.objects: - try: - return obj.gadget(*regexes) - except: - pass - raise LookupError( - f"ROP: Need to define gadget symbol for {'; '.join(regexes)}") - - def gadget(self, gadget): - """ - 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. - """ - return self.gadget(GadHint(requirements=sets)) - - def call(self, func, *params): - """ - Return a ROP payload to call function. - - func (int): Entry address of function to call. - *params (int): Remaining positional args are passed to func. - """ - 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.gadget(gadget) - - def syscall(self, *params): - """ - Return a ROP payload to call kernel. - - *params (int): The first argument is the syscall number. Remaining - positional arguments are passed to the syscall. - """ - if len(params) > len(arch.kernargs): - raise TypeError("ROP: Too many arguments passed to syscall. " - f"Target architecture supports up to {len(arch.kernargs)-1}.") - - register_params = dict(zip(arch.kernargs, params)) - sc = self.__get_gadget("syscall", {}) - return self.gadget(sc.with_requirements(register_params)) - - def memcpy(self, dst, src): - """ - Return a ROP payload to write data into memory. - - dst (int): The destination memory address. - src (bytes): The content to write. - """ - data = Payload() - for idx in range(0, len(src), arch.wordsize): - word = btoi(src[idx:idx+arch.wordsize]) - data(self.gadget(self.__get_write(dst+idx, word))) - return data - - 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): - """ - Populate gadget dependency graph. - - This function recursively looks up gadgets to ensure all necessary - required gadgets can be found, and stores this information into the - given graph object. Established dependencies encode the order that the - chain builder should attempt to satisfy register requirements. - Dependency loops are detected by the TopologicalSorter. - - seen (set): Set of (register,value) tuples we have already discovered. - graph (TopologicalSorter): Dependency graph model object. - current (GadHint): Current gadget we are processing. - """ - for r, v in current.requirements.items(): - # We key on register name _and_ value because some gadgets may - # only be capable of storing specific values in a target register. - # 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) - - # 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 - # clobbering targets it requires, as the builder will satisfy - # them afterward. - for x in gadget.requirements: - graph.add(x, r) - - # Treat gadget's load immediates as pseudo-requirements for - # the sake of target ordering, following the same logic - # as above. - for x in gadget.imms: - graph.add(x, r) - - # Mark node as visited - seen.add((r, v)) - self.__discover_requirements(seen, graph, gadget) - - def __get_gadget(self, target, sets): - """ - Get context-specific gadget. - - target (str): Either "ret", "syscall", or the name of a register we - would like to modify. - - sets (dict{str:int}): The set of other register requirements we are - trying to fulfill in parallel. Values may affect the gadget we decide - to use. - """ - # First, consider user-provided hints before automatically locating - # gadgets. - 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. - addl_reqs = { src:sets[dst] for dst, src in hint.movs.items() if dst in sets } - hint = hint.with_requirements(addl_reqs) - - # Pops will be accounted for by the chain builder. - # Immediates will be handled by gadget ordering in chain builder. - # Writes are a non-issue here. - - if hint.syscall: - # Only consider syscalls if the target is syscall. - if target == "syscall": - return hint - elif target in hint.imms: - if hint.imms[target] == sets[target]: - return hint - elif target in hint.pops: - return hint - elif target in hint.movs: - return hint - - # Automatically locate simple gadgets - if target == "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.search_gadget(*insns), syscall=True) - - # target == register - insns = [ i.format(target) for i in arch.popgad ] - return GadHint(self.search_gadget(*insns), pops=[target]) - - def __get_clean(self, size): - """ - Get a stack cleaning gadget that moves sp by _at least_ size. - - size (int): Minimum stack pointer move. - """ - # spm values of zero (the default) can't be trusted, as in this case - # 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(): - if hint.spm >= size and hint.spm > 0: - return hint - - 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 ]) - - if len(sizes) > 0: - return GadHint(table[sizes[0]], spm=sizes[0]) - - raise LookupError( - f"ROP: Need to define a stack move gadget of at least {size}") - - def __get_write(self, dst, src): - """ - Get a memory write gadget, injected with requirements for user data. - - dst (int): The intended memory write location. - src (int): The intended value to write. - """ - # 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(): - if hint.writes: - d, s = list(hint.writes.items())[0] - return hint.with_requirements({d:dst, s:src}) - - # 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.search_gadgets(*arch.writegad) - - for gad in results: - d = gad.asm[0].group("dst") - s = gad.asm[0].group("src") - - try: - # Assert requirements are met. - gadget = GadHint(gad, writes={d: s}, requirements={d:dst, s:src}) - self.__discover_requirements(set(), TopologicalSorter(), gadget) - return gadget - except: - pass - - raise LookupError("ROP: Need to define gadgets for memory write / deps") - - def __build_chain(self, gadget, sets): - """ - Generate ROP chain data for a given gadget. - - This function recursively builds a ROP chain for the given gadget and - its requirements, returning the result as a Payload. - - 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, 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) - 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: - 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 - # immediates that match our requirements. Non-matching - # immediates will be handled by a later gadget. - imms = g.imms.keys() & reqs.keys() - imms = [ x for x in imms if g.imms[x] == reqs[x] ] - done = g.pops + list(g.movs) + imms - to_do_list = [ x for x in to_do_list if x not in done ] - - # Append chain data to execute this gadget, but respect offset == 0 - # as a way to disable this gadget (perform a NULL gadget). - if gadget.offset != 0: - # Stack alignment if required. - if gadget.align: - ret = self.__get_gadget("ret", {}) - chain.alignment = padalign(0, itob(ret)) - - # "Return address" entry into this gadget. - 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. - 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 gadget.stack: - size = len(gadget.stack) * arch.wordsize - - if self.clean_stack: - clean = self.__get_clean(size) - chain.cleanup = clean.offset - pad = padlen(clean.spm, _SPM_MAGIC) - else: - chain.cleanup = _ERROR_MAGIC - pad = None - - chain.outer = Payload() - chain.outer(*gadget.stack) - if pad: chain.outer.pad = pad - - return chain diff --git a/nsploit/tech/__init__.py b/nsploit/tech/__init__.py new file mode 100644 index 0000000..d7e4b6d --- /dev/null +++ b/nsploit/tech/__init__.py @@ -0,0 +1,2 @@ +from .gadhint import * +from .rop import * diff --git a/nsploit/tech/gadhint.py b/nsploit/tech/gadhint.py new file mode 100644 index 0000000..1918a79 --- /dev/null +++ b/nsploit/tech/gadhint.py @@ -0,0 +1,98 @@ +import copy +from dataclasses import dataclass, field + +from nsploit.rev.gadget import Gadget +from nsploit.types.index_entry import IndexEntry + +@dataclass +class GadHint(IndexEntry): + """ + User-annotated gadget description object + + 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 + occurrence. + + movs (dict{str:str}): The register-to-register moves made by this gadget. + Keys are destination register names, values are source register names. The + order given is insignificant. + + imms (dict{str:int}): The immediate-to-register loads made by this gadget. + Keys are destination register names, values are immediate 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 + required register values. + + stack (list[int]): A list of words to append to the stack following this + gadget. The first element given is nearest to the top of the stack and the + rest follow in order. + + align (bool): If True, this gadget expects the stack to be aligned prior + to entry. + + syscall (bool): If True, this gadget contains a syscall instruction. + + spm (int): "Stack pointer move" - The amount the stack pointer is adjusted + by this gadget. The effect of executing a terminating "return" instruction + should not be accounted for. A value of zero is taken as "unspecified". + """ + + base: int = 0 + pops: list = field(default_factory=list) + movs: dict = field(default_factory=dict) + imms: dict = field(default_factory=dict) + writes: dict = field(default_factory=dict) + requirements: dict = field(default_factory=dict) + stack: list = field(default_factory=list) + align: bool = False + syscall: bool = False + spm: int = 0 + + @property + def offset(self): + """Return gadget offset as an integer.""" + return int(self.base) + + def with_requirements(self, reqs): + """Return new object with additional requirements.""" + for k, v in reqs.items(): + if self.requirements.get(k, v) != v: + raise ValueError( + f"GadHint: Conflicting gadget requirements: " + f"{self.requirements}, {reqs}") + + new = copy.deepcopy(self) + new.requirements |= reqs + return new + + def __repr__(self): + """Return human-readable GadHint.""" + def fmt(name, prop): + if len(prop) > 0: + return f", {name}={prop}" + return "" + + 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) + s += fmt("writes", self.writes) + s += fmt("requirements", self.requirements) + s += fmt("stack", self.stack) + if self.align: + s += ", align" + if self.syscall: + s += ", syscall" + if self.spm > 0: + s += f", spm={self.spm}" + return f"GadHint({s})" diff --git a/nsploit/tech/rop.py b/nsploit/tech/rop.py new file mode 100644 index 0000000..c5b3ef8 --- /dev/null +++ b/nsploit/tech/rop.py @@ -0,0 +1,364 @@ +from graphlib import TopologicalSorter + +from nsploit.arch import arch, btoi, itob +from nsploit.payload.payload import Payload +from nsploit.payload.payload_entry import padalign, padlen +from nsploit.tech.gadhint import GadHint + +_POP_MAGIC = 0xdead +_SPM_MAGIC = b"\x69" +_ERROR_MAGIC = 0xbaadc0de + +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. + + 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. + """ + + def __init__(self, *objects, safe_syscalls=True, align_calls=True, + 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 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 search_gadget(self, *regexes): + """Return the first matching gadget, considering all objects.""" + for obj in self.objects: + try: + return obj.gadget(*regexes) + except: + pass + raise LookupError( + f"ROP: Need to define gadget symbol for {'; '.join(regexes)}") + + def gadget(self, gadget): + """ + 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. + """ + return self.gadget(GadHint(requirements=sets)) + + def call(self, func, *params): + """ + Return a ROP payload to call function. + + func (int): Entry address of function to call. + *params (int): Remaining positional args are passed to func. + """ + 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.gadget(gadget) + + def syscall(self, *params): + """ + Return a ROP payload to call kernel. + + *params (int): The first argument is the syscall number. Remaining + positional arguments are passed to the syscall. + """ + if len(params) > len(arch.kernargs): + raise TypeError("ROP: Too many arguments passed to syscall. " + f"Target architecture supports up to {len(arch.kernargs)-1}.") + + register_params = dict(zip(arch.kernargs, params)) + sc = self.__get_gadget("syscall", {}) + return self.gadget(sc.with_requirements(register_params)) + + def memcpy(self, dst, src): + """ + Return a ROP payload to write data into memory. + + dst (int): The destination memory address. + src (bytes): The content to write. + """ + data = Payload() + for idx in range(0, len(src), arch.wordsize): + word = btoi(src[idx:idx+arch.wordsize]) + data(self.gadget(self.__get_write(dst+idx, word))) + return data + + 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): + """ + Populate gadget dependency graph. + + This function recursively looks up gadgets to ensure all necessary + required gadgets can be found, and stores this information into the + given graph object. Established dependencies encode the order that the + chain builder should attempt to satisfy register requirements. + Dependency loops are detected by the TopologicalSorter. + + seen (set): Set of (register,value) tuples we have already discovered. + graph (TopologicalSorter): Dependency graph model object. + current (GadHint): Current gadget we are processing. + """ + for r, v in current.requirements.items(): + # We key on register name _and_ value because some gadgets may + # only be capable of storing specific values in a target register. + # 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) + + # 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 + # clobbering targets it requires, as the builder will satisfy + # them afterward. + for x in gadget.requirements: + graph.add(x, r) + + # Treat gadget's load immediates as pseudo-requirements for + # the sake of target ordering, following the same logic + # as above. + for x in gadget.imms: + graph.add(x, r) + + # Mark node as visited + seen.add((r, v)) + self.__discover_requirements(seen, graph, gadget) + + def __get_gadget(self, target, sets): + """ + Get context-specific gadget. + + target (str): Either "ret", "syscall", or the name of a register we + would like to modify. + + sets (dict{str:int}): The set of other register requirements we are + trying to fulfill in parallel. Values may affect the gadget we decide + to use. + """ + # First, consider user-provided hints before automatically locating + # gadgets. + 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. + addl_reqs = { src:sets[dst] for dst, src in hint.movs.items() if dst in sets } + hint = hint.with_requirements(addl_reqs) + + # Pops will be accounted for by the chain builder. + # Immediates will be handled by gadget ordering in chain builder. + # Writes are a non-issue here. + + if hint.syscall: + # Only consider syscalls if the target is syscall. + if target == "syscall": + return hint + elif target in hint.imms: + if hint.imms[target] == sets[target]: + return hint + elif target in hint.pops: + return hint + elif target in hint.movs: + return hint + + # Automatically locate simple gadgets + if target == "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.search_gadget(*insns), syscall=True) + + # target == register + insns = [ i.format(target) for i in arch.popgad ] + return GadHint(self.search_gadget(*insns), pops=[target]) + + def __get_clean(self, size): + """ + Get a stack cleaning gadget that moves sp by _at least_ size. + + size (int): Minimum stack pointer move. + """ + # spm values of zero (the default) can't be trusted, as in this case + # 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(): + if hint.spm >= size and hint.spm > 0: + return hint + + 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 ]) + + if len(sizes) > 0: + return GadHint(table[sizes[0]], spm=sizes[0]) + + raise LookupError( + f"ROP: Need to define a stack move gadget of at least {size}") + + def __get_write(self, dst, src): + """ + Get a memory write gadget, injected with requirements for user data. + + dst (int): The intended memory write location. + src (int): The intended value to write. + """ + # 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(): + if hint.writes: + d, s = list(hint.writes.items())[0] + return hint.with_requirements({d:dst, s:src}) + + # 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.search_gadgets(*arch.writegad) + + for gad in results: + d = gad.asm[0].group("dst") + s = gad.asm[0].group("src") + + try: + # Assert requirements are met. + gadget = GadHint(gad, writes={d: s}, requirements={d:dst, s:src}) + self.__discover_requirements(set(), TopologicalSorter(), gadget) + return gadget + except: + pass + + raise LookupError("ROP: Need to define gadgets for memory write / deps") + + def __build_chain(self, gadget, sets): + """ + Generate ROP chain data for a given gadget. + + This function recursively builds a ROP chain for the given gadget and + its requirements, returning the result as a Payload. + + 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, 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) + 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: + 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 + # immediates that match our requirements. Non-matching + # immediates will be handled by a later gadget. + imms = g.imms.keys() & reqs.keys() + imms = [ x for x in imms if g.imms[x] == reqs[x] ] + done = g.pops + list(g.movs) + imms + to_do_list = [ x for x in to_do_list if x not in done ] + + # Append chain data to execute this gadget, but respect offset == 0 + # as a way to disable this gadget (perform a NULL gadget). + if gadget.offset != 0: + # Stack alignment if required. + if gadget.align: + ret = self.__get_gadget("ret", {}) + chain.alignment = padalign(0, itob(ret)) + + # "Return address" entry into this gadget. + 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. + 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 gadget.stack: + size = len(gadget.stack) * arch.wordsize + + if self.clean_stack: + clean = self.__get_clean(size) + chain.cleanup = clean.offset + pad = padlen(clean.spm, _SPM_MAGIC) + else: + chain.cleanup = _ERROR_MAGIC + pad = None + + chain.outer = Payload() + chain.outer(*gadget.stack) + if pad: chain.outer.pad = pad + + return chain -- cgit v1.2.3 From 328ea8cc37477bdece6d79b575b29606156581ee Mon Sep 17 00:00:00 2001 From: Malfurious Date: Sun, 16 Mar 2025 08:08:42 -0400 Subject: fmtstring: Move to tech package Signed-off-by: Malfurious --- nsploit/payload/__init__.py | 1 - nsploit/payload/fmtstring.py | 178 ------------------------------------------- nsploit/tech/__init__.py | 1 + nsploit/tech/fmtstring.py | 178 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 179 insertions(+), 179 deletions(-) delete mode 100644 nsploit/payload/fmtstring.py create mode 100644 nsploit/tech/fmtstring.py diff --git a/nsploit/payload/__init__.py b/nsploit/payload/__init__.py index 9066a05..b77821e 100644 --- a/nsploit/payload/__init__.py +++ b/nsploit/payload/__init__.py @@ -1,4 +1,3 @@ -from .fmtstring import * from .payload import * from .payload_entry import * from .ret2dlresolve import * diff --git a/nsploit/payload/fmtstring.py b/nsploit/payload/fmtstring.py deleted file mode 100644 index 1ec4609..0000000 --- a/nsploit/payload/fmtstring.py +++ /dev/null @@ -1,178 +0,0 @@ -""" -Exploit C-style format string vulnerabilities - -These techniques leverage functions such as printf, fprintf, sprintf, etc. when -run on unchecked user input to perform arbitrary memory read or write. This is -made possible by the unintended use of user input as the function's format -string argument, instead of an ordinary data argument. Attackers may inject -their own conversion specifiers, which act as operating instructions to the -function. Interesting formatters include: - - %p Read argument value. These are the values of function argument - registers and values from the stack. The value is printed as - hexadecimal (with leading "0x") and interprets values as unsigned - long (aka, same size as arch.wordsize). - - %s Read memory as asciiz string. Prints the data pointed to by - argument value. - - %c Read argument as 8-bit character, printing the interpreted character - value. This formatter is useful in combination with a field width - specifier in order to print a controlled number of bytes to the - output, which is meaningful to the next formatter. - - %n Write memory as integer. Prints no output, but writes the number of - characters printed so far to the location pointed to by the argument - pointer. A length modifier will control the bit-width of the - integer written. - -See `man 3 printf` for more details. -""" - -from nsploit.arch import arch, btoi, itob -from nsploit.payload.payload import Payload -from nsploit.payload.payload_entry import padalign, padrel - -_FMTSTR_MAGIC = b"\xcd" - -def _make_fmtstr_payload(): - # A typical layout will look like this: - # b'%123c%10$hn%456c%11$hn\x00\x90\xde\xad\xbe\xef\xca\xfe\xba\xbe' - # ^ ^ ^ ^ ^ ^ - # fmt[0] fmt[1] nul | addrs[0] addrs[1] - # align - # - # Many examples found online will demo placing addresses at the front. Eg: - # b'\xde\xad\xbe\xef\xca\xfe\xba\xbe%123c%7$hn%456c%8$hn\x00' - # This has the benefit that %n positional values are simple to calculate - # (they just start at the payload position and increase by one). However, - # any NULL bytes in the addresses break the exploit, since printf will stop - # processing its string once a NULL is encountered. - # - # Moving addresses to the end mitigates this. Wordsize alignment is then - # necessary to give for valid argument positions. We also intentionally - # NULL terminate the format string portion of the payload to prevent printf - # from processing beyond formatters. - fp = Payload() - fp.fmt = Payload() - fp.null = b"\x00" - fp.align = padalign(arch.wordsize) - fp.addrs = Payload() - return fp - -def _fixup_positionals(fp, position, offset=None): - if offset is None: - offset = fp.addrs.base // arch.wordsize - - fixup = _make_fmtstr_payload() - fixup.addrs = fp.addrs - - for i, fmt in enumerate(fp.fmt): - pos = position + offset + i - fixup.fmt(fmt.decode().format(pos).encode()) - - # String formatting positional values may grow the format string so much - # as to cause the addrs offset to shift. Detect this and correct. - check = fixup.addrs.base // arch.wordsize - if offset != check: - return _fixup_positionals(fp, position, check) - - return fixup - -def fmtstr_dump(start=None, end=None): - """ - Return a format string payload which dumps annotated argument values. - - start (int): Starting argument position (default: 1) - end (int): Ending argument position (default: start + 20) - """ - if start is None: start = 1 - if end is None: end = start + 19 # inclusive, so 20 total arguments - - fp = Payload() - fp.magic = padrel(arch.wordsize, _FMTSTR_MAGIC) - fp.fmt = Payload() - fp.null = b"\x00" - - for pos in range(start, end+1): - if pos < len(arch.funcargs): - label = arch.funcargs[pos] - else: - offset = (pos - len(arch.funcargs)) * arch.wordsize - label = f"stack+{hex(offset)}" - - fp.fmt(f"({pos}$ {label}) %{pos}$p ".encode()) - - return fp - -def fmtstr_get(*positions, join=" "): - """ - Return a format string payload which prints specific argument values. - - positions (*int): Argument positions - join (str): Delimiter string - """ - fp = Payload() - fp.fmt = Payload() - fp.null = join - - for p in positions: - fp.fmt(f"{join}%{p}$p".encode()) - - return fp - -def fmtstr_read(position, address): - """ - Return a format string payload which reads data (as string, via %s). - - position (int): printf positional offset of payload on stack. - address (int): Address of data to read. - """ - fp = _make_fmtstr_payload() - fp.fmt(b"%{}$s") - fp.addrs(address) - return _fixup_positionals(fp, position) - -def fmtstr_write(position, _data, _value=None): - """ - Return a format string payload which writes data. - - One option for calling this function is to give a write destination in _data - as an integer, and the value to write in _value. - - Alternatively, _data may contain a dictionary, with write destinations as - keys and contents to write as values. _value is ignored in this case. - - In either case, the contents to write is generally expected to be bytes. - However, integers are converted automatically via itob(). - - position (int): printf positional offset of payload on stack. - _data (int|dict{int:bytes}): Write data (see above) - _value (int|bytes): Write value (see above) - """ - # Convert from 2-argument style to dictionary. - if type(_data) is int: - _data = { _data: _value } - - pairs = {} - - # Collect each 2-byte word to write. - for addr, value in _data.items(): - value = itob(value) if type(value) is int else bytes(value) - words = [ value[i:i+2] for i in range(0, len(value), 2) ] - words = { addr+(i*2): btoi(w) for i, w in enumerate(words) } - pairs.update(words) - - fp = _make_fmtstr_payload() - prev = 0 - - # Craft writes. - for addr, word in sorted(pairs.items(), key=lambda x: x[1]): - diff = word - prev - prev = word - - size = "" if diff == 0 else f"%{diff}c" - fp.fmt(f"{size}%{{}}$hn".encode()) - fp.addrs(addr) - - return _fixup_positionals(fp, position) diff --git a/nsploit/tech/__init__.py b/nsploit/tech/__init__.py index d7e4b6d..a3220d6 100644 --- a/nsploit/tech/__init__.py +++ b/nsploit/tech/__init__.py @@ -1,2 +1,3 @@ +from .fmtstring import * from .gadhint import * from .rop import * diff --git a/nsploit/tech/fmtstring.py b/nsploit/tech/fmtstring.py new file mode 100644 index 0000000..1ec4609 --- /dev/null +++ b/nsploit/tech/fmtstring.py @@ -0,0 +1,178 @@ +""" +Exploit C-style format string vulnerabilities + +These techniques leverage functions such as printf, fprintf, sprintf, etc. when +run on unchecked user input to perform arbitrary memory read or write. This is +made possible by the unintended use of user input as the function's format +string argument, instead of an ordinary data argument. Attackers may inject +their own conversion specifiers, which act as operating instructions to the +function. Interesting formatters include: + + %p Read argument value. These are the values of function argument + registers and values from the stack. The value is printed as + hexadecimal (with leading "0x") and interprets values as unsigned + long (aka, same size as arch.wordsize). + + %s Read memory as asciiz string. Prints the data pointed to by + argument value. + + %c Read argument as 8-bit character, printing the interpreted character + value. This formatter is useful in combination with a field width + specifier in order to print a controlled number of bytes to the + output, which is meaningful to the next formatter. + + %n Write memory as integer. Prints no output, but writes the number of + characters printed so far to the location pointed to by the argument + pointer. A length modifier will control the bit-width of the + integer written. + +See `man 3 printf` for more details. +""" + +from nsploit.arch import arch, btoi, itob +from nsploit.payload.payload import Payload +from nsploit.payload.payload_entry import padalign, padrel + +_FMTSTR_MAGIC = b"\xcd" + +def _make_fmtstr_payload(): + # A typical layout will look like this: + # b'%123c%10$hn%456c%11$hn\x00\x90\xde\xad\xbe\xef\xca\xfe\xba\xbe' + # ^ ^ ^ ^ ^ ^ + # fmt[0] fmt[1] nul | addrs[0] addrs[1] + # align + # + # Many examples found online will demo placing addresses at the front. Eg: + # b'\xde\xad\xbe\xef\xca\xfe\xba\xbe%123c%7$hn%456c%8$hn\x00' + # This has the benefit that %n positional values are simple to calculate + # (they just start at the payload position and increase by one). However, + # any NULL bytes in the addresses break the exploit, since printf will stop + # processing its string once a NULL is encountered. + # + # Moving addresses to the end mitigates this. Wordsize alignment is then + # necessary to give for valid argument positions. We also intentionally + # NULL terminate the format string portion of the payload to prevent printf + # from processing beyond formatters. + fp = Payload() + fp.fmt = Payload() + fp.null = b"\x00" + fp.align = padalign(arch.wordsize) + fp.addrs = Payload() + return fp + +def _fixup_positionals(fp, position, offset=None): + if offset is None: + offset = fp.addrs.base // arch.wordsize + + fixup = _make_fmtstr_payload() + fixup.addrs = fp.addrs + + for i, fmt in enumerate(fp.fmt): + pos = position + offset + i + fixup.fmt(fmt.decode().format(pos).encode()) + + # String formatting positional values may grow the format string so much + # as to cause the addrs offset to shift. Detect this and correct. + check = fixup.addrs.base // arch.wordsize + if offset != check: + return _fixup_positionals(fp, position, check) + + return fixup + +def fmtstr_dump(start=None, end=None): + """ + Return a format string payload which dumps annotated argument values. + + start (int): Starting argument position (default: 1) + end (int): Ending argument position (default: start + 20) + """ + if start is None: start = 1 + if end is None: end = start + 19 # inclusive, so 20 total arguments + + fp = Payload() + fp.magic = padrel(arch.wordsize, _FMTSTR_MAGIC) + fp.fmt = Payload() + fp.null = b"\x00" + + for pos in range(start, end+1): + if pos < len(arch.funcargs): + label = arch.funcargs[pos] + else: + offset = (pos - len(arch.funcargs)) * arch.wordsize + label = f"stack+{hex(offset)}" + + fp.fmt(f"({pos}$ {label}) %{pos}$p ".encode()) + + return fp + +def fmtstr_get(*positions, join=" "): + """ + Return a format string payload which prints specific argument values. + + positions (*int): Argument positions + join (str): Delimiter string + """ + fp = Payload() + fp.fmt = Payload() + fp.null = join + + for p in positions: + fp.fmt(f"{join}%{p}$p".encode()) + + return fp + +def fmtstr_read(position, address): + """ + Return a format string payload which reads data (as string, via %s). + + position (int): printf positional offset of payload on stack. + address (int): Address of data to read. + """ + fp = _make_fmtstr_payload() + fp.fmt(b"%{}$s") + fp.addrs(address) + return _fixup_positionals(fp, position) + +def fmtstr_write(position, _data, _value=None): + """ + Return a format string payload which writes data. + + One option for calling this function is to give a write destination in _data + as an integer, and the value to write in _value. + + Alternatively, _data may contain a dictionary, with write destinations as + keys and contents to write as values. _value is ignored in this case. + + In either case, the contents to write is generally expected to be bytes. + However, integers are converted automatically via itob(). + + position (int): printf positional offset of payload on stack. + _data (int|dict{int:bytes}): Write data (see above) + _value (int|bytes): Write value (see above) + """ + # Convert from 2-argument style to dictionary. + if type(_data) is int: + _data = { _data: _value } + + pairs = {} + + # Collect each 2-byte word to write. + for addr, value in _data.items(): + value = itob(value) if type(value) is int else bytes(value) + words = [ value[i:i+2] for i in range(0, len(value), 2) ] + words = { addr+(i*2): btoi(w) for i, w in enumerate(words) } + pairs.update(words) + + fp = _make_fmtstr_payload() + prev = 0 + + # Craft writes. + for addr, word in sorted(pairs.items(), key=lambda x: x[1]): + diff = word - prev + prev = word + + size = "" if diff == 0 else f"%{diff}c" + fp.fmt(f"{size}%{{}}$hn".encode()) + fp.addrs(addr) + + return _fixup_positionals(fp, position) -- cgit v1.2.3 From eca005749cf0c720cedc97fbb4ad8aa10dfad44b Mon Sep 17 00:00:00 2001 From: Malfurious Date: Sun, 16 Mar 2025 08:51:29 -0400 Subject: ret2dlresolve: Move to tech package Signed-off-by: Malfurious --- nsploit/payload/__init__.py | 1 - nsploit/payload/ret2dlresolve.py | 226 --------------------------------------- nsploit/tech/__init__.py | 1 + nsploit/tech/ret2dlresolve.py | 226 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 227 insertions(+), 227 deletions(-) delete mode 100644 nsploit/payload/ret2dlresolve.py create mode 100644 nsploit/tech/ret2dlresolve.py diff --git a/nsploit/payload/__init__.py b/nsploit/payload/__init__.py index b77821e..d4b9be7 100644 --- a/nsploit/payload/__init__.py +++ b/nsploit/payload/__init__.py @@ -1,3 +1,2 @@ from .payload import * from .payload_entry import * -from .ret2dlresolve import * diff --git a/nsploit/payload/ret2dlresolve.py b/nsploit/payload/ret2dlresolve.py deleted file mode 100644 index 8446ead..0000000 --- a/nsploit/payload/ret2dlresolve.py +++ /dev/null @@ -1,226 +0,0 @@ -""" -Perform "Return to dlresolve" dynamic linker attack - -The ret2dlresolve technique is useful to defeat library ASLR against targets -with partial relro (or less) and where no useable data leaks are available. -This is specifically a workaround for ASLR of libraries such as libc, and -addresses within the target executable are expected to be known (non-pic or -otherwise). - -When a dynamic library call is performed normally, applications jump to code -stubs in the .plt section to perform the actual relocation. This process relies -on a couple of meta-data structures in the ELF object: - -Elf*_Rel: Contains a pointer to the corresponding GOT entry, which is used to -cache the real subroutine address for later calls, as well as an info field -describing the relocation. This info field contains a type subfield and an -index into the ELF's symbol table for the symbol to be relocated. - -Elf*_Sym: Contains all the data relevant to the symbol. For the purposes of the -exploit, only the symbol name field is utilized (the others are set to zeroes). -The name field is an offset into the ELF's string table, and the actual symbol -name string can be found at this offset. - -All of the data tables mentioned above are located by their corresponding -section in the ELF. The relocation process however does not perform any bounds -checks to ensure the runtime data structures actually come from these sections. -By forging custom structures, and ensuring they can be written into memory at -precise locations, an attacker can trick the resolver to link any library -function they desire by setting up the equivalent PLT function call via ROP. - -Read on for more background details: -http://phrack.org/issues/58/4.html -https://gist.github.com/ricardo2197/8c7f6f5b8950ed6771c1cd3a116f7e62 - -Structure definitions from your standard elf.h header: - -typedef struct { - Elf32_Word st_name; /* 4b Symbol name (string tbl index) */ - Elf32_Addr st_value; /* 4b Symbol value */ - Elf32_Word st_size; /* 4b Symbol size */ - unsigned char st_info; /* 1b Symbol type and binding */ - unsigned char st_other; /* 1b Symbol visibility */ - Elf32_Section st_shndx; /* 2b Section index */ -} Elf32_Sym; - -typedef struct { - Elf64_Word st_name; /* 4b Symbol name (string tbl index) */ - unsigned char st_info; /* 1b Symbol type and binding */ - unsigned char st_other; /* 1b Symbol visibility */ - Elf64_Section st_shndx; /* 2b Section index */ - Elf64_Addr st_value; /* 8b Symbol value */ - Elf64_Xword st_size; /* 8b Symbol size */ -} Elf64_Sym; - -typedef struct { - Elf32_Addr r_offset; /* 4b Address */ - Elf32_Word r_info; /* 4b Relocation type and symbol index */ -} Elf32_Rel; - -typedef struct { - Elf64_Addr r_offset; /* 8b Address */ - Elf64_Xword r_info; /* 8b Relocation type and symbol index */ -} Elf64_Rel; - -Elf32_Rel.r_info = 0xAAAAAABB - | | - | type - symidx - -Elf64_Rel.r_info = 0xAAAAAAAABBBBBBBB - | | - symidx type -""" - -from nsploit.arch import arch, itob -from nsploit.payload.payload import Payload -from nsploit.payload.payload_entry import padalign, padlen, pointer -from nsploit.rev.r2 import run_cmd -from nsploit.tech.gadhint import GadHint -from nsploit.tech.rop import ROP - -_JMP_SLOT = 0x07 - -def _symsize(): - # Size of Elf*_Sym, used for padding and indexing - if arch.wordsize == 4: return 16 - elif arch.wordsize == 8: return 24 - raise ValueError("Ret2dlresolve: Architecture wordsize unsupported") - -def _relsize(): - # Size of Elf*_Rel, used only for indexing on 64bit (32bit uses offset) - if arch.wordsize == 4: return 1 - elif arch.wordsize == 8: return 24 - raise ValueError("Ret2dlresolve: Architecture wordsize unsupported") - -def _infoshift(): - # Partition subfields of Elf*_Rel.r_info - if arch.wordsize == 4: return 8 - elif arch.wordsize == 8: return 32 - raise ValueError("Ret2dlresolve: Architecture wordsize unsupported") - -class Ret2dlresolve(ROP): - # Use constructor from ROP class - - def reloc(self, symbol_name): - """ - Generate relocation structures for the function with given symbol name. - - The returned data structures are packed into a single Payload object. - This payload must be written into the target's memory before attempting - to use it with Ret2dlresolve.call(). Furthermore, the chosen write - location must be assigned to the payload base property, so that internal - pointers may take on the appropriate values. - - See Ret2dlresolve.determine_address() for advice on choosing a write - location. - - symbol_name (str): Name of library function to link - """ - binary = self.objects[0] - symtab = binary.sym.sect['.dynsym'] - strtab = binary.sym.sect['.dynstr'] - - try: - jmprel = binary.sym.sect['.rel.plt'] - except KeyError: - jmprel = binary.sym.sect['.rela.plt'] - - # Elf*_Rel.r_info - info = lambda x: ((int(x - symtab) // _symsize()) << _infoshift()) | _JMP_SLOT - - # The sym structure is the most picky about its location in memory. So - # it is listed first in the main dlres struct, which can be placed at - # the desired location. - sym = Payload() - sym.name = pointer("symbol_string", lambda x: x - strtab) - sym.pad = padlen(_symsize(), b"\x00") - sym.symbol_string = symbol_name - - dlres = Payload() - dlres.symalign = padalign(_symsize(), reference=symtab) - dlres.sym = sym - dlres.relalign = padalign(_relsize(), reference=jmprel) - dlres.offset = pointer() - dlres.info = pointer("sym", info) - return dlres - - def determine_address(self, start=None, end=None, n=0): - """ - Determine recommended address for relocation structures. - - There are a couple considerations to make when determining the memory - locations. First of all, the location must be writable. More - importantly, since most items are referred to by an array index, the - structures themselves must be properly aligned, reference to the array - origins. - - The payload returned from Ret2dlresolve.reloc() has some of these - alignments built in, but one crucial one is not. The index implied by - Elf*_Sym's offset from the symtab base is the same index used to lookup - symbol version information, reference to versym. The data at this index - must constitute a valid version half-word (16-bits). This function - attempts to ensure that the coincident version info for any returned - value is the data \x00\x00. Getting this wrong can cause the dlresolve - routine to crash. - - start (int): Minimum address to recommend (default: .bss section address) - end (int): Maximum address to recommend (default: end of memory page) - n (int): Return the Nth useable address within the defined range - """ - binary = self.objects[0] - symtab = binary.sym.sect['.dynsym'] - versym = binary.sym.sect['.gnu.version'] - bss = binary.sym.sect['.bss'] - - if start is None: start = bss - if end is None: end = (start & ~0xfff) + 0x1000 - - zero_words = run_cmd(binary.path, "/x 0000") - zero_words = [ int(x.split(" ")[0], 0) for x in zero_words ] - - # Size of version entry is always 2 bytes. - veroff = [ x - versym for x in zero_words ] - idx = [ x//2 for x in veroff if x%2 == 0 ] - symoff = [ x * _symsize() for x in idx ] - addr = [ x + symtab for x in symoff ] - addr = [ x for x in addr if start <= x < end ] - - if len(addr) > n: - return addr[n] - - raise AssertionError("Ret2dlresolve: No suitable memory location") - - # Overrides ROP.call() - def call(self, reloc, *params): - """ - Return a ROP payload to call function via dynamic linker. - - reloc's base address must be set appropriately. - - reloc (Payload): Relocation payload obtained from Ret2dlresolve.reloc() - *params (int): Remaining positional args are passed to function. - """ - binary = self.objects[0] - plt = binary.sym.sect['.plt'] - - try: - jmprel = binary.sym.sect['.rel.plt'] - except KeyError: - jmprel = binary.sym.sect['.rela.plt'] - - register_params = dict(zip(arch.funcargs, params)) - stack_params = params[len(register_params):] - index = int(reloc.offset - jmprel) // _relsize() - - reqs = GadHint(requirements=register_params) - call = GadHint(index, stack=stack_params) - ret = GadHint(self.search_gadget(arch.ret)) - - chain = Payload() - try: chain.requirements = self.gadget(reqs).requirements - except KeyError: pass - chain.alignment = padalign(0, itob(ret)) - chain.plt = plt - chain.call = self.gadget(call) - return chain diff --git a/nsploit/tech/__init__.py b/nsploit/tech/__init__.py index a3220d6..a517e7f 100644 --- a/nsploit/tech/__init__.py +++ b/nsploit/tech/__init__.py @@ -1,3 +1,4 @@ from .fmtstring import * from .gadhint import * +from .ret2dlresolve import * from .rop import * diff --git a/nsploit/tech/ret2dlresolve.py b/nsploit/tech/ret2dlresolve.py new file mode 100644 index 0000000..8446ead --- /dev/null +++ b/nsploit/tech/ret2dlresolve.py @@ -0,0 +1,226 @@ +""" +Perform "Return to dlresolve" dynamic linker attack + +The ret2dlresolve technique is useful to defeat library ASLR against targets +with partial relro (or less) and where no useable data leaks are available. +This is specifically a workaround for ASLR of libraries such as libc, and +addresses within the target executable are expected to be known (non-pic or +otherwise). + +When a dynamic library call is performed normally, applications jump to code +stubs in the .plt section to perform the actual relocation. This process relies +on a couple of meta-data structures in the ELF object: + +Elf*_Rel: Contains a pointer to the corresponding GOT entry, which is used to +cache the real subroutine address for later calls, as well as an info field +describing the relocation. This info field contains a type subfield and an +index into the ELF's symbol table for the symbol to be relocated. + +Elf*_Sym: Contains all the data relevant to the symbol. For the purposes of the +exploit, only the symbol name field is utilized (the others are set to zeroes). +The name field is an offset into the ELF's string table, and the actual symbol +name string can be found at this offset. + +All of the data tables mentioned above are located by their corresponding +section in the ELF. The relocation process however does not perform any bounds +checks to ensure the runtime data structures actually come from these sections. +By forging custom structures, and ensuring they can be written into memory at +precise locations, an attacker can trick the resolver to link any library +function they desire by setting up the equivalent PLT function call via ROP. + +Read on for more background details: +http://phrack.org/issues/58/4.html +https://gist.github.com/ricardo2197/8c7f6f5b8950ed6771c1cd3a116f7e62 + +Structure definitions from your standard elf.h header: + +typedef struct { + Elf32_Word st_name; /* 4b Symbol name (string tbl index) */ + Elf32_Addr st_value; /* 4b Symbol value */ + Elf32_Word st_size; /* 4b Symbol size */ + unsigned char st_info; /* 1b Symbol type and binding */ + unsigned char st_other; /* 1b Symbol visibility */ + Elf32_Section st_shndx; /* 2b Section index */ +} Elf32_Sym; + +typedef struct { + Elf64_Word st_name; /* 4b Symbol name (string tbl index) */ + unsigned char st_info; /* 1b Symbol type and binding */ + unsigned char st_other; /* 1b Symbol visibility */ + Elf64_Section st_shndx; /* 2b Section index */ + Elf64_Addr st_value; /* 8b Symbol value */ + Elf64_Xword st_size; /* 8b Symbol size */ +} Elf64_Sym; + +typedef struct { + Elf32_Addr r_offset; /* 4b Address */ + Elf32_Word r_info; /* 4b Relocation type and symbol index */ +} Elf32_Rel; + +typedef struct { + Elf64_Addr r_offset; /* 8b Address */ + Elf64_Xword r_info; /* 8b Relocation type and symbol index */ +} Elf64_Rel; + +Elf32_Rel.r_info = 0xAAAAAABB + | | + | type + symidx + +Elf64_Rel.r_info = 0xAAAAAAAABBBBBBBB + | | + symidx type +""" + +from nsploit.arch import arch, itob +from nsploit.payload.payload import Payload +from nsploit.payload.payload_entry import padalign, padlen, pointer +from nsploit.rev.r2 import run_cmd +from nsploit.tech.gadhint import GadHint +from nsploit.tech.rop import ROP + +_JMP_SLOT = 0x07 + +def _symsize(): + # Size of Elf*_Sym, used for padding and indexing + if arch.wordsize == 4: return 16 + elif arch.wordsize == 8: return 24 + raise ValueError("Ret2dlresolve: Architecture wordsize unsupported") + +def _relsize(): + # Size of Elf*_Rel, used only for indexing on 64bit (32bit uses offset) + if arch.wordsize == 4: return 1 + elif arch.wordsize == 8: return 24 + raise ValueError("Ret2dlresolve: Architecture wordsize unsupported") + +def _infoshift(): + # Partition subfields of Elf*_Rel.r_info + if arch.wordsize == 4: return 8 + elif arch.wordsize == 8: return 32 + raise ValueError("Ret2dlresolve: Architecture wordsize unsupported") + +class Ret2dlresolve(ROP): + # Use constructor from ROP class + + def reloc(self, symbol_name): + """ + Generate relocation structures for the function with given symbol name. + + The returned data structures are packed into a single Payload object. + This payload must be written into the target's memory before attempting + to use it with Ret2dlresolve.call(). Furthermore, the chosen write + location must be assigned to the payload base property, so that internal + pointers may take on the appropriate values. + + See Ret2dlresolve.determine_address() for advice on choosing a write + location. + + symbol_name (str): Name of library function to link + """ + binary = self.objects[0] + symtab = binary.sym.sect['.dynsym'] + strtab = binary.sym.sect['.dynstr'] + + try: + jmprel = binary.sym.sect['.rel.plt'] + except KeyError: + jmprel = binary.sym.sect['.rela.plt'] + + # Elf*_Rel.r_info + info = lambda x: ((int(x - symtab) // _symsize()) << _infoshift()) | _JMP_SLOT + + # The sym structure is the most picky about its location in memory. So + # it is listed first in the main dlres struct, which can be placed at + # the desired location. + sym = Payload() + sym.name = pointer("symbol_string", lambda x: x - strtab) + sym.pad = padlen(_symsize(), b"\x00") + sym.symbol_string = symbol_name + + dlres = Payload() + dlres.symalign = padalign(_symsize(), reference=symtab) + dlres.sym = sym + dlres.relalign = padalign(_relsize(), reference=jmprel) + dlres.offset = pointer() + dlres.info = pointer("sym", info) + return dlres + + def determine_address(self, start=None, end=None, n=0): + """ + Determine recommended address for relocation structures. + + There are a couple considerations to make when determining the memory + locations. First of all, the location must be writable. More + importantly, since most items are referred to by an array index, the + structures themselves must be properly aligned, reference to the array + origins. + + The payload returned from Ret2dlresolve.reloc() has some of these + alignments built in, but one crucial one is not. The index implied by + Elf*_Sym's offset from the symtab base is the same index used to lookup + symbol version information, reference to versym. The data at this index + must constitute a valid version half-word (16-bits). This function + attempts to ensure that the coincident version info for any returned + value is the data \x00\x00. Getting this wrong can cause the dlresolve + routine to crash. + + start (int): Minimum address to recommend (default: .bss section address) + end (int): Maximum address to recommend (default: end of memory page) + n (int): Return the Nth useable address within the defined range + """ + binary = self.objects[0] + symtab = binary.sym.sect['.dynsym'] + versym = binary.sym.sect['.gnu.version'] + bss = binary.sym.sect['.bss'] + + if start is None: start = bss + if end is None: end = (start & ~0xfff) + 0x1000 + + zero_words = run_cmd(binary.path, "/x 0000") + zero_words = [ int(x.split(" ")[0], 0) for x in zero_words ] + + # Size of version entry is always 2 bytes. + veroff = [ x - versym for x in zero_words ] + idx = [ x//2 for x in veroff if x%2 == 0 ] + symoff = [ x * _symsize() for x in idx ] + addr = [ x + symtab for x in symoff ] + addr = [ x for x in addr if start <= x < end ] + + if len(addr) > n: + return addr[n] + + raise AssertionError("Ret2dlresolve: No suitable memory location") + + # Overrides ROP.call() + def call(self, reloc, *params): + """ + Return a ROP payload to call function via dynamic linker. + + reloc's base address must be set appropriately. + + reloc (Payload): Relocation payload obtained from Ret2dlresolve.reloc() + *params (int): Remaining positional args are passed to function. + """ + binary = self.objects[0] + plt = binary.sym.sect['.plt'] + + try: + jmprel = binary.sym.sect['.rel.plt'] + except KeyError: + jmprel = binary.sym.sect['.rela.plt'] + + register_params = dict(zip(arch.funcargs, params)) + stack_params = params[len(register_params):] + index = int(reloc.offset - jmprel) // _relsize() + + reqs = GadHint(requirements=register_params) + call = GadHint(index, stack=stack_params) + ret = GadHint(self.search_gadget(arch.ret)) + + chain = Payload() + try: chain.requirements = self.gadget(reqs).requirements + except KeyError: pass + chain.alignment = padalign(0, itob(ret)) + chain.plt = plt + chain.call = self.gadget(call) + return chain -- cgit v1.2.3 From c7ed242cbfd060e61dc5b73a64e2485988e93c10 Mon Sep 17 00:00:00 2001 From: Malfurious Date: Fri, 7 Mar 2025 02:32:30 -0500 Subject: payload: Move to types package Move the payload modules into the types subpackage, since the Payload system largely functions as a domain-specific data type for nsploit. This removes the payload subpackage. Signed-off-by: Malfurious --- nsploit/payload/__init__.py | 2 - nsploit/payload/payload.py | 224 --------------------------------------- nsploit/payload/payload_entry.py | 113 -------------------- nsploit/tech/fmtstring.py | 4 +- nsploit/tech/ret2dlresolve.py | 4 +- nsploit/tech/rop.py | 4 +- nsploit/types/__init__.py | 2 + nsploit/types/payload.py | 224 +++++++++++++++++++++++++++++++++++++++ nsploit/types/payload_entry.py | 113 ++++++++++++++++++++ 9 files changed, 345 insertions(+), 345 deletions(-) delete mode 100644 nsploit/payload/__init__.py delete mode 100644 nsploit/payload/payload.py delete mode 100644 nsploit/payload/payload_entry.py create mode 100644 nsploit/types/payload.py create mode 100644 nsploit/types/payload_entry.py diff --git a/nsploit/payload/__init__.py b/nsploit/payload/__init__.py deleted file mode 100644 index d4b9be7..0000000 --- a/nsploit/payload/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from .payload import * -from .payload_entry import * diff --git a/nsploit/payload/payload.py b/nsploit/payload/payload.py deleted file mode 100644 index 295e3c8..0000000 --- a/nsploit/payload/payload.py +++ /dev/null @@ -1,224 +0,0 @@ -from nsploit.arch import itob -from nsploit.payload.payload_entry import PayloadEntry -from nsploit.types.indextbl import IndexTbl -from nsploit.types.index_entry import IndexEntry -from nsploit.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)}" - memo = {} - - 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, memo) - data = str(self.__bytesof(i, memo)) - 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.""" - memo = {} - x = [ self.__bytesof(i, memo) 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) - - return self - - def end(self): - """Return the offset or address of the end of the payload.""" - return self.base + len(self) - - # IndexTbl abstract methods - - def __copy__(self): - """Return copy of object with shared data entries.""" - return Payload(self.base, self.__entries__) - - def __iter__(self): - """Iterate over data entries.""" - return iter(self.__entries__) - - def __len__(self): - """Return the size of the payload content in bytes.""" - memo = {} - x = [ self.__lenof(i, memo) for i in range(len(self.__entries__)) ] - return sum(x) - - 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, memo): - """Return a tuple (addr of value, literal value) for index.""" - value = self.__entries__[index] - addr = self.__addrof(index, memo) - if isinstance(value, IndexEntry): - value @= addr - return value, value - return addr, value - - def __addrof(self, index, memo): - """Return address (base + offset) for index.""" - index = self.__entries__.key2idx(index) - try: - return memo[index] - except KeyError: - sizes = [ self.__lenof(i, memo) for i in range(index) ] - addr = self.base + sum(sizes) - memo[index] = addr - return addr - - def __lenof(self, index, memo): - """Return element length for index.""" - _, value = self.__valueof(index, memo) - if isinstance(value, PayloadEntry): - return value.payload_len(self) - return len(value) - - def __bytesof(self, index, memo): - """Return byte output for index.""" - _, value = self.__valueof(index, memo) - 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) - - try: - # Confirm value supports our required operations - len(value) - bytes(value) - except TypeError as ex: - raise TypeError(f"Payload: Bad type {type(value)} given") from ex - - return value diff --git a/nsploit/payload/payload_entry.py b/nsploit/payload/payload_entry.py deleted file mode 100644 index 31eb4f8..0000000 --- a/nsploit/payload/payload_entry.py +++ /dev/null @@ -1,113 +0,0 @@ -from nsploit.arch import arch, itob -from nsploit.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_len(self, payload): - """ - Called to compute size of this entry. - - Implement this method to calculate the length of this dynamic payload - entry. self.base is set to the current entry address or offset. - """ - raise NotImplementedError - - def payload_bytes(self, payload): - """ - Called to generate bytes for this entry. - - Implement this method to generate the binary output for this dynamic - payload entry. self.base is set to the current entry address or offset. - """ - raise NotImplementedError - -# Concrete payload entry definitions - -class pointer(PayloadEntry): - """Generate an integer which tracks the address of another payload field.""" - - def __init__(self, target=None, math=None): - self.target = target - self.math = math - - def payload_len(self, payload): - return arch.wordsize - - def payload_bytes(self, payload): - if self.target is None: - addr = self.base - else: - addr = payload[self.target] - if callable(self.math): - addr = self.math(addr) - return itob(addr) - -class padlen(PayloadEntry): - """Generate padding to reach a target payload length.""" - - def __init__(self, size, data=None): - self.size = size - self.data = data - - def payload_len(self, payload): - return self.size - (self.base - payload.base) - - def payload_bytes(self, payload): - size = self.payload_len(payload) - 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) - -class padabs(padlen): - """Generate padding to reach a target absolute address.""" - - def payload_len(self, payload): - return self.size - self.base - -class padrel(padlen): - """Generate a fixed length of padding (aka: length relative to self).""" - - def payload_len(self, payload): - return self.size - -class padalign(padlen): - """Generate padding to reach next aligned address.""" - - def __init__(self, size=None, data=None, reference=0): - self.size = size - self.data = data - self.reference = reference - - def payload_len(self, payload): - size = self.size or arch.alignment - return (self.reference - 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_len(self, payload): - return self.size or arch.wordsize diff --git a/nsploit/tech/fmtstring.py b/nsploit/tech/fmtstring.py index 1ec4609..6ac74c5 100644 --- a/nsploit/tech/fmtstring.py +++ b/nsploit/tech/fmtstring.py @@ -30,8 +30,8 @@ See `man 3 printf` for more details. """ from nsploit.arch import arch, btoi, itob -from nsploit.payload.payload import Payload -from nsploit.payload.payload_entry import padalign, padrel +from nsploit.types.payload import Payload +from nsploit.types.payload_entry import padalign, padrel _FMTSTR_MAGIC = b"\xcd" diff --git a/nsploit/tech/ret2dlresolve.py b/nsploit/tech/ret2dlresolve.py index 8446ead..4e9aff4 100644 --- a/nsploit/tech/ret2dlresolve.py +++ b/nsploit/tech/ret2dlresolve.py @@ -73,11 +73,11 @@ Elf64_Rel.r_info = 0xAAAAAAAABBBBBBBB """ from nsploit.arch import arch, itob -from nsploit.payload.payload import Payload -from nsploit.payload.payload_entry import padalign, padlen, pointer from nsploit.rev.r2 import run_cmd from nsploit.tech.gadhint import GadHint from nsploit.tech.rop import ROP +from nsploit.types.payload import Payload +from nsploit.types.payload_entry import padalign, padlen, pointer _JMP_SLOT = 0x07 diff --git a/nsploit/tech/rop.py b/nsploit/tech/rop.py index c5b3ef8..a2c348e 100644 --- a/nsploit/tech/rop.py +++ b/nsploit/tech/rop.py @@ -1,9 +1,9 @@ from graphlib import TopologicalSorter from nsploit.arch import arch, btoi, itob -from nsploit.payload.payload import Payload -from nsploit.payload.payload_entry import padalign, padlen from nsploit.tech.gadhint import GadHint +from nsploit.types.payload import Payload +from nsploit.types.payload_entry import padalign, padlen _POP_MAGIC = 0xdead _SPM_MAGIC = b"\x69" diff --git a/nsploit/types/__init__.py b/nsploit/types/__init__.py index a618162..9f2fb30 100644 --- a/nsploit/types/__init__.py +++ b/nsploit/types/__init__.py @@ -1,3 +1,5 @@ from .indextbl import * from .index_entry import * from .lict import * +from .payload import * +from .payload_entry import * diff --git a/nsploit/types/payload.py b/nsploit/types/payload.py new file mode 100644 index 0000000..2f522b4 --- /dev/null +++ b/nsploit/types/payload.py @@ -0,0 +1,224 @@ +from nsploit.arch import itob +from nsploit.types.indextbl import IndexTbl +from nsploit.types.index_entry import IndexEntry +from nsploit.types.lict import Lict +from nsploit.types.payload_entry import PayloadEntry + +_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)}" + memo = {} + + 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, memo) + data = str(self.__bytesof(i, memo)) + 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.""" + memo = {} + x = [ self.__bytesof(i, memo) 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) + + return self + + def end(self): + """Return the offset or address of the end of the payload.""" + return self.base + len(self) + + # IndexTbl abstract methods + + def __copy__(self): + """Return copy of object with shared data entries.""" + return Payload(self.base, self.__entries__) + + def __iter__(self): + """Iterate over data entries.""" + return iter(self.__entries__) + + def __len__(self): + """Return the size of the payload content in bytes.""" + memo = {} + x = [ self.__lenof(i, memo) for i in range(len(self.__entries__)) ] + return sum(x) + + 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, memo): + """Return a tuple (addr of value, literal value) for index.""" + value = self.__entries__[index] + addr = self.__addrof(index, memo) + if isinstance(value, IndexEntry): + value @= addr + return value, value + return addr, value + + def __addrof(self, index, memo): + """Return address (base + offset) for index.""" + index = self.__entries__.key2idx(index) + try: + return memo[index] + except KeyError: + sizes = [ self.__lenof(i, memo) for i in range(index) ] + addr = self.base + sum(sizes) + memo[index] = addr + return addr + + def __lenof(self, index, memo): + """Return element length for index.""" + _, value = self.__valueof(index, memo) + if isinstance(value, PayloadEntry): + return value.payload_len(self) + return len(value) + + def __bytesof(self, index, memo): + """Return byte output for index.""" + _, value = self.__valueof(index, memo) + 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) + + try: + # Confirm value supports our required operations + len(value) + bytes(value) + except TypeError as ex: + raise TypeError(f"Payload: Bad type {type(value)} given") from ex + + return value diff --git a/nsploit/types/payload_entry.py b/nsploit/types/payload_entry.py new file mode 100644 index 0000000..31eb4f8 --- /dev/null +++ b/nsploit/types/payload_entry.py @@ -0,0 +1,113 @@ +from nsploit.arch import arch, itob +from nsploit.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_len(self, payload): + """ + Called to compute size of this entry. + + Implement this method to calculate the length of this dynamic payload + entry. self.base is set to the current entry address or offset. + """ + raise NotImplementedError + + def payload_bytes(self, payload): + """ + Called to generate bytes for this entry. + + Implement this method to generate the binary output for this dynamic + payload entry. self.base is set to the current entry address or offset. + """ + raise NotImplementedError + +# Concrete payload entry definitions + +class pointer(PayloadEntry): + """Generate an integer which tracks the address of another payload field.""" + + def __init__(self, target=None, math=None): + self.target = target + self.math = math + + def payload_len(self, payload): + return arch.wordsize + + def payload_bytes(self, payload): + if self.target is None: + addr = self.base + else: + addr = payload[self.target] + if callable(self.math): + addr = self.math(addr) + return itob(addr) + +class padlen(PayloadEntry): + """Generate padding to reach a target payload length.""" + + def __init__(self, size, data=None): + self.size = size + self.data = data + + def payload_len(self, payload): + return self.size - (self.base - payload.base) + + def payload_bytes(self, payload): + size = self.payload_len(payload) + 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) + +class padabs(padlen): + """Generate padding to reach a target absolute address.""" + + def payload_len(self, payload): + return self.size - self.base + +class padrel(padlen): + """Generate a fixed length of padding (aka: length relative to self).""" + + def payload_len(self, payload): + return self.size + +class padalign(padlen): + """Generate padding to reach next aligned address.""" + + def __init__(self, size=None, data=None, reference=0): + self.size = size + self.data = data + self.reference = reference + + def payload_len(self, payload): + size = self.size or arch.alignment + return (self.reference - 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_len(self, payload): + return self.size or arch.wordsize -- cgit v1.2.3