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
- 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

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
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

View File

@ -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},

View File

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

View File

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

View File

@ -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'
}
},

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.';
@ -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) {

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 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

View File

@ -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.'
}
}
},

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
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

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.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)

View File

@ -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"