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
|
||||
|
|
|
@ -2,7 +2,7 @@ import AdvancedMultiSelect from '../ui-components/AdvancedMultiSelect';
|
|||
import InfoBox from './InfoBox';
|
||||
import TextBox from './TextBox.js';
|
||||
import PbaInput from './PbaInput';
|
||||
import {API_PBA_LINUX, API_PBA_WINDOWS} from '../pages/ConfigurePage';
|
||||
import { API_PBA_LINUX, API_PBA_WINDOWS } from '../pages/ConfigurePage';
|
||||
import SensitiveTextInput from '../ui-components/SensitiveTextInput';
|
||||
|
||||
export default function UiSchema(props) {
|
||||
|
@ -45,13 +45,13 @@ export default function UiSchema(props) {
|
|||
'ui:widget': SensitiveTextInput
|
||||
}
|
||||
},
|
||||
exploit_lm_hash_list:{
|
||||
exploit_lm_hash_list: {
|
||||
items: {
|
||||
classNames: 'config-template-no-header',
|
||||
'ui:widget': SensitiveTextInput
|
||||
}
|
||||
},
|
||||
exploit_ntlm_hash_list:{
|
||||
exploit_ntlm_hash_list: {
|
||||
items: {
|
||||
classNames: 'config-template-no-header',
|
||||
'ui:widget': SensitiveTextInput
|
||||
|
@ -82,11 +82,11 @@ export default function UiSchema(props) {
|
|||
tcp: {
|
||||
ports: {
|
||||
items: {
|
||||
classNames: 'config-template-no-header'
|
||||
classNames: 'config-template-no-header'
|
||||
}
|
||||
}
|
||||
},
|
||||
fingerprinters:{
|
||||
fingerprinters: {
|
||||
classNames: 'config-template-no-header',
|
||||
'ui:widget': AdvancedMultiSelect,
|
||||
fingerprinter_classes: {
|
||||
|
@ -99,9 +99,12 @@ export default function UiSchema(props) {
|
|||
payloads: {
|
||||
classNames: 'config-template-no-header',
|
||||
encryption: {
|
||||
info_box : {
|
||||
info_box: {
|
||||
'ui:field': InfoBox
|
||||
},
|
||||
file_extension: {
|
||||
'ui:emptyValue': ''
|
||||
},
|
||||
directories: {
|
||||
// Directory inputs are dynamically hidden
|
||||
},
|
||||
|
@ -112,7 +115,7 @@ export default function UiSchema(props) {
|
|||
'ui:widget': 'hidden'
|
||||
}
|
||||
},
|
||||
other_behaviors : {
|
||||
other_behaviors: {
|
||||
'ui:widget': '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 `$`
|
||||
|
@ -11,7 +12,7 @@ const linuxPathStartsWithTildeRegex = /^~/ // path starts with `~`
|
|||
const windowsAbsolutePathRegex = /^([A-Za-z]:(\\|\/))/ // path starts like `C:\` OR `C:/`
|
||||
const windowsEnvVarNonNumeric = '[A-Za-z#\\$\'\\(\\)\\*\\+,\\-\\.\\?@\\[\\]_`\\{\\}~ ]'
|
||||
const windowsPathStartsWithEnvVariableRegex = new RegExp(
|
||||
`^%(${windowsEnvVarNonNumeric}+(${windowsEnvVarNonNumeric}|\\d)*)%`
|
||||
`^%(${windowsEnvVarNonNumeric}+(${windowsEnvVarNonNumeric}|\\d)*)%`
|
||||
) // path starts like `$` OR `%abc%`
|
||||
const windowsUncPathRegex = /^\\{2}/ // Path starts like `\\`
|
||||
const emptyRegex = /^$/
|
||||
|
@ -19,32 +20,34 @@ 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()
|
||||
};
|
||||
|
||||
function buildIpRangeRegex(){
|
||||
function buildIpRangeRegex() {
|
||||
return new RegExp([
|
||||
'^'+ipRegex+'$|', // Single: IP
|
||||
'^'+ipRegex+'-'+ipRegex+'$|', // IP range: IP-IP
|
||||
'^'+ipRegex+'/'+cidrNotationRegex+'$|', // IP range with cidr notation: IP/cidr
|
||||
'^' + ipRegex + '$|', // Single: IP
|
||||
'^' + ipRegex + '-' + ipRegex + '$|', // IP range: IP-IP
|
||||
'^' + ipRegex + '/' + cidrNotationRegex + '$|', // IP range with cidr notation: IP/cidr
|
||||
hostnameRegex // Hostname: target.tg
|
||||
].join(''))
|
||||
}
|
||||
|
||||
function buildIpRegex(){
|
||||
return new RegExp('^'+ipRegex+'$')
|
||||
function buildIpRegex() {
|
||||
return new RegExp('^' + ipRegex + '$')
|
||||
}
|
||||
|
||||
function buildValidRansomwarePathLinuxRegex() {
|
||||
return new RegExp([
|
||||
emptyRegex.source,
|
||||
emptyRegex.source,
|
||||
linuxAbsolutePathRegex.source,
|
||||
linuxPathStartsWithEnvVariableRegex.source,
|
||||
linuxPathStartsWithTildeRegex.source
|
||||
|
@ -53,7 +56,7 @@ function buildValidRansomwarePathLinuxRegex() {
|
|||
|
||||
function buildValidRansomwarePathWindowsRegex() {
|
||||
return new RegExp([
|
||||
emptyRegex.source,
|
||||
emptyRegex.source,
|
||||
windowsAbsolutePathRegex.source,
|
||||
windowsPathStartsWithEnvVariableRegex.source,
|
||||
windowsUncPathRegex.source
|
||||
|
|
|
@ -1,24 +1,32 @@
|
|||
const RANSOMWARE_SCHEMA = {
|
||||
'title': 'Payloads',
|
||||
'title': 'Payloads',
|
||||
'properties': {
|
||||
'encryption': {
|
||||
'title': 'Ransomware simulation',
|
||||
'type': 'object',
|
||||
'description': 'To simulate ransomware encryption, you\'ll need to provide Infection ' +
|
||||
'Monkey with files that it can safely encrypt. On each machine where you would like ' +
|
||||
'the ransomware simulation to run, create a directory and put some files in it.' +
|
||||
'\n\nProvide the path to the directory that was created on each machine.',
|
||||
'Monkey with files that it can safely encrypt. On each machine where you would like ' +
|
||||
'the ransomware simulation to run, create a directory and put some files in it.' +
|
||||
'\n\nProvide the path to the directory that was created on each machine.',
|
||||
'properties': {
|
||||
'enabled': {
|
||||
'title': 'Encrypt files',
|
||||
'type': 'boolean',
|
||||
'default': true,
|
||||
'description': 'Ransomware encryption will be simulated by flipping every bit ' +
|
||||
'in the files contained within the target directories.'
|
||||
'in the files contained within the target directories.'
|
||||
},
|
||||
'info_box': {
|
||||
'info': 'No files will be encrypted if a directory is not specified or doesn\'t ' +
|
||||
'exist on a victim machine.'
|
||||
'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',
|
||||
|
@ -30,8 +38,8 @@ const RANSOMWARE_SCHEMA = {
|
|||
'format': 'valid-ransomware-target-path-linux',
|
||||
'default': '',
|
||||
'description': 'A path to a directory on Linux systems that contains ' +
|
||||
'files that you will allow Infection Monkey to encrypt. If no ' +
|
||||
'directory is specified, no files will be encrypted.'
|
||||
'files that you will allow Infection Monkey to encrypt. If no ' +
|
||||
'directory is specified, no files will be encrypted.'
|
||||
},
|
||||
'windows_target_dir': {
|
||||
'title': 'Windows target directory',
|
||||
|
@ -39,8 +47,8 @@ const RANSOMWARE_SCHEMA = {
|
|||
'format': 'valid-ransomware-target-path-windows',
|
||||
'default': '',
|
||||
'description': 'A path to a directory on Windows systems that contains ' +
|
||||
'files that you will allow Infection Monkey to encrypt. If no ' +
|
||||
'directory is specified, no files will be encrypted.'
|
||||
'files that you will allow Infection Monkey to encrypt. If no ' +
|
||||
'directory is specified, no files will be encrypted.'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -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