Skip to content

Commit 30c8a65

Browse files
committed
Detect the DLL proxying technique with InterModex
1 parent 32d9ed5 commit 30c8a65

File tree

3 files changed

+161
-53
lines changed

3 files changed

+161
-53
lines changed

inter_modex.py

+128-25
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,17 @@
88
from typing import Dict, Any, List
99

1010
from modex import Module, Page, get_current_utc_timestamp, create_logger, check_if_all_elements_are_equal, \
11-
check_if_modules_can_be_mixed, mix_modules
11+
check_if_modules_can_be_mixed, mix_modules, get_detection_information_filename, log_detection_process_common_parts, \
12+
get_most_common_element
13+
14+
15+
class ModexDetectionAttempt:
16+
def __init__(self, memory_dump_location: str, mapped_modules: List[Module], result: bool,
17+
suspicious_processes: List[int]):
18+
self.memory_dump_location: str = memory_dump_location
19+
self.mapped_modules: List[Module] = mapped_modules
20+
self.result: bool = result
21+
self.suspicious_processes: List[int] = suspicious_processes
1222

1323

1424
class ModexExtraction:
@@ -82,16 +92,19 @@ def convert_modex_extraction_to_module(modex_extraction: ModexExtraction) -> Mod
8292
return Module('', module_path, module_base_address, module_size, 0, modex_extraction.module_path, pages)
8393

8494

85-
def check_if_modex_run_successfully(modex_output_directory: str) -> bool:
95+
def check_if_modex_ran_successfully(modex_output_directory: str, was_detect_option_supplied: bool) -> bool:
8696
"""Check if an output from the Modex plugin contains the files it should contain for a successful execution."""
87-
# An output from the Modex plugin, without taking directories into account, should contain 3 files if the execution was successful:
97+
# An output from the Modex plugin, without taking directories into account and without supplying the --detect option, should contain 3 files if the execution was successful:
8898
# - A .dmp file
8999
# - A .json file
90100
# - A .txt file
101+
# Note: If the --detect option was supplied to Modex, then a .dmp file will not be created
102+
103+
number_of_generated_files: int = 2 if was_detect_option_supplied else 3
104+
extensions: List[str] = ['.json', '.txt'] if was_detect_option_supplied else ['.dmp', '.json', '.txt']
91105
not_hidden_files_inside_modex_output: List[str] = get_not_hidden_files_inside_directory(modex_output_directory)
92-
if len(not_hidden_files_inside_modex_output) == 3:
93-
extensions: List[str] = ['.dmp', '.json', '.txt']
94-
presence_of_extensions: List[bool] = [False, False, False]
106+
if len(not_hidden_files_inside_modex_output) == number_of_generated_files:
107+
presence_of_extensions: List[bool] = [False, False] if was_detect_option_supplied else [False, False, False]
95108
for file_inside_modex_output in not_hidden_files_inside_modex_output:
96109
file_has_required_extension: bool = False
97110
i: int = 0
@@ -108,8 +121,7 @@ def check_if_modex_run_successfully(modex_output_directory: str) -> bool:
108121
return False
109122

110123

