maybe-0.4.0/0000755000175000017500000000000012676016206012226 5ustar pewpew00000000000000maybe-0.4.0/maybe/0000755000175000017500000000000012676016206013323 5ustar pewpew00000000000000maybe-0.4.0/maybe/__init__.py0000644000175000017500000000000012651175721015423 0ustar pewpew00000000000000maybe-0.4.0/maybe/maybe.py0000644000175000017500000001424412675704444015006 0ustar pewpew00000000000000# maybe - see what a program does before deciding whether you really want it to happen # # Copyright (c) 2016 Philipp Emanuel Weidmann # # Nemo vir est qui mundum non reddat meliorem. # # Released under the terms of the GNU General Public License, version 3 # (https://gnu.org/licenses/gpl.html) import sys import subprocess from logging import getLogger, NullHandler from ptrace.tools import locateProgram from ptrace.debugger import ProcessSignal, NewProcessEvent, ProcessExecution, ProcessExit from ptrace.debugger.child import createChild from ptrace.debugger.debugger import PtraceDebugger, DebuggerError from ptrace.func_call import FunctionCallOptions from ptrace.syscall import SYSCALL_PROTOTYPES, FILENAME_ARGUMENTS from ptrace.syscall.posix_constants import SYSCALL_ARG_DICT from ptrace.syscall.syscall_argument import ARGUMENT_CALLBACK from .syscall_filters import SYSCALL_FILTERS from .utilities import T, SYSCALL_REGISTER, RETURN_VALUE_REGISTER # Python 2/3 compatibility hack # Source: http://stackoverflow.com/a/7321970 try: input = raw_input except NameError: pass # Suppress logging output from python-ptrace getLogger().addHandler(NullHandler()) # Register filtered syscalls with python-ptrace so they are parsed correctly SYSCALL_PROTOTYPES.clear() FILENAME_ARGUMENTS.clear() for syscall_filter in SYSCALL_FILTERS: SYSCALL_PROTOTYPES[syscall_filter.name] = syscall_filter.signature for argument in syscall_filter.signature[1]: if argument[0] == "const char *": FILENAME_ARGUMENTS.add(argument[1]) # Turn list into dictionary indexed by syscall name for fast filter retrieval SYSCALL_FILTERS = {syscall_filter.name: syscall_filter for syscall_filter in SYSCALL_FILTERS} # Prevent python-ptrace from decoding arguments to keep raw numerical values SYSCALL_ARG_DICT.clear() ARGUMENT_CALLBACK.clear() def prepareProcess(process): process.syscall() process.syscall_state.ignore_callback = lambda syscall: syscall.name not in SYSCALL_FILTERS def parse_argument(argument): argument = argument.createText() if argument.startswith(("'", '"')): # Remove quotes from string argument return argument[1:-1] elif argument.startswith(("b'", 'b"')): # Python 3 bytes literal return argument[2:-1] else: # Note that "int" with base 0 infers the base from the prefix return int(argument, 0) format_options = FunctionCallOptions( replace_socketcall=False, string_max_length=4096, ) def get_operations(debugger): operations = [] while True: if not debugger: # All processes have exited break # This logic is mostly based on python-ptrace's "strace" example try: syscall_event = debugger.waitSyscall() except ProcessSignal as event: event.process.syscall(event.signum) continue except NewProcessEvent as event: prepareProcess(event.process) event.process.parent.syscall() continue except ProcessExecution as event: event.process.syscall() continue except ProcessExit as event: continue process = syscall_event.process syscall_state = process.syscall_state syscall = syscall_state.event(format_options) if syscall and syscall_state.next_event == "exit": # Syscall is about to be executed (just switched from "enter" to "exit") syscall_filter = SYSCALL_FILTERS[syscall.name] arguments = [parse_argument(argument) for argument in syscall.arguments] operation = syscall_filter.format(arguments) if operation is not None: operations.append(operation) return_value = syscall_filter.substitute(arguments) if return_value is not None: # Set invalid syscall number to prevent call execution process.setreg(SYSCALL_REGISTER, -1) # Substitute return value to make syscall appear to have succeeded process.setreg(RETURN_VALUE_REGISTER, return_value) process.syscall() return operations def main(argv=sys.argv): if len(argv) < 2: print(T.red("Error: No command given.")) print("Usage: %s COMMAND [ARGUMENT]..." % argv[0]) return 1 # This is basically "shlex.join" command = " ".join([(("'%s'" % arg) if (" " in arg) else arg) for arg in argv[1:]]) arguments = argv[1:] arguments[0] = locateProgram(arguments[0]) try: pid = createChild(arguments, False) except Exception as error: print(T.red("Error executing %s: %s." % (T.bold(command) + T.red, error))) return 1 debugger = PtraceDebugger() debugger.traceExec() try: debugger.traceFork() except DebuggerError: print(T.yellow("Warning: Running without traceFork support. " + "Syscalls from subprocesses can not be intercepted.")) process = debugger.addProcess(pid, True) prepareProcess(process) try: operations = get_operations(debugger) except Exception as error: print(T.red("Error tracing process: %s." % error)) return 1 except KeyboardInterrupt: print(T.yellow("%s terminated by keyboard interrupt." % (T.bold(command) + T.yellow))) return 2 finally: # Cut down all processes no matter what happens # to prevent them from doing any damage debugger.quit() if operations: print("%s has prevented %s from performing %d file system operations:\n" % (T.bold("maybe"), T.bold(command), len(operations))) for operation in operations: print(" " + operation) try: choice = input("\nDo you want to rerun %s and permit these operations? [y/N] " % T.bold(command)) except KeyboardInterrupt: choice = "" # Ctrl+C does not print a newline automatically print("") if choice.lower() == "y": subprocess.call(argv[1:]) else: print("%s has not detected any file system operations from %s." % (T.bold("maybe"), T.bold(command))) maybe-0.4.0/maybe/syscall_filters.py0000644000175000017500000003064012662015721017076 0ustar pewpew00000000000000# maybe - see what a program does before deciding whether you really want it to happen # # Copyright (c) 2016 Philipp Emanuel Weidmann # # Nemo vir est qui mundum non reddat meliorem. # # Released under the terms of the GNU General Public License, version 3 # (https://gnu.org/licenses/gpl.html) from pwd import getpwuid from grp import getgrgid from collections import namedtuple from os.path import abspath, dirname, basename, exists from os import O_WRONLY, O_RDWR, O_APPEND, O_CREAT, O_TRUNC from stat import S_IFCHR, S_IFBLK, S_IFIFO, S_IFSOCK from .utilities import T, format_permissions def format_delete(path): return "%s %s" % (T.red("delete"), T.underline(abspath(path))) def format_move(path_old, path_new): path_old = abspath(path_old) path_new = abspath(path_new) if dirname(path_old) == dirname(path_new): label = "rename" path_new = basename(path_new) else: label = "move" return "%s %s to %s" % (T.green(label), T.underline(path_old), T.underline(path_new)) def format_change_permissions(path, permissions): return "%s of %s to %s" % (T.yellow("change permissions"), T.underline(abspath(path)), T.bold(format_permissions(permissions))) def format_change_owner(path, owner, group): if owner == -1: label = "change group" owner = getgrgid(group)[0] elif group == -1: label = "change owner" owner = getpwuid(owner)[0] else: label = "change owner" owner = getpwuid(owner)[0] + ":" + getgrgid(group)[0] return "%s of %s to %s" % (T.yellow(label), T.underline(abspath(path)), T.bold(owner)) def format_create_directory(path): return "%s %s" % (T.cyan("create directory"), T.underline(abspath(path))) def format_create_link(path_source, path_target, symbolic): label = "create symbolic link" if symbolic else "create hard link" return "%s from %s to %s" % (T.cyan(label), T.underline(abspath(path_source)), T.underline(abspath(path_target))) # Start with a large number to avoid collisions with other FDs # TODO: This approach is extremely brittle! next_file_descriptor = 1000 file_descriptors = {} def get_next_file_descriptor(): global next_file_descriptor file_descriptor = next_file_descriptor next_file_descriptor += 1 return file_descriptor def get_file_descriptor_path(file_descriptor): return file_descriptors.get(file_descriptor, "/dev/fd/%d" % file_descriptor) allowed_files = set(["/dev/null", "/dev/zero", "/dev/tty"]) def format_open(path, flags): path = abspath(path) if path in allowed_files: return None elif (flags & O_CREAT) and not exists(path): return "%s %s" % (T.cyan("create file"), T.underline(path)) elif (flags & O_TRUNC) and exists(path): return "%s %s" % (T.red("truncate file"), T.underline(path)) else: return None def substitute_open(path, flags): path = abspath(path) if path in allowed_files: return None elif (flags & O_WRONLY) or (flags & O_RDWR) or (flags & O_APPEND) or (format_open(path, flags) is not None): # File might be written to later, so we need to track the file descriptor file_descriptor = get_next_file_descriptor() file_descriptors[file_descriptor] = path return file_descriptor else: return None def format_mknod(path, type): path = abspath(path) if exists(path): return None elif (type & S_IFCHR): label = "create character special file" elif (type & S_IFBLK): label = "create block special file" elif (type & S_IFIFO): label = "create named pipe" elif (type & S_IFSOCK): label = "create socket" else: # mknod(2): "Zero file type is equivalent to type S_IFREG" label = "create file" return "%s %s" % (T.cyan(label), T.underline(path)) def substitute_mknod(path, type): return None if (format_mknod(path, type) is None) else 0 def format_write(file_descriptor, byte_count): if file_descriptor in file_descriptors: path = file_descriptors[file_descriptor] return "%s %s to %s" % (T.red("write"), T.bold("%d bytes" % byte_count), T.underline(path)) else: return None def substitute_write(file_descriptor, byte_count): return None if (format_write(file_descriptor, byte_count) is None) else byte_count def substitute_dup(file_descriptor_old, file_descriptor_new=None): if file_descriptor_old in file_descriptors: if file_descriptor_new is None: file_descriptor_new = get_next_file_descriptor() # Copy tracked file descriptor file_descriptors[file_descriptor_new] = file_descriptors[file_descriptor_old] return file_descriptor_new else: return None SyscallFilter = namedtuple("SyscallFilter", ["name", "signature", "format", "substitute"]) # Make returning zero the default substitute function # Source: http://stackoverflow.com/a/18348004 SyscallFilter.__new__.__defaults__ = (lambda args: 0,) SYSCALL_FILTERS = [ # Delete SyscallFilter( name="unlink", signature=("int", (("const char *", "pathname"),)), format=lambda args: format_delete(args[0]), ), SyscallFilter( name="unlinkat", signature=("int", (("int", "dirfd"), ("const char *", "pathname"), ("int", "flags"),)), format=lambda args: format_delete(args[1]), ), SyscallFilter( name="rmdir", signature=("int", (("const char *", "pathname"),)), format=lambda args: format_delete(args[0]), ), # Move SyscallFilter( name="rename", signature=("int", (("const char *", "oldpath"), ("const char *", "newpath"),)), format=lambda args: format_move(args[0], args[1]), ), SyscallFilter( name="renameat", signature=("int", (("int", "olddirfd"), ("const char *", "oldpath"), ("int", "newdirfd"), ("const char *", "newpath"),)), format=lambda args: format_move(args[1], args[3]), ), SyscallFilter( name="renameat2", signature=("int", (("int", "olddirfd"), ("const char *", "oldpath"), ("int", "newdirfd"), ("const char *", "newpath"), ("unsigned int", "flags"),)), format=lambda args: format_move(args[1], args[3]), ), # Change permissions SyscallFilter( name="chmod", signature=("int", (("const char *", "pathname"), ("mode_t", "mode"),)), format=lambda args: format_change_permissions(args[0], args[1]), ), SyscallFilter( name="fchmod", signature=("int", (("int", "fd"), ("mode_t", "mode"),)), format=lambda args: format_change_permissions(get_file_descriptor_path(args[0]), args[1]), ), SyscallFilter( name="fchmodat", signature=("int", (("int", "dirfd"), ("const char *", "pathname"), ("mode_t", "mode"), ("int", "flags"),)), format=lambda args: format_change_permissions(args[1], args[2]), ), # Change owner SyscallFilter( name="chown", signature=("int", (("const char *", "pathname"), ("uid_t", "owner"), ("gid_t", "group"),)), format=lambda args: format_change_owner(args[0], args[1], args[2]), ), SyscallFilter( name="fchown", signature=("int", (("int", "fd"), ("uid_t", "owner"), ("gid_t", "group"),)), format=lambda args: format_change_owner(get_file_descriptor_path(args[0]), args[1], args[2]), ), SyscallFilter( name="lchown", signature=("int", (("const char *", "pathname"), ("uid_t", "owner"), ("gid_t", "group"),)), format=lambda args: format_change_owner(args[0], args[1], args[2]), ), SyscallFilter( name="fchownat", signature=("int", (("int", "dirfd"), ("const char *", "pathname"), ("uid_t", "owner"), ("gid_t", "group"), ("int", "flags"),)), format=lambda args: format_change_owner(args[1], args[2], args[3]), ), # Create directory SyscallFilter( name="mkdir", signature=("int", (("const char *", "pathname"), ("mode_t", "mode"),)), format=lambda args: format_create_directory(args[0]), ), SyscallFilter( name="mkdirat", signature=("int", (("int", "dirfd"), ("const char *", "pathname"), ("mode_t", "mode"),)), format=lambda args: format_create_directory(args[1]), ), # Create link SyscallFilter( name="link", signature=("int", (("const char *", "oldpath"), ("const char *", "newpath"),)), format=lambda args: format_create_link(args[1], args[0], False), ), SyscallFilter( name="linkat", signature=("int", (("int", "olddirfd"), ("const char *", "oldpath"), ("int", "newdirfd"), ("const char *", "newpath"), ("int", "flags"),)), format=lambda args: format_create_link(args[3], args[1], False), ), SyscallFilter( name="symlink", signature=("int", (("const char *", "target"), ("const char *", "linkpath"),)), format=lambda args: format_create_link(args[1], args[0], True), ), SyscallFilter( name="symlinkat", signature=("int", (("const char *", "target"), ("int", "newdirfd"), ("const char *", "linkpath"),)), format=lambda args: format_create_link(args[2], args[0], True), ), # Open/create file SyscallFilter( name="open", # TODO: "open" is overloaded (a version with 3 arguments also exists). Are both handled properly? signature=("int", (("const char *", "pathname"), ("int", "flags"),)), format=lambda args: format_open(args[0], args[1]), substitute=lambda args: substitute_open(args[0], args[1]), ), SyscallFilter( name="creat", signature=("int", (("const char *", "pathname"), ("mode_t", "mode"),)), format=lambda args: format_open(args[0], O_CREAT | O_WRONLY | O_TRUNC), substitute=lambda args: substitute_open(args[0], O_CREAT | O_WRONLY | O_TRUNC), ), SyscallFilter( name="openat", # TODO: "openat" is overloaded (see above) signature=("int", (("int", "dirfd"), ("const char *", "pathname"), ("int", "flags"),)), format=lambda args: format_open(args[1], args[2]), substitute=lambda args: substitute_open(args[1], args[2]), ), SyscallFilter( name="mknod", signature=("int", (("const char *", "pathname"), ("mode_t", "mode"),)), format=lambda args: format_mknod(args[0], args[1]), substitute=lambda args: substitute_mknod(args[0], args[1]), ), SyscallFilter( name="mknodat", signature=("int", (("int", "dirfd"), ("const char *", "pathname"), ("mode_t", "mode"),)), format=lambda args: format_mknod(args[1], args[2]), substitute=lambda args: substitute_mknod(args[1], args[2]), ), SyscallFilter( name="mkfifo", signature=("int", (("const char *", "pathname"), ("mode_t", "mode"),)), format=lambda args: format_mknod(args[0], S_IFIFO), substitute=lambda args: substitute_mknod(args[0], S_IFIFO), ), SyscallFilter( name="mkfifoat", signature=("int", (("int", "dirfd"), ("const char *", "pathname"), ("mode_t", "mode"),)), format=lambda args: format_mknod(args[1], S_IFIFO), substitute=lambda args: substitute_mknod(args[1], S_IFIFO), ), # Write to file SyscallFilter( name="write", signature=("ssize_t", (("int", "fd"), ("const void *", "buf"), ("size_t", "count"),)), format=lambda args: format_write(args[0], args[2]), substitute=lambda args: substitute_write(args[0], args[2]), ), SyscallFilter( name="pwrite", signature=("ssize_t", (("int", "fd"), ("const void *", "buf"), ("size_t", "count"), ("off_t", "offset"),)), format=lambda args: format_write(args[0], args[2]), substitute=lambda args: substitute_write(args[0], args[2]), ), SyscallFilter( name="writev", signature=("ssize_t", (("int", "fd"), ("const struct iovec *", "iov"), ("int", "iovcnt"),)), # TODO: Actual byte count is iovcnt * iov.iov_len format=lambda args: format_write(args[0], args[2]), substitute=lambda args: substitute_write(args[0], args[2]), ), SyscallFilter( name="pwritev", signature=("ssize_t", (("int", "fd"), ("const struct iovec *", "iov"), ("int", "iovcnt"), ("off_t", "offset"),)), # TODO: Actual byte count is iovcnt * iov.iov_len format=lambda args: format_write(args[0], args[2]), substitute=lambda args: substitute_write(args[0], args[2]), ), # Duplicate file descriptor SyscallFilter( name="dup", signature=("int", (("int", "oldfd"),)), format=lambda args: None, substitute=lambda args: substitute_dup(args[0]), ), SyscallFilter( name="dup2", signature=("int", (("int", "oldfd"), ("int", "newfd"),)), format=lambda args: None, substitute=lambda args: substitute_dup(args[0], args[1]), ), SyscallFilter( name="dup3", signature=("int", (("int", "oldfd"), ("int", "newfd"), ("int", "flags"),)), format=lambda args: None, substitute=lambda args: substitute_dup(args[0], args[1]), ), ] maybe-0.4.0/maybe/utilities.py0000644000175000017500000000303212675704201015704 0ustar pewpew00000000000000# maybe - see what a program does before deciding whether you really want it to happen # # Copyright (c) 2016 Philipp Emanuel Weidmann # # Nemo vir est qui mundum non reddat meliorem. # # Released under the terms of the GNU General Public License, version 3 # (https://gnu.org/licenses/gpl.html) from blessings import Terminal from ptrace.os_tools import RUNNING_LINUX from ptrace.cpu_info import CPU_POWERPC, CPU_ARM, CPU_I386, CPU_X86_64 T = Terminal() def format_permissions(permissions): result = "" for i in range(2, -1, -1): result += "r" if permissions & (4 * 8**i) else "-" result += "w" if permissions & (2 * 8**i) else "-" result += "x" if permissions & (1 * 8**i) else "-" return result # Based on python-ptrace's PtraceSyscall.readSyscall def get_syscall_register(): if CPU_POWERPC: return "gpr0" elif CPU_ARM: return "r7" elif RUNNING_LINUX: if CPU_X86_64: return "orig_rax" else: return "orig_eax" else: if CPU_X86_64: return "rax" else: return "eax" # Based on python-ptrace's PtraceSyscall.exit def get_return_value_register(): if CPU_ARM: return "r0" elif CPU_I386: return "eax" elif CPU_X86_64: return "rax" elif CPU_POWERPC: return "result" else: raise NotImplementedError("Unsupported CPU architecture") SYSCALL_REGISTER = get_syscall_register() RETURN_VALUE_REGISTER = get_return_value_register() maybe-0.4.0/PKG-INFO0000644000175000017500000000211512676016206013322 0ustar pewpew00000000000000Metadata-Version: 1.1 Name: maybe Version: 0.4.0 Summary: See what a program does before deciding whether you really want it to happen. Home-page: https://github.com/p-e-w/maybe Author: Philipp Emanuel Weidmann Author-email: pew@worldwidemann.com License: GPLv3 Description: For a detailed description, see https://github.com/p-e-w/maybe. Keywords: sandbox files access Platform: UNKNOWN Classifier: Development Status :: 3 - Alpha Classifier: Intended Audience :: End Users/Desktop Classifier: Intended Audience :: System Administrators Classifier: Topic :: Utilities Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3) Classifier: Operating System :: POSIX :: Linux Classifier: Operating System :: POSIX :: BSD :: FreeBSD Classifier: Operating System :: POSIX :: BSD :: OpenBSD Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 maybe-0.4.0/setup.cfg0000644000175000017500000000012412676016206014044 0ustar pewpew00000000000000[aliases] test = pytest [egg_info] tag_build = tag_date = 0 tag_svn_revision = 0 maybe-0.4.0/maybe.egg-info/0000755000175000017500000000000012676016206015015 5ustar pewpew00000000000000maybe-0.4.0/maybe.egg-info/entry_points.txt0000644000175000017500000000005412676016206020312 0ustar pewpew00000000000000[console_scripts] maybe = maybe.maybe:main maybe-0.4.0/maybe.egg-info/requires.txt0000644000175000017500000000004412676016206017413 0ustar pewpew00000000000000blessings==1.6 python-ptrace==0.8.1 maybe-0.4.0/maybe.egg-info/top_level.txt0000644000175000017500000000000612676016206017543 0ustar pewpew00000000000000maybe maybe-0.4.0/maybe.egg-info/SOURCES.txt0000644000175000017500000000041712676016206016703 0ustar pewpew00000000000000setup.cfg setup.py maybe/__init__.py maybe/maybe.py maybe/syscall_filters.py maybe/utilities.py maybe.egg-info/PKG-INFO maybe.egg-info/SOURCES.txt maybe.egg-info/dependency_links.txt maybe.egg-info/entry_points.txt maybe.egg-info/requires.txt maybe.egg-info/top_level.txtmaybe-0.4.0/maybe.egg-info/dependency_links.txt0000644000175000017500000000000112676016206021063 0ustar pewpew00000000000000 maybe-0.4.0/maybe.egg-info/PKG-INFO0000644000175000017500000000211512676016206016111 0ustar pewpew00000000000000Metadata-Version: 1.1 Name: maybe Version: 0.4.0 Summary: See what a program does before deciding whether you really want it to happen. Home-page: https://github.com/p-e-w/maybe Author: Philipp Emanuel Weidmann Author-email: pew@worldwidemann.com License: GPLv3 Description: For a detailed description, see https://github.com/p-e-w/maybe. Keywords: sandbox files access Platform: UNKNOWN Classifier: Development Status :: 3 - Alpha Classifier: Intended Audience :: End Users/Desktop Classifier: Intended Audience :: System Administrators Classifier: Topic :: Utilities Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3) Classifier: Operating System :: POSIX :: Linux Classifier: Operating System :: POSIX :: BSD :: FreeBSD Classifier: Operating System :: POSIX :: BSD :: OpenBSD Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 maybe-0.4.0/setup.py0000644000175000017500000000305012676014241013733 0ustar pewpew00000000000000# Based on setup.py from https://github.com/pypa/sampleproject from setuptools import setup setup( name="maybe", version="0.4.0", description="See what a program does before deciding whether you really want it to happen.", long_description="For a detailed description, see https://github.com/p-e-w/maybe.", url="https://github.com/p-e-w/maybe", author="Philipp Emanuel Weidmann", author_email="pew@worldwidemann.com", license="GPLv3", classifiers=[ "Development Status :: 3 - Alpha", "Intended Audience :: End Users/Desktop", "Intended Audience :: System Administrators", "Topic :: Utilities", "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", "Operating System :: POSIX :: Linux", "Operating System :: POSIX :: BSD :: FreeBSD", "Operating System :: POSIX :: BSD :: OpenBSD", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", ], keywords="sandbox files access", packages=["maybe"], install_requires=[ "blessings==1.6", "python-ptrace==0.8.1", ], setup_requires=[ "pytest-runner>=2.7", ], tests_require=[ "pytest>=2.9.1", ], entry_points={ "console_scripts": [ "maybe = maybe.maybe:main", ], }, )