From d600aa720894ed49b9381b432c6fb4747898dc17 Mon Sep 17 00:00:00 2001 From: Shreya Date: Tue, 22 Jun 2021 15:10:43 +0530 Subject: [PATCH 01/10] telem: Add telem category for ransomware --- monkey/common/common_consts/telem_categories.py | 1 + vulture_allowlist.py | 1 + 2 files changed, 2 insertions(+) diff --git a/monkey/common/common_consts/telem_categories.py b/monkey/common/common_consts/telem_categories.py index 280cfce05..dc083d4ab 100644 --- a/monkey/common/common_consts/telem_categories.py +++ b/monkey/common/common_consts/telem_categories.py @@ -8,3 +8,4 @@ class TelemCategoryEnum: TRACE = "trace" TUNNEL = "tunnel" ATTACK = "attack" + RANSOMWARE = "ransomware" diff --git a/vulture_allowlist.py b/vulture_allowlist.py index 2c937ee4f..68acc35e0 100644 --- a/vulture_allowlist.py +++ b/vulture_allowlist.py @@ -171,6 +171,7 @@ ISLAND # unused variable (monkey/monkey_island/cc/services/utils/node_states.py MONKEY_LINUX_RUNNING # unused variable (monkey/monkey_island/cc/services/utils/node_states.py:26) import_status # monkey_island\cc\resources\configuration_import.py:19 config_schema # monkey_island\cc\resources\configuration_import.py:25 +RansomwareTelem # monkey/infection_monkey/telemetry/ransomware_telem.py:7 # these are not needed for it to work, but may be useful extra information to understand what's going on WINDOWS_PBA_TYPE # unused variable (monkey/monkey_island/cc/resources/pba_file_upload.py:23) From 29bd48f70378124aee1eab40adc1b782de800ac2 Mon Sep 17 00:00:00 2001 From: Shreya Date: Tue, 22 Jun 2021 15:23:45 +0530 Subject: [PATCH 02/10] telem: Add ransomware telemetry --- monkey/infection_monkey/monkey.py | 4 +++- .../telemetry/ransomware_telem.py | 22 +++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 monkey/infection_monkey/telemetry/ransomware_telem.py diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index abd0b3f18..0b45d3fbd 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -25,6 +25,7 @@ from infection_monkey.system_singleton import SystemSingleton from infection_monkey.telemetry.attack.t1106_telem import T1106Telem from infection_monkey.telemetry.attack.t1107_telem import T1107Telem from infection_monkey.telemetry.attack.victim_host_telem import VictimHostTelem +from infection_monkey.telemetry.ransomware_telem import RansomwareTelem from infection_monkey.telemetry.scan_telem import ScanTelem from infection_monkey.telemetry.state_telem import StateTelem from infection_monkey.telemetry.system_info_telem import SystemInfoTelem @@ -233,7 +234,8 @@ class InfectionMonkey(object): if not self._keep_running: break - RansomewarePayload(WormConfiguration.ransomware).run_payload() + ransomware_attempts = RansomewarePayload(WormConfiguration.ransomware).run_payload() + RansomwareTelem(ransomware_attempts).send() if (not is_empty) and (WormConfiguration.max_iterations > iteration_index + 1): time_to_sleep = WormConfiguration.timeout_between_iterations diff --git a/monkey/infection_monkey/telemetry/ransomware_telem.py b/monkey/infection_monkey/telemetry/ransomware_telem.py new file mode 100644 index 000000000..c56e8337c --- /dev/null +++ b/monkey/infection_monkey/telemetry/ransomware_telem.py @@ -0,0 +1,22 @@ +from typing import List, Tuple + +from common.common_consts.telem_categories import TelemCategoryEnum +from infection_monkey.telemetry.base_telem import BaseTelem + + +class RansomwareTelem(BaseTelem): + def __init__(self, attempts: List[Tuple[str, str]]): + """ + Ransomware telemetry constructor + :param attempts: List of tuples with each tuple containing the path + of a file it tried encrypting and its result. + If ransomware fails completely - list of one tuple + containing the directory path and error string. + """ + super().__init__() + self.attempts = attempts + + telem_category = TelemCategoryEnum.RANSOMWARE + + def get_data(self): + return {"ransomware_attempts": self.attempts} From cec8341b172baddc3e075f17bd62ff006870896a Mon Sep 17 00:00:00 2001 From: Shreya Date: Wed, 23 Jun 2021 15:38:27 +0530 Subject: [PATCH 03/10] tests: Add unit tests for ransomware telem --- .../telemetry/test_ransomware_telem.py | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 monkey/tests/unit_tests/infection_monkey/telemetry/test_ransomware_telem.py diff --git a/monkey/tests/unit_tests/infection_monkey/telemetry/test_ransomware_telem.py b/monkey/tests/unit_tests/infection_monkey/telemetry/test_ransomware_telem.py new file mode 100644 index 000000000..4994c9287 --- /dev/null +++ b/monkey/tests/unit_tests/infection_monkey/telemetry/test_ransomware_telem.py @@ -0,0 +1,20 @@ +import json + +import pytest + +from infection_monkey.telemetry.ransomware_telem import RansomwareTelem + +ATTEMPTS = [("", "")] + + +@pytest.fixture +def ransomware_telem_test_instance(): + return RansomwareTelem(ATTEMPTS) + + +def test_ransomware_telem_send(ransomware_telem_test_instance, spy_send_telemetry): + ransomware_telem_test_instance.send() + expected_data = {"ransomware_attempts": ATTEMPTS} + expected_data = json.dumps(expected_data, cls=ransomware_telem_test_instance.json_encoder) + assert spy_send_telemetry.data == expected_data + assert spy_send_telemetry.telem_category == "ransomware" From 77e3c8a257443713477260a53762932d8a5278ad Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 23 Jun 2021 20:06:36 -0400 Subject: [PATCH 04/10] agent: Add telemetry messenger interface The telemetry classes have too many responsibilities. At the moment, one such responsibility is to send themselves to the island. As our plugin interfaces develop, the need may arise to send telemetry using different mechanisms. To isolate the RansomwarePayload from these changes, the ITelemetryMessenger interface is introduced in this commit. It provides a send_telemetry() method that handles the specific details of how telemetry is sent to the Island. At the present time, the TelemetryMessengerWrapper class is introduced to handle sending telemetry. It simply wraps the existing send() method on the telemetry class. --- .../telemetry/messengers/i_telemetry_messenger.py | 9 +++++++++ .../telemetry/messengers/telemetry_messenger_wrapper.py | 7 +++++++ 2 files changed, 16 insertions(+) create mode 100644 monkey/infection_monkey/telemetry/messengers/i_telemetry_messenger.py create mode 100644 monkey/infection_monkey/telemetry/messengers/telemetry_messenger_wrapper.py diff --git a/monkey/infection_monkey/telemetry/messengers/i_telemetry_messenger.py b/monkey/infection_monkey/telemetry/messengers/i_telemetry_messenger.py new file mode 100644 index 000000000..7cc2efa01 --- /dev/null +++ b/monkey/infection_monkey/telemetry/messengers/i_telemetry_messenger.py @@ -0,0 +1,9 @@ +import abc + +from infection_monkey.telemetry.base_telem import BaseTelem + + +class ITelemetryMessenger(metaclass=abc.ABCMeta): + @abc.abstractmethod + def send_telemetry(self, telemetry: BaseTelem): + pass diff --git a/monkey/infection_monkey/telemetry/messengers/telemetry_messenger_wrapper.py b/monkey/infection_monkey/telemetry/messengers/telemetry_messenger_wrapper.py new file mode 100644 index 000000000..d00f0dd23 --- /dev/null +++ b/monkey/infection_monkey/telemetry/messengers/telemetry_messenger_wrapper.py @@ -0,0 +1,7 @@ +from infection_monkey.telemetry.base_telem import BaseTelem +from infection_monkey.telemetry.messengers.i_telemetry_messenger import ITelemetryMessenger + + +class TelemetryMessengerWrapper(ITelemetryMessenger): + def send_telemetry(self, telemetry: BaseTelem): + telemetry.send() From 46da0b7b1f282e21ebc7ecb1b3c36a543a62515b Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 24 Jun 2021 12:07:14 -0400 Subject: [PATCH 05/10] agent: Add ITelem interface Create a telemetry interface that sits above the BaseTelem abstract class to allow telemetries to be extended without inheritance. --- .../infection_monkey/telemetry/base_telem.py | 21 ++------------ monkey/infection_monkey/telemetry/i_telem.py | 29 +++++++++++++++++++ 2 files changed, 31 insertions(+), 19 deletions(-) create mode 100644 monkey/infection_monkey/telemetry/i_telem.py diff --git a/monkey/infection_monkey/telemetry/base_telem.py b/monkey/infection_monkey/telemetry/base_telem.py index 0fcf4b203..4a37a9eb9 100644 --- a/monkey/infection_monkey/telemetry/base_telem.py +++ b/monkey/infection_monkey/telemetry/base_telem.py @@ -3,6 +3,7 @@ import json import logging from infection_monkey.control import ControlClient +from infection_monkey.telemetry.i_telem import ITelem logger = logging.getLogger(__name__) LOGGED_DATA_LENGTH = 300 # How many characters of telemetry data will be logged @@ -24,14 +25,11 @@ __author__ = "itay.mizeretz" # logging and sending them. -class BaseTelem(object, metaclass=abc.ABCMeta): +class BaseTelem(ITelem, metaclass=abc.ABCMeta): """ Abstract base class for telemetry. """ - def __init__(self): - pass - def send(self, log_data=True): """ Sends telemetry to island @@ -41,13 +39,6 @@ class BaseTelem(object, metaclass=abc.ABCMeta): self._log_telem_sending(serialized_data, log_data) ControlClient.send_telemetry(self.telem_category, serialized_data) - @abc.abstractmethod - def get_data(self) -> dict: - """ - :return: Data of telemetry (should be dict) - """ - pass - @property def json_encoder(self): return json.JSONEncoder @@ -57,14 +48,6 @@ class BaseTelem(object, metaclass=abc.ABCMeta): if log_data: logger.debug(f"Telemetry contents: {BaseTelem._truncate_data(serialized_data)}") - @property - @abc.abstractmethod - def telem_category(self): - """ - :return: Telemetry type - """ - pass - @staticmethod def _truncate_data(data: str): if len(data) <= LOGGED_DATA_LENGTH: diff --git a/monkey/infection_monkey/telemetry/i_telem.py b/monkey/infection_monkey/telemetry/i_telem.py new file mode 100644 index 000000000..faaa0a65e --- /dev/null +++ b/monkey/infection_monkey/telemetry/i_telem.py @@ -0,0 +1,29 @@ +import abc + + +class ITelem(metaclass=abc.ABCMeta): + @abc.abstractmethod + def send(self, log_data=True): + """ + Sends telemetry to island + """ + + @abc.abstractmethod + def get_data(self) -> dict: + """ + :return: Data of telemetry (should be dict) + """ + pass + + @property + @abc.abstractmethod + def json_encoder(self): + pass + + @property + @abc.abstractmethod + def telem_category(self): + """ + :return: Telemetry type + """ + pass From 21525be192aef661bf630467bf7fef1b131ff8fa Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 24 Jun 2021 12:33:48 -0400 Subject: [PATCH 06/10] agent: Use ITelem in ITelemetryMessenger.send() typehint --- .../telemetry/messengers/i_telemetry_messenger.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey/infection_monkey/telemetry/messengers/i_telemetry_messenger.py b/monkey/infection_monkey/telemetry/messengers/i_telemetry_messenger.py index 7cc2efa01..cf5511e83 100644 --- a/monkey/infection_monkey/telemetry/messengers/i_telemetry_messenger.py +++ b/monkey/infection_monkey/telemetry/messengers/i_telemetry_messenger.py @@ -1,9 +1,9 @@ import abc -from infection_monkey.telemetry.base_telem import BaseTelem +from infection_monkey.telemetry.i_telem import ITelem class ITelemetryMessenger(metaclass=abc.ABCMeta): @abc.abstractmethod - def send_telemetry(self, telemetry: BaseTelem): + def send_telemetry(self, telemetry: ITelem): pass From 76da583420297e509281af94e0c24371b8af2a5f Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 24 Jun 2021 15:39:02 -0400 Subject: [PATCH 07/10] agent: Send telemetry from ransomware payload --- monkey/infection_monkey/monkey.py | 8 ++-- .../ransomware/ransomware_payload.py | 13 +++-- .../ransomware/test_ransomware_payload.py | 48 ++++++++++++++++++- 3 files changed, 61 insertions(+), 8 deletions(-) diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 0b45d3fbd..122e9e459 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -25,7 +25,9 @@ from infection_monkey.system_singleton import SystemSingleton from infection_monkey.telemetry.attack.t1106_telem import T1106Telem from infection_monkey.telemetry.attack.t1107_telem import T1107Telem from infection_monkey.telemetry.attack.victim_host_telem import VictimHostTelem -from infection_monkey.telemetry.ransomware_telem import RansomwareTelem +from infection_monkey.telemetry.messengers.telemetry_messenger_wrapper import ( + TelemetryMessengerWrapper, +) from infection_monkey.telemetry.scan_telem import ScanTelem from infection_monkey.telemetry.state_telem import StateTelem from infection_monkey.telemetry.system_info_telem import SystemInfoTelem @@ -234,8 +236,8 @@ class InfectionMonkey(object): if not self._keep_running: break - ransomware_attempts = RansomewarePayload(WormConfiguration.ransomware).run_payload() - RansomwareTelem(ransomware_attempts).send() + telemetry_messenger = TelemetryMessengerWrapper() + RansomewarePayload(WormConfiguration.ransomware, telemetry_messenger).run_payload() if (not is_empty) and (WormConfiguration.max_iterations > iteration_index + 1): time_to_sleep = WormConfiguration.timeout_between_iterations diff --git a/monkey/infection_monkey/ransomware/ransomware_payload.py b/monkey/infection_monkey/ransomware/ransomware_payload.py index 460b0fb4c..49525902b 100644 --- a/monkey/infection_monkey/ransomware/ransomware_payload.py +++ b/monkey/infection_monkey/ransomware/ransomware_payload.py @@ -5,6 +5,8 @@ from typing import List, Optional, Tuple from infection_monkey.ransomware.bitflip_encryptor import BitflipEncryptor from infection_monkey.ransomware.file_selectors import select_production_safe_target_files from infection_monkey.ransomware.valid_file_extensions import VALID_FILE_EXTENSIONS_FOR_ENCRYPTION +from infection_monkey.telemetry.messengers.i_telemetry_messenger import ITelemetryMessenger +from infection_monkey.telemetry.ransomware_telem import RansomwareTelem from infection_monkey.utils.environment import is_windows_os LOG = logging.getLogger(__name__) @@ -14,7 +16,7 @@ CHUNK_SIZE = 4096 * 24 class RansomewarePayload: - def __init__(self, config: dict): + def __init__(self, config: dict, telemetry_messenger: ITelemetryMessenger): LOG.info(f"Windows dir configured for encryption is \"{config['windows_dir']}\"") LOG.info(f"Linux dir configured for encryption is \"{config['linux_dir']}\"") @@ -25,6 +27,7 @@ class RansomewarePayload: self._valid_file_extensions_for_encryption.discard(self._new_file_extension) self._encryptor = BitflipEncryptor(chunk_size=CHUNK_SIZE) + self._telemetry_messenger = telemetry_messenger def run_payload(self): file_list = self._find_files() @@ -44,12 +47,16 @@ class RansomewarePayload: try: self._encryptor.encrypt_file_in_place(filepath) self._add_extension(filepath) - results.append((filepath, None)) + self._send_telemetry(filepath, "") except Exception as ex: - results.append((filepath, ex)) + self._send_telemetry(filepath, str(ex)) return results def _add_extension(self, filepath: Path): new_filepath = filepath.with_suffix(f"{filepath.suffix}{self._new_file_extension}") filepath.rename(new_filepath) + + def _send_telemetry(self, filepath: Path, error: str): + encryption_attempt = RansomwareTelem((str(filepath), str(error))) + self._telemetry_messenger.send_telemetry(encryption_attempt) diff --git a/monkey/tests/unit_tests/infection_monkey/ransomware/test_ransomware_payload.py b/monkey/tests/unit_tests/infection_monkey/ransomware/test_ransomware_payload.py index d5a155f48..138c60004 100644 --- a/monkey/tests/unit_tests/infection_monkey/ransomware/test_ransomware_payload.py +++ b/monkey/tests/unit_tests/infection_monkey/ransomware/test_ransomware_payload.py @@ -1,4 +1,5 @@ import os +from pathlib import PurePath import pytest from tests.unit_tests.infection_monkey.ransomware.ransomware_target_files import ( @@ -20,7 +21,18 @@ from tests.unit_tests.infection_monkey.ransomware.ransomware_target_files import ) from tests.utils import hash_file, is_user_admin +from infection_monkey.ransomware import ransomware_payload as ransomware_payload_module from infection_monkey.ransomware.ransomware_payload import EXTENSION, RansomewarePayload +from infection_monkey.telemetry.i_telem import ITelem +from infection_monkey.telemetry.messengers.i_telemetry_messenger import ITelemetryMessenger + + +class TelemetryMessengerSpy(ITelemetryMessenger): + def __init__(self): + self.telemetries = [] + + def send_telemetry(self, telemetry: ITelem): + self.telemetries.append(telemetry) def with_extension(filename): @@ -33,8 +45,13 @@ def ransomware_payload_config(ransomware_target): @pytest.fixture -def ransomware_payload(ransomware_payload_config): - return RansomewarePayload(ransomware_payload_config) +def telemetry_messenger_spy(): + return TelemetryMessengerSpy() + + +@pytest.fixture +def ransomware_payload(ransomware_payload_config, telemetry_messenger_spy): + return RansomewarePayload(ransomware_payload_config, telemetry_messenger_spy) def test_file_with_excluded_extension_not_encrypted(ransomware_target, ransomware_payload): @@ -120,3 +137,30 @@ def test_skip_already_encrypted_file(ransomware_target, ransomware_payload): hash_file(ransomware_target / ALREADY_ENCRYPTED_TXT_M0NK3Y) == ALREADY_ENCRYPTED_TXT_M0NK3Y_CLEARTEXT_SHA256 ) + + +def test_telemetry_success(ransomware_payload, telemetry_messenger_spy): + ransomware_payload.run_payload() + + assert len(telemetry_messenger_spy.telemetries) == 2 + telem_1 = telemetry_messenger_spy.telemetries[0] + telem_2 = telemetry_messenger_spy.telemetries[1] + + assert ALL_ZEROS_PDF in telem_1.get_data()["ransomware_attempts"][0] + assert telem_1.get_data()["ransomware_attempts"][1] == "" + assert TEST_KEYBOARD_TXT in telem_2.get_data()["ransomware_attempts"][0] + assert telem_2.get_data()["ransomware_attempts"][1] == "" + + +def test_telemetry_failure(monkeypatch, ransomware_payload, telemetry_messenger_spy): + monkeypatch.setattr( + ransomware_payload_module, + "select_production_safe_target_files", + lambda a, b: [PurePath("/file/not/exist")], + ), + + ransomware_payload.run_payload() + telem_1 = telemetry_messenger_spy.telemetries[0] + + assert "/file/not/exist" in telem_1.get_data()["ransomware_attempts"][0] + assert "No such file or directory" in telem_1.get_data()["ransomware_attempts"][1] From 7b9c39edc6ecb23850de749d44d96abfaef7c3ac Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 24 Jun 2021 15:55:17 -0400 Subject: [PATCH 08/10] Remove RansomwareTelem from vulture_allowlist --- vulture_allowlist.py | 1 - 1 file changed, 1 deletion(-) diff --git a/vulture_allowlist.py b/vulture_allowlist.py index 68acc35e0..2c937ee4f 100644 --- a/vulture_allowlist.py +++ b/vulture_allowlist.py @@ -171,7 +171,6 @@ ISLAND # unused variable (monkey/monkey_island/cc/services/utils/node_states.py MONKEY_LINUX_RUNNING # unused variable (monkey/monkey_island/cc/services/utils/node_states.py:26) import_status # monkey_island\cc\resources\configuration_import.py:19 config_schema # monkey_island\cc\resources\configuration_import.py:25 -RansomwareTelem # monkey/infection_monkey/telemetry/ransomware_telem.py:7 # these are not needed for it to work, but may be useful extra information to understand what's going on WINDOWS_PBA_TYPE # unused variable (monkey/monkey_island/cc/resources/pba_file_upload.py:23) From 6773f695ba88bbd514542c84059374fb561c9740 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 24 Jun 2021 15:57:10 -0400 Subject: [PATCH 09/10] agent: Use ITelem in send_telemetry() typehint --- .../telemetry/messengers/telemetry_messenger_wrapper.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey/infection_monkey/telemetry/messengers/telemetry_messenger_wrapper.py b/monkey/infection_monkey/telemetry/messengers/telemetry_messenger_wrapper.py index d00f0dd23..e436cdd46 100644 --- a/monkey/infection_monkey/telemetry/messengers/telemetry_messenger_wrapper.py +++ b/monkey/infection_monkey/telemetry/messengers/telemetry_messenger_wrapper.py @@ -1,7 +1,7 @@ -from infection_monkey.telemetry.base_telem import BaseTelem +from infection_monkey.telemetry.i_telem import ITelem from infection_monkey.telemetry.messengers.i_telemetry_messenger import ITelemetryMessenger class TelemetryMessengerWrapper(ITelemetryMessenger): - def send_telemetry(self, telemetry: BaseTelem): + def send_telemetry(self, telemetry: ITelem): telemetry.send() From 76cf8a1bb4414757d052bfc45126f15e8f1e61e9 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 25 Jun 2021 09:19:15 -0400 Subject: [PATCH 10/10] agent: Wrap ransomware payload build/run in run_ransomware() --- monkey/infection_monkey/monkey.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 122e9e459..a70781333 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -236,8 +236,7 @@ class InfectionMonkey(object): if not self._keep_running: break - telemetry_messenger = TelemetryMessengerWrapper() - RansomewarePayload(WormConfiguration.ransomware, telemetry_messenger).run_payload() + InfectionMonkey.run_ransomware() if (not is_empty) and (WormConfiguration.max_iterations > iteration_index + 1): time_to_sleep = WormConfiguration.timeout_between_iterations @@ -467,3 +466,8 @@ class InfectionMonkey(object): def log_arguments(self): arg_string = " ".join([f"{key}: {value}" for key, value in vars(self._opts).items()]) LOG.info(f"Monkey started with arguments: {arg_string}") + + @staticmethod + def run_ransomware(): + telemetry_messenger = TelemetryMessengerWrapper() + RansomewarePayload(WormConfiguration.ransomware, telemetry_messenger).run_payload()