diff --git a/README.md b/README.md index 3ea07cf..9ee83af 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,41 @@ # secimport +

secimport

-Secure import for python modules using dtrace under the hood.
+An easy way to constrain python modules in your code using backends like bpftrace (eBPF) and dtrace.
Medium Article

`secimport` can be used to: - Confine/Restrict specific python modules inside your production environment. - - Open Source, 3rd party from unstrusted sources. - - Audit the flow of your python application at user-space/os/kernel level. -- Run an entire python application under unified configuration - - Like `seccomp` but not limited to Linux kernels. Cross platform. - + - Restrict 3rd party or open source modules in your code. +- Audit the flow of your python application at user-space/os/kernel level. +- Kill the process upon violoation of a profile. # Quick Start `secimport` can be used out of the box in the following ways: -1. Inside your code using `module = secimport.secure_import('module_name', ...)`. - - Replacing the regular `import` statement with `secure_import` - - Only modules that were imported with `secure_import` will be traced. -2. As a sandbox, by specifying the modules and their policies. - - Use this repository to: - - Generate a YAML policy from your code - - Compile that YAML to dscript. - - Use `dtrace` command to run your main python application, with your tailor-made sandbox. - - No need for `secure_import`, you can keep using regular `import`s +1. Modify your imports + - Inside your code using `module = secimport.secure_import('module_name', ...)`. + - Replacing the regular `import` statement with `secure_import` + - Only modules that were imported with `secure_import` will be traced. +2. As a sandbox that runs your main code. + 1. Generate a YAML policy from your code, by specifying the modules and the policy you want for each module you use. + 2. Convert that YAML policy to dscript/bpftrace sandbox code. + 3. Use `dtrace` or `ebpf` to run your main python application, with your tailor-made sandbox. + - No need for `secure_import`, you can keep using regular `import`s and not change your code at all. For the full list of examples, see EXAMPLES.md. -# Pickle Example +# Docker +A working environment is not easy to create.
+The easiest way to evaluate secimport, is by using our Docker for MacOS and Linux that includes secimport, bpftrace backend and eBPF libraries.
+dtrace backend is not available in docker, and can be tried directly on the compatible hosts (MacOS, Solaris, Unix, some Linux distributions) + +# Use Cases + ### How pickle can be exploited in your 3rd party packages: ```python >>> import pickle @@ -72,7 +77,7 @@ $ less /tmp/.secimport/sandbox_pickle.log : ``` -## YAML Template Example +## YAML Policy Example For a full tutorial, see YAML Profiles Usage ```shell # An example yaml template for a sandbox. @@ -102,7 +107,7 @@ modules: ``` -## Python Processing Example +## Blocking New Processes Example ```python Python 3.10.0 (default, May 2 2022, 21:43:20) [Clang 13.0.0 (clang-1300.0.27.3)] on darwin Type "help", "copyright", "credits" or "license" for more information. @@ -129,8 +134,12 @@ Type "help", "copyright", "credits" or "license" for more information. # Damn! That's cool. ``` -- The dtrace profile for the module is saved under: - - `/tmp/.secimport/sandbox_subprocess.d`: +When using secure_import, the following files are created: +- The dtrace/bpftrace sandbox code for the module is saved under: + - `/tmp/.secimport/sandbox_subprocess.d` + - when using dtrace + - `/tmp/.secimport/sandbox_subprocess.bt`: + - when using bpftrace - The log file for this module is under - `/tmp/.secimport/sandbox_subprocess.log`: ```shell @@ -216,8 +225,9 @@ Not related for python, but for the sake of explanation (Equivilant Demo soon). - Tracing Guides - F.A.Q - Installation -- Mac OS Users - Disabling SIP for dtrace +- Mac OS Users - Disabling SIP (System Intergity Protection) - https://www.brendangregg.com/DTrace/DTrace-cheatsheet.pdf +- https://www.brendangregg.com/blog/2018-10-08/dtrace-for-linux-2018.html

