Merge pull request #2206 from guardicore/1242-allow-custom-ransomware-extension

1242 allow custom ransomware extension
This commit is contained in:
Mike Salvatore 2022-08-19 09:48:34 -04:00 committed by GitHub
commit ce390e41b8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 112 additions and 42 deletions

View File

@ -21,6 +21,8 @@ Changelog](https://keepachangelog.com/en/1.0.0/).
- `/api/registration-status` endpoint. #2149 - `/api/registration-status` endpoint. #2149
- authentication to `/api/island/version`. #2109 - authentication to `/api/island/version`. #2109
- `/api/events` endpoint. #2155 - `/api/events` endpoint. #2155
- The ability to customize the file extension used by ransomware when
encrypting files. #1242
### Changed ### Changed
- Reset workflow. Now it's possible to delete data gathered by agents without - Reset workflow. Now it's possible to delete data gathered by agents without

View File

@ -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 simulation will only encrypt files contained in a user-specified directory. If
no directory is specified, no files will be encrypted. 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") ![Ransomware configuration](/images/usage/scenarios/ransomware-config.png "Ransomware configuration")
### How are the files encrypted? ### How are the files encrypted?
Files are "encrypted" in place with a simple bit flip. Encrypted files are 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 renamed to have a file extension (`.m0nk3y` by default) appended to their
simulate encryption since it is easy to "decrypt" your files. You can simply names. This is a safe way to simulate encryption since it is easy to "decrypt"
perform a bit flip on the files again and rename them to remove the appended your files. You can simply perform a bit flip on the files again and rename
`.m0nk3y` extension. them to remove the appended `.m0nk3y` extension.
Flipping a file's bits is sufficient to simulate the encryption behavior of 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 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

View File

@ -35,6 +35,7 @@ CREDENTIAL_COLLECTOR_CONFIGURATION = tuple(
RANSOMWARE_OPTIONS = { RANSOMWARE_OPTIONS = {
"encryption": { "encryption": {
"enabled": True, "enabled": True,
"file_extension": ".m0nk3y",
"directories": {"linux_target_dir": "", "windows_target_dir": ""}, "directories": {"linux_target_dir": "", "windows_target_dir": ""},
}, },
"other_behaviors": {"readme": True}, "other_behaviors": {"readme": True},

View File

@ -16,7 +16,6 @@ from .ransomware import Ransomware
from .ransomware_options import RansomwareOptions from .ransomware_options import RansomwareOptions
from .targeted_file_extensions import TARGETED_FILE_EXTENSIONS from .targeted_file_extensions import TARGETED_FILE_EXTENSIONS
EXTENSION = ".m0nk3y"
CHUNK_SIZE = 4096 * 24 CHUNK_SIZE = 4096 * 24
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -26,8 +25,8 @@ def build_ransomware(options: dict):
logger.debug(f"Ransomware configuration:\n{pformat(options)}") logger.debug(f"Ransomware configuration:\n{pformat(options)}")
ransomware_options = RansomwareOptions(options) ransomware_options = RansomwareOptions(options)
file_encryptor = _build_file_encryptor() file_encryptor = _build_file_encryptor(ransomware_options.file_extension)
file_selector = _build_file_selector() file_selector = _build_file_selector(ransomware_options.file_extension)
leave_readme = _build_leave_readme() leave_readme = _build_leave_readme()
telemetry_messenger = _build_telemetry_messenger() 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( 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 = TARGETED_FILE_EXTENSIONS.copy()
targeted_file_extensions.discard(EXTENSION) if file_extension:
targeted_file_extensions.discard(file_extension)
return ProductionSafeTargetFileSelector(targeted_file_extensions) return ProductionSafeTargetFileSelector(targeted_file_extensions)

View File

@ -9,6 +9,7 @@ logger = logging.getLogger(__name__)
class RansomwareOptions: class RansomwareOptions:
def __init__(self, options: dict): def __init__(self, options: dict):
self.encryption_enabled = options["encryption"]["enabled"] self.encryption_enabled = options["encryption"]["enabled"]
self.file_extension = options["encryption"]["file_extension"]
self.readme_enabled = options["other_behaviors"]["readme"] self.readme_enabled = options["other_behaviors"]["readme"]
self.target_directory = None self.target_directory = None

View File

@ -2,7 +2,7 @@ import AdvancedMultiSelect from '../ui-components/AdvancedMultiSelect';
import InfoBox from './InfoBox'; import InfoBox from './InfoBox';
import TextBox from './TextBox.js'; import TextBox from './TextBox.js';
import PbaInput from './PbaInput'; 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'; import SensitiveTextInput from '../ui-components/SensitiveTextInput';
export default function UiSchema(props) { export default function UiSchema(props) {
@ -45,13 +45,13 @@ export default function UiSchema(props) {
'ui:widget': SensitiveTextInput 'ui:widget': SensitiveTextInput
} }
}, },
exploit_lm_hash_list:{ exploit_lm_hash_list: {
items: { items: {
classNames: 'config-template-no-header', classNames: 'config-template-no-header',
'ui:widget': SensitiveTextInput 'ui:widget': SensitiveTextInput
} }
}, },
exploit_ntlm_hash_list:{ exploit_ntlm_hash_list: {
items: { items: {
classNames: 'config-template-no-header', classNames: 'config-template-no-header',
'ui:widget': SensitiveTextInput 'ui:widget': SensitiveTextInput
@ -82,11 +82,11 @@ export default function UiSchema(props) {
tcp: { tcp: {
ports: { ports: {
items: { items: {
classNames: 'config-template-no-header' classNames: 'config-template-no-header'
} }
} }
}, },
fingerprinters:{ fingerprinters: {
classNames: 'config-template-no-header', classNames: 'config-template-no-header',
'ui:widget': AdvancedMultiSelect, 'ui:widget': AdvancedMultiSelect,
fingerprinter_classes: { fingerprinter_classes: {
@ -99,9 +99,12 @@ export default function UiSchema(props) {
payloads: { payloads: {
classNames: 'config-template-no-header', classNames: 'config-template-no-header',
encryption: { encryption: {
info_box : { info_box: {
'ui:field': InfoBox 'ui:field': InfoBox
}, },
file_extension: {
'ui:emptyValue': ''
},
directories: { directories: {
// Directory inputs are dynamically hidden // Directory inputs are dynamically hidden
}, },
@ -112,7 +115,7 @@ export default function UiSchema(props) {
'ui:widget': 'hidden' 'ui:widget': 'hidden'
} }
}, },
other_behaviors : { other_behaviors: {
'ui:widget': 'hidden' 'ui:widget': 'hidden'
} }
}, },

View File

@ -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.'; 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.' error.message = 'Invalid IP range, refer to description for valid examples.'
} else if (error.name === 'format' && error.params.format === IP) { } else if (error.name === 'format' && error.params.format === IP) {
error.message = 'Invalid 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) { } else if (error.name === 'format' && error.params.format === VALID_RANSOMWARE_TARGET_PATH_LINUX) {
error.message = invalidDirMessage error.message = invalidDirMessage
} else if (error.name === 'format' && error.params.format === VALID_RANSOMWARE_TARGET_PATH_WINDOWS) { } else if (error.name === 'format' && error.params.format === VALID_RANSOMWARE_TARGET_PATH_WINDOWS) {

View File

@ -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 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 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 linuxAbsolutePathRegex = /^\// // path starts with `/`
const linuxPathStartsWithEnvVariableRegex = /^\$/ // 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 windowsAbsolutePathRegex = /^([A-Za-z]:(\\|\/))/ // path starts like `C:\` OR `C:/`
const windowsEnvVarNonNumeric = '[A-Za-z#\\$\'\\(\\)\\*\\+,\\-\\.\\?@\\[\\]_`\\{\\}~ ]' const windowsEnvVarNonNumeric = '[A-Za-z#\\$\'\\(\\)\\*\\+,\\-\\.\\?@\\[\\]_`\\{\\}~ ]'
const windowsPathStartsWithEnvVariableRegex = new RegExp( const windowsPathStartsWithEnvVariableRegex = new RegExp(
`^%(${windowsEnvVarNonNumeric}+(${windowsEnvVarNonNumeric}|\\d)*)%` `^%(${windowsEnvVarNonNumeric}+(${windowsEnvVarNonNumeric}|\\d)*)%`
) // path starts like `$` OR `%abc%` ) // path starts like `$` OR `%abc%`
const windowsUncPathRegex = /^\\{2}/ // Path starts like `\\` const windowsUncPathRegex = /^\\{2}/ // Path starts like `\\`
const emptyRegex = /^$/ const emptyRegex = /^$/
@ -19,32 +20,34 @@ const emptyRegex = /^$/
export const IP_RANGE = 'ip-range'; export const IP_RANGE = 'ip-range';
export const IP = 'ip'; 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_LINUX = 'valid-ransomware-target-path-linux'
export const VALID_RANSOMWARE_TARGET_PATH_WINDOWS = 'valid-ransomware-target-path-windows' export const VALID_RANSOMWARE_TARGET_PATH_WINDOWS = 'valid-ransomware-target-path-windows'
export const formValidationFormats = { export const formValidationFormats = {
[IP_RANGE]: buildIpRangeRegex(), [IP_RANGE]: buildIpRangeRegex(),
[IP]: buildIpRegex(), [IP]: buildIpRegex(),
[VALID_FILE_EXTENSION]: fileExtensionRegex,
[VALID_RANSOMWARE_TARGET_PATH_LINUX]: buildValidRansomwarePathLinuxRegex(), [VALID_RANSOMWARE_TARGET_PATH_LINUX]: buildValidRansomwarePathLinuxRegex(),
[VALID_RANSOMWARE_TARGET_PATH_WINDOWS]: buildValidRansomwarePathWindowsRegex() [VALID_RANSOMWARE_TARGET_PATH_WINDOWS]: buildValidRansomwarePathWindowsRegex()
}; };
function buildIpRangeRegex(){ function buildIpRangeRegex() {
return new RegExp([ return new RegExp([
'^'+ipRegex+'$|', // Single: IP '^' + ipRegex + '$|', // Single: IP
'^'+ipRegex+'-'+ipRegex+'$|', // IP range: IP-IP '^' + ipRegex + '-' + ipRegex + '$|', // IP range: IP-IP
'^'+ipRegex+'/'+cidrNotationRegex+'$|', // IP range with cidr notation: IP/cidr '^' + ipRegex + '/' + cidrNotationRegex + '$|', // IP range with cidr notation: IP/cidr
hostnameRegex // Hostname: target.tg hostnameRegex // Hostname: target.tg
].join('')) ].join(''))
} }
function buildIpRegex(){ function buildIpRegex() {
return new RegExp('^'+ipRegex+'$') return new RegExp('^' + ipRegex + '$')
} }
function buildValidRansomwarePathLinuxRegex() { function buildValidRansomwarePathLinuxRegex() {
return new RegExp([ return new RegExp([
emptyRegex.source, emptyRegex.source,
linuxAbsolutePathRegex.source, linuxAbsolutePathRegex.source,
linuxPathStartsWithEnvVariableRegex.source, linuxPathStartsWithEnvVariableRegex.source,
linuxPathStartsWithTildeRegex.source linuxPathStartsWithTildeRegex.source
@ -53,7 +56,7 @@ function buildValidRansomwarePathLinuxRegex() {
function buildValidRansomwarePathWindowsRegex() { function buildValidRansomwarePathWindowsRegex() {
return new RegExp([ return new RegExp([
emptyRegex.source, emptyRegex.source,
windowsAbsolutePathRegex.source, windowsAbsolutePathRegex.source,
windowsPathStartsWithEnvVariableRegex.source, windowsPathStartsWithEnvVariableRegex.source,
windowsUncPathRegex.source windowsUncPathRegex.source

View File

@ -1,24 +1,32 @@
const RANSOMWARE_SCHEMA = { const RANSOMWARE_SCHEMA = {
'title': 'Payloads', 'title': 'Payloads',
'properties': { 'properties': {
'encryption': { 'encryption': {
'title': 'Ransomware simulation', 'title': 'Ransomware simulation',
'type': 'object', 'type': 'object',
'description': 'To simulate ransomware encryption, you\'ll need to provide Infection ' + '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 ' + '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.' + '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.', '\n\nProvide the path to the directory that was created on each machine.',
'properties': { 'properties': {
'enabled': { 'enabled': {
'title': 'Encrypt files', 'title': 'Encrypt files',
'type': 'boolean', 'type': 'boolean',
'default': true, 'default': true,
'description': 'Ransomware encryption will be simulated by flipping every bit ' + '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_box': {
'info': 'No files will be encrypted if a directory is not specified or doesn\'t ' + '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': { 'directories': {
'title': 'Directories to encrypt', 'title': 'Directories to encrypt',
@ -30,8 +38,8 @@ const RANSOMWARE_SCHEMA = {
'format': 'valid-ransomware-target-path-linux', 'format': 'valid-ransomware-target-path-linux',
'default': '', 'default': '',
'description': 'A path to a directory on Linux systems that contains ' + 'description': 'A path to a directory on Linux systems that contains ' +
'files that you will allow Infection Monkey to encrypt. If no ' + 'files that you will allow Infection Monkey to encrypt. If no ' +
'directory is specified, no files will be encrypted.' 'directory is specified, no files will be encrypted.'
}, },
'windows_target_dir': { 'windows_target_dir': {
'title': 'Windows target directory', 'title': 'Windows target directory',
@ -39,8 +47,8 @@ const RANSOMWARE_SCHEMA = {
'format': 'valid-ransomware-target-path-windows', 'format': 'valid-ransomware-target-path-windows',
'default': '', 'default': '',
'description': 'A path to a directory on Windows systems that contains ' + 'description': 'A path to a directory on Windows systems that contains ' +
'files that you will allow Infection Monkey to encrypt. If no ' + 'files that you will allow Infection Monkey to encrypt. If no ' +
'directory is specified, no files will be encrypted.' 'directory is specified, no files will be encrypted.'
} }
} }
}, },

6
monkey/tests/conftest.py Normal file
View File

@ -0,0 +1,6 @@
import pytest
@pytest.fixture(params=[".m0nk3y", ".test", ""], ids=["monkeyext", "testext", "noext"])
def ransomware_file_extension(request):
return request.param

View File

@ -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()

View File

@ -41,14 +41,15 @@ def build_ransomware(
@pytest.fixture @pytest.fixture
def ransomware_options(ransomware_test_data): def ransomware_options(ransomware_file_extension, ransomware_test_data):
class RansomwareOptionsStub(RansomwareOptions): 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.encryption_enabled = encryption_enabled
self.readme_enabled = readme_enabled self.readme_enabled = readme_enabled
self.file_extension = file_extension
self.target_directory = target_directory self.target_directory = target_directory
return RansomwareOptionsStub(True, False, ransomware_test_data) return RansomwareOptionsStub(True, False, ransomware_file_extension, ransomware_test_data)
@pytest.fixture @pytest.fixture

View File

@ -7,6 +7,7 @@ from common.utils.file_utils import InvalidPath
from infection_monkey.payload.ransomware import ransomware_options from infection_monkey.payload.ransomware import ransomware_options
from infection_monkey.payload.ransomware.ransomware_options import RansomwareOptions from infection_monkey.payload.ransomware.ransomware_options import RansomwareOptions
EXTENSION = ".testext"
LINUX_DIR = "/tmp/test" LINUX_DIR = "/tmp/test"
WINDOWS_DIR = "C:\\tmp\\test" WINDOWS_DIR = "C:\\tmp\\test"
@ -16,6 +17,7 @@ def options_from_island():
return { return {
"encryption": { "encryption": {
"enabled": None, "enabled": None,
"file_extension": EXTENSION,
"directories": { "directories": {
"linux_target_dir": LINUX_DIR, "linux_target_dir": LINUX_DIR,
"windows_target_dir": WINDOWS_DIR, "windows_target_dir": WINDOWS_DIR,
@ -41,6 +43,12 @@ def test_readme_enabled(enabled, options_from_island):
assert options.readme_enabled == enabled 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): def test_linux_target_dir(monkeypatch, options_from_island):
monkeypatch.setattr(ransomware_options, "is_windows_os", lambda: False) monkeypatch.setattr(ransomware_options, "is_windows_os", lambda: False)

View File

@ -20,7 +20,7 @@ log_cli = 1
log_cli_level = "DEBUG" log_cli_level = "DEBUG"
log_cli_format = "%(asctime)s [%(levelname)s] %(module)s.%(funcName)s.%(lineno)d: %(message)s" log_cli_format = "%(asctime)s [%(levelname)s] %(module)s.%(funcName)s.%(lineno)d: %(message)s"
log_cli_date_format = "%H:%M:%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" norecursedirs = "node_modules dist"
markers = ["slow: mark test as slow"] markers = ["slow: mark test as slow"]
pythonpath = "./monkey" pythonpath = "./monkey"