diff options
Diffstat (limited to 'sploit/payload')
| -rw-r--r-- | sploit/payload/__init__.py | 3 | ||||
| -rw-r--r-- | sploit/payload/gadhint.py | 109 | ||||
| -rw-r--r-- | sploit/payload/payload.py | 94 | ||||
| -rw-r--r-- | sploit/payload/rop.py | 383 | 
4 files changed, 589 insertions, 0 deletions
diff --git a/sploit/payload/__init__.py b/sploit/payload/__init__.py new file mode 100644 index 0000000..78769b4 --- /dev/null +++ b/sploit/payload/__init__.py @@ -0,0 +1,3 @@ +from .gadhint import * +from .payload import * +from .rop import * diff --git a/sploit/payload/gadhint.py b/sploit/payload/gadhint.py new file mode 100644 index 0000000..9b077fe --- /dev/null +++ b/sploit/payload/gadhint.py @@ -0,0 +1,109 @@ +from dataclasses import dataclass, field +from sploit.rev.gadget import Gadget + +@dataclass +class GadHint: +    """ +    User-annotated gadget description object + +    gadget (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 moves (stores) made by this +    gadget.  Keys are destination register names (expected to hold memory +    locations), values are source register names (expected to hold direct +    values).  The order given is insignificant. + +    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". +    """ + +    gadget: 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.gadget) + +    def __index__(self): +        """Convert object to integer using offset value.""" +        return self.offset + +    def __add__(self, x): +        """Return new object with adjusted offset.""" +        return GadHint(self.gadget + x, self.pops, self.movs, self.imms, +                       self.writes, self.requirements, self.stack, self.align, +                       self.syscall, self.spm) + +    def __sub__(self, x): +        """Return new object with adjusted offset.""" +        return self + (-x) + +    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}") + +        return GadHint(self.gadget, self.pops, self.movs, self.imms, +                       self.writes, self.requirements | reqs, self.stack, +                       self.align, self.syscall, self.spm) + +    def __repr__(self): +        """Return human-readable GadHint.""" +        def fmt(name, prop): +            if len(prop) > 0: +                return f", {name}={prop}" +            return "" + +        s = hex(self.gadget) +        s = f"Gadget({s})" if type(self.gadget) is 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/sploit/payload/payload.py b/sploit/payload/payload.py new file mode 100644 index 0000000..cf105c6 --- /dev/null +++ b/sploit/payload/payload.py @@ -0,0 +1,94 @@ +from sploit.arch import arch, itob +from sploit.symtbl import Symtbl + +class Payload: +    MAGIC = b'\xef' + +    def __init__(self, **kwargs): +        self.payload = b'' +        self.sym = Symtbl(**kwargs) +        self.ctrs = {} + +    def __len__(self): +        return len(self.payload) + +    def __call__(self, badbytes=b''): +        found = [ hex(x) for x in set(self.payload).intersection(badbytes) ] +        if len(found) > 0: +            raise Exception(f'Payload: bad bytes in content: {found}') +        return self.payload + +    def _name(self, kind, sym): +        if sym is not None: return sym +        try: ctr = self.ctrs[kind] +        except: ctr = 0 +        self.ctrs[kind] = ctr + 1 +        return f'{kind}_{ctr}' + +    def _append(self, value, sym): +        (self.sym @ 0)[sym] = len(self) +        self.payload += value +        return self + +    def _prepend(self, value, sym): +        self.sym >>= len(value) +        (self.sym @ 0)[sym] = 0 +        self.payload = value + self.payload +        return self + +    def end(self): +        return self.sym.base + len(self) + +    def bin(self, *values, sym=None): +        return self._append(b''.join(values), sym=self._name('bin', sym)) + +    def str(self, *values, sym=None): +        values = [ v.encode() + b'\x00' for v in values ] +        return self.bin(*values, sym=self._name('str', sym)) + +    def int(self, *values, sym=None): +        values = [ itob(v) for v in values ] +        return self.bin(*values, sym=self._name('int', sym)) + +    def int8(self, *values, sym=None): +        values = [ itob(v, 1) for v in values ] +        return self.bin(*values, sym=self._name('int', sym)) + +    def int16(self, *values, sym=None): +        values = [ itob(v, 2) for v in values ] +        return self.bin(*values, sym=self._name('int', sym)) + +    def int32(self, *values, sym=None): +        values = [ itob(v, 4) for v in values ] +        return self.bin(*values, sym=self._name('int', sym)) + +    def int64(self, *values, sym=None): +        values = [ itob(v, 8) for v in values ] +        return self.bin(*values, sym=self._name('int', sym)) + +    def ret(self, *values, sym=None): +        return self.int(*values, sym=self._name('ret', sym)) + +    def sbp(self, *values, sym=None): +        if len(values) == 0: +            return self.rep(self.MAGIC, arch.wordsize, sym=self._name('sbp', sym)) +        return self.int(*values, sym=self._name('sbp', sym)) + +    def rep(self, value, size, sym=None): +        return self.bin(self._rep_helper(value, size), sym=self._name('rep', sym)) + +    def pad(self, size, value=None, sym=None): +        return self.bin(self._pad_helper(size, value), sym=self._name('pad', sym)) + +    def pad_front(self, size, value=None, sym=None): +        return self._prepend(self._pad_helper(size, value), sym=self._name('pad', sym)) + +    def _rep_helper(self, value, size, *, explain=''): +        if size < 0: +            raise Exception(f'Payload: {explain}rep: available space is negative') +        if (size := size / len(value)) != int(size): +            raise Exception(f'Payload: {explain}rep: element does not divide the space evenly') +        return value * int(size) + +    def _pad_helper(self, size, value): +        return self._rep_helper(value or arch.nopcode, size - len(self), explain='pad: ') diff --git a/sploit/payload/rop.py b/sploit/payload/rop.py new file mode 100644 index 0000000..54226b4 --- /dev/null +++ b/sploit/payload/rop.py @@ -0,0 +1,383 @@ +""" +ROP chain generation utilities + +This module contains tools for automating basic return-oriented-programming +workloads, such as loading register values and calling into arbitrary functions +or syscalls.  The tools are currently designed to work on x86 (32 or 64 bit) +and ARM (32 bit only). + +The main appeal of the ROP class is the ability to abstract away the manual +construction of ROP chain data, and instead make declarative statements like +"call this function with these arguments."  The ROP class will also utilize its +supplied binary objects to automatically find and use trivial gadgets. + +The user is able to provide annotations for more complicated gadgets, which help +instruct the class how to incorporate them into a ROP chain.  This is done with +the GadHint dataclass.  GadHint objects are provided to a ROP instance by +including them in the Symtbl of one of the binary objects it is constructed with. +If applicable, a user-supplied gadget will take precedence over automatic gadget +searching. + +See the GadHint class to learn more about the descriptive attributes that are +supported. +""" + +from graphlib import TopologicalSorter + +from sploit.arch import arch, btoi, itob +from sploit.payload.gadhint import GadHint +from sploit.payload.payload import Payload + +class ROP(Payload): +    """ +    ROP-enabled payload builder + +    POP_MAGIC (int): Magic value used for pop instructions where no specific +    value is required by the user. + +    SPM_MAGIC (bytes): Magic value to fill the stack with when the best +    available cleaning gadget is larger than is necessary. + +    objects (list[ELF]): The binary objects this ROP instance will consider +    for gadget searching. + +    safe_syscalls (bool): If True, require that automatically found syscall +    instructions are immediately followed by a return instruction. + +    align_calls (bool): If True, ensure that the stack return address into +    function calls is aligned according to the architecture alignment property. +    Knowledge of alignment is taken from the instance Symtbl's base value. + +    clean_stack (bool): If True, attempt to locate a cleaning gadget to "pop" +    stack data that is leftover from a function call.  Required if attempting +    to make multiple calls that involve stack-based arguments. +    """ + +    POP_MAGIC = 0xdead +    SPM_MAGIC = b'\x69' + +    def __init__(self, *objects, safe_syscalls=True, align_calls=True, +                 clean_stack=True, **symbols): +        """Initialize new ROP builder instance.""" +        super().__init__(**symbols) +        self.objects = objects +        self.safe_syscalls = safe_syscalls +        self.align_calls = align_calls +        self.clean_stack = clean_stack + +    def gadgets(self, *regexes, cont=False): +        """Return a list of matching gadgets, considering all objects.""" +        results = [] +        for obj in self.objects: +            results += obj.gadgets(*regexes, cont=cont) +        return results + +    def gadget(self, *regexes): +        """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 assign(self, *, sym=None, **sets): +        """ +        Insert a ROP chain to control given registers. + +        **sets (str:int): Keyword arguments specify register assignments to +        perform with this ROP chain.  Argument names correspond to register +        names. + +        sym (str): If given, sym is the symbol name used to refer to the +        inserted data. +        """ +        gadget = GadHint(0, requirements=sets) +        return self._start_chain(gadget, sym=self._name("assign", sym)) + +    def call(self, func, *params, sym=None): +        """ +        Insert a ROP chain to call function. + +        func (int): Entry address of function to call. +        *params (int): Remaining positional args are passed to func. + +        sym (str): If given, sym is the symbol name used to refer to the +        inserted data. +        """ +        register_params = dict(zip(arch.funcargs, params)) +        stack_params = params[len(register_params):] +        gadget = GadHint(func, requirements=register_params, stack=stack_params, +                         align=self.align_calls) +        return self._start_chain(gadget, sym=self._name("call", sym)) + +    def syscall(self, *params, sym=None): +        """ +        Insert a ROP chain to call kernel. + +        *params (int): The first argument is the syscall number.  Remaining +        positional arguments are passed to the syscall. + +        sym (str): If given, sym is the symbol name used to refer to the +        inserted data. +        """ +        if len(params) > len(arch.kernargs): +            raise TypeError("ROP: Too many arguments passed to syscall. " +                "Target architecture supports up to {len(arch.kernargs)-1}.") + +        register_params = dict(zip(arch.kernargs, params)) +        gadget = self._get_gadget("syscall", {}).with_requirements(register_params) +        return self._start_chain(gadget, sym=self._name("syscall", sym)) + +    def memcpy(self, dst, src, *, sym=None): +        """ +        Insert a ROP chain to write data into memory. + +        dst (int): The destination memory address. +        src (bytes): The content to write. + +        sym (str): If given, sym is the symbol name used to refer to the +        inserted data. +        """ +        gadgets = [] +        for idx in range(0, len(src), arch.wordsize): +            g = self._get_write(dst + idx, btoi(src[idx:idx+arch.wordsize])) +            gadgets.append(g) +        return self._start_chain(*gadgets, sym=self._name("memcpy", sym)) + +    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 +                # colbbering 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.gadget(arch.ret)) + +        if target == "syscall": +            insns = [arch.syscall, arch.ret] if self.safe_syscalls else [arch.syscall] +            return GadHint(self.gadget(*insns), syscall=True) + +        # target == register +        insns = [ i.format(target) for i in arch.popgad ] +        return GadHint(self.gadget(*insns), pops=[target], spm=arch.wordsize) + +    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.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.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 _start_chain(self, *gadgets, sym=None): +        """ +        Insert a generic ROP chain. + +        *gadgets (GadHint): Annotated gadgets to prepare a chain from. + +        sym (str): If given, sym is the symbol name used to refer to the +        inserted data. +        """ +        stack = Payload(base=self.end()) +        for g in gadgets: +            self._build_chain(stack, g, {}) +        return self.bin(stack(), sym=self._name("gadget", sym)) + +    def _build_chain(self, stack, gadget, sets): +        """ +        Generate chain data for a given ROP gadget. + +        This function recursively builds a ROP chain for the given gadget and +        its requirements, storing data in the 'stack' object. + +        stack (Payload): Stack data being constructed. +        gadget (GadHint): Current gadget we are processing. + +        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). +        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 ] + +        # Start chain by satisfying to-do-list requirements. +        while len(to_do_list) > 0: +            g = self._get_gadget(to_do_list[0], reqs) +            self._build_chain(stack, g, reqs) + +            # 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: +                align = -stack.end() % arch.alignment +                stack.rep(itob(self._get_gadget("ret", {})), align) + +            # "Return address" entry into this gadget. +            stack.ret(gadget.offset) + +            # The gadget's "inner stack data" will be values to be popped +            # and additional junk data to be deallocated by the gadget itself. +            sp_dest = len(stack) + gadget.spm +            stack.int(*[ sets.get(p, self.POP_MAGIC) for p in gadget.pops ]) +            if gadget.spm > 0: +                stack.pad(sp_dest, self.SPM_MAGIC) + +            # The gadget's "outer stack data" will be the additional values +            # explicitly specified by the gadget.  Append a separate gadget +            # to clean up these values. +            if len(gadget.stack) > 0: +                size = len(gadget.stack) * arch.wordsize + +                if self.clean_stack: +                    clean = self._get_clean(size) +                    stack.ret(clean) +                    sp_dest = len(stack) + clean.spm +                else: +                    ret = self._get_gadget("ret", {}) +                    stack.ret(ret) +                    sp_dest = len(stack) + size + +                stack.int(*gadget.stack) +                stack.pad(sp_dest, self.SPM_MAGIC)  | 
