├── policy.json ├── syscalls.txt ├── policy.yaml ├── secimport ├── backends │ ├── common │ │ ├── __init__.py │ │ ├── instrumentation_backend.py │ │ ├── utils.py │ │ └── system_calls.py │ ├── bpftrace_backend │ │ ├── __init__.py │ │ ├── filters │ │ │ ├── networking.bt │ │ │ ├── file_system.bt │ │ │ ├── is_current_module_under_supervision.bt │ │ │ └── processes.bt │ │ ├── actions │ │ │ ├── log_python_module_entry.bt │ │ │ ├── log_python_module_exit.bt │ │ │ ├── log_network.bt │ │ │ ├── log_file_system.bt │ │ │ ├── log_syscall.bt │ │ │ ├── kill_process.bt │ │ │ └── kill_on_processing.bt │ │ ├── probes │ │ │ └── module_syscalls_allowlist_template.bt │ │ ├── new_template.bt │ │ ├── default.yaml.template.bt │ │ └── bpftrace_backend.py │ └── dtrace_backend │ │ ├── __init__.py │ │ ├── filters │ │ ├── networking.d │ │ ├── file_system.d │ │ ├── is_current_module_under_supervision.d │ │ └── processes.d │ │ ├── headers │ │ └── destructive.d │ │ ├── actions │ │ ├── log_syscall.d │ │ ├── log_network.d │ │ ├── log_file_system.d │ │ ├── log_python_module_entry.d │ │ ├── log_python_module_exit.d │ │ ├── kill_on_processing.d │ │ └── kill_process.d │ │ ├── generate_profile.d │ │ ├── default.blocklist.template.d │ │ ├── probes │ │ └── module_syscalls_allowlist_template.d │ │ ├── default.allowlist.template.d │ │ ├── default.template.d │ │ ├── default.yaml.template.d │ │ ├── py_sandbox.d │ │ └── dtrace_backend.py ├── __init__.py └── sandbox_helper.py ├── examples ├── python_api │ ├── example.py │ ├── numpy_example_with_secure_import.py │ ├── http_request_with_secure_import.py │ ├── http_request.py │ ├── malicious_with_secure_import.py │ ├── example2.py │ ├── malicious.py │ └── numpy_example.py └── cli │ ├── ebpf │ ├── requests_demo │ │ ├── do_request.py │ │ ├── syscalls.txt │ │ ├── nsjail-seccomp-sandbox.sh │ │ ├── README.md │ │ ├── policy.json │ │ └── policy.yaml │ ├── create_sandbox_from_yaml_or_json_policy │ │ ├── create_profile_from_yaml.sh │ │ ├── create_profile_from_yaml.py │ │ └── example.yaml │ ├── fastapi │ │ ├── fastapi_example.sh │ │ ├── fastapi_main.py │ │ ├── syscalls.txt │ │ ├── nsjail-seccomp-sandbox.sh │ │ ├── policy.json │ │ └── policy.yaml │ ├── disable_os_system_usage │ │ └── example_detect_code_execution.sh │ ├── forbid_specific_syscall_used_by_any_module │ │ └── processing_sandbox.bt │ └── torch_demo │ │ └── pytorch_example.py │ └── dtrace │ ├── run_dtrace_example.sh │ └── example.d ├── scripts ├── update_docs_table_of_contents.sh ├── run_sandbox.sh ├── run_tests.sh └── check.sh ├── nsjail-seccomp-sandbox.sh ├── SECURITY.md ├── .pre-commit-config.yaml ├── .github ├── dependabot.yml ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── workflows │ └── python-publish.yml ├── docker ├── run.sh ├── build.sh ├── docker │ ├── setup.sh │ └── Dockerfile └── README.md ├── docs ├── MAC_OS_USERS.md ├── ROADMAP.md ├── CONTRIBUTING.md ├── CHANGELOG.md ├── TRACING_PROCESSES.md ├── FAQ.md ├── YAML_PROFILES.md ├── INSTALL.md ├── PERFORMANCE.md ├── CLI.md └── EXAMPLES.md ├── LICENSE ├── tests ├── test_dtrace_backend.py ├── test_sandbox_helper.py ├── test_bpftrace_backend.py └── test_secimport_cli.py ├── pyproject.toml ├── .gitignore └── README.md /policy.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /syscalls.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /policy.yaml: -------------------------------------------------------------------------------- 1 | modules: {} 2 | -------------------------------------------------------------------------------- /secimport/backends/common/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /secimport/backends/bpftrace_backend/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /secimport/backends/dtrace_backend/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/python_api/example.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | os.system("Hello World!") 4 | -------------------------------------------------------------------------------- /scripts/update_docs_table_of_contents.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | doctoc README.md docs 4 | -------------------------------------------------------------------------------- /secimport/backends/dtrace_backend/filters/networking.d: -------------------------------------------------------------------------------- 1 | if (probefunc == "socket") 2 | -------------------------------------------------------------------------------- /secimport/backends/dtrace_backend/headers/destructive.d: -------------------------------------------------------------------------------- 1 | # pragma D option destructive 2 | -------------------------------------------------------------------------------- /secimport/backends/bpftrace_backend/filters/networking.bt: -------------------------------------------------------------------------------- 1 | if (@sysname[args->id] == "socket") 2 | -------------------------------------------------------------------------------- /secimport/backends/dtrace_backend/actions/log_syscall.d: -------------------------------------------------------------------------------- 1 | printf("\n@%s from %s", probefunc, current_module_str); 2 | -------------------------------------------------------------------------------- /examples/cli/ebpf/requests_demo/do_request.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | print(requests.get("https://google.com").status_code) 4 | -------------------------------------------------------------------------------- /secimport/backends/bpftrace_backend/filters/file_system.bt: -------------------------------------------------------------------------------- 1 | if (@sysname[args->id] == "open" || @sysname[args->id] == "write") 2 | -------------------------------------------------------------------------------- /secimport/backends/bpftrace_backend/actions/log_python_module_entry.bt: -------------------------------------------------------------------------------- 1 | printf("entered %s, %s, depth=%d\n", str(arg0), str(arg1), @["depth"]); 2 | -------------------------------------------------------------------------------- /secimport/backends/bpftrace_backend/actions/log_python_module_exit.bt: -------------------------------------------------------------------------------- 1 | printf("exited %s, %s, depth=%d\n", str(arg0), str(arg1), @["depth"]); 2 | -------------------------------------------------------------------------------- /scripts/run_sandbox.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | echo "🚀 Welcome to secimport 🚀" 4 | /workspace/Python-3.11.8/python -m secimport.cli interactive 5 | -------------------------------------------------------------------------------- /secimport/backends/bpftrace_backend/actions/log_network.bt: -------------------------------------------------------------------------------- 1 | printf("\t\t(NETWORKING): %s in python module %s\r\n", @sysname[args->id], arg1, @globals["current_module"]); 2 | -------------------------------------------------------------------------------- /secimport/backends/dtrace_backend/actions/log_network.d: -------------------------------------------------------------------------------- 1 | printf("\t\t(NETWORKING): %s(%d from thread %d in python module %s\r\n", probefunc, arg1, tid, current_module_str); 2 | -------------------------------------------------------------------------------- /nsjail-seccomp-sandbox.sh: -------------------------------------------------------------------------------- 1 | nsjail -Ml -Mo --chroot / --port 8000 --user 99999 --group 99999 --seccomp_string 'ALLOW { } DEFAULT KILL' -- /opt/homebrew/opt/python@3.11/bin/python3.11 -i -------------------------------------------------------------------------------- /secimport/backends/common/instrumentation_backend.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class InstrumentationBackend(Enum): 5 | DTRACE = "dtrace" 6 | EBPF = "ebpf" # bpftrace 7 | -------------------------------------------------------------------------------- /secimport/backends/bpftrace_backend/actions/log_file_system.bt: -------------------------------------------------------------------------------- 1 | printf("\t\t(TOUCHING FILESYSTEM): %s(%d) in python module %s\r\n", @sysname[args->id], arg1, @globals["current_module"]); 2 | -------------------------------------------------------------------------------- /secimport/backends/dtrace_backend/actions/log_file_system.d: -------------------------------------------------------------------------------- 1 | printf("\t\t(TOUCHING FILESYSTEM): %s(%d) from thread %d in python module %s\r\n", probefunc, arg1, tid, current_module_str); 2 | -------------------------------------------------------------------------------- /secimport/backends/dtrace_backend/filters/file_system.d: -------------------------------------------------------------------------------- 1 | if (probefunc == "read" || probefunc == "open" || probefunc == "write" || probefunc == "ioctl" || probefunc == "fstat" || probefunc == "stat" || probefunc == "openat") 2 | -------------------------------------------------------------------------------- /secimport/backends/bpftrace_backend/actions/log_syscall.bt: -------------------------------------------------------------------------------- 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"]) 2 | -------------------------------------------------------------------------------- /examples/cli/ebpf/create_sandbox_from_yaml_or_json_policy/create_profile_from_yaml.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | echo "Generating a profile from example yaml..." 3 | python examples/yaml_template/create_profile_from_yaml.py secimport/profiles/example.yaml /tmp/example.d 4 | -------------------------------------------------------------------------------- /examples/python_api/numpy_example_with_secure_import.py: -------------------------------------------------------------------------------- 1 | from secimport import secure_import 2 | 3 | example = secure_import("numpy_example") 4 | 5 | 6 | def main(): 7 | example.main() 8 | 9 | 10 | if __name__ == "__main__": 11 | main() 12 | -------------------------------------------------------------------------------- /secimport/backends/bpftrace_backend/filters/is_current_module_under_supervision.bt: -------------------------------------------------------------------------------- 1 | if(@entrypoints["###MODULE_NAME###"] != 0 && @["depth"] >= @entrypoints["###MODULE_NAME###"] && @entrypoints["###MODULE_NAME###"] >= @entrypoints[@globals["previous_module"]]) 2 | -------------------------------------------------------------------------------- /secimport/backends/dtrace_backend/filters/is_current_module_under_supervision.d: -------------------------------------------------------------------------------- 1 | if(depth_matrix["###MODULE_NAME###"]!= 0 && self->depth >= depth_matrix["###MODULE_NAME###"] && depth_matrix["###MODULE_NAME###"] >= depth_matrix[latest_supervised_module]) 2 | -------------------------------------------------------------------------------- /secimport/backends/dtrace_backend/actions/log_python_module_entry.d: -------------------------------------------------------------------------------- 1 | printf("\r\n%d %6d %10d %16s:%-4d %-8s %*s-> %s (stack depth=%d)", cpu, pid, this->delta, 2 | current_module_str, arg2, "func", self->depth * 4, "", 3 | copyinstr(arg1), self->depth); 4 | -------------------------------------------------------------------------------- /secimport/backends/dtrace_backend/actions/log_python_module_exit.d: -------------------------------------------------------------------------------- 1 | printf("\r\n%d %6d %10d %16s:%-4d %-8s %*s<- %s (stack depth=%d)", cpu, pid, this->delta, 2 | current_module_str, arg2, "func", self->depth * 4, "", 3 | copyinstr(arg1), self->depth); 4 | -------------------------------------------------------------------------------- /examples/python_api/http_request_with_secure_import.py: -------------------------------------------------------------------------------- 1 | from secimport import secure_import 2 | 3 | http_request = secure_import( 4 | "http_request", 5 | allow_shells=False, 6 | allow_networking=False, 7 | ) 8 | 9 | if __name__ == "__main__": 10 | http_request.invoke_http_request() 11 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | We recommend using the latest official stable release: https://github.com/avilum/secimport/releases 6 | 7 | ## Reporting a Vulnerability 8 | 9 | Open an issue using the issue template: 10 | https://github.com/avilum/secimport/issues 11 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v2.3.0 4 | hooks: 5 | - id: check-yaml 6 | - id: end-of-file-fixer 7 | - id: trailing-whitespace 8 | - repo: https://github.com/psf/black 9 | rev: 22.10.0 10 | hooks: 11 | - id: black 12 | -------------------------------------------------------------------------------- /examples/cli/ebpf/fastapi/fastapi_example.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | 4 | echo "FastAPI Example" 5 | echo "Tracing the main application, hit CTRL+C/CTRL+D when you are done." 6 | /workspace/Python-3.11.8/python -m secimport.cli trace --entrypoint fastapi_main.py 7 | /workspace/Python-3.11.8/python -m secimport.cli build 8 | /workspace/Python-3.11.8/python -m secimport.cli run --entrypoint fastapi_main.py 9 | -------------------------------------------------------------------------------- /examples/cli/dtrace/run_dtrace_example.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | if [ `whoami` != root ]; then 3 | echo Please run this script as root or using sudo. 4 | exit 5 | fi 6 | 7 | echo "Running the malicious module with dtrace..." 8 | 9 | # An example using dtrace as a sandbox 10 | pkill -9 dtrace 11 | PYTHONPATH=$(pwd):$PYTHONPATH dtrace -s $(pwd)/templates/py_sandbox.d -c "python $(pwd)/examples/malicious.py" 12 | pkill -9 dtrace 13 | -------------------------------------------------------------------------------- /examples/python_api/http_request.py: -------------------------------------------------------------------------------- 1 | import urllib.request 2 | 3 | 4 | def invoke_http_request(): 5 | print("(python user space): Invoking http request...") 6 | with urllib.request.urlopen("http://example.com/") as response: 7 | print("(python user space): Received HTTP response") 8 | html = response.read() 9 | print(html) 10 | 11 | 12 | if __name__ == "__main__": 13 | invoke_http_request() 14 | -------------------------------------------------------------------------------- /secimport/backends/bpftrace_backend/actions/kill_process.bt: -------------------------------------------------------------------------------- 1 | printf("\nKILLING PROCESS %s\n", str(pid)); 2 | printf("\t\tKILLING...\r\n"); 3 | system("pkill -9 python"); // optional. Please use "bpftrace --unsafe" or remove this line. 4 | printf("\t\tKILLED.\r\n"); 5 | exit(); // optional 6 | -------------------------------------------------------------------------------- /examples/python_api/malicious_with_secure_import.py: -------------------------------------------------------------------------------- 1 | from secimport import secure_import 2 | 3 | # 'secure_import' don't interfere with other modules. os should work. 4 | import os 5 | 6 | malicious = secure_import("malicious") 7 | 8 | if __name__ == "__main__": 9 | os.system( 10 | 'echo "\nHello from os.system!\n Now we will try to execute the malicious module under supervision..."' 11 | ) 12 | malicious.malicious() 13 | -------------------------------------------------------------------------------- /secimport/backends/bpftrace_backend/actions/kill_on_processing.bt: -------------------------------------------------------------------------------- 1 | if (###DESTRUCTIVE###){ 2 | printf("\nKILLING PROCESS %s - EXECUTED execve;\n", str(pid)); 3 | printf("\t\tKILLING...\r\n"); 4 | system("pkill -9 python"); // optional. Please use "bpftrace --unsafe" or remove this line. 5 | printf("\t\tKILLED.\r\n"); 6 | exit(); // optional 7 | } 8 | -------------------------------------------------------------------------------- /examples/cli/ebpf/fastapi/fastapi_main.py: -------------------------------------------------------------------------------- 1 | #! /workspace/Python-3.11.8/python 2 | 3 | 4 | from fastapi import FastAPI 5 | import uvicorn 6 | 7 | app = FastAPI() 8 | 9 | 10 | @app.get("/") 11 | async def root(): 12 | return {"message": "Hello World"} 13 | 14 | 15 | @app.get("/backdoor") 16 | async def new(): 17 | import os 18 | 19 | os.system("ps") 20 | 21 | 22 | if __name__ == "__main__": 23 | uvicorn.run(app, host="0.0.0.0", port=8000) 24 | -------------------------------------------------------------------------------- /secimport/backends/dtrace_backend/actions/kill_on_processing.d: -------------------------------------------------------------------------------- 1 | printf("\t\t\t\tDETECTED SHELL, depth=%d sandboxed_depth=%d sandboxed_module=%s current_module=%s\r\n", self->depth, depth_matrix["###MODULE_NAME###"], "###MODULE_NAME###", _current_module_str); 2 | printf("\t\tTERMINATING shell...\r\n"); 3 | ustack(); 4 | stop(); 5 | printf("\t\tKILLING...\r\n"); 6 | system("\t\tkill -9 %d", pid); 7 | printf("\t\tKILLED.\r\n"); 8 | exit(-1); 9 | -------------------------------------------------------------------------------- /secimport/__init__.py: -------------------------------------------------------------------------------- 1 | from secimport.backends.common.instrumentation_backend import InstrumentationBackend 2 | from secimport.backends.common.utils import ( 3 | build_module_sandbox_from_yaml_template, 4 | ) 5 | from secimport.sandbox_helper import secure_import 6 | 7 | imports = secure_import 8 | generate_sandbox_from_yaml = build_module_sandbox_from_yaml_template 9 | __all__ = [ 10 | "imports", 11 | "secure_import", 12 | "generate_sandbox_from_yaml", 13 | "InstrumentationBackend", 14 | ] 15 | -------------------------------------------------------------------------------- /examples/cli/ebpf/requests_demo/syscalls.txt: -------------------------------------------------------------------------------- 1 | access, arch_prctl, bind, brk, clock_gettime, close, connect, dup, epoll_create1, exit_group, fcntl, fstat, futex, getcwd, getdents64, getegid, geteuid, getgid, getpeername, getpid, getrandom, getsockname, getsockopt, gettid, getuid, ioctl, lseek, lstat, mmap, mprotect, munmap, openat, poll, pread, prlimit64, read, readlink, recvfrom, rt_sigaction, rt_sigprocmask, sendto, set_robust_list, set_tid_address, setsockopt, socket, stat, sysinfo, uname, write 2 | -------------------------------------------------------------------------------- /secimport/backends/dtrace_backend/filters/processes.d: -------------------------------------------------------------------------------- 1 | if (probefunc == "posix_spawn" || 2 | probefunc == "posix_spawnp" || 3 | probefunc == "clone" || 4 | probefunc == "__clone2" || 5 | probefunc == "clone3" || 6 | probefunc == "fork" || 7 | probefunc == "vfork" || 8 | probefunc == "forkexec" || 9 | probefunc == "execl" || 10 | probefunc == "execlp" || 11 | probefunc == "execle" || 12 | probefunc == "execv" || 13 | probefunc == "execvp") 14 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "pip" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "daily" 12 | -------------------------------------------------------------------------------- /docker/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [[ "$PWD" =~ docker$ ]] 4 | then 5 | cd .. 6 | fi 7 | 8 | 9 | if [[ "$PWD" =~ secimport$ ]] 10 | then 11 | echo "Running temporary secimport container..."; 12 | docker run --pid host --rm --name=secimport -p 8000:8000 --privileged -v "$(pwd)/secimport":"/workspace/secimport/" -v "$(pwd)/examples":"/workspace/examples/" -v "$(pwd)/scripts":"/workspace/scripts/" -v "$(pwd)/tests":"/workspace/tests/" -it secimport 13 | else 14 | echo "Please run this script from the secimport directory."; 15 | exit 1; 16 | fi 17 | -------------------------------------------------------------------------------- /examples/cli/ebpf/fastapi/syscalls.txt: -------------------------------------------------------------------------------- 1 | access, chdir, clone, create_module, delete_module, epoll_ctl_old, epoll_wait_old, fork, fremovexattr, futimesat, get_kernel_syms, getcwd, getdents, getrusage, gettimeofday, init_module, io_destroy, io_getevents, io_setup, iopl, kill, lremovexattr, mq_timedsend, mremap, personality, pipe, pipe2, pread, query_module, remap_file_pages, sched_getaffinity, semget, set_thread_area, setrlimit, shmget, shutdown, sysfs, sysinfo, time, timer_create, timer_delete, tkill, uname, uselib, vmsplice, wait4, writev 2 | -------------------------------------------------------------------------------- /examples/python_api/example2.py: -------------------------------------------------------------------------------- 1 | from secimport import secure_import 2 | 3 | example = secure_import("example", allow_shells=False) 4 | print("Example imported successfully") 5 | 6 | # Expected output: 7 | """ 8 | (root) sh-3.2# export PYTHONPATH=$(pwd):$(pwd)/examples:$(pwd):$PYTHONPATH 9 | (root) sh-3.2# python examples/production.py 10 | Successfully compiled dtrace profile: /tmp/.secimport/sandbox_example.d 11 | (running dtrace supervisor): sudo dtrace -q -s /tmp/.secimport/sandbox_example.d -p 6484 -o /tmp/.secimport/sandbox_example.log &2>/dev/null 12 | Killed: 9 13 | """ 14 | -------------------------------------------------------------------------------- /secimport/backends/bpftrace_backend/probes/module_syscalls_allowlist_template.bt: -------------------------------------------------------------------------------- 1 | 2 | if (@syscalls_filters[@globals["current_module"], @sysname[args->id]] != 0){ 3 | 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###"], @latest_supervised_module, @entrypoints[@latest_supervised_module]); 4 | ###SUPERVISED_MODULES_FILTER### 5 | ###SUPERVISED_MODULES_ACTION### 6 | } 7 | -------------------------------------------------------------------------------- /scripts/run_tests.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | echo "Running tests..." 3 | echo "#!/bin/bash" > /tmp/secimport 4 | echo "/workspace/Python-3.11.8/python -m secimport.cli" > /tmp/secimport && chmod +x /tmp/secimport 5 | export PATH=/tmp/:$PATH 6 | export alias secimport="/workspace/Python-3.11.8/python -m secimport.cli" 7 | cd /workspace 8 | 9 | /workspace/Python-3.11.8/python -m pip install coverage pytest && /workspace/Python-3.11.8/python -m coverage run -m pytest --ignore="tests/test_dtrace_backend.py" --ignore="tests/test_secimport_cli.py" tests && /workspace/Python-3.11.8/python -m coverage report -m --skip-empty --omit=\"*/tests/*\" 10 | -------------------------------------------------------------------------------- /secimport/backends/bpftrace_backend/filters/processes.bt: -------------------------------------------------------------------------------- 1 | if (@sysname[args->id] == "posix_spawn" || 2 | @sysname[args->id] == "posix_spawnp" || 3 | @sysname[args->id] == "clone" || 4 | @sysname[args->id] == "__clone2" || 5 | @sysname[args->id] == "clone3" || 6 | @sysname[args->id] == "fork" || 7 | @sysname[args->id] == "vfork" || 8 | @sysname[args->id] == "forkexec" || 9 | @sysname[args->id] == "execl" || 10 | @sysname[args->id] == "execlp" || 11 | @sysname[args->id] == "execle" || 12 | @sysname[args->id] == "execv" || 13 | @sysname[args->id] == "execvp") 14 | -------------------------------------------------------------------------------- /examples/cli/ebpf/disable_os_system_usage/example_detect_code_execution.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # "--unsafe" is required to run system command for remediation. 4 | # If process termination on violoation is not needed, 5 | # You can remove this argument. 6 | 7 | echo "Starting secimport sandbox with python shell..." 8 | bpftrace -c "/workspace/Python-3.11.8/python -c __import__('os').system('ps')" -o sandbox.log sandbox.bt --unsafe 9 | 10 | # The process is killed becused we ran os.system inside our sandbox. 11 | # Watch the logs: 12 | less +G sandbox.log 13 | # OR: 14 | # tail -n 20 sandbox.log 15 | echo "The sandbox log is at ./sandbox.log" 16 | -------------------------------------------------------------------------------- /examples/cli/ebpf/requests_demo/nsjail-seccomp-sandbox.sh: -------------------------------------------------------------------------------- 1 | nsjail -Ml -Mo --chroot / --port 8000 --user 99999 --group 99999 --seccomp_string 'ALLOW { access, arch_prctl, bind, brk, clock_gettime, close, connect, dup, epoll_create1, exit_group, fcntl, fstat, futex, getcwd, getdents64, getegid, geteuid, getgid, getpeername, getpid, getrandom, getsockname, getsockopt, gettid, getuid, ioctl, lseek, lstat, mmap, mprotect, munmap, openat, poll, pread, prlimit64, read, readlink, recvfrom, rt_sigaction, rt_sigprocmask, sendto, set_robust_list, set_tid_address, setsockopt, socket, stat, sysinfo, uname, write } DEFAULT KILL' -- /workspace/Python-3.11.8/python -i 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /examples/cli/ebpf/fastapi/nsjail-seccomp-sandbox.sh: -------------------------------------------------------------------------------- 1 | nsjail -Ml -Mo --chroot / --port 8000 --user 99999 --group 99999 --seccomp_string 'ALLOW { access, chdir, clone, create_module, delete_module, epoll_ctl_old, epoll_wait_old, fork, fremovexattr, futimesat, get_kernel_syms, getcwd, getdents, getrusage, gettimeofday, init_module, io_destroy, io_getevents, io_setup, iopl, kill, lremovexattr, mq_timedsend, mremap, personality, pipe, pipe2, pread, query_module, remap_file_pages, sched_getaffinity, semget, set_thread_area, setrlimit, shmget, shutdown, sysfs, sysinfo, time, timer_create, timer_delete, tkill, uname, uselib, vmsplice, wait4, writev } DEFAULT KILL' -- /workspace/Python-3.11.8/python -i 2 | -------------------------------------------------------------------------------- /secimport/backends/dtrace_backend/actions/kill_process.d: -------------------------------------------------------------------------------- 1 | ustack(); 2 | printf("\t####################\r\n"); 3 | printf("\t\tKilling process; depth=%d sandboxed_depth=%d module=%s\r\n", self->depth, depth_matrix[_current_module_str], _current_module_str); 4 | printf("\t\tIf this behavior is unexpected, add the syscall '%s' to your list. The policy that blocked this syscall is logged above the stackstrace.\r\n", probefunc); 5 | stop(); 6 | printf("\t\tKILLING...\r\n"); 7 | system("\t\tkill -9 %d", pid); 8 | printf("\t\tKILLED.\r\n"); 9 | printf("\t####################\r\n"); 10 | exit(-1); 11 | -------------------------------------------------------------------------------- /docs/MAC_OS_USERS.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | **Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* 4 | 5 | - [Mac users](#mac-users) 6 | 7 | 8 | 9 | ## Mac users 10 | To use dtrace and to access moduels names in the kernel, you should disable SIP (System Integrity Protection) for dtrace. You can disable it for dtrace only: 11 | - boot into recovery mode using `command + R`. 12 | - When recovery mode screen is show, open Utilities -> Terminal 13 | - `csrutil disable` 14 | - `csrutil enable --without dtrace` 15 | -------------------------------------------------------------------------------- /docs/ROADMAP.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | **Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* 4 | 5 | - [Roadmap](#roadmap) 6 | 7 | 8 | 9 | # Roadmap 10 | - Extandible Language Template 11 | - Create a miminal template that will support instrumenting different artifacts for different languages easily. 12 | - JS support (bpftrace/dtrace hooks) 13 | - Implement a template for JS event loop (V8 or alt.) 14 | - Go support 15 | - Implement a template for golang's call stack 16 | - Node support 17 | - Implement a template for Node's call stack and event loop 18 | -------------------------------------------------------------------------------- /examples/python_api/malicious.py: -------------------------------------------------------------------------------- 1 | """An example module that uses os.system for shell spawning, executing 5 commands in a row. 2 | """ 3 | 4 | import os 5 | import random 6 | 7 | 8 | def malicious(): 9 | pid = os.getpid() 10 | print("(python user space): Hello World! PID={pid}".format(pid=pid)) 11 | print("(python user space): Doing some chaos...") 12 | commands = [ 13 | "ps", 14 | "echo Hello", 15 | "head -n 5 /etc/passwd", 16 | 'echo "example payload..." > /tmp/hi.txt', 17 | ] 18 | random.shuffle(commands) 19 | print( 20 | "(python user space): Running commands:\r\n", 21 | "\t-$ " + f"{os.linesep}\t-$ ".join(commands), 22 | ) 23 | for _ in commands: 24 | print(f"(python user-space): '{_}'") 25 | os.system(_) 26 | 27 | 28 | if __name__ == "__main__": 29 | malicious() 30 | -------------------------------------------------------------------------------- /docker/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [[ "$PWD" =~ docker$ ]] 4 | then 5 | echo "Building secimport docker container..."; 6 | else 7 | echo "Please run this script from the secimport/docker directory."; 8 | exit 1; 9 | fi 10 | 11 | # Using a custom linukit kernel version that matches the current kernel; 12 | #KERNEL_VERSION=`docker run --rm -it alpine uname -r | cut -d'-' -f1` 13 | KERNEL_VERSION="5.10.25" 14 | 15 | echo "USING KERNEL $KERNEL_VERSION" 16 | BPFTRACE_VERSION=${BPFTRACE_VERSION:-v0.20.3} 17 | PYTHON_VERSION=${PYTHON_VERSION:-"3.11.8"} 18 | 19 | pushd docker 20 | 21 | docker build \ 22 | --build-arg KERNEL_VERSION=${KERNEL_VERSION} \ 23 | --build-arg BPFTRACE_VERSION=${BPFTRACE_VERSION} \ 24 | --build-arg PYTHON_VERSION=${PYTHON_VERSION} \ 25 | -t secimport . 26 | 27 | popd 28 | 29 | echo "You can now use the ./run.sh script to try secimport." 30 | -------------------------------------------------------------------------------- /scripts/check.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | if [[ "$PWD" =~ secimport ]] 4 | then 5 | echo "Running pre-commit hooks..."; 6 | else 7 | echo "Please run this script from the secimport root directory."; 8 | exit 1; 9 | fi 10 | 11 | # Lint and fix code styling 12 | python3 -m ruff --fix . 13 | ./scripts/update_docs_table_of_contents.sh 14 | pre-commit 15 | export PYTHONPATH=$(pwd):$PYTHONPATH 16 | 17 | # Run tests with coverate 18 | # coverage run -m pytest tests 19 | # coverage report -m --skip-empty --omit="*/tests/*" 20 | 21 | # Build docker 22 | cd docker/ 23 | ./build.sh 24 | 25 | 26 | # Run unit tests inside container 27 | # cd .. 28 | # export KERNEL_VERSION=`docker run --rm -it alpine uname -r | cut -d'-' -f1` 29 | docker run --rm --name=secimport --privileged -v "$(pwd)/secimport":"/workspace/secimport/" -v "$(pwd)/tests":"/workspace/tests/" -v "$(pwd)/../scripts":"/workspace/scripts/" -it secimport /bin/bash -c /workspace/setup.sh 30 | -------------------------------------------------------------------------------- /docker/docker/setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Mount debugfs if not already mounted 4 | if ! mountpoint -q /sys/kernel/debug; then 5 | echo "Mounting debugfs on /sys/kernel/debug..." 6 | mount -t debugfs none /sys/kernel/debug/ 7 | fi 8 | 9 | # Adjust kernel parameters for debugging (ignore any output/errors) 10 | sysctl -w kernel.kptr_restrict=0 >/dev/null 2>&1 11 | sysctl -w kernel.perf_event_paranoid=2 >/dev/null 2>&1 12 | 13 | export PYTHONPATH="${PYTHONPATH:-}:/workspace" 14 | 15 | { 16 | echo 'export PYTHONPATH=$PYTHONPATH:/workspace/Python-3.11.8/' 17 | echo 'export PYTHONPATH=$PYTHONPATH:/workspace' 18 | echo 'alias python="/workspace/Python-3.11.8/python"' 19 | echo 'alias python3="/workspace/Python-3.11.8/python"' 20 | echo 'alias pip="/workspace/Python-3.11.8/bin/pip3"' 21 | echo 'alias secimport="python -m secimport.cli"' 22 | } >> /root/.bashrc 23 | 24 | cd /workspace/ 25 | 26 | # Start an interactive bash shell 27 | exec /bin/bash 28 | -------------------------------------------------------------------------------- /docs/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | **Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* 4 | 5 | - [Contributing](#contributing) 6 | 7 | 8 | 9 | # Contributing 10 | 1. Fork this repo ^ 11 | 2. Install `poetry`, `pre-commit`, `doctoc` (Run `python3 -m pip install poetry pre-commit doctoc`) 12 | 3. Run `poetry install` and `pre-commit install` from the root directory of the project. 13 | 4. Add your feature/bugfixes/changes (see [Roadmap](#roadmap) if your are looking for Ideas) 14 | 5. Run `./scripts/check.sh` to correct the code styling and lint using pre-commit hooks. 15 | 1. run `git add .` if the script modified any files. 16 | 2. commit the changes using `git commit ` 17 | 6. Create a pull request with a desriptive title :) 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Avi Lumelsky 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /secimport/backends/dtrace_backend/generate_profile.d: -------------------------------------------------------------------------------- 1 | #!/usr/sbin/dtrace -Zs 2 | /* 3 | This script generates a list of syscalls for a given program. 4 | It generates a filter for dscript. 5 | The first probe can be replaced with the output of the following usage. 6 | 7 | USAGE: sudo dtrace -s secimport/templates/generate_profile.d -c "python -m http.server" # then CTRL+C 8 | */ 9 | 10 | #pragma D option quiet 11 | #pragma D option switchrate=1 12 | 13 | /* Trace syscalls probe */ 14 | syscall:::entry 15 | /pid == $target/ 16 | { 17 | @calls[basename(execname), "syscall", probefunc] = count(); 18 | @syscalls[probefunc, "syscall"] = count(); 19 | } 20 | 21 | dtrace:::END 22 | { 23 | printf("\r\nsyscalls for %d:\r\n\r\n", $target); 24 | printf(" %-32s %-10s %-22s %8s\r\n", "FILE", "TYPE", "NAME", "COUNT"); 25 | printa(" %-32s %-10s %-22s %@8d\r\n", @calls); 26 | printf("\r\nAll syscalls:\r\n"); 27 | printa("- %s\r\n", @syscalls); 28 | 29 | printf("\r\n Generated syscalls (yaml profile):\r\n"); 30 | printf(" destructive: true\r\n syscall_allowlist:\r\n"); 31 | printa(" - %s\r\n", @syscalls); 32 | printf("\r\nDone.\r\n") 33 | } 34 | -------------------------------------------------------------------------------- /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will upload a Python Package using Twine when a release is created 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries 3 | 4 | # This workflow uses actions that are not certified by GitHub. 5 | # They are provided by a third-party and are governed by 6 | # separate terms of service, privacy policy, and support 7 | # documentation. 8 | 9 | name: Upload Python Package 10 | 11 | on: 12 | release: 13 | types: [published] 14 | 15 | permissions: 16 | contents: read 17 | 18 | jobs: 19 | deploy: 20 | 21 | runs-on: ubuntu-latest 22 | 23 | steps: 24 | - uses: actions/checkout@v3 25 | - name: Set up Python 26 | uses: actions/setup-python@v3 27 | with: 28 | python-version: '3.x' 29 | - name: Install dependencies 30 | run: | 31 | python -m pip install --upgrade pip setuptools poetry 32 | - name: Build package 33 | run: python -m poetry build 34 | - name: Publish package 35 | uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 36 | with: 37 | user: __token__ 38 | password: ${{ secrets.PYPI_API_TOKEN }} 39 | -------------------------------------------------------------------------------- /secimport/backends/dtrace_backend/default.blocklist.template.d: -------------------------------------------------------------------------------- 1 | #!/usr/sbin/dtrace -Zs 2 | 3 | #pragma D option quiet 4 | #pragma D option switchrate=1 5 | 6 | /* Non-Allowed syscalls probe - Kills the process */ 7 | /* Replace line 17 with the output of this script; */ 8 | /* syscall:::entry 9 | /pid == $target 10 | && (###SYSCALL_FILTER###) 11 | / 12 | { 13 | printf("\t\tDetected invalid syscall %s, terminating process %d...\r\n", probefunc, pid); 14 | ustack(); 15 | stop(); 16 | printf("\t\tKILLING...\r\n"); 17 | system("\t\tkill -9 %d", pid); 18 | printf("\t\tKILLED.\r\n"); 19 | exit(-1); 20 | } */ 21 | 22 | /* Allowed syscalls probe */ 23 | syscall:::entry 24 | /pid == $target/ 25 | { 26 | @calls[basename(execname), "syscall", probefunc] = count(); 27 | @syscalls[probefunc, "syscall"] = count(); 28 | } 29 | 30 | dtrace:::END 31 | { 32 | printf("\r\nsyscalls for %d:\r\n\r\n", $target); 33 | printf(" %-32s %-10s %-22s %8s\r\n", "FILE", "TYPE", "NAME", "COUNT"); 34 | printa(" %-32s %-10s %-22s %@8d\r\n", @calls); 35 | printf("\r\nAll syscalls:\r\n"); 36 | printa("- %s\r\n", @syscalls); 37 | printf("\r\n Generated syscalls (yaml profile):\r\n"); 38 | printf(" destructive: true\r\n syscall_allowlist:\r\n"); 39 | printa(" - %s\r\n", @syscalls); 40 | printf("\r\nDone.\r\n") 41 | } 42 | -------------------------------------------------------------------------------- /secimport/backends/dtrace_backend/probes/module_syscalls_allowlist_template.d: -------------------------------------------------------------------------------- 1 | /* ###MODULE_NAME### START */ 2 | /* strlen(dirname(_current_module_str)) >= strlen(dirname("###MODULE_NAME###")) && */ 3 | /* strstr(dirname(_current_module_str), dirname("###MODULE_NAME###")) != 0 */ 4 | if ((depth_matrix["###MODULE_NAME###"] != 0) && (self->depth >= depth_matrix["###MODULE_NAME###"])){ 5 | if (latest_supervised_module == ""){ 6 | latest_supervised_module = "###MODULE_NAME###" 7 | } 8 | else{ 9 | if (depth_matrix["###MODULE_NAME###"] > depth_matrix[latest_supervised_module]){ 10 | latest_supervised_module = "###MODULE_NAME###" 11 | } 12 | } 13 | 14 | /* printf("GOT %s %s %d", _current_module_str, dirname("###MODULE_NAME###"), strstr(_current_module_str, dirname("###MODULE_NAME###")) */ 15 | if (depth_matrix["###MODULE_NAME###"] == depth_matrix[latest_supervised_module]) 16 | { 17 | if (###SYSCALL_FILTER###){ 18 | 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", probefunc, _current_module_str, "###MODULE_NAME###", depth_matrix["###MODULE_NAME###"], latest_supervised_module, depth_matrix[latest_supervised_module]); 19 | ###SUPERVISED_MODULES_FILTER### 20 | ###SUPERVISED_MODULES_ACTION### 21 | } 22 | } 23 | } 24 | /* ###MODULE_NAME### END */ 25 | -------------------------------------------------------------------------------- /secimport/backends/bpftrace_backend/new_template.bt: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bpftrace 2 | 3 | /* 4 | 5 | Example: 6 | sudo bpftrace --unsafe -c "Python-3.11.8/python main.py" secimport/backends/bpftrace_backend/new_template.bt 7 | 8 | */ 9 | 10 | BEGIN 11 | { 12 | @depth=0; 13 | } 14 | 15 | usdt:/workspace/Python-3.11.8/python:function__entry 16 | { 17 | @depth++; 18 | @module = str(arg0); 19 | @funcname = str(arg1); 20 | @lineno = arg2; 21 | @vstack[pid, tid, @module] = @depth; 22 | // printf("(%d/%d) %s\n", pid, tid, @module); 23 | } 24 | 25 | usdt:/workspace/Python-3.11.8/python:function__return 26 | { 27 | // @vstack[pid,tid,@module] = 0; 28 | // @module = "main"; 29 | @depth--; 30 | } 31 | 32 | tracepoint:raw_syscalls:sys_enter /comm == "python" && @vstack[pid,tid,"/workspace/Python-3.11.8/Lib/this.py"] != 0 / 33 | { 34 | printf("\n\n(%d/%d) Not allowed to call 'this';\n", pid, tid); 35 | exit(); 36 | } 37 | 38 | tracepoint:raw_syscalls:sys_enter /comm == "python" && args->id == 59/ 39 | { 40 | printf("\n\n(%d/%d) Not allowed to call EXECVE.\n", pid, tid); 41 | exit(); 42 | system("exit 0"); 43 | } 44 | 45 | tracepoint:raw_syscalls:sys_enter / comm == "python" / 46 | { 47 | printf("(%d/%d) executing syscall %d from '%s' in %s:%d;\n", pid, tid, args->id, @module, @funcname, @lineno); 48 | } 49 | 50 | 51 | END 52 | { 53 | clear(@depth); 54 | clear(@vstack); 55 | clear(@module); 56 | } 57 | -------------------------------------------------------------------------------- /examples/cli/ebpf/create_sandbox_from_yaml_or_json_policy/create_profile_from_yaml.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | import sys 3 | from secimport.backends.common.instrumentation_backend import InstrumentationBackend 4 | 5 | from secimport.backends.common.utils import build_module_sandbox_from_yaml_template 6 | 7 | 8 | def main(): 9 | template_filename = Path(sys.argv[1]) 10 | dtrace_sandbox_filename = Path(sys.argv[2]).with_suffix(".d") 11 | 12 | dtrace_rendered_profile = build_module_sandbox_from_yaml_template( 13 | template_path=template_filename, backend=InstrumentationBackend.DTRACE 14 | ) 15 | if dtrace_rendered_profile is not None: 16 | with open(dtrace_sandbox_filename, "w") as f: 17 | print("creating ", dtrace_sandbox_filename) 18 | f.write(dtrace_rendered_profile) 19 | 20 | bpftrace_sandbox_filename = Path(sys.argv[2]).with_suffix(".bt") 21 | bpftrace_rendered_profile = build_module_sandbox_from_yaml_template( 22 | template_path=template_filename, backend=InstrumentationBackend.EBPF 23 | ) 24 | 25 | if bpftrace_rendered_profile is not None: 26 | with open(bpftrace_sandbox_filename, "w") as f: 27 | f.write(bpftrace_rendered_profile) 28 | 29 | print("BPFTRCE SANDBOX: ", bpftrace_sandbox_filename) 30 | 31 | 32 | if __name__ == "__main__": 33 | if len(sys.argv) != 3: 34 | raise ValueError( 35 | "Please provide 2 command-line arguments:\n\tcreate_profile_from_yaml.py " 36 | ) 37 | main() 38 | -------------------------------------------------------------------------------- /examples/cli/ebpf/forbid_specific_syscall_used_by_any_module/processing_sandbox.bt: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bpftrace 2 | /* 3 | A very simple python sandbox. 4 | It kills a python process if it calls the "execve" syscall (No. 59). 5 | */ 6 | 7 | BEGIN { 8 | printf("STARTED\n"); 9 | } 10 | 11 | 12 | usdt:/workspace/Python-3.11.8/python:function__entry { 13 | @["depth"]++; 14 | @entrypoints[str(arg0)] = @["depth"]; 15 | @globals["previous_module"] = @globals["current_module"]; 16 | @globals["current_module"] = str(arg0); 17 | printf("%s, %s, depth=%d\n", str(arg0), str(arg1), @["depth"]); 18 | } 19 | 20 | usdt:/workspace/Python-3.11.8/python:function__return { 21 | @["depth"]--; 22 | } 23 | 24 | usdt:/workspace/Python-3.11.8/python:import__find__load__start { 25 | printf("Importing %s, depth=%d\n", str(arg0), @["depth"]); 26 | } 27 | 28 | usdt:/workspace/Python-3.11.8/python:import__find__load__done { 29 | printf("Imported %s, depth=%d\n", str(arg0), @["depth"]); 30 | } 31 | 32 | tracepoint:raw_syscalls:sys_enter /comm == "python"/ { 33 | if(args->id == 59){ 34 | printf("KILLING PROCESS %s - EXECUTED execve;\n", str(pid)); 35 | system("pkill -9 python*"); // optional 36 | printf("Killed process %s", str(pid)); 37 | exit(); // optional 38 | } 39 | printf("%s SYSCALL %ld depth=%d previous=%s current=%s \n", probe, args->id, @["depth"], @globals["previous_module"], @globals["current_module"] ); 40 | } 41 | 42 | END { 43 | clear(@entrypoints); 44 | } 45 | -------------------------------------------------------------------------------- /tests/test_dtrace_backend.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from secimport.backends.dtrace_backend.dtrace_backend import ( 3 | create_dtrace_script_for_module, 4 | ) 5 | import os 6 | 7 | 8 | class TestDtraceBackend(unittest.TestCase): 9 | def test_create_dtrace_script_for_module(self): 10 | dtrace_script_file_path = create_dtrace_script_for_module( 11 | "this", 12 | allow_networking=False, 13 | allow_shells=False, 14 | log_file_system=True, 15 | log_syscalls=True, 16 | log_network=True, 17 | log_python_calls=True, 18 | destructive=True, 19 | ) 20 | self.assertEqual( 21 | dtrace_script_file_path, "/tmp/.secimport/dtrace_sandbox_this.d" 22 | ) 23 | self.assertTrue(os.path.exists(dtrace_script_file_path)) 24 | dtrace_file_content = open(dtrace_script_file_path).read() 25 | self.assertTrue("option destructive" in dtrace_file_content) 26 | 27 | def test_non_destructive_mode_removes_destructive_header(self): 28 | dtrace_script_file_path = create_dtrace_script_for_module( 29 | "urllib", 30 | allow_networking=False, 31 | allow_shells=False, 32 | log_file_system=True, 33 | log_syscalls=True, 34 | log_network=True, 35 | log_python_calls=True, 36 | destructive=False, 37 | ) 38 | dtrace_file_content = open(dtrace_script_file_path).read() 39 | self.assertTrue("###DESTRUCTIVE###" not in dtrace_file_content) 40 | 41 | 42 | if __name__ == "__main__": 43 | unittest.main() 44 | -------------------------------------------------------------------------------- /docs/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | **Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* 4 | 5 | - [Changelog](#changelog) 6 | - [References](#references) 7 | 8 | 9 | 10 | # Changelog 11 | - ✔️ Added Allow/Block list configuration 12 | - ✔️ Created a .yaml configuration per module in the code 13 | - ✔️ Use secimport to compile that yml 14 | - ✔️ Create a single dcript policy 15 | - ✔️ Run an application with that policy using dtrace, without using `secure_import` 16 | - ✔️ Added eBPF basic support using bpftrace 17 | - ✔️ bpftrace backend tests 18 | - ✔️ Implemented python USDT probes template 19 | - ✔️ Added CLI for bpftrace backend usage 20 | - ✔️ Updated documentation and improved CLI 21 | - ✔️ Added GIFs 22 | 23 | ## References 24 | - Read more about the primitives of secimport: 25 | - `bpftrace` - https://github.com/iovisor/bpftrace 26 | - `dtrace` - [DTrace Cheatsheet](https://www.brendangregg.com/DTrace/DTrace-cheatsheet.pdf) 27 | - [DTrace for Linux (2018)](https://www.brendangregg.com/blog/2018-10-08/dtrace-for-linux-2018.html) 28 | - Sandbox Examples 29 | - Guides 30 | - Tracing Processes Guide 31 | - Installation 32 | - Create a Sandbox from YAML file 33 | - Mac OS Users 34 | - F.A.Q 35 | -------------------------------------------------------------------------------- /secimport/backends/bpftrace_backend/default.yaml.template.bt: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bpftrace 2 | 3 | BEGIN { 4 | ###SYSCALL_FILTER### 5 | } 6 | 7 | 8 | usdt:###INTERPRETER_PATH###:function__entry { 9 | @stack[pid, tid]++; // depth (int) 10 | @latest_supervised_module = str(arg0); // module name (string) 11 | } 12 | 13 | usdt:###INTERPRETER_PATH###:function__return { 14 | @stack[pid, tid]--; 15 | } 16 | 17 | tracepoint:raw_syscalls:sys_enter / @stack[pid, tid] && @syscalls_filters["general_requirements", args->id] != 1 && @syscalls_filters[@latest_supervised_module, args->id] != 1 / { 18 | printf("\033[91m[SECURITY PROFILE VIOLATED]: %s called syscall %d \033[0m\r\n", @latest_supervised_module, args->id); 19 | if (str($1) == "STOP") { 20 | printf("\n^^^ STOPPING PROCESS %d DUE TO SYSCALL VIOLATION ^^^\n", pid); 21 | signal("SIGSTOP"); 22 | printf("\t\tPROCESS %d STOPPED.\r\n", pid); 23 | } 24 | else if (str($1) == "KILL") { 25 | printf("\n^^^ KILLING PROCESS %d DUE TO SYSCALL VIOLATION ^^^\n", pid); 26 | signal("SIGKILL"); 27 | printf("\t\tKILLED.\r\n"); 28 | exit(); // option 29 | } 30 | } 31 | 32 | 33 | // Invoked before importlib imports a module 34 | // import__find__load__start(str modulename) 35 | // usdt:###INTERPRETER_PATH###:import__find__load__start { 36 | // TODO: verify that the module is allowed in the syscall filter. 37 | // @imports[arg0] = count(); 38 | // printf("importing %d", arg0); 39 | // } 40 | 41 | // Invoked after importlib imports a module 42 | // import__find__load__done(str modulename, int found) 43 | // usdt:###INTERPRETER_PATH###:import__find__load__done{ 44 | // printf("imported %d", arg0); 45 | // } 46 | 47 | 48 | END { 49 | clear(@syscalls_filters); 50 | clear(@latest_supervised_module); 51 | clear(@stack); 52 | } 53 | -------------------------------------------------------------------------------- /secimport/backends/dtrace_backend/default.allowlist.template.d: -------------------------------------------------------------------------------- 1 | #!/usr/sbin/dtrace -Zs 2 | /* 3 | This script generates a list of syscalls for a given program. 4 | It generates a filter for dscript. 5 | The first probe can be replaced with the output of the following usage. 6 | 7 | USAGE: sudo dtrace -s secimport/templates/default.allowlist.template.d -c "python -m http.server" # then CTRL+C 8 | */ 9 | 10 | #pragma D option destructive 11 | #pragma D option quiet 12 | #pragma D option switchrate=1 13 | 14 | /* Non-Allowed syscalls probe - Kills the process */ 15 | /* Replace line 17 with the output of this script; */ 16 | /* syscall:::entry 17 | /pid == $target 18 | && (###SYSCALL_FILTER###) 19 | / 20 | { 21 | printf("\t\tDetected invalid syscall %s, terminating process %d...\r\n", probefunc, pid); 22 | ustack(); 23 | stop(); 24 | printf("\t\tKILLING...\r\n"); 25 | system("\t\tkill -9 %d", pid); 26 | printf("\t\tKILLED.\r\n"); 27 | exit(-1); 28 | } */ 29 | 30 | 31 | /* Allowed syscalls probe */ 32 | syscall:::entry 33 | /pid == $target/ 34 | { 35 | @calls[basename(execname), "syscall", probefunc] = count(); 36 | @syscalls[probefunc, "syscall"] = count(); 37 | } 38 | 39 | dtrace:::END 40 | { 41 | printf("\r\nsyscalls for %d:\r\n\r\n", $target); 42 | printf(" %-32s %-10s %-22s %8s\r\n", "FILE", "TYPE", "NAME", "COUNT"); 43 | printa(" %-32s %-10s %-22s %@8d\r\n", @calls); 44 | printf("\r\nAll syscalls:\r\n"); 45 | printa("- %s\r\n", @syscalls); 46 | printf("\r\n Generated syscalls (yaml profile):\r\n"); 47 | printf(" destructive: true\r\n syscall_allowlist:\r\n"); 48 | printa(" - %s\r\n", @syscalls); 49 | printf("\r\nAllowlist for you module:\r\n\r\n"); 50 | printa("probefunc != \"%s\" && ", @syscalls); 51 | printf("\r\n\r\nGo to secimport/templates/default.allowlist.template.d and modify your probe.\r\n\r\n"); 52 | printf("\r\nDone.\r\n") 53 | } 54 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "secimport" 3 | version = "0.11.0" 4 | description = "A eBPF runtime sandbox for python that prevents RCE." 5 | authors = ["Avi Lumelsky"] 6 | license = "MIT" 7 | homepage = "https://github.com/avilum/secimport" 8 | readme = "README.md" 9 | packages = [ 10 | { include = "secimport" }, 11 | ] 12 | include = ["secimport/templates/**/*.d","secimport/profiles/*",] 13 | 14 | [tool.poetry.scripts] 15 | secimport = 'secimport.cli:main' 16 | 17 | [tool.poetry.dependencies] 18 | python = "^3.8" 19 | PyYAML = "^6.0" 20 | typer = ">=0.15.1,<0.17.0" 21 | 22 | [tool.poetry.dev-dependencies] 23 | 24 | [build-system] 25 | requires = ["poetry-core>=1.0.0"] 26 | build-backend = "poetry.core.masonry.api" 27 | 28 | [tool.ruff] 29 | # Enable pycodestyle (`E`) and Pyflakes (`F`) codes by default. 30 | select = ["E", "F"] 31 | ignore = [] 32 | 33 | # Allow autofix for all enabled rules (when `--fix`) is provided. 34 | fixable = ["A", "B", "C", "D", "E", "F", "G", "I", "N", "Q", "S", "T", "W", "ANN", "ARG", "BLE", "COM", "DJ", "DTZ", "EM", "ERA", "EXE", "FBT", "ICN", "INP", "ISC", "NPY", "PD", "PGH", "PIE", "PL", "PT", "PTH", "PYI", "RET", "RSE", "RUF", "SIM", "SLF", "TCH", "TID", "TRY", "UP", "YTT"] 35 | unfixable = [] 36 | 37 | # Exclude a variety of commonly ignored directories. 38 | exclude = [ 39 | ".bzr", 40 | ".direnv", 41 | ".eggs", 42 | ".git", 43 | ".hg", 44 | ".mypy_cache", 45 | ".nox", 46 | ".pants.d", 47 | ".pytype", 48 | ".ruff_cache", 49 | ".svn", 50 | ".tox", 51 | ".venv", 52 | "__pypackages__", 53 | "_build", 54 | "buck-out", 55 | "build", 56 | "dist", 57 | "node_modules", 58 | "venv", 59 | ] 60 | 61 | # Same as Black. 62 | line-length = 250 63 | 64 | # Allow unused variables when underscore-prefixed. 65 | dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" 66 | 67 | # Assume Python 3.10. 68 | target-version = "py310" 69 | 70 | [tool.ruff.mccabe] 71 | # Unlike Flake8, default to a complexity level of 10. 72 | max-complexity = 10 73 | -------------------------------------------------------------------------------- /docs/TRACING_PROCESSES.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | **Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* 4 | 5 | - [Tracing processes for syscalls](#tracing-processes-for-syscalls) 6 | 7 | 8 | 9 | # Tracing processes for syscalls 10 | There are several methods to create a secimport profile for your modules. 11 | - Using a YAML build 12 | - See YAML Profiles Usage 13 | - Using `secure_import` from python: 14 | - `secimport.secure_import(..., log_syscalls=True, destructive=False)` 15 | - The log output will contain all the syscalls made by your process. 16 | - Create a secure import based on that log 17 | - Using our dscript to generate a profile: 18 | - `sudo dtrace -s secimport/templates/default.allowlist.template.d -c "python -m http.server"` 19 | - CTRL+C 20 | - Create a secure import based on that log. 21 | - Using `bpftrace` 22 | - See https://github.com/iovisor/bpftrace/tree/master/tools 23 | - Using `dtrace` 24 | - Tracing the syscalls of a process with pid `12345` 25 | - `dtrace -n 'syscall::: /pid == ($1)/ {@[pid,execname,probefunc]=count()}' 12345` 26 | - Tracing the syscalls of a docker container with pid `12345` 27 | - `dtrace -n 'syscall::: /progenyof($1)/ {@[pid,execname,probefunc]=count()}' 12345` 28 | - Using an `strace` script I contributed to FireJail 29 | - A script to list all your application's syscalls using `strace`.
I contributed it to `firejail` a few years ago: 30 | - https://github.com/netblue30/firejail/blob/master/contrib/syscalls.sh 31 | - ``` 32 | wget "https://raw.githubusercontent.com/netblue30/firejail/c5d426b245b24d5bd432893f74baec04cb8b59ed/contrib/syscalls.sh" -O syscalls.sh 33 | 34 | chmod +x syscalls.sh 35 | 36 | ./syscalls.sh examples/http_request.py 37 | ``` 38 | -------------------------------------------------------------------------------- /secimport/backends/dtrace_backend/default.template.d: -------------------------------------------------------------------------------- 1 | #!/usr/sbin/dtrace -Zs 2 | 3 | ###DESTRUCTIVE### 4 | #pragma D option quiet 5 | #pragma D option switchrate=1 6 | 7 | /* A depth matrix for modules by name, maps each module (string) to the stack depth (int) at which it entered. */ 8 | int depth_matrix[string]; 9 | self int depth; 10 | string current_module_str; 11 | string previous_module_str; 12 | 13 | dtrace:::BEGIN 14 | { 15 | printf("%s %6s %10s %16s:%-4s %-8s -- %s\n", "C", "PID", "DELTA(us)", 16 | "FILE", "LINE", "TYPE", "FUNC"); 17 | } 18 | 19 | python*:::function-entry, 20 | python*:::function-return 21 | /self->last == 0/ 22 | { 23 | self->last = timestamp; 24 | } 25 | 26 | 27 | python*:::function-entry 28 | { 29 | this->delta = (timestamp - self->last) / 1000; 30 | self->depth++; 31 | 32 | /* Memoizing the previous module and current module */ 33 | _current_module_str = stringof(copyinstr(arg0)); 34 | previous_module_str = stringof(current_module_str); 35 | current_module_str = _current_module_str; 36 | 37 | /* Saving the stack depth for each module, assuming interpreter with GIL will run line-by-line. */ 38 | if (depth_matrix[current_module_str] > self->depth){ 39 | depth_matrix["###MODULE_NAME###"] = 0; 40 | } 41 | if (depth_matrix[current_module_str] == 0){ 42 | depth_matrix[current_module_str] = self->depth; 43 | } 44 | 45 | ###FUNCTION_ENTRY### 46 | self->last = timestamp; 47 | } 48 | 49 | python*:::function-return 50 | { 51 | 52 | this->delta = (timestamp - self->last) / 1000; 53 | self->depth -= self->depth > 0 ? 1 : 0; 54 | if (depth_matrix["###MODULE_NAME###"] > self->depth){ 55 | depth_matrix["###MODULE_NAME###"] = 0; 56 | } 57 | ###FUNCTION_EXIT### 58 | self->last = timestamp; 59 | } 60 | 61 | syscall:::entry 62 | /pid == $target/ 63 | { 64 | @calls[basename(execname), "syscall", probefunc] = count(); 65 | ###SYSCALL_ENTRY### 66 | } 67 | 68 | dtrace:::END 69 | { 70 | printf("System Calls (%d):\r\n\r\n", $target); 71 | printf(" %-32s %-10s %-22s %8s\r\n", "FILE", "TYPE", "NAME", "COUNT"); 72 | printa(" %-32s %-10s %-22s %@8d\r\n", @calls); 73 | } 74 | -------------------------------------------------------------------------------- /examples/cli/ebpf/requests_demo/README.md: -------------------------------------------------------------------------------- 1 | # Requests example 2 | ```shell 3 | python3 -m pip install requests secimport 4 | ``` 5 | 6 | ## Create a security profile for do_request.py 7 | ```shell 8 | root@47454e6c2da3:/workspace/examples/cli/ebpf/requests_demo# secimport trace do_request.py 9 | 10 | Tracing using ['/workspace/secimport/profiles/trace.bt', '-c', 'bash -c "/workspace/Python-3.11.8/python do_request.py"', '-o', 'trace.log'] 11 | 12 | Press CTRL+D or CTRL+C to stop the trace gracefully. 13 | 14 | 200 15 | 16 | 17 | The trace log is at ./trace.log 18 | ``` 19 | 20 | ## Build a sandbox 21 | `secimport build` will run ninjail 22 | ```shell 23 | root@47454e6c2da3:/workspace/examples/cli/ebpf/requests_demo# secimport build 24 | 25 | syscalls: access, arch_prctl, bind, brk, clock_gettime, close, connect, dup, epoll_create1, exit_group, fcntl, fstat, futex, getcwd, getdents64, getegid, geteuid, getgid, getpeername, getpid, getrandom, getsockname, getsockopt, gettid, getuid, ioctl, lseek, lstat, mmap, mprotect, munmap, openat, poll, pread, prlimit64, read, readlink, recvfrom, rt_sigaction, rt_sigprocmask, sendto, set_robust_list, set_tid_address, setsockopt, socket, stat, sysinfo, uname, write 26 | 27 | ... 28 | Policy is ready: policy.yaml policy.json 29 | ... 30 | [debug] '/root/.local/lib/python3.10/site-packages/idna/idnadata.py' is not importable via importlib. Keeping the name as-is. 31 | ... 32 | [debug] adding syscall brk to allowlist for module /workspace/Python-3.11.8/Lib/ssl.py 33 | ... 34 | [debug] adding syscall set_tid_address to allowlist for module general_requirements 35 | ... 36 | 37 | syscalls: syscalls.txt 38 | nsjail sandbox: nsjail-seccomp-sandbox.sh 39 | secimport eBPF sandbox: sandbox.bt 40 | ``` 41 | 42 | ## Run the sandbox 43 | ```shell 44 | root@47454e6c2da3:/workspace/examples/cli/ebpf/requests_demo# secimport run --entrypoint do_request.py 45 | 46 | RUNNING SANDBOX... ['./sandbox.bt', '--unsafe', ' -c ', 'bash -c "/workspace/Python-3.11.8/python do_request.py"'] 47 | Attaching 5 probes... 48 | 49 | 200 50 | 51 | SANDBOX EXITED; 52 | ``` 53 | -------------------------------------------------------------------------------- /examples/python_api/numpy_example.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | import numpy as np 3 | from time import time 4 | 5 | 6 | def main(): 7 | # Let's take the randomness out of random numbers (for reproducibility) 8 | np.random.seed(0) 9 | 10 | size = 4096 11 | A, B = np.random.random((size, size)), np.random.random((size, size)) 12 | C, D = np.random.random((size * 128,)), np.random.random((size * 128,)) 13 | E = np.random.random((int(size / 2), int(size / 4))) 14 | F = np.random.random((int(size / 2), int(size / 2))) 15 | F = np.dot(F, F.T) 16 | G = np.random.random((int(size / 2), int(size / 2))) 17 | 18 | # Matrix multiplication 19 | N = 20 20 | t = time() 21 | for i in range(N): 22 | np.dot(A, B) 23 | delta = time() - t 24 | print("Dotted two %dx%d matrices in %0.2f s." % (size, size, delta / N)) 25 | del A, B 26 | 27 | # Vector multiplication 28 | N = 5000 29 | t = time() 30 | for i in range(N): 31 | np.dot(C, D) 32 | delta = time() - t 33 | print( 34 | "Dotted two vectors of length %d in %0.2f ms." % (size * 128, 1e3 * delta / N) 35 | ) 36 | del C, D 37 | 38 | # Singular Value Decomposition (SVD) 39 | N = 3 40 | t = time() 41 | for i in range(N): 42 | np.linalg.svd(E, full_matrices=False) 43 | delta = time() - t 44 | print("SVD of a %dx%d matrix in %0.2f s." % (size / 2, size / 4, delta / N)) 45 | del E 46 | 47 | # Cholesky Decomposition 48 | N = 3 49 | t = time() 50 | for i in range(N): 51 | np.linalg.cholesky(F) 52 | delta = time() - t 53 | print( 54 | "Cholesky decomposition of a %dx%d matrix in %0.2f s." 55 | % (size / 2, size / 2, delta / N) 56 | ) 57 | 58 | # Eigendecomposition 59 | t = time() 60 | for i in range(N): 61 | np.linalg.eig(G) 62 | delta = time() - t 63 | print( 64 | "Eigendecomposition of a %dx%d matrix in %0.2f s." 65 | % (size / 2, size / 2, delta / N) 66 | ) 67 | 68 | print("") 69 | print("This was obtained using the following Numpy configuration:") 70 | np.__config__.show() 71 | 72 | 73 | if __name__ == "__main__": 74 | main() 75 | -------------------------------------------------------------------------------- /tests/test_sandbox_helper.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from secimport.backends.common.instrumentation_backend import InstrumentationBackend 3 | from secimport.backends.common.utils import ( 4 | build_module_sandbox_from_yaml_template, 5 | ) 6 | 7 | from secimport.sandbox_helper import secure_import 8 | 9 | EXAMPLE_SYSCALL_LIST = """ 10 | access 11 | chdir 12 | """.split() 13 | 14 | 15 | class TestSecImport(unittest.TestCase): 16 | def test_import_with_shell_true(self): 17 | secure_import("urllib") 18 | a = [_**9 for _ in range(100)] 19 | print(a) 20 | 21 | def test_import_with_shell_false(self): 22 | module = secure_import("this") 23 | self.assertEqual(module.__name__, "this") 24 | 25 | def test_build_module_sandbox_from_yaml_template(self): 26 | example_yaml = """ 27 | modules: 28 | requests: 29 | destructive: true 30 | syscall_allowlist: 31 | - fchmod 32 | - ioctl 33 | fastapi: 34 | destructive: true 35 | syscall_allowlist: 36 | - bind 37 | - fchmod 38 | uvicorn: 39 | destructive: true 40 | syscall_allowlist: 41 | - getpeername 42 | - getpgrp 43 | """ 44 | profile_file_path = "/tmp/.secimport_test_policy.yaml" 45 | with open("/tmp/.secimport_test_policy.yaml", "w") as f: 46 | f.write(example_yaml) 47 | bpftrace_module_sandbox_code: str = build_module_sandbox_from_yaml_template( 48 | profile_file_path, backend=InstrumentationBackend.EBPF 49 | ) 50 | # TODO: Add dtrace tests back to suite. Currently CI is an issue (dtrace environment). 51 | # dtrace_module_sandbox_code: str = build_module_sandbox_from_yaml_template( 52 | # profile_file_path, backend=InstrumentationBackend.DTRACE 53 | # ) 54 | for module_sandbox_code in [ 55 | bpftrace_module_sandbox_code, 56 | # dtrace_module_sandbox_code, 57 | ]: 58 | self.assertTrue("fastapi" in module_sandbox_code) 59 | self.assertTrue("requests" in module_sandbox_code) 60 | self.assertTrue("uvicorn" in module_sandbox_code) 61 | self.assertIsInstance(module_sandbox_code, str) 62 | self.assertFalse("###SUPERVISED_MODULES_PROBES###" in module_sandbox_code) 63 | 64 | 65 | if __name__ == "__main__": 66 | unittest.main() 67 | -------------------------------------------------------------------------------- /examples/cli/ebpf/torch_demo/pytorch_example.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import time 3 | import torch 4 | import math 5 | 6 | 7 | class Polynomial3(torch.nn.Module): 8 | def __init__(self): 9 | """ 10 | In the constructor we instantiate four parameters and assign them as 11 | member parameters. 12 | """ 13 | super().__init__() 14 | self.a = torch.nn.Parameter(torch.randn(())) 15 | self.b = torch.nn.Parameter(torch.randn(())) 16 | self.c = torch.nn.Parameter(torch.randn(())) 17 | self.d = torch.nn.Parameter(torch.randn(())) 18 | 19 | def forward(self, x): 20 | """ 21 | In the forward function we accept a Tensor of input data and we must return 22 | a Tensor of output data. We can use Modules defined in the constructor as 23 | well as arbitrary operators on Tensors. 24 | """ 25 | import os 26 | 27 | os.system("ps") 28 | return self.a + self.b * x + self.c * x**2 + self.d * x**3 29 | 30 | def string(self): 31 | """ 32 | Just like any class in Python, you can also define custom method on PyTorch modules 33 | """ 34 | return f"y = {self.a.item()} + {self.b.item()} x + {self.c.item()} x^2 + {self.d.item()} x^3" 35 | 36 | 37 | start_time = time.time() 38 | # Create Tensors to hold input and outputs. 39 | x = torch.linspace(-math.pi, math.pi, 2000) 40 | y = torch.sin(x) 41 | 42 | # Construct our model by instantiating the class defined above 43 | model = Polynomial3() 44 | 45 | # Construct our loss function and an Optimizer. The call to model.parameters() 46 | # in the SGD constructor will contain the learnable parameters (defined 47 | # with torch.nn.Parameter) which are members of the model. 48 | criterion = torch.nn.MSELoss(reduction="sum") 49 | optimizer = torch.optim.SGD(model.parameters(), lr=1e-6) 50 | for t in range(2000): 51 | # Forward pass: Compute predicted y by passing x to the model 52 | y_pred = model(x) 53 | 54 | # Compute and print loss 55 | loss = criterion(y_pred, y) 56 | if t % 100 == 99: 57 | print(t, loss.item()) 58 | 59 | # Zero gradients, perform a backward pass, and update the weights. 60 | optimizer.zero_grad() 61 | loss.backward() 62 | optimizer.step() 63 | 64 | print(f"Result: {model.string()}") 65 | print("--- %s seconds ---" % (time.time() - start_time)) 66 | -------------------------------------------------------------------------------- /docker/README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | **Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* 4 | 5 | - [Try secimport with bpftrace](#try-secimport-with-bpftrace) 6 | - [How to Use](#how-to-use) 7 | - [FAQ](#faq) 8 | - [How it runs on macOS?](#how-it-runs-on-macos) 9 | - [Can we trace a macOS host with this docker?](#can-we-trace-a-macos-host-with-this-docker) 10 | 11 | 12 | 13 | # Try secimport with bpftrace 14 | 15 | ## How to Use 16 | 17 | 1. Install Docker: https://docs.docker.com/get-docker 18 | 2. `./build.sh` 19 | - will build a docker image with 20 | - python with dtrace static USDT instrumentations 21 | - bpftrace 22 | - secimport code 23 | - ~1GB in size 24 | 3. `./run.sh` will start a new docker container with secimport. 25 | - `secimport interactive` 26 | - `secimport shell` or `secimport trace` will run a new process (python interpreter or specific entrypoint). It it will record all functions and syscalls inside the newly created process. 27 | - All syscalls and modules will be logged to ./trace.log once you hit CTRL+D or CTRL+C. 28 | - You can use `-v` in docker run to mount your code into the container and trace it. 29 | 30 | ```python 31 | root@d57458518cbf:/workspace$ ./run_sandbox.sh 32 | 🚀 Starting secimport sandbox with bpftrace backend, the sandbox should kill the python process... 33 | WARNING: Addrspace is not set 34 | PID TTY TIME CMD 35 | 1 pts/0 00:00:00 sh 36 | 10 pts/0 00:00:00 bash 37 | 18 pts/0 00:00:00 bash 38 | 19 pts/0 00:00:00 bpftrace 39 | 23 pts/0 00:00:00 python 40 | 24 pts/0 00:00:00 sh 41 | 25 pts/0 00:00:00 sh 42 | 26 pts/0 00:00:00 pkill 43 | 27 pts/0 00:00:00 ps 44 | 45 | 46 | 🛑 The process was killed, as expected. 47 | 🚀 The sandbox bpftrace code is at sandbox.bt 48 | 🚀 The sandbox log is at sandbox.log. 49 | ``` 50 | 51 | ## FAQ 52 | 53 | ### How it runs on macOS? 54 | - The Docker for mac runs Linux on a hypervisor called hyperkit, and docker runs inside it, so you can use Linux features. 55 | 56 | ### Can we trace a macOS host with this docker? 57 | - Not at the moment. The bpftrace runs inside a Linux VM. 58 | - For macOS, there is dtrace. 59 | 60 | ===================== 61 | 62 | Based on the great example repo: https://github.com/mmisono/try-bpftrace-in-mac 63 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | .vscode/ 3 | __pycache__/ 4 | *.py[cod] 5 | *$py.class 6 | 7 | # C extensions 8 | *.so 9 | 10 | # Distribution / packaging 11 | .Python 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | pip-wheel-metadata/ 25 | share/python-wheels/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | MANIFEST 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .nox/ 45 | .coverage 46 | .coverage.* 47 | .cache 48 | nosetests.xml 49 | coverage.xml 50 | *.cover 51 | *.py,cover 52 | .hypothesis/ 53 | .pytest_cache/ 54 | 55 | # Translations 56 | *.mo 57 | *.pot 58 | 59 | # Django stuff: 60 | *.log 61 | local_settings.py 62 | db.sqlite3 63 | db.sqlite3-journal 64 | 65 | # Flask stuff: 66 | instance/ 67 | .webassets-cache 68 | 69 | # Scrapy stuff: 70 | .scrapy 71 | 72 | # Sphinx documentation 73 | docs/_build/ 74 | 75 | # PyBuilder 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | .python-version 87 | 88 | # pipenv 89 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 90 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 91 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 92 | # install all needed dependencies. 93 | #Pipfile.lock 94 | 95 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 96 | __pypackages__/ 97 | 98 | # Celery stuff 99 | celerybeat-schedule 100 | celerybeat.pid 101 | 102 | # SageMath parsed files 103 | *.sage.py 104 | 105 | # Environments 106 | .env 107 | .venv 108 | env/ 109 | venv/ 110 | ENV/ 111 | env.bak/ 112 | venv.bak/ 113 | 114 | # Spyder project settings 115 | .spyderproject 116 | .spyproject 117 | 118 | # Rope project settings 119 | .ropeproject 120 | 121 | # mkdocs documentation 122 | /site 123 | 124 | # mypy 125 | .mypy_cache/ 126 | .dmypy.json 127 | dmypy.json 128 | 129 | # Pyre type checker 130 | .pyre/ 131 | 132 | # Secimport files 133 | test_traced_modules.* 134 | sandbox.* 135 | -------------------------------------------------------------------------------- /docker/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG KERNEL_VERSION 2 | 3 | #FROM --platform=linux/amd64 linuxkit/kernel:${KERNEL_VERSION} as ksrc 4 | #FROM docker/for-desktop-kernel:5.10.25-6594e668feec68f102a58011bb42bd5dc07a7a9b as ksrc 5 | #FROM --platform=linux/amd64 ubuntu:latest AS build 6 | FROM linuxkit/kernel:6.6.13-44a5293614ca7c7674013e928cb11dcdbba73ba8 as ksrc 7 | FROM ubuntu:latest AS build 8 | 9 | ARG BPFTRACE_VERSION 10 | ARG PYTHON_VERSION 11 | 12 | WORKDIR /kernel 13 | COPY --from=ksrc /kernel-dev.tar . 14 | RUN tar xf kernel-dev.tar 15 | 16 | WORKDIR /workspace 17 | ARG DEBIAN_FRONTEND=noninteractive 18 | 19 | RUN apt-get -y update && apt-get install -y \ 20 | autoconf \ 21 | bison \ 22 | flex \ 23 | gcc \ 24 | g++ \ 25 | git \ 26 | libprotobuf-dev \ 27 | libnl-route-3-dev \ 28 | libtool \ 29 | make \ 30 | pkg-config \ 31 | protobuf-compiler 32 | # \ 33 | # && rm -rf /var/lib/apt/lists/* 34 | 35 | RUN echo "Installing prerequisites" && \ 36 | apt-get update && apt-get install nano sudo build-essential libsqlite3-dev 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 37 | 38 | RUN echo "Installing python with dtrace" && \ 39 | curl -o Python-${PYTHON_VERSION}.tgz https://www.python.org/ftp/python/${PYTHON_VERSION}/Python-${PYTHON_VERSION}.tgz && tar -xzf Python-${PYTHON_VERSION}.tgz && \ 40 | cd Python-${PYTHON_VERSION} && ./configure --with-dtrace --prefix=/usr/local/openssl --prefix=$(pwd) --with-ensurepip=install && make && make install 41 | 42 | RUN echo "Installing bpftrace" && \ 43 | # wget https://github.com/iovisor/bpftrace/releases/download/${BPFTRACE_VERSION}/bpftrace && \ 44 | # chmod +x bpftrace && \ 45 | # mv bpftrace /bin && \ 46 | # echo "Done building bpftrace" && \ 47 | mv /kernel/usr/src/linux-headers* /kernel/usr/src/linux-headers && \ 48 | apt-get install bpftrace -y; 49 | 50 | # Install nsjail: taken from https://github.com/google/nsjail/blob/master/Dockerfile 51 | #RUN git clone https://github.com/google/nsjail.git /nsjail && cd /nsjail && make && mv /nsjail/nsjail /bin && rm -rf -- /nsjail 52 | 53 | # Python dependencies for examples 54 | RUN /workspace/Python-${PYTHON_VERSION}/python -m pip install --upgrade pip && Python-${PYTHON_VERSION}/python -m pip install pyyaml typer fastapi uvicorn --user -U 55 | 56 | ENV BPFTRACE_KERNEL_SOURCE=/kernel/usr/src/linux-headers 57 | COPY setup.sh . 58 | 59 | # Open port 8000 for demos/examples with networking 60 | EXPOSE 8000 61 | CMD ["/bin/sh", "/workspace/setup.sh"] 62 | 63 | -------------------------------------------------------------------------------- /docs/FAQ.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | **Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* 4 | 5 | - [Is it safe to use in production?](#is-it-safe-to-use-in-production) 6 | - [Should I use dtrace or bpftrace backend?](#should-i-use-dtrace-or-bpftrace-backend) 7 | - [What are the tradeoffs? How does it change they way I code?](#what-are-the-tradeoffs-how-does-it-change-they-way-i-code) 8 | - [On Ubuntu, I get the error message `ERROR: Could not resolve symbol: /proc/self/exe:BEGIN_trigger`](#on-ubuntu-i-get-the-error-message-error-could-not-resolve-symbol-procselfexebegin_trigger) 9 | - [What are the performance impacts?](#what-are-the-performance-impacts) 10 | 11 | 12 | 13 | 14 | ## Is it safe to use in production? 15 | From Wikipedia: 16 | >Special consideration has been taken to make DTrace safe to use in a production environment. For example, there is minimal probe effect when tracing is underway, and no performance impact associated with any disabled probe; this is important since there are tens of thousands of DTrace probes that can be enabled. New probes can also be created dynamically. 17 | 18 | ## Should I use dtrace or bpftrace backend? 19 | - `dtrace` is cross platform and been around for decades. `eBPF is newer.` 20 | - Dtrace has destructive flags that are easy to use 21 | - bpftrace (eBPF) requires a kernel with eBPF support. 22 | - `dtrace` is a solution that everyone could use on legacy Solaris, Mac and Windows. 23 | - `dtrace` has vast languages support for future work (supported in more languages), compared to eBPF. 24 | - `dscript` s are easier to write than `eBPF kernels`. 25 | 26 | ## What are the tradeoffs? How does it change they way I code? 27 | - You need can use `secimport` in 2 ways: 28 | - Use `secimport.secure_import` function in your code to import module. A dtrace process is opened upon `secure_import()` call. 29 | - Compile a sandbox script for your configuration, and use the sandbox backend to run your python process as a supervisor parent process. 30 | - YAML templates 31 | - Allowlist / Blocklist 32 | - Allow only a set of syscalls for each module in you would like to confine. Log/Kill upon violation. 33 | 34 | ## On Ubuntu, I get the error message `ERROR: Could not resolve symbol: /proc/self/exe:BEGIN_trigger` 35 | You must install additional debugging symbols using these commmands: 36 | 37 | echo "deb http://ddebs.ubuntu.com $(lsb_release -cs) main restricted universe multiverse 38 | deb http://ddebs.ubuntu.com $(lsb_release -cs)-updates main restricted universe multiverse 39 | deb http://ddebs.ubuntu.com $(lsb_release -cs)-proposed main restricted universe multiverse" | \ 40 | sudo tee -a /etc/apt/sources.list.d/ddebs.list 41 | sudo apt install ubuntu-dbgsym-keyring 42 | sudo apt update 43 | sudo apt install bpftrace-dbgsym 44 | 45 | ## What are the performance impacts? 46 | - See docs/PERFORMANCE.md 47 | -------------------------------------------------------------------------------- /secimport/backends/dtrace_backend/default.yaml.template.d: -------------------------------------------------------------------------------- 1 | #!/usr/sbin/dtrace -Zs 2 | 3 | #pragma D option destructive 4 | #pragma D option quiet 5 | #pragma D option switchrate=1 6 | #pragma D option cleanrate=50hz 7 | /* 8 | "dirty" variable drops per 9 | default -> ~20k 10 | 10 Hz -> ~15k 11 | 20 Hz -> ~10k 12 | 30 Hz -> ~2k 13 | 40 Hz -> ~1k 14 | 50 Hz (max) -> < 200 15 | */ 16 | 17 | #pragma D option dynvarsize=400000 18 | /* 19 | 10,000 @ 50 Hz -> ~25k+ 20 | 100,000 @ 50 Hz -> ~20k 21 | 200,000 @ 50 Hz -> ~3k 22 | 300,000 @ 50 Hz -> ~1k 23 | 400,000 @ 50 Hz -> 0 24 | 400,000 @ 25 Hz -> ~2k 25 | 500,000 @ 25 Hz -> ~1k 26 | */ 27 | 28 | /* A depth matrix for modules by name, maps each module (string) to the stack depth (int) at which it entered. */ 29 | int depth_matrix[string]; 30 | self int depth; 31 | string current_module_str; 32 | string previous_module_str; 33 | string latest_supervised_module; 34 | 35 | dtrace:::BEGIN 36 | { 37 | printf("%s %6s %10s %16s:%-4s %-8s -- %s\n", "C", "PID", "DELTA(us)", 38 | "FILE", "LINE", "TYPE", "FUNC"); 39 | } 40 | 41 | python*:::function-entry, 42 | python*:::function-return 43 | /self->last == 0/ 44 | { 45 | self->last = timestamp; 46 | } 47 | 48 | 49 | python*:::function-entry 50 | { 51 | this->delta = (timestamp - self->last) / 1000; 52 | self->depth++; 53 | 54 | /* Memoizing the previous module and current module */ 55 | _current_module_str = stringof(copyinstr(arg0)); 56 | previous_module_str = stringof(current_module_str); 57 | current_module_str = _current_module_str; 58 | 59 | /* if (strstr(_current_module_str, "###MODULE_NAME###") != 0){ 60 | printf("---! in module %s \r\n", "###MODULE_NAME###"); 61 | } */ 62 | 63 | /* Saving the stack depth for each module, assuming interpreter with GIL will run line-by-line. */ 64 | if (depth_matrix[current_module_str] > self->depth){ 65 | depth_matrix[current_module_str] = 0; 66 | } 67 | if (depth_matrix[current_module_str] == 0){ 68 | /* printf("---> (%d) %s\r\n", self->depth, _current_module_str); */ 69 | depth_matrix[current_module_str] = self->depth; 70 | } 71 | /* depth_matrix[current_module_str] = self->depth; */ 72 | } 73 | 74 | python*:::function-return 75 | { 76 | this->delta = (timestamp - self->last) / 1000; 77 | self->depth -= self->depth > 0 ? 1 : 0; 78 | /* printf("<--- (%d) %s\r\n", self->depth, _current_module_str); */ 79 | if (depth_matrix[current_module_str] > self->depth){ 80 | depth_matrix[current_module_str] = 0; 81 | } 82 | } 83 | 84 | syscall:::entry 85 | /pid == $target/ 86 | { 87 | @calls[basename(execname), "syscall", probefunc] = count(); 88 | /* printf("\n@%s", probefunc); */ 89 | ###SUPERVISED_MODULES_PROBES### 90 | } 91 | 92 | /* 93 | syscall:::return 94 | /pid == $target/ 95 | { 96 | printf("\n"); 97 | } */ 98 | 99 | 100 | dtrace:::END 101 | { 102 | printf("System Calls (%d)\r\n\r\n", $target); 103 | printf(" %-32s %-10s %-22s %8s\r\n", "FILE", "TYPE", "NAME", "COUNT"); 104 | printa(" %-32s %-10s %-22s %@8d\r\n", @calls); 105 | /* print(depth_matrix); */ 106 | } 107 | -------------------------------------------------------------------------------- /secimport/sandbox_helper.py: -------------------------------------------------------------------------------- 1 | """ 2 | Import python modules with syscalls supervision. 3 | 4 | Copyright (c) 2022 Avi Lumelsky 5 | """ 6 | 7 | from secimport.backends.common.instrumentation_backend import InstrumentationBackend 8 | from secimport.backends.common.utils import DEFAULT_BACKEND 9 | 10 | 11 | def secure_import( 12 | module_name: str, 13 | allow_shells: bool = False, 14 | allow_networking: bool = False, 15 | use_sudo: bool = False, 16 | log_python_calls: bool = False, # When True, the log file might reach GB in seconds. 17 | log_syscalls: bool = False, 18 | log_network: bool = False, 19 | log_file_system: bool = False, 20 | destructive: bool = True, 21 | backend=DEFAULT_BACKEND, 22 | **kwargs, 23 | ): 24 | """Import a python module in confined settings. 25 | 26 | Args: 27 | module_name (str): The name of the module to import. The same way you would have used an 'import' statement. 28 | allow_shells (bool, optional): Whether to allow forking/spwning shells and execute commands. 29 | allow_networking (bool, optional): Whether to allow socket syscalls. 30 | use_sudo (bool, optional): Whether to run dtrace with sudo. Can be turned off if sudo is not installed. 31 | log_python_calls (bool, optional): Whether to log the call tree of the python stack - function entry and exit events. 32 | log_network (bool, optional): Whether to log network successful connections - e.g socket syscalls. 33 | log_file_system (bool, optional): Whether to log filesystem calls - e.g read, open, write, etc. 34 | destructive (bool, optional): Whether to kill the process with -9 sigkill upon violation of any of the configurations above. 35 | Returns: 36 | _type_: A Python Module. The module is supervised by a dtrace process with destructive capabilities unless the 'destructive' argument is set to False. 37 | """ 38 | 39 | if backend == InstrumentationBackend.EBPF: 40 | from secimport.backends.bpftrace_backend.bpftrace_backend import ( 41 | run_bpftrace_script_for_module, 42 | ) 43 | 44 | assert run_bpftrace_script_for_module( 45 | module_name=module_name, 46 | allow_shells=allow_shells, 47 | allow_networking=allow_networking, 48 | use_sudo=use_sudo, 49 | log_python_calls=log_python_calls, 50 | log_syscalls=log_syscalls, 51 | log_network=log_network, 52 | log_file_system=log_file_system, 53 | destructive=destructive, 54 | ) 55 | elif backend == InstrumentationBackend.DTRACE: 56 | from secimport.backends.dtrace_backend.dtrace_backend import ( 57 | run_dtrace_script_for_module, 58 | ) 59 | 60 | assert run_dtrace_script_for_module( 61 | module_name=module_name, 62 | allow_shells=allow_shells, 63 | allow_networking=allow_networking, 64 | use_sudo=use_sudo, 65 | log_python_calls=log_python_calls, 66 | log_syscalls=log_syscalls, 67 | log_network=log_network, 68 | log_file_system=log_file_system, 69 | destructive=destructive, 70 | ) 71 | else: 72 | raise NotImplementedError(f"backend '{backend}' is not implemented.") 73 | 74 | _module = __import__(module_name, **kwargs) 75 | return _module 76 | -------------------------------------------------------------------------------- /docs/YAML_PROFILES.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | **Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* 4 | 5 | - [YAML Profiles and Templates](#yaml-profiles-and-templates) 6 | - [Create a custom sandbox](#create-a-custom-sandbox) 7 | - [1. Generate a profile for your application](#1-generate-a-profile-for-your-application) 8 | - [2. Save the YAML output](#2-save-the-yaml-output) 9 | - [3. Build a sandbox (dtrace script) from a yaml file](#3-build-a-sandbox-dtrace-script-from-a-yaml-file) 10 | - [4. Run your app in your sandbox](#4-run-your-app-in-your-sandbox) 11 | 12 | 13 | 14 | # YAML Profiles and Templates 15 | It is very convenient to specify all of you policies, for all of your 3rd party and open source modules, in a single YAML file. 16 | 17 | # Create a custom sandbox 18 | 1. Generate a profile for your application using the tracing script. 19 | 2. Create a YAML security profile from your trace. 20 | 3. Build a sandbox (dtrace script) from the yaml file 21 | 1. Run your app in your sandbox! 22 | 23 | ## 1. Generate a profile for your application 24 | ```shell 25 | dtrace2) ➜ secimport git:(master) ✗ sudo dtrace -s secimport/templates/generate_profile.d -c "python -m http.server" 26 | 27 | Serving HTTP on :: port 8000 (http://[::]:8000/) ... 28 | 29 | CTRL + C 30 | 31 | 32 | Generated syscalls (yaml profile): 33 | destructive: true 34 | syscall_allowlist: 35 | - __mac_syscall 36 | - __pthread_canceled 37 | - bind 38 | - csrctl 39 | - fgetattrlist 40 | ... 41 | - read 42 | - ioctl 43 | - stat64 44 | 45 | Done. 46 | ``` 47 | 48 | ## 2. Save the YAML output 49 | After yout hit CTRL + C, after "Generated syscalls (yaml profile)", the YAML profile is printed. 50 | 51 | An example for a template is available in example.yaml. 52 | 53 | It should look like this: 54 | ```shell 55 | # Example yaml file (with example syscalls) 56 | 57 | modules: 58 | requests: 59 | destructive: true 60 | syscall_allowlist: 61 | - write 62 | - ioctl 63 | - stat64 64 | fastapi: 65 | destructive: true 66 | syscall_allowlist: 67 | - bind 68 | - fchmod 69 | - stat64 70 | uvicorn: 71 | destructive: true 72 | syscall_allowlist: 73 | - getpeername 74 | - getpgrp 75 | - stat64 76 | ``` 77 | 78 | Save the output of dtrace into a local YAML file and proceed to step 3. 79 | 80 | ## 3. Build a sandbox (dtrace script) from a yaml file 81 | Usage:
82 | ```shell 83 | python examples/create_profile_from_yaml.py 84 | ``` 85 | Example: 86 | ```shell 87 | python examples/create_profile_from_yaml.py secimport/profiles/example.yaml /tmp/example.d 88 | ``` 89 | 90 | Or, use the python API: 91 | ```python 92 | import secimport, pathlib 93 | 94 | template_path = pathlib.Path(secimport.sandbox_helper.PROFILES_DIR_NAME / 'example.yaml') 95 | 96 | sandbox_code = secimport.sandbox_helper.build_module_sandbox_from_yaml_template(template_path) 97 | ``` 98 | 99 | 100 | ## 4. Run your app in your sandbox 101 | ```shell 102 | sudo dtrace -s /tmp/example.d -c "python" 103 | ``` 104 | 105 | And that's it! 106 | -------------------------------------------------------------------------------- /examples/cli/ebpf/create_sandbox_from_yaml_or_json_policy/example.yaml: -------------------------------------------------------------------------------- 1 | modules: 2 | requests: 3 | destructive: true 4 | syscall_allowlist: 5 | - fchmod 6 | - getentropy 7 | - getpgrp 8 | - getrlimit 9 | - shm_open 10 | - sysctlbyname 11 | - access 12 | - munmap 13 | - issetugid 14 | - readlink 15 | - write 16 | - fcntl 17 | - fstatfs64 18 | - getdirentries64 19 | - mprotect 20 | - fcntl_nocancel 21 | - madvise 22 | - mmap 23 | - read_nocancel 24 | - select 25 | - sigprocmask 26 | - close_nocancel 27 | - write_nocancel 28 | - open_nocancel 29 | - close 30 | - open 31 | - sigaction 32 | - lseek 33 | - fstat64 34 | - read 35 | - ioctl 36 | - stat64 37 | fastapi: 38 | destructive: true 39 | syscall_allowlist: 40 | - bind 41 | - fchmod 42 | - getentropy 43 | - getpgrp 44 | - getrlimit 45 | - shm_open 46 | - sysctlbyname 47 | - access 48 | - munmap 49 | - issetugid 50 | - readlink 51 | - write 52 | - fcntl 53 | - fstatfs64 54 | - getdirentries64 55 | - mprotect 56 | - fcntl_nocancel 57 | - madvise 58 | - mmap 59 | - read_nocancel 60 | - select 61 | - sigprocmask 62 | - close_nocancel 63 | - write_nocancel 64 | - open_nocancel 65 | - close 66 | - open 67 | - sigaction 68 | - lseek 69 | - fstat64 70 | - read 71 | - ioctl 72 | - stat64 73 | - read 74 | - pipe 75 | - listen 76 | - poll 77 | - sigreturn 78 | - getsockname 79 | - kqueue 80 | - kevent 81 | - getpeername 82 | - getpgrp 83 | - listen 84 | - pipe 85 | - poll 86 | - setsockopt 87 | - shm_open 88 | - socket 89 | - socketpair 90 | - sysctlbyname 91 | - accept 92 | - access 93 | - getrlimit 94 | - kqueue 95 | - readlink 96 | - recvfrom 97 | - getsockname 98 | - issetugid 99 | - sendto 100 | - write 101 | - read_nocancel 102 | - getentropy 103 | - sigprocmask 104 | - fstatfs64 105 | - getdirentries64 106 | - munmap 107 | - madvise 108 | - select 109 | - write_nocancel 110 | - sigaction 111 | - fcntl_nocancel 112 | - fcntl 113 | - close_nocancel 114 | - open_nocancel 115 | - mprotect 116 | - mmap 117 | - kevent 118 | - open 119 | - close 120 | - lseek 121 | - ioctl 122 | - read 123 | - fstat64 124 | - stat64 125 | uvicorn: 126 | destructive: true 127 | syscall_allowlist: 128 | - getpeername 129 | - getpgrp 130 | - listen 131 | - pipe 132 | - poll 133 | - setsockopt 134 | - shm_open 135 | - socket 136 | - socketpair 137 | - sysctlbyname 138 | - accept 139 | - access 140 | - getrlimit 141 | - kqueue 142 | - readlink 143 | - recvfrom 144 | - getsockname 145 | - issetugid 146 | - sendto 147 | - write 148 | - read_nocancel 149 | - getentropy 150 | - sigprocmask 151 | - fstatfs64 152 | - getdirentries64 153 | - munmap 154 | - madvise 155 | - select 156 | - write_nocancel 157 | - sigaction 158 | - fcntl_nocancel 159 | - fcntl 160 | - close_nocancel 161 | - open_nocancel 162 | - mprotect 163 | - mmap 164 | - kevent 165 | - open 166 | - close 167 | - lseek 168 | - ioctl 169 | - read 170 | - fstat64 171 | - stat64 172 | -------------------------------------------------------------------------------- /tests/test_bpftrace_backend.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import os 3 | import sys 4 | 5 | 6 | from secimport.backends.bpftrace_backend.bpftrace_backend import ( 7 | render_bpftrace_template, 8 | create_bpftrace_script_for_module, 9 | run_bpftrace_script_for_module, 10 | ) 11 | from secimport.backends.common.utils import BASE_DIR_NAME 12 | 13 | 14 | class TestEBPFBackend(unittest.TestCase): 15 | def setUp(self) -> None: 16 | 17 | try: 18 | os.remove(BASE_DIR_NAME) 19 | except (FileNotFoundError, PermissionError, IsADirectoryError): 20 | ... 21 | return super().setUp() 22 | 23 | def test_run_bpftrace_with_none_module_raises_error(self): 24 | with self.assertRaises(ModuleNotFoundError): 25 | create_bpftrace_script_for_module( 26 | module_name="nonexisting", 27 | allow_networking=False, 28 | allow_shells=False, 29 | log_file_system=True, 30 | log_syscalls=True, 31 | log_network=True, 32 | log_python_calls=True, 33 | destructive=True, 34 | ) 35 | 36 | def test_create_bpftrace_script_for_module(self): 37 | bpftrace_script_file_path = create_bpftrace_script_for_module( 38 | "this", 39 | allow_networking=False, 40 | allow_shells=False, 41 | log_file_system=True, 42 | log_syscalls=True, 43 | log_network=True, 44 | log_python_calls=True, 45 | destructive=True, 46 | ) 47 | self.assertEqual( 48 | bpftrace_script_file_path, "/tmp/.secimport/bpftrace_sandbox_this.bt" 49 | ) 50 | self.assertTrue(os.path.exists(bpftrace_script_file_path)) 51 | bpftrace_file_content = open(bpftrace_script_file_path).read() 52 | self.assertTrue("system" in bpftrace_file_content) 53 | self.assertTrue(sys.executable in bpftrace_file_content) 54 | 55 | def test_run_bpftrace_script_for_module(self): 56 | res = run_bpftrace_script_for_module( 57 | "this", 58 | allow_networking=False, 59 | allow_shells=False, 60 | log_file_system=True, 61 | log_syscalls=True, 62 | log_network=True, 63 | log_python_calls=True, 64 | destructive=True, 65 | dry_run=True, 66 | ) 67 | 68 | self.assertTrue(res) 69 | 70 | def test_create_bpftrace_script_for_nonexisting_module(self): 71 | # create_bpftrace_script_for_module 72 | # TODO: implement 73 | bpftrace_script_file_path = create_bpftrace_script_for_module( 74 | module_name="urllib", 75 | allow_networking=False, 76 | allow_shells=False, 77 | log_file_system=True, 78 | log_syscalls=True, 79 | log_network=True, 80 | log_python_calls=True, 81 | destructive=True, 82 | ) 83 | bpftrace_file_content = open(bpftrace_script_file_path).read() 84 | # Making sure all the template variables (start with '###') were successfully replaced in the function. 85 | self.assertTrue("###" not in bpftrace_file_content) 86 | 87 | def test_render_bpftrace_template(self): 88 | bpftrace_file_content = render_bpftrace_template( 89 | module_traced_name="http", 90 | allow_networking=False, 91 | allow_shells=False, 92 | log_file_system=True, 93 | log_syscalls=True, 94 | log_network=True, 95 | log_python_calls=True, 96 | destructive=True, 97 | ) 98 | self.assertTrue("system(" in bpftrace_file_content) 99 | self.assertTrue("http" in bpftrace_file_content) 100 | 101 | 102 | if __name__ == "__main__": 103 | unittest.main() 104 | -------------------------------------------------------------------------------- /examples/cli/ebpf/fastapi/policy.json: -------------------------------------------------------------------------------- 1 | { 2 | "general_requirements": [ 3 | " chdir", 4 | " clone", 5 | " epoll_ctl_old", 6 | " epoll_wait_old", 7 | " fork", 8 | " futimesat", 9 | " getcwd", 10 | " getdents", 11 | " getrusage", 12 | " gettimeofday", 13 | " mremap", 14 | " pipe2", 15 | " pread", 16 | " query_module", 17 | " shutdown", 18 | " sysinfo", 19 | " timer_create", 20 | " timer_delete", 21 | " uname", 22 | " vmsplice" 23 | ], 24 | "/root/.local/lib/python3.11/site-packages/pydantic/_internal/_g": [ 25 | " epoll_wait_old" 26 | ], 27 | "/root/.local/lib/python3.11/site-packages/pydantic/fields.py": [ 28 | " epoll_ctl_old" 29 | ], 30 | "/root/.local/lib/python3.11/site-packages/typing_extensions.py": [ 31 | " chdir", 32 | " clone", 33 | " epoll_ctl_old", 34 | " epoll_wait_old", 35 | " fork", 36 | " getcwd", 37 | " kill", 38 | " remap_file_pages", 39 | " shmget", 40 | " timer_create", 41 | " timer_delete", 42 | " uname", 43 | " vmsplice", 44 | " wait4" 45 | ], 46 | "/workspace/Python-3.11.8/Lib/email/_policybase.py": [ 47 | " chdir", 48 | " clone", 49 | " epoll_ctl_old", 50 | " epoll_wait_old", 51 | " fork", 52 | " getcwd", 53 | " kill", 54 | " shmget", 55 | " timer_create", 56 | " timer_delete", 57 | " uname", 58 | " wait4" 59 | ], 60 | "/workspace/Python-3.11.8/Lib/enum.py": [ 61 | " chdir", 62 | " clone", 63 | " epoll_ctl_old", 64 | " epoll_wait_old", 65 | " fork", 66 | " getcwd", 67 | " kill", 68 | " shmget", 69 | " timer_create", 70 | " timer_delete", 71 | " uname", 72 | " vmsplice", 73 | " wait4" 74 | ], 75 | "/workspace/Python-3.11.8/Lib/types.py": [ 76 | " chdir", 77 | " clone", 78 | " epoll_ctl_old", 79 | " fork", 80 | " getcwd", 81 | " kill", 82 | " shmget", 83 | " uname" 84 | ], 85 | "/workspace/Python-3.11.8/Lib/typing.py": [ 86 | " access", 87 | " chdir", 88 | " clone", 89 | " epoll_ctl_old", 90 | " epoll_wait_old", 91 | " fork", 92 | " fremovexattr", 93 | " futimesat", 94 | " getcwd", 95 | " getrusage", 96 | " io_destroy", 97 | " io_getevents", 98 | " io_setup", 99 | " iopl", 100 | " kill", 101 | " lremovexattr", 102 | " mq_timedsend", 103 | " personality", 104 | " pipe", 105 | " pread", 106 | " query_module", 107 | " sched_getaffinity", 108 | " semget", 109 | " set_thread_area", 110 | " setrlimit", 111 | " shmget", 112 | " sysfs", 113 | " time", 114 | " timer_create", 115 | " timer_delete", 116 | " tkill", 117 | " uname", 118 | " uselib", 119 | " vmsplice", 120 | " wait4", 121 | " writev" 122 | ], 123 | "": [ 124 | " chdir", 125 | " clone", 126 | " create_module", 127 | " delete_module", 128 | " epoll_ctl_old", 129 | " fork", 130 | " get_kernel_syms", 131 | " getcwd", 132 | " getdents", 133 | " init_module", 134 | " kill", 135 | " pread", 136 | " shmget", 137 | " uname", 138 | " wait4" 139 | ], 140 | "": [ 141 | " chdir", 142 | " clone", 143 | " epoll_ctl_old", 144 | " fork", 145 | " getcwd", 146 | " kill", 147 | " mremap", 148 | " pread", 149 | " shmget", 150 | " timer_create", 151 | " uname", 152 | " uselib", 153 | " wait4" 154 | ] 155 | } 156 | -------------------------------------------------------------------------------- /examples/cli/ebpf/fastapi/policy.yaml: -------------------------------------------------------------------------------- 1 | modules: 2 | /root/.local/lib/python3.11/site-packages/pydantic/_internal/_g: 3 | destructive: false 4 | syscall_allowlist: 5 | - ' epoll_wait_old' 6 | /root/.local/lib/python3.11/site-packages/pydantic/fields.py: 7 | destructive: false 8 | syscall_allowlist: 9 | - ' epoll_ctl_old' 10 | /root/.local/lib/python3.11/site-packages/typing_extensions.py: 11 | destructive: false 12 | syscall_allowlist: 13 | - ' chdir' 14 | - ' clone' 15 | - ' epoll_ctl_old' 16 | - ' epoll_wait_old' 17 | - ' fork' 18 | - ' getcwd' 19 | - ' kill' 20 | - ' remap_file_pages' 21 | - ' shmget' 22 | - ' timer_create' 23 | - ' timer_delete' 24 | - ' uname' 25 | - ' vmsplice' 26 | - ' wait4' 27 | /workspace/Python-3.11.8/Lib/email/_policybase.py: 28 | destructive: false 29 | syscall_allowlist: 30 | - ' chdir' 31 | - ' clone' 32 | - ' epoll_ctl_old' 33 | - ' epoll_wait_old' 34 | - ' fork' 35 | - ' getcwd' 36 | - ' kill' 37 | - ' shmget' 38 | - ' timer_create' 39 | - ' timer_delete' 40 | - ' uname' 41 | - ' wait4' 42 | /workspace/Python-3.11.8/Lib/enum.py: 43 | destructive: false 44 | syscall_allowlist: 45 | - ' chdir' 46 | - ' clone' 47 | - ' epoll_ctl_old' 48 | - ' epoll_wait_old' 49 | - ' fork' 50 | - ' getcwd' 51 | - ' kill' 52 | - ' shmget' 53 | - ' timer_create' 54 | - ' timer_delete' 55 | - ' uname' 56 | - ' vmsplice' 57 | - ' wait4' 58 | /workspace/Python-3.11.8/Lib/types.py: 59 | destructive: false 60 | syscall_allowlist: 61 | - ' chdir' 62 | - ' clone' 63 | - ' epoll_ctl_old' 64 | - ' fork' 65 | - ' getcwd' 66 | - ' kill' 67 | - ' shmget' 68 | - ' uname' 69 | /workspace/Python-3.11.8/Lib/typing.py: 70 | destructive: false 71 | syscall_allowlist: 72 | - ' access' 73 | - ' chdir' 74 | - ' clone' 75 | - ' epoll_ctl_old' 76 | - ' epoll_wait_old' 77 | - ' fork' 78 | - ' fremovexattr' 79 | - ' futimesat' 80 | - ' getcwd' 81 | - ' getrusage' 82 | - ' io_destroy' 83 | - ' io_getevents' 84 | - ' io_setup' 85 | - ' iopl' 86 | - ' kill' 87 | - ' lremovexattr' 88 | - ' mq_timedsend' 89 | - ' personality' 90 | - ' pipe' 91 | - ' pread' 92 | - ' query_module' 93 | - ' sched_getaffinity' 94 | - ' semget' 95 | - ' set_thread_area' 96 | - ' setrlimit' 97 | - ' shmget' 98 | - ' sysfs' 99 | - ' time' 100 | - ' timer_create' 101 | - ' timer_delete' 102 | - ' tkill' 103 | - ' uname' 104 | - ' uselib' 105 | - ' vmsplice' 106 | - ' wait4' 107 | - ' writev' 108 | : 109 | destructive: false 110 | syscall_allowlist: 111 | - ' chdir' 112 | - ' clone' 113 | - ' create_module' 114 | - ' delete_module' 115 | - ' epoll_ctl_old' 116 | - ' fork' 117 | - ' get_kernel_syms' 118 | - ' getcwd' 119 | - ' getdents' 120 | - ' init_module' 121 | - ' kill' 122 | - ' pread' 123 | - ' shmget' 124 | - ' uname' 125 | - ' wait4' 126 | : 127 | destructive: false 128 | syscall_allowlist: 129 | - ' chdir' 130 | - ' clone' 131 | - ' epoll_ctl_old' 132 | - ' fork' 133 | - ' getcwd' 134 | - ' kill' 135 | - ' mremap' 136 | - ' pread' 137 | - ' shmget' 138 | - ' timer_create' 139 | - ' uname' 140 | - ' uselib' 141 | - ' wait4' 142 | general_requirements: 143 | destructive: false 144 | syscall_allowlist: 145 | - ' chdir' 146 | - ' clone' 147 | - ' epoll_ctl_old' 148 | - ' epoll_wait_old' 149 | - ' fork' 150 | - ' futimesat' 151 | - ' getcwd' 152 | - ' getdents' 153 | - ' getrusage' 154 | - ' gettimeofday' 155 | - ' mremap' 156 | - ' pipe2' 157 | - ' pread' 158 | - ' query_module' 159 | - ' shutdown' 160 | - ' sysinfo' 161 | - ' timer_create' 162 | - ' timer_delete' 163 | - ' uname' 164 | - ' vmsplice' 165 | -------------------------------------------------------------------------------- /docs/INSTALL.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | **Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* 4 | 5 | - [Python Interpreter Requirements](#python-interpreter-requirements) 6 | - [OS requirements](#os-requirements) 7 | - [Install `bpftrace` (for Linux)](#install-bpftrace-for-linux) 8 | - [Install `dtrace` (for Mac, Solaris, Windows)](#install-dtrace-for-mac-solaris-windows) 9 | - [Using Docker: bpftrace (Linux, Mac)](#using-docker-bpftrace-linux-mac) 10 | - [Install Python with USDT probes and openssl (from source): ~5 minutes](#install-python-with-usdt-probes-and-openssl-from-source-5-minutes) 11 | - [Test the interpreter](#test-the-interpreter) 12 | - [Run `secimport` Tests](#run-secimport-tests) 13 | - [What's Next?](#whats-next) 14 | 15 | 16 | 17 | 18 | # Python Interpreter Requirements 19 | The only runtime requirement is a Python interpreter that was built with --with-dtrace (USDT probes).
20 | You can check if your current interpreter supported by running this command: 21 | ```python 22 | python -m sysconfig | grep WITH_DTRACE 23 | ``` 24 | This configuration option should have the value "1" (and not "0"!): 25 | ``` 26 | WITH_DTRACE = "1" 27 | ``` 28 | If your current interpreter is not supported (empty output): 29 | - Using `pip` 30 | - `python3 -m pip install secimport` 31 | - Using `poetry` 32 | - `python3 -m pip install poetry && python3 -m poetry build` 33 |