111-
def perform_mixture(modex_outputs_directory: str, perform_derelocation: bool, sum_path: str, dump_anomalies: bool,
112-
output_directory: str, logger) -> None:
124+
def get_succesful_modex_outputs(modex_outputs_directory: str, was_detect_option_supplied: bool, logger) -> List[str]:
113125
modex_outputs: List[str] = os.listdir(modex_outputs_directory)
114126
for i in range(0, len(modex_outputs)):
115127
modex_outputs[i] = os.path.join(modex_outputs_directory, modex_outputs[i])
@@ -123,12 +135,78 @@ def perform_mixture(modex_outputs_directory: str, perform_derelocation: bool, su
123135
successful_modex_outputs: List[str] = []
124136
for modex_output in modex_outputs:
125137
if os.path.isdir(modex_output):
126-
if check_if_modex_run_successfully(modex_output):
138+
if check_if_modex_ran_successfully(modex_output, was_detect_option_supplied):
127139
successful_modex_outputs.append(modex_output)
128140
logger.info(f'\t{modex_output} meets the requirements for a successful Modex execution')
129141
else:
130142
logger.info(
131143
f'\t{modex_output} does not meet the requirements for a successful Modex execution. As a result, it will not be considered for the mixture.')
144+
return successful_modex_outputs
145+
146+
147+
def convert_mapped_modules_from_json_to_objects(mapped_modules: List[Dict[str, Any]]) -> List[Module]:
148+
mapped_modules_converted: List[Module] = []
149+
for mapped_module in mapped_modules:
150+
mapped_modules_converted.append(
151+
Module('', mapped_module['path'], mapped_module['base_address'], mapped_module['size'],
152+
mapped_module['process_id'], '', []))
153+
return mapped_modules_converted
154+
155+
156+
def detect_dll_proxying_across_several_memory_dumps(modex_detection_attempts: List[ModexDetectionAttempt],
157+
output_directory: str, logger) -> None:
158+
detection_info: Dict[str, Any] = {}
159+
mapped_modules: List[Module] = []
160+
for modex_detection_attempt in modex_detection_attempts:
161+
mapped_modules += modex_detection_attempt.mapped_modules
162+
163+
most_common_path: str = get_most_common_element([module.path.casefold() for module in mapped_modules])
164+
most_common_size: int = get_most_common_element([module.size for module in mapped_modules])
165+
166+
suspicious_processes: Dict[str, Any] = {} # The keys are memory dumps
167+
for modex_detection_attempt in modex_detection_attempts:
168+
for module in modex_detection_attempt.mapped_modules:
169+
if module.path.casefold() != most_common_path or module.size != most_common_size:
170+
if modex_detection_attempt.memory_dump_location not in suspicious_processes.keys():
171+
suspicious_processes[modex_detection_attempt.memory_dump_location] = [module.process_id]
172+
else:
173+
suspicious_processes[modex_detection_attempt.memory_dump_location].append(module.process_id)
174+
175+
detection_info['dll_proxying_detection_result'] = True if suspicious_processes else False
176+
detection_info['suspicious_processes'] = suspicious_processes
177+
178+
detection_info_path: str = os.path.join(output_directory, get_detection_information_filename())
179+
with open(detection_info_path, 'w') as detection_info_file:
180+
json.dump(detection_info, detection_info_file, ensure_ascii=False, indent=4)
181+
182+
log_detection_process_common_parts(logger)
183+
184+
185+
def perform_detection(modex_outputs_directory: str, output_directory: str, logger) -> None:
186+
successful_modex_outputs: List[str] = get_succesful_modex_outputs(modex_outputs_directory, True, logger)
187+
modex_detection_attempts: List[ModexDetectionAttempt] = []
188+
189+
# Get the .json files from each successful Modex output and perform the detection
190+
for successful_modex_output in successful_modex_outputs:
191+
json_file: str = get_file_from_modex_output(successful_modex_output, '.json')
192+
if json_file is not None:
193+
with open(json_file) as detection_info_file:
194+
detection_info: Dict[str, Any] = json.load(detection_info_file)
195+
mapped_modules: List[Module] = convert_mapped_modules_from_json_to_objects(detection_info['mapped_modules'])
196+
modex_detection_attempts.append(
197+
ModexDetectionAttempt(detection_info['memory_dump_location'], mapped_modules,
198+
detection_info['dll_proxying_detection_result'],
199+
detection_info['suspicious_processes']))
200+
else:
201+
logger.info(
202+
f'\tThe Modex output {successful_modex_output} was considered successful, but the .json file does not exist, and it must exist for a Modex output to be successful. As a result, this output will not be considered in the detection process.')
203+
204+
detect_dll_proxying_across_several_memory_dumps(modex_detection_attempts, output_directory, logger)
205+
206+
207+
def perform_mixture(modex_outputs_directory: str, perform_derelocation: bool, sum_path: str, dump_anomalies: bool,
208+
output_directory: str, logger) -> None:
209+
successful_modex_outputs: List[str] = get_succesful_modex_outputs(modex_outputs_directory, False, logger)
132210

133211
# Take the .dmp and .json files from each successful Modex output and perform the mixture
134212
modex_extractions: List[ModexExtraction] = []
@@ -168,9 +246,10 @@ def perform_mixture(modex_outputs_directory: str, perform_derelocation: bool, su
168246
logger.info(f'\nThe derelocation process was not successful (exit code {derelocator_exit_code})')
169247

170248

171-
def perform_extraction(module: str, memory_dumps_directory: str, remove_modex_outputs: bool, perform_derelocation: bool,
172-
sum_path: str, volatility_path: str, dump_anomalies: bool, output_directory: str,
173-
logger) -> None:
249+
def perform_operation_after_getting_modex_outputs(module: str, memory_dumps_directory: str, remove_modex_outputs: bool,
250+
perform_derelocation: bool, sum_path: str, volatility_path: str,
251+
dump_anomalies: bool, detect: bool, output_directory: str,
252+
logger) -> None:
174253
memory_dumps: List[str] = get_not_hidden_files_inside_directory(memory_dumps_directory)
175254
logger.info('Memory dumps provided:')
176255
for memory_dump in memory_dumps:
@@ -184,8 +263,12 @@ def perform_extraction(module: str, memory_dumps_directory: str, remove_modex_ou
184263
# Invoke the Modex plugin for each memory dump inside the memory dumps directory
185264
logger.info('\nModex plugin execution:')
186265
for memory_dump in memory_dumps:
187-
volatility_command = ['python3', volatility_path, '-f', memory_dump, 'windows.modex', '--module', module,
188-
'--dump-anomalies']
266+
if detect:
267+
volatility_command = ['python3', volatility_path, '-f', memory_dump, 'windows.modex', '--module', module,
268+
'--detect']
269+
else:
270+
volatility_command = ['python3', volatility_path, '-f', memory_dump, 'windows.modex', '--module', module,
271+
'--dump-anomalies']
189272
with subprocess.Popen(volatility_command, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) as modex_plugin:
190273
print(f'Running the Modex plugin for the following memory dump: {memory_dump}')
191274
modex_plugin_exit_code = modex_plugin.wait()
@@ -198,9 +281,12 @@ def perform_extraction(module: str, memory_dumps_directory: str, remove_modex_ou
198281
os.chdir(current_working_directory) # Restore the working directory
199282
logger.debug(f'Working directory after restoring it: {os.getcwd()}')
200283

201-
# Mix the modules previously extracted
202-
perform_mixture(modex_outputs_directory_name, perform_derelocation, sum_path, dump_anomalies, output_directory,
203-
logger)
284+
if detect:
285+
perform_detection(modex_outputs_directory_name, output_directory, logger)
286+
else:
287+
# Mix the modules previously extracted
288+
perform_mixture(modex_outputs_directory_name, perform_derelocation, sum_path, dump_anomalies, output_directory,
289+
logger)
204290

205291
if remove_modex_outputs:
206292
shutil.rmtree(modex_outputs_directory_name)
@@ -218,6 +304,9 @@ def validate_arguments() -> Dict[str, Any]:
218304
arg_parser.add_argument('-d',
219305
'--memory-dumps-directory',
220306
help='directory where the memory dumps are (the Modex plugin will be called)')
307+
arg_parser.add_argument('--detect',
308+
action='store_true',
309+
help='detect the presence of the DLL proxying technique')
221310
arg_parser.add_argument('-l',
222311
'--log-level',
223312
choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'],
@@ -258,6 +347,7 @@ def validate_arguments() -> Dict[str, Any]:
258347
volatility_path = args.volatility_path
259348
dump_anomalies = args.dump_anomalies
260349
sum_path = args.sum_path
350+
detect = args.detect
261351

262352
if memory_dumps_directory is not None and modex_outputs_directory is not None:
263353
raise ValueError(
@@ -306,6 +396,10 @@ def validate_arguments() -> Dict[str, Any]:
306396
if module is not None and len(module) > 255:
307397
raise ValueError('The module name is too long')
308398

399+
if (perform_derelocation or dump_anomalies) and detect:
400+
raise ValueError(
401+
'You cannot supply the --detect option alongside the --perform-derelocation or --dump-anomalies options')
402+
309403
if memory_dumps_directory is not None:
310404
memory_dumps_directory = os.path.abspath(memory_dumps_directory)
311405

@@ -334,7 +428,7 @@ def validate_arguments() -> Dict[str, Any]:
334428
'remove_modex_outputs': remove_modex_outputs,
335429
'perform_derelocation': perform_derelocation, 'volatility_path': volatility_path,
336430
'log_level_supplied': log_level_supplied, 'dump_anomalies': dump_anomalies,
337-
'sum_path': sum_path}
431+
'sum_path': sum_path, 'detect': detect}
338432
return arguments
339433

340434

@@ -349,16 +443,25 @@ def execute() -> None:
349443
if modex_outputs_directory is not None:
350444
create_output_directory(output_directory, False)
351445
logger = get_logger(output_directory, validated_arguments['log_level_supplied'])
352-
perform_mixture(modex_outputs_directory, validated_arguments['perform_derelocation'],
353-
validated_arguments['sum_path'], validated_arguments['dump_anomalies'], output_directory,
354-
logger)
446+
if validated_arguments['detect']:
447+
perform_detection(modex_outputs_directory, output_directory, logger)
448+
else:
449+
perform_mixture(modex_outputs_directory, validated_arguments['perform_derelocation'],
450+
validated_arguments['sum_path'], validated_arguments['dump_anomalies'],
451+
output_directory,
452+
logger)
355453
else:
356454
create_output_directory(output_directory, True)
357455
logger = get_logger(output_directory, validated_arguments['log_level_supplied'])
358-
perform_extraction(validated_arguments['module'], validated_arguments['memory_dumps_directory'],
359-
validated_arguments['remove_modex_outputs'], validated_arguments['perform_derelocation'],
360-
validated_arguments['sum_path'], validated_arguments['volatility_path'],
361-
validated_arguments['dump_anomalies'], output_directory, logger)
456+
perform_operation_after_getting_modex_outputs(validated_arguments['module'],
457+
validated_arguments['memory_dumps_directory'],
458+
validated_arguments['remove_modex_outputs'],
459+
validated_arguments['perform_derelocation'],
460+
validated_arguments['sum_path'],
461+
validated_arguments['volatility_path'],
462+
validated_arguments['dump_anomalies'],
463+
validated_arguments['detect'], output_directory,
464+
logger)
362465

363466
except Exception as exception:
364467
print(f'An error occurred ({exception}). Here are more details about the error:\n')

0 commit comments

Comments
 (0)