From 8b232076240dc7e6f8abdfca3d1634ae065c691b Mon Sep 17 00:00:00 2001 From: Jakob Date: Fri, 1 Sep 2017 16:40:01 -0400 Subject: Process.rtld now returns the base region of the RTLD. --- README.md | 38 ++++++++------- hypodermic/main.py | 6 +-- hypodermic/process.py | 126 ++++++++++++++++++++++++++++++++++++++++++++++++ hypodermic/ptrace.py | 129 -------------------------------------------------- 4 files changed, 150 insertions(+), 149 deletions(-) create mode 100644 hypodermic/process.py delete mode 100644 hypodermic/ptrace.py diff --git a/README.md b/README.md index a14d08b..94582fb 100644 --- a/README.md +++ b/README.md @@ -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..74c4486 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,8 @@ 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) else: - alert("Attaching to process with pid {}".format(args.attach)) + alert("Attaching to process with pid {}...".format(args.attach)) p = Process(pid=args.attach) diff --git a/hypodermic/process.py b/hypodermic/process.py new file mode 100644 index 0000000..6fc1519 --- /dev/null +++ b/hypodermic/process.py @@ -0,0 +1,126 @@ +# 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 . + +"""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) + + @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 . - -"""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 -- cgit v1.3 From 42f581e3e19430c1458be1a86d6c1b74eb148f0e Mon Sep 17 00:00:00 2001 From: Jakob Date: Sat, 2 Sep 2017 18:28:20 -0400 Subject: Began to implement runtime manipulation utilities. --- hypodermic/memory.py | 8 ++-- hypodermic/process.py | 129 +++++++++++++++++++++++++++++++++++++++++++++++++- setup.py | 2 +- wrapper/ptrace.c | 72 ++++++++++++++++++++++++++++ 4 files changed, 205 insertions(+), 6 deletions(-) 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 index 6fc1519..9a321e0 100644 --- a/hypodermic/process.py +++ b/hypodermic/process.py @@ -21,8 +21,60 @@ import ctypes import os.path import re +from elftools.elf.elffile import ELFFile + from hypodermic.memory import Region, maps +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 +} + +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 +} + class Process(object): """Process attached via ptrace. @@ -84,6 +136,10 @@ class Process(object): self._attach = self._so.attach self._detach = self._so.detach self._cont = self._so.cont + self._getreg32 = self._so.getreg32 + self._getreg32.restype = ctypes.c_ulong + self._getreg64 = self._so.getreg64 + self._getreg64.restype = ctypes.c_ulonglong def detach(self): """Explicitly detaches from the process. @@ -94,7 +150,7 @@ class Process(object): 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): + def continue_until_haulted(self): """Continues until the program is haulted. Raises: @@ -103,6 +159,77 @@ class Process(object): if self._cont(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. + + Args: + reg (str): The register to inspect. (e.g. "rax") + + Returns: + An integer representing the value of the register. + """ + regs = AMD64_INDICES if self.arch == "x64" else I386_INDICES + + if reg not in regs: + raise ValueError("{} is not a valid register".format(reg)) + + if self.arch == "x64": + return self._getreg64(self.pid, regs.get(reg)) + return self._getreg32(self.pid, regs.get(reg)) + + @property + def arch(self) -> str: + with open("/proc/{}/exe".format(self.pid), "rb") as elf: + return ELFFile(elf).get_machine_arch() + @property def maps(self) -> list: """Obtain the process' memory map. diff --git a/setup.py b/setup.py index 58ca457..4788a81 100644 --- a/setup.py +++ b/setup.py @@ -30,7 +30,7 @@ setup( packages=["hypodermic"], include_package_data=True, ext_modules=[lib], - install_requires=[], + install_requires=["pyelftools"], extras_require={}, tests_require=[], entry_points={"console_scripts": ["hypodermic = hypodermic.main:main"]}, diff --git a/wrapper/ptrace.c b/wrapper/ptrace.c index 706b13e..7e5dfa8 100644 --- a/wrapper/ptrace.c +++ b/wrapper/ptrace.c @@ -84,3 +84,75 @@ int cont(int pid) { return 0; } + + +/* user_regs_struct is copied from sys/user.h so that we can debug a + 32-bit executable on a 64-bit platform. */ +struct amd64_user_regs_struct { + __extension__ unsigned long long r15; + __extension__ unsigned long long r14; + __extension__ unsigned long long r13; + __extension__ unsigned long long r12; + __extension__ unsigned long long rbp; + __extension__ unsigned long long rbx; + __extension__ unsigned long long r11; + __extension__ unsigned long long r10; + __extension__ unsigned long long r9; + __extension__ unsigned long long r8; + __extension__ unsigned long long rax; + __extension__ unsigned long long rcx; + __extension__ unsigned long long rdx; + __extension__ unsigned long long rsi; + __extension__ unsigned long long rdi; + __extension__ unsigned long long orig_rax; + __extension__ unsigned long long rip; + __extension__ unsigned long long cs; + __extension__ unsigned long long eflags; + __extension__ unsigned long long rsp; + __extension__ unsigned long long ss; + __extension__ unsigned long long fs_base; + __extension__ unsigned long long gs_base; + __extension__ unsigned long long ds; + __extension__ unsigned long long es; + __extension__ unsigned long long fs; + __extension__ unsigned long long gs; +}; + + +struct i386_user_regs_struct { + unsigned long ebx; + unsigned long ecx; + unsigned long edx; + unsigned long esi; + unsigned long edi; + unsigned long ebp; + unsigned long eax; + unsigned long xds; + unsigned long xes; + unsigned long xfs; + unsigned long xgs; + unsigned long orig_eax; + unsigned long eip; + unsigned long xcs; + unsigned long eflags; + unsigned long esp; + unsigned long xss; +}; + + +unsigned long long getreg64(int pid, int idx) { + struct amd64_user_regs_struct regs; + + ptrace(PTRACE_GETREGS, pid, NULL, ®s); + + return ((unsigned long long *) ®s)[idx]; +} + + +unsigned long getreg32(int pid, int idx) { + struct i386_user_regs_struct regs; + + ptrace(PTRACE_GETREGS, pid, NULL, ®s); + + return ((unsigned long *) ®s)[idx]; +} -- cgit v1.3 From 6f0a28b09a670371b22658a295403ab2573ed993 Mon Sep 17 00:00:00 2001 From: Jakob Date: Sun, 3 Sep 2017 14:35:48 -0400 Subject: Fixed up how the user regs struct is accessed. --- hypodermic/process.py | 34 ++++++++++++++------- wrapper/ptrace.c | 83 ++++++++++++++------------------------------------- 2 files changed, 46 insertions(+), 71 deletions(-) diff --git a/hypodermic/process.py b/hypodermic/process.py index 9a321e0..8307e08 100644 --- a/hypodermic/process.py +++ b/hypodermic/process.py @@ -136,10 +136,9 @@ class Process(object): self._attach = self._so.attach self._detach = self._so.detach self._cont = self._so.cont - self._getreg32 = self._so.getreg32 - self._getreg32.restype = ctypes.c_ulong - self._getreg64 = self._so.getreg64 - self._getreg64.restype = ctypes.c_ulonglong + self._isamd64 = self._so.is_amd64 + self._getreg = self._so.getreg + self._getreg.restype = ctypes.c_ulonglong def detach(self): """Explicitly detaches from the process. @@ -210,25 +209,40 @@ class Process(object): def get_register(self, reg: str) -> int: """Returns the value of the given register. + Note: + Registers 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. + Args: reg (str): The register to inspect. (e.g. "rax") Returns: An integer representing the value of the register. + """ - regs = AMD64_INDICES if self.arch == "x64" else I386_INDICES + 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.arch == "x64": - return self._getreg64(self.pid, regs.get(reg)) - return self._getreg32(self.pid, regs.get(reg)) + return self._getreg(self.pid, regs.get(reg)) @property def arch(self) -> str: - with open("/proc/{}/exe".format(self.pid), "rb") as elf: - return ELFFile(elf).get_machine_arch() + """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 how registers should be + addressed. + + 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: diff --git a/wrapper/ptrace.c b/wrapper/ptrace.c index 7e5dfa8..8517519 100644 --- a/wrapper/ptrace.c +++ b/wrapper/ptrace.c @@ -19,9 +19,12 @@ #include #include +#include +#include #include #include +#include #include @@ -86,73 +89,31 @@ int cont(int pid) { } -/* user_regs_struct is copied from sys/user.h so that we can debug a - 32-bit executable on a 64-bit platform. */ -struct amd64_user_regs_struct { - __extension__ unsigned long long r15; - __extension__ unsigned long long r14; - __extension__ unsigned long long r13; - __extension__ unsigned long long r12; - __extension__ unsigned long long rbp; - __extension__ unsigned long long rbx; - __extension__ unsigned long long r11; - __extension__ unsigned long long r10; - __extension__ unsigned long long r9; - __extension__ unsigned long long r8; - __extension__ unsigned long long rax; - __extension__ unsigned long long rcx; - __extension__ unsigned long long rdx; - __extension__ unsigned long long rsi; - __extension__ unsigned long long rdi; - __extension__ unsigned long long orig_rax; - __extension__ unsigned long long rip; - __extension__ unsigned long long cs; - __extension__ unsigned long long eflags; - __extension__ unsigned long long rsp; - __extension__ unsigned long long ss; - __extension__ unsigned long long fs_base; - __extension__ unsigned long long gs_base; - __extension__ unsigned long long ds; - __extension__ unsigned long long es; - __extension__ unsigned long long fs; - __extension__ unsigned long long gs; -}; - - -struct i386_user_regs_struct { - unsigned long ebx; - unsigned long ecx; - unsigned long edx; - unsigned long esi; - unsigned long edi; - unsigned long ebp; - unsigned long eax; - unsigned long xds; - unsigned long xes; - unsigned long xfs; - unsigned long xgs; - unsigned long orig_eax; - unsigned long eip; - unsigned long xcs; - unsigned long eflags; - unsigned long esp; - unsigned long xss; -}; - - -unsigned long long getreg64(int pid, int idx) { - struct amd64_user_regs_struct regs; +/* 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; - ptrace(PTRACE_GETREGS, pid, NULL, ®s); + uname(&ub); - return ((unsigned long long *) ®s)[idx]; + return !strcmp(ub.machine, "x86_64"); } -unsigned long getreg32(int pid, int idx) { - struct i386_user_regs_struct regs; +#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]; +} +#else +unsigned long getreg(int pid, int idx) { + struct user_regs_struct regs; ptrace(PTRACE_GETREGS, pid, NULL, ®s); - return ((unsigned long *) ®s)[idx]; + return ((unsigned long *) ®s)[idx]; } +#endif -- cgit v1.3 From 8927b31b8ef6334a5efd28ea779db5a58ac5d445 Mon Sep 17 00:00:00 2001 From: Jakob Date: Sun, 3 Sep 2017 16:19:33 -0400 Subject: Implemented basic machine code injection. --- hypodermic/main.py | 20 +++++++++++++++++++ hypodermic/process.py | 53 ++++++++++++++++++++++++++++++++++++++++----------- wrapper/ptrace.c | 32 +++++++++++++++++++++++++++++++ 3 files changed, 94 insertions(+), 11 deletions(-) diff --git a/hypodermic/main.py b/hypodermic/main.py index 74c4486..7ce9be6 100644 --- a/hypodermic/main.py +++ b/hypodermic/main.py @@ -108,6 +108,26 @@ def main(): if args.create: alert("Creating process at path '{}'...".format(args.create)) p = Process(path=args.create) + + shellcode = b"\x48\xc7\xc0\x01\x00\x00\x00\x48\xc7\xc7\x01\x00" + \ + b"\x00\x00\x48\xc7\xc2\x29\x00\x00\x00\x48\x8d\x35" + \ + b"\x00\x00\x00\x00\x48\x81\xc6\x0d\x00\x00\x00\x0f" + \ + b"\x05\x90\x90\x90\xcc\x61\x6d\x64\x36\x34\x20\x4c" + \ + b"\x69\x6e\x75\x78\x20\x73\x79\x73\x5f\x77\x72\x69" + \ + b"\x74\x65\x20\x73\x68\x65\x6c\x6c\x63\x6f\x64\x65" + \ + b"\x20\x62\x79\x20\x4a\x61\x6b\x6f\x62\x0a" + + old_rip = p.get_register("rip") + alert("%rip at {}".format(hex(old_rip))) + old_code = p.read_bytes(old_rip, len(shellcode)) + p.write_bytes(old_rip, shellcode) + while p.read_bytes(p.get_register("rip"), 1) != b'\xcc': + p.single_step() + alert("Hit breakpoint!") + p.write_bytes(old_rip, old_code) + p.set_register("rip", old_rip) + alert("%rip reset to {}".format(hex(p.get_register("rip")))) + p.continue_until_haulted() else: alert("Attaching to process with pid {}...".format(args.attach)) p = Process(pid=args.attach) diff --git a/hypodermic/process.py b/hypodermic/process.py index 8307e08..c8a2823 100644 --- a/hypodermic/process.py +++ b/hypodermic/process.py @@ -21,11 +21,9 @@ import ctypes import os.path import re -from elftools.elf.elffile import ELFFile - from hypodermic.memory import Region, maps -AMD64_INDICES = { +_AMD64_INDICES = { "r15": 0, "r14": 1, "r13": 2, @@ -55,7 +53,7 @@ AMD64_INDICES = { "gs": 26 } -I386_INDICES = { +_I386_INDICES = { "ebx": 0, "ecx": 1, "edx": 2, @@ -136,7 +134,9 @@ class Process(object): 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 @@ -158,6 +158,15 @@ class Process(object): 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. @@ -210,24 +219,46 @@ class Process(object): """Returns the value of the given register. Note: - Registers 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. + 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 + 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)) + @property def arch(self) -> str: """Returns the architecture of the host processor. @@ -235,8 +266,8 @@ class Process(object): Note: The architecture of the host platform is not necessarily the architecture of the target executable. However, this - value will accurately represent how registers should be - addressed. + value will accurately represent which registers are + available. Returns: A string representing the host processor. As of now, only diff --git a/wrapper/ptrace.c b/wrapper/ptrace.c index 8517519..706fd4e 100644 --- a/wrapper/ptrace.c +++ b/wrapper/ptrace.c @@ -81,6 +81,18 @@ 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); } @@ -108,6 +120,16 @@ unsigned long long getreg(int pid, int idx) { 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; @@ -116,4 +138,14 @@ unsigned long getreg(int pid, int idx) { 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 -- cgit v1.3 From 26b602c9080572de54458768bf80ef879fb9e8f4 Mon Sep 17 00:00:00 2001 From: Jakob Date: Mon, 4 Sep 2017 13:11:42 -0400 Subject: Improved interface for executing code from the inferior. --- hypodermic/main.py | 20 +------------ hypodermic/process.py | 79 +++++++++++++++++++++++++++++++++++++++++++++++++ hypodermic/shellcode.py | 41 +++++++++++++++++++++++++ setup.py | 2 +- 4 files changed, 122 insertions(+), 20 deletions(-) create mode 100644 hypodermic/shellcode.py diff --git a/hypodermic/main.py b/hypodermic/main.py index 7ce9be6..cb17d3b 100644 --- a/hypodermic/main.py +++ b/hypodermic/main.py @@ -108,26 +108,8 @@ def main(): if args.create: alert("Creating process at path '{}'...".format(args.create)) p = Process(path=args.create) - - shellcode = b"\x48\xc7\xc0\x01\x00\x00\x00\x48\xc7\xc7\x01\x00" + \ - b"\x00\x00\x48\xc7\xc2\x29\x00\x00\x00\x48\x8d\x35" + \ - b"\x00\x00\x00\x00\x48\x81\xc6\x0d\x00\x00\x00\x0f" + \ - b"\x05\x90\x90\x90\xcc\x61\x6d\x64\x36\x34\x20\x4c" + \ - b"\x69\x6e\x75\x78\x20\x73\x79\x73\x5f\x77\x72\x69" + \ - b"\x74\x65\x20\x73\x68\x65\x6c\x6c\x63\x6f\x64\x65" + \ - b"\x20\x62\x79\x20\x4a\x61\x6b\x6f\x62\x0a" - - old_rip = p.get_register("rip") - alert("%rip at {}".format(hex(old_rip))) - old_code = p.read_bytes(old_rip, len(shellcode)) - p.write_bytes(old_rip, shellcode) - while p.read_bytes(p.get_register("rip"), 1) != b'\xcc': - p.single_step() - alert("Hit breakpoint!") - p.write_bytes(old_rip, old_code) - p.set_register("rip", old_rip) - alert("%rip reset to {}".format(hex(p.get_register("rip")))) p.continue_until_haulted() else: alert("Attaching to process with pid {}...".format(args.attach)) p = Process(pid=args.attach) + p.continue_until_haulted() diff --git a/hypodermic/process.py b/hypodermic/process.py index c8a2823..1e2c1d4 100644 --- a/hypodermic/process.py +++ b/hypodermic/process.py @@ -22,6 +22,7 @@ import os.path import re from hypodermic.memory import Region, maps +from hypodermic.shellcode import assemble _AMD64_INDICES = { "r15": 0, @@ -53,6 +54,23 @@ _AMD64_INDICES = { "gs": 26 } +_AMD64_REGS = [ + "rax", + "rbx", + "rcx", + "rdx", + "rsi", + "rdi", + "r8", + "r9", + "r10", + "r11", + "r12", + "r13", + "r14", + "r15", +] + _I386_INDICES = { "ebx": 0, "ecx": 1, @@ -73,6 +91,15 @@ _I386_INDICES = { "xss": 16 } +_I386_REGS = [ + "eax", + "ebx", + "ecx", + "edx", + "esi", + "edi", +] + class Process(object): """Process attached via ptrace. @@ -259,6 +286,58 @@ class Process(object): 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. 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 . + +"""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) diff --git a/setup.py b/setup.py index 4788a81..2ea6ea5 100644 --- a/setup.py +++ b/setup.py @@ -30,7 +30,7 @@ setup( packages=["hypodermic"], include_package_data=True, ext_modules=[lib], - install_requires=["pyelftools"], + install_requires=["pyelftools", "keystone-engine"], extras_require={}, tests_require=[], entry_points={"console_scripts": ["hypodermic = hypodermic.main:main"]}, -- cgit v1.3