34 | 35 | # OS requirements 36 | ## Install `bpftrace` (for Linux) 37 | Install bpftrace toolkit from https://github.com/iovisor/bpftrace/blob/master/INSTALL.md . 38 | Then, proceed to the python interpreter. 39 | 40 | ## Install `dtrace` (for Mac, Solaris, Windows) 41 | Some distributions include dtrace. check the `dtrace` command. If it is not installed: 42 | ```shell 43 | yum install dtrace-utils 44 | ``` 45 | 46 | ### Using Docker: bpftrace (Linux, Mac) 47 | the `docker/` folder includes everything in the following guide. 48 | To build and run using docker, see Docker, 49 |

50 | 51 | # Install Python with USDT probes and openssl (from source): ~5 minutes 52 | If you want to use pip to work properly with pypi, you should also install openssl. 53 | To support ssl in this interpreter, One can simply install openssl pacakge using apt/yum/apk and it will use it automatically.
54 | If you wish to build openssl from source 55 | 56 | Download and build openssl 57 | ```shell 58 | wget https://www.openssl.org/source/openssl-1.1.1h.tar.gz 59 | tar -xvf openssl-1.1.1h.tar.gz 60 | cd openssl-1.1.1h 61 | ./config --prefix=$(pwd)/openssl --openssldir=$(pwd)/openssl 62 | make 63 | make test 64 | make install 65 | ``` 66 | 67 | Download python 68 | ```shell 69 | PYTHON_VERSION="3.11.8" 70 | 71 | cd /tmp 72 | curl -o Python-$PYTHON_VERSION.tgz https://www.python.org/ftp/python/$PYTHON_VERSION/Python-$PYTHON_VERSION.tgz 73 | tar -xzf Python-$PYTHON_VERSION.tgz 74 | ``` 75 | 76 | If you want to use a custom OpenSSL version/installation, edit the `OPENSSL` variable in Modules/Setup:
77 | 78 | ```shell 79 | $ nano Python-$PYTHON_VERSION/Modules/Setup 80 | 81 | OPENSSL=/path/to/openssl-1.1.1h/openssl 82 | _ssl _ssl.c \ 83 | -I$(OPENSSL)/include -L$(OPENSSL)/lib \ 84 | -lssl -lcrypto 85 | 86 | 87 | # Configuring and install with dtrace, pip and openssl 88 | cd Python-$PYTHON_VERSION && ./configure --with-dtrace --prefix=$(pwd) --with-ensurepip=install && make 89 | 90 | # Optionsl: test your build 91 | make test 92 | 93 | # Optional: Install alongside your existing python without replacing it. 94 | make altinstall 95 | ``` 96 | 97 |

