├── ebph ├── libebph │ ├── bin │ │ └── .gitignore │ ├── libebph.c │ ├── include │ │ └── folly │ │ │ └── tracing │ │ │ ├── StaticTracepoint.h │ │ │ └── StaticTracepoint-ELF.h │ ├── __init__.py │ └── libebph.h ├── bpf │ ├── defs.h │ ├── lsm.h │ └── bpf_program.h ├── version.py ├── commands │ ├── ebph_logs.py │ ├── ebph_ps.py │ └── ebph_admin.py ├── defs.py ├── daemon_mixin.py ├── utils.py ├── ebphd.py ├── logger.py ├── structs.py ├── api.py └── bpf_program.py ├── tests ├── driver │ ├── .gitignore │ ├── Makefile │ ├── sample_workload.sh │ ├── hello.c │ └── malicious.c ├── Makefile ├── test_api │ ├── conftest.py │ └── test_api.py ├── test_bpf_program │ ├── test_settings.py │ ├── test_profile_creation.py │ ├── test_saving_loading.py │ └── test_normal_mode.py └── conftest.py ├── MANIFEST.in ├── systemd ├── ebphd.service └── create_service.sh ├── .ccls ├── Pipfile ├── Makefile ├── bin ├── ebphd └── ebph ├── requirements.txt ├── setup.py ├── README.md ├── .gitignore └── LICENSE /ebph/libebph/bin/.gitignore: -------------------------------------------------------------------------------- 1 | libebph.so 2 | -------------------------------------------------------------------------------- /tests/driver/.gitignore: -------------------------------------------------------------------------------- 1 | hello 2 | malicious 3 | -------------------------------------------------------------------------------- /ebph/bpf/defs.h: -------------------------------------------------------------------------------- 1 | #ifndef DEFS_H 2 | #define DEFS_H 3 | 4 | #endif /* ifndef DEFS_H */ 5 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include ebph/ *.c 2 | recursive-include ebph/ *.h 3 | recursive-include ebph/ *.py 4 | -------------------------------------------------------------------------------- /tests/Makefile: -------------------------------------------------------------------------------- 1 | FIXED_PYTEST_ARGS = -v -W ignore::DeprecationWarning 2 | 3 | .PHONY: test 4 | test: | driver 5 | pytest $(FIXED_PYTEST_ARGS) $(ARGS) 6 | 7 | .PHONY: driver 8 | driver: 9 | $(MAKE) -C driver 10 | -------------------------------------------------------------------------------- /systemd/ebphd.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=ebpH daemon service 3 | 4 | [Service] 5 | Type=simple 6 | Restart=always 7 | RestartSec=1 8 | User=root 9 | ExecStart=/bin/ebphd start 10 | ExecStop=/bin/ebphd stop 11 | ExecReload=/bin/ebphd restart 12 | 13 | [Install] 14 | WantedBy=multi-user.target 15 | -------------------------------------------------------------------------------- /systemd/create_service.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | SYSTEMD_PATH=/etc/systemd/system 4 | SERVICE_DIR="$(dirname "${BASH_SOURCE:=$0}")" 5 | SERVICE=ebphd.service 6 | 7 | cp -f "$SERVICE_DIR/$SERVICE" "$SYSTEMD_PATH/$SERVICE" 8 | chown root:root "$SYSTEMD_PATH/$SERVICE" 9 | 10 | systemctl enable "$SERVICE" 11 | -------------------------------------------------------------------------------- /tests/driver/Makefile: -------------------------------------------------------------------------------- 1 | CC = gcc 2 | LDLAGS = 3 | CFLAGS = -O0 4 | 5 | EXES = hello malicious 6 | 7 | .PHONY: all 8 | all: $(EXES) 9 | 10 | .PHONY: clean 11 | clean: 12 | rm -f $(EXES) *.o 13 | 14 | hello: hello.c 15 | $(CC) $(CFLAGS) $(LDFLAGS) -o hello hello.c 16 | 17 | malicious: malicious.c 18 | $(CC) $(CFLAGS) $(LDFLAGS) -o malicious malicious.c 19 | -------------------------------------------------------------------------------- /.ccls: -------------------------------------------------------------------------------- 1 | clang 2 | -DEBPH_SEQLEN=9 3 | -DEBPH_LOCALITY_WIN=128 4 | -DEBPH_MAX_PROFILES=10240 5 | -DEBPH_MAX_PROCESSES=10240 6 | -DEBPH_SEQLEN=9 7 | -DEBPH_SEQSTACK_FRAMES=2 8 | -DEBPH_LOCALITY_WIN=128 9 | -DEBPH_EMPTY=9999 10 | -DEBPH_TOLERANCE_LOW=3 11 | -DEBPH_TOLERANCE_MEDIUM=10 12 | -DEBPH_TOLERANCE_HIGH=30 13 | -DEBPH_TOLERANCE_ALWAYS=0 14 | -DEBPH_BOOT_EPOCH=1 15 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | jedi = "*" 8 | pylint = "*" 9 | pytest = "*" 10 | doq = "*" 11 | 12 | [packages] 13 | python-daemon = "~=2.2.4" 14 | ebph = {editable = true, path = "."} 15 | fastapi = "*" 16 | uvicorn = "*" 17 | requests = "*" 18 | ratelimit = "*" 19 | requests-unixsocket = "*" 20 | proc = "*" 21 | colorama = "*" 22 | 23 | [requires] 24 | python_version = "3.8" 25 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all 2 | all: 3 | @echo "Run 'make install' to install." 4 | @echo "Run 'make systemd' to install and enable the systemd service." 5 | 6 | .PHONY: dev 7 | dev: 8 | sudo su -c "pip3 install -e . -r requirements.txt" 9 | 10 | .PHONY: install 11 | install: 12 | sudo su -c "pip3 install . --compile -r requirements.txt" 13 | 14 | .PHONY: systemd 15 | systemd: 16 | sudo su -c "/bin/sh systemd/create_service.sh" 17 | 18 | test: dev 19 | sudo su -c "$(MAKE) -C tests" 20 | -------------------------------------------------------------------------------- /ebph/libebph/libebph.c: -------------------------------------------------------------------------------- 1 | #include "libebph.h" 2 | 3 | COMMAND2(set_setting, int, key, u_int64_t, value) 4 | COMMAND1(normalize_profile, u_int64_t, profile_key) 5 | COMMAND1(normalize_process, u_int32_t, pid) 6 | COMMAND1(sensitize_profile, u_int64_t, profile_key) 7 | COMMAND1(sensitize_process, u_int32_t, pid) 8 | COMMAND1(tolerize_profile, u_int64_t, profile_key) 9 | COMMAND1(tolerize_process, u_int32_t, pid) 10 | COMMAND4(bootstrap_process, u_int64_t, profile_key, u_int32_t, pid, u_int32_t, 11 | tgid, char *, pathname) 12 | -------------------------------------------------------------------------------- /tests/driver/sample_workload.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | # ebpH (Extended BPF Process Homeostasis) A host-based IDS written in eBPF. 4 | # Copyright (C) 2019-2020 William Findlay 5 | # 6 | # This program is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program. If not, see . 18 | # 19 | # A sample workload for unit tests. 20 | # 21 | # 2020-Jul-16 William Findlay Created this. 22 | 23 | ls | wc -l 24 | ps aux 25 | ls > /tmp/ls.log 26 | cat /tmp/ls.log 27 | /bin/echo foo > /tmp/foo 28 | grep foo /tmp/foo 29 | -------------------------------------------------------------------------------- /bin/ebphd: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | 3 | """ 4 | ebpH (Extended BPF Process Homeostasis) A host-based IDS written in eBPF. 5 | ebpH Copyright (C) 2019-2020 William Findlay 6 | pH Copyright (C) 1999-2003 Anil Somayaji and (C) 2008 Mario Van Velzen 7 | 8 | This program is free software: you can redistribute it and/or modify 9 | it under the terms of the GNU General Public License as published by 10 | the Free Software Foundation, either version 3 of the License, or 11 | (at your option) any later version. 12 | 13 | This program is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | 18 | You should have received a copy of the GNU General Public License 19 | along with this program. If not, see . 20 | 21 | Main entrypoint into ebphd. 22 | 23 | 2020-Jul-13 William Findlay Created this. 24 | """ 25 | 26 | from ebph.ebphd import main 27 | 28 | if __name__ == '__main__': 29 | main() 30 | -------------------------------------------------------------------------------- /ebph/version.py: -------------------------------------------------------------------------------- 1 | """ 2 | ebpH (Extended BPF Process Homeostasis) A host-based IDS written in eBPF. 3 | ebpH Copyright (C) 2019-2020 William Findlay 4 | pH Copyright (C) 1999-2003 Anil Somayaji and (C) 2008 Mario Van Velzen 5 | 6 | This program is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with this program. If not, see . 18 | 19 | 2020-Jul-13 William Findlay Created this. 20 | """ 21 | 22 | import pkg_resources 23 | 24 | try: 25 | __version__ = pkg_resources.require('ebpH')[0].version 26 | except Exception: 27 | __version__ = 'unknown' 28 | -------------------------------------------------------------------------------- /tests/driver/hello.c: -------------------------------------------------------------------------------- 1 | /* ebpH (Extended BPF Process Homeostasis) A host-based IDS written in eBPF. 2 | * ebpH Copyright (C) 2019-2020 William Findlay 3 | * pH Copyright (C) 1999-2003 Anil Somayaji and (C) 2008 Mario Van Velzen 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | * 18 | * A simple hello world program. 19 | * 20 | * 2020-Jul-16 William Findlay Created this. 21 | */ 22 | 23 | #include 24 | 25 | int main(int argc, char **argv) { 26 | write(1, "Hello, world!\n", 14); 27 | 28 | if (argc > 1) { 29 | write(1, "Hello again, world!\n", 20); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tests/test_api/conftest.py: -------------------------------------------------------------------------------- 1 | """ 2 | ebpH (Extended BPF Process Homeostasis) A host-based IDS written in eBPF. 3 | ebpH Copyright (C) 2019-2020 William Findlay 4 | pH Copyright (C) 1999-2003 Anil Somayaji and (C) 2008 Mario Van Velzen 5 | 6 | This program is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with this program. If not, see . 18 | 19 | Provide a fixture for an ebpH API client. 20 | 21 | 2020-Jul-16 William Findlay Created this. 22 | """ 23 | 24 | from fastapi.testclient import TestClient 25 | import pytest 26 | 27 | from ebph.api import app, API 28 | 29 | @pytest.fixture(scope='function') 30 | def client(bpf_program): 31 | client = TestClient(app) 32 | API.connect_bpf_program(bpf_program) 33 | 34 | yield client 35 | -------------------------------------------------------------------------------- /ebph/libebph/include/folly/tracing/StaticTracepoint.h: -------------------------------------------------------------------------------- 1 | /* 2 | * ebpH Copyright 2017 Facebook, Inc. 3 | * pH Copyright (C) 1999-2003 Anil Somayaji and (C) 2008 Mario Van Velzen 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | #pragma once 19 | 20 | #if defined(__ELF__) && \ 21 | (defined(__powerpc64__) || defined(__powerpc__) || defined(__aarch64__) || \ 22 | defined(__x86_64__) || defined(__i386__)) 23 | #include "StaticTracepoint-ELF.h" 24 | 25 | #define FOLLY_SDT(provider, name, ...) \ 26 | FOLLY_SDT_PROBE_N( \ 27 | provider, name, FOLLY_SDT_NARG(0, ##__VA_ARGS__), ##__VA_ARGS__) 28 | #else 29 | #define FOLLY_SDT(provider, name, ...) do {} while(0) 30 | #endif 31 | -------------------------------------------------------------------------------- /tests/driver/malicious.c: -------------------------------------------------------------------------------- 1 | /* ebpH (Extended BPF Process Homeostasis) A host-based IDS written in eBPF. 2 | * ebpH Copyright (C) 2019-2020 William Findlay 3 | * pH Copyright (C) 1999-2003 Anil Somayaji and (C) 2008 Mario Van Velzen 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | * 18 | * A simple hello world program. 19 | * 20 | * 2020-Jul-16 William Findlay Created this. 21 | */ 22 | 23 | #include 24 | #include 25 | #include 26 | #include 27 | 28 | int main(int argc, char **argv) { 29 | if (argc > 1) { 30 | char *arg_list[] = {"ls", "-lah", NULL}; 31 | execvp(arg_list[0], arg_list); 32 | printf("Failed with %s\n", strerror(errno)); 33 | } 34 | 35 | write(1, "Hello, world!\n", 14); 36 | } 37 | -------------------------------------------------------------------------------- /ebph/commands/ebph_logs.py: -------------------------------------------------------------------------------- 1 | """ 2 | ebpH (Extended BPF Process Homeostasis) A host-based IDS written in eBPF. 3 | ebpH Copyright (C) 2019-2020 William Findlay 4 | pH Copyright (C) 1999-2003 Anil Somayaji and (C) 2008 Mario Van Velzen 5 | 6 | This program is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with this program. If not, see . 18 | 19 | Implements ebph ps. 20 | 21 | 2020-Jul-13 William Findlay Created this. 22 | """ 23 | 24 | import os 25 | from argparse import Namespace 26 | 27 | from ebph import defs 28 | from ebph.logger import color_log 29 | 30 | def main(args: Namespace) -> None: 31 | logfile = os.path.join(defs.LOG_DIR, 'ebph.log') 32 | 33 | with open(logfile, 'r') as f: 34 | for line in f: 35 | try: 36 | print(color_log(line)) 37 | except IOError: 38 | pass 39 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | -i https://pypi.org/simple 2 | -e . 3 | certifi==2020.6.20 4 | chardet==3.0.4 5 | click==7.1.2; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' 6 | colorama==0.4.3 7 | coloredlogs==14.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' 8 | docutils==0.16; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' 9 | executor==23.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' 10 | fastapi==0.60.1 11 | fasteners==0.15 12 | h11==0.9.0 13 | httptools==0.1.1; sys_platform != 'win32' and sys_platform != 'cygwin' and platform_python_implementation != 'PyPy' 14 | humanfriendly==8.2; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' 15 | idna==2.10; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' 16 | lockfile==0.12.2 17 | monotonic==1.5 18 | proc==1.0 19 | property-manager==3.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' 20 | pydantic==1.6.1; python_version >= '3.6' 21 | python-daemon==2.2.4 22 | ratelimit==2.2.1 23 | requests-unixsocket==0.2.0 24 | requests==2.24.0 25 | six==1.15.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' 26 | starlette==0.13.6; python_version >= '3.6' 27 | urllib3==1.25.10; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' and python_version < '4' 28 | uvicorn==0.11.8 29 | uvloop==0.14.0; sys_platform != 'win32' and sys_platform != 'cygwin' and platform_python_implementation != 'PyPy' 30 | verboselogs==1.7 31 | websockets==8.1; python_full_version >= '3.6.1' 32 | -------------------------------------------------------------------------------- /tests/test_bpf_program/test_settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | ebpH (Extended BPF Process Homeostasis) A host-based IDS written in eBPF. 3 | ebpH Copyright (C) 2019-2020 William Findlay 4 | pH Copyright (C) 1999-2003 Anil Somayaji and (C) 2008 Mario Van Velzen 5 | 6 | This program is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with this program. If not, see . 18 | 19 | Test changing BPF program settings. 20 | 21 | 2020-Jul-16 William Findlay Created this. 22 | """ 23 | 24 | import os 25 | import subprocess 26 | import ctypes as ct 27 | import time 28 | from random import randint 29 | 30 | from ebph.bpf_program import BPFProgram 31 | from ebph.utils import which, calculate_profile_key 32 | from ebph.structs import EBPH_SETTINGS 33 | 34 | 35 | def test_change_settings(bpf_program: BPFProgram, caplog): 36 | """ 37 | Test getting and setting all ebpH settings. 38 | """ 39 | for setting in EBPH_SETTINGS: 40 | for _ in range(100): 41 | value = randint(0, 2 ** 64 - 1) 42 | bpf_program.change_setting(setting, value) 43 | assert bpf_program.get_setting(setting) == value 44 | 45 | 46 | def test_invalid_settings(bpf_program: BPFProgram, caplog): 47 | """ 48 | Test getting and setting invalid ebpH settings. 49 | """ 50 | for setting in EBPH_SETTINGS: 51 | for _ in range(100): 52 | original_value = bpf_program.get_setting(setting) 53 | value = randint(-(2 ** 64 - 1), -1) 54 | assert bpf_program.change_setting(setting, value) < 0 55 | assert bpf_program.get_setting(setting) == original_value 56 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | """ 2 | ebpH (Extended BPF Process Homeostasis) A host-based IDS written in eBPF. 3 | ebpH Copyright (C) 2019-2020 William Findlay 4 | pH Copyright (C) 1999-2003 Anil Somayaji and (C) 2008 Mario Van Velzen 5 | 6 | This program is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with this program. If not, see . 18 | 19 | Provide a fixture for an ebpH bpf_program continuously calling on_tick. 20 | 21 | 2020-Jul-16 William Findlay Created this. 22 | """ 23 | import os 24 | import time 25 | import logging 26 | import threading 27 | 28 | import pytest 29 | 30 | from ebph.ebphd import parse_args 31 | from ebph.logger import EBPHLoggerClass 32 | from ebph.bpf_program import BPFProgram 33 | from ebph import defs 34 | 35 | NEWSEQ = EBPHLoggerClass.SEQUENCE 36 | 37 | # Redirect profile saving to /tmp/ebph/profiles 38 | defs.EBPH_DATA_DIR = '/tmp/ebph/profiles' 39 | # Redirect logging to /tmp/ebph/log 40 | defs.EBPH_LOG_DIR = '/tmp/ebph/log' 41 | 42 | args = parse_args('--nodaemon'.split()) 43 | defs.init(args) 44 | 45 | def loop_forever(bpf_program: BPFProgram): 46 | def inner(): 47 | while 1: 48 | bpf_program.on_tick() 49 | time.sleep(defs.TICK_SLEEP) 50 | return inner 51 | 52 | @pytest.fixture(scope='function') 53 | def bpf_program(caplog): 54 | for f in os.listdir(defs.EBPH_DATA_DIR): 55 | os.unlink(os.path.join(defs.EBPH_DATA_DIR, f)) 56 | 57 | # Set log level 58 | caplog.set_level(NEWSEQ) 59 | b = BPFProgram() 60 | 61 | thread = threading.Thread(target=loop_forever(b)) 62 | thread.daemon = True 63 | thread.start() 64 | 65 | yield b 66 | 67 | b.on_tick() 68 | b._cleanup 69 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """ 4 | ebpH (Extended BPF Process Homeostasis) A host-based IDS written in eBPF. 5 | Copyright (C) 2019-2020 William Findlay 6 | 7 | This program is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | 20 | 2020-Jul-13 William Findlay Created this. 21 | """ 22 | 23 | import os, sys 24 | import re 25 | from distutils.core import setup, Extension 26 | from distutils.command.build_ext import build_ext 27 | 28 | version = '0.4.0' 29 | 30 | class ct_build_ext(build_ext): 31 | def build_extension(self, ext): 32 | self._ctypes = isinstance(ext, CTypes) 33 | return super().build_extension(ext) 34 | 35 | def get_export_symbols(self, ext): 36 | if self._ctypes: 37 | return ext.export_symbols 38 | return super().get_export_symbols(ext) 39 | 40 | def get_ext_filename(self, ext_name): 41 | if self._ctypes: 42 | return ext_name + '.so' 43 | return super().get_ext_filename(ext_name) 44 | 45 | 46 | class CTypes(Extension): 47 | pass 48 | 49 | 50 | libebph = CTypes('ebph/libebph/bin/libebph', sources=['ebph/libebph/libebph.c']) 51 | 52 | 53 | setup( 54 | name='ebph', 55 | version=version, 56 | description='Extended BPF Process Homeostasis: Host-based anomaly detection in eBPF.', 57 | author='William Findlay', 58 | author_email='william@williamfindlay.com', 59 | url='https://github.com/willfindlay/ebpH', 60 | packages=['ebph'], 61 | scripts=['bin/ebphd', 'bin/ebph'], 62 | include_package_data=True, 63 | package_data={'': ['ebph/bpf/*', 'ebph/libebph/*', 'ebph/commands/*']}, 64 | ext_modules=[libebph], 65 | cmdclass={'build_ext': ct_build_ext}, 66 | ) 67 | -------------------------------------------------------------------------------- /ebph/libebph/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | from bcc import USDT 3 | import ctypes as ct 4 | from ctypes.util import find_library 5 | from typing import get_type_hints, List, Any 6 | 7 | from ebph import defs 8 | from ebph.logger import get_logger 9 | 10 | logger = get_logger() 11 | 12 | libebph = ct.CDLL(defs.LIBEBPH, use_errno=True) 13 | 14 | usdt_context = USDT(pid=os.getpid()) 15 | 16 | def command(func): 17 | """ 18 | A decorator that allows a function to provide an interface into a libebph 19 | command of the same name. Types are determined using Python type hints. 20 | """ 21 | name = func.__name__ 22 | th = get_type_hints(func) 23 | argtypes = [v for k, v in th.items() if k != 'return'] 24 | try: 25 | restype = th['return'] 26 | except KeyError: 27 | restype = None 28 | @staticmethod 29 | def wrapper(*args, **kwargs): 30 | return getattr(libebph, name)(*args, **kwargs) 31 | getattr(libebph, name).argtypes = argtypes 32 | getattr(libebph, name).restype = restype 33 | logger.info(f'Registering USDT probe {name} -> command_{name}...') 34 | logger.debug(f'name={name}, argtypes={argtypes}, restype={restype}') 35 | usdt_context.enable_probe_or_bail(name, 'command_' + name) 36 | return wrapper 37 | 38 | class Lib: 39 | """ 40 | Exports libebph commands, inferring ctypes argtypes and restypes 41 | using Python type hints. All @command methods are static methods. 42 | """ 43 | usdt_context = usdt_context 44 | 45 | @command 46 | def set_setting(key: ct.c_int, value: ct.c_uint64) -> ct.c_int: 47 | pass 48 | 49 | @command 50 | def normalize_profile(profile_key: ct.c_uint64) -> ct.c_int: 51 | pass 52 | 53 | @command 54 | def normalize_process(pid: ct.c_uint32) -> ct.c_int: 55 | pass 56 | 57 | @command 58 | def sensitize_profile(profile_key: ct.c_uint64) -> ct.c_int: 59 | pass 60 | 61 | @command 62 | def sensitize_process(pid: ct.c_uint32) -> ct.c_int: 63 | pass 64 | 65 | @command 66 | def tolerize_profile(profile_key: ct.c_uint64) -> ct.c_int: 67 | pass 68 | 69 | @command 70 | def tolerize_process(pid: ct.c_uint32) -> ct.c_int: 71 | pass 72 | 73 | @command 74 | def bootstrap_process(profile_key: ct.c_uint64, pid: ct.c_uint32, tgid: ct.c_uint32, pathname: ct.c_char_p) -> ct.c_int: 75 | pass 76 | 77 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ebpH 2 | 3 | ## Description 4 | 5 | ebpH stands for Extended BPF Process Homeostasis. 6 | 7 | ebpH is a modern host-based intrusion detection system for Linux 5.8+ that 8 | leverages the power of Extended BPF (eBPF) to monitor processes and detect anomalous behavior. 9 | This effectively constitutes an eBPF implementation of [pH (Process Homeostasis)](https://people.scs.carleton.ca/~mvvelzen/pH/pH.html). 10 | 11 | ## Disclaimer 12 | 13 | This product comes with no warranty, and is built as a research system. It should be perfectly safe to run on your system due to the safety guarantees of eBPF, but we make no claims about functionality. 14 | 15 | ## Papers 16 | 17 | ### ebpH 18 | 19 | - [My thesis](https://www.cisl.carleton.ca/~will/written/coursework/undergrad-ebpH-thesis.pdf) 20 | 21 | ### pH 22 | 23 | - [My supervisor's original dissertation on pH](https://people.scs.carleton.ca/~soma/pubs/soma-diss.pdf) 24 | - [A Sense of Self for UNIX Processes](https://www.cs.unm.edu/~immsec/publications/ieee-sp-96-unix.pdf) 25 | - [Lightweight Intrustion Detection for Networked Operating Systems](http://people.scs.carleton.ca/~soma/pubs/jcs1998.pdf) 26 | - [Lookahead Pairs and Full Sequences: A Tale of Two Anomaly Detection Methods](http://people.scs.carleton.ca/~soma/pubs/inoue-albany2007.pdf) 27 | 28 | ## Prerequisites 29 | 30 | 1. Linux 5.8+ compiled with at least `CONFIG_BPF=y`, `CONFIG_BPF_SYSCALL=y`, `CONFIG_BPF_JIT=y`, `CONFIG_TRACEPOINTS=y`, `CONFIG_BPF_LSM=y`, `CONFIG_DEBUG_INFO=y`, `CONFIG_DEBUG_INFO_BTF=y`, `CONFIG_LSM="bpf"`. pahole >= 0.16 must be installed for the kernel to be built with BTF info. 31 | 1. Either the latest version of bcc from https://github.com/iovisor/bcc or bcc version 0.16+. 32 | - If building from source, be sure to include `-DPYTHON_CMD=python3` in your the cmake flags 33 | 1. Python 3.8+ 34 | 35 | ## Installation 36 | 37 | 1. Install the prerequisites (see above). 38 | 1. `git clone https://github.com/willfindlay/ebpH` 39 | 1. `cd ebpH && make install` (You will be asked for your password) 40 | 1. To install the systemd unit: `make systemd` (You will be asked for your password) 41 | 42 | ## How to Use / Examples 43 | 44 | 1. Run `$ sudo ebphd start` to start the daemon. 45 | 1. Run `$ sudo ebph admin status` to check daemon status. 46 | 1. Run `$ sudo ebph ps` to check monitored processes. 47 | 1. Run `$ sudo ebph ps -p` to list all active profiles. 48 | 49 | Or, with systemd: 50 | 51 | 1. Run `$ sudo systemctl start ebphd` to start the daemon if not already running. 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | Pipfile.lock 2 | 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | pip-wheel-metadata/ 26 | share/python-wheels/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | MANIFEST 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .nox/ 46 | .coverage 47 | .coverage.* 48 | .cache 49 | nosetests.xml 50 | coverage.xml 51 | *.cover 52 | *.py,cover 53 | .hypothesis/ 54 | .pytest_cache/ 55 | 56 | # Translations 57 | *.mo 58 | *.pot 59 | 60 | # Django stuff: 61 | *.log 62 | local_settings.py 63 | db.sqlite3 64 | db.sqlite3-journal 65 | 66 | # Flask stuff: 67 | instance/ 68 | .webassets-cache 69 | 70 | # Scrapy stuff: 71 | .scrapy 72 | 73 | # Sphinx documentation 74 | docs/_build/ 75 | 76 | # PyBuilder 77 | target/ 78 | 79 | # Jupyter Notebook 80 | .ipynb_checkpoints 81 | 82 | # IPython 83 | profile_default/ 84 | ipython_config.py 85 | 86 | # pyenv 87 | .python-version 88 | 89 | # pipenv 90 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 91 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 92 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 93 | # install all needed dependencies. 94 | #Pipfile.lock 95 | 96 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 97 | __pypackages__/ 98 | 99 | # Celery stuff 100 | celerybeat-schedule 101 | celerybeat.pid 102 | 103 | # SageMath parsed files 104 | *.sage.py 105 | 106 | # Environments 107 | .env 108 | .venv 109 | env/ 110 | venv/ 111 | ENV/ 112 | env.bak/ 113 | venv.bak/ 114 | 115 | # Spyder project settings 116 | .spyderproject 117 | .spyproject 118 | 119 | # Rope project settings 120 | .ropeproject 121 | 122 | # mkdocs documentation 123 | /site 124 | 125 | # mypy 126 | .mypy_cache/ 127 | .dmypy.json 128 | dmypy.json 129 | 130 | # Pyre type checker 131 | .pyre/ 132 | -------------------------------------------------------------------------------- /tests/test_api/test_api.py: -------------------------------------------------------------------------------- 1 | """ 2 | ebpH (Extended BPF Process Homeostasis) A host-based IDS written in eBPF. 3 | ebpH Copyright (C) 2019-2020 William Findlay 4 | pH Copyright (C) 1999-2003 Anil Somayaji and (C) 2008 Mario Van Velzen 5 | 6 | This program is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with this program. If not, see . 18 | 19 | Test the ebpH API. 20 | 21 | 2020-Jul-16 William Findlay Created this. 22 | 23 | TODO: Add several more tests here for complete coverage. 24 | """ 25 | 26 | from pprint import pprint 27 | from random import randint 28 | 29 | from ebph import defs 30 | from ebph.utils import ns_to_str, ns_to_delta_str 31 | from ebph.structs import EBPH_SETTINGS 32 | 33 | def test_get_status(client): 34 | """ 35 | Test getting current status. 36 | """ 37 | res = client.get('/status') 38 | 39 | assert res.status_code == 200 40 | 41 | body = res.json() 42 | assert body['Monitoring'] == True 43 | assert body['Anomaly Limit'] == defs.ANOMALY_LIMIT 44 | assert body['Normal Factor'] == f'{defs.NORMAL_FACTOR}/{defs.NORMAL_FACTOR_DEN}' 45 | assert body['Normal Wait'] == ns_to_delta_str(defs.NORMAL_WAIT) 46 | # TODO: parse and test process and profile counts 47 | 48 | def test_get_set_settings(client): 49 | """ 50 | Test getting and setting all ebpH settings through the API. 51 | """ 52 | for setting in EBPH_SETTINGS: 53 | for _ in range(100): 54 | value = randint(0, 2 ** 64 - 1) 55 | 56 | res = client.put(f'/settings/{setting}/{value}') 57 | assert res.status_code == 200 58 | set_json = res.json() 59 | 60 | res = client.get(f'/settings/{setting}') 61 | assert res.status_code == 200 62 | get_json = res.json() 63 | 64 | assert get_json == set_json 65 | 66 | def test_get_set_invalid_settings(client): 67 | """ 68 | Test getting and setting invalid ebpH settings through the API. 69 | """ 70 | for setting in EBPH_SETTINGS: 71 | for _ in range(100): 72 | value = randint(-(2 ** 64 - 1), -1) 73 | 74 | res = client.get(f'/settings/{setting}') 75 | assert res.status_code == 200 76 | orig_json = res.json() 77 | 78 | res = client.put(f'/settings/{setting}/{value}') 79 | assert res.status_code != 200 80 | 81 | res = client.get(f'/settings/{setting}') 82 | assert res.status_code == 200 83 | curr_json = res.json() 84 | 85 | assert orig_json == curr_json 86 | -------------------------------------------------------------------------------- /ebph/bpf/lsm.h: -------------------------------------------------------------------------------- 1 | /* ebpH (Extended BPF Process Homeostasis) A host-based IDS written in eBPF. 2 | * ebpH Copyright (C) 2019-2020 William Findlay 3 | * pH Copyright (C) 1999-2003 Anil Somayaji and (C) 2008 Mario Van Velzen 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | * 18 | * Provides a unique ID for each LSM program. 19 | * 20 | * 2020-Aug-04 William Findlay Created this. 21 | */ 22 | 23 | enum ebph_lsm_id_t { 24 | EBPH_BPRM_CHECK_SECURITY = 0, 25 | EBPH_TASK_ALLOC, 26 | EBPH_TASK_FREE, 27 | EBPH_TASK_SETPGID, 28 | EBPH_TASK_GETPGID, 29 | EBPH_TASK_GETSID, 30 | EBPH_TASK_SETNICE, 31 | EBPH_TASK_SETIOPRIO, 32 | EBPH_TASK_GETIOPRIO, 33 | EBPH_TASK_PRLIMIT, 34 | EBPH_TASK_SETRLIMIT, 35 | EBPH_TASK_SETSCHEDULER, 36 | EBPH_TASK_GETSCHEDULER, 37 | EBPH_TASK_MOVEMEMORY, 38 | EBPH_TASK_KILL, // TODO: split this into coarse signal categories 39 | EBPH_TASK_PRCTL, 40 | EBPH_SB_STATFS, 41 | EBPH_SB_MOUNT, 42 | EBPH_SB_REMOUNT, 43 | EBPH_SB_UMOUNT, 44 | EBPH_SB_PIVOTROOT, 45 | EBPH_MOVE_MOUNT, 46 | EBPH_INODE_CREATE, 47 | EBPH_INODE_LINK, 48 | EBPH_INODE_SYMLINK, 49 | EBPH_INODE_MKDIR, 50 | EBPH_INODE_RMDIR, 51 | EBPH_INODE_MKNOD, 52 | EBPH_INODE_RENAME, 53 | EBPH_INODE_READLINK, 54 | EBPH_INODE_FOLLOW_LINK, 55 | EBPH_INODE_PERMISSION, // TODO: split this into READ, WRITE, APPEND, EXEC 56 | EBPH_INODE_SETATTR, 57 | EBPH_INODE_GETATTR, 58 | EBPH_INODE_SETXATTR, 59 | EBPH_INODE_GETXATTR, 60 | EBPH_INODE_LISTXATTR, 61 | EBPH_INODE_REMOVEXATTR, 62 | EBPH_FILE_PERMISSION, // TODO: split this into READ, WRITE, APPEND, EXEC 63 | EBPH_FILE_IOCTL, 64 | EBPH_MMAP_ADDR, 65 | EBPH_MMAP_FILE, 66 | EBPH_FILE_MPROTECT, 67 | EBPH_FILE_LOCK, 68 | EBPH_FILE_FCNTL, 69 | EBPH_FILE_SEND_SIGIOTASK, 70 | EBPH_FILE_RECEIVE, 71 | EBPH_UNIX_STREAM_CONNECT, 72 | EBPH_UNIX_MAY_SEND, 73 | EBPH_SOCKET_CREATE, 74 | EBPH_SOCKET_SOCKETPAIR, 75 | EBPH_SOCKET_BIND, 76 | EBPH_SOCKET_CONNECT, 77 | EBPH_SOCKET_LISTEN, 78 | EBPH_SOCKET_ACCEPT, 79 | EBPH_SOCKET_SENDMSG, 80 | EBPH_SOCKET_RECVMSG, 81 | EBPH_SOCKET_GETSOCKNAME, 82 | EBPH_SOCKET_GETPEERNAME, 83 | EBPH_SOCKET_GETSOCKOPT, 84 | EBPH_SOCKET_SETSOCKOPT, 85 | EBPH_SOCKET_SHUTDOWN, 86 | EBPH_TUN_DEV_CREATE, 87 | EBPH_TUN_DEV_ATTACH, 88 | EBPH_KEY_ALLOC, 89 | EBPH_KEY_FREE, 90 | EBPH_KEY_PERMISSION, // TODO: maybe split this into operations 91 | EBPH_IPC_PERMISSION, 92 | EBPH_MSG_QUEUE_ASSOCIATE, 93 | EBPH_MSG_QUEUE_MSGCTL, 94 | EBPH_MSG_QUEUE_MSGSND, 95 | EBPH_MSG_QUEUE_MSGRCV, 96 | EBPH_SHM_ASSOCIATE, 97 | EBPH_SHM_SHMCTL, 98 | EBPH_SHM_SHMAT, 99 | EBPH_PTRACE_ACCESS_CHECK, 100 | EBPH_PTRACE_TRACEME, 101 | EBPH_CAPGET, 102 | EBPH_CAPSET, 103 | EBPH_CAPABLE, 104 | EBPH_QUOTACTL, 105 | EBPH_QUOTA_ON, 106 | EBPH_SYSLOG, 107 | EBPH_SETTIME, 108 | EBPH_VM_ENOUGH_MEMORY, 109 | EBPH_BPF, 110 | EBPH_BPF_MAP, 111 | EBPH_BPF_PROG, 112 | EBPH_PERF_EVENT_OPEN, 113 | EBPH_LSM_MAX, // This must always be the last entry 114 | }; 115 | -------------------------------------------------------------------------------- /ebph/defs.py: -------------------------------------------------------------------------------- 1 | """ 2 | ebpH (Extended BPF Process Homeostasis) A host-based IDS written in eBPF. 3 | ebpH Copyright (C) 2019-2020 William Findlay 4 | pH Copyright (C) 1999-2003 Anil Somayaji and (C) 2008 Mario Van Velzen 5 | 6 | This program is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with this program. If not, see . 18 | 19 | Provides serveral constants for ebpH. 20 | 21 | 2020-Jul-13 William Findlay Created this. 22 | """ 23 | 24 | import os 25 | from argparse import Namespace 26 | 27 | from ebph.utils import project_path 28 | 29 | # Root directory of ebpH 30 | EBPH_DIR = project_path('ebph') 31 | # Path to BPF source directory 32 | BPF_DIR = project_path('ebph/bpf') 33 | # Path to BPF source code 34 | BPF_PROGRAM_C = project_path('ebph/bpf/bpf_program.c') 35 | 36 | # Path to libebph.so 37 | LIBEBPH = project_path('ebph/libebph/bin/libebph.so') 38 | 39 | # train_count / (train_count - last_mod_count) must exceed 40 | # NORMAL_FACTOR / NORMAL_FACTOR_DEN for a profile to become normal 41 | NORMAL_FACTOR = 128 42 | NORMAL_FACTOR_DEN = 32 43 | 44 | # Number of allowed anomalies before a profile is no longer normal 45 | ANOMALY_LIMIT = 30 46 | 47 | # Allowed LFC count before resetting training data 48 | TOLERIZE_LIMIT = 12 49 | 50 | # Time in nanoseconds that a profile must remain frozen in order to become normal 51 | NORMAL_WAIT = 1000000000 * 60 * 60 * 24 * 7 # 1 week 52 | #NORMAL_WAIT = 1000000000 * 60 * 10 # 10 minutes 53 | #NORMAL_WAIT = 1000000000 * 30 # 30 seconds 54 | 55 | # Start in enforcing mode 56 | ENFORCING = False 57 | 58 | PATH_MAX = 4096 59 | 60 | # Compiler defines used in BPF program 61 | BPF_DEFINES = { 62 | # Maximum number of active profiles 63 | 'EBPH_MAX_PROFILES': 10240, 64 | # Maximum number of active processes at a given time 65 | 'EBPH_MAX_PROCESSES': 10240, 66 | 67 | # Length of a sequence 68 | 'EBPH_SEQLEN': 9, 69 | # Number of frames in sequence stack 70 | 'EBPH_SEQSTACK_FRAMES': 2, 71 | 72 | # Length of ALF 73 | 'EBPH_LOCALITY_WIN': 128, 74 | 75 | # The empty lsm call 76 | 'EBPH_EMPTY': 9999, 77 | 78 | # TODO: tweak these to sensible values 79 | 'EBPH_TOLERANCE_LOW': 3, 80 | 'EBPH_TOLERANCE_MEDIUM': 10, 81 | 'EBPH_TOLERANCE_HIGH': 30, 82 | 'EBPH_TOLERANCE_ALWAYS': 0, 83 | } 84 | 85 | LOG_DIR = '/var/log/ebpH' 86 | 87 | PIDFILE = '/run/ebpH.pid' 88 | 89 | EBPH_DATA_DIR = '/var/lib/ebpH/profiles' 90 | 91 | EBPH_PORT = 1337 92 | 93 | EBPH_SOCK = '/var/run/ebpH.sock' 94 | 95 | PROFILE_SAVE_INTERVAL = 10000 96 | 97 | TICK_SLEEP = 0.1 98 | 99 | 100 | def init(args: Namespace) -> None: 101 | """ 102 | Perform basic setup. 103 | """ 104 | # Set log file location 105 | global LOGFILE 106 | LOGFILE = os.path.join(LOG_DIR, 'ebph.log') 107 | 108 | # Make working_directory or set permissions of existing working_directory 109 | try: 110 | os.makedirs(EBPH_DATA_DIR, mode=0o700, exist_ok=True) 111 | except OSError: 112 | os.chmod(EBPH_DATA_DIR, mode=0o700) 113 | 114 | # Make policy_directory or set permissions of existing policy_directory 115 | try: 116 | os.makedirs(LOG_DIR, mode=0o755, exist_ok=True) 117 | except OSError: 118 | os.chmod(LOG_DIR, mode=0o755) 119 | 120 | from ebph.logger import setup_logger 121 | setup_logger(args) 122 | 123 | # Make pidfile parent directory 124 | os.makedirs(os.path.dirname(PIDFILE), exist_ok=True) 125 | os.makedirs(os.path.dirname(EBPH_SOCK), exist_ok=True) 126 | -------------------------------------------------------------------------------- /tests/test_bpf_program/test_profile_creation.py: -------------------------------------------------------------------------------- 1 | """ 2 | ebpH (Extended BPF Process Homeostasis) A host-based IDS written in eBPF. 3 | ebpH Copyright (C) 2019-2020 William Findlay 4 | pH Copyright (C) 1999-2003 Anil Somayaji and (C) 2008 Mario Van Velzen 5 | 6 | This program is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with this program. If not, see . 18 | 19 | Test profile creation. 20 | 21 | 2020-Jul-16 William Findlay Created this. 22 | """ 23 | 24 | import os 25 | import subprocess 26 | import ctypes as ct 27 | import time 28 | 29 | from ebph.bpf_program import BPFProgram 30 | from ebph.utils import which, calculate_profile_key, project_path 31 | 32 | def test_one_profile(bpf_program: BPFProgram, caplog): 33 | """ 34 | Make sure that a single profile is created properly. 35 | """ 36 | ls = which('ls') 37 | 38 | # There should be at least one profile after this 39 | subprocess.Popen(ls).wait() 40 | 41 | assert len(bpf_program.bpf['profiles']) >= 1 42 | 43 | # Make sure we can look up the profile by its key 44 | profile_key = calculate_profile_key(ls) 45 | bpf_program.bpf['profiles'][ct.c_uint64(profile_key)] 46 | 47 | # Force a tick update here 48 | bpf_program.on_tick() 49 | 50 | # Make sure the profile has the correct name associated with it 51 | assert bpf_program.profile_key_to_exe[profile_key] in ['ls', ls] 52 | 53 | def test_multiple_profiles(bpf_program: BPFProgram, caplog): 54 | """ 55 | Make sure that multiple profiles are created properly. 56 | """ 57 | ls = which('ls') 58 | ps = which('ps') 59 | 60 | # There should be one profile after this 61 | subprocess.Popen(ls).wait() 62 | 63 | assert len(bpf_program.bpf['profiles']) >= 1 64 | 65 | # There should be two profiles after this 66 | subprocess.Popen(ps).wait() 67 | 68 | assert len(bpf_program.bpf['profiles']) >= 2 69 | 70 | # Make sure we can look up the profile by its key 71 | profile_key = calculate_profile_key(ls) 72 | bpf_program.bpf['profiles'][ct.c_uint64(profile_key)] 73 | # Make sure the profile has the correct name associated with it 74 | bpf_program.on_tick() 75 | assert bpf_program.profile_key_to_exe[profile_key] in ['ls', ls] 76 | 77 | # Make sure we can look up the profile by its key 78 | profile_key = calculate_profile_key(ps) 79 | bpf_program.bpf['profiles'][ct.c_uint64(profile_key)] 80 | # Make sure the profile has the correct name associated with it 81 | bpf_program.on_tick() 82 | assert bpf_program.profile_key_to_exe[profile_key] in ['ps', ps] 83 | 84 | def test_sample_workload(bpf_program: BPFProgram, caplog): 85 | """ 86 | Test profile creation for several processes executed within a shell script. 87 | """ 88 | sample_workload = project_path('tests/driver/sample_workload.sh') 89 | subprocess.Popen(sample_workload).wait() 90 | 91 | # Profiles should at least include the following: 92 | profile_names = ['bash', 'ls', 'wc', 'ps', 'cat', 'echo', 'grep'] 93 | profile_locations = [which(n) for n in profile_names] 94 | 95 | assert len(bpf_program.bpf['profiles']) >= 7 96 | 97 | # Force a tick update here 98 | bpf_program.on_tick() 99 | 100 | for n, p in zip(profile_names, profile_locations): 101 | # Make sure we can look up the profile by its key 102 | profile_key = calculate_profile_key(p) 103 | bpf_program.bpf['profiles'][ct.c_uint64(profile_key)] 104 | # Make sure the profile has the correct name associated with it 105 | assert bpf_program.profile_key_to_exe[profile_key] in [n, p] 106 | 107 | -------------------------------------------------------------------------------- /ebph/daemon_mixin.py: -------------------------------------------------------------------------------- 1 | """ 2 | ebpH (Extended BPF Process Homeostasis) A host-based IDS written in eBPF. 3 | ebpH Copyright (C) 2019-2020 William Findlay 4 | pH Copyright (C) 1999-2003 Anil Somayaji and (C) 2008 Mario Van Velzen 5 | 6 | This program is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with this program. If not, see . 18 | 19 | Daemon logic using python-daemon. 20 | 21 | 2020-Jul-13 William Findlay Created this. 22 | """ 23 | 24 | import time 25 | import os, sys 26 | import signal 27 | import atexit 28 | import socket 29 | from typing import Union, NoReturn 30 | 31 | from daemon import DaemonContext, pidfile 32 | 33 | from ebph import defs 34 | from ebph.logger import get_logger 35 | 36 | logger = get_logger() 37 | 38 | class DaemonMixin: 39 | def loop_forever(self): 40 | raise NotImplementedError('Implement loop_forever(self) in the subclass.') 41 | 42 | def bind_socket(self): 43 | """ 44 | Bind ebpH UDS. 45 | """ 46 | if os.path.exists(defs.EBPH_SOCK): 47 | logger.error(f'Unable to start daemon because {defs.EBPH_SOCK} exists in filesystem. If this is a mistake, delete the file manually.') 48 | sys.exit(-1) 49 | try: 50 | sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) 51 | old_umask = os.umask(0o177) 52 | sock.bind(defs.EBPH_SOCK) 53 | os.umask(old_umask) 54 | atexit.register(self._cleanup_socket) 55 | except Exception as e: 56 | logger.error(f'Failed to bind {defs.EBPH_SOCK}!') 57 | raise e 58 | 59 | def _cleanup_socket(self): 60 | try: 61 | os.unlink(defs.EBPH_SOCK) 62 | except OSError as e: 63 | if os.path.exists(self.server_address): 64 | logger.error(f'Failed to unlink {defs.EBPH_SOCK}!', exc_info=e) 65 | 66 | def get_pid(self) -> Union[int, None]: 67 | """ 68 | Get pid of the running daemon. 69 | """ 70 | try: 71 | with open(defs.PIDFILE, 'r') as f: 72 | return int(f.read().strip()) 73 | except: 74 | return None 75 | 76 | def stop_daemon(self, in_restart: bool = False) -> None: 77 | """ 78 | Stop the daemon. 79 | """ 80 | pid = self.get_pid() 81 | try: 82 | os.kill(pid, signal.SIGTERM) 83 | except TypeError: 84 | if not in_restart: 85 | logger.warn(f'Attempted to kill daemon with pid {pid}, but no such process exists') 86 | sys.exit(-1) 87 | 88 | def start_daemon(self) -> NoReturn: 89 | """ 90 | Start the daemon. 91 | """ 92 | if self.get_pid(): 93 | logger.error(f'ebpH daemon is already running! If you believe this is an error, try deleting {defs.PIDFILE}.') 94 | sys.exit(-1) 95 | logger.info('Starting ebpH daemon...') 96 | with DaemonContext( 97 | umask=0o022, 98 | working_directory=defs.EBPH_DATA_DIR, 99 | pidfile=pidfile.TimeoutPIDLockFile(defs.PIDFILE), 100 | # Necessary to preserve logging 101 | files_preserve=[handler.stream for handler in logger.handlers] 102 | ): 103 | logger.info('ebpH daemon started successfully!') 104 | self.loop_forever() 105 | 106 | def restart_daemon(self) -> NoReturn: 107 | """ 108 | Restart the daemon. 109 | """ 110 | self.stop_daemon(in_restart=True) 111 | time.sleep(1) 112 | self.start_daemon() 113 | -------------------------------------------------------------------------------- /ebph/libebph/libebph.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | #include "include/folly/tracing/StaticTracepoint.h" 8 | 9 | struct timespec sleepy_time = { 10 | .tv_sec = 0, 11 | .tv_nsec = (long)1e7, 12 | }; 13 | 14 | #define MAGIC_RC -1337 15 | 16 | #define DO_COMMAND_START \ 17 | time_t start = time(NULL); \ 18 | time_t timeout = start + 200; \ 19 | int rc = MAGIC_RC; 20 | 21 | #define DO_COMMAND_END \ 22 | while (rc == MAGIC_RC && start < timeout) { \ 23 | nanosleep(&sleepy_time, NULL); \ 24 | start = time(NULL); \ 25 | } \ 26 | \ 27 | if (rc == MAGIC_RC) { \ 28 | return -EPERM; \ 29 | } \ 30 | \ 31 | if (rc < 0) { \ 32 | errno = -rc; \ 33 | } \ 34 | \ 35 | return rc; 36 | 37 | #define COMMAND0(name, ...) \ 38 | int name(void) \ 39 | { \ 40 | DO_COMMAND_START \ 41 | FOLLY_SDT(libebph, name, &rc); \ 42 | DO_COMMAND_END \ 43 | } 44 | 45 | #define COMMAND1(name, arg1_t, arg1) \ 46 | int name(arg1_t arg1) \ 47 | { \ 48 | DO_COMMAND_START \ 49 | FOLLY_SDT(libebph, name, &rc, &arg1); \ 50 | DO_COMMAND_END \ 51 | } 52 | 53 | #define COMMAND2(name, arg1_t, arg1, arg2_t, arg2) \ 54 | int name(arg1_t arg1, arg2_t arg2) \ 55 | { \ 56 | DO_COMMAND_START \ 57 | FOLLY_SDT(libebph, name, &rc, &arg1, &arg2); \ 58 | DO_COMMAND_END \ 59 | } 60 | 61 | #define COMMAND3(name, arg1_t, arg1, arg2_t, arg2, arg3_t, arg3) \ 62 | int name(arg1_t arg1, arg2_t arg2, arg3_t arg3) \ 63 | { \ 64 | DO_COMMAND_START \ 65 | FOLLY_SDT(libebph, name, &rc, &arg1, &arg2, &arg3); \ 66 | DO_COMMAND_END \ 67 | } 68 | 69 | #define COMMAND4(name, arg1_t, arg1, arg2_t, arg2, arg3_t, arg3, arg4_t, arg4) \ 70 | int name(arg1_t arg1, arg2_t arg2, arg3_t arg3, arg4_t arg4) \ 71 | { \ 72 | DO_COMMAND_START \ 73 | FOLLY_SDT(libebph, name, &rc, &arg1, &arg2, &arg3, &arg4); \ 74 | DO_COMMAND_END \ 75 | } 76 | 77 | #define COMMAND5(name, arg1_t, arg1, arg2_t, arg2, arg3_t, arg3, arg4_t, arg4, \ 78 | arg5_t, arg5) \ 79 | int name(arg1_t arg1, arg2_t arg2, arg3_t arg3, arg4_t arg4, arg5_t arg5) \ 80 | { \ 81 | DO_COMMAND_START \ 82 | FOLLY_SDT(libebph, name, &rc, &arg1, &arg2, &arg3, &arg4, &arg5); \ 83 | DO_COMMAND_END \ 84 | } 85 | 86 | #define COMMAND6(name, arg1_t, arg1, arg2_t, arg2, arg3_t, arg3, arg4_t, arg4, \ 87 | arg5_t, arg5, arg6_t, arg6) \ 88 | int name(arg1_t arg1, arg2_t arg2, arg3_t arg3, arg4_t arg4, arg5_t arg5, \ 89 | arg6_t arg6) \ 90 | { \ 91 | DO_COMMAND_START \ 92 | FOLLY_SDT(libebph, name, &rc, &arg1, &arg2, &arg3, &arg4, &arg5, \ 93 | &arg6); \ 94 | DO_COMMAND_END \ 95 | } 96 | 97 | -------------------------------------------------------------------------------- /ebph/utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | ebpH (Extended BPF Process Homeostasis) A host-based IDS written in eBPF. 3 | ebpH Copyright (C) 2019-2020 William Findlay 4 | pH Copyright (C) 1999-2003 Anil Somayaji and (C) 2008 Mario Van Velzen 5 | 6 | This program is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with this program. If not, see . 18 | 19 | Provides several utility functions that don't really fit elsewhere. 20 | 21 | 2020-Jul-13 William Findlay Created this. 22 | """ 23 | 24 | import os 25 | import sys 26 | from datetime import datetime, timedelta 27 | from typing import Callable, Iterator, Union, Tuple 28 | 29 | from proc.core import find_processes 30 | import requests 31 | import requests_unixsocket 32 | requests_unixsocket.monkeypatch() 33 | 34 | def project_path(f: str) -> str: 35 | """ 36 | Return the path of a file relative to the root dir of this project (parent directory of "src"). 37 | """ 38 | curr_dir = os.path.realpath(os.path.dirname(__file__)) 39 | project_dir = os.path.realpath(os.path.join(curr_dir, "..")) 40 | path = os.path.realpath(os.path.join(project_dir, f)) 41 | return path 42 | 43 | def read_chunks(f: str, size: int = 1024) -> Iterator[str]: 44 | """ 45 | Read a file in chunks. 46 | Default chunk size is 1024. 47 | """ 48 | while 1: 49 | data = f.read(size) 50 | if not data: 51 | break 52 | yield data 53 | 54 | def ns_to_str(ns: int) -> str: 55 | dt = datetime.fromtimestamp(ns // 1000000000) 56 | return dt.strftime('%Y-%m-%d %H:%M:%S') 57 | 58 | def ns_to_delta_str(ns: int) -> str: 59 | td = timedelta(seconds=(ns // 1000000000)) 60 | return str(td) 61 | 62 | def which(program: str) -> Union[str, None]: 63 | import os 64 | 65 | def is_exe(fpath): 66 | return os.path.isfile(fpath) and os.access(fpath, os.X_OK) 67 | 68 | fpath, _fname = os.path.split(program) 69 | if fpath: 70 | if is_exe(program): 71 | return program 72 | else: 73 | for path in os.environ["PATH"].split(os.pathsep): 74 | exe_file = os.path.join(path, program) 75 | if is_exe(exe_file): 76 | return exe_file 77 | 78 | return None 79 | 80 | def calculate_profile_key(fpath: str) -> int: 81 | s = os.stat(fpath) 82 | st_dev = s.st_dev 83 | st_ino = s.st_ino 84 | return st_dev << 32 | st_ino 85 | 86 | def fail_with(err: str) -> None: 87 | print(err, file=sys.stderr) 88 | sys.exit(-1) 89 | 90 | def request_or_die(req_method: Callable, url: str, fail_message:str = 'Operation failed', 91 | data=None, json=None, **kwargs) -> requests.Response: 92 | """ 93 | Either make a request, or die with an error message. 94 | """ 95 | from ebph.defs import EBPH_PORT, EBPH_SOCK 96 | sock = EBPH_SOCK.replace('/', '%2F') 97 | try: 98 | url = f'http+unix://{sock}{url}' 99 | res = req_method(url, data=data, json=json, **kwargs) 100 | if res.status_code != 200: 101 | try: 102 | fail_with(f'{fail_message}: {res.json()["detail"]}') 103 | except KeyError: 104 | fail_with(fail_message) 105 | return res 106 | except requests.ConnectTimeout: 107 | fail_with('Connection to ebpH daemon timed out during request!') 108 | except requests.ConnectionError: 109 | fail_with('Unable to connect to ebpH daemon!') 110 | 111 | def running_processes() -> Iterator[Tuple[int, str, int, int]]: 112 | """ 113 | Returns an interator of all processes running on the 114 | system. Iterator contains tuples of [@profile_key, @exe, @pid, @tid] 115 | """ 116 | for p in find_processes(): 117 | exe = p.exe 118 | pid = p.pgrp 119 | tid = p.pid 120 | if not exe: 121 | continue 122 | try: 123 | profile_key = calculate_profile_key(exe) 124 | except Exception: 125 | continue 126 | yield (profile_key, exe, pid, tid) 127 | -------------------------------------------------------------------------------- /tests/test_bpf_program/test_saving_loading.py: -------------------------------------------------------------------------------- 1 | """ 2 | ebpH (Extended BPF Process Homeostasis) A host-based IDS written in eBPF. 3 | ebpH Copyright (C) 2019-2020 William Findlay 4 | pH Copyright (C) 1999-2003 Anil Somayaji and (C) 2008 Mario Van Velzen 5 | 6 | This program is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with this program. If not, see . 18 | 19 | Test saving and loading. 20 | 21 | 2020-Jul-16 William Findlay Created this. 22 | """ 23 | 24 | import os 25 | import subprocess 26 | import ctypes as ct 27 | import time 28 | 29 | from ebph.bpf_program import BPFProgram 30 | from ebph.utils import which, calculate_profile_key, project_path 31 | from ebph.structs import EBPHProfileStruct 32 | 33 | 34 | def test_save_then_load_hello(bpf_program: BPFProgram, caplog): 35 | """ 36 | Make sure that saving, erasing, and then loading one profile 37 | works as expected. 38 | """ 39 | hello = project_path('tests/driver/hello') 40 | subprocess.Popen(hello).wait() 41 | bpf_program.on_tick() 42 | 43 | assert len(bpf_program.bpf['profiles']) >= 1 44 | 45 | profile_key = calculate_profile_key(hello) 46 | 47 | profile_before = bpf_program.get_full_profile(profile_key) 48 | 49 | bpf_program.stop_monitoring() 50 | bpf_program.save_profiles() 51 | 52 | # Clear relevant profile data 53 | bpf_program.profile_key_to_exe.clear() 54 | assert not bpf_program.profile_key_to_exe 55 | bpf_program.bpf['profiles'].clear() 56 | assert len(bpf_program.bpf['profiles']) == 0 57 | bpf_program.bpf['training_data'].clear() 58 | assert len(bpf_program.bpf['training_data']) == 0 59 | bpf_program.bpf['testing_data'].clear() 60 | assert len(bpf_program.bpf['testing_data']) == 0 61 | 62 | bpf_program.load_profiles() 63 | bpf_program.start_monitoring() 64 | profile_key = calculate_profile_key(hello) 65 | 66 | assert len(bpf_program.bpf['profiles']) >= 1 67 | 68 | profile_after = bpf_program.get_full_profile(profile_key) 69 | 70 | assert profile_before == profile_after 71 | 72 | 73 | def test_save_then_load_sample_workload(bpf_program: BPFProgram, caplog): 74 | """ 75 | Make sure that saving, erasing, and then loading several profiles 76 | works as expected. 77 | """ 78 | sample_workload = project_path('tests/driver/sample_workload.sh') 79 | subprocess.Popen(sample_workload).wait() 80 | bpf_program.on_tick() 81 | 82 | # Profiles shold now include the following: 83 | profile_names = ['bash', 'ls', 'wc', 'ps', 'cat', 'echo', 'grep'] 84 | profile_locations = [which(n) for n in profile_names] 85 | profiles_keys = [calculate_profile_key(loc) for loc in profile_locations] 86 | profiles_before = [bpf_program.get_full_profile(key) for key in profiles_keys] 87 | 88 | assert len(bpf_program.bpf['profiles']) >= 7 89 | 90 | bpf_program.stop_monitoring() 91 | bpf_program.save_profiles() 92 | 93 | # Clear relevant profile data 94 | bpf_program.profile_key_to_exe.clear() 95 | assert not bpf_program.profile_key_to_exe 96 | bpf_program.bpf['profiles'].clear() 97 | assert len(bpf_program.bpf['profiles']) == 0 98 | bpf_program.bpf['training_data'].clear() 99 | assert len(bpf_program.bpf['training_data']) == 0 100 | bpf_program.bpf['testing_data'].clear() 101 | assert len(bpf_program.bpf['testing_data']) == 0 102 | 103 | bpf_program.load_profiles() 104 | bpf_program.start_monitoring() 105 | 106 | assert len(bpf_program.bpf['profiles']) >= 7 107 | 108 | profiles_after = [bpf_program.get_full_profile(key) for key in profiles_keys] 109 | 110 | for pb, pa in zip(profiles_before, profiles_after): 111 | assert pb == pa 112 | 113 | for n, p in zip(profile_names, profile_locations): 114 | # Make sure we can look up the profile by its key 115 | profile_key = calculate_profile_key(p) 116 | bpf_program.bpf['profiles'][ct.c_uint64(profile_key)] 117 | # Make sure the profile has the correct name associated with it 118 | assert bpf_program.profile_key_to_exe[profile_key] in [n, p] 119 | -------------------------------------------------------------------------------- /ebph/commands/ebph_ps.py: -------------------------------------------------------------------------------- 1 | """ 2 | ebpH (Extended BPF Process Homeostasis) A host-based IDS written in eBPF. 3 | ebpH Copyright (C) 2019-2020 William Findlay 4 | pH Copyright (C) 1999-2003 Anil Somayaji and (C) 2008 Mario Van Velzen 5 | 6 | This program is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with this program. If not, see . 18 | 19 | Implements ebph ps. 20 | 21 | 2020-Jul-13 William Findlay Created this. 22 | """ 23 | 24 | import os 25 | import sys 26 | from argparse import Namespace 27 | from typing import Dict 28 | 29 | import requests 30 | from requests.exceptions import ConnectionError 31 | 32 | from ebph.structs import EBPH_PROFILE_STATUS 33 | from ebph.utils import request_or_die, fail_with 34 | from ebph import defs 35 | 36 | header = False 37 | 38 | def format_comm(comm: str) -> str: 39 | return comm if len(comm) < 20 else ''.join(['...', comm[-17:]]) 40 | 41 | def print_profile_information(profile: Dict) -> None: 42 | comm = format_comm(profile["exe"]) 43 | status = profile['status'] 44 | status = status.split('EBPH_PROFILE_STATUS.')[1].lower() 45 | anomalies = profile['anomaly_count'] 46 | train_count = profile['train_count'] 47 | last_mod_count = profile['last_mod_count'] 48 | # Don't show normal time if we are training 49 | if 'NORMAL' in profile['status'] or 'FROZEN' in profile['status']: 50 | normal_time = profile['normal_time'] 51 | else: 52 | normal_time = '' 53 | 54 | global header 55 | if not header: 56 | print(f"{'COMM':<20} {'STATUS':<16} {'TRAIN_COUNT':>12} {'LAST_MOD':>12} " 57 | f"{'ANOMALIES':>12} {'NORMAL TIME':<16}") 58 | header = True 59 | 60 | print(f"{comm:<20} {status:<16} {train_count:>12} {last_mod_count:>12} " 61 | f"{anomalies:>12} {normal_time:<16}") 62 | 63 | def print_process_information(process: Dict, show_tid: bool) -> None: 64 | # Process stuff 65 | pid = process['pid'] 66 | tid = process['tid'] 67 | total_lfc = process['total_lfc'] 68 | max_lfc = process['max_lfc'] 69 | # Profile stuff 70 | profile = process['profile'] 71 | comm = format_comm(profile["exe"]) 72 | status = profile['status'] 73 | status = status.split('EBPH_PROFILE_STATUS.')[1].lower() 74 | anomalies = profile['anomaly_count'] 75 | train_count = profile['train_count'] 76 | last_mod_count = profile['last_mod_count'] 77 | # Don't show normal time if we are training 78 | if 'NORMAL' in profile['status'] or 'FROZEN' in profile['status']: 79 | normal_time = profile['normal_time'] 80 | else: 81 | normal_time = '' 82 | 83 | if show_tid: 84 | process_part = f"{'PID':<8} {'TID':<8}" 85 | else: 86 | process_part = f"{'PID':<8}" 87 | 88 | global header 89 | if not header: 90 | print(f"{process_part} {'COMM':<20} {'STATUS':<16} {'TRAIN_COUNT':>12} " 91 | f"{'LAST_MOD':>12} {'LOCAL ANOMALIES':>16} {'ANOMALIES':>12} {'NORMAL TIME':<16}") 92 | header = True 93 | 94 | if show_tid: 95 | process_part = f"{pid:<8} {tid:<8}" 96 | else: 97 | process_part = f"{pid:<8}" 98 | 99 | lfc_part = f'{total_lfc} ({max_lfc} max)' 100 | 101 | print(f"{process_part} {comm:<20} {status:<16} {train_count:>12} {last_mod_count:>12} " 102 | f"{lfc_part:>16} {anomalies:>12} {normal_time:<16}") 103 | 104 | 105 | def main(args: Namespace) -> None: 106 | if not (os.geteuid() == 0): 107 | fail_with('This script must be run with root privileges! Exiting.') 108 | 109 | if args.profiles: 110 | res = request_or_die(requests.get, '/profiles', 'Unable to get profiles') 111 | for p in sorted(res.json(), key=lambda p: p['exe']): 112 | print_profile_information(p) 113 | else: 114 | res = request_or_die(requests.get, '/processes', 'Unable to get processes') 115 | processes = res.json() 116 | if not args.threads: 117 | processes = [p for p in processes if p['pid'] == p['tid']] 118 | for p in sorted(processes, key=lambda p: (p['pid'], p['tid'])): 119 | print_process_information(p, args.threads) 120 | -------------------------------------------------------------------------------- /tests/test_bpf_program/test_normal_mode.py: -------------------------------------------------------------------------------- 1 | """ 2 | ebpH (Extended BPF Process Homeostasis) A host-based IDS written in eBPF. 3 | ebpH Copyright (C) 2019-2020 William Findlay 4 | pH Copyright (C) 1999-2003 Anil Somayaji and (C) 2008 Mario Van Velzen 5 | 6 | This program is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with this program. If not, see . 18 | 19 | Test frozen and normal modes. 20 | 21 | 2020-Jul-16 William Findlay Created this. 22 | """ 23 | 24 | import os 25 | import subprocess 26 | import ctypes as ct 27 | import time 28 | 29 | from ebph.structs import EBPH_SETTINGS, EBPH_PROFILE_STATUS 30 | from ebph.bpf_program import BPFProgram 31 | from ebph.utils import which, calculate_profile_key, project_path, ns_to_str 32 | 33 | def test_freeze(bpf_program: BPFProgram, caplog): 34 | """ 35 | Make sure that profiles freeze properly. 36 | """ 37 | hello = project_path('tests/driver/hello') 38 | 39 | # We want normal wait to be pretty high, but not so high that it wraps 40 | # around when the BPF program adds it to the current time. 41 | # 42 | # FIXME: We probably want to add a hard limit for normal wait 43 | # in the BPF program itself, since it probably doesn't make sense 44 | # to have normal wait be super long regardless. 45 | bpf_program.change_setting(EBPH_SETTINGS.NORMAL_WAIT, 2 ** 60) 46 | 47 | # Spawn several hello processes so that we can freeze 48 | for _ in range(50): 49 | subprocess.Popen(hello, stdout=subprocess.DEVNULL).wait() 50 | bpf_program.on_tick() 51 | 52 | assert len(bpf_program.bpf['profiles']) >= 1 53 | 54 | # Fetch the profile for hello 55 | profile_key = calculate_profile_key(hello) 56 | profile = bpf_program.get_profile(profile_key) 57 | 58 | # We should be frozen with zero anomalies 59 | assert profile.status & (EBPH_PROFILE_STATUS.FROZEN | EBPH_PROFILE_STATUS.TRAINING) 60 | assert not (profile.status & EBPH_PROFILE_STATUS.NORMAL) 61 | assert profile.anomaly_count == 0 62 | 63 | def test_normal(bpf_program: BPFProgram, caplog): 64 | """ 65 | Make sure that profiles normalize properly. 66 | """ 67 | hello = project_path('tests/driver/hello') 68 | 69 | # Set normal wait so that we normalize right away 70 | bpf_program.change_setting(EBPH_SETTINGS.NORMAL_WAIT, 0) 71 | 72 | # Spawn several hello processes so that we can freeze AND normalize 73 | for _ in range(50): 74 | subprocess.Popen(hello, stdout=subprocess.DEVNULL).wait() 75 | bpf_program.on_tick() 76 | 77 | assert len(bpf_program.bpf['profiles']) >= 1 78 | 79 | # Fetch the profile for hello 80 | profile_key = calculate_profile_key(hello) 81 | profile = bpf_program.get_profile(profile_key) 82 | 83 | # We should now be normal 84 | assert profile.status & EBPH_PROFILE_STATUS.NORMAL 85 | assert not (profile.status & (EBPH_PROFILE_STATUS.FROZEN | EBPH_PROFILE_STATUS.TRAINING)) 86 | assert profile.anomaly_count == 0 87 | 88 | def test_anomaly(bpf_program: BPFProgram, caplog): 89 | """ 90 | Make sure that anomalies in normal profiles are detected. 91 | """ 92 | hello = project_path('tests/driver/hello') 93 | 94 | # Set normal wait so that we normalize right away 95 | bpf_program.change_setting(EBPH_SETTINGS.NORMAL_WAIT, 0) 96 | 97 | # Spawn several hello processes so that we can freeze AND normalize 98 | for _ in range(50): 99 | subprocess.Popen(hello, stdout=subprocess.DEVNULL).wait() 100 | bpf_program.on_tick() 101 | 102 | assert len(bpf_program.bpf['profiles']) >= 1 103 | 104 | # Fetch the profile for hello 105 | profile_key = calculate_profile_key(hello) 106 | profile = bpf_program.get_profile(profile_key) 107 | 108 | assert profile.status & EBPH_PROFILE_STATUS.NORMAL 109 | assert not (profile.status & (EBPH_PROFILE_STATUS.FROZEN | EBPH_PROFILE_STATUS.TRAINING)) 110 | assert profile.anomaly_count == 0 111 | 112 | # This will cause an anomaly 113 | subprocess.Popen([hello, 'foo'], stdout=subprocess.DEVNULL).wait() 114 | bpf_program.on_tick() 115 | 116 | # Fetch profile again 117 | profile = bpf_program.get_profile(profile_key) 118 | 119 | # We should have seen an anomaly for the write system call 120 | # (as well as some others (e.g. EXIT_GROUP), but don't test for that) 121 | assert profile.anomaly_count > 0 122 | assert 'Anomalous WRITE' in caplog.text 123 | 124 | 125 | -------------------------------------------------------------------------------- /ebph/commands/ebph_admin.py: -------------------------------------------------------------------------------- 1 | """ 2 | ebpH (Extended BPF Process Homeostasis) A host-based IDS written in eBPF. 3 | ebpH Copyright (C) 2019-2020 William Findlay 4 | pH Copyright (C) 1999-2003 Anil Somayaji and (C) 2008 Mario Van Velzen 5 | 6 | This program is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with this program. If not, see . 18 | 19 | Implements ebph admin. 20 | 21 | 2020-Jul-13 William Findlay Created this. 22 | """ 23 | 24 | import os 25 | import sys 26 | from argparse import Namespace 27 | from typing import Dict, Callable 28 | import subprocess 29 | from pprint import pprint 30 | 31 | import requests 32 | from requests.exceptions import ConnectionError 33 | 34 | from ebph.structs import EBPH_PROFILE_STATUS, EBPH_SETTINGS 35 | from ebph import defs 36 | from ebph.utils import fail_with, request_or_die, which 37 | 38 | commands = {} 39 | 40 | def command(name: str) -> Callable: 41 | """ 42 | Register an ebph admin command. 43 | """ 44 | def inner(func: Callable) -> Callable: 45 | def wrapper(args) -> None: 46 | func(args) 47 | 48 | global commands 49 | commands[name] = wrapper 50 | return inner 51 | 52 | @command('start') 53 | def start(args: Namespace) -> None: 54 | try: 55 | subprocess.check_call(['ebphd', 'start']) 56 | except subprocess.CalledProcessError: 57 | fail_with('Failed to start the daemon. Check logs for more info.') 58 | 59 | @command('stop') 60 | def stop(args: Namespace) -> None: 61 | try: 62 | subprocess.check_call(['ebphd', 'stop']) 63 | except subprocess.CalledProcessError: 64 | fail_with('Failed to stop the daemon. Check logs for more info.') 65 | 66 | @command('restart') 67 | def restart(args: Namespace) -> None: 68 | try: 69 | subprocess.check_call(['ebphd', 'restart']) 70 | except subprocess.CalledProcessError: 71 | fail_with('Failed to restart the daemon. Check logs for more info.') 72 | 73 | @command('save') 74 | def save(args: Namespace) -> None: 75 | res = request_or_die(requests.put, f'/profiles/save', 'Unable to save profiles') 76 | body = res.json() 77 | saved = body['saved'] 78 | err = body['error'] 79 | print(f'Saved {saved} profiles, with {err} errors.') 80 | 81 | @command('load') 82 | def load(args: Namespace) -> None: 83 | res = request_or_die(requests.put, f'/profiles/load', 'Unable to load profiles') 84 | body = res.json() 85 | loaded = body['loaded'] 86 | err = body['error'] 87 | print(f'Loaded {loaded} profiles, with {err} errors.') 88 | 89 | @command('status') 90 | def status(args: Namespace) -> None: 91 | res = request_or_die(requests.get, f'/status', 'Unable to get status') 92 | body = res.json() 93 | for k, v in body.items(): 94 | keystr = f'{k}:' 95 | print(f'{keystr:<16} {v}') 96 | 97 | @command('set') 98 | def set(args: Namespace) -> None: 99 | setting = EBPH_SETTINGS(args.category) 100 | value = args.value 101 | res = request_or_die(requests.put, f'/settings/{setting}/{value}', f'Failed to change {setting.name} to {value}') 102 | print(f'Changed {setting.name} to {value}.') 103 | 104 | @command('normalize') 105 | def normalize(args: Namespace) -> None: 106 | if args.profile: 107 | res = request_or_die(requests.put, f'/profiles/exe/{args.profile}/normalize', f'Unable to normalize profile at exe {args.profile}') 108 | elif args.pid: 109 | res = request_or_die(requests.put, f'/processes/pid/{args.pid}/normalize', f'Unable to normalize profile at pid {args.pid}') 110 | else: 111 | raise NotImplementedError('No PID or profile supplied.') 112 | body = res.json() 113 | if args.profile: 114 | print(f'Normalized profile {body["exe"]} successfully.') 115 | else: 116 | print(f'Normalized PID {body["pid"]} ({body["profile"]["exe"]}) successfully.') 117 | 118 | @command('sensitize') 119 | def sensitize(args: Namespace) -> None: 120 | if args.profile: 121 | res = request_or_die(requests.put, f'/profiles/exe/{args.profile}/sensitize', f'Unable to sensitize profile at exe {args.profile}') 122 | elif args.pid: 123 | res = request_or_die(requests.put, f'/processes/pid/{args.pid}/sensitize', f'Unable to sensitize profile at pid {args.pid}') 124 | else: 125 | raise NotImplementedError('No PID or profile supplied.') 126 | body = res.json() 127 | if args.profile: 128 | print(f'Sensitized profile {body["exe"]} successfully.') 129 | else: 130 | print(f'Sensitized PID {body["pid"]} ({body["profile"]["exe"]}) successfully.') 131 | 132 | @command('tolerize') 133 | def tolerize(args: Namespace) -> None: 134 | if args.profile: 135 | res = request_or_die(requests.put, f'/profiles/exe/{args.profile}/tolerize', f'Unable to tolerize profile at exe {args.profile}') 136 | elif args.pid: 137 | res = request_or_die(requests.put, f'/processes/pid/{args.pid}/tolerize', f'Unable to tolerize profile at pid {args.pid}') 138 | else: 139 | raise NotImplementedError('No PID or profile supplied.') 140 | body = res.json() 141 | if args.profile: 142 | print(f'Tolerized profile {body["exe"]} successfully.') 143 | else: 144 | print(f'Tolerized PID {body["pid"]} ({body["profile"]["exe"]}) successfully.') 145 | 146 | 147 | def main(args: Namespace) -> None: 148 | if args.admin_command not in commands.keys(): 149 | fail_with(f'Invalid command: {args.admin_command}!') 150 | if not (os.geteuid() == 0): 151 | fail_with("This script must be run with root privileges! Exiting.") 152 | commands[args.admin_command](args) 153 | -------------------------------------------------------------------------------- /ebph/bpf/bpf_program.h: -------------------------------------------------------------------------------- 1 | /* ebpH (Extended BPF Process Homeostasis) A host-based IDS written in eBPF. 2 | * ebpH Copyright (C) 2019-2020 William Findlay 3 | * pH Copyright (C) 1999-2003 Anil Somayaji and (C) 2008 Mario Van Velzen 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | * 18 | * Data structes for BPF program. 19 | * 20 | * 2020-Jul-13 William Findlay Created this. 21 | * 2020-Jul-17 William Findlay Added support for ALF window. 22 | */ 23 | 24 | #ifndef BPF_PROGRAM_H 25 | #define BPF_PROGRAM_H 26 | 27 | #include 28 | #include 29 | #include 30 | 31 | #include "lsm.h" 32 | 33 | /* ========================================================================= 34 | * Data Structures and Types 35 | * ========================================================================= */ 36 | 37 | /* Keys into settings map */ 38 | enum ebph_setting_key_t { 39 | EBPH_SETTING_MONITORING = 0, 40 | EBPH_SETTING_LOG_SEQUENCES, 41 | EBPH_SETTING_NORMAL_WAIT, 42 | EBPH_SETTING_NORMAL_FACTOR, 43 | EBPH_SETTING_NORMAL_FACTOR_DEN, 44 | EBPH_SETTING_ANOMALY_LIMIT, 45 | EBPH_SETTING_TOLERIZE_LIMIT, 46 | EBPH_SETTING_ENFORCING, 47 | EBPH_SETTING__END, // This must be the last entry 48 | }; 49 | 50 | struct ebph_alf_t { 51 | u8 win[EBPH_LOCALITY_WIN]; 52 | u8 first; 53 | }; 54 | 55 | struct ebph_task_state_t { 56 | u32 pid; 57 | u32 tgid; 58 | u64 profile_key; 59 | char seqstack_top; 60 | u64 count; 61 | // ALF stats 62 | u8 total_lfc; 63 | u8 max_lfc; 64 | }; 65 | 66 | struct ebph_sequence_key_t { 67 | u32 pid; 68 | char seqstack_top; 69 | }; 70 | 71 | struct ebph_sequence_t { 72 | u16 calls[EBPH_SEQLEN]; 73 | }; 74 | 75 | struct ebph_flags_t { 76 | u8 flags[EBPH_LSM_MAX * EBPH_LSM_MAX]; 77 | }; 78 | 79 | /* Current status of the ebpH profile. 80 | * Possible values: training, frozen, and normal. */ 81 | enum ebph_profile_status_t { 82 | EBPH_PROFILE_STATUS_TRAINING = 0x1, 83 | EBPH_PROFILE_STATUS_FROZEN = 0x2, 84 | EBPH_PROFILE_STATUS_NORMAL = 0x4, 85 | }; 86 | 87 | /* An ebpH profile. */ 88 | struct ebph_profile_t { 89 | u8 status; 90 | u64 anomaly_count; 91 | u64 train_count; 92 | u64 last_mod_count; 93 | u64 sequences; 94 | u64 normal_time; 95 | u64 count; 96 | }; 97 | 98 | /* ========================================================================= 99 | * Helper Functions 100 | * ========================================================================= */ 101 | 102 | /* Calculate current epoch time in nanoseconds. */ 103 | static __always_inline u64 ebph_current_time(); 104 | 105 | /* Look up and return a copy of training data for profile @profile_key 106 | * at position {@curr, @prev}. */ 107 | static __always_inline u8 ebph_get_training_data(u64 profile_key, u16 curr, 108 | u16 prev); 109 | 110 | /* Look up and return a copy of testing data for profile @profile_key 111 | * at position {@curr, @prev}. */ 112 | static __always_inline u8 ebph_get_testing_data(u64 profile_key, u16 curr, 113 | u16 prev); 114 | 115 | static __always_inline int ebph_set_training_data(u64 profile_key, u16 curr, 116 | u16 prev, u8 new_flag); 117 | 118 | static __always_inline void ebph_reset_training_data( 119 | u64 profile_key, struct ebph_task_state_t *s, struct ebph_profile_t *p); 120 | 121 | /* Create a new task_state {@pid, @tgid, @profile_key} at @pid. */ 122 | static __always_inline struct ebph_task_state_t *ebph_new_task_state( 123 | u32 pid, u32 tgid, u64 profile_key); 124 | 125 | static __always_inline int ebph_reset_alf(struct ebph_task_state_t *s); 126 | 127 | /* Calculate normal time for a new profile. */ 128 | static __always_inline void ebph_set_normal_time( 129 | struct ebph_profile_t *profile); 130 | 131 | /* Create a new profile at @profile_key and log @pathname association to 132 | * userspace. */ 133 | static __always_inline struct ebph_profile_t *ebph_new_profile( 134 | u64 profile_key, const char *pathname); 135 | 136 | /* Push a new frame onto the sequence stack for @task_state. */ 137 | static __always_inline struct ebph_sequence_t *ebph_push_seq( 138 | struct ebph_task_state_t *task_state); 139 | 140 | /* Pop a frame from the sequence stack for @task_state. */ 141 | static __always_inline struct ebph_sequence_t *ebph_pop_seq( 142 | struct ebph_task_state_t *task_state); 143 | 144 | /* Peek a frame from the sequence stack for @task_state. */ 145 | static __always_inline struct ebph_sequence_t *ebph_peek_seq( 146 | struct ebph_task_state_t *task_state); 147 | 148 | static __always_inline int ebph_test(struct ebph_task_state_t *task_state, 149 | struct ebph_sequence_t *sequence, 150 | bool use_testing_data); 151 | 152 | static __always_inline void ebph_update_training_data( 153 | struct ebph_task_state_t *task_state, struct ebph_sequence_t *sequence); 154 | 155 | static __always_inline void ebph_do_train(struct ebph_task_state_t *task_state, 156 | struct ebph_profile_t *profile, 157 | struct ebph_sequence_t *sequence); 158 | 159 | static __always_inline void ebph_add_anomaly_count( 160 | struct ebph_task_state_t *task_state, struct ebph_profile_t *profile, 161 | int count); 162 | 163 | static __always_inline void ebph_copy_train_to_test(u64 profile_key); 164 | 165 | static __always_inline void ebph_start_normal( 166 | u64 profile_key, struct ebph_task_state_t *task_state, 167 | struct ebph_profile_t *profile); 168 | 169 | static __always_inline void ebph_stop_normal( 170 | u64 profile_key, struct ebph_task_state_t *task_state, 171 | struct ebph_profile_t *profile); 172 | 173 | static __always_inline void ebph_do_normal(struct ebph_task_state_t *task_state, 174 | struct ebph_profile_t *profile, 175 | struct ebph_sequence_t *sequence); 176 | 177 | /* Process a new syscall. */ 178 | static __always_inline void ebph_handle_syscall( 179 | struct ebph_task_state_t *task_state, u16 syscall); 180 | 181 | #endif /* ifndef BPF_PROGRAM_H */ 182 | -------------------------------------------------------------------------------- /ebph/libebph/include/folly/tracing/StaticTracepoint-ELF.h: -------------------------------------------------------------------------------- 1 | /* 2 | * ebpH Copyright 2017 Facebook, Inc. 3 | * pH Copyright (C) 1999-2003 Anil Somayaji and (C) 2008 Mario Van Velzen 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | #pragma once 19 | 20 | // Default constraint for the probe arguments as operands. 21 | #ifndef FOLLY_SDT_ARG_CONSTRAINT 22 | #if defined(__powerpc64__) || defined(__powerpc__) 23 | #define FOLLY_SDT_ARG_CONSTRAINT "nZr" 24 | #else 25 | #define FOLLY_SDT_ARG_CONSTRAINT "nor" 26 | #endif 27 | #endif 28 | 29 | // Instruction to emit for the probe. 30 | #define FOLLY_SDT_NOP nop 31 | 32 | // Note section properties. 33 | #define FOLLY_SDT_NOTE_NAME "stapsdt" 34 | #define FOLLY_SDT_NOTE_TYPE 3 35 | 36 | // Size of address depending on platform. 37 | #ifdef __LP64__ 38 | #define FOLLY_SDT_ASM_ADDR .8byte 39 | #else 40 | #define FOLLY_SDT_ASM_ADDR .4byte 41 | #endif 42 | 43 | // Assembler helper Macros. 44 | #define FOLLY_SDT_S(x) #x 45 | #define FOLLY_SDT_ASM_1(x) FOLLY_SDT_S(x) "\n" 46 | #define FOLLY_SDT_ASM_2(a, b) FOLLY_SDT_S(a) "," FOLLY_SDT_S(b) "\n" 47 | #define FOLLY_SDT_ASM_3(a, b, c) FOLLY_SDT_S(a) "," FOLLY_SDT_S(b) "," \ 48 | FOLLY_SDT_S(c) "\n" 49 | #define FOLLY_SDT_ASM_STRING(x) FOLLY_SDT_ASM_1(.asciz FOLLY_SDT_S(x)) 50 | 51 | // Helper to determine the size of an argument. 52 | #define FOLLY_SDT_ISARRAY(x) (__builtin_classify_type(x) == 14) 53 | #define FOLLY_SDT_ARGSIZE(x) (FOLLY_SDT_ISARRAY(x) ? sizeof(void*) : sizeof(x)) 54 | 55 | // Format of each probe arguments as operand. 56 | // Size of the argument tagged with FOLLY_SDT_Sn, with "n" constraint. 57 | // Value of the argument tagged with FOLLY_SDT_An, with configured constraint. 58 | #define FOLLY_SDT_ARG(n, x) \ 59 | [FOLLY_SDT_S##n] "n" ((size_t)FOLLY_SDT_ARGSIZE(x)), \ 60 | [FOLLY_SDT_A##n] FOLLY_SDT_ARG_CONSTRAINT (x) 61 | 62 | // Templates to append arguments as operands. 63 | #define FOLLY_SDT_OPERANDS_0() [__sdt_dummy] "g" (0) 64 | #define FOLLY_SDT_OPERANDS_1(_1) FOLLY_SDT_ARG(1, _1) 65 | #define FOLLY_SDT_OPERANDS_2(_1, _2) \ 66 | FOLLY_SDT_OPERANDS_1(_1), FOLLY_SDT_ARG(2, _2) 67 | #define FOLLY_SDT_OPERANDS_3(_1, _2, _3) \ 68 | FOLLY_SDT_OPERANDS_2(_1, _2), FOLLY_SDT_ARG(3, _3) 69 | #define FOLLY_SDT_OPERANDS_4(_1, _2, _3, _4) \ 70 | FOLLY_SDT_OPERANDS_3(_1, _2, _3), FOLLY_SDT_ARG(4, _4) 71 | #define FOLLY_SDT_OPERANDS_5(_1, _2, _3, _4, _5) \ 72 | FOLLY_SDT_OPERANDS_4(_1, _2, _3, _4), FOLLY_SDT_ARG(5, _5) 73 | #define FOLLY_SDT_OPERANDS_6(_1, _2, _3, _4, _5, _6) \ 74 | FOLLY_SDT_OPERANDS_5(_1, _2, _3, _4, _5), FOLLY_SDT_ARG(6, _6) 75 | #define FOLLY_SDT_OPERANDS_7(_1, _2, _3, _4, _5, _6, _7) \ 76 | FOLLY_SDT_OPERANDS_6(_1, _2, _3, _4, _5, _6), FOLLY_SDT_ARG(7, _7) 77 | #define FOLLY_SDT_OPERANDS_8(_1, _2, _3, _4, _5, _6, _7, _8) \ 78 | FOLLY_SDT_OPERANDS_7(_1, _2, _3, _4, _5, _6, _7), FOLLY_SDT_ARG(8, _8) 79 | 80 | // Templates to reference the arguments from operands in note section. 81 | #if defined(__powerpc64__ ) || defined(__powerpc__) 82 | #define FOLLY_SDT_ARGTMPL(id) %I[id]%[id] 83 | #elif defined(__i386__) 84 | #define FOLLY_SDT_ARGTMPL(id) %w[id] 85 | #else 86 | #define FOLLY_SDT_ARGTMPL(id) %[id] 87 | #endif 88 | #define FOLLY_SDT_ARGFMT(no) %n[FOLLY_SDT_S##no]@FOLLY_SDT_ARGTMPL(FOLLY_SDT_A##no) 89 | #define FOLLY_SDT_ARG_TEMPLATE_0 /*No arguments*/ 90 | #define FOLLY_SDT_ARG_TEMPLATE_1 FOLLY_SDT_ARGFMT(1) 91 | #define FOLLY_SDT_ARG_TEMPLATE_2 FOLLY_SDT_ARG_TEMPLATE_1 FOLLY_SDT_ARGFMT(2) 92 | #define FOLLY_SDT_ARG_TEMPLATE_3 FOLLY_SDT_ARG_TEMPLATE_2 FOLLY_SDT_ARGFMT(3) 93 | #define FOLLY_SDT_ARG_TEMPLATE_4 FOLLY_SDT_ARG_TEMPLATE_3 FOLLY_SDT_ARGFMT(4) 94 | #define FOLLY_SDT_ARG_TEMPLATE_5 FOLLY_SDT_ARG_TEMPLATE_4 FOLLY_SDT_ARGFMT(5) 95 | #define FOLLY_SDT_ARG_TEMPLATE_6 FOLLY_SDT_ARG_TEMPLATE_5 FOLLY_SDT_ARGFMT(6) 96 | #define FOLLY_SDT_ARG_TEMPLATE_7 FOLLY_SDT_ARG_TEMPLATE_6 FOLLY_SDT_ARGFMT(7) 97 | #define FOLLY_SDT_ARG_TEMPLATE_8 FOLLY_SDT_ARG_TEMPLATE_7 FOLLY_SDT_ARGFMT(8) 98 | 99 | // Structure of note section for the probe. 100 | #define FOLLY_SDT_NOTE_CONTENT(provider, name, arg_template) \ 101 | FOLLY_SDT_ASM_1(990: FOLLY_SDT_NOP) \ 102 | FOLLY_SDT_ASM_3( .pushsection .note.stapsdt,"","note") \ 103 | FOLLY_SDT_ASM_1( .balign 4) \ 104 | FOLLY_SDT_ASM_3( .4byte 992f-991f, 994f-993f, FOLLY_SDT_NOTE_TYPE) \ 105 | FOLLY_SDT_ASM_1(991: .asciz FOLLY_SDT_NOTE_NAME) \ 106 | FOLLY_SDT_ASM_1(992: .balign 4) \ 107 | FOLLY_SDT_ASM_1(993: FOLLY_SDT_ASM_ADDR 990b) \ 108 | FOLLY_SDT_ASM_1( FOLLY_SDT_ASM_ADDR 0) /*Reserved for Semaphore address*/\ 109 | FOLLY_SDT_ASM_1( FOLLY_SDT_ASM_ADDR 0) /*Reserved for Semaphore name*/ \ 110 | FOLLY_SDT_ASM_STRING(provider) \ 111 | FOLLY_SDT_ASM_STRING(name) \ 112 | FOLLY_SDT_ASM_STRING(arg_template) \ 113 | FOLLY_SDT_ASM_1(994: .balign 4) \ 114 | FOLLY_SDT_ASM_1( .popsection) 115 | 116 | // Main probe Macro. 117 | #define FOLLY_SDT_PROBE(provider, name, n, arglist) \ 118 | __asm__ __volatile__ ( \ 119 | FOLLY_SDT_NOTE_CONTENT(provider, name, FOLLY_SDT_ARG_TEMPLATE_##n) \ 120 | :: FOLLY_SDT_OPERANDS_##n arglist \ 121 | ) \ 122 | 123 | // Helper Macros to handle variadic arguments. 124 | #define FOLLY_SDT_NARG_(_0, _1, _2, _3, _4, _5, _6, _7, _8, N, ...) N 125 | #define FOLLY_SDT_NARG(...) \ 126 | FOLLY_SDT_NARG_(__VA_ARGS__, 8, 7, 6, 5, 4, 3, 2, 1, 0) 127 | #define FOLLY_SDT_PROBE_N(provider, name, N, ...) \ 128 | FOLLY_SDT_PROBE(provider, name, N, (__VA_ARGS__)) 129 | -------------------------------------------------------------------------------- /ebph/ebphd.py: -------------------------------------------------------------------------------- 1 | """ 2 | ebpH (Extended BPF Process Homeostasis) A host-based IDS written in eBPF. 3 | ebpH Copyright (C) 2019-2020 William Findlay 4 | pH Copyright (C) 1999-2003 Anil Somayaji and (C) 2008 Mario Van Velzen 5 | 6 | This program is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with this program. If not, see . 18 | 19 | Main ebpH daemon. 20 | 21 | 2020-Jul-13 William Findlay Created this. 22 | """ 23 | 24 | import sys 25 | import time 26 | import argparse 27 | import os 28 | import signal 29 | import threading 30 | from typing import NoReturn, List 31 | 32 | 33 | from ebph.logger import get_logger 34 | from ebph.daemon_mixin import DaemonMixin 35 | from ebph import defs 36 | 37 | signal.signal(signal.SIGTERM, lambda _, __: sys.exit()) 38 | signal.signal(signal.SIGINT, lambda _, __: sys.exit()) 39 | 40 | class EBPHDaemon(DaemonMixin): 41 | """ 42 | EBPHDaemon 43 | 44 | This class provides the logic for the daemon and exposes methods for interacting with the 45 | underlying BPFProgram class. 46 | """ 47 | def __init__(self, args: argparse.Namespace) -> 'EBPHDaemon': 48 | # BPF Program 49 | self.bpf_program = None 50 | 51 | self.debug = args.debug 52 | self.log_sequences = args.log_sequences 53 | self.auto_save = not args.nosave 54 | self.auto_load = not args.noload 55 | 56 | def tick(self) -> None: 57 | """ 58 | Invoked on every tick in the main event loop. 59 | """ 60 | self.bpf_program.on_tick() 61 | 62 | def loop_forever(self) -> NoReturn: 63 | """ 64 | Main daemon setup + event loop. 65 | """ 66 | self.bind_socket() 67 | 68 | self._init_bpf_program() 69 | 70 | bpf_thread = threading.Thread(target=self._bpf_work_loop) 71 | bpf_thread.daemon = True 72 | bpf_thread.start() 73 | 74 | from ebph.api import API 75 | logger.info('Starting ebpH server...') 76 | API.connect_bpf_program(self.bpf_program) 77 | API.serve_forever() 78 | 79 | def stop_daemon(self, in_restart: bool = False) -> None: 80 | """ 81 | Stop the daemon. Overloaded from base daemon class to print log info. 82 | """ 83 | logger.info("Stopping ebpH daemon...") 84 | super().stop_daemon(in_restart=in_restart) 85 | 86 | def _init_bpf_program(self) -> None: 87 | assert self.bpf_program is None 88 | from ebph.bpf_program import BPFProgram 89 | self.bpf_program = BPFProgram(debug=self.debug, 90 | log_sequences=self.log_sequences, auto_save=self.auto_save, 91 | auto_load=self.auto_load) 92 | global bpf_program 93 | bpf_program = self.bpf_program 94 | 95 | def _bpf_work_loop(self) -> NoReturn: 96 | while 1: 97 | self.tick() 98 | time.sleep(defs.TICK_SLEEP) 99 | 100 | 101 | OPERATIONS = ["start", "stop", "restart"] 102 | 103 | 104 | def parse_args(args: List[str] = []) -> argparse.Namespace: 105 | parser = argparse.ArgumentParser(description="Daemon script for ebpH.", 106 | prog="ebphd", #epilog="Configuration file can be found at /etc/ebpH/ebpH.cfg", 107 | formatter_class=argparse.RawDescriptionHelpFormatter) 108 | 109 | parser.add_argument('operation', metavar="Operation", type=lambda s: str(s).lower(), 110 | choices=OPERATIONS, nargs='?', 111 | help=f"Operation you want to perform. Not allowed with --nodaemon. " 112 | f"Choices are: {', '.join(OPERATIONS)}.") 113 | parser.add_argument('--nodaemon', dest='nodaemon', action='store_true', 114 | help=f"Run this as a foreground process instead of a daemon.") 115 | parser.add_argument('--nolog', dest='nolog', action='store_true', 116 | help=f"Write to stderr instead of logfile. In daemon mode, " 117 | "this will simply not write any logging information.") 118 | parser.add_argument('--logseq', dest='log_sequences', action='store_true', 119 | help=f"Log new sequences. WARNING: This option can use a lot of resources if profiles are not stable!") 120 | parser.add_argument('--nosave', dest='nosave', action='store_true', 121 | help=f"Don't save profiles on exit.") 122 | parser.add_argument('--noload', dest='noload', action='store_true', 123 | help=f"Don't load profiles.") 124 | parser.add_argument('--debug', action='store_true', 125 | help=f"Run in debug mode. Side effect: sets verbosity level to debug regardless of what is set in configuration options.") 126 | # Quick testing mode. This option sets --nodaemon --nolog --nosave --noload flags. 127 | parser.add_argument('--testing', action='store_true', 128 | help=argparse.SUPPRESS) 129 | 130 | args = parser.parse_args(args) 131 | 132 | # Quick and dirty testing mode 133 | if args.testing: 134 | args.nodaemon = True 135 | args.nolog = True 136 | args.nosave = True 137 | args.noload = True 138 | 139 | # Check for root 140 | if not (os.geteuid() == 0): 141 | parser.error("This script must be run with root privileges! Exiting.") 142 | 143 | # Error checking 144 | if args.nodaemon and args.operation: 145 | parser.error("You cannot specify an operation with the --nodaemon flag.") 146 | if not (args.nodaemon or args.operation): 147 | parser.error("You must either specify an operation or set the --nodaemon flag.") 148 | 149 | return args 150 | 151 | 152 | def main(sys_args: List[str] = sys.argv[1:]) -> NoReturn: 153 | args = parse_args(sys_args) 154 | defs.init(args) 155 | 156 | global logger 157 | logger = get_logger() 158 | 159 | ebphd = EBPHDaemon(args) 160 | 161 | if args.operation == "start": 162 | try: 163 | ebphd.start_daemon() 164 | except Exception as e: 165 | logger.error('Unable to start daemon', exc_info=e) 166 | sys.exit(-1) 167 | elif args.operation == "stop": 168 | try: 169 | ebphd.stop_daemon() 170 | except Exception as e: 171 | logger.error('Unable to stop daemon', exc_info=e) 172 | sys.exit(-1) 173 | elif args.operation == "restart": 174 | try: 175 | ebphd.restart_daemon() 176 | except Exception as e: 177 | logger.error('Unable to restart daemon', exc_info=e) 178 | sys.exit(-1) 179 | elif args.nodaemon: 180 | ebphd.loop_forever() 181 | -------------------------------------------------------------------------------- /ebph/logger.py: -------------------------------------------------------------------------------- 1 | """ 2 | ebpH (Extended BPF Process Homeostasis) A host-based IDS written in eBPF. 3 | ebpH Copyright (C) 2019-2020 William Findlay 4 | pH Copyright (C) 1999-2003 Anil Somayaji and (C) 2008 Mario Van Velzen 5 | 6 | This program is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with this program. If not, see . 18 | 19 | Provides logging capabilities to ebphd. 20 | 21 | 2020-Jul-13 William Findlay Created this. 22 | """ 23 | 24 | import os, sys 25 | import stat 26 | import time 27 | import re 28 | import gzip 29 | import datetime as dt 30 | from argparse import Namespace 31 | import logging 32 | from logging import handlers as handlers 33 | 34 | from colorama import Fore, Back, Style 35 | 36 | from ebph.utils import read_chunks 37 | from ebph import defs 38 | 39 | class EBPHLoggerClass(logging.getLoggerClass()): 40 | """ 41 | Custom logger class that allows for the logging of audit messages. 42 | """ 43 | AUDIT = logging.WARN - 5 44 | SEQUENCE = logging.INFO - 5 45 | 46 | def __init__(self, name, level: int = logging.NOTSET) -> 'EBPHLoggerClass': 47 | super().__init__(name, level) 48 | 49 | logging.addLevelName(EBPHLoggerClass.AUDIT, "AUDIT") 50 | logging.addLevelName(EBPHLoggerClass.SEQUENCE, "NEWSEQ") 51 | 52 | def audit(self, msg: str, *args, **kwargs) -> None: 53 | """ 54 | Write a policy message to logs. 55 | This should be used to inform the user about policy decisions/enforcement. 56 | """ 57 | if self.isEnabledFor(EBPHLoggerClass.AUDIT): 58 | self._log(EBPHLoggerClass.AUDIT, msg, args, **kwargs) 59 | 60 | def sequence(self, msg: str, *args, **kwargs) -> None: 61 | """ 62 | Write a policy message to logs. 63 | This should be used to inform the user about policy decisions/enforcement. 64 | """ 65 | if self.isEnabledFor(EBPHLoggerClass.SEQUENCE): 66 | self._log(EBPHLoggerClass.SEQUENCE, msg, args, **kwargs) 67 | 68 | logging.setLoggerClass(EBPHLoggerClass) 69 | 70 | class EBPHRotatingFileHandler(handlers.TimedRotatingFileHandler): 71 | """ 72 | Rotates log files either when they have reached the specified 73 | time or when they have reached the specified size. Keeps 74 | backupCount many backups. 75 | 76 | This class uses camel casing because that's what the logging module uses. 77 | """ 78 | def __init__(self, filename, maxBytes=0, backupCount=0, encoding=None, 79 | delay=0, when='h', interval=1, utc=False): 80 | handlers.TimedRotatingFileHandler.__init__(self, filename, when, 81 | interval, backupCount, encoding, delay, utc) 82 | self.maxBytes = maxBytes 83 | self.suffix = "%Y-%m-%d_%H-%M-%S" 84 | 85 | def rotator(source: str, dest: str) -> None: 86 | dest = f'{dest}.gz' 87 | try: 88 | os.unlink(dest) 89 | except FileNotFoundError: 90 | pass 91 | with open(source, 'r') as sf, gzip.open(dest ,'ab') as df: 92 | for chunk in read_chunks(sf): 93 | df.write(chunk.encode('utf-8')) 94 | try: 95 | os.unlink(source) 96 | except FileNotFoundError: 97 | pass 98 | 99 | self.rotator=rotator 100 | 101 | def shouldRollover(self, record: logging.LogRecord) -> int: 102 | """ 103 | Overload shouldRollover method from base class. 104 | 105 | Does file exceed size limit or have we exceeded time limit? 106 | """ 107 | if self.stream is None: 108 | self.stream = self._open() 109 | if self.maxBytes > 0: 110 | msg = f'{self.format(record)}\n' 111 | self.stream.seek(0, 2) 112 | if self.stream.tell() + len(msg) >= self.maxBytes: 113 | return 1 114 | t = int(time.time()) 115 | if t >= self.rolloverAt: 116 | return 1 117 | return 0 118 | 119 | class EBPHFormatter(logging.Formatter): 120 | converter=dt.datetime.fromtimestamp 121 | def formatTime(self, record, datefmt=None): 122 | ct = self.converter(record.created) 123 | if datefmt: 124 | s = ct.strftime(datefmt) 125 | else: 126 | t = ct.strftime("%Y-%m-%d %H:%M:%S") 127 | s = "%s.%03d" % (t, record.msecs) 128 | return s 129 | 130 | def format(self, record): 131 | record.levelname = record.levelname.lower() 132 | return logging.Formatter.format(self, record) 133 | 134 | class EBPHColoredFormatter(EBPHFormatter): 135 | def format(self, record): 136 | formatted = EBPHFormatter.format(self, record) 137 | return color_log(formatted) 138 | 139 | def setup_logger(args: Namespace) -> None: 140 | """ 141 | Perform (most) logging setup. This function should be called 142 | from defs.init(). 143 | """ 144 | # Make logfile parent directory 145 | os.makedirs(os.path.dirname(defs.LOGFILE), exist_ok=True) 146 | 147 | # Configure logging 148 | formatter_class = EBPHColoredFormatter if args.nolog else EBPHFormatter 149 | formatter = formatter_class('[%(asctime)s] [%(name)s] [%(levelname)s] %(message)s') 150 | 151 | logger = get_logger() 152 | if args.debug: 153 | logger.setLevel(logging.DEBUG) 154 | else: 155 | logger.setLevel(EBPHLoggerClass.SEQUENCE) 156 | 157 | # Create and add handler 158 | if args.nolog: 159 | # Stream handler if we are writing to stdout 160 | handler = logging.StreamHandler() 161 | else: 162 | # Rotating handler if we are writing to log files 163 | # TODO: change this to allow configurable sizes, times, backup counts 164 | handler = EBPHRotatingFileHandler( 165 | defs.LOGFILE, 166 | maxBytes=(1024**3), 167 | backupCount=12, 168 | when='w0', 169 | interval=4 170 | ) 171 | handler.setFormatter(formatter) 172 | logger.addHandler(handler) 173 | 174 | # A little debug message to tell us the logger has started 175 | logger.debug('Logging initialized.') 176 | 177 | def get_logger(name='ebphd') -> logging.Logger: 178 | """ 179 | Get the ebpH logger. 180 | """ 181 | return logging.getLogger(name) 182 | 183 | def color_time(time: str): 184 | return Fore.GREEN + time 185 | 186 | def color_logger(logger: str): 187 | return Fore.LIGHTBLACK_EX + logger 188 | 189 | def color_category(category: str): 190 | if 'info' in category: 191 | color = Fore.BLUE 192 | elif 'debug' in category: 193 | color = Fore.CYAN 194 | elif 'warn' in category: 195 | color = Fore.YELLOW 196 | elif 'audit' in category: 197 | color = Fore.LIGHTYELLOW_EX 198 | elif 'newseq' in category: 199 | color = Fore.LIGHTMAGENTA_EX 200 | elif 'error' in category: 201 | color = Fore.RED 202 | else: 203 | color = Fore.RESET 204 | return color + category 205 | 206 | line_re = re.compile(r'(\[.*\]\s+)(\[.*\]\s+)(\[.*\])(.*)') 207 | def color_log(line: str): 208 | match = line_re.match(line) 209 | if not match: 210 | raise IOError('Log message does not match pattern!') 211 | line = color_time(match[1]) + color_logger(match[2]) + color_category(match[3]) + Style.RESET_ALL + match[4] 212 | return line 213 | -------------------------------------------------------------------------------- /bin/ebph: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | 3 | """ 4 | ebpH (Extended BPF Process Homeostasis) A host-based IDS written in eBPF. 5 | ebpH Copyright (C) 2019-2020 William Findlay 6 | pH Copyright (C) 1999-2003 Anil Somayaji and (C) 2008 Mario Van Velzen 7 | 8 | This program is free software: you can redistribute it and/or modify 9 | it under the terms of the GNU General Public License as published by 10 | the Free Software Foundation, either version 3 of the License, or 11 | (at your option) any later version. 12 | 13 | This program is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | 18 | You should have received a copy of the GNU General Public License 19 | along with this program. If not, see . 20 | 21 | Provides a "one-executable-multiple-commands" interface for interacting 22 | with the ebpH daemon using its API. 23 | 24 | 2020-Jul-13 William Findlay Created this. 25 | """ 26 | 27 | import os 28 | import sys 29 | import argparse 30 | from typing import Callable, List, Union 31 | from pprint import pformat 32 | 33 | from ebph.structs import EBPH_SETTINGS 34 | 35 | commands = {} 36 | 37 | 38 | def command(name: str): 39 | def inner(func: Callable): 40 | def wrapper(args): 41 | func(args) 42 | 43 | global commands 44 | commands[name] = wrapper 45 | return inner 46 | 47 | 48 | class MappingAction(argparse.Action): 49 | def __init__(self, option_strings, dest, mapping, **kwargs): 50 | self.mapping = mapping 51 | super(MappingAction, self).__init__(option_strings, dest, **kwargs) 52 | 53 | def __call__(self, parser, namespace, values, option_string=None): 54 | values = self.mapping.get(values, None) 55 | setattr(namespace, self.dest, values) 56 | 57 | 58 | def option_value(value): 59 | if value.lower() in ['t', 'true']: 60 | ivalue = 1 61 | elif value.lower() in ['f', 'false']: 62 | ivalue = 0 63 | else: 64 | try: 65 | ivalue = int(value) 66 | except Exception: 67 | raise argparse.ArgumentTypeError("Invalid option value") 68 | if ivalue < 0: 69 | raise argparse.ArgumentTypeError("Option values must not be negative") 70 | return ivalue 71 | 72 | 73 | def parse_args(sys_args: List[str]): 74 | description = """ 75 | Issue commands to the ebpH daemon. 76 | The ebpH daemon (ebphd) must be running in order to run this software. 77 | """ 78 | 79 | epilog = """ 80 | """ 81 | 82 | parser = argparse.ArgumentParser( 83 | description=description, 84 | epilog=epilog, 85 | prog="ebph", 86 | formatter_class=argparse.RawTextHelpFormatter, 87 | ) 88 | 89 | commands = parser.add_subparsers( 90 | dest='command', metavar='command', required=True 91 | ) 92 | 93 | # ebpH ps 94 | ps = commands.add_parser( 95 | 'ps', help='List traced processes, threads, or profiles.' 96 | ) 97 | 98 | process_or_thread = ps.add_mutually_exclusive_group() 99 | process_or_thread.add_argument( 100 | '-t', 101 | '--threads', 102 | action='store_true', 103 | help=f"Print all threads instead of just thread group leader.", 104 | ) 105 | 106 | process_or_thread.add_argument( 107 | '-p', 108 | '--profiles', 109 | action='store_true', 110 | help=f"Print all profiles instead of active processes.", 111 | ) 112 | 113 | # ebpH admin 114 | admin = commands.add_parser( 115 | 'admin', help='Issue commands to the ebpH daemon.' 116 | ) 117 | 118 | admin_commands = admin.add_subparsers( 119 | dest='admin_command', metavar='subcommand', required=True 120 | ) 121 | 122 | start = admin_commands.add_parser( 123 | 'start', 124 | help='Start the daemon. You must ' 125 | 'have root privileges to do this. For more advanced options, ' 126 | 'consider using ebphd instead.', 127 | ) 128 | 129 | stop = admin_commands.add_parser( 130 | 'stop', 131 | help='Stop the daemon. You must ' 132 | 'have root privileges to do this. For more advanced options, ' 133 | 'consider using ebphd instead.', 134 | ) 135 | 136 | restart = admin_commands.add_parser( 137 | 'restart', 138 | help='Restart the daemon. You must ' 139 | 'have root privileges to do this. For more advanced options, ' 140 | 'consider using ebphd instead.', 141 | ) 142 | 143 | save = admin_commands.add_parser( 144 | 'save', 145 | help='Force ebpH to save all profiles to disk.', 146 | ) 147 | 148 | load = admin_commands.add_parser( 149 | 'load', 150 | help='Force ebpH to load all profiles from disk. Warning: This will overwrite your currently active profiles.', 151 | ) 152 | 153 | status = admin_commands.add_parser( 154 | 'status', 155 | help='Print basic information about ebpH\'s current state.', 156 | ) 157 | 158 | _set = admin_commands.add_parser('set', help='Change ebpH options.') 159 | 160 | set_categories = { 161 | 'monitoring': EBPH_SETTINGS.MONITORING, 162 | 'log-sequences': EBPH_SETTINGS.LOG_SEQUENCES, 163 | 'normal-wait': EBPH_SETTINGS.NORMAL_WAIT, 164 | 'normal-factor': EBPH_SETTINGS.NORMAL_FACTOR, 165 | 'normal-factor-den': EBPH_SETTINGS.NORMAL_FACTOR_DEN, 166 | 'anomaly-limit': EBPH_SETTINGS.ANOMALY_LIMIT, 167 | 'tolerize-limit': EBPH_SETTINGS.TOLERIZE_LIMIT, 168 | 'enforcing': EBPH_SETTINGS.ENFORCING, 169 | } 170 | 171 | _set.add_argument( 172 | 'category', 173 | metavar='category', 174 | choices=set_categories.keys(), 175 | action=MappingAction, 176 | mapping=set_categories, 177 | help='Option to change. Choices include: { %s }' 178 | % (', '.join(set_categories.keys())), 179 | ) 180 | 181 | _set.add_argument( 182 | 'value', 183 | type=option_value, 184 | help='Value to which the setting will be ' 185 | 'changed. For T/F values, any positive integer is true and 0 is false.', 186 | ) 187 | 188 | normalize = admin_commands.add_parser( 189 | 'normalize', 190 | help='Start normal monitoring in a process or profile.', 191 | ) 192 | targets = normalize.add_mutually_exclusive_group(required=True) 193 | targets.add_argument('--profile', type=str, help='String representing the profile to normalize.') 194 | targets.add_argument('--pid', type=int, help='Integer representing the PID of the process to normalize.') 195 | 196 | sensitize = admin_commands.add_parser( 197 | 'sensitize', 198 | help='Forget recently learned behavior in a process or profile.', 199 | ) 200 | targets = sensitize.add_mutually_exclusive_group(required=True) 201 | targets.add_argument('--profile', type=str, help='String representing the profile to sensitize.') 202 | targets.add_argument('--pid', type=int, help='Integer representing the PID of the process to sensitize.') 203 | 204 | tolerize = admin_commands.add_parser( 205 | 'tolerize', 206 | help='Accept recently learned behavior in a process or profile.', 207 | ) 208 | targets = tolerize.add_mutually_exclusive_group(required=True) 209 | targets.add_argument('--profile', type=str, help='String representing the profile to tolerize.') 210 | targets.add_argument('--pid', type=int, help='Integer representing the PID of the process to tolerize.') 211 | 212 | logs = commands.add_parser('logs', help='Interact with the ebpH logs.') 213 | 214 | return parser.parse_args(sys_args) 215 | 216 | 217 | @command('ps') 218 | def ps(args): 219 | from ebph.commands.ebph_ps import main 220 | 221 | main(args) 222 | 223 | 224 | @command('admin') 225 | def admin(args): 226 | from ebph.commands.ebph_admin import main 227 | 228 | main(args) 229 | 230 | @command('logs') 231 | def logs(args): 232 | from ebph.commands.ebph_logs import main 233 | 234 | main(args) 235 | 236 | 237 | def main(sys_args: List[str] = sys.argv[1:]): 238 | args = parse_args(sys_args) 239 | commands[args.command](args) 240 | 241 | 242 | if __name__ == '__main__': 243 | main() 244 | -------------------------------------------------------------------------------- /ebph/structs.py: -------------------------------------------------------------------------------- 1 | """ 2 | ebpH (Extended BPF Process Homeostasis) A host-based IDS written in eBPF. 3 | ebpH Copyright (C) 2019-2020 William Findlay 4 | pH Copyright (C) 1999-2003 Anil Somayaji and (C) 2008 Mario Van Velzen 5 | 6 | This program is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with this program. If not, see . 18 | 19 | Defines several structs and enums for interacting with the BPF program. 20 | 21 | 2020-Jul-13 William Findlay Created this. 22 | """ 23 | 24 | import os 25 | import sys 26 | from pprint import pformat 27 | import ctypes as ct 28 | from enum import IntEnum, IntFlag, unique, auto 29 | from typing import List, Dict 30 | 31 | from bcc import BPF 32 | 33 | from ebph.logger import get_logger 34 | from ebph import defs 35 | 36 | logger = get_logger() 37 | 38 | 39 | def calculate_profile_magic() -> int: 40 | """ 41 | Calculate the magic number that corresponds to ebpH profiles. 42 | """ 43 | from hashlib import sha256 44 | from ebph.version import __version__ 45 | 46 | # take x.x part of version 47 | version = '.'.join(__version__.split('.')[:2]).encode('ascii') 48 | 49 | return int(sha256(version).hexdigest(), 16) & 0xFFFF_FFFF_FFFF_FFFF 50 | 51 | 52 | @unique 53 | class EBPH_PROFILE_STATUS(IntFlag): 54 | """ 55 | The status of an ebpH profile. 56 | Warning: Keep in sync with BPF program. 57 | """ 58 | TRAINING = 0x1 59 | FROZEN = 0x2 60 | NORMAL = 0x4 61 | 62 | 63 | @unique 64 | class EBPH_SETTINGS(IntEnum): 65 | """ 66 | The various settings that may be changed within the BPF program. 67 | Warning: Keep in sync with BPF program. 68 | """ 69 | MONITORING = 0 70 | LOG_SEQUENCES = auto() 71 | NORMAL_WAIT = auto() 72 | NORMAL_FACTOR = auto() 73 | NORMAL_FACTOR_DEN = auto() 74 | ANOMALY_LIMIT = auto() 75 | TOLERIZE_LIMIT = auto() 76 | ENFORCING = auto() 77 | 78 | @unique 79 | class EBPH_LSM(IntEnum): 80 | """ 81 | The various LSM programs that ebpH tracks. 82 | Warning: Keep in sync with BPF program. 83 | """ 84 | BPRM_CHECK_SECURITY = 0 85 | TASK_ALLOC = auto() 86 | TASK_FREE = auto() 87 | TASK_SETPGID = auto() 88 | TASK_GETPGID = auto() 89 | TASK_GETSID = auto() 90 | TASK_SETNICE = auto() 91 | TASK_SETIOPRIO = auto() 92 | TASK_GETIOPRIO = auto() 93 | TASK_PRLIMIT = auto() 94 | TASK_SETRLIMIT = auto() 95 | TASK_SETSCHEDULER = auto() 96 | TASK_GETSCHEDULER = auto() 97 | TASK_MOVEMEMORY = auto() 98 | TASK_KILL = auto() # TODO: split this into coarse signal categories 99 | TASK_PRCTL = auto() 100 | SB_STATFS = auto() 101 | SB_MOUNT = auto() 102 | SB_REMOUNT = auto() 103 | SB_UMOUNT = auto() 104 | SB_PIVOTROOT = auto() 105 | MOVE_MOUNT = auto() 106 | INODE_CREATE = auto() 107 | INODE_LINK = auto() 108 | INODE_SYMLINK = auto() 109 | INODE_MKDIR = auto() 110 | INODE_RMDIR = auto() 111 | INODE_MKNOD = auto() 112 | INODE_RENAME = auto() 113 | INODE_READLINK = auto() 114 | INODE_FOLLOW_LINK = auto() 115 | INODE_PERMISSION = auto() # TODO: split this into READ, WRITE, APPEND, EXEC 116 | INODE_SETATTR = auto() 117 | INODE_GETATTR = auto() 118 | INODE_SETXATTR = auto() 119 | INODE_GETXATTR = auto() 120 | INODE_LISTXATTR = auto() 121 | INODE_REMOVEXATTR = auto() 122 | FILE_PERMISSION = auto() # TODO: split this into READ, WRITE, APPEND, EXEC 123 | FILE_IOCTL = auto() 124 | MMAP_ADDR = auto() 125 | MMAP_FILE = auto() 126 | FILE_MPROTECT = auto() 127 | FILE_LOCK = auto() 128 | FILE_FCNTL = auto() 129 | FILE_SEND_SIGIOTASK = auto() 130 | FILE_RECEIVE = auto() 131 | UNIX_STREAM_CONNECT = auto() 132 | UNIX_MAY_SEND = auto() 133 | SOCKET_CREATE = auto() 134 | SOCKET_SOCKETPAIR = auto() 135 | SOCKET_BIND = auto() 136 | SOCKET_CONNECT = auto() 137 | SOCKET_LISTEN = auto() 138 | SOCKET_ACCEPT = auto() 139 | SOCKET_SENDMSG = auto() 140 | SOCKET_RECVMSG = auto() 141 | SOCKET_GETSOCKNAME = auto() 142 | SOCKET_GETPEERNAME = auto() 143 | SOCKET_GETSOCKOPT = auto() 144 | SOCKET_SETSOCKOPT = auto() 145 | SOCKET_SHUTDOWN = auto() 146 | TUN_DEV_CREATE = auto() 147 | TUN_DEV_ATTACH = auto() 148 | KEY_ALLOC = auto() 149 | KEY_FREE = auto() 150 | KEY_PERMISSION = auto() # TODO: maybe split this into operations 151 | IPC_PERMISSION = auto() 152 | MSG_QUEUE_ASSOCIATE = auto() 153 | MSG_QUEUE_MSGCTL = auto() 154 | MSG_QUEUE_MSGSND = auto() 155 | MSG_QUEUE_MSGRCV = auto() 156 | SHM_ASSOCIATE = auto() 157 | SHM_SHMCTL = auto() 158 | SHM_SHMAT = auto() 159 | PTRACE_ACCESS_CHECK = auto() 160 | PTRACE_TRACEME = auto() 161 | CAPGET = auto() 162 | CAPSET = auto() 163 | CAPABLE = auto() 164 | QUOTACTL = auto() 165 | QUOTA_ON = auto() 166 | SYSLOG = auto() 167 | SETTIME = auto() 168 | VM_ENOUGH_MEMORY = auto() 169 | BPF = auto() 170 | BPF_MAP = auto() 171 | BPF_PROG = auto() 172 | PERF_EVENT_OPEN = auto() 173 | LSM_MAX = auto() # This must always be the last entry 174 | 175 | @staticmethod 176 | def get_name(num: int) -> str: 177 | try: 178 | return EBPH_LSM(num).name.lower() 179 | except ValueError: 180 | return 'empty' 181 | 182 | 183 | NUM_LSM = int(EBPH_LSM.LSM_MAX) 184 | 185 | class EBPHProfileDataStruct(ct.Structure): 186 | """ 187 | Represents userspace's view of profile data. 188 | Warning: Keep in sync with BPF program. 189 | """ 190 | _fields_ = ( 191 | ( 192 | 'flags', 193 | ct.c_uint8 * ((NUM_LSM * NUM_LSM) & sys.maxsize), 194 | ), 195 | ) 196 | 197 | def __eq__(self, other): 198 | try: 199 | self_len = len(self.flags) 200 | other_len = len(other.flags) 201 | assert self_len == other_len 202 | for i in range(self_len): 203 | assert self.flags[i] == other.flags[i] 204 | except Exception: 205 | return False 206 | return True 207 | 208 | 209 | class EBPHProfileStruct(ct.Structure): 210 | """ 211 | Represents userspace's view of the profile structure and its data. 212 | Warning: Keep in sync with BPF program. 213 | """ 214 | _fields_ = ( 215 | ('magic', ct.c_uint64), 216 | ('profile_key', ct.c_uint64), 217 | ('status', ct.c_uint8), 218 | ('anomaly_count', ct.c_uint64), 219 | ('train_count', ct.c_uint64), 220 | ('last_mod_count', ct.c_uint64), 221 | ('sequences', ct.c_uint64), 222 | ('normal_time', ct.c_uint64), 223 | ('count', ct.c_uint64), 224 | ('train', EBPHProfileDataStruct), 225 | ('test', EBPHProfileDataStruct), 226 | ('exe', ct.c_char * defs.PATH_MAX), 227 | ) 228 | 229 | def __eq__(self, other: 'EBPHProfileDataStruct') -> bool: 230 | try: 231 | assert self.profile_key == other.profile_key 232 | assert self.status == other.status 233 | assert self.anomaly_count == other.anomaly_count 234 | assert self.train_count == other.train_count 235 | assert self.last_mod_count == other.last_mod_count 236 | assert self.sequences == other.sequences 237 | assert self.normal_time == other.normal_time 238 | assert self.count == other.count 239 | assert self.exe == other.exe 240 | except Exception: 241 | return False 242 | return True 243 | 244 | 245 | def _asdict(self) -> dict: 246 | return {field[0]: getattr(self, field[0]) for field in self._fields_} 247 | 248 | def __str__(self) -> str: 249 | return pformat((self.__class__.__name__, self._asdict())) 250 | 251 | @classmethod 252 | def from_bpf(cls, bpf: BPF, exe: bytes, profile_key: int,) -> 'EBPHProfileStruct': 253 | """ 254 | Create a new profile structure from the BPF program, its exe name 255 | (in bytes), and its key. 256 | """ 257 | profile = EBPHProfileStruct() 258 | profile.magic = calculate_profile_magic() 259 | profile.profile_key = profile_key 260 | profile.exe = exe 261 | 262 | try: 263 | bpf_profile = bpf['profiles'][ct.c_uint64(profile_key)] 264 | except (KeyError, IndexError): 265 | raise KeyError('Profile does not exist in BPF map') 266 | 267 | profile.status = bpf_profile.status 268 | profile.anomaly_count = bpf_profile.anomaly_count 269 | profile.train_count = bpf_profile.train_count 270 | profile.last_mod_count = bpf_profile.last_mod_count 271 | profile.sequences = bpf_profile.sequences 272 | profile.normal_time = bpf_profile.normal_time 273 | profile.count = bpf_profile.count 274 | 275 | try: 276 | # Look up value 277 | train = bpf['training_data'][ct.c_uint64(profile_key)] 278 | # Copy values over 279 | if not ct.memmove(ct.addressof(profile.train), ct.addressof(train), ct.sizeof(profile.train)): 280 | raise RuntimeError('Failed to memmove training data!') 281 | except (KeyError, IndexError): 282 | pass 283 | 284 | try: 285 | # Look up value 286 | test = bpf['testing_data'][ct.c_uint64(profile_key)] 287 | # Copy values over 288 | if not ct.memmove(ct.addressof(profile.test), ct.addressof(test), ct.sizeof(profile.test)): 289 | raise RuntimeError('Failed to memove testing data!') 290 | except (KeyError, IndexError): 291 | pass 292 | 293 | return profile 294 | 295 | def load_into_bpf(self, bpf: BPF) -> None: 296 | """ 297 | Load a profile into the BPF program. 298 | """ 299 | # Get leaf 300 | bpf_profile = bpf['profiles'].Leaf() 301 | # Set values 302 | bpf_profile.status = self.status 303 | bpf_profile.anomaly_count = self.anomaly_count 304 | bpf_profile.train_count = self.train_count 305 | bpf_profile.last_mod_count = self.last_mod_count 306 | bpf_profile.sequences = self.sequences 307 | bpf_profile.normal_time = self.normal_time 308 | bpf_profile.count = self.count 309 | # Update map 310 | bpf['profiles'][ct.c_uint64(self.profile_key)] = bpf_profile 311 | 312 | # Get leaf 313 | train = bpf['training_data'].Leaf() 314 | # Copy values over 315 | if not ct.memmove(ct.addressof(train), ct.addressof(self.train), ct.sizeof(self.train)): 316 | raise RuntimeError('Failed to memmove training data!') 317 | # Update map 318 | bpf['training_data'][ct.c_uint64(self.profile_key)] = train 319 | 320 | # Get leaf 321 | test = bpf['testing_data'].Leaf() 322 | # Copy values over 323 | if not ct.memmove(ct.addressof(test), ct.addressof(self.test), ct.sizeof(self.test)): 324 | raise RuntimeError('Failed to memmove testing data!') 325 | # Update map 326 | bpf['testing_data'][ct.c_uint64(self.profile_key)] = test 327 | -------------------------------------------------------------------------------- /ebph/api.py: -------------------------------------------------------------------------------- 1 | """ 2 | ebpH (Extended BPF Process Homeostasis) A host-based IDS written in eBPF. 3 | ebpH Copyright (C) 2019-2020 William Findlay 4 | pH Copyright (C) 1999-2003 Anil Somayaji and (C) 2008 Mario Van Velzen 5 | 6 | This program is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with this program. If not, see . 18 | 19 | Uses FastAPI to provide a REST API for interacting with the daemon. 20 | 21 | 2020-Jul-13 William Findlay Created this. 22 | """ 23 | 24 | from http import HTTPStatus 25 | import logging 26 | from typing import List, Dict, NoReturn 27 | 28 | from fastapi import FastAPI, HTTPException, Path, Query 29 | from uvicorn.config import LOGGING_CONFIG 30 | import uvicorn 31 | 32 | from ebph import defs 33 | from ebph.bpf_program import BPFProgram 34 | from ebph.structs import EBPH_PROFILE_STATUS, EBPH_SETTINGS 35 | from ebph.utils import ns_to_str, ns_to_delta_str 36 | from ebph.version import __version__ 37 | from ebph.logger import get_logger 38 | 39 | app = FastAPI() 40 | logger = get_logger() 41 | 42 | # Monkeypatch uvicorn to not hijack root logger 43 | try: 44 | LOGGING_CONFIG['loggers']['uvicorn'] = LOGGING_CONFIG['loggers'][''] 45 | del LOGGING_CONFIG['loggers'][''] 46 | except KeyError: 47 | pass 48 | 49 | class API: 50 | bpf_program: BPFProgram = None 51 | 52 | @classmethod 53 | def connect_bpf_program(cls, bpf_program: BPFProgram) -> None: 54 | cls.bpf_program = bpf_program 55 | 56 | @staticmethod 57 | def serve_forever() -> NoReturn: 58 | uvicorn.run( 59 | app, 60 | uds=defs.EBPH_SOCK, 61 | log_level=logging.WARNING, 62 | log_config=LOGGING_CONFIG, 63 | ) 64 | 65 | @staticmethod 66 | @app.get('/status') 67 | def get_status() -> Dict: 68 | """ 69 | Returns the status of the BPF program. 70 | """ 71 | try: 72 | num_profiles = 0 73 | num_training = 0 74 | num_frozen = 0 75 | num_normal = 0 76 | for k, v in API.bpf_program.bpf['profiles'].iteritems(): 77 | num_profiles += 1 78 | if v.status & EBPH_PROFILE_STATUS.TRAINING: 79 | num_training += 1 80 | if v.status & EBPH_PROFILE_STATUS.FROZEN: 81 | num_frozen += 1 82 | if v.status & EBPH_PROFILE_STATUS.NORMAL: 83 | num_normal += 1 84 | 85 | num_processes = 0 86 | num_threads = 0 87 | for k, v in API.bpf_program.bpf['task_states'].iteritems(): 88 | if v.pid == v.tgid: 89 | num_processes += 1 90 | num_threads += 1 91 | res = { 92 | 'ebpH Version': __version__, 93 | 'Monitoring': bool(API.bpf_program.get_setting(EBPH_SETTINGS.MONITORING)), 94 | 'Logging New Seq': bool(API.bpf_program.get_setting(EBPH_SETTINGS.LOG_SEQUENCES)), 95 | 'Enforcing': bool(API.bpf_program.get_setting(EBPH_SETTINGS.ENFORCING)), 96 | 'Profiles': f'{num_profiles} ({num_training} training ({num_frozen} frozen), {num_normal} normal)', 97 | 'Processes': f'{num_processes} ({num_threads} threads)', 98 | 'Normal Wait': ns_to_delta_str(API.bpf_program.get_setting(EBPH_SETTINGS.NORMAL_WAIT)), 99 | 'Normal Factor': f'{API.bpf_program.get_setting(EBPH_SETTINGS.NORMAL_FACTOR)}/' 100 | f'{API.bpf_program.get_setting(EBPH_SETTINGS.NORMAL_FACTOR_DEN)}', 101 | 'Anomaly Limit': API.bpf_program.get_setting(EBPH_SETTINGS.ANOMALY_LIMIT), 102 | 'Tolerize Limit': API.bpf_program.get_setting(EBPH_SETTINGS.TOLERIZE_LIMIT), 103 | } 104 | return res 105 | except Exception as e: 106 | logger.error('', exc_info=e) 107 | raise HTTPException(HTTPStatus.BAD_REQUEST, f'Unable to get status.') 108 | 109 | @staticmethod 110 | @app.get('/profiles') 111 | def get_profiles() -> List[Dict]: 112 | """ 113 | Returns a dictionary of key -> executable. 114 | """ 115 | try: 116 | return [API.get_profile_by_key(k.value) for k in API.bpf_program.bpf['profiles'].keys()] 117 | except Exception as e: 118 | logger.error('', exc_info=e) 119 | raise HTTPException(HTTPStatus.BAD_REQUEST, f'Error getting profiles.') 120 | 121 | @staticmethod 122 | @app.get('/profiles/key/{key}') 123 | def get_profile_by_key(key: int) -> Dict: 124 | """ 125 | Returns a profile by @key. 126 | """ 127 | try: 128 | profile = API.bpf_program.get_profile(key) 129 | return { 130 | 'exe': str(API.bpf_program.profile_key_to_exe[key]), 131 | 'profile_key': key, 132 | 'status': str(EBPH_PROFILE_STATUS(profile.status)), 133 | 'anomaly_count': profile.anomaly_count, 134 | 'count': profile.count, 135 | 'train_count': profile.train_count, 136 | 'last_mod_count': profile.last_mod_count, 137 | 'sequences': profile.sequences, 138 | 'normal_time': ns_to_str(profile.normal_time), 139 | } 140 | except KeyError: 141 | raise HTTPException(HTTPStatus.NOT_FOUND, f'Profile {key} does not exist.') 142 | except Exception as e: 143 | logger.error('', exc_info=e) 144 | raise HTTPException(HTTPStatus.BAD_REQUEST, f'Error getting profile {key}.') 145 | 146 | @staticmethod 147 | @app.get('/profiles/exe/{exe:path}') 148 | def get_profile_by_exe(exe: str) -> Dict: 149 | """ 150 | Returns a profile by @exe. 151 | """ 152 | rev = {v: k for k, v in API.bpf_program.profile_key_to_exe.items()} 153 | try: 154 | return API.get_profile_by_key(rev[exe]) 155 | except KeyError as e: 156 | raise HTTPException(HTTPStatus.NOT_FOUND, f'Profile {exe} does not exist.') 157 | except Exception as e: 158 | logger.error('', exc_info=e) 159 | raise HTTPException(HTTPStatus.BAD_REQUEST, f'Error getting profile {exe}.') 160 | 161 | @staticmethod 162 | @app.put('/profiles/key/{key}/normalize') 163 | def normalize_profile_by_key(key: int) -> Dict: 164 | """ 165 | Normalize a profile by its @key. 166 | """ 167 | try: 168 | rc = API.bpf_program.normalize_profile(key) 169 | except Exception as e: 170 | logger.error('', exc_info=e) 171 | raise HTTPException(HTTPStatus.BAD_REQUEST, f'Error normalizing profile {key}.') 172 | if rc < 0: 173 | raise HTTPException(HTTPStatus.NOT_FOUND, f'Unable to normalize profile {key}.') 174 | return API.get_profile_by_key(key) 175 | 176 | @staticmethod 177 | @app.put('/profiles/exe/{exe:path}/normalize') 178 | def normalize_profile_by_exe(exe: str) -> Dict: 179 | """ 180 | Normalize a profile by its @exe. 181 | """ 182 | rev = {v: k for k, v in API.bpf_program.profile_key_to_exe.items()} 183 | try: 184 | return API.normalize_profile_by_key(rev[exe]) 185 | except KeyError as e: 186 | raise HTTPException(HTTPStatus.NOT_FOUND, f'Profile {exe} does not exist.') 187 | except Exception as e: 188 | logger.error('', exc_info=e) 189 | raise HTTPException(HTTPStatus.BAD_REQUEST, f'Error normalizing profile {exe}.') 190 | 191 | @staticmethod 192 | @app.put('/processes/pid/{pid}/normalize') 193 | def normalize_process(pid: int) -> Dict: 194 | """ 195 | Normalize a profile by its @pid. 196 | """ 197 | try: 198 | rc = API.bpf_program.normalize_process(pid) 199 | except Exception as e: 200 | logger.error('', exc_info=e) 201 | raise HTTPException(HTTPStatus.BAD_REQUEST, f'Error normalizing process {pid}.') 202 | if rc < 0: 203 | raise HTTPException(HTTPStatus.NOT_FOUND, f'Unable to normalize process {pid}.') 204 | return API.get_process(pid) 205 | 206 | @staticmethod 207 | @app.put('/profiles/key/{key}/sensitize') 208 | def sensitize_profile_by_key(key: int) -> Dict: 209 | """ 210 | Normalize a profile by its @key. 211 | """ 212 | try: 213 | rc = API.bpf_program.sensitize_profile(key) 214 | except Exception as e: 215 | logger.error('', exc_info=e) 216 | raise HTTPException(HTTPStatus.BAD_REQUEST, f'Error normalizing profile {key}.') 217 | if rc < 0: 218 | raise HTTPException(HTTPStatus.NOT_FOUND, f'Unable to sensitize profile {key}.') 219 | return API.get_profile_by_key(key) 220 | 221 | @staticmethod 222 | @app.put('/profiles/exe/{exe:path}/sensitize') 223 | def sensitize_profile_by_exe(exe: str) -> Dict: 224 | """ 225 | Normalize a profile by its @exe. 226 | """ 227 | rev = {v: k for k, v in API.bpf_program.profile_key_to_exe.items()} 228 | try: 229 | return API.sensitize_profile_by_key(rev[exe]) 230 | except KeyError as e: 231 | raise HTTPException(HTTPStatus.NOT_FOUND, f'Profile {exe} does not exist.') 232 | except Exception as e: 233 | logger.error('', exc_info=e) 234 | raise HTTPException(HTTPStatus.BAD_REQUEST, f'Error sensitizing profile {exe}.') 235 | 236 | @staticmethod 237 | @app.put('/processes/pid/{pid}/sensitize') 238 | def sensitize_process(pid: int) -> Dict: 239 | """ 240 | Normalize a profile by its @pid. 241 | """ 242 | try: 243 | rc = API.bpf_program.sensitize_process(pid) 244 | except Exception as e: 245 | logger.error('', exc_info=e) 246 | raise HTTPException(HTTPStatus.BAD_REQUEST, f'Error sensitizing process {pid}.') 247 | if rc < 0: 248 | raise HTTPException(HTTPStatus.NOT_FOUND, f'Unable to sensitize process {pid}.') 249 | return API.get_process(pid) 250 | 251 | @staticmethod 252 | @app.put('/profiles/key/{key}/tolerize') 253 | def tolerize_profile_by_key(key: int) -> Dict: 254 | """ 255 | Normalize a profile by its @key. 256 | """ 257 | try: 258 | rc = API.bpf_program.tolerize_profile(key) 259 | except Exception as e: 260 | logger.error('', exc_info=e) 261 | raise HTTPException(HTTPStatus.BAD_REQUEST, f'Error tolerizing profile {key}.') 262 | if rc < 0: 263 | raise HTTPException(HTTPStatus.NOT_FOUND, f'Unable to tolerize profile {key}.') 264 | return API.get_profile_by_key(key) 265 | 266 | @staticmethod 267 | @app.put('/profiles/exe/{exe:path}/tolerize') 268 | def tolerize_profile_by_exe(exe: str) -> Dict: 269 | """ 270 | Normalize a profile by its @exe. 271 | """ 272 | rev = {v: k for k, v in API.bpf_program.profile_key_to_exe.items()} 273 | try: 274 | return API.tolerize_profile_by_key(rev[exe]) 275 | except KeyError as e: 276 | raise HTTPException(HTTPStatus.NOT_FOUND, f'Profile {exe} does not exist.') 277 | except Exception as e: 278 | logger.error('', exc_info=e) 279 | raise HTTPException(HTTPStatus.BAD_REQUEST, f'Error tolerizing profile {exe}.') 280 | 281 | @staticmethod 282 | @app.put('/processes/pid/{pid}/tolerize') 283 | def tolerize_process(pid: int) -> Dict: 284 | """ 285 | Normalize a profile by its @pid. 286 | """ 287 | try: 288 | rc = API.bpf_program.tolerize_process(pid) 289 | except Exception as e: 290 | logger.error('', exc_info=e) 291 | raise HTTPException(HTTPStatus.BAD_REQUEST, f'Error tolerizing process {pid}.') 292 | if rc < 0: 293 | raise HTTPException(HTTPStatus.NOT_FOUND, f'Unable to tolerize process {pid}.') 294 | return API.get_process(pid) 295 | 296 | @staticmethod 297 | @app.put('/profiles/save') 298 | def save_profiles() -> Dict: 299 | """ 300 | Save profiles. 301 | """ 302 | saved, error = API.bpf_program.save_profiles() 303 | return {'saved': saved, 'error': error} 304 | 305 | @staticmethod 306 | @app.put('/profiles/load') 307 | def load_profiles() -> Dict: 308 | """ 309 | Load profiles. 310 | """ 311 | loaded, error = API.bpf_program.load_profiles() 312 | return {'loaded': loaded, 'error': error} 313 | 314 | @staticmethod 315 | @app.get('/processes') 316 | def get_processes() -> List[Dict]: 317 | """ 318 | Returns a process by pid. 319 | """ 320 | processes = [] 321 | for k in API.bpf_program.bpf['task_states'].keys(): 322 | try: 323 | processes.append(API.get_process(k.value)) 324 | except (KeyError, HTTPException): 325 | continue 326 | except Exception as e: 327 | logger.error('', exc_info=e) 328 | continue 329 | return processes 330 | 331 | @staticmethod 332 | @app.get('/processes/pid/{pid}') 333 | def get_process(pid: int) -> Dict: 334 | """ 335 | Returns a process by pid. 336 | """ 337 | try: 338 | process = API.bpf_program.get_process(pid) 339 | except KeyError: 340 | raise HTTPException(HTTPStatus.NOT_FOUND, f'Process {pid} does not exist.') 341 | except Exception as e: 342 | logger.error('', exc_info=e) 343 | raise HTTPException(HTTPStatus.BAD_REQUEST, f'Error getting process {pid}.') 344 | try: 345 | profile = API.get_profile_by_key(process.profile_key) 346 | except KeyError: 347 | raise HTTPException(HTTPStatus.NOT_FOUND, f'Profile for {pid} does not exist.') 348 | except Exception as e: 349 | logger.error('', exc_info=e) 350 | raise HTTPException(HTTPStatus.BAD_REQUEST, f'Error getting profile for process {pid}.') 351 | return { 352 | 'pid': process.tgid, 353 | 'tid': process.pid, 354 | 'count': process.count, 355 | 'total_lfc': process.total_lfc, 356 | 'max_lfc': process.max_lfc, 357 | 'profile': profile, 358 | } 359 | 360 | @staticmethod 361 | @app.get('/settings/{setting}') 362 | def get_setting(setting: EBPH_SETTINGS) -> Dict: 363 | """ 364 | Get an ebpH setting. 365 | """ 366 | value = API.bpf_program.get_setting(setting) 367 | if value is None: 368 | raise HTTPException(HTTPStatus.BAD_REQUEST, f'No such setting {setting}.') 369 | return {'setting': setting, 'value': value} 370 | 371 | @staticmethod 372 | @app.put('/settings/{setting}/{value}') 373 | def change_setting(setting: EBPH_SETTINGS, value: int = Path(..., ge=0)) -> Dict: 374 | """ 375 | Change an ebpH setting. 376 | """ 377 | res = API.bpf_program.change_setting(setting, value) 378 | if res < 0: 379 | raise HTTPException(HTTPStatus.BAD_REQUEST, f'Unable to change {setting} to {value}.') 380 | return API.get_setting(setting) 381 | -------------------------------------------------------------------------------- /ebph/bpf_program.py: -------------------------------------------------------------------------------- 1 | """ 2 | ebpH (Extended BPF Process Homeostasis) A host-based IDS written in eBPF. 3 | ebpH Copyright (C) 2019-2020 William Findlay 4 | pH Copyright (C) 1999-2003 Anil Somayaji and (C) 2008 Mario Van Velzen 5 | 6 | This program is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with this program. If not, see . 18 | 19 | A wrapper around the BPF program. Exposes methods for interacting 20 | with it from userspace and for handling events. 21 | 22 | 2020-Jul-13 William Findlay Created this. 23 | """ 24 | 25 | import os 26 | import sys 27 | import time 28 | import atexit 29 | import ctypes as ct 30 | from collections import defaultdict 31 | from typing import List, Optional, Tuple 32 | 33 | from bcc import BPF 34 | from ratelimit import limits 35 | 36 | from ebph.libebph import Lib 37 | from ebph.logger import get_logger 38 | from ebph.utils import running_processes 39 | from ebph.structs import ( 40 | EBPHProfileStruct, 41 | EBPH_SETTINGS, 42 | calculate_profile_magic, 43 | EBPH_LSM, 44 | ) 45 | from ebph import defs 46 | 47 | logger = get_logger() 48 | 49 | 50 | def ringbuf_callback(bpf: BPF, map_name: str, infer_type: bool = True, ratelimit_per_sec = 9999999999): 51 | """ 52 | Decorator that wraps a function in all of the logic 53 | to associate it with a ringbuffer @map_name in BPF land. 54 | 55 | If @infer_type is set, automatically get @bpf to cast 56 | event data to the correct structure. Pretty neat! 57 | 58 | TODO: Consider upstreaming this in bcc 59 | """ 60 | def _inner(func): 61 | @limits(calls=ratelimit_per_sec, period=1, raise_on_limit=False) 62 | def _wrapper(ctx, data, size): 63 | if infer_type: 64 | data = bpf[map_name].event(data) 65 | func(ctx, data, size) 66 | 67 | bpf[map_name].open_ring_buffer(_wrapper) 68 | 69 | return _inner 70 | 71 | 72 | class BPFProgram: 73 | """ 74 | Wraps the BPF program and exposes methods for interacting with it. 75 | """ 76 | def __init__(self, debug: bool = False, log_sequences: bool = False, auto_save = True, auto_load = True): 77 | self.bpf = None 78 | self.usdt_contexts = [] 79 | self.seqstack_inner_bpf = None 80 | self.cflags = [] 81 | 82 | # Number of elapsed ticks 83 | self.tick_count = 0 84 | 85 | self.debug = debug 86 | self.auto_save = auto_save 87 | self.auto_load = auto_load 88 | 89 | self.profile_key_to_exe = defaultdict(lambda: '[unknown]') 90 | self.syscall_number_to_name = defaultdict(lambda: '[unknown]') 91 | 92 | self._set_cflags() 93 | try: 94 | self._load_bpf() 95 | except Exception as e: 96 | logger.error('Unable to load BPF program', exc_info=e) 97 | sys.exit(1) 98 | try: 99 | self._register_ring_buffers() 100 | except Exception as e: 101 | logger.error('Unable to register ring buffers', exc_info=e) 102 | sys.exit(1) 103 | if self.auto_load: 104 | self.load_profiles() 105 | 106 | atexit.register(self._cleanup) 107 | 108 | if log_sequences: 109 | self.change_setting(EBPH_SETTINGS.LOG_SEQUENCES, log_sequences) 110 | 111 | if defs.ENFORCING: 112 | self.change_setting(EBPH_SETTINGS.ENFORCING, defs.ENFORCING) 113 | 114 | self.change_setting(EBPH_SETTINGS.NORMAL_WAIT, defs.NORMAL_WAIT) 115 | self.change_setting(EBPH_SETTINGS.NORMAL_FACTOR, defs.NORMAL_FACTOR) 116 | self.change_setting(EBPH_SETTINGS.NORMAL_FACTOR_DEN, defs.NORMAL_FACTOR_DEN) 117 | self.change_setting(EBPH_SETTINGS.ANOMALY_LIMIT, defs.ANOMALY_LIMIT) 118 | self.change_setting(EBPH_SETTINGS.TOLERIZE_LIMIT, defs.TOLERIZE_LIMIT) 119 | 120 | try: 121 | self._bootstrap_processes() 122 | except Exception as e: 123 | logger.error('Unable to bootstrap processes', exc_info=e) 124 | 125 | self.start_monitoring() 126 | 127 | def on_tick(self) -> None: 128 | """ 129 | Perform this operation every time ebphd ticks. 130 | """ 131 | try: 132 | self.tick_count += 1 133 | 134 | if self.auto_save and self.tick_count % defs.PROFILE_SAVE_INTERVAL == 0: 135 | self.save_profiles() 136 | 137 | self.bpf.ring_buffer_consume() 138 | except Exception: 139 | pass 140 | 141 | def change_setting(self, setting: EBPH_SETTINGS, value: int) -> int: 142 | """ 143 | Change a @setting in the BPF program to @value if it is an integer >= 0. 144 | """ 145 | if value < 0: 146 | logger.error( 147 | f'Value for {setting.name} must be a positive integer.' 148 | ) 149 | return -1 150 | 151 | rc = Lib.set_setting(setting, value) 152 | err = os.strerror(ct.get_errno()) 153 | 154 | if rc < 0: 155 | logger.error(f'Failed to set {setting.name} to {value}: {err}') 156 | if rc == 1: 157 | logger.info(f'{setting.name} is already set to {value}.') 158 | if rc == 0: 159 | logger.info(f'{setting.name} set to {value}.') 160 | return rc 161 | 162 | def get_setting(self, setting: EBPH_SETTINGS) -> Optional[int]: 163 | """ 164 | Get @setting from the BPF program. 165 | """ 166 | try: 167 | return self.bpf['_ebph_settings'][ct.c_uint64(setting)].value 168 | except (KeyError, IndexError): 169 | logger.error(f'Failed to get {setting.name}: Key does not exist') 170 | return None 171 | 172 | def start_monitoring(self, silent=False) -> int: 173 | """ 174 | Start monitoring the system. (Equivalent to setting MONITORING to 1). 175 | """ 176 | if self.get_setting(EBPH_SETTINGS.MONITORING) and not silent: 177 | logger.info('System is already being monitored.') 178 | return 1 179 | rc = Lib.set_setting(EBPH_SETTINGS.MONITORING, True) 180 | err = os.strerror(ct.get_errno()) 181 | if rc < 0 and not silent: 182 | logger.error(f'Failed to start monitoring: {err}') 183 | if rc == 0 and not silent: 184 | logger.info('Started monitoring the system.') 185 | return rc 186 | 187 | def stop_monitoring(self, silent=False) -> int: 188 | """ 189 | Stop monitoring the system. (Equivalent to setting MONITORING to 0). 190 | """ 191 | if not self.get_setting(EBPH_SETTINGS.MONITORING) and not silent: 192 | logger.info('System is not being monitored.') 193 | return 1 194 | rc = Lib.set_setting(EBPH_SETTINGS.MONITORING, False) 195 | err = os.strerror(ct.get_errno()) 196 | if rc < 0 and not silent: 197 | logger.error(f'Failed to stop monitoring: {err}') 198 | if rc == 0 and not silent: 199 | logger.info('Stopped monitoring the system.') 200 | return rc 201 | 202 | def save_profiles(self) -> Tuple[int, int]: 203 | """ 204 | Save all profiles. 205 | """ 206 | saved = 0 207 | error = 0 208 | 209 | logger.info('Saving profiles...') 210 | 211 | for k in self.bpf['profiles'].keys(): 212 | key = k.value 213 | exe = self.profile_key_to_exe[key] 214 | fname = f'{key}' 215 | try: 216 | profile = EBPHProfileStruct.from_bpf( 217 | self.bpf, exe.encode('ascii'), key 218 | ) 219 | with open(os.path.join(defs.EBPH_DATA_DIR, fname), 'wb') as f: 220 | f.write(profile) 221 | logger.debug(f'Successfully saved profile {fname} ({exe}).') 222 | except Exception as e: 223 | logger.error( 224 | f'Unable to save profile {fname} ({exe}).', exc_info=e 225 | ) 226 | error += 1 227 | saved += 1 228 | logger.info(f'Saved {saved} profiles successfully!') 229 | return saved, error 230 | 231 | def load_profiles(self) -> Tuple[int, int]: 232 | """ 233 | Load all profiles. 234 | """ 235 | loaded = 0 236 | error = 0 237 | 238 | logger.info('Loading profiles...') 239 | # If we are monitoring, stop 240 | monitoring = self.get_setting(EBPH_SETTINGS.MONITORING) 241 | 242 | if monitoring: 243 | self.stop_monitoring() 244 | 245 | for fname in os.listdir(defs.EBPH_DATA_DIR): 246 | try: 247 | profile = EBPHProfileStruct() 248 | with open(os.path.join(defs.EBPH_DATA_DIR, fname), 'rb') as f: 249 | f.readinto(profile) 250 | # Wrong version 251 | if profile.magic != calculate_profile_magic(): 252 | logger.debug(f'Wrong magic number for profile {fname}, skipping.') 253 | continue 254 | profile.load_into_bpf(self.bpf) 255 | self.profile_key_to_exe[profile.profile_key] = profile.exe.decode('ascii') 256 | exe = self.profile_key_to_exe[profile.profile_key] 257 | logger.debug(f'Successfully loaded profile {fname} ({exe}).') 258 | except Exception as e: 259 | logger.error(f'Unable to load profile {fname}.', exc_info=e) 260 | error += 1 261 | loaded += 1 262 | 263 | # If we were monitoring, resume 264 | if monitoring: 265 | self.start_monitoring() 266 | logger.info(f'Loaded {loaded} profiles successfully!') 267 | return loaded, error 268 | 269 | def get_full_profile(self, key: int) -> EBPHProfileStruct: 270 | """ 271 | Get a profile indexed by @key from the BPF program, INCLUDING its 272 | flags and return it as an EBPHProfileStruct. 273 | """ 274 | exe = self.profile_key_to_exe[key] 275 | return EBPHProfileStruct.from_bpf(self.bpf, exe.encode('ascii'), key) 276 | 277 | def get_profile(self, key: int) -> ct.Structure: 278 | """ 279 | Get just the profile struct indexed by @key from the BPF program. 280 | """ 281 | return self.bpf['profiles'][ct.c_uint64(key)] 282 | 283 | def get_process(self, pid: int) -> ct.Structure: 284 | """ 285 | Get a task_state indexed by @pid from the BPF program. 286 | """ 287 | return self.bpf['task_states'][ct.c_uint32(pid)] 288 | 289 | def normalize_profile(self, profile_key: int): 290 | """ 291 | Normalize the profile indexed by @profile_key. 292 | """ 293 | try: 294 | rc = Lib.normalize_profile(profile_key) 295 | except Exception as e: 296 | logger.error(f'Unable to normalize profile.', exc_info=e) 297 | return -1 298 | if rc < 0: 299 | logger.error(f'Unable to normalize profile: {os.strerror(ct.get_errno())}') 300 | return rc 301 | 302 | def normalize_process(self, pid: int): 303 | """ 304 | Normalize the process indexed by @pid. 305 | """ 306 | try: 307 | rc = Lib.normalize_process(pid) 308 | except Exception as e: 309 | logger.error(f'Unable to normalize process {pid}.', exc_info=e) 310 | return -1 311 | if rc < 0: 312 | logger.error(f'Unable to normalize process {pid}: {os.strerror(ct.get_errno())}') 313 | return rc 314 | 315 | def sensitize_profile(self, profile_key: int): 316 | """ 317 | Sensitize the profile indexed by @profile_key. 318 | """ 319 | try: 320 | rc = Lib.sensitize_profile(profile_key) 321 | except Exception as e: 322 | logger.error(f'Unable to sensitize profile.', exc_info=e) 323 | return -1 324 | if rc < 0: 325 | logger.error(f'Unable to sensitize profile: {os.strerror(ct.get_errno())}') 326 | return rc 327 | exe = self.profile_key_to_exe[profile_key] 328 | logger.info(f'Sensitized profile {exe}. Training data reset.') 329 | return rc 330 | 331 | def sensitize_process(self, pid: int): 332 | """ 333 | Sensitize the process indexed by @pid. 334 | """ 335 | try: 336 | rc = Lib.sensitize_process(pid) 337 | except Exception as e: 338 | logger.error(f'Unable to sensitize process {pid}.', exc_info=e) 339 | return -1 340 | if rc < 0: 341 | logger.error(f'Unable to sensitize process {pid}: {os.strerror(ct.get_errno())}') 342 | return rc 343 | try: 344 | process = self.get_process(pid) 345 | exe = self.profile_key_to_exe[process.profile_key] 346 | except (KeyError, IndexError): 347 | exe = '[unknown]' 348 | logger.info(f'Sensitized PID {pid} ({exe}). Training data reset.') 349 | return rc 350 | 351 | def tolerize_profile(self, profile_key: int): 352 | """ 353 | Tolerize the profile indexed by @profile_key. 354 | """ 355 | try: 356 | rc = Lib.tolerize_profile(profile_key) 357 | except Exception as e: 358 | logger.error(f'Unable to tolerize profile.', exc_info=e) 359 | return -1 360 | if rc < 0: 361 | logger.error(f'Unable to tolerize profile: {os.strerror(ct.get_errno())}') 362 | return rc 363 | exe = self.profile_key_to_exe[profile_key] 364 | logger.info(f'Tolerized profile {exe}. Stopped normal monitoring.') 365 | return rc 366 | 367 | def tolerize_process(self, pid: int): 368 | """ 369 | Tolerize the process indexed by @pid. 370 | """ 371 | try: 372 | rc = Lib.tolerize_process(pid) 373 | except Exception as e: 374 | logger.error(f'Unable to tolerize process {pid}.', exc_info=e) 375 | return -1 376 | if rc < 0: 377 | logger.error(f'Unable to tolerize process {pid}: {os.strerror(ct.get_errno())}') 378 | return rc 379 | try: 380 | process = self.get_process(pid) 381 | exe = self.profile_key_to_exe[process.profile_key] 382 | except (KeyError, IndexError): 383 | exe = '[unknown]' 384 | logger.info(f'Tolerized PID {pid} ({exe}). Stopped normal monitoring.') 385 | return rc 386 | 387 | def _register_ring_buffers(self) -> None: 388 | logger.info('Registering ring buffers...') 389 | 390 | @ringbuf_callback(self.bpf, 'new_profile_events') 391 | def new_profile_events(ctx, event, size): 392 | """ 393 | new_profile_events. 394 | 395 | Callback for new profile creation. 396 | Logs creation and caches key -> pathname mapping 397 | for later use. 398 | """ 399 | pathname = event.pathname.decode('utf-8') 400 | try: 401 | pass 402 | except Exception: 403 | pass 404 | self.profile_key_to_exe[event.profile_key] = pathname 405 | 406 | if self.debug: 407 | logger.info( 408 | f'Created new profile for {pathname} ({event.profile_key}).' 409 | ) 410 | else: 411 | logger.info(f'Created new profile for {pathname}.') 412 | 413 | @ringbuf_callback(self.bpf, 'anomaly_events') 414 | def anomaly_events(ctx, event, size): 415 | """ 416 | anomaly_events. 417 | 418 | Log anomalies. 419 | """ 420 | exe = self.profile_key_to_exe[event.profile_key] 421 | number = event.syscall 422 | name = EBPH_LSM.get_name(number) 423 | misses = event.misses 424 | pid = event.pid 425 | count = event.task_count 426 | 427 | logger.audit( 428 | f'Anomalous {name} ({misses} misses) ' 429 | f'in PID {pid} ({exe}) after {count} calls.' 430 | ) 431 | 432 | @ringbuf_callback(self.bpf, 'new_sequence_events') 433 | def new_sequence_events(ctx, event, size): 434 | """ 435 | new_sequence_events. 436 | 437 | Log new sequences. 438 | """ 439 | exe = self.profile_key_to_exe[event.profile_key] 440 | if not exe: 441 | exe = event.profile_key 442 | sequence = [ 443 | EBPH_LSM.get_name(call) 444 | for call in event.sequence 445 | if call != defs.BPF_DEFINES['EBPH_EMPTY'] 446 | ] 447 | sequence = reversed(sequence) 448 | pid = event.pid 449 | profile_count = event.profile_count 450 | task_count = event.task_count 451 | 452 | logger.debug( 453 | f'New sequence in PID {pid} ({exe}), task count = {task_count}, profile count = {profile_count}.' 454 | ) 455 | logger.sequence(f'PID {pid} ({exe}): ' + ', '.join(sequence)) 456 | 457 | @ringbuf_callback(self.bpf, 'start_normal_events') 458 | def start_normal_events(ctx, event, size): 459 | """ 460 | start_normal_events. 461 | 462 | Log when a profile starts normal monitoring. 463 | """ 464 | exe = self.profile_key_to_exe[event.profile_key] 465 | profile_count = event.profile_count 466 | sequences = event.sequences 467 | train_count = event.train_count 468 | last_mod_count = event.last_mod_count 469 | 470 | in_task = event.in_task 471 | task_count = event.task_count 472 | pid = event.pid 473 | 474 | if in_task: 475 | logger.info( 476 | f'PID {pid} ({exe}) now has {train_count} ' 477 | f'training calls and {last_mod_count} since last ' 478 | f'change ({profile_count} total).' 479 | ) 480 | logger.info( 481 | f'Starting normal monitoring in PID {pid} ({exe}) ' 482 | f'after {task_count} calls ({sequences} sequences).' 483 | ) 484 | else: 485 | logger.info( 486 | f'{exe} now has {train_count} ' 487 | f'training calls and {last_mod_count} since last ' 488 | f'change ({profile_count} total).' 489 | ) 490 | logger.info( 491 | f'Starting normal monitoring for {exe} ' 492 | f'with {sequences} sequences.' 493 | ) 494 | 495 | @ringbuf_callback(self.bpf, 'stop_normal_events') 496 | def stop_normal_events(ctx, event, size): 497 | """ 498 | stop_normal_events. 499 | 500 | Log when a profile stops normal monitoring. 501 | """ 502 | exe = self.profile_key_to_exe[event.profile_key] 503 | anomalies = event.anomalies 504 | anomaly_limit = event.anomaly_limit 505 | 506 | in_task = event.in_task 507 | task_count = event.task_count 508 | pid = event.pid 509 | 510 | if in_task: 511 | logger.info( 512 | f'Stopped normal monitoring in PID {pid} ({exe}) ' 513 | f'after {task_count} calls and {anomalies} anomalies ' 514 | f'(limit {anomaly_limit}).' 515 | ) 516 | else: 517 | logger.info( 518 | f'Stopped normal monitoring for {exe} ' 519 | f'with {anomalies} anomalies (limit {anomaly_limit}).' 520 | ) 521 | 522 | @ringbuf_callback(self.bpf, 'tolerize_limit_events', ratelimit_per_sec=10) 523 | def tolerize_limit_events(ctx, event, size): 524 | """ 525 | tolerize_limit_events. 526 | 527 | Callback for when a process exceeds its tolerize limit. 528 | """ 529 | profile_key = event.profile_key 530 | pid = event.pid 531 | lfc = event.lfc 532 | exe = self.profile_key_to_exe[profile_key] 533 | 534 | logger.info(f'Tolerize limit exceeded for PID {pid} ({exe}), LFC is {lfc}. Training data reset.') 535 | 536 | def _generate_syscall_defines(self, flags: List[str]) -> None: 537 | from bcc.syscall import syscalls 538 | 539 | for num, name in syscalls.items(): 540 | name = name.decode('utf-8').upper() 541 | self.syscall_number_to_name[num] = name 542 | definition = f'-DEBPH_SYS_{name}={num}' 543 | flags.append(definition) 544 | 545 | def _calculate_boot_epoch(self): 546 | boot_time = time.monotonic() * int(1e9) 547 | boot_epoch = time.time() * int(1e9) - boot_time 548 | return int(boot_epoch) 549 | 550 | def _bootstrap_processes(self): 551 | for profile_key, exe, pid, tid in running_processes(): 552 | logger.debug(f'Found process {pid},{tid} running {exe} ({profile_key})') 553 | Lib.bootstrap_process(profile_key, tid, pid, exe.encode('ascii')) 554 | self.bpf.ring_buffer_consume() 555 | 556 | def _set_cflags(self) -> None: 557 | logger.info('Setting cflags...') 558 | 559 | self.cflags.append(f'-I{defs.BPF_DIR}') 560 | for k, v in defs.BPF_DEFINES.items(): 561 | self.cflags.append(f'-D{k}={v}') 562 | 563 | if self.debug: 564 | self.cflags.append('-DEBPH_DEBUG') 565 | 566 | for flag in self.cflags: 567 | logger.debug(f'Using {flag}...') 568 | 569 | self.cflags.append( 570 | f'-DEBPH_BOOT_EPOCH=((u64){self._calculate_boot_epoch()})' 571 | ) 572 | self._generate_syscall_defines(self.cflags) 573 | 574 | def _load_bpf(self) -> None: 575 | assert self.bpf is None 576 | logger.info('Loading BPF program...') 577 | 578 | with open(defs.BPF_PROGRAM_C, 'r') as f: 579 | bpf_text = f.read() 580 | 581 | self.bpf = BPF( 582 | text=bpf_text, usdt_contexts=[Lib.usdt_context], cflags=self.cflags 583 | ) 584 | # FIXME: BPF cleanup function is segfaulting, so unregister it for now. 585 | # It actually doesn't really do anything particularly useful. 586 | atexit.unregister(self.bpf.cleanup) 587 | 588 | def _cleanup(self) -> None: 589 | if self.auto_save: 590 | self.save_profiles() 591 | del self.bpf 592 | self.bpf = None 593 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | --------------------------------------------------------------------------------