Agent: Accept a "leave_readme" Callable instead of copy_file

This commit is contained in:
Mike Salvatore 2021-07-13 16:17:26 -04:00
parent 45a382f5ff
commit 222c394dbc
7 changed files with 92 additions and 52 deletions

View File

@ -1,7 +1,6 @@
import argparse import argparse
import logging import logging
import os import os
import shutil
import subprocess import subprocess
import sys import sys
import time import time
@ -20,6 +19,7 @@ from infection_monkey.network.HostFinger import HostFinger
from infection_monkey.network.network_scanner import NetworkScanner from infection_monkey.network.network_scanner import NetworkScanner
from infection_monkey.network.tools import get_interface_to_target, is_running_on_island from infection_monkey.network.tools import get_interface_to_target, is_running_on_island
from infection_monkey.post_breach.post_breach_handler import PostBreach from infection_monkey.post_breach.post_breach_handler import PostBreach
from infection_monkey.ransomware import readme_utils
from infection_monkey.ransomware.ransomware_payload import RansomwarePayload from infection_monkey.ransomware.ransomware_payload import RansomwarePayload
from infection_monkey.system_info import SystemInfoCollector from infection_monkey.system_info import SystemInfoCollector
from infection_monkey.system_singleton import SystemSingleton from infection_monkey.system_singleton import SystemSingleton
@ -478,7 +478,9 @@ class InfectionMonkey(object):
try: try:
RansomwarePayload( RansomwarePayload(
WormConfiguration.ransomware, batching_telemetry_messenger, shutil.copyfile WormConfiguration.ransomware,
readme_utils.leave_readme,
batching_telemetry_messenger,
).run_payload() ).run_payload()
except Exception as ex: except Exception as ex:
LOG.error(f"An unexpected error occurred while running the ransomware payload: {ex}") LOG.error(f"An unexpected error occurred while running the ransomware payload: {ex}")

View File

@ -24,8 +24,8 @@ class RansomwarePayload:
def __init__( def __init__(
self, self,
config: dict, config: dict,
leave_readme: Callable[[Path, Path], None],
telemetry_messenger: ITelemetryMessenger, telemetry_messenger: ITelemetryMessenger,
copy_file: Callable[[Path, Path], None],
): ):
LOG.debug(f"Ransomware payload configuration:\n{pformat(config)}") LOG.debug(f"Ransomware payload configuration:\n{pformat(config)}")
@ -38,7 +38,7 @@ class RansomwarePayload:
self._targeted_file_extensions.discard(self._new_file_extension) self._targeted_file_extensions.discard(self._new_file_extension)
self._encryptor = BitflipEncryptor(chunk_size=CHUNK_SIZE) self._encryptor = BitflipEncryptor(chunk_size=CHUNK_SIZE)
self._copy_file = copy_file self._leave_readme = leave_readme
self._telemetry_messenger = telemetry_messenger self._telemetry_messenger = telemetry_messenger
@staticmethod @staticmethod
@ -66,7 +66,7 @@ class RansomwarePayload:
self._encrypt_files(file_list) self._encrypt_files(file_list)
if self._readme_enabled: if self._readme_enabled:
self._leave_readme() self._leave_readme(README_SRC, self._target_dir / README_DEST)
def _find_files(self) -> List[Path]: def _find_files(self) -> List[Path]:
LOG.info(f"Collecting files in {self._target_dir}") LOG.info(f"Collecting files in {self._target_dir}")
@ -97,21 +97,3 @@ class RansomwarePayload:
def _send_telemetry(self, filepath: Path, success: bool, error: str): def _send_telemetry(self, filepath: Path, success: bool, error: str):
encryption_attempt = FileEncryptionTelem(str(filepath), success, error) encryption_attempt = FileEncryptionTelem(str(filepath), success, error)
self._telemetry_messenger.send_telemetry(encryption_attempt) self._telemetry_messenger.send_telemetry(encryption_attempt)
def _leave_readme(self):
readme_dest_path = self._target_dir / README_DEST
if readme_dest_path.exists():
LOG.warning(f"{readme_dest_path} already exists, not leaving a new README.txt")
return
self._copy_readme_file(readme_dest_path)
def _copy_readme_file(self, dest: Path):
LOG.info(f"Leaving a ransomware README file at {dest}")
try:
self._copy_file(README_SRC, dest)
except Exception as ex:
LOG.warning(f"An error occurred while attempting to leave a README.txt file: {ex}")