## TODO: @@ -226,6 +236,14 @@ Not related for python, but for the sake of explanation (Equivilant Demo soon). - ✔️ Use secimport to compile that yml - ✔️ Create a single dcript policy - ✔️ Run an application with that policy using dtrace, without using `secure_import` -- Node support (dtrace hooks) -- Go support (dtrace hooks) -- Use current_module_str together with thread ID +- ✔️ Add eBPF basic support using bpftrace + - ✔️ bpftrace backend tests +- Extandible Language Template + - Increase extandability for new languages tracing with bpftace/dtrace. + - Adding a new integration will be easy, in a single directory, using templates for filters, actions, etc. +- Node support (bpftrace/dtrace hooks) + - Implement a template for Node's call stack and event loop +- Go support (bpftrace/dtrace hooks) + - Implement a template for golang's call stack +- Multi Process support: Use current_module_str together with thread ID to distinguish between events in different processes +- Update all linux syscalls in the templates (filesystem, networking, processing) to improve the sandbox blocking of unknowns. diff --git a/docker/README.md b/docker/README.md new file mode 100755 index 0000000..8f1a98c --- /dev/null +++ b/docker/README.md @@ -0,0 +1,30 @@ +# Try secimport with bpftrace + +## How to Use + +1. Install Docker: https://docs.docker.com/get-docker +2. ./build.sh + - will build a docker image with + - python with dtrace static USDT instrumentations + - bpftrace + - secimport code + - ~1GB in size +3. ./run.sh + - Runs temporary example sandbox using bpftrace + - Then, it will execute os.system('ps'). + - the process should be killed. + - Once the process is killed, it prints the logs of the sandbox. + + +## FAQ + +### How it runs on macOS? +- The Docker for mac runs Linux on a hypervisor called hyperkit, and docker runs inside it, so you can use Linux features. + +### Can we trace a macOS host with this docker? +- Not at the moment. The bpftrace runs inside a Linux VM. +- For macOS, there is dtrace. + +===================== + +Based on the great example repo: https://github.com/mmisono/try-bpftrace-in-mac diff --git a/docker/build.sh b/docker/build.sh new file mode 100755 index 0000000..b540ac5 --- /dev/null +++ b/docker/build.sh @@ -0,0 +1,26 @@ +#!/bin/bash + +if [[ "$PWD" =~ docker$ ]] +then + echo "Building secimport docker container..."; +else + echo "Please run this script from the secimport/docker directory."; + exit 1; +fi + +# linukit kernel version +KERNEL_VERSION=`docker run --rm -it alpine uname -r | cut -d'-' -f1` +BPFTRACE_VERSION=${BPFTRACE_VERSION:-v0.16.0} +PYTHON_VERSION=${PYTHON_VERSION:-"3.10.0"} + +pushd docker + +docker build \ + --build-arg KERNEL_VERSION=${KERNEL_VERSION} \ + --build-arg BPFTRACE_VERSION=${BPFTRACE_VERSION} \ + --build-arg PYTHON_VERSION=${PYTHON_VERSION} \ + -t secimport:${KERNEL_VERSION} . + +popd + +echo "You can now use the ./run.sh script to try secimport." \ No newline at end of file diff --git a/docker/docker/Dockerfile b/docker/docker/Dockerfile new file mode 100755 index 0000000..cf653fc --- /dev/null +++ b/docker/docker/Dockerfile @@ -0,0 +1,38 @@ +ARG KERNEL_VERSION + +FROM linuxkit/kernel:${KERNEL_VERSION} as ksrc +FROM ubuntu:20.04 AS build + +ARG BPFTRACE_VERSION +ARG PYTHON_VERSION + +WORKDIR /kernel +COPY --from=ksrc /kernel-dev.tar . +RUN tar xf kernel-dev.tar + +WORKDIR /workspace +ARG DEBIAN_FRONTEND=noninteractive + +# TODO: add openssl (longer build time, but pip will work for our interpreter) +RUN echo "Installing prerequisites" && \ + apt-get update && apt-get install sudo build-essential libncurses5-dev libgdbm-dev libnss3-dev libssl-dev libreadline-dev libffi-dev curl wget auditd vim tmux git binutils unzip gcc systemtap-sdt-dev cmake zlib1g-dev -y +RUN echo "Installing python with dtrace" && \ + curl -o Python-${PYTHON_VERSION}.tgz https://www.python.org/ftp/python/${PYTHON_VERSION}/Python-${PYTHON_VERSION}.tgz && tar -xzf Python-${PYTHON_VERSION}.tgz && \ + cd Python-${PYTHON_VERSION} && ./configure --with-dtrace --prefix=/usr/local/openssl --prefix=$(pwd) --with-ensurepip=install && make && make install +RUN echo "Installing bpftrace" && \ + wget https://github.com/iovisor/bpftrace/releases/download/${BPFTRACE_VERSION}/bpftrace && \ + chmod +x bpftrace && \ + mv bpftrace /bin && \ + wget https://github.com/iovisor/bpftrace/archive/${BPFTRACE_VERSION}.zip && \ + unzip ${BPFTRACE_VERSION}.zip && \ + cp -r bpftrace*/tools /workspace/bpftrace/ && \ + echo "Done building bpftrace" && \ + mv /kernel/usr/src/linux-headers* /kernel/usr/src/linux-headers + +ENV BPFTRACE_KERNEL_SOURCE=/kernel/usr/src/linux-headers +COPY setup.sh . +COPY sandbox.bt . +COPY run_sandbox.sh . +RUN chmod 755 sandbox.bt run_sandbox.sh + +ENTRYPOINT ["/bin/sh", "/workspace/setup.sh"] diff --git a/docker/docker/run_sandbox.sh b/docker/docker/run_sandbox.sh new file mode 100755 index 0000000..461a4b2 --- /dev/null +++ b/docker/docker/run_sandbox.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +# "--unsafe" is required to run system command for remediation. +# If process termination on violoation is not needed, +# You can remove this argument. + +echo "Starting secimport sandbox with bpftrace backend, the sandbox should kill the python process..." +bpftrace -c "/workspace/Python-3.10.0/python -c __import__('os').system('ps')" -o sandbox.log sandbox.bt --unsafe || echo "The process was killed, as expected." +echo "The sandbox bpftrace code is at sandbox.bt" +echo "The sandbox log is at sandbox.log" +# tail -n 20 sandbox.log +# less +G sandbox.log \ No newline at end of file diff --git a/docker/docker/sandbox.bt b/docker/docker/sandbox.bt new file mode 100755 index 0000000..890e321 --- /dev/null +++ b/docker/docker/sandbox.bt @@ -0,0 +1,32 @@ +#!/usr/bin/env bpftrace + +BEGIN { + printf("STARTED\n") +} + + +usdt:/workspace/Python-3.10.0/python:function__entry { + @["depth"]++; + @entrypoints[str(arg0)] = @["depth"]; + @globals["previous_module"] = @globals["current_module"]; + @globals["current_module"] = str(arg0); + printf("%s, %s, depth=%d\n", str(arg0), str(arg1), @["depth"]) ; +} + +usdt:/workspace/Python-3.10.0/python:function__return { + @["depth"]--; +} + +tracepoint:raw_syscalls:sys_enter /comm == "python"/ { + if(args->id == 59){ + printf("KILLING PROCESS %s - EXECUTED execve;\n", str(pid)); + system("pkill -9 args"); // optional + printf("Killed process %s", str(pid)); + exit(); // optional + } + printf("%s SYSCALL %ld depth=%d previous=%s current=%s \n", probe, args->id, @["depth"], @globals["previous_module"], @globals["current_module"] ); +} + +END { + clear(@entrypoints); +} diff --git a/docker/docker/setup.sh b/docker/docker/setup.sh new file mode 100755 index 0000000..d0d912d --- /dev/null +++ b/docker/docker/setup.sh @@ -0,0 +1,7 @@ +#!/bin/sh +mount -t debugfs none /sys/kernel/debug/ +sysctl -w kernel.kptr_restrict=0 >/dev/null 2>&1 +sysctl -w kernel.perf_event_paranoid=2 >/dev/null 2>&1 +cd /workspace/ +/bin/bash +# /bin/bash ./run_sandbox.sh \ No newline at end of file diff --git a/docker/run.sh b/docker/run.sh new file mode 100755 index 0000000..dcbaef6 --- /dev/null +++ b/docker/run.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +if [[ "$PWD" =~ docker$ ]] +then + echo "Running secimport docker container..."; +else + echo "Please run this script from the secimport/docker directory."; + exit 1; +fi + +KERNEL_VERSION=`docker run --rm -it alpine uname -r | cut -d'-' -f1` + +cd .. # back to repo root dir +docker run --rm --name=secimport --privileged -v "$(pwd)/src/secimport":"/workspace/secimport/" -it secimport:${KERNEL_VERSION} diff --git a/docs/TRACING_PROCESSES.md b/docs/TRACING_PROCESSES.md index 34b28c4..7f88e26 100644 --- a/docs/TRACING_PROCESSES.md +++ b/docs/TRACING_PROCESSES.md @@ -10,12 +10,14 @@ There are several ways to create a secimport profile for your modules. - `sudo dtrace -s src/secimport/templates/default.allowlist.template.d -c "python -m http.server"` - CTRL+C - Create a secure import based on that log. - - Using simple `dtrace` + - Using `bpftrace` + - See https://github.com/iovisor/bpftrace/tree/master/tools + - Using `dtrace` - Tracing the syscalls of a process with pid `12345` - `dtrace -n 'syscall::: /pid == ($1)/ {@[pid,execname,probefunc]=count()}' 12345` - Tracing the syscalls of a docker container with pid `12345` - `dtrace -n 'syscall::: /progenyof($1)/ {@[pid,execname,probefunc]=count()}' 12345` - - Using `strace` + - Using an `strace` script I contributed to FireJail - A script to list all your application's syscalls using `strace`.
I contributed it to `firejail` a few years ago: - https://github.com/netblue30/firejail/blob/master/contrib/syscalls.sh - ``` diff --git a/examples/run_bpftrace_example.sh b/examples/run_bpftrace_example.sh new file mode 100644 index 0000000..404573b --- /dev/null +++ b/examples/run_bpftrace_example.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash + +# "--unsafe" is required to run system command for remediation. +# If process termination on violoation is not needed, +# You can remove this argument. + +echo "Starting secimport sandbox with python shell..." +bpftrace -c "/workspace/Python-3.10.0/python -c __import__('os').system('ps')" -o sandbox.log sandbox.bt --unsafe + +# The process is killed becused we ran os.system inside our sandbox. +# Watch the logs: +less +G sandbox.log +# OR: +# tail -n 20 sandbox.log +echo "The sandbox log is at ./sandbox.log" \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 952e4ef..0a7ff5d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "secimport" -version = "0.4.2" +version = "0.5.0" description = "A sandbox/supervisor for python modules." authors = ["Avi Lumelsky"] license = "MIT" diff --git a/src/secimport/backends/bpftrace_backend/__init__.py b/src/secimport/backends/bpftrace_backend/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/secimport/backends/bpftrace_backend/bpftrace_backend.py b/src/secimport/backends/bpftrace_backend/bpftrace_backend.py new file mode 100644 index 0000000..3dc4373 --- /dev/null +++ b/src/secimport/backends/bpftrace_backend/bpftrace_backend.py @@ -0,0 +1,330 @@ +""" +Import python modules with bpftrace supervision. +This is a direct copy/pase from dtrace backend, +TODO: abstract base class for backend, inclusing this supervisor/sandbox backend API. + +Copyright (c) 2022 Avi Lumelsky +""" + +import importlib +import os +from sys import executable as PYTHON_EXECUTABLE +import stat +import time +from pathlib import Path +from typing import List + +from secimport.backends.common.instrumentation_backend import InstrumentationBackend +from secimport.backends.common.utils import ( + BASE_DIR_NAME, + SECIMPORT_ROOT, + render_syscalls_filter, +) + +TEMPLATES_DIR_NAME = SECIMPORT_ROOT / "templates" / "bpftrace" + + +def create_bpftrace_script_for_module( + module_name: str, + allow_shells: bool, + allow_networking: bool, + log_python_calls: bool, + log_syscalls: bool, + log_network: bool, + log_file_system: bool, + destructive: bool, + syscalls_allowlist: List[str] = None, + syscalls_blocklist: List[str] = None, + templates_dir: Path = TEMPLATES_DIR_NAME, +) -> Path: + """ + # Template components available at the moment: + # ###DESTRUCTIVE### + # ###FUNCTION_ENTRY### + # ###FUNCTION_EXIT### + # ###SYSCALL_ENTRY### + # ###MODULE_NAME### + """ + + module = importlib.machinery.PathFinder().find_spec(module_name) + if module is None: + raise ModuleNotFoundError(module) + module_traced_name = module.origin # e.g this.py + + assert not ( + syscalls_allowlist is not None and syscalls_blocklist is not None + ), "Please specify either syscalls_allowlist OR syscalls_blocklist." + + # If we have an allowlist + if syscalls_allowlist is not None: + script_template = render_allowlist_template( + module_name=module_name, + destructive=destructive, + syscalls_allowlist=syscalls_allowlist, + templates_dir=templates_dir, + ) + elif syscalls_blocklist is not None: + script_template = render_blocklist_template( + module_name=module_name, + destructive=destructive, + syscalls_blocklist=syscalls_blocklist, + templates_dir=templates_dir, + ) + else: + script_template = render_bpftrace_template( + module_traced_name=module_traced_name, + allow_shells=allow_shells, + allow_networking=allow_networking, + log_python_calls=log_python_calls, + log_syscalls=log_syscalls, + log_network=log_network, + log_file_system=log_file_system, + destructive=destructive, + templates_dir=templates_dir, + ) + + # Creating a dscript file with the modified template + if not os.path.exists(BASE_DIR_NAME): + os.mkdir(BASE_DIR_NAME) + + module_file_name = os.path.join(BASE_DIR_NAME, f"bpftrace_sandbox_{module_name}.bt") + with open(module_file_name, "w") as module_file: + module_file.write(script_template) + + # TODO: FIGURE OUT A WAY TO COMPILE WITHOUT EXECUTING - MSKING SURE THE GENERATION WORKED SYNTAX-WISE. + return module_file_name + + +def run_bpftrace_script_for_module( + module_name: str, + allow_shells: bool, + allow_networking: bool, + log_python_calls: bool, + log_syscalls: bool, + log_network: bool, + log_file_system: bool, + destructive: bool, + syscalls_allowlist: List[str], + syscalls_blocklist: List[str], + use_sudo: bool = False, + templates_dir: Path = TEMPLATES_DIR_NAME, +): + module_file_path = create_bpftrace_script_for_module( + module_name=module_name, + allow_shells=allow_shells, + allow_networking=allow_networking, + log_python_calls=log_python_calls, + log_syscalls=log_syscalls, + log_network=log_network, + log_file_system=log_file_system, + destructive=destructive, + syscalls_allowlist=syscalls_allowlist, + syscalls_blocklist=syscalls_blocklist, + templates_dir=templates_dir, + ) + output_file = BASE_DIR_NAME / f"bpftrace_sandbox_{module_name}.log" + current_pid = os.getpid() + bpftrace_command = f'{"sudo " if use_sudo else ""} {module_file_path} --unsafe -p {current_pid} -o {output_file} &2>/dev/null' + st = os.stat(module_file_path) + os.chmod(module_file_path, st.st_mode | stat.S_IEXEC) + print("(running bpftrace supervisor): ", bpftrace_command) + os.system(bpftrace_command) + time.sleep(5) # TODO: change from 5 seconds (wait) to fd creation (event) + return True + + +def render_allowlist_template( + module_name: str, + destructive: bool, + syscalls_allowlist: List[str], + templates_dir: Path = TEMPLATES_DIR_NAME, +): + return render_bpftrace_probe_for_module( + module_name=module_name, + destructive=destructive, + syscalls_list=syscalls_allowlist, + syscalls_allow=True, + templates_dir=templates_dir, + ) + + +def render_blocklist_template( + module_name: str, + destructive: bool, + syscalls_blocklist: List[str], + templates_dir: Path = TEMPLATES_DIR_NAME, +): + return render_bpftrace_probe_for_module( + module_name=module_name, + destructive=destructive, + syscalls_list=syscalls_blocklist, + syscalls_allow=False, + templates_dir=templates_dir, + ) + + +def render_bpftrace_template( + module_traced_name: str, + allow_shells: bool, + allow_networking: bool, + log_python_calls: bool, + log_syscalls: bool, + log_network: bool, + log_file_system: bool, + destructive: bool, + templates_dir: Path = TEMPLATES_DIR_NAME, + default_template_filename: str = "default.template.bt", + interpreter_path: str = PYTHON_EXECUTABLE, +): + script_template = open( + templates_dir / default_template_filename, + "r", + ).read() + + # Updating the right binary to be probed by bpftrace + script_template = script_template.replace( + "###INTERPRETER_PATH###", interpreter_path + ) + + # PYTHON instrumentations + code_syscall_entry = "" + if log_python_calls is True: + code_function_entry = open( + templates_dir / "actions/log_python_module_entry.bt", + "r", + ).read() + code_function_exit = open( + templates_dir / "actions/log_python_module_exit.bt", + "r", + ).read() + script_template = script_template.replace( + "###FUNCTION_ENTRY###", code_function_entry + ) + script_template = script_template.replace( + "###FUNCTION_EXIT###", code_function_exit + ) + else: + script_template = script_template.replace("###FUNCTION_ENTRY###", "") + script_template = script_template.replace("###FUNCTION_EXIT###", "") + + # SYSCALLS instrumentations + if log_syscalls is True: + _code = open( + templates_dir / "actions/log_syscall.bt", + "r", + ).read() + code_syscall_entry += f"{_code};\n" + + if log_file_system is True: + filter_fs_code = open( + templates_dir / "filters/file_system.bt", + "r", + ).read() + code_syscall_entry += f"{filter_fs_code}\n{{" + action_log_file_system = open( + templates_dir / "actions/log_file_system.bt", + "r", + ).read() + code_syscall_entry += f"{action_log_file_system}}}\n" + + if allow_networking is False or log_network is True: + filter_networking_code = open( + templates_dir / "filters/networking.bt", + "r", + ).read() + code_syscall_entry += f"{filter_networking_code}{{\n" + + if log_network is True: + action_log_network = open( + templates_dir / "actions/log_network.bt", + "r", + ).read() + code_syscall_entry += f"{action_log_network}\n" + + if allow_networking is False: + action_kill = open( + templates_dir / "actions/kill_process.bt", + "r", + ).read() + code_syscall_entry += f"{action_kill}\n" + code_syscall_entry += "}\n" + + if allow_shells is False: + filter_processes_code = open( + templates_dir / "filters/processes.bt", + "r", + ).read() + code_syscall_entry += f"{filter_processes_code}{{\n" + action_kill_on_processing = open( + templates_dir / "actions/kill_on_processing.bt", + "r", + ).read() + code_syscall_entry += f"{action_kill_on_processing}}}\n" + + script_template = script_template.replace("###SYSCALL_ENTRY###", code_syscall_entry) + script_template = script_template.replace("###MODULE_NAME###", module_traced_name) + + # Updating the destructive behavior + script_template = script_template.replace( + "###DESTRUCTIVE###", "1" if destructive else "0" + ) + return script_template + + +def render_bpftrace_probe_for_module( + module_name: str, + destructive: bool, + syscalls_list: List[str], + syscalls_allow: bool, + templates_dir: Path = TEMPLATES_DIR_NAME, +) -> str: + # Loading the probe allowlist template + probe_template = open( + templates_dir / "probes/module_syscalls_allowlist_template.bt", + "r", + ).read() + + # Adding a syscalls filter + syscalls_filter = render_syscalls_filter( + syscalls_list=syscalls_list, + allow=syscalls_allow, + instrumentation_backend=InstrumentationBackend.EBPF, + ) + probe_template = probe_template.replace("###SYSCALL_FILTER###", syscalls_filter) + + # Adding a probe filter for the specified python module + supervision_filter = open( + templates_dir / "filters/is_current_module_under_supervision.bt", + "r", + ).read() + + # Adding the action according to the 'destructive' flag: kill if destructive, log otherwise + supervision_action = "" + if destructive is True: + action_kill_process = open( + templates_dir / "actions/kill_process.bt", + "r", + ).read() + supervision_action = f"{{{action_kill_process}}}\n" + else: + action_log_syscall = open( + templates_dir / "actions/log_syscall.bt", + "r", + ).read() + supervision_action = f"{{{action_log_syscall}}}\n" + + # Updating the template with the filters, their actions, and module name + probe_template = probe_template.replace( + "###SUPERVISED_MODULES_FILTER###", supervision_filter + ) + probe_template = probe_template.replace( + "###SUPERVISED_MODULES_ACTION###", supervision_action + ) + probe_template = probe_template.replace("###MODULE_NAME###", module_name) + + # Updating the destructive behavior + probe_template = probe_template.replace( + "###DESTRUCTIVE###", "true" if destructive else "false" + ) + + return probe_template diff --git a/src/secimport/backends/common/__init__.py b/src/secimport/backends/common/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/secimport/backends/common/instrumentation_backend.py b/src/secimport/backends/common/instrumentation_backend.py new file mode 100644 index 0000000..1f92dcd --- /dev/null +++ b/src/secimport/backends/common/instrumentation_backend.py @@ -0,0 +1,6 @@ +from enum import Enum + + +class InstrumentationBackend(Enum): + DTRACE = "dtrace" + EBPF = "ebpf" # bpftrace diff --git a/src/secimport/backends/common/utils.py b/src/secimport/backends/common/utils.py new file mode 100644 index 0000000..a3a65f9 --- /dev/null +++ b/src/secimport/backends/common/utils.py @@ -0,0 +1,55 @@ +import os +from sys import platform +from pathlib import Path +from typing import List + +from secimport.backends.common.instrumentation_backend import InstrumentationBackend + + +BASE_DIR_NAME = Path("/tmp/.secimport") +SECIMPORT_ROOT = Path( + os.path.realpath( + os.path.split(__file__)[:-1][0] + os.sep + os.pardir + os.sep + os.pardir + ) +) +DEFAULT_BACKEND = None + +if "linux" in platform.lower(): + TEMPLATES_DIR_NAME = SECIMPORT_ROOT / "templates" / "bpftrace" + DEFAULT_BACKEND = InstrumentationBackend.EBPF + # TODO: verify bpftrace is installed, if not, link to the repo documentation on how to install. +else: + TEMPLATES_DIR_NAME = SECIMPORT_ROOT / "templates" / "dtrace" + DEFAULT_BACKEND = InstrumentationBackend.DTRACE + +PROFILES_DIR_NAME = SECIMPORT_ROOT / "profiles" + + +def render_syscalls_filter( + syscalls_list: List[str], + allow: bool, + instrumentation_backend: InstrumentationBackend, +): + assert isinstance(allow, bool), '"allow" must be a bool value' + # "=="" means the syscall matches (blocklist), while "!="" means allow only the following. + match_sign = "!=" if allow else "==" + syscalls_filter = "" + for i, _syscall in enumerate(syscalls_list): + if i > 0: + syscalls_filter += " && " + assert isinstance( + _syscall, str + ), f"The provided syscall it not a syscall string name: {_syscall}" + + if instrumentation_backend == InstrumentationBackend.DTRACE: + syscalls_filter += f'probefunc {match_sign} "{_syscall}"' + elif instrumentation_backend == InstrumentationBackend.EBPF: + syscalls_filter += f'@sysname[args->id] {match_sign} "{_syscall}"' + else: + raise NotImplementedError( + f"backend '{instrumentation_backend}' is not supported" + ) + + filter_name = "allowlist" if allow else "blocklist" + print(f"Adding syscall {_syscall} to {filter_name}") + return syscalls_filter diff --git a/src/secimport/backends/dtrace_backend/__init__.py b/src/secimport/backends/dtrace_backend/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/secimport/backends/dtrace_backend/dtrace_backend.py b/src/secimport/backends/dtrace_backend/dtrace_backend.py new file mode 100644 index 0000000..cdde4b2 --- /dev/null +++ b/src/secimport/backends/dtrace_backend/dtrace_backend.py @@ -0,0 +1,397 @@ +""" +Import python modules with dtrace supervision. + +Copyright (c) 2022 Avi Lumelsky +""" +import importlib +import importlib.machinery # important for importlib to work for our use case +import os +import stat +import time +from pathlib import Path +from typing import List + + +from secimport.backends.common.instrumentation_backend import InstrumentationBackend +from secimport.backends.common.utils import ( + BASE_DIR_NAME, + SECIMPORT_ROOT, + render_syscalls_filter, +) + +TEMPLATES_DIR_NAME = SECIMPORT_ROOT / "templates" / "dtrace" +DEFAULT_BACKEND = InstrumentationBackend.DTRACE +PROFILES_DIR_NAME = SECIMPORT_ROOT / "profiles" + + +def run_dtrace_script_for_module( + module_name: str, + allow_shells: bool, + allow_networking: bool, + use_sudo: bool, + log_python_calls: bool, + log_syscalls: bool, + log_network: bool, + log_file_system: bool, + destructive: bool, + syscalls_allowlist: List[str], + syscalls_blocklist: List[str], + templates_dir: Path = TEMPLATES_DIR_NAME, +): + module_file_path = create_dtrace_script_for_module( + module_name=module_name, + allow_shells=allow_shells, + allow_networking=allow_networking, + log_python_calls=log_python_calls, + log_syscalls=log_syscalls, + log_network=log_network, + log_file_system=log_file_system, + destructive=destructive, + syscalls_allowlist=syscalls_allowlist, + syscalls_blocklist=syscalls_blocklist, + templates_dir=templates_dir, + ) + output_file = BASE_DIR_NAME / f"dtrace_sandbox_{module_name}.log" + current_pid = os.getpid() + dtrace_command = f'{"sudo " if use_sudo else ""} dtrace -q -s {module_file_path} -p {current_pid} -o {output_file} &2>/dev/null' + print("(running dtrace supervisor): ", dtrace_command) + st = os.stat(module_file_path) + os.chmod(module_file_path, st.st_mode | stat.S_IEXEC) + os.system(dtrace_command) + + # TODO: wait for dtrace to start explicitly using an event/fd, not time based - although 2 seconds is more than enough. + # TODO: add startup logs for dropped packets without python modules (until the first python enter takes place in the syscalls probe, the python module is null). + time.sleep(2) + return True + + +def create_dtrace_script_for_module( + module_name: str, + allow_shells: bool, + allow_networking: bool, + log_python_calls: bool, + log_syscalls: bool, + log_network: bool, + log_file_system: bool, + destructive: bool, + syscalls_allowlist: List[str] = None, + syscalls_blocklist: List[str] = None, + templates_dir: Path = TEMPLATES_DIR_NAME, +) -> str: + """ + # Template components available at the moment: + # ###DESTRUCTIVE### + # ###FUNCTION_ENTRY### + # ###FUNCTION_EXIT### + # ###SYSCALL_ENTRY### + # ###MODULE_NAME### + """ + + module = importlib.machinery.PathFinder().find_spec(module_name) + if module is None: + raise ModuleNotFoundError(module) + module_traced_name = module.origin # e.g this.py + + assert not ( + syscalls_allowlist is not None and syscalls_blocklist is not None + ), "Please specify either syscalls_allowlist OR syscalls_blocklist." + + # If we have an allowlist + if syscalls_allowlist is not None: + script_template = render_allowlist_template( + syscalls_allowlist=syscalls_allowlist, + templates_dir=templates_dir, + ) + elif syscalls_blocklist is not None: + script_template = render_blocklist_template( + syscalls_blocklist=syscalls_blocklist, templates_dir=templates_dir + ) + else: + script_template = render_dscript_template( + module_traced_name=module_traced_name, + allow_shells=allow_shells, + allow_networking=allow_networking, + log_python_calls=log_python_calls, + log_syscalls=log_syscalls, + log_network=log_network, + log_file_system=log_file_system, + destructive=destructive, + templates_dir=templates_dir, + ) + + # Creating a dscript file with the modified template + if not os.path.exists(BASE_DIR_NAME): + os.mkdir(BASE_DIR_NAME) + + module_file_name = os.path.join(BASE_DIR_NAME, f"dtrace_sandbox_{module_name}.d") + with open(module_file_name, "w") as module_file: + module_file.write(script_template) + + # Making sure the script compiles + dtrace_compile_command = f"dtrace -s {module_file_name} -e" + compile_exit_code = os.system(dtrace_compile_command) + assert ( + compile_exit_code == 0 + ), f"Failed to compile the dtrace script at {module_file_name}" + print("Successfully compiled dtrace profile: ", module_file_name) + return module_file_name + + +def render_allowlist_template( + syscalls_allowlist: List[str], templates_dir: Path = TEMPLATES_DIR_NAME +): + print("Found syscall allowlist, ignoring other configurations") + script_template = open( + templates_dir / "default.allowlist.template.d", + "r", + ).read() + syscalls_filter = render_syscalls_filter( + syscalls_list=syscalls_allowlist, + allow=True, + instrumentation_backend=InstrumentationBackend.DTRACE, + ) + script_template = script_template.replace("###SYSCALL_FILTER###", syscalls_filter) + return script_template + + +def render_blocklist_template( + syscalls_blocklist: List[str], templates_dir: Path = TEMPLATES_DIR_NAME +): + print("Found syscall allowlist, ignoring other configurations") + script_template = open( + templates_dir / "default.allowlist.template.d", + "r", + ).read() + syscalls_filter = render_syscalls_filter( + syscalls_list=syscalls_blocklist, + allow=False, + instrumentation_backend=InstrumentationBackend.DTRACE, + ) + script_template = script_template.replace("###SYSCALL_FILTER###", syscalls_filter) + return script_template + + +def render_dscript_template( + module_traced_name: str, + allow_shells: bool, + allow_networking: bool, + log_python_calls: bool, + log_syscalls: bool, + log_network: bool, + log_file_system: bool, + destructive: bool, + templates_dir: Path = TEMPLATES_DIR_NAME, + default_template_filename: str = "default.template.d", +): + script_template = open( + templates_dir / default_template_filename, + "r", + ).read() + + if destructive is True: + destructive = open(templates_dir / "headers/destructive.d", "r").read() + script_template = script_template.replace("###DESTRUCTIVE###", destructive) + else: + script_template = script_template.replace("###DESTRUCTIVE###", "") + + # PYTHON instrumentations + code_syscall_entry = "" + if log_python_calls is True: + code_function_entry = open( + templates_dir / "actions/log_python_module_entry.d", + "r", + ).read() + code_function_exit = open( + templates_dir / "actions/log_python_module_exit.d", + "r", + ).read() + script_template = script_template.replace( + "###FUNCTION_ENTRY###", code_function_entry + ) + script_template = script_template.replace( + "###FUNCTION_EXIT###", code_function_exit + ) + else: + script_template = script_template.replace("###FUNCTION_ENTRY###", "") + script_template = script_template.replace("###FUNCTION_EXIT###", "") + + # SYSCALLS instrumentations + if log_syscalls is True: + _code = open( + templates_dir / "actions/log_syscall.d", + "r", + ).read() + code_syscall_entry += f"{_code};\n" + + if log_file_system is True: + filter_fs_code = open( + templates_dir / "filters/file_system.d", + "r", + ).read() + code_syscall_entry += f"{filter_fs_code}\n{{" + action_log_file_system = open( + templates_dir / "actions/log_file_system.d", + "r", + ).read() + code_syscall_entry += f"{action_log_file_system}}}\n" + + if allow_networking is False or log_network is True: + filter_networking_code = open( + templates_dir / "filters/networking.d", + "r", + ).read() + code_syscall_entry += f"{filter_networking_code}{{\n" + + if log_network is True: + action_log_network = open( + templates_dir / "actions/log_network.d", + "r", + ).read() + code_syscall_entry += f"{action_log_network}\n" + + if allow_networking is False: + action_kill = open( + templates_dir / "actions/kill_process.d", + "r", + ).read() + code_syscall_entry += f"{action_kill}\n" + code_syscall_entry += "}\n" + + if allow_shells is False: + filter_processes_code = open( + templates_dir / "filters/processes.d", + "r", + ).read() + code_syscall_entry += f"{filter_processes_code}{{\n" + action_kill_on_processing = open( + templates_dir / "actions/kill_on_processing.d", + "r", + ).read() + code_syscall_entry += f"{action_kill_on_processing}}}\n" + + script_template = script_template.replace("###SYSCALL_ENTRY###", code_syscall_entry) + script_template = script_template.replace("###MODULE_NAME###", module_traced_name) + return script_template + + +def _render_probe_for_module( + module_name: str, + destructive: bool, + syscalls_allowlist: List[str], + templates_dir: Path = TEMPLATES_DIR_NAME, +) -> str: + # Loading the probe allowlist template + probe_template = open( + templates_dir / "probes/module_syscalls_allowlist_template.d", + "r", + ).read() + + # Adding a syscalls filter + syscalls_filter = render_syscalls_filter( + syscalls_list=syscalls_allowlist, + allow=True, + instrumentation_backend=InstrumentationBackend.DTRACE, + ) + probe_template = probe_template.replace("###SYSCALL_FILTER###", syscalls_filter) + + # Adding a probe filter for the specified python module + supervision_filter = open( + templates_dir / "filters/is_current_module_under_supervision.d", + "r", + ).read() + + # Adding the action according to the 'destructive' flag: kill if destructive, log otherwise + supervision_action = "" + if destructive is True: + action_kill_process = open( + templates_dir / "actions/kill_process.d", + "r", + ).read() + supervision_action = f"{{{action_kill_process}}}\n" + else: + action_log_syscall = open( + templates_dir / "actions/log_syscall.d", + "r", + ).read() + supervision_action = f"{{{action_log_syscall}}}\n" + + # Updating the template + probe_template = probe_template.replace( + "###SUPERVISED_MODULES_FILTER###", supervision_filter + ) + probe_template = probe_template.replace( + "###SUPERVISED_MODULES_ACTION###", supervision_action + ) + probe_template = probe_template.replace("###MODULE_NAME###", module_name) + return probe_template + + +def build_module_sandbox_from_yaml_template( + template_path: Path, templates_dir: Path = TEMPLATES_DIR_NAME +): + """Generated dscript sandbox code for secure imports based on a YAML file. + + Args: + template_path (Path): The path to the YAML file, describing the policies. + templates_dir (Path, optional): The directory of the templates. Defaults to TEMPLATES_DIR_NAME. + + Raises: + ModuleNotFoundError: _description_ + + Returns: + _type_: _description_ + """ + assert template_path.exists(), f"The template does not exist at {template_path}" + import yaml + + safe_yaml = yaml.safe_load(open(template_path, "r").read()) + parsed_probes = [] + for module_name, module_config in safe_yaml.get("modules", {}).items(): + # Finding the module without loading + module = importlib.machinery.PathFinder().find_spec(module_name) + if module is None: + raise ModuleNotFoundError(module) + + # Tracing module entrypoint + module_traced_name = module.origin + # module_traced_name = os.path.split(module_traced_name)[:-1][0] + + _destructive = module_config.get("destructive") + assert isinstance(_destructive, bool), ValueError( + f'The "destructive" field for module {module_name} is empty.' + ) + + _syscall_allowlist = module_config.get("syscall_allowlist") + assert _syscall_allowlist, ValueError( + f'The "syscall_allowlist" for module {module_name} is empty.' + ) + for _ in _syscall_allowlist: + assert isinstance(_, str), ValueError( + f'The "syscall_allowlist" field for module {module_name} contains invalid string: {_}' + ) + + module_sandbox_probe = _render_probe_for_module( + module_name=module_traced_name, + destructive=_destructive, + syscalls_allowlist=_syscall_allowlist, + ) + assert module_sandbox_probe, ValueError( + f"Failed to create a probe for module {module_name}" + ) + parsed_probes.append(module_sandbox_probe) + + if not parsed_probes: + print(f"The profile does not contain any modules: {template_path}") + return + + ###SUPERVISED_MODULES_PROBES### + script_template = open( + templates_dir / "default.yaml.template.d", + "r", + ).read() + + probes_code = ("\n" * 2).join(parsed_probes) + script_template = script_template.replace( + "###SUPERVISED_MODULES_PROBES###", probes_code + ) + return script_template diff --git a/src/secimport/sandbox_helper.py b/src/secimport/sandbox_helper.py index 8e2466a..313300b 100644 --- a/src/secimport/sandbox_helper.py +++ b/src/secimport/sandbox_helper.py @@ -1,35 +1,33 @@ -"""Import python modules with dtrace supervision. +""" +Import python modules with syscalls supervision. Copyright (c) 2022 Avi Lumelsky """ - -import importlib -import os -import time -from pathlib import Path from typing import List - -import yaml - -BASE_DIR_NAME = Path("/tmp/.secimport") -SECIMPORT_ROOT = Path(os.path.split(__file__)[:-1][0]) -TEMPLATES_DIR_NAME = SECIMPORT_ROOT / "templates" -PROFILES_DIR_NAME = SECIMPORT_ROOT / "profiles" +from secimport.backends.common.instrumentation_backend import InstrumentationBackend +from secimport.backends.common.utils import DEFAULT_BACKEND +from secimport.backends.bpftrace_backend.bpftrace_backend import ( + run_bpftrace_script_for_module, +) +from secimport.backends.dtrace_backend.dtrace_backend import ( + run_dtrace_script_for_module, +) def secure_import( module_name: str, allow_shells: bool = False, allow_networking: bool = False, - use_sudo: bool = True, + use_sudo: bool = False, log_python_calls: bool = False, # When True, the log file might reach GB in seconds. - log_syscalls: bool = False, # When True, the log file might reach GB in seconds. - log_network: bool = False, # When True, the log file might reach GB in seconds. - log_file_system: bool = False, # When True, the log file might reach GB in seconds. + log_syscalls: bool = False, + log_network: bool = False, + log_file_system: bool = False, destructive: bool = True, syscalls_allowlist: List[str] = None, syscalls_blocklist: List[str] = None, + backend=DEFAULT_BACKEND, ): """Import a python module in confined settings. @@ -47,396 +45,37 @@ def secure_import( Returns: _type_: A Python Module. The module is supervised by a dtrace process with destructive capabilities unless the 'destructive' argument is set to False. """ - assert run_dtrace_script_for_module( - module_name=module_name, - allow_shells=allow_shells, - allow_networking=allow_networking, - use_sudo=use_sudo, - log_python_calls=log_python_calls, - log_syscalls=log_syscalls, - log_network=log_network, - log_file_system=log_file_system, - destructive=destructive, - syscalls_allowlist=syscalls_allowlist, - syscalls_blocklist=syscalls_blocklist, - ) - _module = __import__(module_name) - return _module - - -def run_dtrace_script_for_module( - module_name: str, - allow_shells: bool, - allow_networking: bool, - use_sudo: bool, - log_python_calls: bool, - log_syscalls: bool, - log_network: bool, - log_file_system: bool, - destructive: bool, - syscalls_allowlist: List[str], - syscalls_blocklist: List[str], - templates_dir: Path = TEMPLATES_DIR_NAME, -): - module_file_path = create_dtrace_script_for_module( - module_name=module_name, - allow_shells=allow_shells, - allow_networking=allow_networking, - log_python_calls=log_python_calls, - log_syscalls=log_syscalls, - log_network=log_network, - log_file_system=log_file_system, - destructive=destructive, - syscalls_allowlist=syscalls_allowlist, - syscalls_blocklist=syscalls_blocklist, - templates_dir=templates_dir, - ) - output_file = BASE_DIR_NAME / f"sandbox_{module_name}.log" - current_pid = os.getpid() - dtrace_command = f'{"sudo " if use_sudo else ""} dtrace -q -s {module_file_path} -p {current_pid} -o {output_file} &2>/dev/null' - print("(running dtrace supervisor): ", dtrace_command) - os.system(dtrace_command) - - # TODO: wait for dtrace to start explicitly using an event/fd, not time based - although 2 seconds is more than enough. - # TODO: add startup logs for dropped packets without python modules (until the first python enter takes place in the syscalls probe, the python module is null). - time.sleep(2) - return True - -def create_dtrace_script_for_module( - module_name: str, - allow_shells: bool, - allow_networking: bool, - log_python_calls: bool, - log_syscalls: bool, - log_network: bool, - log_file_system: bool, - destructive: bool, - syscalls_allowlist: List[str] = None, - syscalls_blocklist: List[str] = None, - templates_dir: Path = TEMPLATES_DIR_NAME, -) -> str: - """ - # Template components available at the moment: - # ###DESTRUCTIVE### - # ###FUNCTION_ENTRY### - # ###FUNCTION_EXIT### - # ###SYSCALL_ENTRY### - # ###MODULE_NAME### - """ - - module = importlib.machinery.PathFinder().find_spec(module_name) - if module is None: - raise ModuleNotFoundError(module) - module_traced_name = module.origin # e.g this.py - - assert not ( - syscalls_allowlist is not None and syscalls_blocklist is not None - ), "Please specify either syscalls_allowlist OR syscalls_blocklist." - - # If we have an allowlist - if syscalls_allowlist is not None: - script_template = render_allowlist_template( - syscalls_allowlist=syscalls_allowlist, templates_dir=templates_dir - ) - elif syscalls_blocklist is not None: - script_template = render_blocklist_template( - syscalls_blocklist=syscalls_blocklist, templates_dir=templates_dir - ) - else: - script_template = render_dscript_template( - module_traced_name=module_traced_name, + if backend == InstrumentationBackend.EBPF: + assert run_bpftrace_script_for_module( + module_name=module_name, allow_shells=allow_shells, allow_networking=allow_networking, + use_sudo=use_sudo, log_python_calls=log_python_calls, log_syscalls=log_syscalls, log_network=log_network, log_file_system=log_file_system, destructive=destructive, - templates_dir=templates_dir, + syscalls_allowlist=syscalls_allowlist, + syscalls_blocklist=syscalls_blocklist, ) - - # Creating a dscript file with the modified template - if not os.path.exists(BASE_DIR_NAME): - os.mkdir(BASE_DIR_NAME) - - module_file_name = os.path.join(BASE_DIR_NAME, f"sandbox_{module_name}.d") - with open(module_file_name, "w") as module_file: - module_file.write(script_template) - - # Making sure the script compiles - dtrace_compile_command = f"dtrace -s {module_file_name} -e" - compile_exit_code = os.system(dtrace_compile_command) - assert ( - compile_exit_code == 0 - ), f"Failed to compile the dtrace script at {module_file_name}" - print("Successfully compiled dtrace profile: ", module_file_name) - return module_file_name - - -def render_allowlist_template( - syscalls_allowlist: List[str], templates_dir: Path = TEMPLATES_DIR_NAME -): - print("Found syscall allowlist, ignoring other configurations") - script_template = open( - templates_dir / "default.allowlist.template.d", - "r", - ).read() - syscalls_filter = render_syscalls_filter( - syscalls_list=syscalls_allowlist, allow=True - ) - script_template = script_template.replace("###SYSCALL_FILTER###", syscalls_filter) - return script_template - - -def render_blocklist_template( - syscalls_blocklist: List[str], templates_dir: Path = TEMPLATES_DIR_NAME -): - print("Found syscall allowlist, ignoring other configurations") - script_template = open( - templates_dir / "default.allowlist.template.d", - "r", - ).read() - syscalls_filter = render_syscalls_filter( - syscalls_list=syscalls_blocklist, allow=False - ) - script_template = script_template.replace("###SYSCALL_FILTER###", syscalls_filter) - return script_template - - -def render_dscript_template( - module_traced_name: str, - allow_shells: bool, - allow_networking: bool, - log_python_calls: bool, - log_syscalls: bool, - log_network: bool, - log_file_system: bool, - destructive: bool, - templates_dir: Path = TEMPLATES_DIR_NAME, - default_template_filename: str = "default.template.d", -): - script_template = open( - templates_dir / default_template_filename, - "r", - ).read() - - if destructive is True: - destructive = open(templates_dir / "headers/destructive.d", "r").read() - script_template = script_template.replace("###DESTRUCTIVE###", destructive) - else: - script_template = script_template.replace("###DESTRUCTIVE###", "") - - # PYTHON instrumentations - code_syscall_entry = "" - if log_python_calls is True: - code_function_entry = open( - templates_dir / "actions/log_python_module_entry.d", - "r", - ).read() - code_function_exit = open( - templates_dir / "actions/log_python_module_exit.d", - "r", - ).read() - script_template = script_template.replace( - "###FUNCTION_ENTRY###", code_function_entry - ) - script_template = script_template.replace( - "###FUNCTION_EXIT###", code_function_exit + elif backend == InstrumentationBackend.DTRACE: + assert run_dtrace_script_for_module( + module_name=module_name, + allow_shells=allow_shells, + allow_networking=allow_networking, + use_sudo=use_sudo, + log_python_calls=log_python_calls, + log_syscalls=log_syscalls, + log_network=log_network, + log_file_system=log_file_system, + destructive=destructive, + syscalls_allowlist=syscalls_allowlist, + syscalls_blocklist=syscalls_blocklist, ) else: - script_template = script_template.replace("###FUNCTION_ENTRY###", "") - script_template = script_template.replace("###FUNCTION_EXIT###", "") - - # SYSCALLS instrumentations - if log_syscalls is True: - _code = open( - templates_dir / "actions/log_syscall.d", - "r", - ).read() - code_syscall_entry += f"{_code};\n" - - if log_file_system is True: - filter_fs_code = open( - templates_dir / "filters/file_system.d", - "r", - ).read() - code_syscall_entry += f"{filter_fs_code}\n{{" - action_log_file_system = open( - templates_dir / "actions/log_file_system.d", - "r", - ).read() - code_syscall_entry += f"{action_log_file_system}}}\n" - - if allow_networking is False or log_network is True: - filter_networking_code = open( - templates_dir / "filters/networking.d", - "r", - ).read() - code_syscall_entry += f"{filter_networking_code}{{\n" - - if log_network is True: - action_log_network = open( - templates_dir / "actions/log_network.d", - "r", - ).read() - code_syscall_entry += f"{action_log_network}\n" - - if allow_networking is False: - action_kill = open( - templates_dir / "actions/kill_process.d", - "r", - ).read() - code_syscall_entry += f"{action_kill}\n" - code_syscall_entry += "}\n" - - if allow_shells is False: - filter_processes_code = open( - templates_dir / "filters/processes.d", - "r", - ).read() - code_syscall_entry += f"{filter_processes_code}{{\n" - action_kill_on_processing = open( - templates_dir / "actions/kill_on_processing.d", - "r", - ).read() - code_syscall_entry += f"{action_kill_on_processing}}}\n" - - script_template = script_template.replace("###SYSCALL_ENTRY###", code_syscall_entry) - script_template = script_template.replace("###MODULE_NAME###", module_traced_name) - return script_template - - -def render_syscalls_filter(syscalls_list: List[str], allow: bool): - assert isinstance(allow, bool), '"allow" must be a bool value' - # "=="" means the syscall matches (blocklist), while "!="" means allow only the following. - match_sign = "!=" if allow else "==" - syscalls_filter = "" - for i, _syscall in enumerate(syscalls_list): - if i > 0: - syscalls_filter += " && " - assert isinstance( - _syscall, str - ), f"The provided syscall it not a syscall string name: {_syscall}" - syscalls_filter += f'probefunc {match_sign} "{_syscall}"' - print(f"Adding syscall {_syscall} to allowlist") - return syscalls_filter - - -def _render_probe_for_module( - module_name: str, - destructive: bool, - syscalls_allowlist: List[str], - templates_dir: Path = TEMPLATES_DIR_NAME, -) -> str: - # Loading the probe allowlist template - probe_template = open( - templates_dir / "probes/module_syscalls_allowlist_template.d", - "r", - ).read() - - # Adding a syscalls filter - syscalls_filter = render_syscalls_filter( - syscalls_list=syscalls_allowlist, allow=True - ) - probe_template = probe_template.replace("###SYSCALL_FILTER###", syscalls_filter) - - # Adding a probe filter for the specified python module - supervision_filter = open( - templates_dir / "filters/is_current_module_under_supervision.d", - "r", - ).read() - - # Adding the action according to the 'destructive' flag: kill if destructive, log otherwise - supervision_action = "" - if destructive is True: - action_kill_process = open( - templates_dir / "actions/kill_process.d", - "r", - ).read() - supervision_action = f"{{{action_kill_process}}}\n" - else: - action_log_syscall = open( - templates_dir / "actions/log_syscall.d", - "r", - ).read() - supervision_action = f"{{{action_log_syscall}}}\n" - - # Updating the template - probe_template = probe_template.replace( - "###SUPERVISED_MODULES_FILTER###", supervision_filter - ) - probe_template = probe_template.replace( - "###SUPERVISED_MODULES_ACTION###", supervision_action - ) - probe_template = probe_template.replace("###MODULE_NAME###", module_name) - return probe_template + raise NotImplementedError(f"backend '{backend}' is not implemented.") - -def build_module_sandbox_from_yaml_template( - template_path: Path, templates_dir: Path = TEMPLATES_DIR_NAME -): - """Generated dscript sandbox code for secure imports based on a YAML file. - - Args: - template_path (Path): The path to the YAML file, describing the policies. - templates_dir (Path, optional): The directory of the templates. Defaults to TEMPLATES_DIR_NAME. - - Raises: - ModuleNotFoundError: _description_ - - Returns: - _type_: _description_ - """ - assert template_path.exists(), f"The template does not exist at {template_path}" - safe_yaml = yaml.safe_load(open(template_path, "r").read()) - parsed_probes = [] - for module_name, module_config in safe_yaml.get("modules", {}).items(): - # Finding the module without loading - module = importlib.machinery.PathFinder().find_spec(module_name) - if module is None: - raise ModuleNotFoundError(module) - - # Tracing module entrypoint - module_traced_name = module.origin - # module_traced_name = os.path.split(module_traced_name)[:-1][0] - - _destructive = module_config.get("destructive") - assert isinstance(_destructive, bool), ValueError( - f'The "destructive" field for module {module_name} is empty.' - ) - - _syscall_allowlist = module_config.get("syscall_allowlist") - assert _syscall_allowlist, ValueError( - f'The "syscall_allowlist" for module {module_name} is empty.' - ) - for _ in _syscall_allowlist: - assert isinstance(_, str), ValueError( - f'The "syscall_allowlist" field for module {module_name} contains invalid string: {_}' - ) - - module_sandbox_probe = _render_probe_for_module( - module_name=module_traced_name, - destructive=_destructive, - syscalls_allowlist=_syscall_allowlist, - ) - assert module_sandbox_probe, ValueError( - f"Failed to create a probe for module {module_name}" - ) - parsed_probes.append(module_sandbox_probe) - - if not parsed_probes: - print(f"The profile does not contain any modules: {template_path}") - return - - ###SUPERVISED_MODULES_PROBES### - script_template = open( - templates_dir / "default.yaml.template.d", - "r", - ).read() - - probes_code = ("\n" * 2).join(parsed_probes) - script_template = script_template.replace( - "###SUPERVISED_MODULES_PROBES###", probes_code - ) - return script_template + _module = __import__(module_name) + return _module diff --git a/src/secimport/templates/bpftrace/actions/kill_on_processing.bt b/src/secimport/templates/bpftrace/actions/kill_on_processing.bt new file mode 100644 index 0000000..539ae5a --- /dev/null +++ b/src/secimport/templates/bpftrace/actions/kill_on_processing.bt @@ -0,0 +1,7 @@ + if (###DESTRUCTIVE###){ + printf("\nKILLING PROCESS %s - EXECUTED execve;\n", str(pid)); + printf("\t\tKILLING...\r\n"); + system("pkill -9 python"); // optional + printf("\t\tKILLED.\r\n"); + exit(); // optional + } diff --git a/src/secimport/templates/bpftrace/actions/kill_process.bt b/src/secimport/templates/bpftrace/actions/kill_process.bt new file mode 100644 index 0000000..b03d0f6 --- /dev/null +++ b/src/secimport/templates/bpftrace/actions/kill_process.bt @@ -0,0 +1,5 @@ + printf("\nKILLING PROCESS %s - EXECUTED execve;\n", str(pid)); + printf("\t\tKILLING...\r\n"); + system("pkill -9 python"); // optional + printf("\t\tKILLED.\r\n"); + exit(); // optional \ No newline at end of file diff --git a/src/secimport/templates/bpftrace/actions/log_file_system.bt b/src/secimport/templates/bpftrace/actions/log_file_system.bt new file mode 100644 index 0000000..b293f2c --- /dev/null +++ b/src/secimport/templates/bpftrace/actions/log_file_system.bt @@ -0,0 +1 @@ +printf("\t\t(TOUCHING FILESYSTEM): %s(%d) in python module %s\r\n", @sysname[args->id], arg1, @globals["current_module"]); \ No newline at end of file diff --git a/src/secimport/templates/bpftrace/actions/log_network.bt b/src/secimport/templates/bpftrace/actions/log_network.bt new file mode 100644 index 0000000..171b94f --- /dev/null +++ b/src/secimport/templates/bpftrace/actions/log_network.bt @@ -0,0 +1 @@ +printf("\t\t(NETWORKING): %s in python module %s\r\n", @sysname[args->id], arg1, @globals["current_module"]); \ No newline at end of file diff --git a/src/secimport/templates/bpftrace/actions/log_python_module_entry.bt b/src/secimport/templates/bpftrace/actions/log_python_module_entry.bt new file mode 100644 index 0000000..a52c71f --- /dev/null +++ b/src/secimport/templates/bpftrace/actions/log_python_module_entry.bt @@ -0,0 +1 @@ +printf("%s, %s, depth=%d\n", str(arg0), str(arg1), @["depth"]) ; \ No newline at end of file diff --git a/src/secimport/templates/bpftrace/actions/log_python_module_exit.bt b/src/secimport/templates/bpftrace/actions/log_python_module_exit.bt new file mode 100644 index 0000000..e69de29 diff --git a/src/secimport/templates/bpftrace/actions/log_syscall.bt b/src/secimport/templates/bpftrace/actions/log_syscall.bt new file mode 100644 index 0000000..08857e0 --- /dev/null +++ b/src/secimport/templates/bpftrace/actions/log_syscall.bt @@ -0,0 +1 @@ +printf("%s SYSCALL %ld %s depth=%d previous_module=%s current_module=%s \n", probe, args->id, @sysname[args->id], @["depth"], @globals["previous_module"], @globals["current_module"] ); \ No newline at end of file diff --git a/src/secimport/templates/bpftrace/default.template.bt b/src/secimport/templates/bpftrace/default.template.bt new file mode 100644 index 0000000..2993c6d --- /dev/null +++ b/src/secimport/templates/bpftrace/default.template.bt @@ -0,0 +1,708 @@ +#!/usr/bin/env bpftrace + +BEGIN { + // Mapping all syscalls both ways, based on https://github.com/iovisor/bpftrace/blob/2c7a7a598dbe1aa790db2dfe2db242aa69137d5b/tools/syscount.bt + // Generates using bash: + // $ apt-get install auditd + // $ ausyscall --dump | awk 'NR > 1 { printf("\t@sysname[%d] = \"%s\";\n", $1, $2); }'; + printf("REGISTERING SYSCALLS...\n"); + @sysname[0] = "read"; + @sysname[1] = "write"; + @sysname[2] = "open"; + @sysname[3] = "close"; + @sysname[4] = "stat"; + @sysname[5] = "fstat"; + @sysname[6] = "lstat"; + @sysname[7] = "poll"; + @sysname[8] = "lseek"; + @sysname[9] = "mmap"; + @sysname[10] = "mprotect"; + @sysname[11] = "munmap"; + @sysname[12] = "brk"; + @sysname[13] = "rt_sigaction"; + @sysname[14] = "rt_sigprocmask"; + @sysname[15] = "rt_sigreturn"; + @sysname[16] = "ioctl"; + @sysname[17] = "pread"; + @sysname[18] = "pwrite"; + @sysname[19] = "readv"; + @sysname[20] = "writev"; + @sysname[21] = "access"; + @sysname[22] = "pipe"; + @sysname[23] = "select"; + @sysname[24] = "sched_yield"; + @sysname[25] = "mremap"; + @sysname[26] = "msync"; + @sysname[27] = "mincore"; + @sysname[28] = "madvise"; + @sysname[29] = "shmget"; + @sysname[30] = "shmat"; + @sysname[31] = "shmctl"; + @sysname[32] = "dup"; + @sysname[33] = "dup2"; + @sysname[34] = "pause"; + @sysname[35] = "nanosleep"; + @sysname[36] = "getitimer"; + @sysname[37] = "alarm"; + @sysname[38] = "setitimer"; + @sysname[39] = "getpid"; + @sysname[40] = "sendfile"; + @sysname[41] = "socket"; + @sysname[42] = "connect"; + @sysname[43] = "accept"; + @sysname[44] = "sendto"; + @sysname[45] = "recvfrom"; + @sysname[46] = "sendmsg"; + @sysname[47] = "recvmsg"; + @sysname[48] = "shutdown"; + @sysname[49] = "bind"; + @sysname[50] = "listen"; + @sysname[51] = "getsockname"; + @sysname[52] = "getpeername"; + @sysname[53] = "socketpair"; + @sysname[54] = "setsockopt"; + @sysname[55] = "getsockopt"; + @sysname[56] = "clone"; + @sysname[57] = "fork"; + @sysname[58] = "vfork"; + @sysname[59] = "execve"; + @sysname[60] = "exit"; + @sysname[61] = "wait4"; + @sysname[62] = "kill"; + @sysname[63] = "uname"; + @sysname[64] = "semget"; + @sysname[65] = "semop"; + @sysname[66] = "semctl"; + @sysname[67] = "shmdt"; + @sysname[68] = "msgget"; + @sysname[69] = "msgsnd"; + @sysname[70] = "msgrcv"; + @sysname[71] = "msgctl"; + @sysname[72] = "fcntl"; + @sysname[73] = "flock"; + @sysname[74] = "fsync"; + @sysname[75] = "fdatasync"; + @sysname[76] = "truncate"; + @sysname[77] = "ftruncate"; + @sysname[78] = "getdents"; + @sysname[79] = "getcwd"; + @sysname[80] = "chdir"; + @sysname[81] = "fchdir"; + @sysname[82] = "rename"; + @sysname[83] = "mkdir"; + @sysname[84] = "rmdir"; + @sysname[85] = "creat"; + @sysname[86] = "link"; + @sysname[87] = "unlink"; + @sysname[88] = "symlink"; + @sysname[89] = "readlink"; + @sysname[90] = "chmod"; + @sysname[91] = "fchmod"; + @sysname[92] = "chown"; + @sysname[93] = "fchown"; + @sysname[94] = "lchown"; + @sysname[95] = "umask"; + @sysname[96] = "gettimeofday"; + @sysname[97] = "getrlimit"; + @sysname[98] = "getrusage"; + @sysname[99] = "sysinfo"; + @sysname[100] = "times"; + @sysname[101] = "ptrace"; + @sysname[102] = "getuid"; + @sysname[103] = "syslog"; + @sysname[104] = "getgid"; + @sysname[105] = "setuid"; + @sysname[106] = "setgid"; + @sysname[107] = "geteuid"; + @sysname[108] = "getegid"; + @sysname[109] = "setpgid"; + @sysname[110] = "getppid"; + @sysname[111] = "getpgrp"; + @sysname[112] = "setsid"; + @sysname[113] = "setreuid"; + @sysname[114] = "setregid"; + @sysname[115] = "getgroups"; + @sysname[116] = "setgroups"; + @sysname[117] = "setresuid"; + @sysname[118] = "getresuid"; + @sysname[119] = "setresgid"; + @sysname[120] = "getresgid"; + @sysname[121] = "getpgid"; + @sysname[122] = "setfsuid"; + @sysname[123] = "setfsgid"; + @sysname[124] = "getsid"; + @sysname[125] = "capget"; + @sysname[126] = "capset"; + @sysname[127] = "rt_sigpending"; + @sysname[128] = "rt_sigtimedwait"; + @sysname[129] = "rt_sigqueueinfo"; + @sysname[130] = "rt_sigsuspend"; + @sysname[131] = "sigaltstack"; + @sysname[132] = "utime"; + @sysname[133] = "mknod"; + @sysname[134] = "uselib"; + @sysname[135] = "personality"; + @sysname[136] = "ustat"; + @sysname[137] = "statfs"; + @sysname[138] = "fstatfs"; + @sysname[139] = "sysfs"; + @sysname[140] = "getpriority"; + @sysname[141] = "setpriority"; + @sysname[142] = "sched_setparam"; + @sysname[143] = "sched_getparam"; + @sysname[144] = "sched_setscheduler"; + @sysname[145] = "sched_getscheduler"; + @sysname[146] = "sched_get_priority_max"; + @sysname[147] = "sched_get_priority_min"; + @sysname[148] = "sched_rr_get_interval"; + @sysname[149] = "mlock"; + @sysname[150] = "munlock"; + @sysname[151] = "mlockall"; + @sysname[152] = "munlockall"; + @sysname[153] = "vhangup"; + @sysname[154] = "modify_ldt"; + @sysname[155] = "pivot_root"; + @sysname[156] = "_sysctl"; + @sysname[157] = "prctl"; + @sysname[158] = "arch_prctl"; + @sysname[159] = "adjtimex"; + @sysname[160] = "setrlimit"; + @sysname[161] = "chroot"; + @sysname[162] = "sync"; + @sysname[163] = "acct"; + @sysname[164] = "settimeofday"; + @sysname[165] = "mount"; + @sysname[166] = "umount2"; + @sysname[167] = "swapon"; + @sysname[168] = "swapoff"; + @sysname[169] = "reboot"; + @sysname[170] = "sethostname"; + @sysname[171] = "setdomainname"; + @sysname[172] = "iopl"; + @sysname[173] = "ioperm"; + @sysname[174] = "create_module"; + @sysname[175] = "init_module"; + @sysname[176] = "delete_module"; + @sysname[177] = "get_kernel_syms"; + @sysname[178] = "query_module"; + @sysname[179] = "quotactl"; + @sysname[180] = "nfsservctl"; + @sysname[181] = "getpmsg"; + @sysname[182] = "putpmsg"; + @sysname[183] = "afs_syscall"; + @sysname[184] = "tuxcall"; + @sysname[185] = "security"; + @sysname[186] = "gettid"; + @sysname[187] = "readahead"; + @sysname[188] = "setxattr"; + @sysname[189] = "lsetxattr"; + @sysname[190] = "fsetxattr"; + @sysname[191] = "getxattr"; + @sysname[192] = "lgetxattr"; + @sysname[193] = "fgetxattr"; + @sysname[194] = "listxattr"; + @sysname[195] = "llistxattr"; + @sysname[196] = "flistxattr"; + @sysname[197] = "removexattr"; + @sysname[198] = "lremovexattr"; + @sysname[199] = "fremovexattr"; + @sysname[200] = "tkill"; + @sysname[201] = "time"; + @sysname[202] = "futex"; + @sysname[203] = "sched_setaffinity"; + @sysname[204] = "sched_getaffinity"; + @sysname[205] = "set_thread_area"; + @sysname[206] = "io_setup"; + @sysname[207] = "io_destroy"; + @sysname[208] = "io_getevents"; + @sysname[209] = "io_submit"; + @sysname[210] = "io_cancel"; + @sysname[211] = "get_thread_area"; + @sysname[212] = "lookup_dcookie"; + @sysname[213] = "epoll_create"; + @sysname[214] = "epoll_ctl_old"; + @sysname[215] = "epoll_wait_old"; + @sysname[216] = "remap_file_pages"; + @sysname[217] = "getdents64"; + @sysname[218] = "set_tid_address"; + @sysname[219] = "restart_syscall"; + @sysname[220] = "semtimedop"; + @sysname[221] = "fadvise64"; + @sysname[222] = "timer_create"; + @sysname[223] = "timer_settime"; + @sysname[224] = "timer_gettime"; + @sysname[225] = "timer_getoverrun"; + @sysname[226] = "timer_delete"; + @sysname[227] = "clock_settime"; + @sysname[228] = "clock_gettime"; + @sysname[229] = "clock_getres"; + @sysname[230] = "clock_nanosleep"; + @sysname[231] = "exit_group"; + @sysname[232] = "epoll_wait"; + @sysname[233] = "epoll_ctl"; + @sysname[234] = "tgkill"; + @sysname[235] = "utimes"; + @sysname[236] = "vserver"; + @sysname[237] = "mbind"; + @sysname[238] = "set_mempolicy"; + @sysname[239] = "get_mempolicy"; + @sysname[240] = "mq_open"; + @sysname[241] = "mq_unlink"; + @sysname[242] = "mq_timedsend"; + @sysname[243] = "mq_timedreceive"; + @sysname[244] = "mq_notify"; + @sysname[245] = "mq_getsetattr"; + @sysname[246] = "kexec_load"; + @sysname[247] = "waitid"; + @sysname[248] = "add_key"; + @sysname[249] = "request_key"; + @sysname[250] = "keyctl"; + @sysname[251] = "ioprio_set"; + @sysname[252] = "ioprio_get"; + @sysname[253] = "inotify_init"; + @sysname[254] = "inotify_add_watch"; + @sysname[255] = "inotify_rm_watch"; + @sysname[256] = "migrate_pages"; + @sysname[257] = "openat"; + @sysname[258] = "mkdirat"; + @sysname[259] = "mknodat"; + @sysname[260] = "fchownat"; + @sysname[261] = "futimesat"; + @sysname[262] = "newfstatat"; + @sysname[263] = "unlinkat"; + @sysname[264] = "renameat"; + @sysname[265] = "linkat"; + @sysname[266] = "symlinkat"; + @sysname[267] = "readlinkat"; + @sysname[268] = "fchmodat"; + @sysname[269] = "faccessat"; + @sysname[270] = "pselect6"; + @sysname[271] = "ppoll"; + @sysname[272] = "unshare"; + @sysname[273] = "set_robust_list"; + @sysname[274] = "get_robust_list"; + @sysname[275] = "splice"; + @sysname[276] = "tee"; + @sysname[277] = "sync_file_range"; + @sysname[278] = "vmsplice"; + @sysname[279] = "move_pages"; + @sysname[280] = "utimensat"; + @sysname[281] = "epoll_pwait"; + @sysname[282] = "signalfd"; + @sysname[283] = "timerfd"; + @sysname[284] = "eventfd"; + @sysname[285] = "fallocate"; + @sysname[286] = "timerfd_settime"; + @sysname[287] = "timerfd_gettime"; + @sysname[288] = "accept4"; + @sysname[289] = "signalfd4"; + @sysname[290] = "eventfd2"; + @sysname[291] = "epoll_create1"; + @sysname[292] = "dup3"; + @sysname[293] = "pipe2"; + @sysname[294] = "inotify_init1"; + @sysname[295] = "preadv"; + @sysname[296] = "pwritev"; + @sysname[297] = "rt_tgsigqueueinfo"; + @sysname[298] = "perf_event_open"; + @sysname[299] = "recvmmsg"; + @sysname[300] = "fanotify_init"; + @sysname[301] = "fanotify_mark"; + @sysname[302] = "prlimit64"; + @sysname[303] = "name_to_handle_at"; + @sysname[304] = "open_by_handle_at"; + @sysname[305] = "clock_adjtime"; + @sysname[306] = "syncfs"; + @sysname[307] = "sendmmsg"; + @sysname[308] = "setns"; + @sysname[309] = "getcpu"; + @sysname[310] = "process_vm_readv"; + @sysname[311] = "process_vm_writev"; + @sysname[312] = "kcmp"; + @sysname[313] = "finit_module"; + @sysname[314] = "sched_setattr"; + @sysname[315] = "sched_getattr"; + @sysname[316] = "renameat2"; + @sysname[317] = "seccomp"; + @sysname[318] = "getrandom"; + @sysname[319] = "memfd_create"; + @sysname[320] = "kexec_file_load"; + @sysname[321] = "bpf"; + @sysname[322] = "execveat"; + @sysname[323] = "userfaultfd"; + @sysname[324] = "membarrier"; + @sysname[325] = "mlock2"; + @sysname[326] = "copy_file_range"; + @sysname[327] = "preadv2"; + @sysname[328] = "pwritev2"; + @sysname[329] = "pkey_mprotect"; + @sysname[330] = "pkey_alloc"; + @sysname[331] = "pkey_free"; + @sysname[332] = "statx"; + @sysname[333] = "io_pgetevents"; + @sysname[334] = "rseq"; + + // Reverse mapping + // ausyscall --dump | awk 'NR > 1 { printf("\t@sysnum[\"%s\"] = %d;\n", $2, $1); }'; + @sysnum["read"] = 0; + @sysnum["write"] = 1; + @sysnum["open"] = 2; + @sysnum["close"] = 3; + @sysnum["stat"] = 4; + @sysnum["fstat"] = 5; + @sysnum["lstat"] = 6; + @sysnum["poll"] = 7; + @sysnum["lseek"] = 8; + @sysnum["mmap"] = 9; + @sysnum["mprotect"] = 10; + @sysnum["munmap"] = 11; + @sysnum["brk"] = 12; + @sysnum["rt_sigaction"] = 13; + @sysnum["rt_sigprocmask"] = 14; + @sysnum["rt_sigreturn"] = 15; + @sysnum["ioctl"] = 16; + @sysnum["pread"] = 17; + @sysnum["pwrite"] = 18; + @sysnum["readv"] = 19; + @sysnum["writev"] = 20; + @sysnum["access"] = 21; + @sysnum["pipe"] = 22; + @sysnum["select"] = 23; + @sysnum["sched_yield"] = 24; + @sysnum["mremap"] = 25; + @sysnum["msync"] = 26; + @sysnum["mincore"] = 27; + @sysnum["madvise"] = 28; + @sysnum["shmget"] = 29; + @sysnum["shmat"] = 30; + @sysnum["shmctl"] = 31; + @sysnum["dup"] = 32; + @sysnum["dup2"] = 33; + @sysnum["pause"] = 34; + @sysnum["nanosleep"] = 35; + @sysnum["getitimer"] = 36; + @sysnum["alarm"] = 37; + @sysnum["setitimer"] = 38; + @sysnum["getpid"] = 39; + @sysnum["sendfile"] = 40; + @sysnum["socket"] = 41; + @sysnum["connect"] = 42; + @sysnum["accept"] = 43; + @sysnum["sendto"] = 44; + @sysnum["recvfrom"] = 45; + @sysnum["sendmsg"] = 46; + @sysnum["recvmsg"] = 47; + @sysnum["shutdown"] = 48; + @sysnum["bind"] = 49; + @sysnum["listen"] = 50; + @sysnum["getsockname"] = 51; + @sysnum["getpeername"] = 52; + @sysnum["socketpair"] = 53; + @sysnum["setsockopt"] = 54; + @sysnum["getsockopt"] = 55; + @sysnum["clone"] = 56; + @sysnum["fork"] = 57; + @sysnum["vfork"] = 58; + @sysnum["execve"] = 59; + @sysnum["exit"] = 60; + @sysnum["wait4"] = 61; + @sysnum["kill"] = 62; + @sysnum["uname"] = 63; + @sysnum["semget"] = 64; + @sysnum["semop"] = 65; + @sysnum["semctl"] = 66; + @sysnum["shmdt"] = 67; + @sysnum["msgget"] = 68; + @sysnum["msgsnd"] = 69; + @sysnum["msgrcv"] = 70; + @sysnum["msgctl"] = 71; + @sysnum["fcntl"] = 72; + @sysnum["flock"] = 73; + @sysnum["fsync"] = 74; + @sysnum["fdatasync"] = 75; + @sysnum["truncate"] = 76; + @sysnum["ftruncate"] = 77; + @sysnum["getdents"] = 78; + @sysnum["getcwd"] = 79; + @sysnum["chdir"] = 80; + @sysnum["fchdir"] = 81; + @sysnum["rename"] = 82; + @sysnum["mkdir"] = 83; + @sysnum["rmdir"] = 84; + @sysnum["creat"] = 85; + @sysnum["link"] = 86; + @sysnum["unlink"] = 87; + @sysnum["symlink"] = 88; + @sysnum["readlink"] = 89; + @sysnum["chmod"] = 90; + @sysnum["fchmod"] = 91; + @sysnum["chown"] = 92; + @sysnum["fchown"] = 93; + @sysnum["lchown"] = 94; + @sysnum["umask"] = 95; + @sysnum["gettimeofday"] = 96; + @sysnum["getrlimit"] = 97; + @sysnum["getrusage"] = 98; + @sysnum["sysinfo"] = 99; + @sysnum["times"] = 100; + @sysnum["ptrace"] = 101; + @sysnum["getuid"] = 102; + @sysnum["syslog"] = 103; + @sysnum["getgid"] = 104; + @sysnum["setuid"] = 105; + @sysnum["setgid"] = 106; + @sysnum["geteuid"] = 107; + @sysnum["getegid"] = 108; + @sysnum["setpgid"] = 109; + @sysnum["getppid"] = 110; + @sysnum["getpgrp"] = 111; + @sysnum["setsid"] = 112; + @sysnum["setreuid"] = 113; + @sysnum["setregid"] = 114; + @sysnum["getgroups"] = 115; + @sysnum["setgroups"] = 116; + @sysnum["setresuid"] = 117; + @sysnum["getresuid"] = 118; + @sysnum["setresgid"] = 119; + @sysnum["getresgid"] = 120; + @sysnum["getpgid"] = 121; + @sysnum["setfsuid"] = 122; + @sysnum["setfsgid"] = 123; + @sysnum["getsid"] = 124; + @sysnum["capget"] = 125; + @sysnum["capset"] = 126; + @sysnum["rt_sigpending"] = 127; + @sysnum["rt_sigtimedwait"] = 128; + @sysnum["rt_sigqueueinfo"] = 129; + @sysnum["rt_sigsuspend"] = 130; + @sysnum["sigaltstack"] = 131; + @sysnum["utime"] = 132; + @sysnum["mknod"] = 133; + @sysnum["uselib"] = 134; + @sysnum["personality"] = 135; + @sysnum["ustat"] = 136; + @sysnum["statfs"] = 137; + @sysnum["fstatfs"] = 138; + @sysnum["sysfs"] = 139; + @sysnum["getpriority"] = 140; + @sysnum["setpriority"] = 141; + @sysnum["sched_setparam"] = 142; + @sysnum["sched_getparam"] = 143; + @sysnum["sched_setscheduler"] = 144; + @sysnum["sched_getscheduler"] = 145; + @sysnum["sched_get_priority_max"] = 146; + @sysnum["sched_get_priority_min"] = 147; + @sysnum["sched_rr_get_interval"] = 148; + @sysnum["mlock"] = 149; + @sysnum["munlock"] = 150; + @sysnum["mlockall"] = 151; + @sysnum["munlockall"] = 152; + @sysnum["vhangup"] = 153; + @sysnum["modify_ldt"] = 154; + @sysnum["pivot_root"] = 155; + @sysnum["_sysctl"] = 156; + @sysnum["prctl"] = 157; + @sysnum["arch_prctl"] = 158; + @sysnum["adjtimex"] = 159; + @sysnum["setrlimit"] = 160; + @sysnum["chroot"] = 161; + @sysnum["sync"] = 162; + @sysnum["acct"] = 163; + @sysnum["settimeofday"] = 164; + @sysnum["mount"] = 165; + @sysnum["umount2"] = 166; + @sysnum["swapon"] = 167; + @sysnum["swapoff"] = 168; + @sysnum["reboot"] = 169; + @sysnum["sethostname"] = 170; + @sysnum["setdomainname"] = 171; + @sysnum["iopl"] = 172; + @sysnum["ioperm"] = 173; + @sysnum["create_module"] = 174; + @sysnum["init_module"] = 175; + @sysnum["delete_module"] = 176; + @sysnum["get_kernel_syms"] = 177; + @sysnum["query_module"] = 178; + @sysnum["quotactl"] = 179; + @sysnum["nfsservctl"] = 180; + @sysnum["getpmsg"] = 181; + @sysnum["putpmsg"] = 182; + @sysnum["afs_syscall"] = 183; + @sysnum["tuxcall"] = 184; + @sysnum["security"] = 185; + @sysnum["gettid"] = 186; + @sysnum["readahead"] = 187; + @sysnum["setxattr"] = 188; + @sysnum["lsetxattr"] = 189; + @sysnum["fsetxattr"] = 190; + @sysnum["getxattr"] = 191; + @sysnum["lgetxattr"] = 192; + @sysnum["fgetxattr"] = 193; + @sysnum["listxattr"] = 194; + @sysnum["llistxattr"] = 195; + @sysnum["flistxattr"] = 196; + @sysnum["removexattr"] = 197; + @sysnum["lremovexattr"] = 198; + @sysnum["fremovexattr"] = 199; + @sysnum["tkill"] = 200; + @sysnum["time"] = 201; + @sysnum["futex"] = 202; + @sysnum["sched_setaffinity"] = 203; + @sysnum["sched_getaffinity"] = 204; + @sysnum["set_thread_area"] = 205; + @sysnum["io_setup"] = 206; + @sysnum["io_destroy"] = 207; + @sysnum["io_getevents"] = 208; + @sysnum["io_submit"] = 209; + @sysnum["io_cancel"] = 210; + @sysnum["get_thread_area"] = 211; + @sysnum["lookup_dcookie"] = 212; + @sysnum["epoll_create"] = 213; + @sysnum["epoll_ctl_old"] = 214; + @sysnum["epoll_wait_old"] = 215; + @sysnum["remap_file_pages"] = 216; + @sysnum["getdents64"] = 217; + @sysnum["set_tid_address"] = 218; + @sysnum["restart_syscall"] = 219; + @sysnum["semtimedop"] = 220; + @sysnum["fadvise64"] = 221; + @sysnum["timer_create"] = 222; + @sysnum["timer_settime"] = 223; + @sysnum["timer_gettime"] = 224; + @sysnum["timer_getoverrun"] = 225; + @sysnum["timer_delete"] = 226; + @sysnum["clock_settime"] = 227; + @sysnum["clock_gettime"] = 228; + @sysnum["clock_getres"] = 229; + @sysnum["clock_nanosleep"] = 230; + @sysnum["exit_group"] = 231; + @sysnum["epoll_wait"] = 232; + @sysnum["epoll_ctl"] = 233; + @sysnum["tgkill"] = 234; + @sysnum["utimes"] = 235; + @sysnum["vserver"] = 236; + @sysnum["mbind"] = 237; + @sysnum["set_mempolicy"] = 238; + @sysnum["get_mempolicy"] = 239; + @sysnum["mq_open"] = 240; + @sysnum["mq_unlink"] = 241; + @sysnum["mq_timedsend"] = 242; + @sysnum["mq_timedreceive"] = 243; + @sysnum["mq_notify"] = 244; + @sysnum["mq_getsetattr"] = 245; + @sysnum["kexec_load"] = 246; + @sysnum["waitid"] = 247; + @sysnum["add_key"] = 248; + @sysnum["request_key"] = 249; + @sysnum["keyctl"] = 250; + @sysnum["ioprio_set"] = 251; + @sysnum["ioprio_get"] = 252; + @sysnum["inotify_init"] = 253; + @sysnum["inotify_add_watch"] = 254; + @sysnum["inotify_rm_watch"] = 255; + @sysnum["migrate_pages"] = 256; + @sysnum["openat"] = 257; + @sysnum["mkdirat"] = 258; + @sysnum["mknodat"] = 259; + @sysnum["fchownat"] = 260; + @sysnum["futimesat"] = 261; + @sysnum["newfstatat"] = 262; + @sysnum["unlinkat"] = 263; + @sysnum["renameat"] = 264; + @sysnum["linkat"] = 265; + @sysnum["symlinkat"] = 266; + @sysnum["readlinkat"] = 267; + @sysnum["fchmodat"] = 268; + @sysnum["faccessat"] = 269; + @sysnum["pselect6"] = 270; + @sysnum["ppoll"] = 271; + @sysnum["unshare"] = 272; + @sysnum["set_robust_list"] = 273; + @sysnum["get_robust_list"] = 274; + @sysnum["splice"] = 275; + @sysnum["tee"] = 276; + @sysnum["sync_file_range"] = 277; + @sysnum["vmsplice"] = 278; + @sysnum["move_pages"] = 279; + @sysnum["utimensat"] = 280; + @sysnum["epoll_pwait"] = 281; + @sysnum["signalfd"] = 282; + @sysnum["timerfd"] = 283; + @sysnum["eventfd"] = 284; + @sysnum["fallocate"] = 285; + @sysnum["timerfd_settime"] = 286; + @sysnum["timerfd_gettime"] = 287; + @sysnum["accept4"] = 288; + @sysnum["signalfd4"] = 289; + @sysnum["eventfd2"] = 290; + @sysnum["epoll_create1"] = 291; + @sysnum["dup3"] = 292; + @sysnum["pipe2"] = 293; + @sysnum["inotify_init1"] = 294; + @sysnum["preadv"] = 295; + @sysnum["pwritev"] = 296; + @sysnum["rt_tgsigqueueinfo"] = 297; + @sysnum["perf_event_open"] = 298; + @sysnum["recvmmsg"] = 299; + @sysnum["fanotify_init"] = 300; + @sysnum["fanotify_mark"] = 301; + @sysnum["prlimit64"] = 302; + @sysnum["name_to_handle_at"] = 303; + @sysnum["open_by_handle_at"] = 304; + @sysnum["clock_adjtime"] = 305; + @sysnum["syncfs"] = 306; + @sysnum["sendmmsg"] = 307; + @sysnum["setns"] = 308; + @sysnum["getcpu"] = 309; + @sysnum["process_vm_readv"] = 310; + @sysnum["process_vm_writev"] = 311; + @sysnum["kcmp"] = 312; + @sysnum["finit_module"] = 313; + @sysnum["sched_setattr"] = 314; + @sysnum["sched_getattr"] = 315; + @sysnum["renameat2"] = 316; + @sysnum["seccomp"] = 317; + @sysnum["getrandom"] = 318; + @sysnum["memfd_create"] = 319; + @sysnum["kexec_file_load"] = 320; + @sysnum["bpf"] = 321; + @sysnum["execveat"] = 322; + @sysnum["userfaultfd"] = 323; + @sysnum["membarrier"] = 324; + @sysnum["mlock2"] = 325; + @sysnum["copy_file_range"] = 326; + @sysnum["preadv2"] = 327; + @sysnum["pwritev2"] = 328; + @sysnum["pkey_mprotect"] = 329; + @sysnum["pkey_alloc"] = 330; + @sysnum["pkey_free"] = 331; + @sysnum["statx"] = 332; + @sysnum["io_pgetevents"] = 333; + @sysnum["rseq"] = 334; + printf("STARTED\n") +} + + +usdt:###INTERPRETER_PATH###:function__entry { + @["depth"]++; + @entrypoints[str(arg0)] = @["depth"]; + @globals["previous_module"] = @globals["current_module"]; + @globals["current_module"] = str(arg0); + printf("%s, %s, depth=%d\n", str(arg0), str(arg1), @["depth"]) ; + ###FUNCTION_ENTRY### +} + +usdt:###INTERPRETER_PATH###:function__return { + @["depth"]--; + ###FUNCTION_EXIT### +} + +tracepoint:raw_syscalls:sys_enter /comm == "python"/ { + ###SYSCALL_ENTRY### +} + +END { + clear(@sysname); + clear(@sysnum); + clear(@entrypoints); +} \ No newline at end of file diff --git a/src/secimport/templates/bpftrace/filters/file_system.bt b/src/secimport/templates/bpftrace/filters/file_system.bt new file mode 100644 index 0000000..25cef75 --- /dev/null +++ b/src/secimport/templates/bpftrace/filters/file_system.bt @@ -0,0 +1 @@ +if (@sysname[args->id] == "open" || @sysname[args->id] == "write") \ No newline at end of file diff --git a/src/secimport/templates/bpftrace/filters/is_current_module_under_supervision.bt b/src/secimport/templates/bpftrace/filters/is_current_module_under_supervision.bt new file mode 100644 index 0000000..617448b --- /dev/null +++ b/src/secimport/templates/bpftrace/filters/is_current_module_under_supervision.bt @@ -0,0 +1 @@ +if(@entrypoints["###MODULE_NAME###"] != 0 && @["depth"] >= @entrypoints["###MODULE_NAME###"] && @entrypoints["###MODULE_NAME###"] >= @entrypoints[@globals["previous_module"]]) diff --git a/src/secimport/templates/bpftrace/filters/networking.bt b/src/secimport/templates/bpftrace/filters/networking.bt new file mode 100644 index 0000000..83beecb --- /dev/null +++ b/src/secimport/templates/bpftrace/filters/networking.bt @@ -0,0 +1 @@ + if (@sysname[args->id] == "socket") \ No newline at end of file diff --git a/src/secimport/templates/bpftrace/filters/processes.bt b/src/secimport/templates/bpftrace/filters/processes.bt new file mode 100644 index 0000000..45d9c93 --- /dev/null +++ b/src/secimport/templates/bpftrace/filters/processes.bt @@ -0,0 +1,13 @@ + if (@sysname[args->id] == "posix_spawn" || + @sysname[args->id] == "posix_spawnp" || + @sysname[args->id] == "clone" || + @sysname[args->id] == "__clone2" || + @sysname[args->id] == "clone3" || + @sysname[args->id] == "fork" || + @sysname[args->id] == "vfork" || + @sysname[args->id] == "forkexec" || + @sysname[args->id] == "execl" || + @sysname[args->id] == "execlp" || + @sysname[args->id] == "execle" || + @sysname[args->id] == "execv" || + @sysname[args->id] == "execvp") \ No newline at end of file diff --git a/src/secimport/templates/bpftrace/probes/module_syscalls_allowlist_template.bt b/src/secimport/templates/bpftrace/probes/module_syscalls_allowlist_template.bt new file mode 100644 index 0000000..55f82af --- /dev/null +++ b/src/secimport/templates/bpftrace/probes/module_syscalls_allowlist_template.bt @@ -0,0 +1,20 @@ +/* ###MODULE_NAME### START */ +if ((@entrypoints["###MODULE_NAME###"] != 0) && (@["depth"] >= @entrypoints["###MODULE_NAME###"])){ + if (@globals["latest_supervised_module"] == ""){ + @globals["latest_supervised_module"] = "###MODULE_NAME###" + } + else{ + if (@entrypoints["###MODULE_NAME###"] > @entrypoints[@globals["latest_supervised_module"]]){ + @globals["latest_supervised_module"] = "###MODULE_NAME###" + } + } + + if (@entrypoints["###MODULE_NAME###"] == @entrypoints[@globals["latest_supervised_module"]]) + if (###SYSCALL_FILTER###){ + printf("\n*SUPERVISED FLOW: syscall '%s' called in '%s' from '%s'; which entered at depth %d;\nThe supervised module is %s which entered the stack in depth %d;\r\n", @sysname[args->id], @globals["current_module"], "###MODULE_NAME###", @entrypoints["###MODULE_NAME###"], @globals["latest_supervised_module"], @entrypoints[@globals["latest_supervised_module"]]); + ###SUPERVISED_MODULES_FILTER### + ###SUPERVISED_MODULES_ACTION### + } + } +} +/* ###MODULE_NAME### END */ \ No newline at end of file diff --git a/src/secimport/templates/actions/kill_on_processing.d b/src/secimport/templates/dtrace/actions/kill_on_processing.d similarity index 100% rename from src/secimport/templates/actions/kill_on_processing.d rename to src/secimport/templates/dtrace/actions/kill_on_processing.d diff --git a/src/secimport/templates/actions/kill_process.d b/src/secimport/templates/dtrace/actions/kill_process.d similarity index 100% rename from src/secimport/templates/actions/kill_process.d rename to src/secimport/templates/dtrace/actions/kill_process.d diff --git a/src/secimport/templates/actions/log_file_system.d b/src/secimport/templates/dtrace/actions/log_file_system.d similarity index 100% rename from src/secimport/templates/actions/log_file_system.d rename to src/secimport/templates/dtrace/actions/log_file_system.d diff --git a/src/secimport/templates/actions/log_network.d b/src/secimport/templates/dtrace/actions/log_network.d similarity index 100% rename from src/secimport/templates/actions/log_network.d rename to src/secimport/templates/dtrace/actions/log_network.d diff --git a/src/secimport/templates/actions/log_python_module_entry.d b/src/secimport/templates/dtrace/actions/log_python_module_entry.d similarity index 100% rename from src/secimport/templates/actions/log_python_module_entry.d rename to src/secimport/templates/dtrace/actions/log_python_module_entry.d diff --git a/src/secimport/templates/actions/log_python_module_exit.d b/src/secimport/templates/dtrace/actions/log_python_module_exit.d similarity index 100% rename from src/secimport/templates/actions/log_python_module_exit.d rename to src/secimport/templates/dtrace/actions/log_python_module_exit.d diff --git a/src/secimport/templates/actions/log_syscall.d b/src/secimport/templates/dtrace/actions/log_syscall.d similarity index 100% rename from src/secimport/templates/actions/log_syscall.d rename to src/secimport/templates/dtrace/actions/log_syscall.d diff --git a/src/secimport/templates/default.allowlist.template.d b/src/secimport/templates/dtrace/default.allowlist.template.d similarity index 100% rename from src/secimport/templates/default.allowlist.template.d rename to src/secimport/templates/dtrace/default.allowlist.template.d diff --git a/src/secimport/templates/default.blocklist.template.d b/src/secimport/templates/dtrace/default.blocklist.template.d similarity index 100% rename from src/secimport/templates/default.blocklist.template.d rename to src/secimport/templates/dtrace/default.blocklist.template.d diff --git a/src/secimport/templates/default.template.d b/src/secimport/templates/dtrace/default.template.d similarity index 100% rename from src/secimport/templates/default.template.d rename to src/secimport/templates/dtrace/default.template.d diff --git a/src/secimport/templates/default.yaml.template.d b/src/secimport/templates/dtrace/default.yaml.template.d similarity index 100% rename from src/secimport/templates/default.yaml.template.d rename to src/secimport/templates/dtrace/default.yaml.template.d diff --git a/src/secimport/templates/filters/file_system.d b/src/secimport/templates/dtrace/filters/file_system.d similarity index 100% rename from src/secimport/templates/filters/file_system.d rename to src/secimport/templates/dtrace/filters/file_system.d diff --git a/src/secimport/templates/filters/is_current_module_under_supervision.d b/src/secimport/templates/dtrace/filters/is_current_module_under_supervision.d similarity index 100% rename from src/secimport/templates/filters/is_current_module_under_supervision.d rename to src/secimport/templates/dtrace/filters/is_current_module_under_supervision.d diff --git a/src/secimport/templates/filters/networking.d b/src/secimport/templates/dtrace/filters/networking.d similarity index 100% rename from src/secimport/templates/filters/networking.d rename to src/secimport/templates/dtrace/filters/networking.d diff --git a/src/secimport/templates/filters/processes.d b/src/secimport/templates/dtrace/filters/processes.d similarity index 100% rename from src/secimport/templates/filters/processes.d rename to src/secimport/templates/dtrace/filters/processes.d diff --git a/src/secimport/templates/generate_profile.d b/src/secimport/templates/dtrace/generate_profile.d similarity index 100% rename from src/secimport/templates/generate_profile.d rename to src/secimport/templates/dtrace/generate_profile.d diff --git a/src/secimport/templates/headers/destructive.d b/src/secimport/templates/dtrace/headers/destructive.d similarity index 100% rename from src/secimport/templates/headers/destructive.d rename to src/secimport/templates/dtrace/headers/destructive.d diff --git a/src/secimport/templates/probes/module_syscalls_allowlist_template.d b/src/secimport/templates/dtrace/probes/module_syscalls_allowlist_template.d similarity index 100% rename from src/secimport/templates/probes/module_syscalls_allowlist_template.d rename to src/secimport/templates/dtrace/probes/module_syscalls_allowlist_template.d diff --git a/src/secimport/templates/py_sandbox.d b/src/secimport/templates/dtrace/py_sandbox.d similarity index 100% rename from src/secimport/templates/py_sandbox.d rename to src/secimport/templates/dtrace/py_sandbox.d diff --git a/tests/test_bpftrace_backend.py b/tests/test_bpftrace_backend.py new file mode 100644 index 0000000..bf98808 --- /dev/null +++ b/tests/test_bpftrace_backend.py @@ -0,0 +1,154 @@ +import unittest +import os +import sys + + +from secimport.backends.bpftrace_backend.bpftrace_backend import ( + render_bpftrace_template, + create_bpftrace_script_for_module, + run_bpftrace_script_for_module, +) + + +class TestEBPFBackend(unittest.TestCase): + def test_run_bpftrace_script_for_module(self): + bpftrace_script_file_path = create_bpftrace_script_for_module( + "this", + allow_networking=False, + allow_shells=False, + log_file_system=True, + log_syscalls=True, + log_network=True, + log_python_calls=True, + destructive=True, + syscalls_allowlist=None, + ) + self.assertEqual( + bpftrace_script_file_path, "/tmp/.secimport/bpftrace_sandbox_this.bt" + ) + self.assertTrue(os.path.exists(bpftrace_script_file_path)) + bpftrace_file_content = open(bpftrace_script_file_path).read() + self.assertTrue("system" in bpftrace_file_content) + self.assertTrue(sys.executable in bpftrace_file_content) + + def test_create_bpftrace_script_for_module(self): + # create_bpftrace_script_for_module + # TODO: implement + bpftrace_script_file_path = create_bpftrace_script_for_module( + module_name="urllib", + allow_networking=False, + allow_shells=False, + log_file_system=True, + log_syscalls=True, + log_network=True, + log_python_calls=True, + destructive=True, + syscalls_allowlist=None, + ) + bpftrace_file_content = open(bpftrace_script_file_path).read() + # print(bpftrace_file_content) + # Making sure all the template variables (start with '###') were successfully replaced in the function. + self.assertTrue("###" not in bpftrace_file_content) + + def test_create_bpftrace_script_for_module_with_syscalls_allowlist(self): + syscall_allowlist = """ + __mac_syscall + __pthread_canceled + bind + csrctl + fgetattrlist + getattrlist + getrlimit + listen + pipe + sendmsg_nocancel + shm_open + sigreturn + socketpair + sysctlbyname + __disable_threadsignal + accept + access + bsdthread_create + bsdthread_terminate + connect_nocancel + kqueue + openat + proc_info + readlink + recvfrom + shutdown + thread_selfid + gettimeofday + issetugid + select_nocancel + socket + write + getsockname + recvfrom_nocancel + sendto + kevent + psynch_cvsignal + psynch_cvwait + sysctl + sendto_nocancel + fcntl_nocancel + setsockopt + lstat64 + fstatfs64 + stat64 + getdirentries64 + munmap + read_nocancel + """.split() + bpftrace_script_file_path = create_bpftrace_script_for_module( + module_name="http", + allow_networking=False, + allow_shells=False, + log_file_system=True, + log_syscalls=True, + log_network=True, + log_python_calls=True, + destructive=True, + syscalls_allowlist=syscall_allowlist, + ) + bpftrace_file_content = open(bpftrace_script_file_path).read() + self.assertTrue("system(" in bpftrace_file_content) + self.assertTrue("__mac_syscall" in bpftrace_file_content) + self.assertTrue("read_nocancel" in bpftrace_file_content) + + def test_create_bpftrace_script_for_module_with_syscalls_blocklist(self): + syscall_blocklist = """ + bind + """.split() + bpftrace_script_file_path = create_bpftrace_script_for_module( + module_name="http", + allow_networking=False, + allow_shells=False, + log_file_system=True, + log_syscalls=True, + log_network=True, + log_python_calls=True, + destructive=True, + syscalls_blocklist=syscall_blocklist, + ) + bpftrace_file_content = open(bpftrace_script_file_path).read() + self.assertTrue("system(" in bpftrace_file_content) + self.assertTrue("bind" in bpftrace_file_content) + + def test_render_bpftrace_template(self): + bpftrace_file_content = render_bpftrace_template( + module_traced_name="http", + allow_networking=False, + allow_shells=False, + log_file_system=True, + log_syscalls=True, + log_network=True, + log_python_calls=True, + destructive=True, + ) + self.assertTrue("system(" in bpftrace_file_content) + self.assertTrue("http" in bpftrace_file_content) + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_dtrace_backend.py b/tests/test_dtrace_backend.py new file mode 100644 index 0000000..78166c4 --- /dev/null +++ b/tests/test_dtrace_backend.py @@ -0,0 +1,126 @@ +import unittest +from secimport.backends.dtrace_backend.dtrace_backend import ( + PROFILES_DIR_NAME, + build_module_sandbox_from_yaml_template, + create_dtrace_script_for_module, +) +import os + + +class TestDtraceBackend(unittest.TestCase): + def test_create_dtrace_script_for_module(self): + dtrace_script_file_path = create_dtrace_script_for_module( + "this", + allow_networking=False, + allow_shells=False, + log_file_system=True, + log_syscalls=True, + log_network=True, + log_python_calls=True, + destructive=True, + syscalls_allowlist=None, + ) + self.assertEqual(dtrace_script_file_path, "/tmp/.secimport/dtrace_sandbox_this.d") + self.assertTrue(os.path.exists(dtrace_script_file_path)) + dtrace_file_content = open(dtrace_script_file_path).read() + self.assertTrue("#pragma D option destructive" in dtrace_file_content) + + def test_non_destructive_mode_removes_destructive_header(self): + dtrace_script_file_path = create_dtrace_script_for_module( + "urllib", + allow_networking=False, + allow_shells=False, + log_file_system=True, + log_syscalls=True, + log_network=True, + log_python_calls=True, + destructive=False, + syscalls_allowlist=None, + ) + dtrace_file_content = open(dtrace_script_file_path).read() + self.assertTrue("###DESTRUCTIVE###" not in dtrace_file_content) + + def test_syscall_allowlist_script_generation(self): + syscall_allowlist = """ + __mac_syscall + __pthread_canceled + bind + csrctl + fgetattrlist + getattrlist + getrlimit + listen + pipe + sendmsg_nocancel + shm_open + sigreturn + socketpair + sysctlbyname + __disable_threadsignal + accept + access + bsdthread_create + bsdthread_terminate + connect_nocancel + kqueue + openat + proc_info + readlink + recvfrom + shutdown + thread_selfid + gettimeofday + issetugid + select_nocancel + socket + write + getsockname + recvfrom_nocancel + sendto + kevent + psynch_cvsignal + psynch_cvwait + sysctl + sendto_nocancel + fcntl_nocancel + setsockopt + lstat64 + fstatfs64 + stat64 + getdirentries64 + munmap + read_nocancel + """.split() + dtrace_script_file_path = create_dtrace_script_for_module( + "http", + allow_networking=False, + allow_shells=False, + log_file_system=True, + log_syscalls=True, + log_network=True, + log_python_calls=True, + destructive=False, + syscalls_allowlist=syscall_allowlist, + ) + dtrace_file_content = open(dtrace_script_file_path).read() + self.assertTrue("#pragma D option destructive" in dtrace_file_content) + self.assertTrue("__mac_syscall" in dtrace_file_content) + self.assertTrue("read_nocancel" in dtrace_file_content) + + def test_build_module_sandbox_from_yaml_template(self): + profile_file_path = PROFILES_DIR_NAME / "example.yaml" + module_sandbox_code: str = build_module_sandbox_from_yaml_template( + profile_file_path + ) + self.assertTrue("fastapi" in module_sandbox_code) + self.assertTrue("requests" in module_sandbox_code) + self.assertTrue("uvicorn" in module_sandbox_code) + # with open("/tmp/.secimport/example_sandbox.d", "w") as example_sandbox: + # example_sandbox.write(module_sandbox_code) + + self.assertIsInstance(module_sandbox_code, str) + self.assertFalse("###SUPERVISED_MODULES_PROBES###" in module_sandbox_code) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_sandbox_helper.py b/tests/test_sandbox_helper.py index e16c6ec..3490cf9 100644 --- a/tests/test_sandbox_helper.py +++ b/tests/test_sandbox_helper.py @@ -1,9 +1,90 @@ -from unittest import TestCase -from secimport.sandbox_helper import secure_import, create_dtrace_script_for_module -import os - - -class TestSecImport(TestCase): +import unittest + +from secimport.sandbox_helper import secure_import + +EXAMPLE_SYSCALL_LIST = """ +access +exit +getentropy +getrlimit +mprotect +shm_open +sysctlbyname +write +fcntl_nocancel +readlink +fcntl +mmap +fstatfs64 +getdirentries64 +read_nocancel +madvise +close_nocancel +open_nocancel +sigaction +close +open +lseek +read +fstat64 +ioctl +stat64 + __mac_syscall +__pthread_canceled +bind +csrctl +fgetattrlist +getattrlist +getrlimit +listen +pipe +sendmsg_nocancel +shm_open +sigreturn +socketpair +sysctlbyname +__disable_threadsignal +accept +access +bsdthread_create +bsdthread_terminate +connect_nocancel +kqueue +openat +proc_info +readlink +recvfrom +shutdown +thread_selfid +gettimeofday +issetugid +select_nocancel +socket +write +getsockname +recvfrom_nocancel +sendto +kevent +psynch_cvsignal +psynch_cvwait +sysctl +sendto_nocancel +fcntl_nocancel +setsockopt +lstat64 +fstatfs64 +stat64 +getdirentries64 +munmap +read_nocancel +dup2 +ftruncate +chdir +sigaltstack +""".split() + + +class TestSecImport(unittest.TestCase): def test_import_with_shell_true(self): secure_import("urllib") a = [_**9 for _ in range(100)] @@ -13,237 +94,24 @@ def test_import_with_shell_false(self): module = secure_import("this") self.assertEqual(module.__name__, "this") - def test_create_dtrace_script_for_module(self): - dtrace_script_file_path = create_dtrace_script_for_module( - "this", - allow_networking=False, - allow_shells=False, - log_file_system=True, - log_syscalls=True, - log_network=True, - log_python_calls=True, - destructive=True, - syscalls_allowlist=None, - ) - self.assertEqual(dtrace_script_file_path, "/tmp/.secimport/sandbox_this.d") - self.assertTrue(os.path.exists(dtrace_script_file_path)) - dtrace_file_content = open(dtrace_script_file_path).read() - self.assertTrue("#pragma D option destructive" in dtrace_file_content) - - def test_non_destructive_mode_removes_destructive_header(self): - dtrace_script_file_path = create_dtrace_script_for_module( - "urllib", - allow_networking=False, - allow_shells=False, - log_file_system=True, - log_syscalls=True, - log_network=True, - log_python_calls=True, - destructive=False, - syscalls_allowlist=None, - ) - dtrace_file_content = open(dtrace_script_file_path).read() - self.assertTrue("###DESTRUCTIVE###" not in dtrace_file_content) - - def test_syscall_allowlist_script_generation(self): - syscall_allowlist = """ - __mac_syscall - __pthread_canceled - bind - csrctl - fgetattrlist - getattrlist - getrlimit - listen - pipe - sendmsg_nocancel - shm_open - sigreturn - socketpair - sysctlbyname - __disable_threadsignal - accept - access - bsdthread_create - bsdthread_terminate - connect_nocancel - kqueue - openat - proc_info - readlink - recvfrom - shutdown - thread_selfid - gettimeofday - issetugid - select_nocancel - socket - write - getsockname - recvfrom_nocancel - sendto - kevent - psynch_cvsignal - psynch_cvwait - sysctl - sendto_nocancel - fcntl_nocancel - setsockopt - lstat64 - fstatfs64 - stat64 - getdirentries64 - munmap - read_nocancel - """.split() - dtrace_script_file_path = create_dtrace_script_for_module( - "http", - allow_networking=False, - allow_shells=False, - log_file_system=True, - log_syscalls=True, - log_network=True, - log_python_calls=True, - destructive=False, - syscalls_allowlist=syscall_allowlist, - ) - dtrace_file_content = open(dtrace_script_file_path).read() - self.assertTrue("#pragma D option destructive" in dtrace_file_content) - self.assertTrue("__mac_syscall" in dtrace_file_content) - self.assertTrue("read_nocancel" in dtrace_file_content) - def test_syscall_allowlist_secure_import(self): module = secure_import( module_name="http", - syscalls_allowlist=""" - access - exit - getentropy - getrlimit - mprotect - shm_open - sysctlbyname - write - fcntl_nocancel - readlink - fcntl - mmap - fstatfs64 - getdirentries64 - read_nocancel - madvise - close_nocancel - open_nocancel - sigaction - close - open - lseek - read - fstat64 - ioctl - stat64 - __mac_syscall - __pthread_canceled - bind - csrctl - fgetattrlist - getattrlist - getrlimit - listen - pipe - sendmsg_nocancel - shm_open - sigreturn - socketpair - sysctlbyname - __disable_threadsignal - accept - access - bsdthread_create - bsdthread_terminate - connect_nocancel - kqueue - openat - proc_info - readlink - recvfrom - shutdown - thread_selfid - gettimeofday - issetugid - select_nocancel - socket - write - getsockname - recvfrom_nocancel - sendto - kevent - psynch_cvsignal - psynch_cvwait - sysctl - sendto_nocancel - fcntl_nocancel - setsockopt - lstat64 - fstatfs64 - stat64 - getdirentries64 - munmap - read_nocancel - dup2 - ftruncate - chdir - sigaltstack - """.split(), + syscalls_allowlist=EXAMPLE_SYSCALL_LIST, + log_file_system=True, + log_python_calls=True, ) self.assertEqual(module.__name__, "http") - def test_render_probe_for_module(self): - from secimport.sandbox_helper import _render_probe_for_module - - probe_text = _render_probe_for_module( - module_name="this.py", - destructive=True, - syscalls_allowlist=["open", "accept", "select"], - ) - with open("/tmp/.secimport/example_probe.d", "w") as example_probe: - example_probe.write(probe_text) - - self.assertTrue("open" in probe_text) - self.assertTrue("accept" in probe_text) - self.assertTrue("select" in probe_text) - self.assertTrue("kill" in probe_text) - - probe_text = _render_probe_for_module( - module_name="yaml.py", + def test_syscall_blocklist_secure_import(self): + # Only log violations + module = secure_import( + module_name="http", + syscalls_blocklist=EXAMPLE_SYSCALL_LIST, destructive=False, - syscalls_allowlist=["read", "ioctl", "fstat64"], ) - - self.assertTrue("fstat64" in probe_text) - self.assertTrue("read" in probe_text) - self.assertTrue("ioctl" in probe_text) - self.assertFalse("kill" in probe_text) - - def test_build_module_sandbox_from_yaml_template(self): - from secimport.sandbox_helper import ( - build_module_sandbox_from_yaml_template, - PROFILES_DIR_NAME, - ) - - profile_file_path = PROFILES_DIR_NAME / "example.yaml" - module_sandbox_code: str = build_module_sandbox_from_yaml_template( - profile_file_path - ) - self.assertTrue("fastapi" in module_sandbox_code) - self.assertTrue("requests" in module_sandbox_code) - self.assertTrue("uvicorn" in module_sandbox_code) - # with open("/tmp/.secimport/example_sandbox.d", "w") as example_sandbox: - # example_sandbox.write(module_sandbox_code) - - self.assertIsInstance(module_sandbox_code, str) - self.assertFalse("###SUPERVISED_MODULES_PROBES###" in module_sandbox_code) + self.assertEqual(module.__name__, "http") -if __name__ == '__main__': - \ No newline at end of file +if __name__ == "__main__": + unittest.main()