maybe-0.3.0/0000755000175000017500000000000012655701374012231 5ustar pewpew00000000000000maybe-0.3.0/maybe/0000755000175000017500000000000012655701374013326 5ustar pewpew00000000000000maybe-0.3.0/maybe/__init__.py0000644000175000017500000000000012651175721015422 0ustar pewpew00000000000000maybe-0.3.0/maybe/maybe.py0000644000175000017500000001372012655670525015003 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 sys import argv, exit from subprocess import call 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 # 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(): if len(argv) < 2: print(T.red("Error: No command given.")) print("Usage: %s COMMAND [ARGUMENT]..." % argv[0]) exit(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))) exit(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)) exit(1) except KeyboardInterrupt: print(T.yellow("%s terminated by keyboard interrupt." % (T.bold(command) + T.yellow))) exit(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 = "" if choice.lower() == "y": call(argv[1:]) else: print("%s has not detected any file system operations from %s." % (T.bold("maybe"), T.bold(command))) maybe-0.3.0/maybe/syscall_filters.py0000644000175000017500000002656112655675122017115 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 import O_WRONLY, O_RDWR, O_APPEND, O_CREAT, O_TRUNC from os.path import abspath, dirname, basename, exists from .utilities import T, format_permissions def return_zero(args): return 0 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_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"]) SYSCALL_FILTERS = [ # Delete SyscallFilter( name="remove", signature=("int", (("const char *", "pathname"),)), format=lambda args: format_delete(args[0]), substitute=return_zero ), SyscallFilter( name="unlink", signature=("int", (("const char *", "pathname"),)), format=lambda args: format_delete(args[0]), substitute=return_zero ), SyscallFilter( name="unlinkat", signature=("int", (("int", "dirfd"), ("const char *", "pathname"), ("int", "flags"),)), format=lambda args: format_delete(args[1]), substitute=return_zero ), SyscallFilter( name="rmdir", signature=("int", (("const char *", "pathname"),)), format=lambda args: format_delete(args[0]), substitute=return_zero ), # Move SyscallFilter( name="rename", signature=("int", (("const char *", "oldpath"), ("const char *", "newpath"),)), format=lambda args: format_move(args[0], args[1]), substitute=return_zero ), SyscallFilter( name="renameat", signature=("int", (("int", "olddirfd"), ("const char *", "oldpath"), ("int", "newdirfd"), ("const char *", "newpath"),)), format=lambda args: format_move(args[1], args[3]), substitute=return_zero ), 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]), substitute=return_zero ), # Change permissions SyscallFilter( name="chmod", signature=("int", (("const char *", "pathname"), ("mode_t", "mode"),)), format=lambda args: format_change_permissions(args[0], args[1]), substitute=return_zero ), SyscallFilter( name="fchmod", signature=("int", (("int", "fd"), ("mode_t", "mode"),)), format=lambda args: format_change_permissions(get_file_descriptor_path(args[0]), args[1]), substitute=return_zero ), 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]), substitute=return_zero ), # 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]), substitute=return_zero ), 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]), substitute=return_zero ), 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]), substitute=return_zero ), 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]), substitute=return_zero ), # Create directory SyscallFilter( name="mkdir", signature=("int", (("const char *", "pathname"), ("mode_t", "mode"),)), format=lambda args: format_create_directory(args[0]), substitute=return_zero ), SyscallFilter( name="mkdirat", signature=("int", (("int", "dirfd"), ("const char *", "pathname"), ("mode_t", "mode"),)), format=lambda args: format_create_directory(args[1]), substitute=return_zero ), # Create link SyscallFilter( name="link", signature=("int", (("const char *", "oldpath"), ("const char *", "newpath"),)), format=lambda args: format_create_link(args[1], args[0], False), substitute=return_zero ), 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), substitute=return_zero ), SyscallFilter( name="symlink", signature=("int", (("const char *", "target"), ("const char *", "linkpath"),)), format=lambda args: format_create_link(args[1], args[0], True), substitute=return_zero ), SyscallFilter( name="symlinkat", signature=("int", (("const char *", "target"), ("int", "newdirfd"), ("const char *", "linkpath"),)), format=lambda args: format_create_link(args[2], args[0], True), substitute=return_zero ), # 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]) ), # Write to file # TODO: Handle "fwrite"? 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.3.0/maybe/utilities.py0000644000175000017500000000303212651174210015676 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.3.0/PKG-INFO0000644000175000017500000000217712655701374013335 0ustar pewpew00000000000000Metadata-Version: 1.1 Name: maybe Version: 0.3.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.2 Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 maybe-0.3.0/setup.cfg0000644000175000017500000000007312655701374014052 0ustar pewpew00000000000000[egg_info] tag_build = tag_date = 0 tag_svn_revision = 0 maybe-0.3.0/maybe.egg-info/0000755000175000017500000000000012655701374015020 5ustar pewpew00000000000000maybe-0.3.0/maybe.egg-info/entry_points.txt0000644000175000017500000000005412655701374020315 0ustar pewpew00000000000000[console_scripts] maybe = maybe.maybe:main maybe-0.3.0/maybe.egg-info/requires.txt0000644000175000017500000000003012655701374017411 0ustar pewpew00000000000000blessings python-ptrace maybe-0.3.0/maybe.egg-info/top_level.txt0000644000175000017500000000000612655701374017546 0ustar pewpew00000000000000maybe maybe-0.3.0/maybe.egg-info/SOURCES.txt0000644000175000017500000000040512655701374016703 0ustar pewpew00000000000000setup.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.3.0/maybe.egg-info/dependency_links.txt0000644000175000017500000000000112655701374021066 0ustar pewpew00000000000000 maybe-0.3.0/maybe.egg-info/PKG-INFO0000644000175000017500000000217712655701374016124 0ustar pewpew00000000000000Metadata-Version: 1.1 Name: maybe Version: 0.3.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.2 Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 maybe-0.3.0/setup.py0000644000175000017500000000276312655677505013763 0ustar pewpew00000000000000# Based on setup.py from https://github.com/pypa/sampleproject from setuptools import setup, find_packages setup( name="maybe", version="0.3.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.2", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", ], keywords="sandbox files access", packages=find_packages(), install_requires=[ "blessings", "python-ptrace", ], entry_points={ "console_scripts": [ "maybe = maybe.maybe:main", ], }, )