├── 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 |
--------------------------------------------------------------------------------