Full Code of dsnet/termijack for AI

master 994baa577377 cached
4 files
17.0 KB
4.1k tokens
11 symbols
1 requests
Download .txt
Repository: dsnet/termijack
Branch: master
Commit: 994baa577377
Files: 4
Total size: 17.0 KB

Directory structure:
gitextract_e95ahg22/

├── README.md
├── doc/
│   ├── diagram.psd
│   └── terminal.psd
└── termijack.py

================================================
FILE CONTENTS
================================================

================================================
FILE: README.md
================================================
# Terminal Hijacker #

## Introduction ##

![terminal](doc/terminal.gif)

TermiJack hijacks the standard streams (stdout, stdin, and/or stderr) from an already
running process and silently returns them back after finishing. While this
script is running and attached to another process, the user may interact with
the running process as if they were interacting with the original terminal.

This script also provides the ability to mirror hijacked streams. In the case
of standard input, this means that inputs from both this terminal and the
remote terminal will be forwarded to the target process. Similarly, standard
output and error coming from the target process will be forwarded to both this
terminal and the remote terminal.

While gdb is being used to hijack standard streams, there may be a small
latency during the transition where the target process is paused. Do _not_ use
this script on time-critical processes. Also, this script may need to be run as
root in order for gdb to do its business.

Lastly, this script performs poorly with programs using either the ncurses or
readline GNU libraries due to the special way they interact with input/output
streams. Support for them may be added in the future.

Requires the GNU Debugger (gdb) in order to run.


## Theory ##

Typically, the standard streams (stdin, stdout, stderr) are connected to a
virtual terminal like ```/dev/pts/23``` as show below:

![before_hijack](doc/before_hijack_lite.png)

Using gdb to intercept the target process, we can use syscalls (open, fcntl)
to create a set of named pipes that will act as the intermediate socket between
the target process and the hijacker script. Other syscalls (dup, dup2) are used
to clone the original standard streams to temporary place-holders and to swap
the file descriptors of the named pipes and standard streams.

In the situation where we only hijack the standard streams and don't reflect
the to/from the original streams, this setup looks something like the following:

![after_hijack](doc/after_hijack_lite.png)

The termijack script also allows the ability to mirror the standard streams
to/from the hijacked process. This means that the hijacked stdin and hijacker's
stdin will be multiplexed to the target process. Additionally, and stdout or
stderr coming from the hijacked process will be sent to both the hijacked
virtual terminal and to the hijacker's virtual terminal. This setup looks
something like the following:

![after_hijack_reflect](doc/after_hijack_reflect_lite.png)

Of course, at the very end, when the termijack script detaches from the target
process, it will undo all of the shenanigans and close file descriptors that it
opened. Ideally, it's operation should be very surreptitious.


## Usage ##

Hijack stdin, stdout, and stderr:

* ```./termijack.py -ioe $TARGET_PID```

Hijack stdin, stdout, and stderr. Also, reflect them back to the target process:

* ```./termijack.py -IOE $TARGET_PID```


================================================
FILE: termijack.py
================================================
#!/usr/bin/env python

# Written in 2012 by Joe Tsai <joetsai@digital-static.net>
#
# ===================================================================
# The contents of this file are dedicated to the public domain. To
# the extent that dedication to the public domain is not available,
# everyone is granted a worldwide, perpetual, royalty-free,
# non-exclusive license to exercise all rights associated with the
# contents of this file for any purpose whatsoever.
# No rights are reserved.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# ===================================================================

import re
import os
import sys
import time
import stat
import fcntl
import errno
import signal
import tempfile
import optparse
import subprocess

################################################################################
############################### Global variables ###############################
################################################################################

# Dictionary of streams to hijack
#  Key: 0 for stdin, 1 for stdout, 2 for stderr
#  Values: [local terminal file object, remote terminal file object, FIFO file object, target process original file descriptor]
streams = {0:[sys.stdin,None,None,None], 1:[sys.stdout,None,None,None], 2:[sys.stderr,None,None,None]}
hijack = [False, False, False] # Streams to hijack
mirror = [False, False, False] # Streams to reflect
pid = None # Target process
tempdir = None # Temporary directory, will clean-up at the end
sys_exit = False

