diff --git a/analyzer/windows/analyzer.py b/analyzer/windows/analyzer.py old mode 100644 new mode 100755 index d048e34c86f..f4d39beff59 --- a/analyzer/windows/analyzer.py +++ b/analyzer/windows/analyzer.py @@ -233,8 +233,7 @@ def __init__(self): self.LASTINJECT_TIME = None self.NUM_INJECTED = 0 - @staticmethod - def get_pipe_path(name): + def get_pipe_path(self, name): """Return \\\\.\\PIPE on Windows XP and \\??\\PIPE elsewhere.""" version = sys.getwindowsversion() if version.major == 5 and version.minor == 1: @@ -351,34 +350,6 @@ def complete(self): def get_completion_key(self): return getattr(self.config, "completion_key", "") - def monitor_dcom(self): - """Add the 'DcomLaunch' service to critical process list.""" - if not self.MONITORED_DCOM: - self.MONITORED_DCOM = True - self.critical_process_by_name("DcomLaunch") - - def monitor_wmi(self): - """Add the 'winmgmt' service to critical process list.""" - if not self.MONITORED_WMI: - self.MONITORED_WMI = True - self.critical_process_by_name("winmgmt") - - def critical_process_by_name(self, process_name): - """Add the named process to the CRITICAL_PROCESS_LIST.""" - process_id = pid_from_service_name(process_name) - self.critical_process(process_id) - - def critical_process(self, process_id, sleep_msec=2000): - """Add this process_id to the CRITICAL_PROCESS_LIST.""" - if process_id: - servproc = Process(options=self.options, config=self.config, pid=process_id) - self.CRITICAL_PROCESS_LIST.append(int(process_id)) - filepath = servproc.get_filepath() - servproc.inject(interest=filepath, nosleepskip=True) - self.LASTINJECT_TIME = timeit.default_timer() - servproc.close() - KERNEL32.Sleep(sleep_msec) - def run(self): """Run analysis. @return: operation status. @@ -975,6 +946,7 @@ def remove_pid(self, pid): class CommandPipeHandler: """Pipe Handler. + This class handles the notifications received through the Pipe Server and decides what to do with them. """ @@ -1037,16 +1009,22 @@ def _handle_getpids(self, data): hidepids.update([self.analyzer.pid, self.analyzer.ppid]) return struct.pack("%dI" % len(hidepids), *hidepids) - # remove pid from process list because we received a notification - # from kernel land def _handle_kterminate(self, data): + """Handle terminate notification. + + Remove pid from process list because we received a notification + from kernel land + """ process_id = int(data) if process_id and process_id in self.analyzer.process_list.pids: self.analyzer.process_list.remove_pid(process_id) - # same than below but we don't want to inject any DLLs because - # it's a kernel analysis def _handle_kprocess(self, data): + """Handle process notification. + + Same than below but we don't want to inject any DLLs because + it's a kernel analysis + """ self.analyzer.process_lock.acquire() process_id = int(data) thread_id = None @@ -1071,15 +1049,51 @@ def _handle_ksubvert(self, data): def _handle_shell(self, data): explorer_pid = get_explorer_pid() if explorer_pid: - self.analyzer.critical_process(explorer_pid) + explorer = Process(options=self.analyzer.options, config=self.analyzer.config, pid=explorer_pid) + self.analyzer.CRITICAL_PROCESS_LIST.append(int(explorer_pid)) + filepath = explorer.get_filepath() + explorer.inject(interest=filepath, nosleepskip=True) + self.analyzer.LASTINJECT_TIME = timeit.default_timer() + explorer.close() + KERNEL32.Sleep(2000) def _handle_interop(self, data): - self.analyzer.monitor_dcom() + if not self.analyzer.MONITORED_DCOM: + self.analyzer.MONITORED_DCOM = True + dcom_pid = pid_from_service_name("DcomLaunch") + if dcom_pid: + servproc = Process(options=self.analyzer.options, config=self.analyzer.config, pid=dcom_pid) + self.analyzer.CRITICAL_PROCESS_LIST.append(int(dcom_pid)) + filepath = servproc.get_filepath() + servproc.inject(interest=filepath, nosleepskip=True) + self.analyzer.LASTINJECT_TIME = timeit.default_timer() + servproc.close() + KERNEL32.Sleep(2000) def _handle_wmi(self, data): - if not ANALYSIS_TIMED_OUT: - self.analyzer.monitor_wmi() - self.analyzer.monitor_dcom() + if not self.analyzer.MONITORED_WMI and not ANALYSIS_TIMED_OUT: + self.analyzer.MONITORED_WMI = True + if not self.analyzer.MONITORED_DCOM: + self.analyzer.MONITORED_DCOM = True + dcom_pid = pid_from_service_name("DcomLaunch") + if dcom_pid: + servproc = Process(options=self.analyzer.options, config=self.analyzer.config, pid=dcom_pid) + self.analyzer.CRITICAL_PROCESS_LIST.append(int(dcom_pid)) + filepath = servproc.get_filepath() + servproc.inject(interest=filepath, nosleepskip=True) + self.analyzer.LASTINJECT_TIME = timeit.default_timer() + servproc.close() + KERNEL32.Sleep(2000) + + wmi_pid = pid_from_service_name("winmgmt") + if wmi_pid: + servproc = Process(options=self.analyzer.options, config=self.analyzer.config, pid=wmi_pid) + self.analyzer.CRITICAL_PROCESS_LIST.append(int(wmi_pid)) + filepath = servproc.get_filepath() + servproc.inject(interest=filepath, nosleepskip=True) + self.analyzer.LASTINJECT_TIME = timeit.default_timer() + servproc.close() + KERNEL32.Sleep(2000) def _handle_tasksched(self, data): if not self.analyzer.MONITORED_TASKSCHED and not ANALYSIS_TIMED_OUT: @@ -1098,7 +1112,15 @@ def _handle_tasksched(self, data): subprocess.call("net start schedule", startupinfo=si) log.info("Started Task Scheduler Service") - self.analyzer.critical_process_by_name("schedule") + sched_pid = pid_from_service_name("schedule") + if sched_pid: + servproc = Process(options=self.analyzer.options, config=self.analyzer.config, pid=sched_pid) + self.analyzer.CRITICAL_PROCESS_LIST.append(int(sched_pid)) + filepath = servproc.get_filepath() + servproc.inject(interest=filepath, nosleepskip=True) + self.analyzer.LASTINJECT_TIME = timeit.default_timer() + servproc.close() + KERNEL32.Sleep(2000) def _handle_bits(self, data): if not self.analyzer.MONITORED_BITS and not ANALYSIS_TIMED_OUT: @@ -1114,12 +1136,31 @@ def _handle_bits(self, data): log.info("Stopped BITS Service") subprocess.call("sc config BITS type= own", startupinfo=si) - self.analyzer.monitor_dcom() + if not self.analyzer.MONITORED_DCOM: + self.analyzer.MONITORED_DCOM = True + dcom_pid = pid_from_service_name("DcomLaunch") + if dcom_pid: + servproc = Process(options=self.analyzer.options, config=self.analyzer.config, pid=dcom_pid) + + self.analyzer.CRITICAL_PROCESS_LIST.append(int(dcom_pid)) + filepath = servproc.get_filepath() + servproc.inject(interest=filepath, nosleepskip=True) + self.analyzer.LASTINJECT_TIME = timeit.default_timer() + servproc.close() + KERNEL32.Sleep(2000) log.info("Starting BITS Service") subprocess.call("net start BITS", startupinfo=si) log.info("Started BITS Service") - self.analyzer.critical_process_by_name("BITS") + bits_pid = pid_from_service_name("BITS") + if bits_pid: + servproc = Process(options=self.analyzer.options, config=self.analyzer.config, pid=bits_pid) + self.analyzer.CRITICAL_PROCESS_LIST.append(int(bits_pid)) + filepath = servproc.get_filepath() + servproc.inject(interest=filepath, nosleepskip=True) + self.analyzer.LASTINJECT_TIME = timeit.default_timer() + servproc.close() + KERNEL32.Sleep(2000) # Handle case of a service being started by a monitored process # Switch the service type to own process behind its back so we @@ -1138,8 +1179,14 @@ def _handle_service(self, servname): # if tasklist previously failed to get the services.exe PID we'll be # unable to inject if self.analyzer.SERVICES_PID: + servproc = Process(options=self.analyzer.options, config=self.analyzer.config, pid=self.analyzer.SERVICES_PID) + self.analyzer.CRITICAL_PROCESS_LIST.append(int(self.analyzer.SERVICES_PID)) + filepath = servproc.get_filepath() + servproc.inject(interest=filepath, nosleepskip=True) + self.analyzer.LASTINJECT_TIME = timeit.default_timer() + servproc.close() + KERNEL32.Sleep(1000) self.analyzer.MONITORED_SERVICES = True - self.analyzer.critical_process(self.analyzer.SERVICES_PID, sleep_msec=1000) else: log.error("Unable to monitor service %s", servname) @@ -1147,9 +1194,11 @@ def _handle_resume(self, data): # RESUME:2560,3728' self.analyzer.LASTINJECT_TIME = timeit.default_timer() - # Handle attempted shutdowns/restarts -- flush logs for all monitored processes - # additional handling can be added later def _handle_shutdown(self, data): + """Handle attempted shutdowns/restarts. + + Flush logs for all monitored processes. Additional handling can be added later. + """ log.info("Received shutdown request") self.analyzer.process_lock.acquire() for process_id in self.analyzer.process_list.pids: @@ -1161,9 +1210,11 @@ def _handle_shutdown(self, data): self.analyzer.files.dump_files() self.analyzer.process_lock.release() - # Handle case of malware terminating a process -- notify the target - # ahead of time so that it can flush its log buffer def _handle_kill(self, data): + """Handle case of malware terminating a process. + + Notify the target ahead of time so that it can flush its log buffer. + """ self.analyzer.process_lock.acquire() process_id = int(data) diff --git a/analyzer/windows/tests/test_analyzer.py b/analyzer/windows/tests/test_analyzer.py index fdd37365a52..c621355a8cc 100644 --- a/analyzer/windows/tests/test_analyzer.py +++ b/analyzer/windows/tests/test_analyzer.py @@ -2,59 +2,147 @@ import unittest from unittest.mock import MagicMock, patch +import analyzer from analyzer import Analyzer, CommandPipeHandler class TestAnalyzer(unittest.TestCase): + def setUp(self): + self.patches = [] + patch_sleep = patch("lib.common.defines.KERNEL32.Sleep") + self.patches.append(patch_sleep) + patch_call = patch("subprocess.call") + self.patches.append(patch_call) + for p in self.patches: + self.addCleanup(p.stop) + p.start() + self.analyzer = Analyzer() + self.cph = CommandPipeHandler(self.analyzer) + def test_can_instantiate(self): - analyzer = Analyzer() - self.assertIsInstance(analyzer, Analyzer) + self.assertIsInstance(self.analyzer, Analyzer) + self.assertIsInstance(self.cph, CommandPipeHandler) + + def test_get_pipe_path(self): + pipe_name = "random_text" + pipe_path = self.analyzer.get_pipe_path(pipe_name) + self.assertIsNotNone(pipe_path) + self.assertIsInstance(pipe_path, str) + self.assertIn(pipe_name, pipe_path) + self.assertIn("PIPE", pipe_path) @patch("analyzer.pid_from_service_name") - @patch("lib.api.process.Process") - def test_monitor_dcom(self, mock_process, mock_pid_from_service_name): + @patch("analyzer.Process") + def test_handle_interop(self, mock_process, mock_pid_from_service_name): mock_process.return_value = MagicMock() random_pid = random.randint(1, 99999999) mock_pid_from_service_name.return_value = random_pid - analyzer = Analyzer() - self.assertEqual(0, len(analyzer.CRITICAL_PROCESS_LIST)) - self.assertFalse(analyzer.MONITORED_DCOM) - self.assertIsNone(analyzer.LASTINJECT_TIME) - analyzer.monitor_dcom() - self.assertEqual(1, len(analyzer.CRITICAL_PROCESS_LIST)) - self.assertTrue(analyzer.MONITORED_DCOM) - self.assertIsNotNone(analyzer.LASTINJECT_TIME) - self.assertIn(random_pid, analyzer.CRITICAL_PROCESS_LIST) + ana = self.analyzer + # instead of mocking Process mock Process.get_filepath and Process.open maybe + self.assertEqual(0, len(ana.CRITICAL_PROCESS_LIST)) + self.assertFalse(ana.MONITORED_DCOM) + self.assertIsNone(ana.LASTINJECT_TIME) + self.cph._handle_interop(None) + self.assertEqual(1, len(ana.CRITICAL_PROCESS_LIST)) + self.assertTrue(ana.MONITORED_DCOM) + self.assertIsNotNone(ana.LASTINJECT_TIME) + self.assertIn(random_pid, ana.CRITICAL_PROCESS_LIST) + mock_pid_from_service_name.assert_called_once() + + @patch("analyzer.Process") + def test_handle_interop_already(self, mock_process): + """If dcom process already monitored, do nothing.""" + ana = self.analyzer + ana.MONITORED_DCOM = True + self.assertEqual(0, len(ana.CRITICAL_PROCESS_LIST)) + self.cph._handle_interop(None) + # No change to process list or last inject time + self.assertEqual(0, len(ana.CRITICAL_PROCESS_LIST)) + self.assertIsNone(ana.LASTINJECT_TIME) + mock_process.assert_not_called() @patch("analyzer.pid_from_service_name") - def test_monitor_wmi(self, mock_pid_from_service_name): - random_pid = random.randint(1, 99999999) - mock_pid_from_service_name.return_value = random_pid - analyzer = Analyzer() - self.assertEqual(0, len(analyzer.CRITICAL_PROCESS_LIST)) - self.assertFalse(analyzer.MONITORED_WMI) - self.assertIsNone(analyzer.LASTINJECT_TIME) - analyzer.monitor_wmi() - self.assertEqual(1, len(analyzer.CRITICAL_PROCESS_LIST)) - self.assertTrue(analyzer.MONITORED_WMI) - self.assertIsNotNone(analyzer.LASTINJECT_TIME) - self.assertIn(random_pid, analyzer.CRITICAL_PROCESS_LIST) + def test_handle_wmi(self, mock_pid_from_service_name): + random_pid1 = random.randint(1, 99999999) + random_pid2 = random.randint(1, 99999999) + mock_pid_from_service_name.side_effect = [random_pid1, random_pid2] + ana = self.analyzer + self.assertEqual(0, len(ana.CRITICAL_PROCESS_LIST)) + self.assertFalse(ana.MONITORED_WMI) + self.assertFalse(ana.MONITORED_DCOM) + self.assertIsNone(ana.LASTINJECT_TIME) + self.cph._handle_wmi(None) + self.assertEqual(2, len(ana.CRITICAL_PROCESS_LIST)) + self.assertTrue(ana.MONITORED_WMI) + self.assertTrue(ana.MONITORED_DCOM) + self.assertIsNotNone(ana.LASTINJECT_TIME) + self.assertIn(random_pid1, ana.CRITICAL_PROCESS_LIST) + self.assertIn(random_pid2, ana.CRITICAL_PROCESS_LIST) - def test_get_pipe_path(self): - pipe_name = "random_text" - pipe_path = Analyzer.get_pipe_path(pipe_name) - self.assertIsNotNone(pipe_path) - self.assertIsInstance(pipe_path, str) - self.assertIn(pipe_name, pipe_path) - self.assertIn("PIPE", pipe_path) + @patch("analyzer.pid_from_service_name") + def test_handle_wmi_already(self, mock_pid_from_service_name): + ana = self.analyzer + ana.MONITORED_WMI = True + self.assertEqual(0, len(ana.CRITICAL_PROCESS_LIST)) + self.assertFalse(ana.MONITORED_DCOM) + self.assertIsNone(ana.LASTINJECT_TIME) + self.cph._handle_wmi(None) + # Should be no change to DCOM or last inject time + self.assertEqual(0, len(ana.CRITICAL_PROCESS_LIST)) + self.assertFalse(ana.MONITORED_DCOM) + self.assertIsNone(ana.LASTINJECT_TIME) + mock_pid_from_service_name.assert_not_called() + + def test_handle_wmi_timed_out(self): + ana = self.analyzer + self.assertFalse(ana.MONITORED_WMI) + self.assertFalse(ana.MONITORED_DCOM) + self.assertIsNone(ana.LASTINJECT_TIME) + analyzer.ANALYSIS_TIMED_OUT = True + self.cph._handle_bits(data=None) + # Should be no change to DCOM, WMI, or last inject time + self.assertFalse(ana.MONITORED_WMI) + self.assertFalse(ana.MONITORED_DCOM) + self.assertIsNone(ana.LASTINJECT_TIME) + # put back the default value + analyzer.ANALYSIS_TIMED_OUT = False + + @patch("analyzer.pid_from_service_name") + def test_handle_bits(self, mock_pid_from_service_name): + random_pid1 = random.randint(1, 99999999) + random_pid2 = random.randint(1, 99999999) + mock_pid_from_service_name.side_effect = [random_pid1, random_pid2] + ana = self.analyzer + self.assertEqual(0, len(ana.CRITICAL_PROCESS_LIST)) + self.assertFalse(ana.MONITORED_BITS) + self.assertFalse(ana.MONITORED_DCOM) + self.assertIsNone(ana.LASTINJECT_TIME) + self.cph._handle_bits(data=None) + self.assertEqual(2, len(ana.CRITICAL_PROCESS_LIST)) + self.assertTrue(ana.MONITORED_BITS) + self.assertTrue(ana.MONITORED_DCOM) + self.assertIsNotNone(ana.LASTINJECT_TIME) + + def test_handle_bits_already(self): + ana = self.analyzer + self.assertFalse(ana.MONITORED_DCOM) + self.assertIsNone(ana.LASTINJECT_TIME) + ana.MONITORED_BITS = True + self.cph._handle_bits(data=None) + # Should be no change to DCOM or last inject time + self.assertFalse(ana.MONITORED_DCOM) + self.assertIsNone(ana.LASTINJECT_TIME) - def test_handle_bits(self): - analyzer = Analyzer() - cph = CommandPipeHandler(analyzer) - self.assertEqual(0, len(analyzer.CRITICAL_PROCESS_LIST)) - self.assertFalse(analyzer.MONITORED_DCOM) - self.assertIsNone(analyzer.LASTINJECT_TIME) - cph._handle_bits(data=None) - self.assertEqual(2, len(analyzer.CRITICAL_PROCESS_LIST)) - self.assertTrue(analyzer.MONITORED_DCOM) - self.assertIsNotNone(analyzer.LASTINJECT_TIME) + def test_handle_bits_timed_out(self): + ana = self.analyzer + self.assertFalse(ana.MONITORED_BITS) + self.assertFalse(ana.MONITORED_DCOM) + self.assertIsNone(ana.LASTINJECT_TIME) + analyzer.ANALYSIS_TIMED_OUT = True + self.cph._handle_bits(data=None) + # Should be no change to DCOM, BITS, or last inject time + self.assertFalse(ana.MONITORED_BITS) + self.assertFalse(ana.MONITORED_DCOM) + self.assertIsNone(ana.LASTINJECT_TIME) + # put back the default value + analyzer.ANALYSIS_TIMED_OUT = False diff --git a/analyzer/windows/tests/test_analyzer_files.py b/analyzer/windows/tests/test_analyzer_files.py index ca0c264a4b7..7fbd28af599 100644 --- a/analyzer/windows/tests/test_analyzer_files.py +++ b/analyzer/windows/tests/test_analyzer_files.py @@ -1,6 +1,8 @@ +"""Tests for analyzer.Files class and for protected_path() functions.""" import unittest -from analyzer import Files +import analyzer +from analyzer import Files, add_protected_path, in_protected_path class TestFiles(unittest.TestCase): @@ -20,3 +22,19 @@ def test_is_protected_filename_class_method(self): self.assertFalse(Files.is_protected_filename(not_protected)) should_be_protected = "PYTHON.EXE" self.assertTrue(Files.is_protected_filename(should_be_protected)) + + def test_add_protected_path(self): + self.assertEqual(0, len(analyzer.PROTECTED_PATH_LIST)) + add_protected_path("FOO") + self.assertEqual(1, len(analyzer.PROTECTED_PATH_LIST)) + self.assertIn("foo", analyzer.PROTECTED_PATH_LIST) + # Restore original value + analyzer.PROTECTED_PATH_LIST = [] + + def test_in_protected_path(self): + self.assertEqual(0, len(analyzer.PROTECTED_PATH_LIST)) + analyzer.PROTECTED_PATH_LIST.append("abcde") + self.assertTrue(in_protected_path("ABCDE")) + self.assertFalse(in_protected_path("foo")) + # Restore original value + analyzer.PROTECTED_PATH_LIST = [] diff --git a/analyzer/windows/tests/test_analyzer_process_list.py b/analyzer/windows/tests/test_analyzer_process_list.py index 67995908dfd..b525d431851 100644 --- a/analyzer/windows/tests/test_analyzer_process_list.py +++ b/analyzer/windows/tests/test_analyzer_process_list.py @@ -1,3 +1,4 @@ +"""Tests for analyzer.ProcessList class.""" import unittest from analyzer import ProcessList