summaryrefslogtreecommitdiff
path: root/hypodermic
diff options
context:
space:
mode:
Diffstat (limited to 'hypodermic')
-rw-r--r--hypodermic/main.py8
-rw-r--r--hypodermic/memory.py8
-rw-r--r--hypodermic/process.py377
-rw-r--r--hypodermic/ptrace.py129
-rw-r--r--hypodermic/shellcode.py41
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)