diff options
Diffstat (limited to 'hypodermic')
| -rw-r--r-- | hypodermic/main.py | 75 | ||||
| -rw-r--r-- | hypodermic/memory.py | 111 | ||||
| -rw-r--r-- | hypodermic/ptrace.py | 21 |
3 files changed, 196 insertions, 11 deletions
diff --git a/hypodermic/main.py b/hypodermic/main.py index 05d467a..b0d709b 100644 --- a/hypodermic/main.py +++ b/hypodermic/main.py @@ -15,6 +15,79 @@ # You should have received a copy of the GNU General Public License along # with Hypodermic. If not, see <http://www.gnu.org/licenses/>. +"""Command-line interface to Hypodermic.""" + +import argparse +import sys +import textwrap +import pprint # + +from hypodermic.memory import maps +from hypodermic.ptrace import Process + + +class CustomHelp(argparse.HelpFormatter): + """Modifications to argparse's default HelpFormatter.""" + def _fill_text(self, text, width, indent): + filled = [] + for line in text.splitlines(keepends=True): + filled.append(indent + line) + return "".join(filled) + + def _split_lines(self, text, width): + return text.splitlines() + + def add_usage(self, usage, actions, groups, prefix=None): + prefix = prefix or "Usage: " + both = super(CustomHelp, self) + return both.add_usage(usage, actions, groups, prefix) + def main(): - print("Don't share needles, brah!") + parser = argparse.ArgumentParser( + add_help=False, + formatter_class=CustomHelp, + usage="%(prog)s [-a pid] [-c path] [options]", + description="Don't share needles, brah!" + ) + + doc = parser.add_argument_group("Documentation") + doc.add_argument( + "-h", + "--help", + action="help", + help="Display this help page and exit." + ) + doc.add_argument( + "-V", + "--version", + action="version", + version="What version?", + help="Display the currently installed version and exit." + ) + + proc = parser.add_argument_group("Process Manipulation") + meth = proc.add_mutually_exclusive_group() + proc.add_argument( + "-a", + "--attach", + metavar="PID", + help="The pid of a process to attach to." + ) + proc.add_argument( + "-c", + "--create", + metavar="BIN", + help="The path of a binary to execute and attach to." + ) + + args = parser.parse_args() + + if args.attach is None and args.create is None: + print("No action specified. Quitting!") + sys.exit(1) + + if args.create: + p = Process(path=args.create) + else: + p = Process(pid=args.attach) diff --git a/hypodermic/memory.py b/hypodermic/memory.py new file mode 100644 index 0000000..6ffa5d1 --- /dev/null +++ b/hypodermic/memory.py @@ -0,0 +1,111 @@ +# 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/>. + +"""Parsing of a program's memory mapping info from procfs.""" + +import collections +import re + + +Device = collections.namedtuple( + "Device", + ["major", "minor"] +) + +Perms = collections.namedtuple( + "Perms", + ["r", "w", "x", "s"] +) + +Region = collections.namedtuple( + "Region", + ["start", "end", "perms", "off", "dev", "inode", "path"] +) + + +def parse_device(line: str) -> Device: + """Converts a device line of the form "maj:min" into a Device + object. + + Args: + line(str): The line to parse. + + Returns: + The parsed Device object. + """ + major, minor = line.split(':') + return Device(major, minor) + + +def parse_perms(line: str) -> Perms: + """Converts a permissions line of the form "rwxp" into a Perms + object. + + Args: + line(str): The line to parse. + + Returns: + The parsed Perms object. + """ + return Perms(line[0] == 'r', line[1] == 'w', line[2] == 'x', line[3] == 's') + + +def parse_region(line: str) -> Region: + """Converts a line of text from the "maps" file into a Region + object. + + Args: + line(str): The line to parse. + + Returns: + The parsed Region object. + """ + ret = re.split(r"\s+", line.strip()) + if len(ret) == 6: + address, perms, off, dev, inode, path = ret + else: + address, perms, off, dev, inode = ret + path = "" + start, end = address.split('-') + return Region(int(start, 16), int(end, 16), parse_perms(perms), + int(off, 16), parse_device(dev), int(inode), path) + + +def maps(pid: int) -> list: + """Gets memory mapping information for a given pid. + + Args: + pid (int): The pid of the process to get memory mapping + information for. + + Raises: + TypeError: If the pid argument is not an int. + PermissionError: If the mapping information cannot be read. + + Returns: + A list of Region objects. + """ + if not isinstance(pid, int): + raise TypeError("pid argument must be an int") + + regions = [] + + with open("/proc/{}/maps".format(pid)) as maps: + for line in maps: + regions.append(parse_region(line)) + + return regions diff --git a/hypodermic/ptrace.py b/hypodermic/ptrace.py index d073930..7a14dea 100644 --- a/hypodermic/ptrace.py +++ b/hypodermic/ptrace.py @@ -15,28 +15,31 @@ # You should have received a copy of the GNU General Public License along # with Hypodermic. If not, see <http://www.gnu.org/licenses/>. -"""Wrapper for the ptrace system call, since python-ptrace sucks.""" +"""ctypes wrapper for ptrace.""" import ctypes -# TODO: Instantiate from binary path, as well. class Process(object): """Process attached via ptrace. Note: The process is implicitly detached from upon destruction of this - object. + object, if appropriate. Args: pid (:obj:`int`, optional): The pid of the process to attach to. - Defaults to 0, which means that it will not be used. + 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 be used if no pid is specified. + Defaults to "", which will as the target if a pid is not + specified, either. Raises: - TypeError: If the pid argument is not an int. - OSError: If the pid cannot be attached to. + 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, or if the process + could not be created for the given binary. """ def __init__(self, pid=0, path=""): @@ -56,14 +59,12 @@ class Process(object): 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): - # FIXME: How are we going to deal with the path? - self._so = ctypes.cdll.LoadLibrary("/tmp/libptracew.so") + self._so = ctypes.cdll.LoadLibrary("/tmp/libhypodermicw.so") self._new_proc = self._so.new_proc self._attach = self._so.attach self._detach = self._so.detach |