[
  {
    "path": "README.md",
    "content": "# Terminal Hijacker #\n\n## Introduction ##\n\n![terminal](doc/terminal.gif)\n\nTermiJack hijacks the standard streams (stdout, stdin, and/or stderr) from an already\nrunning process and silently returns them back after finishing. While this\nscript is running and attached to another process, the user may interact with\nthe running process as if they were interacting with the original terminal.\n\nThis script also provides the ability to mirror hijacked streams. In the case\nof standard input, this means that inputs from both this terminal and the\nremote terminal will be forwarded to the target process. Similarly, standard\noutput and error coming from the target process will be forwarded to both this\nterminal and the remote terminal.\n\nWhile gdb is being used to hijack standard streams, there may be a small\nlatency during the transition where the target process is paused. Do _not_ use\nthis script on time-critical processes. Also, this script may need to be run as\nroot in order for gdb to do its business.\n\nLastly, this script performs poorly with programs using either the ncurses or\nreadline GNU libraries due to the special way they interact with input/output\nstreams. Support for them may be added in the future.\n\nRequires the GNU Debugger (gdb) in order to run.\n\n\n## Theory ##\n\nTypically, the standard streams (stdin, stdout, stderr) are connected to a\nvirtual terminal like ```/dev/pts/23``` as show below:\n\n![before_hijack](doc/before_hijack_lite.png)\n\nUsing gdb to intercept the target process, we can use syscalls (open, fcntl)\nto create a set of named pipes that will act as the intermediate socket between\nthe target process and the hijacker script. Other syscalls (dup, dup2) are used\nto clone the original standard streams to temporary place-holders and to swap\nthe file descriptors of the named pipes and standard streams.\n\nIn the situation where we only hijack the standard streams and don't reflect\nthe to/from the original streams, this setup looks something like the following:\n\n![after_hijack](doc/after_hijack_lite.png)\n\nThe termijack script also allows the ability to mirror the standard streams\nto/from the hijacked process. This means that the hijacked stdin and hijacker's\nstdin will be multiplexed to the target process. Additionally, and stdout or\nstderr coming from the hijacked process will be sent to both the hijacked\nvirtual terminal and to the hijacker's virtual terminal. This setup looks\nsomething like the following:\n\n![after_hijack_reflect](doc/after_hijack_reflect_lite.png)\n\nOf course, at the very end, when the termijack script detaches from the target\nprocess, it will undo all of the shenanigans and close file descriptors that it\nopened. Ideally, it's operation should be very surreptitious.\n\n\n## Usage ##\n\nHijack stdin, stdout, and stderr:\n\n* ```./termijack.py -ioe $TARGET_PID```\n\nHijack stdin, stdout, and stderr. Also, reflect them back to the target process:\n\n* ```./termijack.py -IOE $TARGET_PID```\n"
  },
  {
    "path": "termijack.py",
    "content": "#!/usr/bin/env python\n\n# Written in 2012 by Joe Tsai <joetsai@digital-static.net>\n#\n# ===================================================================\n# The contents of this file are dedicated to the public domain. To\n# the extent that dedication to the public domain is not available,\n# everyone is granted a worldwide, perpetual, royalty-free,\n# non-exclusive license to exercise all rights associated with the\n# contents of this file for any purpose whatsoever.\n# No rights are reserved.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\n# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\n# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\n# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS\n# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\n# ===================================================================\n\nimport re\nimport os\nimport sys\nimport time\nimport stat\nimport fcntl\nimport errno\nimport signal\nimport tempfile\nimport optparse\nimport subprocess\n\n################################################################################\n############################### Global variables ###############################\n################################################################################\n\n# Dictionary of streams to hijack\n#  Key: 0 for stdin, 1 for stdout, 2 for stderr\n#  Values: [local terminal file object, remote terminal file object, FIFO file object, target process original file descriptor]\nstreams = {0:[sys.stdin,None,None,None], 1:[sys.stdout,None,None,None], 2:[sys.stderr,None,None,None]}\nhijack = [False, False, False] # Streams to hijack\nmirror = [False, False, False] # Streams to reflect\npid = None # Target process\ntempdir = None # Temporary directory, will clean-up at the end\nsys_exit = False\n\n################################################################################\n################################ Helper classes ################################\n################################################################################\n\nclass GDB_Client():\n    def __init__(self):\n        # Start a GDB process\n        self.proc = subprocess.Popen(['gdb'], stdin = subprocess.PIPE, stdout = subprocess.PIPE, stderr = subprocess.PIPE)\n        non_blocking(self.proc.stdout)\n        non_blocking(self.proc.stderr)\n\n        # Flush initial text\n        self.proc.stdin.write(\"set prompt \\\\033[X\\\\n\\n\")\n        while True:\n            try:\n                if '\\x1b[X' in readline(self.proc.stdout): break\n            except: pass\n        while True:\n            try: lines += self.proc.stderr.readline()\n            except: break\n\n    def command(self, cmd):\n        self.proc.stdin.write(cmd+'\\n')\n        lines = ''\n        while True:\n            line = ''\n            try: line = readline(self.proc.stdout)\n            except: pass\n            if '\\x1b[X' in line: break\n            lines += line\n        while True:\n            try: lines += self.proc.stderr.readline()\n            except: break\n        return lines\n\n    def close(self):\n        self.proc.stdin.write(\"set confirm off\\n\")\n        self.proc.stdin.write(\"quit\\n\")\n\n################################################################################\n############################### Helper functions ###############################\n################################################################################\n\ndef show_help(message):\n    print message\n    print \"Try '%s --help' for more information\" % sys.argv[0].strip()\n    sys.exit(1)\n\ndef safe_exit(ret_code = 0, message = None):\n    if message:\n        print message\n\n    # Perform clean-up on the target process\n    gdb = GDB_Client()\n    gdb.command('attach %s' % pid)\n    for stream_num in range(3):\n        # Copy temporary holders back into original stream and close the swap\n        if streams[stream_num][3]:\n            ret_text = gdb.command('call dup2(%s,%s)' % (streams[stream_num][3],str(stream_num)))\n            ret_text = gdb.command('call close(%s)' % streams[stream_num][3])\n    gdb.close()\n\n    # Close the files opened for mirror reflection operations\n    for stream_num in range(3):\n        if streams[stream_num][1]:\n            streams[stream_num][1].close()\n\n    # Close each FIFO\n    for stream_num in range(3):\n        if streams[stream_num][2]:\n            streams[stream_num][2].close()\n\n    # Delete each FIFO and the temporary directory\n    if tempdir:\n        for stream_num in range(3):\n            if streams[stream_num][2]:\n                os.remove(os.path.join(tempdir,str(stream_num)))\n        os.removedirs(tempdir)\n\n    sys.exit(ret_code)\n\ndef interrupt_handler(sig_num, frame):\n    global sys_exit\n    if not sys_exit:\n        sys_exit = True\n        safe_exit(0, \"\\r----------\\nDetached from target process!\")\n\ndef check_pid(pid):\n    try: os.kill(pid, 0)\n    except OSError: return False\n    else: return True\n\ndef non_blocking(file):\n    file_desc = file.fileno()\n    file_flags = fcntl.fcntl(file_desc, fcntl.F_GETFL)\n    fcntl.fcntl(file_desc, fcntl.F_SETFL, file_flags | os.O_NONBLOCK)\n\ndef readline(file, timeout = 5):\n        line = ''\n        start_mark = time.time()\n        while True:\n            try:\n                char = file.read(1)\n                line += char\n                if char == '\\n': break\n            except:\n                if time.time() - start_mark > timeout: break\n        return line\n\n################################################################################\n################################# Script setup #################################\n################################################################################\n\nepilog = \"\"\"\nHijacks the standard streams (stdout, stdin, and/or stderr) from an already\nrunning process and silently returns them back after finishing. While this\nscript is running and attached to another process, the user may interact with\nthe running process as if they were interacting with the original terminal.\n\nThis script also provides the ability to mirror hijacked streams. In the case\nof standard input, this means that inputs from both this terminal and the\nremote terminal will be forwarded to the target process. Similarly, standard\noutput and error coming from the target process will be forwarded to both this\nterminal and the remote terminal.\n\nWhile gdb is being used to hijack standard streams, there may be a small\nlatency during the transition where the target process is paused. Do NOT use\nthis script on time-critical processes. Also, this script may need to be run as\nroot in order for gdb to do its business.\n\nLastly, this script performs poorly with programs using either the ncurses or\nreadline GNU libraries due to the special way they interact with input/output\nstreams. Support for them may be added in the future.\n\nRequires the GNU Debugger (gdb) in order to run.\n\"\"\"\n\n# Create a option parser\nopts_parser = optparse.OptionParser(usage = \"%s [options] PID\" % sys.argv[0].strip(), epilog = epilog, add_help_option = False)\ndef func_epilog(formatter): return epilog\nopts_parser.format_epilog = func_epilog\nopts_parser.add_option('-h', '--help', action = 'help', help = 'Display this help and exit')\nopts_parser.add_option('-v', '--version', dest = 'version', action = 'store_true', help = 'Display the script version and exit')\nopts_parser.add_option('-i', '--hijack_stdin',  dest = 'hijack_stdin',  action = 'store_true', help = 'Hijack the standard input stream going to the target process [Default: False]')\nopts_parser.add_option('-o', '--hijack_stdout', dest = 'hijack_stdout', action = 'store_true', help = 'Hijack the standard output stream coming from the target process [Default: False]')\nopts_parser.add_option('-e', '--hijack_stderr', dest = 'hijack_stderr', action = 'store_true', help = 'Hijack the standard error stream coming from the target process [Default: False]')\nopts_parser.add_option('-I', '--mirror_stdin',  dest = 'mirror_stdin',  action = 'store_true', help = 'Mirror input streams from both local and remote terminals to the target process [Default: False]')\nopts_parser.add_option('-O', '--mirror_stdout', dest = 'mirror_stdout', action = 'store_true', help = 'Mirror the output stream from the target process to both the local and remote terminals [Default: False]')\nopts_parser.add_option('-E', '--mirror_stderr', dest = 'mirror_stderr', action = 'store_true', help = 'Mirror the error stream from the target process to both the local and remote terminals [Default: False]')\n(opts, args) = opts_parser.parse_args()\n\n# Display version and quit\nif opts.version:\n    print \"Terminal Hijacking Script 1.0\"\n    print \" This is free software: you are free to change and redistribute it.\"\n    print \" Written in 2012 by Joe Tsai <joetsai@digital-static.net>\"\n    sys.exit(0)\n\n# Check the target process argument\nif len(args) != 1:\n    show_help(\"Invalid number of required arguments\")\ntry:\n    pid = str(int(args[0]))\nexcept:\n    show_help(\"Invalid target process: %s\" % args[0])\n\n# Check which streams to hijack (If mirror is enabled, assume a hijacking was on order)\nopts.hijack_stdin = True if opts.mirror_stdin else opts.hijack_stdin\nopts.hijack_stdout = True if opts.mirror_stdout else opts.hijack_stdout\nopts.hijack_stderr = True if opts.mirror_stderr else opts.hijack_stderr\nhijack = [opts.hijack_stdin, opts.hijack_stdout, opts.hijack_stderr] # Streams to hijack\nmirror = [opts.mirror_stdin, opts.mirror_stdout, opts.mirror_stderr] # Streams to reflect\nif True not in hijack:\n    show_help(\"Must hijack at least one stream\")\n\n# Interrupt handler\nsignal.signal(signal.SIGTERM, interrupt_handler)\nsignal.signal(signal.SIGQUIT, interrupt_handler)\nsignal.signal(signal.SIGINT, interrupt_handler)\n\n# Set local stdin as non-blocking\nnon_blocking(sys.stdin)\n\n################################################################################\n################################# Script start #################################\n################################################################################\n\n# Check that gdb is even installed\ntry:\n    subprocess.Popen(['gdb','--version'], stdout = subprocess.PIPE, stderr = subprocess.PIPE).wait()\nexcept:\n    safe_exit(1, \"Error: Could not find an installation of GNU Debugger (gdb) on this system\")\n\n# Generated named pipes\ntempdir = tempfile.mkdtemp(prefix = 'termijack_')\nos.chmod(tempdir,0711) # Target process must be able to access this folder\ntry:\n    for stream_num in range(3):\n        if hijack[stream_num]:\n            os.mkfifo(os.path.join(tempdir,str(stream_num)))\n            os.chmod(os.path.join(tempdir,str(stream_num)),0666) # Target process must be able to read these pipes\n            streams[stream_num][2] = open(os.path.join(tempdir,str(stream_num)), 'w+' if (stream_num == 0) else 'r+')\n            non_blocking(streams[stream_num][2])\nexcept:\n    safe_exit(1, \"Error: Could not create temporary FIFO pipes\")\n\n# Attach gdb to the target process\ngdb = GDB_Client()\nline = gdb.command('attach %s' % pid)\nif \"No such process\" in line:\n    safe_exit(1, \"Error: The target process does not exist\")\nelif \"Operation not permitted\" in line:\n    safe_exit(1, \"Error: Attaching to target process not permitted\")\nelif \"Could not attach\" in line:\n    safe_exit(1, \"Error: Could not attach to target process\")\n\n# Redirect streams as necessary\nfor stream_num in range(3):\n    if hijack[stream_num]:\n        # Open named pipes on target process\n        ret_text = gdb.command('call open(\"%s\",66)' % os.path.join(tempdir,str(stream_num)))\n        pipe_fd = re.search(r\"\\$[0-9]+ = ([0-9]+)\",ret_text).groups()[0]\n\n        # Copy original flags to the new pipes\n        ret_text = gdb.command('call fcntl(%s,4,fcntl(%s,3))' % (pipe_fd,str(stream_num)))\n\n        # Copy original stream into temporary holders\n        ret_text = gdb.command('call dup(%s)' % str(stream_num))\n        streams[stream_num][3] = re.search(r\"\\$[0-9]+ = ([0-9]+)\",ret_text).groups()[0]\n\n        # Copy new pipes into original stream\n        ret_text = gdb.command('call dup2(%s,%s)' % (pipe_fd,str(stream_num)))\n\n        # Close the opened pipe\n        ret_text = gdb.command('call close(%s)' % pipe_fd)\ngdb.close()\nprint  \"Attached to target process %s\" % pid\n\n# Open virtual terminals for stealthy reflection tricks\nfor stream_num in range(3):\n    if mirror[stream_num]:\n        stream_type = {0:'stdin', 1:'stdout', 2:'stderr'}\n        file_real = os.path.realpath(os.path.join(\"/proc\",pid,'fd',streams[stream_num][3]))\n        try:\n            if re.search(\"^/dev/\",file_real) and stat.S_ISCHR(os.stat(file_real).st_mode):\n                streams[stream_num][1] = open(file_real,'rw+')\n                non_blocking(streams[stream_num][1])\n            else:\n                print \"Warning: The file %s does not represent a valid terminal for %s\" % (file_real, stream_type[stream_num])\n        except OSError, ex:\n            print \"Warning: %s while accessing %s for %s\" % (ex.strerror, file_real, stream_type[stream_num])\nprint \"----------\"\n\nwhile True:\n    # Forward to target stdin from:\n    if hijack[0]:\n        # Local stdin\n        try:\n            streams[0][2].write(streams[0][0].read())\n            streams[0][2].flush()\n        except: pass\n        # Remote stdin\n        try:\n            streams[0][2].write(streams[0][1].read())\n            streams[0][2].flush()\n        except: pass\n\n    # Forward target stdout to:\n    if hijack[1]:\n        try:\n            data = streams[1][2].read()\n            # Local stdout\n            if streams[1][0]:\n                streams[1][0].write(data)\n                streams[1][0].flush()\n            # Remote stdout\n            if streams[1][1]:\n                streams[1][1].write(data)\n                streams[1][1].flush()\n        except: pass\n\n    # Forward target stderr to:\n    if hijack[2]:\n        try:\n            data = streams[2][2].read()\n            # Local stderr\n            if streams[2][0]:\n                streams[2][0].write(data)\n                streams[2][0].flush()\n            # Remote stderr\n            if streams[2][1]:\n                streams[2][1].write(data)\n                streams[2][1].flush()\n        except: pass\n\n    # Check if target process died\n    if not check_pid(int(pid)):\n        safe_exit(0,\"\\r----------\\nTarget process died!\")\n\n    time.sleep(0.01)\n\n# EOF\n"
  }
]