Merge pull request #2206 from guardicore/1242-allow-custom-ransomware-extension
1242 allow custom ransomware extension
This commit is contained in:
commit
ce390e41b8
|
@ -21,6 +21,8 @@ Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||
- `/api/registration-status` endpoint. #2149
|
||||
- authentication to `/api/island/version`. #2109
|
||||
- `/api/events` endpoint. #2155
|
||||
- The ability to customize the file extension used by ransomware when
|
||||
encrypting files. #1242
|
||||
|
||||
### Changed
|
||||
- Reset workflow. Now it's possible to delete data gathered by agents without
|
||||
|
|
|
@ -37,15 +37,21 @@ To ensure minimum interference and easy recoverability, the ransomware
|
|||
simulation will only encrypt files contained in a user-specified directory. If
|
||||
no directory is specified, no files will be encrypted.
|
||||
|
||||
Infection Monkey appends the `.m0nk3y` file extension to files that it
|
||||
encrypts. You may optionally provide a custom file extension for Infection
|
||||
Monkey to use instead. You can even provide no file extension, but take
|
||||
caution: you'll no longer be able to tell if the file has been encrypted based
|
||||
on the filename alone!
|
||||
|
||||
![Ransomware configuration](/images/usage/scenarios/ransomware-config.png "Ransomware configuration")
|
||||
|
||||
### How are the files encrypted?
|
||||
|
||||
Files are "encrypted" in place with a simple bit flip. Encrypted files are
|
||||
renamed to have `.m0nk3y` appended to their names. This is a safe way to
|
||||
simulate encryption since it is easy to "decrypt" your files. You can simply
|
||||
perform a bit flip on the files again and rename them to remove the appended
|
||||
`.m0nk3y` extension.
|
||||
renamed to have a file extension (`.m0nk3y` by default) appended to their
|
||||
names. This is a safe way to simulate encryption since it is easy to "decrypt"
|
||||
your files. You can simply perform a bit flip on the files again and rename
|
||||
them to remove the appended `.m0nk3y` extension.
|
||||
|
||||
Flipping a file's bits is sufficient to simulate the encryption behavior of
|
||||
ransomware, as the data in your files has been manipulated (leaving them
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 140 KiB After Width: | Height: | Size: 436 KiB |
|
@ -35,6 +35,7 @@ CREDENTIAL_COLLECTOR_CONFIGURATION = tuple(
|
|||
RANSOMWARE_OPTIONS = {
|
||||
"encryption": {
|
||||
"enabled": True,
|
||||
"file_extension": ".m0nk3y",
|
||||
"directories": {"linux_target_dir": "", "windows_target_dir": ""},
|
||||
},
|
||||
"other_behaviors": {"readme": True},
|
||||
|
|
|
@ -16,7 +16,6 @@ from .ransomware import Ransomware
|
|||
from .ransomware_options import RansomwareOptions
|
||||
from .targeted_file_extensions import TARGETED_FILE_EXTENSIONS
|
||||
|
||||
EXTENSION = ".m0nk3y"
|
||||
CHUNK_SIZE = 4096 * 24
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -26,8 +25,8 @@ def build_ransomware(options: dict):
|
|||
logger.debug(f"Ransomware configuration:\n{pformat(options)}")
|
||||
ransomware_options = RansomwareOptions(options)
|
||||
|
||||
file_encryptor = _build_file_encryptor()
|
||||
file_selector = _build_file_selector()
|
||||
file_encryptor = _build_file_encryptor(ransomware_options.file_extension)
|
||||
file_selector = _build_file_selector(ransomware_options.file_extension)
|
||||
leave_readme = _build_leave_readme()
|
||||
telemetry_messenger = _build_telemetry_messenger()
|
||||
|
||||
|
@ -40,15 +39,16 @@ def build_ransomware(options: dict):
|
|||
)
|
||||
|
||||
|
||||
def _build_file_encryptor():
|
||||
def _build_file_encryptor(file_extension: str):
|
||||
return InPlaceFileEncryptor(
|
||||
encrypt_bytes=flip_bits, new_file_extension=EXTENSION, chunk_size=CHUNK_SIZE
|
||||
encrypt_bytes=flip_bits, new_file_extension=file_extension, chunk_size=CHUNK_SIZE
|
||||
)
|
||||
|
||||
|
||||
def _build_file_selector():
|
||||
def _build_file_selector(file_extension: str):
|
||||
targeted_file_extensions = TARGETED_FILE_EXTENSIONS.copy()
|
||||
targeted_file_extensions.discard(EXTENSION)
|
||||
if file_extension:
|
||||
targeted_file_extensions.discard(file_extension)
|
||||
|
||||
return ProductionSafeTargetFileSelector(targeted_file_extensions)
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ logger = logging.getLogger(__name__)
|
|||
class RansomwareOptions:
|
||||
def __init__(self, options: dict):
|
||||
self.encryption_enabled = options["encryption"]["enabled"]
|
||||
self.file_extension = options["encryption"]["file_extension"]
|
||||
self.readme_enabled = options["other_behaviors"]["readme"]
|
||||
|
||||
self.target_directory = None
|
||||
|
|
|
@ -102,6 +102,9 @@ export default function UiSchema(props) {
|
|||
info_box: {
|
||||
'ui:field': InfoBox
|
||||
},
|
||||
file_extension: {
|
||||
'ui:emptyValue': ''
|
||||
},
|
||||
directories: {
|
||||
// Directory inputs are dynamically hidden
|
||||
},
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import {IP, IP_RANGE, VALID_RANSOMWARE_TARGET_PATH_LINUX, VALID_RANSOMWARE_TARGET_PATH_WINDOWS} from './ValidationFormats';
|
||||
import { IP, IP_RANGE, VALID_FILE_EXTENSION, VALID_RANSOMWARE_TARGET_PATH_LINUX, VALID_RANSOMWARE_TARGET_PATH_WINDOWS } from './ValidationFormats';
|
||||
|
||||
let invalidDirMessage = 'Invalid directory. Path should be absolute or begin with an environment variable.';
|
||||
|
||||
|
@ -10,6 +10,8 @@ export default function transformErrors(errors) {
|
|||
error.message = 'Invalid IP range, refer to description for valid examples.'
|
||||
} else if (error.name === 'format' && error.params.format === IP) {
|
||||
error.message = 'Invalid IP.'
|
||||
} else if (error.name === 'format' && error.params.format === VALID_FILE_EXTENSION) {
|
||||
error.message = 'Invalid file extension.'
|
||||
} else if (error.name === 'format' && error.params.format === VALID_RANSOMWARE_TARGET_PATH_LINUX) {
|
||||
error.message = invalidDirMessage
|
||||
} else if (error.name === 'format' && error.params.format === VALID_RANSOMWARE_TARGET_PATH_WINDOWS) {
|
||||
|
|
|
@ -2,6 +2,7 @@ const ipRegex = '((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(25[0-5]|2[0-4][0
|
|||
const cidrNotationRegex = '([0-9]|1[0-9]|2[0-9]|3[0-2])'
|
||||
const hostnameRegex = '^([A-Za-z0-9]*[A-Za-z]+[A-Za-z0-9]*.?)*([A-Za-z0-9]*[A-Za-z]+[A-Za-z0-9]*)$'
|
||||
|
||||
const fileExtensionRegex = /^(\.[A-Za-z0-9_]+)*$/
|
||||
|
||||
const linuxAbsolutePathRegex = /^\// // path starts with `/`
|
||||
const linuxPathStartsWithEnvVariableRegex = /^\$/ // path starts with `$`
|
||||
|
@ -19,12 +20,14 @@ const emptyRegex = /^$/
|
|||
|
||||
export const IP_RANGE = 'ip-range';
|
||||
export const IP = 'ip';
|
||||
export const VALID_FILE_EXTENSION = 'valid-file-extension'
|
||||
export const VALID_RANSOMWARE_TARGET_PATH_LINUX = 'valid-ransomware-target-path-linux'
|
||||
export const VALID_RANSOMWARE_TARGET_PATH_WINDOWS = 'valid-ransomware-target-path-windows'
|
||||
|
||||
export const formValidationFormats = {
|
||||
[IP_RANGE]: buildIpRangeRegex(),
|
||||
[IP]: buildIpRegex(),
|
||||
[VALID_FILE_EXTENSION]: fileExtensionRegex,
|
||||
[VALID_RANSOMWARE_TARGET_PATH_LINUX]: buildValidRansomwarePathLinuxRegex(),
|
||||
[VALID_RANSOMWARE_TARGET_PATH_WINDOWS]: buildValidRansomwarePathWindowsRegex()
|
||||
};
|
||||
|
|
|
@ -20,6 +20,14 @@ const RANSOMWARE_SCHEMA = {
|
|||
'info': 'No files will be encrypted if a directory is not specified or doesn\'t ' +
|
||||
'exist on a victim machine.'
|
||||
},
|
||||
'file_extension': {
|
||||
'title': 'File extension',
|
||||
'type': 'string',
|
||||
'format': 'valid-file-extension',
|
||||
'default': '.m0nk3y',
|
||||
'description': 'The file extension that the Infection Monkey will use for the ' +
|
||||
'encrypted file.'
|
||||
},
|
||||
'directories': {
|
||||
'title': 'Directories to encrypt',
|
||||
'type': 'object',
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture(params=[".m0nk3y", ".test", ""], ids=["monkeyext", "testext", "noext"])
|
||||
def ransomware_file_extension(request):
|
||||
return request.param
|
|
@ -0,0 +1,29 @@
|
|||
import threading
|
||||
|
||||
import pytest
|
||||
|
||||
import infection_monkey.payload.ransomware.ransomware_builder as ransomware_builder
|
||||
from monkey.common.agent_configuration.default_agent_configuration import RANSOMWARE_OPTIONS
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def ransomware_options_dict(ransomware_file_extension):
|
||||
options = RANSOMWARE_OPTIONS
|
||||
options["encryption"]["file_extension"] = ransomware_file_extension
|
||||
return options
|
||||
|
||||
|
||||
def test_uses_correct_extension(ransomware_options_dict, tmp_path, ransomware_file_extension):
|
||||
target_dir = tmp_path
|
||||
ransomware_directories = ransomware_options_dict["encryption"]["directories"]
|
||||
ransomware_directories["linux_target_dir"] = target_dir
|
||||
ransomware_directories["windows_target_dir"] = target_dir
|
||||
ransomware = ransomware_builder.build_ransomware(ransomware_options_dict)
|
||||
file = target_dir / "file.txt"
|
||||
file.write_text("Do your worst!")
|
||||
|
||||
ransomware.run(threading.Event())
|
||||
|
||||
# Verify that the file has been encrypted with the correct ending
|
||||
encrypted_file = file.with_suffix(file.suffix + ransomware_file_extension)
|
||||
assert encrypted_file.is_file()
|
|
@ -41,14 +41,15 @@ def build_ransomware(
|
|||
|
||||
|
||||
@pytest.fixture
|
||||
def ransomware_options(ransomware_test_data):
|
||||
def ransomware_options(ransomware_file_extension, ransomware_test_data):
|
||||
class RansomwareOptionsStub(RansomwareOptions):
|
||||
def __init__(self, encryption_enabled, readme_enabled, target_directory):
|
||||
def __init__(self, encryption_enabled, readme_enabled, file_extension, target_directory):
|
||||
self.encryption_enabled = encryption_enabled
|
||||
self.readme_enabled = readme_enabled
|
||||
self.file_extension = file_extension
|
||||
self.target_directory = target_directory
|
||||
|
||||
return RansomwareOptionsStub(True, False, ransomware_test_data)
|
||||
return RansomwareOptionsStub(True, False, ransomware_file_extension, ransomware_test_data)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
|
|
@ -7,6 +7,7 @@ from common.utils.file_utils import InvalidPath
|
|||
from infection_monkey.payload.ransomware import ransomware_options
|
||||
from infection_monkey.payload.ransomware.ransomware_options import RansomwareOptions
|
||||
|
||||
EXTENSION = ".testext"
|
||||
LINUX_DIR = "/tmp/test"
|
||||
WINDOWS_DIR = "C:\\tmp\\test"
|
||||
|
||||
|
@ -16,6 +17,7 @@ def options_from_island():
|
|||
return {
|
||||
"encryption": {
|
||||
"enabled": None,
|
||||
"file_extension": EXTENSION,
|
||||
"directories": {
|
||||
"linux_target_dir": LINUX_DIR,
|
||||
"windows_target_dir": WINDOWS_DIR,
|
||||
|
@ -41,6 +43,12 @@ def test_readme_enabled(enabled, options_from_island):
|
|||
assert options.readme_enabled == enabled
|
||||
|
||||
|
||||
def test_file_extension(options_from_island):
|
||||
options = RansomwareOptions(options_from_island)
|
||||
|
||||
assert options.file_extension == EXTENSION
|
||||
|
||||
|
||||
def test_linux_target_dir(monkeypatch, options_from_island):
|
||||
monkeypatch.setattr(ransomware_options, "is_windows_os", lambda: False)
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@ log_cli = 1
|
|||
log_cli_level = "DEBUG"
|
||||
log_cli_format = "%(asctime)s [%(levelname)s] %(module)s.%(funcName)s.%(lineno)d: %(message)s"
|
||||
log_cli_date_format = "%H:%M:%S"
|
||||
addopts = "-v --capture=sys tests/unit_tests"
|
||||
addopts = "-v --capture=sys tests/unit_tests tests/integration_tests"
|
||||
norecursedirs = "node_modules dist"
|
||||
markers = ["slow: mark test as slow"]
|
||||
pythonpath = "./monkey"
|
||||
|
|
Loading…
Reference in New Issue