View File

@ -0,0 +1,22 @@
import logging
import shutil
from pathlib import Path
LOG = logging.getLogger(__name__)
def leave_readme(src: Path, dest: Path):
if dest.exists():
LOG.warning(f"{dest} already exists, not leaving a new README.txt")
return
_copy_readme_file(src, dest)
def _copy_readme_file(src: Path, dest: Path):
LOG.info(f"Leaving a ransomware README file at {dest}")
try:
shutil.copyfile(src, dest)
except Exception as ex:
LOG.warning(f"An error occurred while attempting to leave a README.txt file: {ex}")

View File

@ -10,4 +10,4 @@ sys.path.insert(0, MONKEY_BASE_PATH)
@pytest.fixture(scope="session") @pytest.fixture(scope="session")
def data_for_tests_dir(pytestconfig): def data_for_tests_dir(pytestconfig):
return os.path.join(pytestconfig.rootdir, "monkey", "tests", "data_for_tests") return Path(os.path.join(pytestconfig.rootdir, "monkey", "tests", "data_for_tests"))

View File

@ -0,0 +1 @@
Hello, World!

View File

@ -1,6 +1,5 @@
import os import os
import shutil from pathlib import PurePosixPath
from pathlib import Path, PurePosixPath
from unittest.mock import MagicMock from unittest.mock import MagicMock
import pytest import pytest
@ -24,7 +23,12 @@ from tests.unit_tests.infection_monkey.ransomware.ransomware_target_files import
from tests.utils import hash_file, is_user_admin from tests.utils import hash_file, is_user_admin
from infection_monkey.ransomware import ransomware_payload as ransomware_payload_module from infection_monkey.ransomware import ransomware_payload as ransomware_payload_module
from infection_monkey.ransomware.ransomware_payload import EXTENSION, README_DEST, RansomwarePayload from infection_monkey.ransomware.ransomware_payload import (
EXTENSION,
README_DEST,
README_SRC,
RansomwarePayload,
)
def with_extension(filename): def with_extension(filename):
@ -51,13 +55,18 @@ def ransomware_payload(build_ransomware_payload, ransomware_payload_config):
@pytest.fixture @pytest.fixture
def build_ransomware_payload(telemetry_messenger_spy): def build_ransomware_payload(telemetry_messenger_spy, mock_leave_readme):
def inner(config): def inner(config):
return RansomwarePayload(config, telemetry_messenger_spy, shutil.copyfile) return RansomwarePayload(config, mock_leave_readme, telemetry_messenger_spy)
return inner return inner
@pytest.fixture
def mock_leave_readme():
return MagicMock()
def test_env_variables_in_target_dir_resolved_linux( def test_env_variables_in_target_dir_resolved_linux(
ransomware_payload_config, build_ransomware_payload, ransomware_target, patched_home_env ransomware_payload_config, build_ransomware_payload, ransomware_target, patched_home_env
): ):
@ -215,49 +224,41 @@ def test_telemetry_failure(monkeypatch, ransomware_payload, telemetry_messenger_
assert "No such file or directory" in telem_1.get_data()["files"][0]["error"] assert "No such file or directory" in telem_1.get_data()["files"][0]["error"]
def test_readme_false(build_ransomware_payload, ransomware_payload_config, ransomware_target): def test_readme_false(
build_ransomware_payload, ransomware_payload_config, mock_leave_readme, ransomware_target
):
ransomware_payload_config["other_behaviors"]["readme"] = False ransomware_payload_config["other_behaviors"]["readme"] = False
ransomware_payload = build_ransomware_payload(ransomware_payload_config) ransomware_payload = build_ransomware_payload(ransomware_payload_config)
ransomware_payload.run_payload() ransomware_payload.run_payload()
assert not Path(ransomware_target / README_DEST).exists() mock_leave_readme.assert_not_called()
def test_readme_true(build_ransomware_payload, ransomware_payload_config, ransomware_target): def test_readme_true(
build_ransomware_payload, ransomware_payload_config, mock_leave_readme, ransomware_target
):
ransomware_payload_config["other_behaviors"]["readme"] = True ransomware_payload_config["other_behaviors"]["readme"] = True
ransomware_payload = build_ransomware_payload(ransomware_payload_config) ransomware_payload = build_ransomware_payload(ransomware_payload_config)
ransomware_payload.run_payload() ransomware_payload.run_payload()
assert Path(ransomware_target / README_DEST).exists() mock_leave_readme.assert_called_with(README_SRC, ransomware_target / README_DEST)
def test_readme_already_exists(
monkeypatch, ransomware_payload_config, telemetry_messenger_spy, ransomware_target
):
monkeypatch.setattr(ransomware_payload_module, "TARGETED_FILE_EXTENSIONS", set()),
mock_copy_file = MagicMock()
ransomware_payload_config["other_behaviors"]["readme"] = True
Path(ransomware_target / README_DEST).touch()
RansomwarePayload(
ransomware_payload_config, telemetry_messenger_spy, mock_copy_file
).run_payload()
mock_copy_file.assert_not_called()
def test_no_readme_if_no_directory( def test_no_readme_if_no_directory(
monkeypatch, ransomware_payload_config, telemetry_messenger_spy, ransomware_target monkeypatch,
ransomware_payload_config,
mock_leave_readme,
telemetry_messenger_spy,
ransomware_target,
): ):
monkeypatch.setattr(ransomware_payload_module, "TARGETED_FILE_EXTENSIONS", set()), monkeypatch.setattr(ransomware_payload_module, "TARGETED_FILE_EXTENSIONS", set()),
mock_copy_file = MagicMock()
ransomware_payload_config["encryption"]["directories"]["linux_target_dir"] = "" ransomware_payload_config["encryption"]["directories"]["linux_target_dir"] = ""
ransomware_payload_config["encryption"]["directories"]["windows_target_dir"] = "" ransomware_payload_config["encryption"]["directories"]["windows_target_dir"] = ""
ransomware_payload_config["other_behaviors"]["readme"] = True ransomware_payload_config["other_behaviors"]["readme"] = True
RansomwarePayload( RansomwarePayload(
ransomware_payload_config, telemetry_messenger_spy, mock_copy_file ransomware_payload_config, mock_leave_readme, telemetry_messenger_spy
).run_payload() ).run_payload()
mock_copy_file.assert_not_called() mock_leave_readme.assert_not_called()

View File

@ -0,0 +1,32 @@
import pytest
from tests.utils import hash_file
from infection_monkey.ransomware.readme_utils import leave_readme
DEST_FILE = "README.TXT"
README_HASH = "c98c24b677eff44860afea6f493bbaec5bb1c4cbb209c6fc2bbb47f66ff2ad31"
EMPTY_FILE_HASH = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
@pytest.fixture(scope="module")
def src_readme(data_for_tests_dir):
return data_for_tests_dir / "test_readme.txt"
@pytest.fixture
def dest_readme(tmp_path):
return tmp_path / DEST_FILE
def test_readme_already_exists(src_readme, dest_readme):
dest_readme.touch()
leave_readme(src_readme, dest_readme)
assert hash_file(dest_readme) == EMPTY_FILE_HASH
def test_leave_readme(src_readme, dest_readme):
leave_readme(src_readme, dest_readme)
assert hash_file(dest_readme) == README_HASH