From 4c03a2015405073b4bbb1d1bdde4b0483f6b7602 Mon Sep 17 00:00:00 2001 From: "Archer.Bowen" Date: Tue, 30 Jul 2024 16:24:36 +0100 Subject: [PATCH] #2689 C2 Beacon command methods implemented. Additional docustrings also added. --- .../red_applications/c2/c2_beacon.py | 144 +++++++++++++++--- 1 file changed, 122 insertions(+), 22 deletions(-) diff --git a/src/primaite/simulator/system/applications/red_applications/c2/c2_beacon.py b/src/primaite/simulator/system/applications/red_applications/c2/c2_beacon.py index ab5f47d4..bee00c5d 100644 --- a/src/primaite/simulator/system/applications/red_applications/c2/c2_beacon.py +++ b/src/primaite/simulator/system/applications/red_applications/c2/c2_beacon.py @@ -9,6 +9,8 @@ from ipaddress import IPv4Address from typing import Dict,Optional from primaite.simulator.network.transmission.transport_layer import Port from enum import Enum +from primaite.simulator.system.software import SoftwareHealthState +from primaite.simulator.system.applications.application import ApplicationOperatingState class C2Beacon(AbstractC2): """ @@ -36,12 +38,12 @@ class C2Beacon(AbstractC2): # Uncomment the terminal Import and the terminal property after terminal PR #@property - #def _host_db_client(self) -> Terminal: - # """Return the database client that is installed on the same machine as the Ransomware Script.""" - # db_client: DatabaseClient = self.software_manager.software.get("DatabaseClient") - # if db_client is None: - # self.sys_log.warning(f"{self.__class__.__name__} cannot find a database client on its host.") - # return db_client + #def _host_terminal(self) -> Terminal: + # """Return the Terminal that is installed on the same machine as the C2 Beacon.""" + # host_terminal: Terminal = self.software_manager.software.get("Terminal") + # if host_terminal: is None: + # self.sys_log.warning(f"{self.__class__.__name__} cannot find a terminal on its host.") + # return host_terminal def _init_request_manager(self) -> RequestManager: """ @@ -122,18 +124,18 @@ class C2Beacon(AbstractC2): return True + # I THINK that once the application is running it can respond to incoming traffic but I'll need to test this later. def establish(self) -> bool: """Establishes connection to the C2 server via a send alive. Must be called after the C2 Beacon is configured.""" - # I THINK that once the application is running it can respond to incoming traffic but I'll need to test this later. self.run() self._send_keep_alive() self.num_executions += 1 - def _handle_command_input(self, payload: MasqueradePacket) -> RequestResponse: + def _handle_command_input(self, payload: MasqueradePacket) -> bool: """ - Handles C2 Commands and executes them via the terminal service. - + Handles the parsing of C2 Commands from C2 Traffic (Masquerade Packets) + as well as then calling the relevant method dependant on the C2 Command. :param payload: The INPUT C2 Payload :type payload: MasqueradePacket @@ -143,29 +145,127 @@ class C2Beacon(AbstractC2): command = payload.payload_type if command != C2Payload: self.sys_log.warning(f"{self.name}: Received unexpected C2 command. Unable to resolve command") - return RequestResponse(status="failure", data={"Received unexpected C2Command. Unable to resolve command."}) + return self._return_command_output(RequestResponse(status="failure", data={"Received unexpected C2Command. Unable to resolve command."})) if command == C2Command.RANSOMWARE_CONFIGURE: self.sys_log.info(f"{self.name}: Received a ransomware configuration C2 command.") - return self._command_ransomware_config(payload) + return self._return_command_output(self._command_ransomware_config(payload)) elif command == C2Command.RANSOMWARE_LAUNCH: self.sys_log.info(f"{self.name}: Received a ransomware launch C2 command.") - return self._command_ransomware_launch(payload) + return self._return_command_output(self._command_ransomware_launch(payload)) elif payload.payload_type == C2Command.TERMINAL: - self.sys_log.info(f"{self.name} Received a terminal C2 command.") - return self._command_terminal(payload) + self.sys_log.info(f"{self.name}: Received a terminal C2 command.") + return self._return_command_output(self._command_terminal(payload)) else: - self.sys_log.error(f"{self.name} received an C2 command: {command} but was unable to resolve command.") - return RequestResponse(status="failure", data={"Unexpected Behaviour. Unable to resolve command."}) + self.sys_log.error(f"{self.name}: Received an C2 command: {command} but was unable to resolve command.") + return self._return_command_output(RequestResponse(status="failure", data={"Unexpected Behaviour. Unable to resolve command."})) - def _command_ransomware_config(self, payload: MasqueradePacket): - pass - def _command_ransomware_launch(self, payload: MasqueradePacket): - pass + def _return_command_output(self, command_output: RequestResponse) -> bool: + """Responsible for responding to the C2 Server with the output of the given command.""" + output_packet = MasqueradePacket( + masquerade_protocol=self.current_masquerade_protocol, + masquerade_port=self.current_masquerade_port, + payload_type=C2Payload.OUTPUT, + payload=command_output + ) + if self.send( + self, + payload=output_packet, + dest_ip_address=self.c2_remote_connection, + port=self.current_masquerade_port, + protocol=self.current_masquerade_protocol, + ): + self.sys_log.info(f"{self.name}: Command output sent to {self.c2_remote_connection}") + self.sys_log.debug(f"{self.name}: on {self.current_masquerade_port} via {self.current_masquerade_protocol}") + return True + else: + self.sys_log.warning( + f"{self.name}: failed to send a output packet. The node may be unable to access the network." + ) + return False - def _command_terminal(self, payload: MasqueradePacket): + def _command_ransomware_config(self, payload: MasqueradePacket) -> RequestResponse: + """ + C2 Command: Ransomware Configuration + + Creates a request that configures the ransomware based off the configuration options given. + This request is then sent to the terminal service in order to be executed. + + :return: Returns the Request Response returned by the Terminal execute method. + :rtype: Request Response + """ pass + #return self._host_terminal.execute(command) + + def _command_ransomware_launch(self, payload: MasqueradePacket) -> RequestResponse: + """ + C2 Command: Ransomware Execute + + Creates a request that executes the ransomware script. + This request is then sent to the terminal service in order to be executed. + + :return: Returns the Request Response returned by the Terminal execute method. + :rtype: Request Response + + Creates a Request that launches the ransomware. + """ + pass + #return self._host_terminal.execute(command) + + def _command_terminal(self, payload: MasqueradePacket) -> RequestResponse: + """ + C2 Command: Ransomware Execute + + Creates a request that executes the ransomware script. + This request is then sent to the terminal service in order to be executed. + + :return: Returns the Request Response returned by the Terminal execute method. + :rtype: Request Response + + Creates a Request that launches the ransomware. + """ + pass + #return self._host_terminal.execute(command) + + + # Not entirely sure if this actually works. + def apply_timestep(self, timestep: int) -> None: + """ + Apply a timestep to the c2_beacon. + Used to keep track of when the c2 beacon should send another keep alive. + + The following logic is applied: + + 1. Each timestep the keep_alive_inactivity is increased. + + 2. If the keep alive inactivity eclipses that of the keep alive frequency then another keep alive is sent. + + 3. If the c2 beacon receives a keep alive response packet then the ``keep_alive_inactivity`` attribute is set to 0 + + Therefore, if ``keep_alive_inactivity`` attribute is not 0, then the connection is considered severed and c2 beacon will shut down. + + :param timestep: The current timestep of the simulation. + """ + super().apply_timestep(timestep=timestep) + if self.operating_state is ApplicationOperatingState.RUNNING and self.health_state_actual is SoftwareHealthState.GOOD: + self.keep_alive_inactivity += 1 + if not self._check_c2_connection(timestep): + self.sys_log.error(f"{self.name}: Connection Severed - Application Closing.") + self.clear_connections() + self.close() + return + + + def _check_c2_connection(self, timestep) -> bool: + """Checks the C2 Server connection. If a connection cannot be confirmed then the c2 beacon will halt and close.""" + if self.keep_alive_inactivity > self.keep_alive_frequency: + self.sys_log.info(f"{self.name}: Keep Alive sent to {self.c2_remote_connection} at timestep {timestep}.") + self._send_keep_alive() + if self.keep_alive_inactivity != 0: + self.sys_log.warning(f"{self.name}: Did not receive keep alive from c2 Server. Connection considered severed.") + return False + return True \ No newline at end of file