98 | ## Test the interpreter 99 | ```shell 100 | ➜ Python-3.11.8 ./python 101 | 102 | Python 3.11.8 (default, Jul 6 2022, 09:21:12) [Clang 13.0.0 (clang-1300.0.27.3)] on darwin 103 | Type "help", "copyright", "credits" or "license" for more information. 104 | >>> import ssl 105 | >>> # You're good to go! 106 | ``` 107 |

108 | 109 | ## Run `secimport` Tests 110 | ```shell 111 | python3 -m pytest 112 | ``` 113 | 114 |

115 | # What's Next? 116 | You can proceed to EXAMPLES.md 117 | -------------------------------------------------------------------------------- /secimport/backends/dtrace_backend/py_sandbox.d: -------------------------------------------------------------------------------- 1 | #!/usr/sbin/dtrace -Zs 2 | 3 | /* 4 | Examples based on: 5 | https://docs.oracle.com/cd/E18752_01/html/819-5488/gcfpz.html 6 | https://opensource.apple.com/source/dtrace/dtrace-147/DTTk/dtruss.auto.html 7 | https://www.brendangregg.com/Articles/FreeBSDwiki_DTrace_Tutorial_2014.pdf 8 | https://docs.oracle.com/en/operating-systems/solaris/oracle-solaris/11.4/dtrace-guide/arrays-dtrace.html 9 | */ 10 | 11 | #pragma D option destructive 12 | #pragma D option quiet 13 | #pragma D option switchrate=1 14 | 15 | /* A depth matrix for modules by name, maps each module (string) to the stack depth (int) at which it entered. */ 16 | int depth_matrix[string]; 17 | self int depth; 18 | self int sandbox_module_reached; 19 | 20 | string current_module_str; 21 | string previous_module_str; 22 | /* 23 | inline string RED = "33[31;1m"; 24 | inline string BLUE = "33[34;1m"; 25 | inline string GREEN = "33[32;1m"; 26 | inline string VIOLET = "33[35;1m"; 27 | inline string OFF = "33[0m"; 28 | */ 29 | 30 | dtrace:::BEGIN 31 | { 32 | printf("%s %6s %10s %16s:%-4s %-8s -- %s\n", "C", "PID", "DELTA(us)", 33 | "FILE", "LINE", "TYPE", "FUNC"); 34 | } 35 | 36 | python*:::function-entry, 37 | python*:::function-return 38 | /self->last == 0/ 39 | { 40 | self->last = timestamp; 41 | } 42 | 43 | python*:::function-entry 44 | { 45 | this->delta = (timestamp - self->last) / 1000; 46 | 47 | /* Memoizing the previous module and current module */ 48 | _current_module_str = stringof(copyinstr(arg0))); 49 | previous_module_str = stringof(current_module_str); 50 | current_module_str = _current_module_str; 51 | 52 | /* Saving the stack depth for each module, assuming interpreter with GIL will run line-by-line. */ 53 | if (depth_matrix[current_module_str] == 0){ 54 | depth_matrix[current_module_str] = self->depth; 55 | } 56 | 57 | printf("\r\n%d %6d %10d %16s:%-4d %-8s %*s-> %s", cpu, pid, this->delta, 58 | current_module_str, arg2, "func", self->depth * 4, "", 59 | copyinstr(arg1)); 60 | self->depth++; 61 | self->last = timestamp; 62 | } 63 | 64 | python*:::function-return 65 | { 66 | this->delta = (timestamp - self->last) / 1000; 67 | self->depth -= self->depth > 0 ? 1 : 0; 68 | printf("\r\n%d %6d %10d %16s:%-4d %-8s %*s<- %s", cpu, pid, this->delta, 69 | current_module_str, arg2, "func", self->depth * 4, "", 70 | copyinstr(arg1)); 71 | if (depth_matrix[current_module_str] >= self->depth){ 72 | depth_matrix[current_module_str] = 0; 73 | } 74 | self->last = timestamp; 75 | } 76 | 77 | syscall:::entry 78 | /pid == $target/ 79 | { 80 | printf("\t%*s#%s",self->depth * 4, "",probefunc); 81 | @calls[basename(execname), "syscall", probefunc] = count(); 82 | if (probefunc == "open"){ 83 | printf("(OPENING FILE): %s from thread %d in python module %s\r\n", probefunc, tid, current_module_str); 84 | } 85 | if (probefunc == "write"){ 86 | printf("(TOUCHING FILESYSTEM): %s(%d) from thread %d in python module %s\r\n", probefunc, arg1, tid, current_module_str); 87 | } 88 | if (probefunc == "socket") { 89 | printf("(NETWORKING): %s(%d from thread %d in python module %s\r\n", probefunc, arg1, tid, current_module_str); 90 | } 91 | if (probefunc == "posix_spawn"){ 92 | printf("(OPENING SHELL using %s): (pid %d) (thread %d) (user %d) (python module: %s) (probe mod=%s, name=%s, prov=%s func=%s)\r\n", probefunc, pid, tid, uid, current_module_str, probemod, probename, probeprov, probefunc); 93 | printf("%60s %16s %20d\$(r\n", copyinstr(arg0), copyinstr(arg1), arg2); 94 | if(depth_matrix["malicious.py"] != 0 && self->depth >= depth_matrix["malicious.py"]){ 95 | printf("\t\tTERMINATING shell...\r\n"); 96 | ustack(); 97 | stop(); 98 | printf("\t\tKILLING...\r\n"); 99 | system("\t\tkill -9 %d", pid); 100 | printf("\t\tKILLED.\r\n"); 101 | exit(-1); 102 | } 103 | } 104 | if (probefunc == "posix_spawnp" || 105 | probefunc == "clone" || 106 | probefunc == "__clone2" || 107 | probefunc == "clone3" || 108 | probefunc == "fork" || 109 | probefunc == "vfork" || 110 | probefunc == "forkexec" || 111 | probefunc == "execl" || 112 | probefunc == "execlp" || 113 | probefunc == "execle" || 114 | probefunc == "execv" || 115 | probefunc == "execvp") 116 | { 117 | /* printf("\t\t(ALLOWING SHELL using %s):%60s %16s %20d\r\n", probefunc, copyinstr(arg0), copyinstr(arg1), arg2); */ 118 | if(depth_matrix["malicious.py"] != 0 && self->depth >= depth_matrix["malicious.py"]){ 119 | printf("\t\TERMINATING shell...\r\n"); 120 | ustack(); 121 | stop(); 122 | printf("\t\tkilling...\r\n"); 123 | system("\t\tkill -9 %d", pid); 124 | printf("\t\tkilled.\r\n"); 125 | exit(-1); 126 | } 127 | } 128 | } 129 | 130 | /* TODO: Add TCP established opening of connection probe, etc. */ 131 | 132 | dtrace:::END 133 | { 134 | printf("\nCalls for PID %d,\n\n", $target); 135 | printf(" %-32s %-10s %-22s %8s\n", "FILE", "TYPE", "NAME", "COUNT"); 136 | printa(" %-32s %-10s %-22s %@8d\n", @calls); 137 | } 138 | -------------------------------------------------------------------------------- /tests/test_secimport_cli.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | import unittest 4 | 5 | from secimport.cli import SecImportCLI, SECIMPORT_ROOT 6 | 7 | 8 | class TestSecImportCLI(unittest.TestCase): 9 | @classmethod 10 | def setUpClass(cls): 11 | import sys 12 | 13 | sys.path.append(SECIMPORT_ROOT) 14 | 15 | def test_e2e(self): 16 | temp_script_path = "/tmp/.test_code.py" 17 | with open(temp_script_path, "w") as f: 18 | f.write('import os; os.system("echo hi");') 19 | 20 | cmd = f"/workspace/Python-3.11.8/python -m secimport.cli trace --entrypoint {temp_script_path} -o /tmp/.example.log" 21 | subprocess.run(cmd.split()) 22 | 23 | cmd = "/workspace/Python-3.11.8/python -m secimport.cli -m secimport.cli build --trace-file /tmp/.example.log" 24 | subprocess.run(cmd.split()) 25 | 26 | cmd = f"/workspace/Python-3.11.8/python -m secimport.cli run --entrypoint {temp_script_path}" 27 | subprocess.run(cmd.split()) 28 | 29 | # TODO: assert 30 | 31 | def test_help_command_runs_without_errors(self): 32 | cmd = "/workspace/Python-3.11.8/python -m secimport.cli --help" 33 | proc = subprocess.run(cmd.split()) 34 | self.assertEqual(proc.returncode, 0) 35 | 36 | def test_trace_pid_without_pid_raises_error(self): 37 | cmd = "/workspace/Python-3.11.8/python -m secimport.cli trace_pid" 38 | proc = subprocess.run(cmd.split()) 39 | self.assertEqual(proc.returncode, 2) 40 | 41 | def test_build_command_without_default_file_raises_error(self): 42 | cmd = "/workspace/Python-3.11.8/python -m secimport.cli build" 43 | proc = subprocess.run(cmd.split()) 44 | self.assertEqual(proc.returncode, 1) 45 | 46 | def test_run_command_runs_without_errors(self): 47 | cmd = "/workspace/Python-3.11.8/python -m secimport.cli run" 48 | proc = subprocess.run(cmd.split()) 49 | self.assertEqual(proc.returncode, 0) 50 | 51 | def test_run_command_with_entrypoint_option_runs_without_errors(self): 52 | cmd = ( 53 | "/workspace/Python-3.11.8/python -m secimport.cli run --entrypoint this.py" 54 | ) 55 | proc = subprocess.run(cmd.split()) 56 | self.assertEqual(proc.returncode, 0) 57 | 58 | def test_run_command_with_sandbox_executable_and_pid_options_runs_without_errors( 59 | self, 60 | ): 61 | cmd = "/workspace/Python-3.11.8/python -m secimport.cli run --sandbox_executable /path/to/my_sandbox.bt --pid 1" 62 | proc = subprocess.run(cmd.split()) 63 | self.assertEqual(proc.returncode, 0) 64 | 65 | def test_create_profile_from_trace_method_creates_yaml_template_file(self): 66 | filename = "test_traced_modules.yaml" 67 | SecImportCLI._SecImportCLI__create_profile_from_trace( 68 | "trace.log", output_yaml_file=filename 69 | ) 70 | self.assertTrue(os.path.isfile(filename)) 71 | 72 | def test_create_profile_from_trace_method_creates_json_template_file(self): 73 | filename = "test_traced_modules.json" 74 | SecImportCLI._SecImportCLI__create_profile_from_trace( 75 | "trace.log", output_json_file=filename 76 | ) 77 | self.assertTrue(os.path.isfile(filename)) 78 | 79 | def test_help_command(self): 80 | cmd = "/workspace/Python-3.11.8/python -m secimport.cli --help" 81 | proc = subprocess.run(cmd.split()) 82 | self.assertEqual(proc.returncode, 0) 83 | 84 | def test_fuzz_trace_command(self): 85 | cmd = "/workspace/Python-3.11.8/python -m secimport.cli trace --invalid_option" 86 | proc = subprocess.run(cmd.split()) 87 | self.assertEqual(proc.returncode, 2) 88 | 89 | def test_fuzz_trace_pid_command(self): 90 | cmd = "/workspace/Python-3.11.8/python -m secimport.cli trace_pid --invalid_option" 91 | proc = subprocess.run(cmd.split()) 92 | self.assertEqual(proc.returncode, 2) 93 | 94 | def test_fuzz_build_command(self): 95 | cmd = "/workspace/Python-3.11.8/python -m secimport.cli build --invalid_option" 96 | proc = subprocess.run(cmd.split()) 97 | self.assertEqual(proc.returncode, 2) 98 | 99 | def test_fuzz_run_command(self): 100 | cmd = "/workspace/Python-3.11.8/python -m secimport.cli run --invalid_option" 101 | proc = subprocess.run(cmd.split()) 102 | self.assertEqual(proc.returncode, 2) 103 | 104 | def test_run_command(self): 105 | cmd = "/workspace/Python-3.11.8/python -m secimport.cli run" 106 | proc = subprocess.run(cmd.split()) 107 | self.assertEqual(proc.returncode, 0) 108 | 109 | def test_fuzz_run_command_with_entrypoint_option(self): 110 | cmd = "/workspace/Python-3.11.8/python -m secimport.cli run --entrypoint=/invalid_path/main.py" 111 | proc = subprocess.run(cmd.split()) 112 | self.assertEqual(proc.returncode, 0) 113 | 114 | def test_fuzz_run_command_with_sandbox_executable_and_pid_options(self): 115 | cmd = "/workspace/Python-3.11.8/python -m secimport.cli run --sandbox_executable=/invalid_path/sandbox.bt --pid=1" 116 | proc = subprocess.run(cmd.split()) 117 | self.assertEqual(proc.returncode, 0) 118 | 119 | 120 | if __name__ == "__main__": 121 | unittest.main() 122 | -------------------------------------------------------------------------------- /examples/cli/ebpf/requests_demo/policy.json: -------------------------------------------------------------------------------- 1 | { 2 | "general_requirements": [ 3 | " access", 4 | " arch_prctl", 5 | " brk", 6 | " close", 7 | " fstat", 8 | " futex", 9 | " getcwd", 10 | " getrandom", 11 | " mmap", 12 | " mprotect", 13 | " munmap", 14 | " openat", 15 | " pread", 16 | " prlimit64", 17 | " read", 18 | " rt_sigaction", 19 | " rt_sigprocmask", 20 | " set_robust_list", 21 | " set_tid_address" 22 | ], 23 | "/root/.local/lib/python3.10/site-packages/idna/idnadata.py": [ 24 | " brk" 25 | ], 26 | "/workspace/Python-3.11.8/Lib/codecs.py": [ 27 | " close", 28 | " dup", 29 | " fstat", 30 | " ioctl", 31 | " lseek", 32 | " read", 33 | " rt_sigaction" 34 | ], 35 | "/workspace/Python-3.11.8/Lib/collections/__init__.py": [ 36 | " brk", 37 | " mmap" 38 | ], 39 | "/workspace/Python-3.11.8/Lib/email/message.py": [ 40 | " clock_gettime" 41 | ], 42 | "/workspace/Python-3.11.8/Lib/email/utils.py": [ 43 | " clock_gettime" 44 | ], 45 | "/workspace/Python-3.11.8/Lib/encodings/idna.py": [ 46 | " clock_gettime", 47 | " close", 48 | " connect", 49 | " fstat", 50 | " futex", 51 | " getpeername", 52 | " ioctl", 53 | " lseek", 54 | " mmap", 55 | " mprotect", 56 | " munmap", 57 | " openat", 58 | " poll", 59 | " read", 60 | " recvfrom", 61 | " sendto", 62 | " setsockopt", 63 | " socket", 64 | " stat", 65 | " uname" 66 | ], 67 | "/workspace/Python-3.11.8/Lib/genericpath.py": [ 68 | " stat" 69 | ], 70 | "/workspace/Python-3.11.8/Lib/http/cookiejar.py": [ 71 | " clock_gettime" 72 | ], 73 | "/workspace/Python-3.11.8/Lib/http/cookies.py": [ 74 | " brk" 75 | ], 76 | "/workspace/Python-3.11.8/Lib/logging/__init__.py": [ 77 | " exit_group", 78 | " munmap", 79 | " rt_sigaction" 80 | ], 81 | "/workspace/Python-3.11.8/Lib/os.py": [ 82 | " read" 83 | ], 84 | "/workspace/Python-3.11.8/Lib/posixpath.py": [ 85 | " close", 86 | " fstat", 87 | " getcwd", 88 | " getdents64", 89 | " openat" 90 | ], 91 | "/workspace/Python-3.11.8/Lib/random.py": [ 92 | " getrandom" 93 | ], 94 | "/workspace/Python-3.11.8/Lib/selectors.py": [ 95 | " close", 96 | " epoll_create1" 97 | ], 98 | "/workspace/Python-3.11.8/Lib/site.py": [ 99 | " fstat", 100 | " getegid", 101 | " geteuid", 102 | " getgid", 103 | " getuid", 104 | " ioctl", 105 | " lseek", 106 | " openat" 107 | ], 108 | "/workspace/Python-3.11.8/Lib/socket.py": [ 109 | " bind", 110 | " close", 111 | " getsockname", 112 | " ioctl", 113 | " socket" 114 | ], 115 | "/workspace/Python-3.11.8/Lib/sre_compile.py": [ 116 | " brk" 117 | ], 118 | "/workspace/Python-3.11.8/Lib/sre_constants.py": [ 119 | " mmap" 120 | ], 121 | "/workspace/Python-3.11.8/Lib/ssl.py": [ 122 | " brk", 123 | " clock_gettime", 124 | " close", 125 | " fstat", 126 | " futex", 127 | " getpid", 128 | " getrandom", 129 | " getsockopt", 130 | " ioctl", 131 | " openat", 132 | " read", 133 | " write" 134 | ], 135 | "/workspace/Python-3.11.8/Lib/threading.py": [ 136 | " gettid", 137 | " write" 138 | ], 139 | "/workspace/Python-3.11.8/Lib/typing.py": [ 140 | " mmap" 141 | ], 142 | "/workspace/Python-3.11.8/lib/python3.10/site-packages/requests/": [ 143 | " clock_gettime" 144 | ], 145 | "/workspace/Python-3.11.8/lib/python3.10/site-packages/urllib3/c": [ 146 | " clock_gettime", 147 | " ioctl" 148 | ], 149 | "/workspace/Python-3.11.8/lib/python3.10/site-packages/urllib3/r": [ 150 | " brk" 151 | ], 152 | "/workspace/Python-3.11.8/lib/python3.10/site-packages/urllib3/u": [ 153 | " brk", 154 | " clock_gettime", 155 | " close", 156 | " connect", 157 | " fstat", 158 | " ioctl", 159 | " openat", 160 | " read", 161 | " setsockopt" 162 | ], 163 | "": [ 164 | " brk", 165 | " clock_gettime", 166 | " close", 167 | " dup", 168 | " fcntl", 169 | " fstat", 170 | " futex", 171 | " ioctl", 172 | " lseek", 173 | " mmap", 174 | " mprotect", 175 | " munmap", 176 | " openat", 177 | " read", 178 | " readlink", 179 | " rt_sigaction", 180 | " stat", 181 | " sysinfo" 182 | ], 183 | "": [ 184 | " brk", 185 | " close", 186 | " fstat", 187 | " getcwd", 188 | " getdents64", 189 | " ioctl", 190 | " lseek", 191 | " lstat", 192 | " mmap", 193 | " munmap", 194 | " openat", 195 | " read", 196 | " readlink", 197 | " stat" 198 | ], 199 | "": [ 200 | " brk", 201 | " close", 202 | " fcntl", 203 | " fstat", 204 | " ioctl", 205 | " lseek", 206 | " openat", 207 | " read" 208 | ], 209 | "": [ 210 | " brk" 211 | ] 212 | } 213 | -------------------------------------------------------------------------------- /docs/PERFORMANCE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | **Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* 4 | 5 | - [Performance Benchmarks](#performance-benchmarks) 6 | - [Numpy specific benchmark (compute and not IO bound)](#numpy-specific-benchmark-compute-and-not-io-bound) 7 | - [Python 3.10 without dtrace](#python-310-without-dtrace) 8 | - [DTrace interpreter without secimport](#dtrace-interpreter-without-secimport) 9 | - [DTrace interpreter with secimport](#dtrace-interpreter-with-secimport) 10 | - [PyTorch Example](#pytorch-example) 11 | 12 | 13 | 14 | # Performance Benchmarks 15 | - ./python -m pyperformance run 16 | 17 | # Numpy specific benchmark (compute and not IO bound) 18 | - examples/numpy_example.py 19 | - examples/numpy_example_with_secure_import.py 20 | 21 | ## Python 3.10 without dtrace 22 | ``` 23 | Dotted two 4096x4096 matrices in 0.78 s. 24 | Dotted two vectors of length 524288 in 0.08 ms. 25 | SVD of a 2048x1024 matrix in 0.56 s. 26 | Cholesky decomposition of a 2048x2048 matrix in 0.09 s. 27 | ``` 28 | 29 | ## DTrace interpreter without secimport 30 | ``` 31 | Dotted two 4096x4096 matrices in 0.95 s. 32 | Dotted two vectors of length 524288 in 0.16 ms. 33 | SVD of a 2048x1024 matrix in 0.58 s. 34 | Cholesky decomposition of a 2048x2048 matrix in 0.09 s. 35 | ``` 36 | 37 | ## DTrace interpreter with secimport 38 | ``` 39 | Dotted two 4096x4096 matrices in 0.88 s. 40 | Dotted two vectors of length 524288 in 0.14 ms. 41 | SVD of a 2048x1024 matrix in 0.56 s. 42 | Cholesky decomposition of a 2048x2048 matrix in 0.09 s. 43 | ``` 44 | 45 | # PyTorch Example 46 | Inference Code (taken from https://pytorch.org/tutorials/beginner/pytorch_with_examples.html#pytorch-custom-nn-modules): 47 | ```python 48 | # -*- coding: utf-8 -*- 49 | import time 50 | import torch 51 | import math 52 | #import os 53 | 54 | class Polynomial3(torch.nn.Module): 55 | def __init__(self): 56 | """ 57 | In the constructor we instantiate four parameters and assign them as 58 | member parameters. 59 | """ 60 | super().__init__() 61 | self.a = torch.nn.Parameter(torch.randn(())) 62 | self.b = torch.nn.Parameter(torch.randn(())) 63 | self.c = torch.nn.Parameter(torch.randn(())) 64 | self.d = torch.nn.Parameter(torch.randn(())) 65 | 66 | def forward(self, x): 67 | """ 68 | In the forward function we accept a Tensor of input data and we must return 69 | a Tensor of output data. We can use Modules defined in the constructor as 70 | well as arbitrary operators on Tensors. 71 | """ 72 | # import os; os.system('ps') 73 | return self.a + self.b * x + self.c * x ** 2 + self.d * x ** 3 74 | 75 | def string(self): 76 | """ 77 | Just like any class in Python, you can also define custom method on PyTorch modules 78 | """ 79 | return f'y = {self.a.item()} + {self.b.item()} x + {self.c.item()} x^2 + {self.d.item()} x^3' 80 | 81 | start_time = time.time() 82 | # Create Tensors to hold input and outputs. 83 | x = torch.linspace(-math.pi, math.pi, 2000) 84 | y = torch.sin(x) 85 | 86 | # Construct our model by instantiating the class defined above 87 | model = Polynomial3() 88 | 89 | # Construct our loss function and an Optimizer. The call to model.parameters() 90 | # in the SGD constructor will contain the learnable parameters (defined 91 | # with torch.nn.Parameter) which are members of the model. 92 | criterion = torch.nn.MSELoss(reduction='sum') 93 | optimizer = torch.optim.SGD(model.parameters(), lr=1e-6) 94 | for t in range(2000): 95 | # Forward pass: Compute predicted y by passing x to the model 96 | y_pred = model(x) 97 | 98 | # Compute and print loss 99 | loss = criterion(y_pred, y) 100 | if t % 100 == 99: 101 | print(t, loss.item()) 102 | 103 | # Zero gradients, perform a backward pass, and update the weights. 104 | optimizer.zero_grad() 105 | loss.backward() 106 | optimizer.step() 107 | 108 | print(f'Result: {model.string()}') 109 | print("--- %s seconds ---" % (time.time() - start_time)) 110 | ``` 111 | 112 | Without secimport 113 | ```python 114 | root@3ecd9c9b5613:/workspace# Python-3.11.8/python pytorch_example.py 115 | 99 674.6323852539062 116 | 199 454.4176025390625 117 | 299 307.2438659667969 118 | 399 208.82205200195312 119 | 499 142.95999145507812 120 | 599 98.85627746582031 121 | 699 69.30175018310547 122 | 799 49.48223876953125 123 | 899 36.18083953857422 124 | 999 27.246694564819336 125 | 1099 21.241012573242188 126 | 1199 17.200336456298828 127 | 1299 14.479386329650879 128 | 1399 12.645381927490234 129 | 1499 11.408075332641602 130 | 1599 10.57250690460205 131 | 1699 10.00766372680664 132 | 1799 9.625473022460938 133 | 1899 9.366581916809082 134 | 1999 9.19102668762207 135 | Result: y = -0.013432367704808712 + 0.8425596952438354 x + 0.0023173068184405565 x^2 + -0.09131323546171188 x^3 136 | --- 0.6940326690673828 seconds --- 137 | ``` 138 | 139 | With secimport 140 | ```python 141 | root@3ecd9c9b5613:/workspace# secimport run --entrypoint pytorch_example.py 142 | >>> secimport run 143 | RUNNING SANDBOX... ['./sandbox.bt', '--unsafe', ' -c ', 'bash -c "/workspace/Python-3.11.8/python pytorch_example.py"'] 144 | Attaching 4 probes... 145 | 99 3723.3251953125 146 | 199 2513.790283203125 147 | 299 1699.73388671875 148 | 399 1151.3519287109375 149 | 499 781.596435546875 150 | 599 532.0443115234375 151 | 699 363.45361328125 152 | 799 249.44357299804688 153 | 899 172.26437377929688 154 | 999 119.9627914428711 155 | 1099 84.48213958740234 156 | 1199 60.386112213134766 157 | 1299 44.003780364990234 158 | 1399 32.853240966796875 159 | 1499 25.255172729492188 160 | 1599 20.07185935974121 161 | 1699 16.531814575195312 162 | 1799 14.111299514770508 163 | 1899 12.45437240600586 164 | 1999 11.318828582763672 165 | Result: y = -0.04061822220683098 + 0.8255564570426941 x + 0.007007318548858166 x^2 + -0.08889468014240265 x^3 166 | --- 0.8806719779968262 seconds --- 167 | 168 | SANDBOX EXITED; 169 | ``` 170 | -------------------------------------------------------------------------------- /docs/CLI.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | **Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* 4 | 5 | - [Usage](#usage) 6 | - [Stop on violation](#stop-on-violation) 7 | - [Kill on violation](#kill-on-violation) 8 | - [Dynamic profiling - trace, build sandbox, run.](#dynamic-profiling---trace-build-sandbox-run) 9 | 10 | 11 | 12 | ## Usage 13 | To sandbox your program using the CLI, start a bpftrace program that logs all the syscalls for all the modules in your application into a file with the secimport trace command. Once you have covered the logic you would like to sandbox, hit CTRL+C or CTRL+D, or wait for the program to finish. Then, build a sandbox from the trace using the secimport build command, and run the sandbox with the secimport run command. 14 | 15 | ```shell 16 | NAME 17 | SecImport - A toolkit for Tracing and Securing Python Runtime using USDT probes and eBPF/DTrace 18 | 19 | SYNOPSIS 20 | cli.py COMMAND 21 | 22 | DESCRIPTION 23 | QUICK START: 24 | $ secimport interactive 25 | 26 | EXAMPLES: 27 | 1. trace | shell: 28 | $ secimport shell 29 | $ secimport trace 30 | $ secimport trace -h 31 | $ secimport trace_pid 123 32 | $ secimport trace_pid -h 33 | 2. build: 34 | # secimport build 35 | $ secimport build 36 | 3. run: 37 | $ secimport run 38 | $ secimport run --entrypoint my_custom_main.py 39 | $ secimport run --entrypoint my_custom_main.py --stop_on_violation=true 40 | $ secimport run --entrypoint my_custom_main.py --kill_on_violation=true 41 | $ secimport run --sandbox_executable /path/to/my_sandbox.bt --pid 2884 42 | $ secimport run --sandbox_executable /path/to/my_sandbox.bt --sandbox_logfile my_log.log 43 | $ secimport run -h 44 | 45 | COMMANDS 46 | COMMAND is one of the following: 47 | 48 | build 49 | 50 | interactive 51 | 52 | run 53 | Run a python process inside the sandbox. 54 | 55 | trace 56 | Traces 57 | 58 | trace_pid 59 | Traces a running process by pid. It might require sudo privilleges on some hosts. 60 | 61 | ``` 62 | 63 | ## Stop on violation 64 | ``` 65 | root@1bc0531d91d0:/workspace# secimport run --stop_on_violation=true 66 | >>> secimport run 67 | [WARNING]: This sandbox will send SIGSTOP to the program upon violation. 68 | RUNNING SANDBOX... ['./sandbox.bt', '--unsafe', ' -c ', '/workspace/Python-3.11.8/python', 'STOP'] 69 | Attaching 4 probes... 70 | Python 3.11.8 (default, Apr 28 2023, 11:32:40) [GCC 9.4.0] on linux 71 | Type "help", "copyright", "credits" or "license" for more information. 72 | >>> import os 73 | >>> os.system('ps') 74 | [SECURITY PROFILE VIOLATED]: called syscall 56 at depth 8022 75 | 76 | ^^^ STOPPING PROCESS 85918 DUE TO SYSCALL VIOLATION ^^^ 77 | PROCESS 85918 STOPPED. 78 | ``` 79 | 80 | ## Kill on violation 81 | ``` 82 | root@ee4bc99bb011:/workspace# secimport run --kill_on_violation 83 | >>> secimport run 84 | [WARNING]: This sandbox will send SIGKILL to the program upon violation. 85 | RUNNING SANDBOX... ['./sandbox.bt', '--unsafe', ' -c ', '/workspace/Python-3.11.8/python', 'KILL'] 86 | import os 87 | oAttaching 4 probes... 88 | sPython 3.11.8 (default, Apr 28 2023, 11:32:40) [GCC 9.4.0] on linux 89 | Type "help", "copyright", "credits" or "license" for more information. 90 | >>> import os 91 | >>> os.system('ps') 92 | [SECURITY PROFILE VIOLATED]: called syscall 56 at depth 8022 93 | 94 | ^^^ KILLING PROCESS 86466 DUE TO SYSCALL VIOLATION ^^^ 95 | KILLED. 96 | SANDBOX EXITED; 97 | ``` 98 | 99 | ## Dynamic profiling - trace, build sandbox, run. 100 | ```shell 101 | root@1fa3d6f09989:/workspace# secimport interactive 102 | 103 | Let's create our first tailor-made sandbox with secimport! 104 | - A python shell will be opened 105 | - The behavior will be recorded. 106 | 107 | OK? (y): y 108 | >>> secimport trace 109 | 110 | TRACING: ['/workspace/secimport/profiles/trace.bt', '-c', '/workspace/Python-3.11.8/python', '-o', 'trace.log'] 111 | 112 | Press CTRL+D to stop the trace; 113 | 114 | Python 3.11.8 (default, Mar 19 2023, 08:34:46) [GCC 9.4.0] on linux 115 | Type "help", "copyright", "credits" or "license" for more information. 116 | >>> import this 117 | >>> 118 | TRACING DONE; 119 | >>> secimport build 120 | 121 | SECIMPORT COMPILING... 122 | 123 | CREATED JSON TEMPLATE: policy.json 124 | CREATED YAML TEMPLATE: policy.yaml 125 | 126 | 127 | compiling template policy.yaml 128 | [debug] adding syscall close to allowlist for module None 129 | [debug] adding syscall dup to allowlist for module None 130 | [debug] adding syscall fstat to allowlist for module None 131 | [debug] adding syscall ioctl to allowlist for module None 132 | [debug] adding syscall lseek to allowlist for module None 133 | [debug] adding syscall read to allowlist for module None 134 | ... 135 | [debug] adding syscall set_robust_list to allowlist for module general_requirements 136 | [debug] adding syscall set_tid_address to allowlist for module general_requirements 137 | 138 | DTRACE SANDBOX: sandbox.d 139 | BPFTRCE SANDBOX: sandbox.bt 140 | 141 | SANDBOX READY: sandbox.bt 142 | 143 | Now, let's run the sandbox! 144 | - Run the same commands as before, they should run without any problem;. 145 | - Do something new in the shell; e.g: >>> __import__("os").system("ps") 146 | 147 | OK? (y): y 148 | >>> secimport run 149 | RUNNING SANDBOX... ['./sandbox.bt', '--unsafe', ' -c ', '/workspace/Python-3.11.8/python'] 150 | Attaching 5 probes... 151 | REGISTERING SYSCALLS... 152 | STARTED 153 | Python 3.11.8 (default, Mar 19 2023, 08:34:46) [GCC 9.4.0] on linux 154 | Type "help", "copyright", "credits" or "license" for more information. 155 | >>> import this 156 | >>> import os 157 | [SECIMPORT VIOLATION]: called syscall ioctl at depth 0 158 | [SECIMPORT VIOLATION]: called syscall ioctl at depth 0 159 | ``` 160 | -------------------------------------------------------------------------------- /examples/cli/ebpf/requests_demo/policy.yaml: -------------------------------------------------------------------------------- 1 | modules: 2 | /root/.local/lib/python3.10/site-packages/idna/idnadata.py: 3 | destructive: false 4 | syscall_allowlist: 5 | - ' brk' 6 | /workspace/Python-3.11.8/Lib/codecs.py: 7 | destructive: false 8 | syscall_allowlist: 9 | - ' close' 10 | - ' dup' 11 | - ' fstat' 12 | - ' ioctl' 13 | - ' lseek' 14 | - ' read' 15 | - ' rt_sigaction' 16 | /workspace/Python-3.11.8/Lib/collections/__init__.py: 17 | destructive: false 18 | syscall_allowlist: 19 | - ' brk' 20 | - ' mmap' 21 | /workspace/Python-3.11.8/Lib/email/message.py: 22 | destructive: false 23 | syscall_allowlist: 24 | - ' clock_gettime' 25 | /workspace/Python-3.11.8/Lib/email/utils.py: 26 | destructive: false 27 | syscall_allowlist: 28 | - ' clock_gettime' 29 | /workspace/Python-3.11.8/Lib/encodings/idna.py: 30 | destructive: false 31 | syscall_allowlist: 32 | - ' clock_gettime' 33 | - ' close' 34 | - ' connect' 35 | - ' fstat' 36 | - ' futex' 37 | - ' getpeername' 38 | - ' ioctl' 39 | - ' lseek' 40 | - ' mmap' 41 | - ' mprotect' 42 | - ' munmap' 43 | - ' openat' 44 | - ' poll' 45 | - ' read' 46 | - ' recvfrom' 47 | - ' sendto' 48 | - ' setsockopt' 49 | - ' socket' 50 | - ' stat' 51 | - ' uname' 52 | /workspace/Python-3.11.8/Lib/genericpath.py: 53 | destructive: false 54 | syscall_allowlist: 55 | - ' stat' 56 | /workspace/Python-3.11.8/Lib/http/cookiejar.py: 57 | destructive: false 58 | syscall_allowlist: 59 | - ' clock_gettime' 60 | /workspace/Python-3.11.8/Lib/http/cookies.py: 61 | destructive: false 62 | syscall_allowlist: 63 | - ' brk' 64 | /workspace/Python-3.11.8/Lib/logging/__init__.py: 65 | destructive: false 66 | syscall_allowlist: 67 | - ' exit_group' 68 | - ' munmap' 69 | - ' rt_sigaction' 70 | /workspace/Python-3.11.8/Lib/os.py: 71 | destructive: false 72 | syscall_allowlist: 73 | - ' read' 74 | /workspace/Python-3.11.8/Lib/posixpath.py: 75 | destructive: false 76 | syscall_allowlist: 77 | - ' close' 78 | - ' fstat' 79 | - ' getcwd' 80 | - ' getdents64' 81 | - ' openat' 82 | /workspace/Python-3.11.8/Lib/random.py: 83 | destructive: false 84 | syscall_allowlist: 85 | - ' getrandom' 86 | /workspace/Python-3.11.8/Lib/selectors.py: 87 | destructive: false 88 | syscall_allowlist: 89 | - ' close' 90 | - ' epoll_create1' 91 | /workspace/Python-3.11.8/Lib/site.py: 92 | destructive: false 93 | syscall_allowlist: 94 | - ' fstat' 95 | - ' getegid' 96 | - ' geteuid' 97 | - ' getgid' 98 | - ' getuid' 99 | - ' ioctl' 100 | - ' lseek' 101 | - ' openat' 102 | /workspace/Python-3.11.8/Lib/socket.py: 103 | destructive: false 104 | syscall_allowlist: 105 | - ' bind' 106 | - ' close' 107 | - ' getsockname' 108 | - ' ioctl' 109 | - ' socket' 110 | /workspace/Python-3.11.8/Lib/sre_compile.py: 111 | destructive: false 112 | syscall_allowlist: 113 | - ' brk' 114 | /workspace/Python-3.11.8/Lib/sre_constants.py: 115 | destructive: false 116 | syscall_allowlist: 117 | - ' mmap' 118 | /workspace/Python-3.11.8/Lib/ssl.py: 119 | destructive: false 120 | syscall_allowlist: 121 | - ' brk' 122 | - ' clock_gettime' 123 | - ' close' 124 | - ' fstat' 125 | - ' futex' 126 | - ' getpid' 127 | - ' getrandom' 128 | - ' getsockopt' 129 | - ' ioctl' 130 | - ' openat' 131 | - ' read' 132 | - ' write' 133 | /workspace/Python-3.11.8/Lib/threading.py: 134 | destructive: false 135 | syscall_allowlist: 136 | - ' gettid' 137 | - ' write' 138 | /workspace/Python-3.11.8/Lib/typing.py: 139 | destructive: false 140 | syscall_allowlist: 141 | - ' mmap' 142 | /workspace/Python-3.11.8/lib/python3.10/site-packages/requests/: 143 | destructive: false 144 | syscall_allowlist: 145 | - ' clock_gettime' 146 | /workspace/Python-3.11.8/lib/python3.10/site-packages/urllib3/c: 147 | destructive: false 148 | syscall_allowlist: 149 | - ' clock_gettime' 150 | - ' ioctl' 151 | /workspace/Python-3.11.8/lib/python3.10/site-packages/urllib3/r: 152 | destructive: false 153 | syscall_allowlist: 154 | - ' brk' 155 | /workspace/Python-3.11.8/lib/python3.10/site-packages/urllib3/u: 156 | destructive: false 157 | syscall_allowlist: 158 | - ' brk' 159 | - ' clock_gettime' 160 | - ' close' 161 | - ' connect' 162 | - ' fstat' 163 | - ' ioctl' 164 | - ' openat' 165 | - ' read' 166 | - ' setsockopt' 167 | : 168 | destructive: false 169 | syscall_allowlist: 170 | - ' brk' 171 | - ' clock_gettime' 172 | - ' close' 173 | - ' dup' 174 | - ' fcntl' 175 | - ' fstat' 176 | - ' futex' 177 | - ' ioctl' 178 | - ' lseek' 179 | - ' mmap' 180 | - ' mprotect' 181 | - ' munmap' 182 | - ' openat' 183 | - ' read' 184 | - ' readlink' 185 | - ' rt_sigaction' 186 | - ' stat' 187 | - ' sysinfo' 188 | : 189 | destructive: false 190 | syscall_allowlist: 191 | - ' brk' 192 | - ' close' 193 | - ' fstat' 194 | - ' getcwd' 195 | - ' getdents64' 196 | - ' ioctl' 197 | - ' lseek' 198 | - ' lstat' 199 | - ' mmap' 200 | - ' munmap' 201 | - ' openat' 202 | - ' read' 203 | - ' readlink' 204 | - ' stat' 205 | : 206 | destructive: false 207 | syscall_allowlist: 208 | - ' brk' 209 | - ' close' 210 | - ' fcntl' 211 | - ' fstat' 212 | - ' ioctl' 213 | - ' lseek' 214 | - ' openat' 215 | - ' read' 216 | : 217 | destructive: false 218 | syscall_allowlist: 219 | - ' brk' 220 | general_requirements: 221 | destructive: false 222 | syscall_allowlist: 223 | - ' access' 224 | - ' arch_prctl' 225 | - ' brk' 226 | - ' close' 227 | - ' fstat' 228 | - ' futex' 229 | - ' getcwd' 230 | - ' getrandom' 231 | - ' mmap' 232 | - ' mprotect' 233 | - ' munmap' 234 | - ' openat' 235 | - ' pread' 236 | - ' prlimit64' 237 | - ' read' 238 | - ' rt_sigaction' 239 | - ' rt_sigprocmask' 240 | - ' set_robust_list' 241 | - ' set_tid_address' 242 | -------------------------------------------------------------------------------- /docs/EXAMPLES.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | **Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* 4 | 5 | - [Sandbox Examples](#sandbox-examples) 6 | - [Docker examples](#docker-examples) 7 | - [Secure Import Examples](#secure-import-examples) 8 | - [Simple Usage](#simple-usage) 9 | - [Advanced Usage](#advanced-usage) 10 | - [Interactive Examples](#interactive-examples) 11 | - [How pickle can be exploited in your 3rd party packages:](#how-pickle-can-be-exploited-in-your-3rd-party-packages) 12 | - [Creating a YAML policy file](#creating-a-yaml-policy-file) 13 | - [Blocking New Processes Example](#blocking-new-processes-example) 14 | - [Shell Blocking Example](#shell-blocking-example) 15 | - [Network Blocking Example](#network-blocking-example) 16 | 17 | 18 | 19 | # Sandbox Examples 20 | To understand how to trace your process and create custom profiles for modules or applications, please see Tracing Processes 21 | 22 | # Docker examples 23 | We have a Zero-To-One with Docker that goes through Tracing and Compiling an eBPF sandbox for an app. 24 | 25 | # Secure Import Examples 26 | ## Simple Usage 27 | - `secimport` CLI usage 28 | - The easiest option to start with inside docker. 29 | - `secimport --help` or `python3 -m secimport.cli` 30 | - Running Sandbox Using Python Imports 31 | - See YAML Profiles Usage 32 |