################################################################################
################################ Helper classes ################################
################################################################################

class GDB_Client():
    def __init__(self):
        # Start a GDB process
        self.proc = subprocess.Popen(['gdb'], stdin = subprocess.PIPE, stdout = subprocess.PIPE, stderr = subprocess.PIPE)
        non_blocking(self.proc.stdout)
        non_blocking(self.proc.stderr)

        # Flush initial text
        self.proc.stdin.write("set prompt \\033[X\\n\n")
        while True:
            try:
                if '\x1b[X' in readline(self.proc.stdout): break
            except: pass
        while True:
            try: lines += self.proc.stderr.readline()
            except: break

    def command(self, cmd):
        self.proc.stdin.write(cmd+'\n')
        lines = ''
        while True:
            line = ''
            try: line = readline(self.proc.stdout)
            except: pass
            if '\x1b[X' in line: break
            lines += line
        while True:
            try: lines += self.proc.stderr.readline()
            except: break
        return lines

    def close(self):
        self.proc.stdin.write("set confirm off\n")
        self.proc.stdin.write("quit\n")

################################################################################
############################### Helper functions ###############################
################################################################################

def show_help(message):
    print message
    print "Try '%s --help' for more information" % sys.argv[0].strip()
    sys.exit(1)

def safe_exit(ret_code = 0, message = None):
    if message:
        print message

    # Perform clean-up on the target process
    gdb = GDB_Client()
    gdb.command('attach %s' % pid)
    for stream_num in range(3):
        # Copy temporary holders back into original stream and close the swap
        if streams[stream_num][3]:
            ret_text = gdb.command('call dup2(%s,%s)' % (streams[stream_num][3],str(stream_num)))
            ret_text = gdb.command('call close(%s)' % streams[stream_num][3])
    gdb.close()

    # Close the files opened for mirror reflection operations
    for stream_num in range(3):
        if streams[stream_num][1]:
            streams[stream_num][1].close()

    # Close each FIFO
    for stream_num in range(3):
        if streams[stream_num][2]:
            streams[stream_num][2].close()

    # Delete each FIFO and the temporary directory
    if tempdir:
        for stream_num in range(3):
            if streams[stream_num][2]:
                os.remove(os.path.join(tempdir,str(stream_num)))
        os.removedirs(tempdir)

    sys.exit(ret_code)

def interrupt_handler(sig_num, frame):
    global sys_exit
    if not sys_exit:
        sys_exit = True
        safe_exit(0, "\r----------\nDetached from target process!")

def check_pid(pid):
    try: os.kill(pid, 0)
    except OSError: return False
    else: return True

def non_blocking(file):
    file_desc = file.fileno()
    file_flags = fcntl.fcntl(file_desc, fcntl.F_GETFL)
    fcntl.fcntl(file_desc, fcntl.F_SETFL, file_flags | os.O_NONBLOCK)

def readline(file, timeout = 5):
        line = ''
        start_mark = time.time()
        while True:
            try:
                char = file.read(1)
                line += char
                if char == '\n': break
            except:
                if time.time() - start_mark > timeout: break
        return line

################################################################################
################################# Script setup #################################
################################################################################

epilog = """
Hijacks the standard streams (stdout, stdin, and/or stderr) from an already
running process and silently returns them back after finishing. While this
script is running and attached to another process, the user may interact with
the running process as if they were interacting with the original terminal.

This script also provides the ability to mirror hijacked streams. In the case
of standard input, this means that inputs from both this terminal and the
remote terminal will be forwarded to the target process. Similarly, standard
output and error coming from the target process will be forwarded to both this
terminal and the remote terminal.

While gdb is being used to hijack standard streams, there may be a small
latency during the transition where the target process is paused. Do NOT use
this script on time-critical processes. Also, this script may need to be run as
root in order for gdb to do its business.

Lastly, this script performs poorly with programs using either the ncurses or
readline GNU libraries due to the special way they interact with input/output
streams. Support for them may be added in the future.

Requires the GNU Debugger (gdb) in order to run.
"""

