diff options
Diffstat (limited to 'hypodermic')
| -rw-r--r-- | hypodermic/main.py | 8 | ||||
| -rw-r--r-- | hypodermic/memory.py | 8 | ||||
| -rw-r--r-- | hypodermic/process.py | 377 | ||||
| -rw-r--r-- | hypodermic/ptrace.py | 129 | ||||
| -rw-r--r-- | hypodermic/shellcode.py | 41 |
5 files changed, 427 insertions, 136 deletions
diff --git a/hypodermic/main.py b/hypodermic/main.py index 635e3a7..cb17d3b 100644 --- a/hypodermic/main.py +++ b/hypodermic/main.py @@ -21,7 +21,7 @@ import argparse import sys import textwrap -from hypodermic.ptrace import Process +from hypodermic.process import Process class CustomHelp(argparse.HelpFormatter): @@ -106,8 +106,10 @@ def main(): sys.exit(1) if args.create: - alert("Creating process at path {}".format(args.create)) + alert("Creating process at path '{}'...".format(args.create)) p = Process(path=args.create) + p.continue_until_haulted() else: - alert("Attaching to process with pid {}".format(args.attach)) + alert("Attaching to process with pid {}...".format(args.attach)) p = Process(pid=args.attach) + p.continue_until_haulted() diff --git a/hypodermic/memory.py b/hypodermic/memory.py index 6ffa5d1..0d86855 100644 --- a/hypodermic/memory.py +++ b/hypodermic/memory.py @@ -42,7 +42,7 @@ def parse_device(line: str) -> Device: object. Args: - line(str): The line to parse. + line (str): The line to parse. Returns: The parsed Device object. @@ -56,7 +56,7 @@ def parse_perms(line: str) -> Perms: object. Args: - line(str): The line to parse. + line (str): The line to parse. Returns: The parsed Perms object. @@ -69,7 +69,7 @@ def parse_region(line: str) -> Region: object. Args: - line(str): The line to parse. + line (str): The line to parse. Returns: The parsed Region object. @@ -90,7 +90,7 @@ def maps(pid: int) -> list: Args: pid (int): The pid of the process to get memory mapping - information for. + information for. Raises: TypeError: If the pid argument is not an int. diff --git a/hypodermic/process.py b/hypodermic/process.py new file mode 100644 index 0000000..1e2c1d4 --- /dev/null +++ b/hypodermic/process.py @@ -0,0 +1,377 @@ +# Copyright (C) 2017 Jakob Kreuze, All Rights Reserved. +# +# This file is part of Hypodermic. +# +# Hypodermic is free software: you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by the +# Free Software Foundation, either version 3 of the License, or (at your +# option) any later version. +# +# Hypodermic is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General +# Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with Hypodermic. If not, see <http://www.gnu.org/licenses/>. + +"""ctypes wrapper for ptrace.""" + +import ctypes +import os.path +import re + +from hypodermic.memory import Region, maps +from hypodermic.shellcode import assemble + +_AMD64_INDICES = { + "r15": 0, + "r14": 1, + "r13": 2, + "r12": 3, + "rbp": 4, + "rbx": 5, + "r11": 6, + "r10": 7, + "r9": 8, + "r8": 9, + "rax": 10, + "rcx": 11, + "rdx": 12, + "rsi": 13, + "rdi": 14, + "orig_rax": 15, + "rip": 16, + "cs": 17, + "eflags": 18, + "rsp": 19, + "ss": 20, + "fs_base": 21, + "gs_base": 22, + "ds": 23, + "es": 24, + "fs": 25, + "gs": 26 +} + +_AMD64_REGS = [ + "rax", + "rbx", + "rcx", + "rdx", + "rsi", + "rdi", + "r8", + "r9", + "r10", + "r11", + "r12", + "r13", + "r14", + "r15", +] + +_I386_INDICES = { + "ebx": 0, + "ecx": 1, + "edx": 2, + "esi": 3, + "edi": 4, + "ebp": 5, + "eax": 6, + "xds": 7, + "xes": 8, + "xfs": 9, + "xgs": 10, + "orig_eax": 11, + "eip": 12, + "xcs": 13, + "eflags": 14, + "esp": 15, + "xss": 16 +} + +_I386_REGS = [ + "eax", + "ebx", + "ecx", + "edx", + "esi", + "edi", +] + + +class Process(object): + """Process attached via ptrace. + + Note: + The process is implicitly detached from upon destruction of this + object, if appropriate. + + Args: + pid (:obj:`int`, optional): The pid of the process to attach to. + Defaults to 0, which means that the argument will not be + used. + path (:obj:`str`, optional): The path of the binary to run. + Defaults to "", which will as the target if a pid is not + specified, either. + + Raises: + TypeError: If the pid argument is not an int, or if the path + argument is not a string. + OSError: If the pid cannot be attached to, if the process could + not be created for the given binary, or if any wrapper + libraries could not be loaded. + """ + + def __init__(self, pid=0, path=""): + if not isinstance(pid, int): + raise TypeError("pid argument must be an int") + elif not isinstance(path, str): + raise TypeError("path argument must be a string") + self._load_ffi_methods() + + if pid != 0: + self._is_parent = False + if self._attach(ctypes.c_int(pid)): + raise OSError("Could not attach to pid {}".format(pid)) + else: + self._is_parent = True + self.pid = self._new_proc(ctypes.c_char_p(path.encode())) + if self.pid < 0: + raise OSError("Could not create process {}".format(path)) + + def __del__(self): + if hasattr(self, "_is_parent") and not self._is_parent: + self.detach() + + def _load_ffi_methods(self): + # setuptools/cython hack. + script_path = os.path.abspath(os.path.dirname(__file__)) + + for filename in os.listdir(os.path.join(script_path, "..")): + if filename.startswith("libhypodermicw"): + lib_path = os.path.join(script_path, "..", filename) + break + else: + raise OSError("Could not find wrapper library.") + + self._so = ctypes.cdll.LoadLibrary(lib_path) + self._new_proc = self._so.new_proc + self._attach = self._so.attach + self._detach = self._so.detach + self._cont = self._so.cont + self._step = self._so.step + self._isamd64 = self._so.is_amd64 + self._setreg = self._so.setreg + self._getreg = self._so.getreg + self._getreg.restype = ctypes.c_ulonglong + + def detach(self): + """Explicitly detaches from the process. + + Raises: + OSError: If the process cannot be detached from. + """ + if not self._is_parent and self._detach(ctypes.c_int(self.pid)): + raise OSError("Could not detach from pid {}".format(self.pid)) + + def continue_until_haulted(self): + """Continues until the program is haulted. + + Raises: + OSError: If the process cannot be continued. + """ + if self._cont(ctypes.c_int(self.pid)): + raise OSError("Could not continue") + + def single_step(self): + """Execute a single instruction. + + Raises: + OSError: If the process cannot be put into single step mode. + """ + if self._step(ctypes.c_int(self.pid)): + raise OSError("Could not continue") + + def write_bytes(self, address: int, src: bytes) -> int: + """Writes data into process memory. + + Args: + address (int): The address at which to write the bytes. + src (:obj:`bytes`): The bytes to write. + + Raises: + ValueError: If the address does not exist in the process + address space. + + Returns: + The number of bytes written. + """ + for region in self.maps: + if address >= region.start and address + len(src) < region.end: + break + else: + raise ValueError("address was not in the process address space") + + with open("/proc/{}/mem".format(self.pid), "wb") as mem: + mem.seek(address) + return mem.write(src) + + def read_bytes(self, address: int, n: int) -> bytes: + """Reads data from process memory. + + Args: + address (int): The address at which to read from. + n (int): The number of bytes to read. + + Raises: + ValueError: If the address does not exist in the process + address space. + + Returns: + A `bytes` object containing the bytes read. + """ + for region in self.maps: + if address >= region.start and address + n < region.end: + break + else: + raise ValueError("address was not in the process address space") + + with open("/proc/{}/mem".format(self.pid), "rb") as mem: + mem.seek(address) + return mem.read(n) + + def get_register(self, reg: str) -> int: + """Returns the value of the given register. + + Note: + Registers names are tied to the host processor, not the + target processor. For example, a 32-bit ELF will still have + 64-bit registers on 64-bit Linux. It would be wise to query + the `arch` property of the Process object. + + Args: + reg (str): The register to inspect. (e.g. "rax") + + Returns: + An integer representing the value of the register. + """ + regs = _AMD64_INDICES if self._isamd64 else _I386_INDICES + + if reg not in regs: + raise ValueError("{} is not a valid register".format(reg)) + + return self._getreg(self.pid, regs.get(reg)) + + def set_register(self, reg: str, val: int): + """Sets the value of the given register. + + Note: + Registers names are tied to the host processor, not the + target processor. For example, a 32-bit ELF will still have + 64-bit registers on 64-bit Linux. It would be wise to query + the `arch` property of the Process object. + + Args: + reg (str): The register to modify. (e.g. "rax") + val (int): The new value for the register. + """ + regs = _AMD64_INDICES if self._isamd64 else _I386_INDICES + + if reg not in regs: + raise ValueError("{} is not a valid register".format(reg)) + + if self._isamd64: + return self._setreg(self.pid, regs.get(reg), ctypes.c_ulonglong(val)) + return self._setreg(self.pid, regs.get(reg), ctypes.c_ulong(val)) + + def _run_code_32(self, code: bytes, preserve: list): + reg_order = [reg for reg in _I386_REGS if reg not in preserve] + push = assemble("".join("pushl %{};".format(reg) for reg in reg_order), "i386") + pop = assemble("".join("popl %{};".format(reg) for reg in reversed(reg_order)), "i386") + bp = assemble("nop; nop; int3;", "i386") + payload = push + code + pop + bp + + old_eip = self.get_register("eip") + old_code = self.read_bytes(old_eip, len(payload)) + self.write_bytes(old_eip, payload) + while self.read_bytes(self.get_register("eip"), 1) != b"\xcc": + self.single_step() + self.write_bytes(old_eip, old_code) + self.set_register("eip", old_eip) + + def _run_code_64(self, code: bytes, preserve: list): + reg_order = [reg for reg in _AMD64_REGS if reg not in preserve] + push = assemble("".join("pushq %{};".format(reg) for reg in reg_order)) + pop = assemble("".join("popq %{};".format(reg) for reg in reversed(reg_order))) + bp = assemble("nop; nop; int3;") + payload = push + code + pop + bp + + old_rip = self.get_register("rip") + old_code = self.read_bytes(old_rip, len(payload)) + self.write_bytes(old_rip, payload) + while self.read_bytes(self.get_register("rip"), 1) != b"\xcc": + self.single_step() + self.write_bytes(old_rip, old_code) + self.set_register("rip", old_rip) + + def run_code(self, code: bytes, preserve=[]) -> tuple: + """Executes code on the inferior. + + Args: + code (:obj:`bytes`): The code to execute. + preserve (:obj:`list`, optional): Registers that should be + allowed to be clobbered. + + Returns: + A pair of lists, the first containing the values of + preserved registers before the code was executed, and the + second containing the values of preserved registers after + the code was executed. + """ + before = [self.get_register(reg) for reg in preserve] + if self.arch == "x64": + self._run_code_64(code, preserve) + else: + self._run_code_32(code, preserve) + after = [self.get_register(reg) for reg in preserve] + return before, after + + @property + def arch(self) -> str: + """Returns the architecture of the host processor. + + Note: + The architecture of the host platform is not necessarily + the architecture of the target executable. However, this + value will accurately represent which registers are + available. + + Returns: + A string representing the host processor. As of now, only + "x64" and "x86" are supported. + """ + return "x64" if self._isamd64 else "x86" + + @property + def maps(self) -> list: + """Obtain the process' memory map. + + Returns: + A list of Region objects. + """ + return maps(self.pid) + + @property + def rtld(self) -> Region: + """Obtain the base region of memory for the process' RTLD, if it + exists. + + Returns: + The Region object belonging to the RTLD, or None if no + RTLD was found. + """ + for region in self.maps: + if re.search(r"ld.+\.so", region.path) and region.off == 0: + return region diff --git a/hypodermic/ptrace.py b/hypodermic/ptrace.py deleted file mode 100644 index 714b567..0000000 --- a/hypodermic/ptrace.py +++ /dev/null @@ -1,129 +0,0 @@ -# Copyright (C) 2017 Jakob Kreuze, All Rights Reserved. -# -# This file is part of Hypodermic. -# -# Hypodermic is free software: you can redistribute it and/or modify it -# under the terms of the GNU General Public License as published by the -# Free Software Foundation, either version 3 of the License, or (at your -# option) any later version. -# -# Hypodermic is distributed in the hope that it will be useful, but -# WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General -# Public License for more details. -# -# You should have received a copy of the GNU General Public License along -# with Hypodermic. If not, see <http://www.gnu.org/licenses/>. - -"""ctypes wrapper for ptrace.""" - -import ctypes -import os.path -import re - -from hypodermic.memory import Region, maps - - -class Process(object): - """Process attached via ptrace. - - Note: - The process is implicitly detached from upon destruction of this - object, if appropriate. - - Args: - pid (:obj:`int`, optional): The pid of the process to attach to. - Defaults to 0, which means that the argument will not be - used. - path (:obj:`str`, optional): The path of the binary to run. - Defaults to "", which will as the target if a pid is not - specified, either. - - Raises: - TypeError: If the pid argument is not an int, or if the path - argument is not a string. - OSError: If the pid cannot be attached to, if the process could - not be created for the given binary, or if any wrapper - libraries could not be loaded. - """ - - def __init__(self, pid=0, path=""): - if not isinstance(pid, int): - raise TypeError("pid argument must be an int") - elif not isinstance(path, str): - raise TypeError("path argument must be a string") - self._load_ffi_methods() - - if pid != 0: - self._is_parent = False - if self._attach(ctypes.c_int(pid)): - raise OSError("Could not attach to pid {}".format(pid)) - else: - self._is_parent = True - self.pid = self._new_proc(ctypes.c_char_p(path.encode())) - if self.pid < 0: - raise OSError("Could not create process {}".format(path)) - - def __del__(self): - if hasattr(self, "_is_parent") and not self._is_parent: - self.detach() - - def _load_ffi_methods(self): - # setuptools/cython hack. - script_path = os.path.abspath(os.path.dirname(__file__)) - - for filename in os.listdir(os.path.join(script_path, "..")): - if filename.startswith("libhypodermicw"): - lib_path = os.path.join(script_path, "..", filename) - break - else: - raise OSError("Could not find wrapper library.") - - self._so = ctypes.cdll.LoadLibrary(lib_path) - self._new_proc = self._so.new_proc - self._attach = self._so.attach - self._detach = self._so.detach - self._cont = self._so.cont - - def detach(self): - """Explicitly detaches from the process. - - Raises: - OSError: If the process cannot be detached from. - """ - if not self._is_parent and self._detach(ctypes.c_int(self.pid)): - raise OSError("Could not detach from pid {}".format(self.pid)) - - def cont(self): - """Continues until the program is haulted. - - Raises: - OSError: If the process cannot be continued. - """ - if self._cont(ctypes.c_int(self.pid)): - raise OSError("Could not continue") - - @property - def maps(self) -> list: - """Obtain the process' memory map. - - Returns: - A list of Region objects. - """ - return maps(self.pid) - - # FIXME: This approach does not work outside of seeing if the - # process has an RTLD page. The reality is that the RTLD is - # broken up into independent several pages. - @property - def rtld(self) -> Region: - """Obtain the region of memory for the process' RTLD, if it - exists. - - Returns: - The Region object belonging to the RTLD, or None if no - RTLD was found. - """ - for region in self.maps: - if re.search(r"ld.+\.so", region.path): - return region diff --git a/hypodermic/shellcode.py b/hypodermic/shellcode.py new file mode 100644 index 0000000..0a35e0a --- /dev/null +++ b/hypodermic/shellcode.py @@ -0,0 +1,41 @@ +# Copyright (C) 2017 Jakob Kreuze, All Rights Reserved. +# +# This file is part of Hypodermic. +# +# Hypodermic is free software: you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by the +# Free Software Foundation, either version 3 of the License, or (at your +# option) any later version. +# +# Hypodermic is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General +# Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with Hypodermic. If not, see <http://www.gnu.org/licenses/>. + +"""Module for generating payloads.""" + +from keystone import * + + +def assemble(code: str, arch="amd64", syntax="att") -> bytes: + """Assembles the given assembly code. + + Args: + code (str): The code to assemble + arch (:obj:`str`, optional): The target architecture. + Defaults to "amd64" + syntax (:obj:`str`, optional): The assembly syntax to use. + Defaults to "att" + + Returns: + A `bytes` object containing the resultant machine code. + """ + wordlen = KS_MODE_64 if arch == "amd64" else KS_MODE_32 + ks = Ks(KS_ARCH_X86, wordlen) + if syntax == "att": + ks.syntax = keystone.KS_OPT_SYNTAX_ATT + encoded, _ = ks.asm(code) + return bytes(encoded) |