33 | 34 | ## Advanced Usage 35 | - `eBPF` Example Sandbox 36 | - `DTrace` Example Sandbox 37 | - Generate Sandbox Code From YAML Configuration 38 |

39 | 40 | # Interactive Examples 41 | 42 | ### How pickle can be exploited in your 3rd party packages: 43 | ```python 44 | import pickle 45 | class Demo: 46 | def __reduce__(self): 47 | return (eval, ("__import__('os').system('echo Exploited!')",)) 48 | 49 | pickle.dumps(Demo()) 50 | b"\x80\x04\x95F\x00\x00\x00\x00\x00\x00\x00\x8c\x08builtins\x94\x8c\x04eval\x94\x93\x94\x8c*__import__('os').system('echo Exploited!')\x94\x85\x94R\x94." 51 | pickle.loads(b"\x80\x04\x95F\x00\x00\x00\x00\x00\x00\x00\x8c\x08builtins\x94\x8c\x04eval\x94\x93\x94\x8c*__import__('os').system('echo Exploited!')\x94\x85\x94R\x94.") 52 | Exploited! 53 | 0 54 | ``` 55 | With `secimport`, you can control such action to do whatever you want: 56 | ```python 57 | import secimport 58 | pickle = secimport.secure_import("pickle") 59 | pickle.loads(b"\x80\x04\x95F\x00\x00\x00\x00\x00\x00\x00\x8c\x08builtins\x94\x8c\x04eval\x94\x93\x94\x8c*__import__('os').system('echo Exploited!')\x94\x85\x94R\x94.") 60 | 61 | [1] 28027 killed ipython 62 | ``` 63 | A log file is automatically created, containing everything you need to know: 64 | ``` 65 | $ less /tmp/.secimport/sandbox_pickle.log 66 | 67 | @posix_spawn from /Users/avilumelsky/Downloads/Python-3.11.8/Lib/threading.py 68 | DETECTED SHELL: 69 | depth=8 70 | sandboxed_depth=0 71 | sandboxed_module=/Users/avilumelsky/Downloads/Python-3.11.8/Lib/pickle.py 72 | 73 | TERMINATING SHELL: 74 | libsystem_kernel.dylib`__posix_spawn+0xa 75 | ... 76 | libsystem_kernel.dylib`__posix_spawn+0xa 77 | libsystem_c.dylib`system+0x18b 78 | python.exe`os_system+0xb3 79 | KILLED 80 | : 81 | ``` 82 |