# Create a option parser
opts_parser = optparse.OptionParser(usage = "%s [options] PID" % sys.argv[0].strip(), epilog = epilog, add_help_option = False)
def func_epilog(formatter): return epilog
opts_parser.format_epilog = func_epilog
opts_parser.add_option('-h', '--help', action = 'help', help = 'Display this help and exit')
opts_parser.add_option('-v', '--version', dest = 'version', action = 'store_true', help = 'Display the script version and exit')
opts_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]')
opts_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]')
opts_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]')
opts_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]')
opts_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]')
opts_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]')
(opts, args) = opts_parser.parse_args()

# Display version and quit
if opts.version:
    print "Terminal Hijacking Script 1.0"
    print " This is free software: you are free to change and redistribute it."
    print " Written in 2012 by Joe Tsai <joetsai@digital-static.net>"
    sys.exit(0)

# Check the target process argument
if len(args) != 1:
    show_help("Invalid number of required arguments")
try:
    pid = str(int(args[0]))
except:
    show_help("Invalid target process: %s" % args[0])

# Check which streams to hijack (If mirror is enabled, assume a hijacking was on order)
opts.hijack_stdin = True if opts.mirror_stdin else opts.hijack_stdin
opts.hijack_stdout = True if opts.mirror_stdout else opts.hijack_stdout
opts.hijack_stderr = True if opts.mirror_stderr else opts.hijack_stderr
hijack = [opts.hijack_stdin, opts.hijack_stdout, opts.hijack_stderr] # Streams to hijack
mirror = [opts.mirror_stdin, opts.mirror_stdout, opts.mirror_stderr] # Streams to reflect
if True not in hijack:
    show_help("Must hijack at least one stream")

# Interrupt handler
signal.signal(signal.SIGTERM, interrupt_handler)
signal.signal(signal.SIGQUIT, interrupt_handler)
signal.signal(signal.SIGINT, interrupt_handler)

# Set local stdin as non-blocking
non_blocking(sys.stdin)

################################################################################
################################# Script start #################################
################################################################################

# Check that gdb is even installed
try:
    subprocess.Popen(['gdb','--version'], stdout = subprocess.PIPE, stderr = subprocess.PIPE).wait()
except:
    safe_exit(1, "Error: Could not find an installation of GNU Debugger (gdb) on this system")

# Generated named pipes
tempdir = tempfile.mkdtemp(prefix = 'termijack_')
os.chmod(tempdir,0711) # Target process must be able to access this folder
try:
    for stream_num in range(3):
        if hijack[stream_num]:
            os.mkfifo(os.path.join(tempdir,str(stream_num)))
            os.chmod(os.path.join(tempdir,str(stream_num)),0666) # Target process must be able to read these pipes
            streams[stream_num][2] = open(os.path.join(tempdir,str(stream_num)), 'w+' if (stream_num == 0) else 'r+')
            non_blocking(streams[stream_num][2])
except:
    safe_exit(1, "Error: Could not create temporary FIFO pipes")

# Attach gdb to the target process
gdb = GDB_Client()
line = gdb.command('attach %s' % pid)
if "No such process" in line:
    safe_exit(1, "Error: The target process does not exist")
elif "Operation not permitted" in line:
    safe_exit(1, "Error: Attaching to target process not permitted")
elif "Could not attach" in line:
    safe_exit(1, "Error: Could not attach to target process")

# Redirect streams as necessary
for stream_num in range(3):
    if hijack[stream_num]:
        # Open named pipes on target process
        ret_text = gdb.command('call open("%s",66)' % os.path.join(tempdir,str(stream_num)))
        pipe_fd = re.search(r"\$[0-9]+ = ([0-9]+)",ret_text).groups()[0]

        # Copy original flags to the new pipes
        ret_text = gdb.command('call fcntl(%s,4,fcntl(%s,3))' % (pipe_fd,str(stream_num)))

        # Copy original stream into temporary holders
        ret_text = gdb.command('call dup(%s)' % str(stream_num))
        streams[stream_num][3] = re.search(r"\$[0-9]+ = ([0-9]+)",ret_text).groups()[0]

        # Copy new pipes into original stream
        ret_text = gdb.command('call dup2(%s,%s)' % (pipe_fd,str(stream_num)))

        # Close the opened pipe
        ret_text = gdb.command('call close(%s)' % pipe_fd)
