diff options
| author | jakob <jakob@memeware.net> | 2017-09-08 20:36:11 -0400 |
|---|---|---|
| committer | jakob <jakob@memeware.net> | 2017-09-08 20:36:11 -0400 |
| commit | 1a17f23fce09b8325e237a2a65a2227ecf1310b8 (patch) | |
| tree | 3ee8bec5ea0feef74f43d72a9d69254c8664bb90 | |
| parent | 085f81c9ad630bff88cb4b1482760ec6ff95bc6a (diff) | |
| parent | 26b602c9080572de54458768bf80ef879fb9e8f4 (diff) | |
Merge branch 'master' of https://github.com/TsarFox/hypodermic
| -rw-r--r-- | README.md | 38 | ||||
| -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 | ||||
| -rw-r--r-- | setup.py | 2 | ||||
| -rw-r--r-- | wrapper/ptrace.c | 65 |
8 files changed, 514 insertions, 154 deletions
@@ -14,11 +14,14 @@ redirected. There have been a few attempts at this in the past, such as This is oftentimes unsuccessful, being very dependent upon how glibc was compiled. -The goal of Hypodermic is to find a means of injecting a dynamic library into -any Linux executable, even ones that are statically-linked, and tranferring this -method over to [PINCE][2] when it is stable enough. +The point of Hypodermic is to find a means of injecting a dynamic library into +any Linux executable, even ones that are statically-linked, and transferring +this method over to [PINCE][2] when it is stable enough. The current goal is the +ability to inject an internal cheat into Counter-Strike: Global Offensive, such +as [AimTux][3]. This will signal that the method has reached a point of +viability. -Hypodermic is free software, licensed under the [GNU General Public License.][3] +Hypodermic is free software, licensed under the [GNU General Public License.][4] ## Current Attempts @@ -37,20 +40,21 @@ stack in an attempt to trick the RTLD. ## Important Resources -* [Understanding Linux ELF RTLD internals][4] -* [Runtime Process Infection][5] -* [ELF Program Header][6] -* [Dynamic Loader Operation][7] -* [About ELF Auxiliary Vectors][8] -* [Code Injection into Running Linux Application][9] +* [Understanding Linux ELF RTLD internals][5] +* [Runtime Process Infection][6] +* [ELF Program Header][7] +* [Dynamic Loader Operation][8] +* [About ELF Auxiliary Vectors][9] +* [Code Injection into Running Linux Application][10] [1]: https://github.com/gaffe23/linux-inject [2]: https://github.com/korcankaraokcu/PINCE -[3]: https://www.gnu.org/licenses/gpl.html -[4]: http://s.eresi-project.org/inc/articles/elf-rtld.txt -[5]: http://phrack.org/issues/59/8.html -[6]: http://www.sco.com/developers/gabi/latest/ch5.pheader.html -[7]: https://sourceware.org/glibc/wiki/DynamicLoader -[8]: http://articles.manugarg.com/aboutelfauxiliaryvectors -[9]: https://www.codeproject.com/Articles/33340/Code-Injection-into-Running-Linux-Application +[3]: https://github.com/AimTuxOfficial/AimTux/ +[4]: https://www.gnu.org/licenses/gpl.html +[5]: http://s.eresi-project.org/inc/articles/elf-rtld.txt +[6]: http://phrack.org/issues/59/8.html +[7]: http://www.sco.com/developers/gabi/latest/ch5.pheader.html +[8]: https://sourceware.org/glibc/wiki/DynamicLoader +[9]: http://articles.manugarg.com/aboutelfauxiliaryvectors +[10]: https://www.codeproject.com/Articles/33340/Code-Injection-into-Running-Linux-Application 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) @@ -30,7 +30,7 @@ setup( packages=["hypodermic"], include_package_data=True, ext_modules=[lib], - install_requires=[], + install_requires=["pyelftools", "keystone-engine"], extras_require={}, tests_require=[], entry_points={"console_scripts": ["hypodermic = hypodermic.main:main"]}, diff --git a/wrapper/ptrace.c b/wrapper/ptrace.c index 706b13e..706fd4e 100644 --- a/wrapper/ptrace.c +++ b/wrapper/ptrace.c @@ -19,9 +19,12 @@ #include <sys/ptrace.h> #include <sys/types.h> +#include <sys/user.h> +#include <sys/utsname.h> #include <sys/wait.h> #include <stddef.h> +#include <string.h> #include <unistd.h> @@ -78,9 +81,71 @@ int cont(int pid) { return -1; } + waitpid(pid, &s, WNOHANG); + return 0; +} + + +int step(int pid) { + int s; + + if ((ptrace(PTRACE_SINGLESTEP, pid, NULL, NULL)) < 0) { + return -1; + } + while (!WIFSTOPPED(s)) { waitpid(pid, &s, WNOHANG); } return 0; } + + +/* TODO: As of now, Hypodermis is strongly tied to the Intel x86 + family of processors. This should really be expanded. */ +int is_amd64(void) { + struct utsname ub; + + uname(&ub); + + return !strcmp(ub.machine, "x86_64"); +} + + +#ifdef __x86_64__ +unsigned long long getreg(int pid, int idx) { + struct user_regs_struct regs; + + ptrace(PTRACE_GETREGS, pid, NULL, ®s); + + return ((unsigned long long *) ®s)[idx]; +} + +void setreg(int pid, int idx, unsigned long long value) { + struct user_regs_struct regs; + + ptrace(PTRACE_GETREGS, pid, NULL, ®s); + + ((unsigned long long *) ®s)[idx] = value; + + ptrace(PTRACE_SETREGS, pid, NULL, ®s); +} +#else +unsigned long getreg(int pid, int idx) { + struct user_regs_struct regs; + + ptrace(PTRACE_GETREGS, pid, NULL, ®s); + + return ((unsigned long *) ®s)[idx]; +} + +void setreg(int pid, int idx, unsigned long value) { + struct user_regs_struct regs; + + ptrace(PTRACE_GETREGS, pid, NULL, ®s); + + ((unsigned long *) ®s)[idx] = value; + + ptrace(PTRACE_SETREGS, pid, NULL, ®s); +} +#endif |