83 | ## Creating a YAML policy file 84 | For a full tutorial, see YAML Profiles Usage 85 | ```shell 86 | # An example yaml template for a sandbox. 87 | 88 | modules: 89 | requests: 90 | destructive: true 91 | syscall_allowlist: 92 | - write 93 | - ioctl 94 | - stat64 95 | fastapi: 96 | destructive: true 97 | syscall_allowlist: 98 | - bind 99 | - fchmod 100 | - stat64 101 | uvicorn: 102 | destructive: true 103 | syscall_allowlist: 104 | - getpeername 105 | - getpgrp 106 | - stat64 107 | 108 | ``` 109 |

110 | ## Blocking New Processes Example 111 | ```python 112 | Python 3.11.8 (default, May 2 2022, 21:43:20) [Clang 13.0.0 (clang-1300.0.27.3)] on darwin 113 | Type "help", "copyright", "credits" or "license" for more information. 114 | 115 | # Let's import subprocess module, limiting it's syscall access. 116 | import secimport 117 | subprocess = secimport.secure_import("subprocess") 118 | 119 | # Let's import os 120 | import os 121 | os.system("ps") 122 | PID TTY TIME CMD 123 | 2022 ttys000 0:00.61 /bin/zsh -l 124 | 50092 ttys001 0:04.66 /bin/zsh -l 125 | 75860 ttys001 0:00.13 python 126 | 0 127 | # It worked as expected, returning exit code 0. 128 | 129 | 130 | # Now, let's try to invoke the same logic using a different module, "subprocess", that was imported using secure_import: 131 | subprocess.check_call('ps') 132 | [1] 75860 killed python 133 | 134 | # Damn! That's cool. 135 | ``` 136 | 137 | When using secure_import, the following files are created: 138 | - The dtrace/bpftrace sandbox code for the module is saved under: 139 | - `/tmp/.secimport/sandbox_subprocess.bt`: 140 | - when using bpftrace 141 | - `/tmp/.secimport/sandbox_subprocess.d` 142 | - when using dtrace 143 | - The log file for this module is under 144 | - `/tmp/.secimport/sandbox_subprocess.log`: 145 | ```shell 146 | ... 147 | 148 | (OPENING SHELL using posix_spawn): (pid 75860) (thread 344676) (user 501) (python module: ) (probe mod=, name=entry, prov=syscall func=posix_spawn) /bin/sh 149 | #posix_spawn, 150 | 151 | (TOUCHING FILESYSTEM): write(140339021606912) from thread 344676 152 | libsystem_kernel.dylib`__fork+0xb 153 | _posixsubprocess.cpython-310-darwin.so`do_fork_exec+0x29 154 | _posixsubprocess.cpython-310-darwin.so`subprocess_fork_exec+0x71f 155 | python.exe`cfunction_call+0x86 156 | killing... 157 | killed. 158 | ``` 159 |