gdb.close()
print  "Attached to target process %s" % pid

# Open virtual terminals for stealthy reflection tricks
for stream_num in range(3):
    if mirror[stream_num]:
        stream_type = {0:'stdin', 1:'stdout', 2:'stderr'}
        file_real = os.path.realpath(os.path.join("/proc",pid,'fd',streams[stream_num][3]))
        try:
            if re.search("^/dev/",file_real) and stat.S_ISCHR(os.stat(file_real).st_mode):
                streams[stream_num][1] = open(file_real,'rw+')
                non_blocking(streams[stream_num][1])
            else:
                print "Warning: The file %s does not represent a valid terminal for %s" % (file_real, stream_type[stream_num])
        except OSError, ex:
            print "Warning: %s while accessing %s for %s" % (ex.strerror, file_real, stream_type[stream_num])
print "----------"

while True:
    # Forward to target stdin from:
    if hijack[0]:
        # Local stdin
        try:
            streams[0][2].write(streams[0][0].read())
            streams[0][2].flush()
        except: pass
        # Remote stdin
        try:
            streams[0][2].write(streams[0][1].read())
            streams[0][2].flush()
        except: pass

    # Forward target stdout to:
    if hijack[1]:
        try:
            data = streams[1][2].read()
            # Local stdout
            if streams[1][0]:
                streams[1][0].write(data)
                streams[1][0].flush()
            # Remote stdout
            if streams[1][1]:
                streams[1][1].write(data)
                streams[1][1].flush()
        except: pass

    # Forward target stderr to:
    if hijack[2]:
        try:
            data = streams[2][2].read()
            # Local stderr
            if streams[2][0]:
                streams[2][0].write(data)
                streams[2][0].flush()
            # Remote stderr
            if streams[2][1]:
                streams[2][1].write(data)
                streams[2][1].flush()
        except: pass

    # Check if target process died
    if not check_pid(int(pid)):
        safe_exit(0,"\r----------\nTarget process died!")

    time.sleep(0.01)

# EOF
Download .txt
gitextract_e95ahg22/

├── README.md
├── doc/
│   ├── diagram.psd
│   └── terminal.psd
└── termijack.py
Download .txt
SYMBOL INDEX (11 symbols across 1 files)

FILE: termijack.py
  class GDB_Client (line 53) | class GDB_Client():
    method __init__ (line 54) | def __init__(self):
    method command (line 70) | def command(self, cmd):
    method close (line 84) | def close(self):
  function show_help (line 92) | def show_help(message):
  function safe_exit (line 97) | def safe_exit(ret_code = 0, message = None):
  function interrupt_handler (line 130) | def interrupt_handler(sig_num, frame):
  function check_pid (line 136) | def check_pid(pid):
  function non_blocking (line 141) | def non_blocking(file):
  function readline (line 146) | def readline(file, timeout = 5):
  function func_epilog (line 188) | def func_epilog(formatter): return epilog
Condensed preview — 4 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (18K chars).
[
  {
    "path": "README.md",
    "chars": 2948,
    "preview": "# Terminal Hijacker #\n\n## Introduction ##\n\n![terminal](doc/terminal.gif)\n\nTermiJack hijacks the standard streams (stdout"
  },
  {
    "path": "termijack.py",
    "chars": 14429,
    "preview": "#!/usr/bin/env python\n\n# Written in 2012 by Joe Tsai <joetsai@digital-static.net>\n#\n# =================================="
  }
]

// ... and 2 more files (download for full content)

About this extraction

This page contains the full source code of the dsnet/termijack GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 4 files (17.0 KB), approximately 4.1k tokens, and a symbol index with 11 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!