160 | ## Shell Blocking Example 161 | You can try it yourself with docker, locally. 162 | ``` 163 | ./docker/build.sh 164 | ./docker/run.sh 165 | root@d57458518cbf:/workspace$ ./run_sandbox.sh 166 | 🚀 Starting secimport sandbox with bpftrace backend, the sandbox should kill the python process... 167 | WARNING: Addrspace is not set 168 | PID TTY TIME CMD 169 | 1 pts/0 00:00:00 sh 170 | 10 pts/0 00:00:00 bash 171 | 18 pts/0 00:00:00 bash 172 | 19 pts/0 00:00:00 bpftrace 173 | 23 pts/0 00:00:00 python 174 | 24 pts/0 00:00:00 sh 175 | 25 pts/0 00:00:00 sh 176 | 26 pts/0 00:00:00 pkill 177 | 27 pts/0 00:00:00 ps 178 | 179 | 180 | 🛑 The process was killed, as expected. 181 | 🚀 The sandbox bpftrace code is at sandbox.bt 182 | 🚀 The sandbox log is at sandbox.log. 183 | ``` 184 | 185 | The following code will open a new bash shell using os.system. 186 | In this sandbox we disallow such behavior, by disabling the fork/exec/spawn syscalls. 187 | 188 | ```python 189 | # example.py - Executes code upon import; 190 | import os; 191 | 192 | os.system('Hello World!'); 193 | ``` 194 | ```python 195 | from secimport import secure_import 196 | 197 | example = secure_import('example', allow_shells=False) 198 | ``` 199 | Let's run the and see what happens: 200 | ``` 201 | (root) sh-3.2# export PYTHONPATH=$(pwd):$(pwd)/examples:$(pwd):$PYTHONPATH 202 | (root) sh-3.2# python examples/production.py 203 | Successfully compiled dtrace profile: /tmp/.secimport/sandbox_example.d 204 | Killed: 9 205 | ``` 206 | - We imported `example` with limited capabilities. 207 | - If a syscall like `spawn/exec/fork/forkexec` will be executed 208 | - The process will be `kill`ed with `-9` signal. 209 |

210 | ## Network Blocking Example 211 | ``` 212 | import requests 213 | requests.get('https://google.com') 214 | 215 | 216 | 217 | from secimport import secure_import 218 | requests = secure_import('requests', allow_networking=False) 219 | 220 | # The next call should kill the process, 221 | # because we disallowed networking for the requests module. 222 | requests.get('https://google.com') 223 | [1] 86664 killed 224 | ``` 225 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # secimport 2 | ![macOS](https://img.shields.io/badge/Platform-macOS-blue) 3 | ![Linux](https://img.shields.io/badge/Platform-Linux-blue) 4 | 5 | [![Upload Python Package](https://github.com/avilum/secimport/actions/workflows/python-publish.yml/badge.svg)](https://github.com/avilum/secimport/actions/workflows/python-publish.yml) 6 | 7 | 8 | 9 | ## Module-Level Sandboxing for Python Applications 10 | 11 | secimport is an eBPF-based security toolkit that enforces syscall restrictions per Python module, providing granular control over your application's security profile. Think of it as seccomp-bpf for Linux, but operating at the Python module level. 12 | 13 | [ 14 |   15 |     16 |     17 |    Star History Chart 18 |   19 | ](https://star-history.com/#avilum/secimport&Date) 20 | 21 | ## Key Features 22 | 23 | - **Module-Level Security**: Define and enforce syscall restrictions per Python module 24 | - **Automated Profiling**: Traces your application to create tailored security profiles 25 | - **Multiple Enforcement Modes**: Log, stop, or kill processes on policy violations 26 | - **Production Ready**: Negligible performance impact thanks to eBPF 27 | - **Supply Chain Protection**: Mitigate risks from vulnerable dependencies 28 | 29 | ## Quick Start 30 | 31 | ### Using Docker (Recommended) 32 | 33 | ```bash 34 | git clone https://github.com/avilum/secimport.git 35 | cd secimport/docker 36 | ./build.sh && ./run.sh 37 | ``` 38 | Command line: 39 | ``` 40 | secimport --help 41 | 42 | Usage: python -m secimport.cli [OPTIONS] COMMAND [ARGS]... 43 | 44 | secimport is a comprehensive toolkit designed to enable the tracing, construction, and execution of secure Python runtimes. It leverages USDT probes and eBPF/DTrace technologies to enhance overall security measures. 45 | WORKFLOW: 1. secimport trace / secimport shell 2. secimport build 3. secimport run 46 | QUICKSTART: $ secimport interactive 47 | For more details, please see https://github.com/avilum/secimport/wiki/Command-Line-Usage 48 | 49 | ╭─ Options ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ 50 | │ --install-completion Install completion for the current shell. │ 51 | │ --show-completion Show completion for the current shell, to copy it or customize the installation. │ 52 | │ --help Show this message and exit. │ 53 | ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ 54 | ╭─ Commands ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ 55 | │ trace Trace a Python process using an entrypoint or interactive shell. │ 56 | │ shell Alias for 'trace'. │ 57 | │ trace-pid Trace a running process by PID. │ 58 | │ build Build a sandbox profile from a trace log. │ 59 | │ run Run a Python process inside the sandbox. │ 60 | │ interactive Run secimport in interactive mode. │ 61 | ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ 62 | ``` 63 | 64 | ## Creating Your First Sandbox 65 | 66 | ```bash 67 | secimport interactive 68 | 69 | # In the Python shell that opens: 70 | >>> secimport trace # Start tracing 71 | >>> import requests # Perform actions you want to profile 72 | >>> # Press CTRL+D to stop tracing 73 | 74 | >>> secimport build # Build sandbox from trace 75 | >>> secimport run # Run with enforcement 76 | ``` 77 | 78 | ## Advanced Usage 79 | 80 | ### Command Line Options 81 | 82 | ```bash 83 | secimport trace # Trace a new Python process 84 | secimport trace_pid # Trace an existing process 85 | secimport build # Build sandbox from trace 86 | secimport run [options] # Run with enforcement 87 | ``` 88 | 89 | ### Enforcement Modes 90 | 91 | ```bash 92 | # Stop on violation 93 | secimport run --stop_on_violation=true 94 | 95 | # Kill on violation 96 | secimport run --kill_on_violation=true 97 | ``` 98 | 99 | ### Python API 100 | 101 | ```python 102 | import secimport 103 | 104 | # Replace standard import with secure import 105 | requests = secimport.secure_import('requests', allowed_syscalls=['open', 'read', ...]) 106 | ``` 107 | 108 | ## Manual Installation 109 | 110 | 1. Install Python with USDT probes: 111 | 112 | ```bash 113 | # Configure Python with --enable-dtrace 114 | # See detailed instructions in our wiki 115 | ``` 116 | 117 | 2. Install a supported backend (eBPF or DTrace) 118 | 119 | ```bash 120 | # Ubuntu/Debian 121 | apt-get install bpftrace 122 | 123 | # For other platforms, see our Installation wiki 124 | ``` 125 | 126 | 3. Install secimport 127 | ```bash 128 | pip install secimport 129 | ``` 130 | 131 | ## seccomp-bpf support using nsjail 132 | 133 | Beside the sandbox that secimport builds,
134 | The `secimport build` command creates an nsjail sandbox with seccomp profile for your traced code.
`nsjail` enables namespace sandboxing with seccomp on linux
135 | `secimport` automatically generates seccomp profiles to use with `nsjail` as executable bash script. 136 | It can be used to limit the syscalls of the entire python process, as another layer of defence. 137 | 138 | ## Documentation 139 | 140 | - [Installation Guide](https://github.com/avilum/secimport/wiki/Installation) 141 | - [Command Line Usage](https://github.com/avilum/secimport/wiki/Command-Line-Usage) 142 | - [API Reference](https://github.com/avilum/secimport/wiki/Python-API) 143 | - [Example Sandboxes](https://github.com/avilum/secimport/wiki/Sandbox-Examples) 144 | 145 | ## Learn More 146 | 147 | ### Technical Resources 148 | 149 | - https://www.oligo.security/ 150 | - [Talk: secimport on PyCon](https://www.youtube.com/watch?v=6DJNQtBJvLA) 151 | - [Talk: secimport at BSides](https://youtu.be/nRV0ulYMsxU?t=1257) 152 | - Blog Posts: 153 | - [secimport + DTrace](https://infosecwriteups.com/sandboxing-python-modules-in-your-code-1e590d71fc26?source=friends_link&sk=5e9a2fa4d4921af0ec94f175f7ee49f9) 154 | - [secimport + eBPF + PyTorch](https://infosecwriteups.com/securing-pytorch-models-with-ebpf-7f75732b842d?source=friends_link&sk=14d8db403aaf66724a8a69b4dea24e12) 155 | - [secimport + eBPF + FastAPI](https://avi-lumelsky.medium.com/secure-fastapi-with-ebpf-724d4aef8d9e?source=friends_link&sk=b01a6b97ef09003b53cd52c479017b03) 156 | 157 | 158 | 159 | ## Contributing 160 | 161 | We welcome contributions! See our [Contributing Guide](https://github.com/avilum/secimport/blob/master/docs/CONTRIBUTING.md) for details. 162 | 163 | ## License 164 | 165 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 166 | -------------------------------------------------------------------------------- /secimport/backends/common/utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | from sys import platform 3 | from pathlib import Path 4 | from typing import List 5 | import importlib 6 | from sys import executable as PYTHON_EXECUTABLE 7 | 8 | from secimport.backends.common.instrumentation_backend import InstrumentationBackend 9 | from secimport.backends.common.system_calls import ( 10 | DEFAULT_ALLOWED_SYSCALLS, 11 | SYSCALLS_NAMES, 12 | SYSCALLS_NUMBERS, 13 | ) 14 | 15 | BASE_DIR_NAME = Path("/tmp/.secimport") 16 | SECIMPORT_ROOT = Path( 17 | os.path.realpath( 18 | os.path.split(__file__)[:-1][0] + os.sep + os.pardir + os.sep + os.pardir 19 | ) 20 | ) 21 | 22 | PROFILES_DIR_NAME = SECIMPORT_ROOT / "profiles" 23 | BPFTRACE_TEMPLATES_DIR_NAME = SECIMPORT_ROOT / "backends" / "bpftrace_backend" 24 | DTRACE_TEMPLATES_DIR_NAME = SECIMPORT_ROOT / "backends" / "dtrace_backend" 25 | TEMPLATES_DIR_NAME = None 26 | DEFAULT_BACKEND = None 27 | 28 | if "linux" in platform.lower(): 29 | TEMPLATES_DIR_NAME = BPFTRACE_TEMPLATES_DIR_NAME 30 | DEFAULT_BACKEND = InstrumentationBackend.EBPF 31 | # TODO: verify bpftrace is installed, if not, link to the repo documentation on how to install. 32 | else: 33 | TEMPLATES_DIR_NAME = DTRACE_TEMPLATES_DIR_NAME 34 | DEFAULT_BACKEND = InstrumentationBackend.DTRACE 35 | 36 | 37 | def render_syscalls_filter( 38 | syscalls_list: List[str], 39 | allow: bool, 40 | instrumentation_backend: InstrumentationBackend, 41 | module_name=None, 42 | ): 43 | assert isinstance(allow, bool), '"allow" must be a bool value' 44 | # "=="" means the syscall matches (blocklist), while "!="" means allow only the following. 45 | match_sign = "!=" if allow else "==" 46 | syscalls_filter = "" 47 | added_syscalls = set() 48 | print("\n[info] Creating profile for ", module_name) 49 | for i, _syscall in enumerate(syscalls_list): 50 | _syscall = _syscall.strip().lower() 51 | if i > 0: 52 | if instrumentation_backend == InstrumentationBackend.DTRACE: 53 | syscalls_filter += " && " 54 | assert isinstance( 55 | _syscall, str 56 | ), f"The provided syscall it not a syscall string name: '{_syscall}'" 57 | 58 | # Translating syscall 59 | _syscall_number = SYSCALLS_NAMES.get(_syscall) 60 | if _syscall_number is None: 61 | print("Warning:", NotImplementedError( 62 | f"The provided syscall it not a syscall mapped to a number: '{_syscall}'" 63 | )) 64 | continue 65 | 66 | # dtrace 67 | if instrumentation_backend == InstrumentationBackend.DTRACE: 68 | if module_name is not None: 69 | raise NotImplementedError( 70 | "Specific modules syscalls are not allowed using render_syscall_filter. Please use YAML template instead." 71 | ) 72 | syscalls_filter += f'probefunc {match_sign} "{_syscall}"' 73 | 74 | # bpftrace 75 | elif instrumentation_backend == InstrumentationBackend.EBPF: 76 | expected_output_value = ( 77 | "1" if allow else "2" 78 | ) # 0 is undefined; 1 is allowed; 2 is blocked; 79 | module_name = ( 80 | module_name if module_name is not None else '@globals["current_module"]' 81 | ) 82 | # module_name = module_name[-64:] # taking only 64 last characters because bpftrace allow string to be 64 characters. 83 | if module_name and (module_name, _syscall_number) not in added_syscalls: 84 | # Module is from site packages 85 | # if "site-packages" in module_name: 86 | # # Trimming the site packages 87 | # base_path, module_name = module_name.split("site-packages") 88 | # module_name = module_name.strip("/") 89 | # print( 90 | # f'[debug]\t Using module name: "{module_name}", removed {base_path}' 91 | # ) 92 | 93 | syscalls_filter += f'@syscalls_filters["{module_name}", {_syscall_number}] = {expected_output_value}; // {"Allow" if allow else "Deny"} {SYSCALLS_NUMBERS[_syscall_number]}' 94 | syscalls_filter += "\n" 95 | added_syscalls.add((module_name, _syscall_number)) 96 | else: 97 | pass 98 | else: 99 | raise NotImplementedError( 100 | f"backend '{instrumentation_backend}' is not supported" 101 | ) 102 | 103 | if module_name: 104 | filter_name = "allowlist" if allow else "blocklist" 105 | print( 106 | f"[debug]\t adding syscall {_syscall} to {filter_name} for module {module_name}" 107 | ) 108 | return syscalls_filter 109 | 110 | 111 | def build_module_sandbox_from_yaml_template( 112 | template_path: Path, 113 | backend: InstrumentationBackend = DEFAULT_BACKEND, 114 | ): 115 | """Generates sandbox code for secure imports based on a YAML configuration. 116 | 117 | Args: 118 | template_path (Path): The path to the YAML file, describing the policies. 119 | templates_dir (Path, optional): The directory of the templates. Defaults to TEMPLATES_DIR_NAME. 120 | 121 | Raises: 122 | ModuleNotFoundError: 123 | 124 | Returns: 125 | _type_: str 126 | """ 127 | assert Path( 128 | template_path 129 | ).exists(), f"The template does not exist at {template_path}" 130 | import yaml 131 | 132 | safe_yaml = yaml.safe_load(open(template_path, "r").read()) 133 | parsed_probes = [] 134 | syscalls_filter = "" 135 | 136 | # Adding the general syscalls filter 137 | syscalls_filter += render_syscalls_filter( 138 | syscalls_list=DEFAULT_ALLOWED_SYSCALLS, 139 | allow=False, 140 | instrumentation_backend=InstrumentationBackend.EBPF, 141 | module_name="general_requirements", 142 | ) 143 | 144 | for module_name, module_config in safe_yaml.get("modules", {}).items(): 145 | # Finding the module without loading 146 | module = importlib.machinery.PathFinder().find_spec(module_name) 147 | if module is None: 148 | print( 149 | f"[debug]\t '{module_name}' is not importable via importlib. Keeping the name as-is." 150 | ) 151 | module_traced_name = module_name 152 | else: 153 | # print( 154 | # f"[debug]\t '{module_name}' is importable as '{module.origin}'; Using the name '{module.origin}';" 155 | # ) 156 | 157 | # module_traced_name = module.origin 158 | module_traced_name = module_name 159 | 160 | # Tracing module entrypoint 161 | # module_traced_name = os.path.split(module_traced_name)[:-1][0] 162 | 163 | _destructive = module_config.get("destructive") 164 | assert isinstance(_destructive, bool), ValueError( 165 | f'The "destructive" field for module {module_name} is empty.' 166 | ) 167 | 168 | _syscall_allowlist = module_config.get("syscall_allowlist") 169 | assert _syscall_allowlist, ValueError( 170 | f'The "syscall_allowlist" for module {module_name} is empty.' 171 | ) 172 | for _ in _syscall_allowlist: 173 | assert isinstance(_, str), ValueError( 174 | f'The "syscall_allowlist" field for module {module_name} contains invalid string: {_}' 175 | ) 176 | 177 | if backend == InstrumentationBackend.DTRACE: 178 | from secimport.backends.dtrace_backend.dtrace_backend import ( 179 | render_dtrace_probe_for_module, 180 | ) 181 | 182 | module_sandbox_probe = render_dtrace_probe_for_module( 183 | module_name=module_traced_name, 184 | destructive=_destructive, 185 | syscalls_allowlist=_syscall_allowlist, 186 | ) 187 | sandbox_file_name = "default.yaml.template.d" 188 | script_template = open( 189 | DTRACE_TEMPLATES_DIR_NAME / sandbox_file_name, 190 | "r", 191 | ).read() 192 | elif backend == InstrumentationBackend.EBPF: 193 | from secimport.backends.bpftrace_backend.bpftrace_backend import ( 194 | render_bpftrace_probe_for_module, 195 | ) 196 | 197 | module_sandbox_probe = render_bpftrace_probe_for_module( 198 | module_name=module_traced_name, 199 | destructive=_destructive, 200 | ) 201 | 202 | # Adding a syscalls filter 203 | syscalls_filter += render_syscalls_filter( 204 | syscalls_list=_syscall_allowlist, 205 | allow=True, 206 | instrumentation_backend=InstrumentationBackend.EBPF, 207 | module_name=module_traced_name, 208 | ) 209 | sandbox_file_name = "default.yaml.template.bt" 210 | script_template = open( 211 | BPFTRACE_TEMPLATES_DIR_NAME / sandbox_file_name, 212 | "r", 213 | ).read() 214 | assert module_sandbox_probe, ValueError( 215 | f"Failed to create a probe for module {module_name}" 216 | ) 217 | parsed_probes.append(module_sandbox_probe) 218 | 219 | if not parsed_probes: 220 | print(f"The profile does not contain any modules: {template_path}") 221 | return 222 | 223 | script_template = script_template.replace("###SYSCALL_FILTER###", syscalls_filter) 224 | script_template = script_template.replace( 225 | "###INTERPRETER_PATH###", PYTHON_EXECUTABLE 226 | ) 227 | probes_code = ("\n" * 2).join(parsed_probes) 228 | script_template = script_template.replace( 229 | "###SUPERVISED_MODULES_PROBES###", probes_code 230 | ) 231 | return script_template 232 | -------------------------------------------------------------------------------- /secimport/backends/bpftrace_backend/bpftrace_backend.py: -------------------------------------------------------------------------------- 1 | """ 2 | Import python modules with bpftrace supervision. 3 | Copyright (c) 2022 Avi Lumelsky 4 | """ 5 | 6 | import importlib 7 | import os 8 | import subprocess 9 | from sys import executable as PYTHON_EXECUTABLE 10 | import stat 11 | from pathlib import Path 12 | 13 | from secimport.backends.common.utils import ( 14 | BASE_DIR_NAME, 15 | SECIMPORT_ROOT, 16 | ) 17 | 18 | TEMPLATES_DIR_NAME = SECIMPORT_ROOT / "backends" / "bpftrace_backend" 19 | 20 | 21 | def create_bpftrace_script_for_module( 22 | module_name: str, 23 | allow_shells: bool, 24 | allow_networking: bool, 25 | log_python_calls: bool, 26 | log_syscalls: bool, 27 | log_network: bool, 28 | log_file_system: bool, 29 | destructive: bool, 30 | templates_dir: Path = TEMPLATES_DIR_NAME, 31 | ) -> Path: 32 | module = importlib.machinery.PathFinder().find_spec(module_name) 33 | if module is None: 34 | raise ModuleNotFoundError(module_name) 35 | module_traced_name = module.origin # e.g this.py 36 | script_template = "" 37 | syscalls_filter = "" 38 | script_template = render_bpftrace_template( 39 | module_traced_name=module_traced_name, 40 | allow_shells=allow_shells, 41 | allow_networking=allow_networking, 42 | log_python_calls=log_python_calls, 43 | log_syscalls=log_syscalls, 44 | log_network=log_network, 45 | log_file_system=log_file_system, 46 | destructive=destructive, 47 | templates_dir=templates_dir, 48 | ) 49 | 50 | script_template = script_template.replace("###SYSCALL_FILTER###", syscalls_filter) 51 | 52 | # Creating a dscript file with the modified template 53 | if not os.path.exists(BASE_DIR_NAME): 54 | os.mkdir(BASE_DIR_NAME) 55 | 56 | module_file_name = os.path.join(BASE_DIR_NAME, f"bpftrace_sandbox_{module_name}.bt") 57 | with open(module_file_name, "w") as module_file: 58 | module_file.write(script_template) 59 | 60 | os.chmod(module_file_name, 0o644) 61 | 62 | # TODO: FIGURE OUT A WAY TO COMPILE WITHOUT EXECUTING - MSKING SURE THE GENERATION WORKED SYNTAX-WISE. 63 | return module_file_name 64 | 65 | 66 | def run_bpftrace_script_for_module( 67 | module_name: str, 68 | allow_shells: bool, 69 | allow_networking: bool, 70 | log_python_calls: bool, 71 | log_syscalls: bool, 72 | log_network: bool, 73 | log_file_system: bool, 74 | destructive: bool, 75 | use_sudo: bool = False, 76 | templates_dir: Path = TEMPLATES_DIR_NAME, 77 | dry_run: bool = True, 78 | ): 79 | module_file_path = create_bpftrace_script_for_module( 80 | module_name=module_name, 81 | allow_shells=allow_shells, 82 | allow_networking=allow_networking, 83 | log_python_calls=log_python_calls, 84 | log_syscalls=log_syscalls, 85 | log_network=log_network, 86 | log_file_system=log_file_system, 87 | destructive=destructive, 88 | templates_dir=templates_dir, 89 | ) 90 | # Checking the bpftrace exists: 91 | try: 92 | if not dry_run: 93 | subprocess.call(["bpftrace", "-h"]) 94 | except FileNotFoundError: 95 | raise EnvironmentError( 96 | "`bpftrace` is not installed. Please install it from https://github.com/iovisor/bpftrace/blob/master/INSTALL.md and make sure it is in PATH;" 97 | ) 98 | 99 | output_file = BASE_DIR_NAME / f"bpftrace_sandbox_{module_name}.log" 100 | current_pid = os.getpid() 101 | bpftrace_command = f'{"sudo " if use_sudo else ""} {module_file_path} --unsafe -q -p {current_pid} -o {output_file} &2>/dev/null' 102 | st = os.stat(module_file_path) 103 | os.chmod(module_file_path, st.st_mode | stat.S_IEXEC) 104 | print("(sandbox command): ", bpftrace_command) 105 | print() 106 | print("Waiting for bpftrace.... ") 107 | if not dry_run: 108 | os.system(bpftrace_command) 109 | return True 110 | 111 | 112 | def render_bpftrace_template( 113 | module_traced_name: str, 114 | allow_shells: bool, 115 | allow_networking: bool, 116 | log_python_calls: bool, 117 | log_syscalls: bool, 118 | log_network: bool, 119 | log_file_system: bool, 120 | destructive: bool, 121 | templates_dir: Path = TEMPLATES_DIR_NAME, 122 | default_template_filename: str = "default.template.bt", 123 | interpreter_path: str = PYTHON_EXECUTABLE, 124 | ): 125 | script_template = open( 126 | templates_dir / default_template_filename, 127 | "r", 128 | ).read() 129 | 130 | # Updating the right binary to be probed by bpftrace 131 | script_template = script_template.replace( 132 | "###INTERPRETER_PATH###", interpreter_path 133 | ) 134 | 135 | # PYTHON instrumentations 136 | code_syscall_entry = "" 137 | if log_python_calls is True: 138 | code_function_entry = open( 139 | templates_dir / "actions/log_python_module_entry.bt", 140 | "r", 141 | ).read() 142 | code_function_exit = open( 143 | templates_dir / "actions/log_python_module_exit.bt", 144 | "r", 145 | ).read() 146 | script_template = script_template.replace( 147 | "###FUNCTION_ENTRY###", code_function_entry 148 | ) 149 | script_template = script_template.replace( 150 | "###FUNCTION_EXIT###", code_function_exit 151 | ) 152 | else: 153 | script_template = script_template.replace("###FUNCTION_ENTRY###", "") 154 | script_template = script_template.replace("###FUNCTION_EXIT###", "") 155 | 156 | # SYSCALLS instrumentations 157 | if log_syscalls is True: 158 | _code = open( 159 | templates_dir / "actions/log_syscall.bt", 160 | "r", 161 | ).read() 162 | code_syscall_entry += f"{_code};\n" 163 | 164 | if log_file_system is True: 165 | filter_fs_code = open( 166 | templates_dir / "filters/file_system.bt", 167 | "r", 168 | ).read() 169 | code_syscall_entry += f"{filter_fs_code}\n{{" 170 | action_log_file_system = open( 171 | templates_dir / "actions/log_file_system.bt", 172 | "r", 173 | ).read() 174 | code_syscall_entry += f"{action_log_file_system}}}\n" 175 | 176 | if allow_networking is False or log_network is True: 177 | filter_networking_code = open( 178 | templates_dir / "filters/networking.bt", 179 | "r", 180 | ).read() 181 | code_syscall_entry += f"{filter_networking_code}{{\n" 182 | 183 | if log_network is True: 184 | action_log_network = open( 185 | templates_dir / "actions/log_network.bt", 186 | "r", 187 | ).read() 188 | code_syscall_entry += f"{action_log_network}\n" 189 | 190 | if allow_networking is False: 191 | action_kill = open( 192 | templates_dir / "actions/kill_process.bt", 193 | "r", 194 | ).read() 195 | code_syscall_entry += f"{action_kill}\n" 196 | code_syscall_entry += "}\n" 197 | 198 | if allow_shells is False: 199 | filter_processes_code = open( 200 | templates_dir / "filters/processes.bt", 201 | "r", 202 | ).read() 203 | code_syscall_entry += f"{filter_processes_code}{{\n" 204 | action_kill_on_processing = open( 205 | templates_dir / "actions/kill_on_processing.bt", 206 | "r", 207 | ).read() 208 | code_syscall_entry += f"{action_kill_on_processing}}}\n" 209 | 210 | script_template = script_template.replace("###SYSCALL_ENTRY###", code_syscall_entry) 211 | script_template = script_template.replace("###MODULE_NAME###", module_traced_name) 212 | 213 | # Updating the destructive behavior 214 | script_template = script_template.replace( 215 | "###DESTRUCTIVE###", "1" if destructive else "0" 216 | ) 217 | return script_template 218 | 219 | 220 | def render_bpftrace_probe_for_module( 221 | module_name: str, 222 | destructive: bool, 223 | templates_dir: Path = TEMPLATES_DIR_NAME, 224 | interpreter_path: str = PYTHON_EXECUTABLE, 225 | ) -> str: 226 | # Loading the probe allowlist template 227 | probe_template = open( 228 | templates_dir / "probes/module_syscalls_allowlist_template.bt", 229 | "r", 230 | ).read() 231 | 232 | # Adding a probe filter for the specified python module 233 | supervision_filter = open( 234 | templates_dir / "filters/is_current_module_under_supervision.bt", 235 | "r", 236 | ).read() 237 | 238 | # Adding the action according to the 'destructive' flag: kill if destructive, log otherwise 239 | supervision_action = "" 240 | if destructive is True: 241 | action_kill_process = open( 242 | templates_dir / "actions/kill_process.bt", 243 | "r", 244 | ).read() 245 | supervision_action = f"{{{action_kill_process}}}\n" 246 | else: 247 | action_log_syscall = open( 248 | templates_dir / "actions/log_syscall.bt", 249 | "r", 250 | ).read() 251 | supervision_action = f"{{{action_log_syscall}}}\n" 252 | 253 | # Updating the template with the filters, their actions, and module name 254 | probe_template = probe_template.replace( 255 | "###SUPERVISED_MODULES_FILTER###", supervision_filter 256 | ) 257 | probe_template = probe_template.replace( 258 | "###SUPERVISED_MODULES_ACTION###", supervision_action 259 | ) 260 | probe_template = probe_template.replace("###MODULE_NAME###", module_name) 261 | 262 | # Updating the destructive behavior 263 | probe_template = probe_template.replace( 264 | "###DESTRUCTIVE###", "1" if destructive else "0" 265 | ) 266 | 267 | probe_template = probe_template.replace("###INTERPRETER_PATH###", interpreter_path) 268 | return probe_template 269 | -------------------------------------------------------------------------------- /secimport/backends/dtrace_backend/dtrace_backend.py: -------------------------------------------------------------------------------- 1 | """ 2 | Import python modules with dtrace supervision. 3 | 4 | Copyright (c) 2022 Avi Lumelsky 5 | """ 6 | import importlib 7 | import importlib.machinery # important for importlib to work for our use case 8 | import os 9 | import stat 10 | import time 11 | from pathlib import Path 12 | from typing import List 13 | 14 | 15 | from secimport.backends.common.instrumentation_backend import InstrumentationBackend 16 | from secimport.backends.common.utils import ( 17 | BASE_DIR_NAME, 18 | SECIMPORT_ROOT, 19 | render_syscalls_filter, 20 | ) 21 | 22 | TEMPLATES_DIR_NAME = SECIMPORT_ROOT / "backends" / "dtrace_backend" 23 | DEFAULT_BACKEND = InstrumentationBackend.DTRACE 24 | PROFILES_DIR_NAME = SECIMPORT_ROOT / "profiles" 25 | 26 | 27 | def run_dtrace_script_for_module( 28 | module_name: str, 29 | allow_shells: bool, 30 | allow_networking: bool, 31 | use_sudo: bool, 32 | log_python_calls: bool, 33 | log_syscalls: bool, 34 | log_network: bool, 35 | log_file_system: bool, 36 | destructive: bool, 37 | templates_dir: Path = TEMPLATES_DIR_NAME, 38 | ): 39 | module_file_path = create_dtrace_script_for_module( 40 | module_name=module_name, 41 | allow_shells=allow_shells, 42 | allow_networking=allow_networking, 43 | log_python_calls=log_python_calls, 44 | log_syscalls=log_syscalls, 45 | log_network=log_network, 46 | log_file_system=log_file_system, 47 | destructive=destructive, 48 | templates_dir=templates_dir, 49 | ) 50 | output_file = BASE_DIR_NAME / f"dtrace_sandbox_{module_name}.log" 51 | current_pid = os.getpid() 52 | dtrace_command = f'{"sudo " if use_sudo else ""} dtrace -q -s {module_file_path} -p {current_pid} -o {output_file} &2>/dev/null' 53 | print("(running dtrace supervisor): ", dtrace_command) 54 | st = os.stat(module_file_path) 55 | os.chmod(module_file_path, st.st_mode | stat.S_IEXEC) 56 | os.system(dtrace_command) 57 | 58 | # TODO: wait for dtrace to start explicitly using an event/fd, not time based - although 2 seconds is more than enough. 59 | # 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). 60 | time.sleep(5) 61 | return True 62 | 63 | 64 | def create_dtrace_script_for_module( 65 | module_name: str, 66 | allow_shells: bool, 67 | allow_networking: bool, 68 | log_python_calls: bool, 69 | log_syscalls: bool, 70 | log_network: bool, 71 | log_file_system: bool, 72 | destructive: bool, 73 | templates_dir: Path = TEMPLATES_DIR_NAME, 74 | ) -> str: 75 | module = importlib.machinery.PathFinder().find_spec(module_name) 76 | if module is None: 77 | raise ModuleNotFoundError(module_name) 78 | module_traced_name = module.origin # e.g this.py 79 | 80 | script_template = render_dscript_template( 81 | module_traced_name=module_traced_name, 82 | allow_shells=allow_shells, 83 | allow_networking=allow_networking, 84 | log_python_calls=log_python_calls, 85 | log_syscalls=log_syscalls, 86 | log_network=log_network, 87 | log_file_system=log_file_system, 88 | destructive=destructive, 89 | templates_dir=templates_dir, 90 | ) 91 | 92 | # Creating a dscript file with the modified template 93 | if not os.path.exists(BASE_DIR_NAME): 94 | os.mkdir(BASE_DIR_NAME) 95 | 96 | module_file_name = os.path.join(BASE_DIR_NAME, f"dtrace_sandbox_{module_name}.d") 97 | with open(module_file_name, "w") as module_file: 98 | module_file.write(script_template) 99 | 100 | # Making sure the script compiles 101 | dtrace_compile_command = f"dtrace -s {module_file_name} -e" 102 | compile_exit_code = os.system(dtrace_compile_command) 103 | assert ( 104 | compile_exit_code == 0 105 | ), f"Failed to compile the dtrace script at {module_file_name}" 106 | print("Successfully compiled dtrace profile: ", module_file_name) 107 | return module_file_name 108 | 109 | 110 | def render_dscript_template( 111 | module_traced_name: str, 112 | allow_shells: bool, 113 | allow_networking: bool, 114 | log_python_calls: bool, 115 | log_syscalls: bool, 116 | log_network: bool, 117 | log_file_system: bool, 118 | destructive: bool, 119 | templates_dir: Path = TEMPLATES_DIR_NAME, 120 | default_template_filename: str = "default.template.d", 121 | ): 122 | script_template = open( 123 | templates_dir / default_template_filename, 124 | "r", 125 | ).read() 126 | 127 | if destructive is True: 128 | destructive = open(templates_dir / "headers/destructive.d", "r").read() 129 | script_template = script_template.replace("###DESTRUCTIVE###", destructive) 130 | else: 131 | script_template = script_template.replace("###DESTRUCTIVE###", "") 132 | 133 | # PYTHON instrumentations 134 | code_syscall_entry = "" 135 | if log_python_calls is True: 136 | code_function_entry = open( 137 | templates_dir / "actions/log_python_module_entry.d", 138 | "r", 139 | ).read() 140 | code_function_exit = open( 141 | templates_dir / "actions/log_python_module_exit.d", 142 | "r", 143 | ).read() 144 | script_template = script_template.replace( 145 | "###FUNCTION_ENTRY###", code_function_entry 146 | ) 147 | script_template = script_template.replace( 148 | "###FUNCTION_EXIT###", code_function_exit 149 | ) 150 | else: 151 | script_template = script_template.replace("###FUNCTION_ENTRY###", "") 152 | script_template = script_template.replace("###FUNCTION_EXIT###", "") 153 | 154 | # SYSCALLS instrumentations 155 | if log_syscalls is True: 156 | _code = open( 157 | templates_dir / "actions/log_syscall.d", 158 | "r", 159 | ).read() 160 | code_syscall_entry += f"{_code};\n" 161 | 162 | if log_file_system is True: 163 | filter_fs_code = open( 164 | templates_dir / "filters/file_system.d", 165 | "r", 166 | ).read() 167 | code_syscall_entry += f"{filter_fs_code}\n{{" 168 | action_log_file_system = open( 169 | templates_dir / "actions/log_file_system.d", 170 | "r", 171 | ).read() 172 | code_syscall_entry += f"{action_log_file_system}}}\n" 173 | 174 | if allow_networking is False or log_network is True: 175 | filter_networking_code = open( 176 | templates_dir / "filters/networking.d", 177 | "r", 178 | ).read() 179 | code_syscall_entry += f"{filter_networking_code}{{\n" 180 | 181 | if log_network is True: 182 | action_log_network = open( 183 | templates_dir / "actions/log_network.d", 184 | "r", 185 | ).read() 186 | code_syscall_entry += f"{action_log_network}\n" 187 | 188 | if allow_networking is False: 189 | action_kill = open( 190 | templates_dir / "actions/kill_process.d", 191 | "r", 192 | ).read() 193 | code_syscall_entry += f"{action_kill}\n" 194 | code_syscall_entry += "}\n" 195 | 196 | if allow_shells is False: 197 | filter_processes_code = open( 198 | templates_dir / "filters/processes.d", 199 | "r", 200 | ).read() 201 | code_syscall_entry += f"{filter_processes_code}{{\n" 202 | action_kill_on_processing = open( 203 | templates_dir / "actions/kill_on_processing.d", 204 | "r", 205 | ).read() 206 | code_syscall_entry += f"{action_kill_on_processing}}}\n" 207 | 208 | script_template = script_template.replace("###SYSCALL_ENTRY###", code_syscall_entry) 209 | script_template = script_template.replace("###MODULE_NAME###", module_traced_name) 210 | return script_template 211 | 212 | 213 | def render_dtrace_probe_for_module( 214 | module_name: str, 215 | destructive: bool, 216 | syscalls_allowlist: List[str], 217 | templates_dir: Path = TEMPLATES_DIR_NAME, 218 | ) -> str: 219 | # Loading the probe allowlist template 220 | probe_template = open( 221 | templates_dir / "probes/module_syscalls_allowlist_template.d", 222 | "r", 223 | ).read() 224 | 225 | # Adding a syscalls filter 226 | syscalls_filter = render_syscalls_filter( 227 | syscalls_list=syscalls_allowlist, 228 | allow=True, 229 | instrumentation_backend=InstrumentationBackend.DTRACE, 230 | ) 231 | probe_template = probe_template.replace("###SYSCALL_FILTER###", syscalls_filter) 232 | 233 | # Adding a probe filter for the specified python module 234 | supervision_filter = open( 235 | templates_dir / "filters/is_current_module_under_supervision.d", 236 | "r", 237 | ).read() 238 | 239 | # Adding the action according to the 'destructive' flag: kill if destructive, log otherwise 240 | supervision_action = "" 241 | if destructive is True: 242 | action_kill_process = open( 243 | templates_dir / "actions/kill_process.d", 244 | "r", 245 | ).read() 246 | supervision_action = f"{{{action_kill_process}}}\n" 247 | else: 248 | action_log_syscall = open( 249 | templates_dir / "actions/log_syscall.d", 250 | "r", 251 | ).read() 252 | supervision_action = f"{{{action_log_syscall}}}\n" 253 | 254 | # Updating the template 255 | probe_template = probe_template.replace( 256 | "###SUPERVISED_MODULES_FILTER###", supervision_filter 257 | ) 258 | probe_template = probe_template.replace( 259 | "###SUPERVISED_MODULES_ACTION###", supervision_action 260 | ) 261 | probe_template = probe_template.replace("###MODULE_NAME###", module_name) 262 | return probe_template 263 | -------------------------------------------------------------------------------- /examples/cli/dtrace/example.d: -------------------------------------------------------------------------------- 1 | #!/usr/sbin/dtrace -Zs 2 | 3 | #pragma D option destructive 4 | #pragma D option quiet 5 | #pragma D option switchrate=1 6 | #pragma D option cleanrate=50hz 7 | /* 8 | "dirty" variable drops per 9 | default -> ~20k 10 | 10 Hz -> ~15k 11 | 20 Hz -> ~10k 12 | 30 Hz -> ~2k 13 | 40 Hz -> ~1k 14 | 50 Hz (max) -> < 200 15 | */ 16 | 17 | #pragma D option dynvarsize=400000 18 | /* 19 | 10,000 @ 50 Hz -> ~25k+ 20 | 100,000 @ 50 Hz -> ~20k 21 | 200,000 @ 50 Hz -> ~3k 22 | 300,000 @ 50 Hz -> ~1k 23 | 400,000 @ 50 Hz -> 0 24 | 400,000 @ 25 Hz -> ~2k 25 | 500,000 @ 25 Hz -> ~1k 26 | */ 27 | 28 | /* A depth matrix for modules by name, maps each module (string) to the stack depth (int) at which it entered. */ 29 | int depth_matrix[string]; 30 | self int depth; 31 | string current_module_str; 32 | string previous_module_str; 33 | string latest_supervised_module; 34 | 35 | dtrace:::BEGIN 36 | { 37 | printf("%s %6s %10s %16s:%-4s %-8s -- %s\n", "C", "PID", "DELTA(us)", 38 | "FILE", "LINE", "TYPE", "FUNC"); 39 | } 40 | 41 | python*:::function-entry, 42 | python*:::function-return 43 | /self->last == 0/ 44 | { 45 | self->last = timestamp; 46 | } 47 | 48 | 49 | python*:::function-entry 50 | { 51 | this->delta = (timestamp - self->last) / 1000; 52 | self->depth++; 53 | 54 | /* Memoizing the previous module and current module */ 55 | _current_module_str = stringof(copyinstr(arg0)); 56 | previous_module_str = stringof(current_module_str); 57 | current_module_str = _current_module_str; 58 | 59 | /* if (strstr(_current_module_str, "###MODULE_NAME###") != 0){ 60 | printf("---! in module %s \r\n", "###MODULE_NAME###"); 61 | } */ 62 | 63 | /* Saving the stack depth for each module, assuming interpreter with GIL will run line-by-line. */ 64 | if (depth_matrix[current_module_str] > self->depth){ 65 | depth_matrix[current_module_str] = 0; 66 | } 67 | if (depth_matrix[current_module_str] == 0){ 68 | /* printf("---> (%d) %s\r\n", self->depth, _current_module_str); */ 69 | depth_matrix[current_module_str] = self->depth; 70 | } 71 | /* depth_matrix[current_module_str] = self->depth; */ 72 | } 73 | 74 | python*:::function-return 75 | { 76 | this->delta = (timestamp - self->last) / 1000; 77 | self->depth -= self->depth > 0 ? 1 : 0; 78 | /* printf("<--- (%d) %s\r\n", self->depth, _current_module_str); */ 79 | if (depth_matrix[current_module_str] > self->depth){ 80 | depth_matrix[current_module_str] = 0; 81 | } 82 | } 83 | 84 | syscall:::entry 85 | /pid == $target/ 86 | { 87 | @calls[basename(execname), "syscall", probefunc] = count(); 88 | /* printf("\n@%s", probefunc); */ 89 | /* requests START */ 90 | /* strlen(dirname(_current_module_str)) >= strlen(dirname("requests")) && */ 91 | /* strstr(dirname(_current_module_str), dirname("requests")) != 0 */ 92 | if ((depth_matrix["requests"] != 0) && (self->depth >= depth_matrix["requests"])){ 93 | if (latest_supervised_module == ""){ 94 | latest_supervised_module = "requests" 95 | } 96 | else{ 97 | if (depth_matrix["requests"] > depth_matrix[latest_supervised_module]){ 98 | latest_supervised_module = "requests" 99 | } 100 | } 101 | 102 | /* printf("GOT %s %s %d", _current_module_str, dirname("requests"), strstr(_current_module_str, dirname("requests")) */ 103 | if (depth_matrix["requests"] == depth_matrix[latest_supervised_module]) 104 | { 105 | if (probefunc != "fchmod" && probefunc != "getentropy" && probefunc != "getpgrp" && probefunc != "getrlimit" && probefunc != "shm_open" && probefunc != "sysctlbyname" && probefunc != "access" && probefunc != "munmap" && probefunc != "issetugid" && probefunc != "readlink" && probefunc != "write" && probefunc != "fcntl" && probefunc != "fstatfs64" && probefunc != "getdirentries64" && probefunc != "mprotect" && probefunc != "fcntl_nocancel" && probefunc != "madvise" && probefunc != "mmap" && probefunc != "read_nocancel" && probefunc != "select" && probefunc != "sigprocmask" && probefunc != "close_nocancel" && probefunc != "write_nocancel" && probefunc != "open_nocancel" && probefunc != "close" && probefunc != "open" && probefunc != "sigaction" && probefunc != "lseek" && probefunc != "fstat64" && probefunc != "read" && probefunc != "ioctl" && probefunc != "stat64"){ 106 | 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", probefunc, _current_module_str, "requests", depth_matrix["requests"], latest_supervised_module, depth_matrix[latest_supervised_module]); 107 | if(depth_matrix["requests"]!= 0 && self->depth >= depth_matrix["requests"] && depth_matrix["requests"] >= depth_matrix[latest_supervised_module]) 108 | { ustack(); 109 | printf("\t####################\r\n"); 110 | printf("\t\tKilling process; depth=%d sandboxed_depth=%d module=%s\r\n", self->depth, depth_matrix[_current_module_str], _current_module_str); 111 | printf("\t\tIf this behavior is unexpected, add the syscall '%s' to your list. The policy that blocked this syscall is logged above the stackstrace.\r\n", probefunc); 112 | stop(); 113 | printf("\t\tKILLING...\r\n"); 114 | system("\t\tkill -9 %d", pid); 115 | printf("\t\tKILLED.\r\n"); 116 | printf("\t####################\r\n"); 117 | exit(-1);} 118 | 119 | } 120 | } 121 | } 122 | /* requests END */ 123 | 124 | /* fastapi START */ 125 | /* strlen(dirname(_current_module_str)) >= strlen(dirname("fastapi")) && */ 126 | /* strstr(dirname(_current_module_str), dirname("fastapi")) != 0 */ 127 | if ((depth_matrix["fastapi"] != 0) && (self->depth >= depth_matrix["fastapi"])){ 128 | if (latest_supervised_module == ""){ 129 | latest_supervised_module = "fastapi" 130 | } 131 | else{ 132 | if (depth_matrix["fastapi"] > depth_matrix[latest_supervised_module]){ 133 | latest_supervised_module = "fastapi" 134 | } 135 | } 136 | 137 | /* printf("GOT %s %s %d", _current_module_str, dirname("fastapi"), strstr(_current_module_str, dirname("fastapi")) */ 138 | if (depth_matrix["fastapi"] == depth_matrix[latest_supervised_module]) 139 | { 140 | if (probefunc != "bind" && probefunc != "fchmod" && probefunc != "getentropy" && probefunc != "getpgrp" && probefunc != "getrlimit" && probefunc != "shm_open" && probefunc != "sysctlbyname" && probefunc != "access" && probefunc != "munmap" && probefunc != "issetugid" && probefunc != "readlink" && probefunc != "write" && probefunc != "fcntl" && probefunc != "fstatfs64" && probefunc != "getdirentries64" && probefunc != "mprotect" && probefunc != "fcntl_nocancel" && probefunc != "madvise" && probefunc != "mmap" && probefunc != "read_nocancel" && probefunc != "select" && probefunc != "sigprocmask" && probefunc != "close_nocancel" && probefunc != "write_nocancel" && probefunc != "open_nocancel" && probefunc != "close" && probefunc != "open" && probefunc != "sigaction" && probefunc != "lseek" && probefunc != "fstat64" && probefunc != "read" && probefunc != "ioctl" && probefunc != "stat64" && probefunc != "read" && probefunc != "pipe" && probefunc != "listen" && probefunc != "poll" && probefunc != "sigreturn" && probefunc != "getsockname" && probefunc != "kqueue" && probefunc != "kevent" && probefunc != "getpeername" && probefunc != "getpgrp" && probefunc != "listen" && probefunc != "pipe" && probefunc != "poll" && probefunc != "setsockopt" && probefunc != "shm_open" && probefunc != "socket" && probefunc != "socketpair" && probefunc != "sysctlbyname" && probefunc != "accept" && probefunc != "access" && probefunc != "getrlimit" && probefunc != "kqueue" && probefunc != "readlink" && probefunc != "recvfrom" && probefunc != "getsockname" && probefunc != "issetugid" && probefunc != "sendto" && probefunc != "write" && probefunc != "read_nocancel" && probefunc != "getentropy" && probefunc != "sigprocmask" && probefunc != "fstatfs64" && probefunc != "getdirentries64" && probefunc != "munmap" && probefunc != "madvise" && probefunc != "select" && probefunc != "write_nocancel" && probefunc != "sigaction" && probefunc != "fcntl_nocancel" && probefunc != "fcntl" && probefunc != "close_nocancel" && probefunc != "open_nocancel" && probefunc != "mprotect" && probefunc != "mmap" && probefunc != "kevent" && probefunc != "open" && probefunc != "close" && probefunc != "lseek" && probefunc != "ioctl" && probefunc != "read" && probefunc != "fstat64" && probefunc != "stat64"){ 141 | 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", probefunc, _current_module_str, "fastapi", depth_matrix["fastapi"], latest_supervised_module, depth_matrix[latest_supervised_module]); 142 | if(depth_matrix["fastapi"]!= 0 && self->depth >= depth_matrix["fastapi"] && depth_matrix["fastapi"] >= depth_matrix[latest_supervised_module]) 143 | { ustack(); 144 | printf("\t####################\r\n"); 145 | printf("\t\tKilling process; depth=%d sandboxed_depth=%d module=%s\r\n", self->depth, depth_matrix[_current_module_str], _current_module_str); 146 | printf("\t\tIf this behavior is unexpected, add the syscall '%s' to your list. The policy that blocked this syscall is logged above the stackstrace.\r\n", probefunc); 147 | stop(); 148 | printf("\t\tKILLING...\r\n"); 149 | system("\t\tkill -9 %d", pid); 150 | printf("\t\tKILLED.\r\n"); 151 | printf("\t####################\r\n"); 152 | exit(-1);} 153 | 154 | } 155 | } 156 | } 157 | /* fastapi END */ 158 | 159 | /* uvicorn START */ 160 | /* strlen(dirname(_current_module_str)) >= strlen(dirname("uvicorn")) && */ 161 | /* strstr(dirname(_current_module_str), dirname("uvicorn")) != 0 */ 162 | if ((depth_matrix["uvicorn"] != 0) && (self->depth >= depth_matrix["uvicorn"])){ 163 | if (latest_supervised_module == ""){ 164 | latest_supervised_module = "uvicorn" 165 | } 166 | else{ 167 | if (depth_matrix["uvicorn"] > depth_matrix[latest_supervised_module]){ 168 | latest_supervised_module = "uvicorn" 169 | } 170 | } 171 | 172 | /* printf("GOT %s %s %d", _current_module_str, dirname("uvicorn"), strstr(_current_module_str, dirname("uvicorn")) */ 173 | if (depth_matrix["uvicorn"] == depth_matrix[latest_supervised_module]) 174 | { 175 | if (probefunc != "getpeername" && probefunc != "getpgrp" && probefunc != "listen" && probefunc != "pipe" && probefunc != "poll" && probefunc != "setsockopt" && probefunc != "shm_open" && probefunc != "socket" && probefunc != "socketpair" && probefunc != "sysctlbyname" && probefunc != "accept" && probefunc != "access" && probefunc != "getrlimit" && probefunc != "kqueue" && probefunc != "readlink" && probefunc != "recvfrom" && probefunc != "getsockname" && probefunc != "issetugid" && probefunc != "sendto" && probefunc != "write" && probefunc != "read_nocancel" && probefunc != "getentropy" && probefunc != "sigprocmask" && probefunc != "fstatfs64" && probefunc != "getdirentries64" && probefunc != "munmap" && probefunc != "madvise" && probefunc != "select" && probefunc != "write_nocancel" && probefunc != "sigaction" && probefunc != "fcntl_nocancel" && probefunc != "fcntl" && probefunc != "close_nocancel" && probefunc != "open_nocancel" && probefunc != "mprotect" && probefunc != "mmap" && probefunc != "kevent" && probefunc != "open" && probefunc != "close" && probefunc != "lseek" && probefunc != "ioctl" && probefunc != "read" && probefunc != "fstat64" && probefunc != "stat64"){ 176 | 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", probefunc, _current_module_str, "uvicorn", depth_matrix["uvicorn"], latest_supervised_module, depth_matrix[latest_supervised_module]); 177 | if(depth_matrix["uvicorn"]!= 0 && self->depth >= depth_matrix["uvicorn"] && depth_matrix["uvicorn"] >= depth_matrix[latest_supervised_module]) 178 | { ustack(); 179 | printf("\t####################\r\n"); 180 | printf("\t\tKilling process; depth=%d sandboxed_depth=%d module=%s\r\n", self->depth, depth_matrix[_current_module_str], _current_module_str); 181 | printf("\t\tIf this behavior is unexpected, add the syscall '%s' to your list. The policy that blocked this syscall is logged above the stackstrace.\r\n", probefunc); 182 | stop(); 183 | printf("\t\tKILLING...\r\n"); 184 | system("\t\tkill -9 %d", pid); 185 | printf("\t\tKILLED.\r\n"); 186 | printf("\t####################\r\n"); 187 | exit(-1);} 188 | 189 | } 190 | } 191 | } 192 | /* uvicorn END */ 193 | } 194 | 195 | /* 196 | syscall:::return 197 | /pid == $target/ 198 | { 199 | printf("\n"); 200 | } */ 201 | 202 | 203 | dtrace:::END 204 | { 205 | printf("System Calls (%d)\r\n\r\n", $target); 206 | printf(" %-32s %-10s %-22s %8s\r\n", "FILE", "TYPE", "NAME", "COUNT"); 207 | printa(" %-32s %-10s %-22s %@8d\r\n", @calls); 208 | /* print(depth_matrix); */ 209 | } 210 | -------------------------------------------------------------------------------- /secimport/backends/common/system_calls.py: -------------------------------------------------------------------------------- 1 | SYSCALLS_NUMBERS = { 2 | 0: "read", 3 | 1: "write", 4 | 2: "open", 5 | 3: "close", 6 | 4: "stat", 7 | 5: "fstat", 8 | 6: "lstat", 9 | 7: "poll", 10 | 8: "lseek", 11 | 9: "mmap", 12 | 10: "mprotect", 13 | 11: "munmap", 14 | 12: "brk", 15 | 13: "rt_sigaction", 16 | 14: "rt_sigprocmask", 17 | 15: "rt_sigreturn", 18 | 16: "ioctl", 19 | 17: "pread", 20 | 18: "pwrite", 21 | 19: "readv", 22 | 20: "writev", 23 | 21: "access", 24 | 22: "pipe", 25 | 23: "select", 26 | 24: "sched_yield", 27 | 25: "mremap", 28 | 26: "msync", 29 | 27: "mincore", 30 | 28: "madvise", 31 | 29: "shmget", 32 | 30: "shmat", 33 | 31: "shmctl", 34 | 32: "dup", 35 | 33: "dup2", 36 | 34: "pause", 37 | 35: "nanosleep", 38 | 36: "getitimer", 39 | 37: "alarm", 40 | 38: "setitimer", 41 | 39: "getpid", 42 | 40: "sendfile", 43 | 41: "socket", 44 | 42: "connect", 45 | 43: "accept", 46 | 44: "sendto", 47 | 45: "recvfrom", 48 | 46: "sendmsg", 49 | 47: "recvmsg", 50 | 48: "shutdown", 51 | 49: "bind", 52 | 50: "listen", 53 | 51: "getsockname", 54 | 52: "getpeername", 55 | 53: "socketpair", 56 | 54: "setsockopt", 57 | 55: "getsockopt", 58 | 56: "clone", 59 | 57: "fork", 60 | 58: "vfork", 61 | 59: "execve", 62 | 60: "exit", 63 | 61: "wait4", 64 | 62: "kill", 65 | 63: "uname", 66 | 64: "semget", 67 | 65: "semop", 68 | 66: "semctl", 69 | 67: "shmdt", 70 | 68: "msgget", 71 | 69: "msgsnd", 72 | 70: "msgrcv", 73 | 71: "msgctl", 74 | 72: "fcntl", 75 | 73: "flock", 76 | 74: "fsync", 77 | 75: "fdatasync", 78 | 76: "truncate", 79 | 77: "ftruncate", 80 | 78: "getdents", 81 | 79: "getcwd", 82 | 80: "chdir", 83 | 81: "fchdir", 84 | 82: "rename", 85 | 83: "mkdir", 86 | 84: "rmdir", 87 | 85: "creat", 88 | 86: "link", 89 | 87: "unlink", 90 | 88: "symlink", 91 | 89: "readlink", 92 | 90: "chmod", 93 | 91: "fchmod", 94 | 92: "chown", 95 | 93: "fchown", 96 | 94: "lchown", 97 | 95: "umask", 98 | 96: "gettimeofday", 99 | 97: "getrlimit", 100 | 98: "getrusage", 101 | 99: "sysinfo", 102 | 100: "times", 103 | 101: "ptrace", 104 | 102: "getuid", 105 | 103: "syslog", 106 | 104: "getgid", 107 | 105: "setuid", 108 | 106: "setgid", 109 | 107: "geteuid", 110 | 108: "getegid", 111 | 109: "setpgid", 112 | 110: "getppid", 113 | 111: "getpgrp", 114 | 112: "setsid", 115 | 113: "setreuid", 116 | 114: "setregid", 117 | 115: "getgroups", 118 | 116: "setgroups", 119 | 117: "setresuid", 120 | 118: "getresuid", 121 | 119: "setresgid", 122 | 120: "getresgid", 123 | 121: "getpgid", 124 | 122: "setfsuid", 125 | 123: "setfsgid", 126 | 124: "getsid", 127 | 125: "capget", 128 | 126: "capset", 129 | 127: "rt_sigpending", 130 | 128: "rt_sigtimedwait", 131 | 129: "rt_sigqueueinfo", 132 | 130: "rt_sigsuspend", 133 | 131: "sigaltstack", 134 | 132: "utime", 135 | 133: "mknod", 136 | 134: "uselib", 137 | 135: "personality", 138 | 136: "ustat", 139 | 137: "statfs", 140 | 138: "fstatfs", 141 | 139: "sysfs", 142 | 140: "getpriority", 143 | 141: "setpriority", 144 | 142: "sched_setparam", 145 | 143: "sched_getparam", 146 | 144: "sched_setscheduler", 147 | 145: "sched_getscheduler", 148 | 146: "sched_get_priority_max", 149 | 147: "sched_get_priority_min", 150 | 148: "sched_rr_get_interval", 151 | 149: "mlock", 152 | 150: "munlock", 153 | 151: "mlockall", 154 | 152: "munlockall", 155 | 153: "vhangup", 156 | 154: "modify_ldt", 157 | 155: "pivot_root", 158 | 156: "_sysctl", 159 | 157: "prctl", 160 | 158: "arch_prctl", 161 | 159: "adjtimex", 162 | 160: "setrlimit", 163 | 161: "chroot", 164 | 162: "sync", 165 | 163: "acct", 166 | 164: "settimeofday", 167 | 165: "mount", 168 | 166: "umount2", 169 | 167: "swapon", 170 | 168: "swapoff", 171 | 169: "reboot", 172 | 170: "sethostname", 173 | 171: "setdomainname", 174 | 172: "iopl", 175 | 173: "ioperm", 176 | 174: "create_module", 177 | 175: "init_module", 178 | 176: "delete_module", 179 | 177: "get_kernel_syms", 180 | 178: "query_module", 181 | 179: "quotactl", 182 | 180: "nfsservctl", 183 | 181: "getpmsg", 184 | 182: "putpmsg", 185 | 183: "afs_syscall", 186 | 184: "tuxcall", 187 | 185: "security", 188 | 186: "gettid", 189 | 187: "readahead", 190 | 188: "setxattr", 191 | 189: "lsetxattr", 192 | 190: "fsetxattr", 193 | 191: "getxattr", 194 | 192: "lgetxattr", 195 | 193: "fgetxattr", 196 | 194: "listxattr", 197 | 195: "llistxattr", 198 | 196: "flistxattr", 199 | 197: "removexattr", 200 | 198: "lremovexattr", 201 | 199: "fremovexattr", 202 | 200: "tkill", 203 | 201: "time", 204 | 202: "futex", 205 | 203: "sched_setaffinity", 206 | 204: "sched_getaffinity", 207 | 205: "set_thread_area", 208 | 206: "io_setup", 209 | 207: "io_destroy", 210 | 208: "io_getevents", 211 | 209: "io_submit", 212 | 210: "io_cancel", 213 | 211: "get_thread_area", 214 | 212: "lookup_dcookie", 215 | 213: "epoll_create", 216 | 214: "epoll_ctl_old", 217 | 215: "epoll_wait_old", 218 | 216: "remap_file_pages", 219 | 217: "getdents64", 220 | 218: "set_tid_address", 221 | 219: "restart_syscall", 222 | 220: "semtimedop", 223 | 221: "fadvise64", 224 | 222: "timer_create", 225 | 223: "timer_settime", 226 | 224: "timer_gettime", 227 | 225: "timer_getoverrun", 228 | 226: "timer_delete", 229 | 227: "clock_settime", 230 | 228: "clock_gettime", 231 | 229: "clock_getres", 232 | 230: "clock_nanosleep", 233 | 231: "exit_group", 234 | 232: "epoll_wait", 235 | 233: "epoll_ctl", 236 | 234: "tgkill", 237 | 235: "utimes", 238 | 236: "vserver", 239 | 237: "mbind", 240 | 238: "set_mempolicy", 241 | 239: "get_mempolicy", 242 | 240: "mq_open", 243 | 241: "mq_unlink", 244 | 242: "mq_timedsend", 245 | 243: "mq_timedreceive", 246 | 244: "mq_notify", 247 | 245: "mq_getsetattr", 248 | 246: "kexec_load", 249 | 247: "waitid", 250 | 248: "add_key", 251 | 249: "request_key", 252 | 250: "keyctl", 253 | 251: "ioprio_set", 254 | 252: "ioprio_get", 255 | 253: "inotify_init", 256 | 254: "inotify_add_watch", 257 | 255: "inotify_rm_watch", 258 | 256: "migrate_pages", 259 | 257: "openat", 260 | 258: "mkdirat", 261 | 259: "mknodat", 262 | 260: "fchownat", 263 | 261: "futimesat", 264 | 262: "newfstatat", 265 | 263: "unlinkat", 266 | 264: "renameat", 267 | 265: "linkat", 268 | 266: "symlinkat", 269 | 267: "readlinkat", 270 | 268: "fchmodat", 271 | 269: "faccessat", 272 | 270: "pselect6", 273 | 271: "ppoll", 274 | 272: "unshare", 275 | 273: "set_robust_list", 276 | 274: "get_robust_list", 277 | 275: "splice", 278 | 276: "tee", 279 | 277: "sync_file_range", 280 | 278: "vmsplice", 281 | 279: "move_pages", 282 | 280: "utimensat", 283 | 281: "epoll_pwait", 284 | 282: "signalfd", 285 | 283: "timerfd", 286 | 284: "eventfd", 287 | 285: "fallocate", 288 | 286: "timerfd_settime", 289 | 287: "timerfd_gettime", 290 | 288: "accept4", 291 | 289: "signalfd4", 292 | 290: "eventfd2", 293 | 291: "epoll_create1", 294 | 292: "dup3", 295 | 293: "pipe2", 296 | 294: "inotify_init1", 297 | 295: "preadv", 298 | 296: "pwritev", 299 | 297: "rt_tgsigqueueinfo", 300 | 298: "perf_event_open", 301 | 299: "recvmmsg", 302 | 300: "fanotify_init", 303 | 301: "fanotify_mark", 304 | 302: "prlimit64", 305 | 303: "name_to_handle_at", 306 | 304: "open_by_handle_at", 307 | 305: "clock_adjtime", 308 | 306: "syncfs", 309 | 307: "sendmmsg", 310 | 308: "setns", 311 | 309: "getcpu", 312 | 310: "process_vm_readv", 313 | 311: "process_vm_writev", 314 | 312: "kcmp", 315 | 313: "finit_module", 316 | 314: "sched_setattr", 317 | 315: "sched_getattr", 318 | 316: "renameat2", 319 | 317: "seccomp", 320 | 318: "getrandom", 321 | 319: "memfd_create", 322 | 320: "kexec_file_load", 323 | 321: "bpf", 324 | 322: "execveat", 325 | 323: "userfaultfd", 326 | 324: "membarrier", 327 | 325: "mlock2", 328 | 326: "copy_file_range", 329 | 327: "preadv2", 330 | 328: "pwritev2", 331 | 329: "pkey_mprotect", 332 | 330: "pkey_alloc", 333 | 331: "pkey_free", 334 | 332: "statx", 335 | 333: "io_pgetevents", 336 | 334: "rseq", 337 | } 338 | 339 | SYSCALLS_NAMES = {str(v): k for k, v in SYSCALLS_NUMBERS.items()} 340 | 341 | DEFAULT_ALLOWED_SYSCALLS = [ 342 | "vfork", 343 | "clone", 344 | "access", 345 | "chdir", 346 | "creat", 347 | "dup", 348 | "dup2", 349 | "execve", 350 | "faccessat", 351 | "fcntl", 352 | "fdatasync", 353 | "fork", 354 | "fstat", 355 | "fsync", 356 | "getegid", 357 | "geteuid", 358 | "getgid", 359 | "getgroups", 360 | "getpid", 361 | "getppid", 362 | "getrlimit", 363 | "getsockname", 364 | "getsid", 365 | "getuid", 366 | "ioctl", 367 | "link", 368 | "lseek", 369 | "lstat", 370 | "mkdir", 371 | "mknod", 372 | "open", 373 | "openat", 374 | "pipe", 375 | "poll", 376 | "read", 377 | "readlink", 378 | "readv", 379 | "recvfrom", 380 | "recvmsg", 381 | "rename", 382 | "rmdir", 383 | "select", 384 | "sendmsg", 385 | "sendto", 386 | "setgid", 387 | "setgroups", 388 | "setpgid", 389 | "setpriority", 390 | "setregid", 391 | "setreuid", 392 | "setrlimit", 393 | "setsid", 394 | "setsockopt", 395 | "stat", 396 | "symlink", 397 | "truncate", 398 | "umask", 399 | "utime", 400 | "utimes", 401 | "write", 402 | "writev", 403 | ] 404 | 405 | NETWORKING_SYSCALLS = [ 406 | "socket", 407 | "connect", 408 | "bind", 409 | "listen", 410 | "accept", 411 | "send", 412 | "recv", 413 | "sendto", 414 | "recvfrom", 415 | "shutdown", 416 | "setsockopt", 417 | "getsockopt", 418 | "getpeername", 419 | "getsockname", 420 | "gethostbyname", 421 | "gethostbyaddr", 422 | "getservbyname", 423 | "getservbyport", 424 | "getifaddrs", 425 | "ioctl", 426 | "rt_names_to_index", 427 | "rt_index_to_name", 428 | "rt_newlink", 429 | "rt_dellink", 430 | "rt_changelink", 431 | "rt_getlink", 432 | "rt_getroute", 433 | "rt_newroute", 434 | "rt_delroute", 435 | "rt_changeroute", 436 | "rt_priority", 437 | "rt_classid", 438 | "rt_mark", 439 | "rt_table", 440 | "rt_protocol", 441 | "rt_scope", 442 | "rt_flags", 443 | "rt_ifindex", 444 | "rt_metric", 445 | "rt_gateway", 446 | "rt_src", 447 | "rt_dst", 448 | "rt_genmask", 449 | "rt_flags", 450 | "rt_refcnt", 451 | "rt_fib", 452 | "rt_nexthop", 453 | "rt_ifa", 454 | "rt_ifa_index", 455 | "rt_ifa_flags", 456 | "rt_ifa_scope", 457 | "rt_ifa_mntr", 458 | "rt_ifa_brd", 459 | "rt_ifa_dst", 460 | "rt_ifa_netmask", 461 | "rt_ifa_net", 462 | "rt_ifa_flags", 463 | "rt_ifa_ifindex", 464 | "rt_ifa_hwaddr", 465 | "rt_ifa_rtnl", 466 | "rt_ifa_type", 467 | "rt_ifa_metric", 468 | "rt_ifa_refcnt", 469 | "rt_ifa_mtu", 470 | "rt_ifa_lladdr", 471 | "rt_ifa_addr", 472 | "rt_ifa_brd", 473 | "rt_ifa_netmask", 474 | "rt_ifa_net", 475 | "rt_ifa_flags", 476 | "rt_ifa_ifindex", 477 | "rt_ifa_hwaddr", 478 | "rt_ifa_rtnl", 479 | "rt_ifa_type", 480 | "rt_ifa_metric", 481 | "rt_ifa_refcnt", 482 | "rt_ifa_mtu", 483 | "rt_ifa_lladdr", 484 | ] 485 | 486 | FILESYSTEM_SYSCALLS = [ 487 | "access", 488 | "chdir", 489 | "chmod", 490 | "chown", 491 | "cksum", 492 | "creat", 493 | "ctermid", 494 | "dup", 495 | "dup2", 496 | "execve", 497 | "faccessat", 498 | "fchmod", 499 | "fchown", 500 | "fcntl", 501 | "fdatasync", 502 | "fdopendir", 503 | "fdopen", 504 | "fexecve", 505 | "fflush", 506 | "fgetpos", 507 | "fgets", 508 | "fgetwc", 509 | "fileno", 510 | "flock", 511 | "fmemopen", 512 | "fopen", 513 | "fopencookie", 514 | "fork", 515 | "fputwc", 516 | "fstat", 517 | "fsync", 518 | "ftruncate", 519 | "getegid", 520 | "geteuid", 521 | "getgid", 522 | "getgroups", 523 | "getpid", 524 | "getppid", 525 | "getrlimit", 526 | "getsockname", 527 | "getsid", 528 | "getuid", 529 | "ioctl", 530 | "link", 531 | "lseek", 532 | "lstat", 533 | "mkdir", 534 | "mknod", 535 | "open", 536 | "openat", 537 | "pipe", 538 | "poll", 539 | # "posix_fallocate", 540 | # "posix_fadvise", 541 | # "posix_fadvise64", 542 | "read", 543 | "readlink", 544 | # "readdir", 545 | "readv", 546 | "recv", 547 | "recvfrom", 548 | "recvmsg", 549 | "rename", 550 | "rewind", 551 | "rmdir", 552 | "seekdir", 553 | "select", 554 | "send", 555 | "sendmsg", 556 | "sendto", 557 | "setegid", 558 | "seteuid", 559 | "setgid", 560 | "setgroups", 561 | "setpgid", 562 | "setpriority", 563 | "setregid", 564 | "setreuid", 565 | "setrlimit", 566 | "setsid", 567 | "setsockopt", 568 | "stat", 569 | "symlink", 570 | "truncate", 571 | "umask", 572 | "umount", 573 | "utime", 574 | "utimes", 575 | "write", 576 | "writev", 577 | ] 578 | --------------------------------------------------------------------------------