From 556239a535312e26775bba22147228b5a3e6f0ca Mon Sep 17 00:00:00 2001 From: "Archer.Bowen" Date: Tue, 30 Jul 2024 11:17:10 +0100 Subject: [PATCH 01/46] #2689 Initial base class implementation --- .../simulator/network/protocols/masquerade.py | 30 +++ .../red_applications/c2/__init__.py | 1 + .../red_applications/c2/abstract_c2.py | 225 ++++++++++++++++++ .../red_applications/c2/c2_beacon.py | 1 + .../red_applications/c2/c2_server.py | 1 + 5 files changed, 258 insertions(+) create mode 100644 src/primaite/simulator/network/protocols/masquerade.py create mode 100644 src/primaite/simulator/system/applications/red_applications/c2/__init__.py create mode 100644 src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py create mode 100644 src/primaite/simulator/system/applications/red_applications/c2/c2_beacon.py create mode 100644 src/primaite/simulator/system/applications/red_applications/c2/c2_server.py diff --git a/src/primaite/simulator/network/protocols/masquerade.py b/src/primaite/simulator/network/protocols/masquerade.py new file mode 100644 index 00000000..93554f57 --- /dev/null +++ b/src/primaite/simulator/network/protocols/masquerade.py @@ -0,0 +1,30 @@ +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +from enum import Enum +from typing import Optional + +from primaite.simulator.network.protocols.packet import DataPacket + + +class C2Payload(Enum): + """Represents the different types of command and control payloads.""" + + KEEP_ALIVE = "keep_alive" + """C2 Keep Alive payload. Used by the C2 beacon and C2 Server to confirm their connection.""" + + INPUT = "input_command" + """C2 Input Command payload. Used by the C2 Server to send a command to the c2 beacon.""" + + OUTPUT = "output_command" + """C2 Output Command. Used by the C2 Beacon to send the results of a Input command to the c2 server.""" + + +class MasqueradePacket(DataPacket): + """Represents an generic malicious packet that is masquerading as another protocol.""" + + masquerade_protocol: Enum # The 'Masquerade' protocol that is currently in use + + masquerade_port: Enum # The 'Masquerade' port that is currently in use + + payload_type: C2Payload # The type of C2 traffic (e.g keep alive, command or command out) + + command: Optional[str] # Used to pass the actual C2 Command in C2 INPUT diff --git a/src/primaite/simulator/system/applications/red_applications/c2/__init__.py b/src/primaite/simulator/system/applications/red_applications/c2/__init__.py new file mode 100644 index 00000000..be6c00e7 --- /dev/null +++ b/src/primaite/simulator/system/applications/red_applications/c2/__init__.py @@ -0,0 +1 @@ +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK diff --git a/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py b/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py new file mode 100644 index 00000000..647bfcb5 --- /dev/null +++ b/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py @@ -0,0 +1,225 @@ +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +from abc import abstractmethod +from enum import Enum +from ipaddress import IPv4Address +from typing import Dict, Optional + +from pydantic import validate_call + +from primaite.simulator.network.protocols.masquerade import C2Payload, MasqueradePacket +from primaite.simulator.network.transmission.network_layer import IPProtocol +from primaite.simulator.network.transmission.transport_layer import Port +from primaite.simulator.system.applications.application import Application + + +class AbstractC2(Application): + """ + An abstract command and control (c2) application. + + Extends the Application class to provide base functionality for c2 suite applications + such as c2 beacons and c2 servers. + + Provides the base methods for handling ``Keep Alive`` connections, configuring masquerade ports and protocols + as well as providing the abstract methods for sending, receiving and parsing commands. + """ + + c2_connection_active: bool = False + """Indicates if the c2 server and c2 beacon are currently connected.""" + + c2_remote_connection: IPv4Address = None + """The IPv4 Address of the remote c2 connection. (Either the IP of the beacon or the server)""" + + keep_alive_sent: bool = False + """Indicates if a keep alive has been sent this timestep. Used to prevent packet storms.""" + + # We should set the application to NOT_RUNNING if the inactivity count reaches a certain thresh hold. + keep_alive_inactivity: int = 0 + """Indicates how many timesteps since the last time the c2 application received a keep alive.""" + + # These two attributes are set differently in the c2 server and c2 beacon. + # The c2 server parses the keep alive and sets these accordingly. + # The c2 beacon will set this attributes upon installation and configuration + + current_masquerade_protocol: Enum = IPProtocol.TCP + """The currently chosen protocol that the C2 traffic is masquerading as. Defaults as TCP.""" + + current_masquerade_port: Enum = Port.FTP + """The currently chosen port that the C2 traffic is masquerading as. Defaults at FTP.""" + + def __init__(self, **kwargs): + kwargs["name"] = "C2" + kwargs["port"] = self.current_masquerade_port + kwargs["protocol"] = self.current_masquerade_protocol + + # TODO: Move this duplicate method from NMAP class into 'Application' to adhere to DRY principle. + def _can_perform_network_action(self) -> bool: + """ + Checks if the C2 application can perform outbound network actions. + + This is done by checking the parent application can_per_action functionality. + Then checking if there is an enabled NIC that can be used for outbound traffic. + + :return: True if outbound network actions can be performed, otherwise False. + """ + if not super()._can_perform_action(): + return False + + for nic in self.software_manager.node.network_interface.values(): + if nic.enabled: + return True + return False + + def describe_state(self) -> Dict: + """ + Describe the state of the C2 application. + + :return: A dictionary representation of the C2 application's state. + :rtype: Dict + """ + return super().describe_state() + + # Validate call ensures we are only handling Masquerade Packets. + @validate_call + def _handle_c2_payload(self, payload: MasqueradePacket) -> bool: + """Handles masquerade payloads for both c2 beacons and c2 servers. + + Currently, the C2 application suite can handle the following payloads: + + KEEP ALIVE: + Establishes or confirms connection from the C2 Beacon to the C2 server. + Sent by both C2 beacons and C2 Servers. + + INPUT: + Contains a c2 command which must be executed by the C2 beacon. + Sent by C2 Servers and received by C2 Beacons. + + OUTPUT: + Contains the output of a c2 command which must be returned to the C2 Server. + Sent by C2 Beacons and received by C2 Servers + + The payload is passed to a different method dependant on the payload type. + + :param payload: The C2 Payload to be parsed and handled. + :return: True if the c2 payload was handled successfully, False otherwise. + """ + if payload.payload_type == C2Payload.KEEP_ALIVE: + self.sys_log.info(f"{self.name} received a KEEP ALIVE!") + return self._handle_keep_alive(payload) + + elif payload.payload_type == C2Payload.INPUT: + self.sys_log.info(f"{self.name} received an INPUT COMMAND!") + return self._handle_command_input(payload) + + elif payload.payload_type == C2Payload.OUTPUT: + self.sys_log.info(f"{self.name} received an OUTPUT COMMAND!") + return self._handle_command_input(payload) + + else: + self.sys_log.warning( + f"{self.name} received an unexpected c2 payload:{payload.payload_type}. Dropping Packet." + ) + return False + + # Abstract method + # Used in C2 server to prase and receive the output of commands sent to the c2 beacon. + @abstractmethod + def _handle_command_output(payload): + """Abstract Method: Used in C2 server to prase and receive the output of commands sent to the c2 beacon.""" + pass + + # Abstract method + # Used in C2 beacon to parse and handle commands received from the c2 server. + @abstractmethod + def _handle_command_input(payload): + """Abstract Method: Used in C2 beacon to parse and handle commands received from the c2 server.""" + pass + + def _handle_keep_alive(self) -> bool: + """ + Handles receiving and sending keep alive payloads. This method is only called if we receive a keep alive. + + Returns False if a keep alive was unable to be sent. + Returns True if a keep alive was successfully sent or already has been sent this timestep. + """ + # Using this guard clause to prevent packet storms and recognise that we've achieved a connection. + if self.keep_alive_sent: + self.c2_connection_active = True # Sets the connection to active + self.keep_alive_inactivity = 0 # Sets the keep alive inactivity to zero + + # Return early without sending another keep alive and then setting keep alive_sent false for next timestep. + self.keep_alive_sent = False + return True + + # If we've reached this part of the method then we've received a keep alive but haven't sent a reply. + + # If this method returns true then we have sent successfully sent a keep alive. + if self._send_keep_alive(self): + # debugging/info logging that we successfully sent a keep alive + + # Now when the returning keep_alive comes back we won't send another keep alive + self.keep_alive_sent = True + return True + + else: + # debugging/info logging that we unsuccessfully sent a keep alive. + return False + + def receive(self, payload: MasqueradePacket, session_id: Optional[str] = None) -> bool: + """Receives masquerade packets. Used by both c2 server and c2 client. + + :param payload: The Masquerade Packet to be received. + :param session: The transport session that the payload is originating from. + """ + return self._handle_c2_payload(payload, session_id) + + def _send_keep_alive(self) -> bool: + """Sends a C2 keep alive payload to the self.remote_connection IPv4 Address.""" + # Checking that the c2 application is capable of performing both actions and has an enabled NIC + # (Using NOT to improve code readability) + if not self._can_perform_network_action(): + self.sys_log.warning(f"{self.name}: Unable to perform network actions.") + return False + + # We also Pass masquerade protocol/port so that the c2 server can reply on the correct protocol/port. + # (This also lays the foundations for switching masquerade port/protocols mid episode.) + keep_alive_packet = MasqueradePacket( + masquerade_protocol=self.current_masquerade_protocol, + masquerade_port=self.current_masquerade_port, + payload_type=C2Payload.KEEP_ALIVE, + ) + + # C2 Server will need to c2_remote_connection after it receives it's first keep alive. + if self.send( + self, + payload=keep_alive_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}: Keep Alive sent to {self.c2_remote_connection}") + self.sys_log.debug(f"{self.name}: on {self.current_masquerade_port} via {self.current_masquerade_protocol}") + self.receive(payload=keep_alive_packet) + return True + else: + self.sys_log.warning( + f"{self.name}: failed to send a Keep Alive. The node may be unable to access the network." + ) + return False + + @abstractmethod + def configure( + self, + c2_server_ip_address: Optional[IPv4Address] = None, + keep_alive_frequency: Optional[int] = 5, + masquerade_protocol: Optional[Enum] = IPProtocol.TCP, + masquerade_port: Optional[Enum] = Port.FTP, + ) -> bool: + """ + Configures the C2 beacon to communicate with the C2 server with following additional parameters. + + :param c2_server_ip_address: The IP Address of the C2 Server. Used to establish connection. + :param keep_alive_frequency: The frequency (timesteps) at which the C2 beacon will send keep alives. + :param masquerade_protocol: The Protocol that C2 Traffic will masquerade as. Defaults as TCP. + :param masquerade_port: The Port that the C2 Traffic will masquerade as. Defaults to FTP. + """ + pass 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 new file mode 100644 index 00000000..be6c00e7 --- /dev/null +++ b/src/primaite/simulator/system/applications/red_applications/c2/c2_beacon.py @@ -0,0 +1 @@ +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK diff --git a/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py b/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py new file mode 100644 index 00000000..be6c00e7 --- /dev/null +++ b/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py @@ -0,0 +1 @@ +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK From 8320ec524b35ca05eb0a43c009529ddbfb3069fc Mon Sep 17 00:00:00 2001 From: "Archer.Bowen" Date: Tue, 30 Jul 2024 13:04:20 +0100 Subject: [PATCH 02/46] #2689 Initial C2 Beacon command handling functionality implemented. --- .../red_applications/c2/abstract_c2.py | 50 +++--- .../red_applications/c2/c2_beacon.py | 167 ++++++++++++++++++ .../red_applications/c2/c2_server.py | 9 + 3 files changed, 201 insertions(+), 25 deletions(-) diff --git a/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py b/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py index 647bfcb5..c7f90b9d 100644 --- a/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py +++ b/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py @@ -11,16 +11,34 @@ from primaite.simulator.network.transmission.network_layer import IPProtocol from primaite.simulator.network.transmission.transport_layer import Port from primaite.simulator.system.applications.application import Application +class C2Command(Enum): + """ + Enumerations representing the different commands the C2 suite currently supports. + """ + + RANSOMWARE_CONFIGURE = "Ransomware Configure" + "Instructs the c2 beacon to configure the ransomware with the provided options." + + RANSOMWARE_LAUNCH = "Ransomware Launch" + "Instructs the c2 beacon to execute the installed ransomware." + + TERMINAL = "Terminal" + "Instructs the c2 beacon to execute the provided terminal command." + + # The terminal command should also be able to pass a session which can be used for remote connections. + class AbstractC2(Application): """ An abstract command and control (c2) application. - Extends the Application class to provide base functionality for c2 suite applications - such as c2 beacons and c2 servers. + Extends the application class to provide base functionality for c2 suite applications + such as c2 beacons and c2 servers. Provides the base methods for handling ``Keep Alive`` connections, configuring masquerade ports and protocols as well as providing the abstract methods for sending, receiving and parsing commands. + + Defaults to masquerading as HTTP (Port 80) via TCP. """ c2_connection_active: bool = False @@ -43,13 +61,8 @@ class AbstractC2(Application): current_masquerade_protocol: Enum = IPProtocol.TCP """The currently chosen protocol that the C2 traffic is masquerading as. Defaults as TCP.""" - current_masquerade_port: Enum = Port.FTP - """The currently chosen port that the C2 traffic is masquerading as. Defaults at FTP.""" - - def __init__(self, **kwargs): - kwargs["name"] = "C2" - kwargs["port"] = self.current_masquerade_port - kwargs["protocol"] = self.current_masquerade_protocol + current_masquerade_port: Enum = Port.HTTP + """The currently chosen port that the C2 traffic is masquerading as. Defaults at HTTP.""" # TODO: Move this duplicate method from NMAP class into 'Application' to adhere to DRY principle. def _can_perform_network_action(self) -> bool: @@ -176,6 +189,9 @@ class AbstractC2(Application): """Sends a C2 keep alive payload to the self.remote_connection IPv4 Address.""" # Checking that the c2 application is capable of performing both actions and has an enabled NIC # (Using NOT to improve code readability) + if self.c2_remote_connection == None: + self.sys_log.error(f"{self.name}: Unable to Establish connection as the C2 Server's IP Address has not been given.") + if not self._can_perform_network_action(): self.sys_log.warning(f"{self.name}: Unable to perform network actions.") return False @@ -206,20 +222,4 @@ class AbstractC2(Application): ) return False - @abstractmethod - def configure( - self, - c2_server_ip_address: Optional[IPv4Address] = None, - keep_alive_frequency: Optional[int] = 5, - masquerade_protocol: Optional[Enum] = IPProtocol.TCP, - masquerade_port: Optional[Enum] = Port.FTP, - ) -> bool: - """ - Configures the C2 beacon to communicate with the C2 server with following additional parameters. - :param c2_server_ip_address: The IP Address of the C2 Server. Used to establish connection. - :param keep_alive_frequency: The frequency (timesteps) at which the C2 beacon will send keep alives. - :param masquerade_protocol: The Protocol that C2 Traffic will masquerade as. Defaults as TCP. - :param masquerade_port: The Port that the C2 Traffic will masquerade as. Defaults to FTP. - """ - pass 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 be6c00e7..822e6ba9 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 @@ -1 +1,168 @@ # © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +from primaite.simulator.system.applications.red_applications.c2.abstract_c2 import AbstractC2, C2Command +#from primaite.simulator.system.services.terminal.terminal import Terminal +from primaite.simulator.core import RequestManager, RequestType +from primaite.interface.request import RequestFormat, RequestResponse +from primaite.simulator.network.protocols.masquerade import C2Payload, MasqueradePacket +from primaite.simulator.network.transmission.network_layer import IPProtocol +from ipaddress import IPv4Address +from typing import Dict,Optional +from primaite.simulator.network.transmission.transport_layer import Port +from enum import Enum + +class C2Beacon(AbstractC2): + """ + C2 Beacon Application. + + Represents a generic C2 beacon which can be used in conjunction with the C2 Server + to simulate malicious communications within primAITE. + + Must be configured with the C2 Server's Ip Address upon installation. + + Extends the Abstract C2 application to include the following: + + 1. Receiving commands from the C2 Server (Command input) + 2. Leveraging the terminal application to execute requests (dependant on the command given) + 3. Sending the RequestResponse back to the C2 Server (Command output) + """ + + keep_alive_frequency: int = 5 + "The frequency at which ``Keep Alive`` packets are sent to the C2 Server from the C2 Beacon." + + + # Uncomment the Import and this 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 _init_request_manager(self) -> RequestManager: + """ + Initialise the request manager. + + More information in user guide and docstring for SimComponent._init_request_manager. + """ + rm = super()._init_request_manager() + rm.add_request( + name="execute", + request_type=RequestType(func=lambda request, context: RequestResponse.from_bool(self.establish())), + ) + + def _configure(request: RequestFormat, context: Dict) -> RequestResponse: + """ + Request for configuring the C2 Beacon. + + :param request: Request with one element containing a dict of parameters for the configure method. + :type request: RequestFormat + :param context: additional context for resolving this action, currently unused + :type context: dict + :return: RequestResponse object with a success code reflecting whether the configuration could be applied. + :rtype: RequestResponse + """ + server_ip = request[-1].get("c2_server_ip_address") + if server_ip == None: + self.sys_log.error(f"{self.name}: Did not receive C2 Server IP in configuration parameters.") + RequestResponse(status="failure", data={"No C2 Server IP given to C2 beacon. Unable to configure C2 Beacon"}) + + c2_remote_ip = IPv4Address(c2_remote_ip) + frequency = request[-1].get("keep_alive_frequency") + protocol= request[-1].get("masquerade_protocol") + port = request[-1].get("masquerade_port") + return RequestResponse.from_bool(self.configure(c2_server_ip_address=server_ip, + keep_alive_frequency=frequency, + masquerade_protocol=protocol, + masquerade_port=port)) + + rm.add_request("configure", request_type=RequestType(func=_configure)) + return rm + + def __init__(self, **kwargs): + self.name = "C2Beacon" + super.__init__(**kwargs) + + def configure( + self, + c2_server_ip_address: IPv4Address = None, + keep_alive_frequency: Optional[int] = 5, + masquerade_protocol: Optional[Enum] = IPProtocol.TCP, + masquerade_port: Optional[Enum] = Port.HTTP, + ) -> bool: + """ + Configures the C2 beacon to communicate with the C2 server with following additional parameters. + + + :param c2_server_ip_address: The IP Address of the C2 Server. Used to establish connection. + :type c2_server_ip_address: IPv4Address + :param keep_alive_frequency: The frequency (timesteps) at which the C2 beacon will send keep alive(s). + :type keep_alive_frequency: Int + :param masquerade_protocol: The Protocol that C2 Traffic will masquerade as. Defaults as TCP. + :type masquerade_protocol: Enum (IPProtocol) + :param masquerade_port: The Port that the C2 Traffic will masquerade as. Defaults to FTP. + :type masquerade_port: Enum (Port) + """ + self.c2_remote_connection = c2_server_ip_address + self.keep_alive_frequency = keep_alive_frequency + self.current_masquerade_port = masquerade_port + self.current_masquerade_protocol = masquerade_protocol + self.sys_log.info( + f"{self.name}: Configured {self.name} with remote C2 server connection: {c2_server_ip_address=}." + ) + self.sys_log.debug(f"{self.name}: configured with the following settings:" + f"Remote C2 Server: {c2_server_ip_address}" + f"Keep Alive Frequency {keep_alive_frequency}" + f"Masquerade Protocol: {masquerade_protocol}" + f"Masquerade Port: {masquerade_port}") + return True + + + 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: + """ + Handles C2 Commands and executes them via the terminal service. + + + :param payload: The INPUT C2 Payload + :type payload: MasqueradePacket + :return: The Request Response provided by the terminal execute method. + :rtype Request Response: + """ + 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."}) + + if command == C2Command.RANSOMWARE_CONFIGURE: + self.sys_log.info(f"{self.name}: Received a ransomware configuration C2 command.") + return 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) + + elif payload.payload_type == C2Command.TERMINAL: + self.sys_log.info(f"{self.name} Received a terminal C2 command.") + return 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."}) + + def _command_ransomware_config(self, payload: MasqueradePacket): + pass + + def _command_ransomware_launch(self, payload: MasqueradePacket): + pass + + def _command_terminal(self, payload: MasqueradePacket): + pass diff --git a/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py b/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py index be6c00e7..648ace47 100644 --- a/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py +++ b/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py @@ -1 +1,10 @@ # © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +from primaite.simulator.system.applications.red_applications.c2.abstract_c2 import AbstractC2, C2Command +from primaite.simulator.network.protocols.masquerade import C2Payload, MasqueradePacket + + +class C2Server(AbstractC2): + + def _handle_command_output(payload): + """Abstract Method: Used in C2 server to prase and receive the output of commands sent to the c2 beacon.""" + pass From 8a00a2a29d990c39dbc2dd130a7a1da15964a811 Mon Sep 17 00:00:00 2001 From: "Archer.Bowen" Date: Tue, 30 Jul 2024 13:10:23 +0100 Subject: [PATCH 03/46] #2689 Added TODOs for future reference. --- .../system/applications/red_applications/c2/abstract_c2.py | 7 +++++++ .../system/applications/red_applications/c2/c2_beacon.py | 7 +++++-- .../system/applications/red_applications/c2/c2_server.py | 5 ++++- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py b/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py index c7f90b9d..eabbf476 100644 --- a/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py +++ b/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py @@ -11,6 +11,13 @@ from primaite.simulator.network.transmission.network_layer import IPProtocol from primaite.simulator.network.transmission.transport_layer import Port from primaite.simulator.system.applications.application import Application +# TODO: +# Complete C2 Server and C2 Beacon TODOs +# Create test that leverage all the functionality needed for the different TAPs +# Create a .RST doc +# Potentially? A notebook which demonstrates a custom red agent using the c2 server for various means. + + class C2Command(Enum): """ Enumerations representing the different commands the C2 suite currently supports. 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 822e6ba9..ab5f47d4 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 @@ -29,8 +29,11 @@ class C2Beacon(AbstractC2): keep_alive_frequency: int = 5 "The frequency at which ``Keep Alive`` packets are sent to the C2 Server from the C2 Beacon." - - # Uncomment the Import and this Property after terminal PR + # TODO: + # Implement the placeholder command methods + # Implement the keep alive frequency. + # Implement a command output method that sends the RequestResponse to the C2 server. + # Uncomment the terminal Import and the terminal property after terminal PR #@property #def _host_db_client(self) -> Terminal: diff --git a/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py b/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py index 648ace47..5f8824cd 100644 --- a/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py +++ b/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py @@ -4,7 +4,10 @@ from primaite.simulator.network.protocols.masquerade import C2Payload, Masquerad class C2Server(AbstractC2): + # TODO: + # Implement the request manager and agent actions. + # Implement the output handling methods. (These need to interface with the actions) def _handle_command_output(payload): - """Abstract Method: Used in C2 server to prase and receive the output of commands sent to the c2 beacon.""" + """Abstract Method: Used in C2 server to parse and receive the output of commands sent to the c2 beacon.""" pass From 4c03a2015405073b4bbb1d1bdde4b0483f6b7602 Mon Sep 17 00:00:00 2001 From: "Archer.Bowen" Date: Tue, 30 Jul 2024 16:24:36 +0100 Subject: [PATCH 04/46] #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 From e4358b02bc262517934f8a4c72950182a58c95be Mon Sep 17 00:00:00 2001 From: "Archer.Bowen" Date: Tue, 30 Jul 2024 17:18:28 +0100 Subject: [PATCH 05/46] #2689 Improving comments in abstract c2 --- .../applications/red_applications/c2/abstract_c2.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py b/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py index eabbf476..bd9219c2 100644 --- a/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py +++ b/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py @@ -161,8 +161,10 @@ class AbstractC2(Application): Returns False if a keep alive was unable to be sent. Returns True if a keep alive was successfully sent or already has been sent this timestep. """ + self.sys_log.info(f"{self.name}: Keep Alive Received from {self.c2_remote_connection}") # Using this guard clause to prevent packet storms and recognise that we've achieved a connection. if self.keep_alive_sent: + self.sys_log.info(f"{self.name}: Connection successfully established with {self.c2_remote_connection}") self.c2_connection_active = True # Sets the connection to active self.keep_alive_inactivity = 0 # Sets the keep alive inactivity to zero @@ -174,14 +176,11 @@ class AbstractC2(Application): # If this method returns true then we have sent successfully sent a keep alive. if self._send_keep_alive(self): - # debugging/info logging that we successfully sent a keep alive - - # Now when the returning keep_alive comes back we won't send another keep alive - self.keep_alive_sent = True + self.keep_alive_sent = True # Setting the guard clause to true (prevents packet storms.) return True + # Return false if we're unable to send handle the keep alive correctly. else: - # debugging/info logging that we unsuccessfully sent a keep alive. return False def receive(self, payload: MasqueradePacket, session_id: Optional[str] = None) -> bool: From f097ed575dd23f7987c0c93ec016ce25268af3e9 Mon Sep 17 00:00:00 2001 From: "Archer.Bowen" Date: Wed, 31 Jul 2024 10:26:58 +0100 Subject: [PATCH 06/46] #2689 minor docustring and type hint change --- .../system/applications/red_applications/c2/abstract_c2.py | 3 ++- .../system/applications/red_applications/c2/c2_beacon.py | 2 +- .../system/applications/red_applications/c2/c2_server.py | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py b/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py index bd9219c2..e45333e5 100644 --- a/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py +++ b/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py @@ -191,7 +191,7 @@ class AbstractC2(Application): """ return self._handle_c2_payload(payload, session_id) - def _send_keep_alive(self) -> bool: + def _send_keep_alive(self, session_id: Optional[str]) -> bool: """Sends a C2 keep alive payload to the self.remote_connection IPv4 Address.""" # Checking that the c2 application is capable of performing both actions and has an enabled NIC # (Using NOT to improve code readability) @@ -217,6 +217,7 @@ class AbstractC2(Application): dest_ip_address=self.c2_remote_connection, port=self.current_masquerade_port, protocol=self.current_masquerade_protocol, + session_id=session_id, ): self.sys_log.info(f"{self.name}: Keep Alive sent to {self.c2_remote_connection}") self.sys_log.debug(f"{self.name}: on {self.current_masquerade_port} via {self.current_masquerade_protocol}") 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 bee00c5d..e0ad30f8 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 @@ -261,7 +261,7 @@ class C2Beacon(AbstractC2): 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.""" + """Checks the C2 Server connection. If a connection cannot be confirmed then this method will return false otherwise true.""" 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() diff --git a/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py b/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py index 5f8824cd..05ff30d9 100644 --- a/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py +++ b/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py @@ -11,3 +11,4 @@ class C2Server(AbstractC2): def _handle_command_output(payload): """Abstract Method: Used in C2 server to parse and receive the output of commands sent to the c2 beacon.""" pass + \ No newline at end of file From 4c7e465f0df5c7496f43096f9f723b43fa4ffeea Mon Sep 17 00:00:00 2001 From: "Archer.Bowen" Date: Wed, 31 Jul 2024 16:43:17 +0100 Subject: [PATCH 07/46] #2689 Initial Implementation of C2 Server. --- src/primaite/game/game.py | 2 + .../red_applications/c2/abstract_c2.py | 50 +++++-- .../red_applications/c2/c2_beacon.py | 2 +- .../red_applications/c2/c2_server.py | 138 +++++++++++++++++- .../_red_applications/test_c2_suite.py | 56 +++++++ 5 files changed, 233 insertions(+), 15 deletions(-) create mode 100644 tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_c2_suite.py diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index 5ef8c14c..8cddbcda 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -36,6 +36,8 @@ from primaite.simulator.system.applications.red_applications.data_manipulation_b ) from primaite.simulator.system.applications.red_applications.dos_bot import DoSBot # noqa: F401 from primaite.simulator.system.applications.red_applications.ransomware_script import RansomwareScript # noqa: F401 +from primaite.simulator.system.applications.red_applications.c2.c2_beacon import C2Beacon +from primaite.simulator.system.applications.red_applications.c2.c2_server import C2Server from primaite.simulator.system.applications.web_browser import WebBrowser # noqa: F401 from primaite.simulator.system.services.database.database_service import DatabaseService from primaite.simulator.system.services.dns.dns_client import DNSClient diff --git a/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py b/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py index e45333e5..1cea972f 100644 --- a/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py +++ b/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py @@ -100,7 +100,7 @@ class AbstractC2(Application): # Validate call ensures we are only handling Masquerade Packets. @validate_call - def _handle_c2_payload(self, payload: MasqueradePacket) -> bool: + def _handle_c2_payload(self, payload: MasqueradePacket, session_id: Optional[str] = None) -> bool: """Handles masquerade payloads for both c2 beacons and c2 servers. Currently, the C2 application suite can handle the following payloads: @@ -121,18 +121,19 @@ class AbstractC2(Application): :param payload: The C2 Payload to be parsed and handled. :return: True if the c2 payload was handled successfully, False otherwise. + :rtype: Bool """ if payload.payload_type == C2Payload.KEEP_ALIVE: - self.sys_log.info(f"{self.name} received a KEEP ALIVE!") - return self._handle_keep_alive(payload) + self.sys_log.info(f"{self.name} received a KEEP ALIVE payload.") + return self._handle_keep_alive(payload, session_id) elif payload.payload_type == C2Payload.INPUT: - self.sys_log.info(f"{self.name} received an INPUT COMMAND!") - return self._handle_command_input(payload) + self.sys_log.info(f"{self.name} received an INPUT COMMAND payload.") + return self._handle_command_input(payload, session_id) elif payload.payload_type == C2Payload.OUTPUT: - self.sys_log.info(f"{self.name} received an OUTPUT COMMAND!") - return self._handle_command_input(payload) + self.sys_log.info(f"{self.name} received an OUTPUT COMMAND payload.") + return self._handle_command_input(payload, session_id) else: self.sys_log.warning( @@ -154,12 +155,15 @@ class AbstractC2(Application): """Abstract Method: Used in C2 beacon to parse and handle commands received from the c2 server.""" pass - def _handle_keep_alive(self) -> bool: + def _handle_keep_alive(self, payload: MasqueradePacket, session_id: Optional[str]) -> bool: """ Handles receiving and sending keep alive payloads. This method is only called if we receive a keep alive. Returns False if a keep alive was unable to be sent. Returns True if a keep alive was successfully sent or already has been sent this timestep. + + :return: True if successfully handled, false otherwise. + :rtype: Bool """ self.sys_log.info(f"{self.name}: Keep Alive Received from {self.c2_remote_connection}") # Using this guard clause to prevent packet storms and recognise that we've achieved a connection. @@ -173,9 +177,12 @@ class AbstractC2(Application): return True # If we've reached this part of the method then we've received a keep alive but haven't sent a reply. + # Therefore we also need to configure the masquerade attributes based off the keep alive sent. + if not self._resolve_keep_alive(self, payload): + return False # If this method returns true then we have sent successfully sent a keep alive. - if self._send_keep_alive(self): + if self._send_keep_alive(self, session_id): self.keep_alive_sent = True # Setting the guard clause to true (prevents packet storms.) return True @@ -230,3 +237,28 @@ class AbstractC2(Application): return False + def _resolve_keep_alive(self, payload: MasqueradePacket) -> bool: + """ + Parses the Masquerade Port/Protocol within the received Keep Alive packet. + + Used to dynamically set the Masquerade Port and Protocol based on incoming traffic. + + Returns True on successfully extracting and configuring the masquerade port/protocols. + Returns False otherwise. + + :param payload: The Keep Alive payload received. + :type payload: MasqueradePacket + :return: True on successful configuration, false otherwise. + :rtype: bool + """ + # Validating that they are valid Enums. + if payload.masquerade_port or payload.masquerade_protocol != Enum: + self.sys_log.warning(f"{self.name}: Received invalid Masquerade type. Port: {type(payload.masquerade_port)} Protocol: {type(payload.masquerade_protocol)}") + return False + # TODO: Validation on Ports (E.g only allow HTTP, FTP etc) + # Potentially compare to IPProtocol & Port children (Same way that abstract TAP does it with kill chains) + + # Setting the Ports + self.current_masquerade_port = payload.masquerade_port + self.current_masquerade_protocol = payload.masquerade_protocol + return True \ No newline at end of file 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 e0ad30f8..f8db5398 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 @@ -155,7 +155,7 @@ class C2Beacon(AbstractC2): self.sys_log.info(f"{self.name}: Received a ransomware launch C2 command.") return self._return_command_output(self._command_ransomware_launch(payload)) - elif payload.payload_type == C2Command.TERMINAL: + elif command == C2Command.TERMINAL: self.sys_log.info(f"{self.name}: Received a terminal C2 command.") return self._return_command_output(self._command_terminal(payload)) diff --git a/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py b/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py index 05ff30d9..8ab10d22 100644 --- a/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py +++ b/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py @@ -1,14 +1,142 @@ # © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from primaite.simulator.system.applications.red_applications.c2.abstract_c2 import AbstractC2, C2Command from primaite.simulator.network.protocols.masquerade import C2Payload, MasqueradePacket - +from primaite.simulator.core import RequestManager, RequestType +from primaite.interface.request import RequestFormat, RequestResponse +from typing import Dict,Optional class C2Server(AbstractC2): # TODO: # Implement the request manager and agent actions. # Implement the output handling methods. (These need to interface with the actions) + def _init_request_manager(self) -> RequestManager: + """ + Initialise the request manager. + + More information in user guide and docstring for SimComponent._init_request_manager. + """ + rm = super()._init_request_manager() + rm.add_request( + name="c2_ransomware_configure", + request_type=RequestType(func=lambda request, context: RequestResponse.from_bool(_configure_ransomware_action())), + ) + rm.add_request( + name="c2_ransomware_launch", + request_type=RequestType(func=lambda request, context: RequestResponse.from_bool(_launch_ransomware_action())), + ) + rm.add_request( + name="c2_terminal_command", + request_type=RequestType(func=lambda request, context: RequestResponse.from_bool(_remote_terminal_action())), + ) + + def _configure_ransomware_action(request: RequestFormat, context: Dict) -> RequestResponse: + """Requests - Sends a RANSOMWARE_CONFIGURE C2Command to the C2 Beacon with the given parameters. + + :param request: Request with one element containing a dict of parameters for the configure method. + :type request: RequestFormat + :param context: additional context for resolving this action, currently unused + :type context: dict + :return: RequestResponse object with a success code reflecting whether the configuration could be applied. + :rtype: RequestResponse + """ + # TODO: Parse the parameters from the request to get the parameters + placeholder: dict = {} + return self._send_command(given_command=C2Command.RANSOMWARE_CONFIGURE, command_options=placeholder) + + def _launch_ransomware_action(request: RequestFormat, context: Dict) -> RequestResponse: + """Agent Action - Sends a RANSOMWARE_LAUNCH C2Command to the C2 Beacon with the given parameters. + + :param request: Request with one element containing a dict of parameters for the configure method. + :type request: RequestFormat + :param context: additional context for resolving this action, currently unused + :type context: dict + :return: RequestResponse object with a success code reflecting whether the ransomware was launched. + :rtype: RequestResponse + """ + # TODO: Parse the parameters from the request to get the parameters + placeholder: dict = {} + return self._send_command(given_command=C2Command.RANSOMWARE_LAUNCH, command_options=placeholder) + + def _remote_terminal_action(request: RequestFormat, context: Dict) -> RequestResponse: + """Agent Action - Sends a TERMINAL C2Command to the C2 Beacon with the given parameters. + + :param request: Request with one element containing a dict of parameters for the configure method. + :type request: RequestFormat + :param context: additional context for resolving this action, currently unused + :type context: dict + :return: RequestResponse object with a success code reflecting whether the ransomware was launched. + :rtype: RequestResponse + """ + # TODO: Parse the parameters from the request to get the parameters + placeholder: dict = {} + return self._send_command(given_command=C2Command.RANSOMWARE_LAUNCH, command_options=placeholder) + + + def _handle_command_output(self, payload: MasqueradePacket) -> RequestResponse: + """ + Handles the parsing of C2 Command Output from C2 Traffic (Masquerade Packets) + as well as then calling the relevant method dependant on the C2 Command. + + :param payload: The OUTPUT C2 Payload + :type payload: MasqueradePacket + :return: Returns the Request Response of the C2 Beacon's host terminal service execute method. + :rtype Request Response: + """ + self.sys_log.info(f"{self.name}: Received command response from C2 Beacon: {payload}.") + command_output = payload.payload + if command_output != MasqueradePacket: + self.sys_log.warning(f"{self.name}: Received invalid command response: {command_output}.") + return RequestResponse(status="failure", data={"Received unexpected C2 Response."}) + return command_output - def _handle_command_output(payload): - """Abstract Method: Used in C2 server to parse and receive the output of commands sent to the c2 beacon.""" - pass - \ No newline at end of file + def _send_command(self, given_command: C2Command, command_options: Dict) -> RequestResponse: + """ + Sends a command to the C2 Beacon. + + # TODO: Expand this docustring. + + :param given_command: The C2 command to be sent to the C2 Beacon. + :type given_command: C2Command. + :param command_options: The relevant C2 Beacon parameters. + :type command_options: Dict + :return: Returns the Request Response of the C2 Beacon's host terminal service execute method. + :rtype: RequestResponse + """ + if given_command != C2Payload: + self.sys_log.warning(f"{self.name}: Received unexpected C2 command. Unable to send command.") + return RequestResponse(status="failure", data={"Received unexpected C2Command. Unable to send command."}) + + self.sys_log.info(f"{self.name}: Attempting to send command {given_command}.") + command_packet = self._craft_packet(given_command=given_command, command_options=command_options) + + # Need to investigate if this is correct. + if self.send(self, payload=command_packet,dest_ip_address=self.c2_remote_connection, + port=self.current_masquerade_port, protocol=self.current_masquerade_protocol,session_id=None): + self.sys_log.info(f"{self.name}: Successfully sent {given_command}.") + self.sys_log.info(f"{self.name}: Awaiting command response {given_command}.") + return self._handle_command_output(command_packet) + + + # TODO: Perhaps make a new pydantic base model for command_options? + # TODO: Perhaps make the return optional? Returns False is the packet was unable to be crafted. + def _craft_packet(self, given_command: C2Command, command_options: Dict) -> MasqueradePacket: + """ + Creates a Masquerade Packet based off the command parameter and the arguments given. + + :param given_command: The C2 command to be sent to the C2 Beacon. + :type given_command: C2Command. + :param command_options: The relevant C2 Beacon parameters. + :type command_options: Dict + :return: Returns the construct MasqueradePacket + :rtype: MasqueradePacket + """ + # TODO: Validation on command_options. + constructed_packet = MasqueradePacket( + masquerade_protocol=self.current_masquerade_protocol, + masquerade_port=self.current_masquerade_port, + payload_type=C2Payload.INPUT, + command=given_command, + payload=command_options + ) + return constructed_packet + \ No newline at end of file diff --git a/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_c2_suite.py b/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_c2_suite.py new file mode 100644 index 00000000..e5fee496 --- /dev/null +++ b/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_c2_suite.py @@ -0,0 +1,56 @@ +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +from typing import Tuple + +import pytest + +from primaite.game.agent.interface import ProxyAgent +from primaite.game.game import PrimaiteGame +from primaite.simulator.network.container import Network +from primaite.simulator.network.hardware.nodes.host.computer import Computer +from primaite.simulator.network.hardware.nodes.host.server import Server +from primaite.simulator.network.hardware.nodes.network.router import ACLAction, Router +from primaite.simulator.network.hardware.nodes.network.switch import Switch +from primaite.simulator.network.transmission.network_layer import IPProtocol +from primaite.simulator.network.transmission.transport_layer import Port +from primaite.simulator.system.services.dns.dns_server import DNSServer +from primaite.simulator.system.services.service import ServiceOperatingState +from primaite.simulator.system.services.web_server.web_server import WebServer +from primaite.simulator.system.applications.red_applications.c2.c2_beacon import C2Beacon +from primaite.simulator.system.applications.red_applications.c2.c2_server import C2Server + +# TODO: Update these tests. + +@pytest.fixture(scope="function") +def c2_server_on_computer() -> Tuple[C2Beacon, Computer]: + computer: Computer = Computer( + hostname="node_a", ip_address="192.168.0.10", subnet_mask="255.255.255.0", start_up_duration=0 + ) + computer.power_on() + c2_beacon = computer.software_manager.software.get("C2Beacon") + + return [c2_beacon, computer] + +@pytest.fixture(scope="function") +def c2_server_on_computer() -> Tuple[C2Server, Computer]: + computer: Computer = Computer( + hostname="node_b", ip_address="192.168.0.11", subnet_mask="255.255.255.0", start_up_duration=0 + ) + computer.power_on() + c2_server = computer.software_manager.software.get("C2Server") + + return [c2_server, computer] + + + +@pytest.fixture(scope="function") +def basic_network() -> Network: + network = Network() + node_a = Computer(hostname="node_a", ip_address="192.168.0.10", subnet_mask="255.255.255.0", start_up_duration=0) + node_a.power_on() + node_a.software_manager.get_open_ports() + + node_b = Computer(hostname="node_b", ip_address="192.168.0.11", subnet_mask="255.255.255.0", start_up_duration=0) + node_b.power_on() + network.connect(node_a.network_interface[1], node_b.network_interface[1]) + + return network \ No newline at end of file From e09c0ad4ac49a5de34e2b4b9dea719acfe56ddd7 Mon Sep 17 00:00:00 2001 From: "Archer.Bowen" Date: Thu, 1 Aug 2024 10:11:03 +0100 Subject: [PATCH 08/46] #2689 added test template and fixed class instancing issues. --- .../red_applications/c2/abstract_c2.py | 7 +++- .../red_applications/c2/c2_beacon.py | 16 ++++++-- .../red_applications/c2/c2_server.py | 40 ++++++++++++------- .../_red_applications/test_c2_suite.py | 12 +++++- 4 files changed, 54 insertions(+), 21 deletions(-) diff --git a/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py b/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py index 1cea972f..af5c37b9 100644 --- a/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py +++ b/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py @@ -35,7 +35,7 @@ class C2Command(Enum): # The terminal command should also be able to pass a session which can be used for remote connections. -class AbstractC2(Application): +class AbstractC2(Application, identifier="AbstractC2"): """ An abstract command and control (c2) application. @@ -97,6 +97,11 @@ class AbstractC2(Application): :rtype: Dict """ return super().describe_state() + + def __init__(self, **kwargs): + kwargs["port"] = Port.NONE + kwargs["protocol"] = IPProtocol.NONE + super().__init__(**kwargs) # Validate call ensures we are only handling Masquerade Packets. @validate_call 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 f8db5398..1d61e3b1 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 @@ -12,7 +12,7 @@ from enum import Enum from primaite.simulator.system.software import SoftwareHealthState from primaite.simulator.system.applications.application import ApplicationOperatingState -class C2Beacon(AbstractC2): +class C2Beacon(AbstractC2, identifier="C2Beacon"): """ C2 Beacon Application. @@ -86,8 +86,8 @@ class C2Beacon(AbstractC2): return rm def __init__(self, **kwargs): - self.name = "C2Beacon" - super.__init__(**kwargs) + kwargs["name"] = "C2Beacon" + super().__init__(**kwargs) def configure( self, @@ -268,4 +268,12 @@ class C2Beacon(AbstractC2): 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 + return True + + + # Defining this abstract method from Abstract C2 + def _handle_command_output(self, payload): + """C2 Beacons currently do not need to handle output commands coming from the C2 Servers.""" + self.sys_log.warning(f"{self.name}: C2 Beacon received an unexpected OUTPUT payload: {payload}") + pass + \ No newline at end of file diff --git a/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py b/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py index 8ab10d22..6cff1972 100644 --- a/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py +++ b/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py @@ -5,7 +5,7 @@ from primaite.simulator.core import RequestManager, RequestType from primaite.interface.request import RequestFormat, RequestResponse from typing import Dict,Optional -class C2Server(AbstractC2): +class C2Server(AbstractC2, identifier="C2 Server"): # TODO: # Implement the request manager and agent actions. # Implement the output handling methods. (These need to interface with the actions) @@ -16,18 +16,6 @@ class C2Server(AbstractC2): More information in user guide and docstring for SimComponent._init_request_manager. """ rm = super()._init_request_manager() - rm.add_request( - name="c2_ransomware_configure", - request_type=RequestType(func=lambda request, context: RequestResponse.from_bool(_configure_ransomware_action())), - ) - rm.add_request( - name="c2_ransomware_launch", - request_type=RequestType(func=lambda request, context: RequestResponse.from_bool(_launch_ransomware_action())), - ) - rm.add_request( - name="c2_terminal_command", - request_type=RequestType(func=lambda request, context: RequestResponse.from_bool(_remote_terminal_action())), - ) def _configure_ransomware_action(request: RequestFormat, context: Dict) -> RequestResponse: """Requests - Sends a RANSOMWARE_CONFIGURE C2Command to the C2 Beacon with the given parameters. @@ -71,6 +59,23 @@ class C2Server(AbstractC2): placeholder: dict = {} return self._send_command(given_command=C2Command.RANSOMWARE_LAUNCH, command_options=placeholder) + rm.add_request( + name="c2_ransomware_configure", + request_type=RequestType(func=_configure_ransomware_action), + ) + rm.add_request( + name="c2_ransomware_launch", + request_type=RequestType(func=_launch_ransomware_action), + ) + rm.add_request( + name="c2_terminal_command", + request_type=RequestType(func=_remote_terminal_action), + ) + return rm + + def __init__(self, **kwargs): + kwargs["name"] = "C2Server" + super().__init__(**kwargs) def _handle_command_output(self, payload: MasqueradePacket) -> RequestResponse: """ @@ -125,7 +130,7 @@ class C2Server(AbstractC2): :param given_command: The C2 command to be sent to the C2 Beacon. :type given_command: C2Command. - :param command_options: The relevant C2 Beacon parameters. + :param command_options: The relevant C2 Beacon parameters.F :type command_options: Dict :return: Returns the construct MasqueradePacket :rtype: MasqueradePacket @@ -139,4 +144,9 @@ class C2Server(AbstractC2): payload=command_options ) return constructed_packet - \ No newline at end of file + + # Defining this abstract method + def _handle_command_input(self, payload): + """C2 Servers currently do not receive input commands coming from the C2 Beacons.""" + self.sys_log.warning(f"{self.name}: C2 Server received an unexpected INPUT payload: {payload}") + pass diff --git a/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_c2_suite.py b/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_c2_suite.py index e5fee496..20da4140 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_c2_suite.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_c2_suite.py @@ -48,9 +48,19 @@ def basic_network() -> Network: node_a = Computer(hostname="node_a", ip_address="192.168.0.10", subnet_mask="255.255.255.0", start_up_duration=0) node_a.power_on() node_a.software_manager.get_open_ports() + node_b = Computer(hostname="node_b", ip_address="192.168.0.11", subnet_mask="255.255.255.0", start_up_duration=0) node_b.power_on() network.connect(node_a.network_interface[1], node_b.network_interface[1]) - return network \ No newline at end of file + return network + +def test_c2_suite_setup_receive(basic_network): + """Test that C2 Beacon can successfully establish connection with the c2 Server""" + network: Network = basic_network + computer_a: Computer = network.get_node_by_hostname("node_a") + c2_server: C2Server = computer_a.software_manager.software.get("C2Server") + + computer_b: Computer = network.get_node_by_hostname("node_b") + c2_beacon: C2Server = computer_a.software_manager.software.get("C2Beacon") From e554a2d2241523a2f2047d4c278261ddee17e837 Mon Sep 17 00:00:00 2001 From: "Archer.Bowen" Date: Thu, 1 Aug 2024 17:18:10 +0100 Subject: [PATCH 09/46] #2689 Remote connections now successfully establishing however current issues with keep alive inactivity causing the c2 beacon to close even when it does have connection to the c2 server. --- .../red_applications/c2/abstract_c2.py | 80 ++++++++++++------- .../red_applications/c2/c2_beacon.py | 45 +++++++---- .../red_applications/c2/c2_server.py | 25 +++++- .../_red_applications/test_c2_suite.py | 12 ++- 4 files changed, 111 insertions(+), 51 deletions(-) diff --git a/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py b/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py index af5c37b9..9c840616 100644 --- a/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py +++ b/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py @@ -10,6 +10,7 @@ from primaite.simulator.network.protocols.masquerade import C2Payload, Masquerad from primaite.simulator.network.transmission.network_layer import IPProtocol from primaite.simulator.network.transmission.transport_layer import Port from primaite.simulator.system.applications.application import Application +from primaite.simulator.system.core.session_manager import Session # TODO: # Complete C2 Server and C2 Beacon TODOs @@ -52,7 +53,7 @@ class AbstractC2(Application, identifier="AbstractC2"): """Indicates if the c2 server and c2 beacon are currently connected.""" c2_remote_connection: IPv4Address = None - """The IPv4 Address of the remote c2 connection. (Either the IP of the beacon or the server)""" + """The IPv4 Address of the remote c2 connection. (Either the IP of the beacon or the server).""" keep_alive_sent: bool = False """Indicates if a keep alive has been sent this timestep. Used to prevent packet storms.""" @@ -65,12 +66,18 @@ class AbstractC2(Application, identifier="AbstractC2"): # The c2 server parses the keep alive and sets these accordingly. # The c2 beacon will set this attributes upon installation and configuration - current_masquerade_protocol: Enum = IPProtocol.TCP + current_masquerade_protocol: IPProtocol = IPProtocol.TCP """The currently chosen protocol that the C2 traffic is masquerading as. Defaults as TCP.""" - current_masquerade_port: Enum = Port.HTTP + current_masquerade_port: Port = Port.HTTP """The currently chosen port that the C2 traffic is masquerading as. Defaults at HTTP.""" + current_c2_session: Session = None + """The currently active session that the C2 Traffic is using. Set after establishing connection.""" + + # TODO: Create a attribute call 'LISTENER' which indicates whenever the c2 application should listen for incoming connections or establish connections. + # This in order to simulate a blind shell (the current implementation is more akin to a reverse shell) + # TODO: Move this duplicate method from NMAP class into 'Application' to adhere to DRY principle. def _can_perform_network_action(self) -> bool: """ @@ -99,8 +106,8 @@ class AbstractC2(Application, identifier="AbstractC2"): return super().describe_state() def __init__(self, **kwargs): - kwargs["port"] = Port.NONE - kwargs["protocol"] = IPProtocol.NONE + kwargs["port"] = Port.HTTP # TODO: Update this post application/services requiring to listen to multiple ports + kwargs["protocol"] = IPProtocol.TCP # Update this as well super().__init__(**kwargs) # Validate call ensures we are only handling Masquerade Packets. @@ -147,7 +154,7 @@ class AbstractC2(Application, identifier="AbstractC2"): return False # Abstract method - # Used in C2 server to prase and receive the output of commands sent to the c2 beacon. + # Used in C2 server to parse and receive the output of commands sent to the c2 beacon. @abstractmethod def _handle_command_output(payload): """Abstract Method: Used in C2 server to prase and receive the output of commands sent to the c2 beacon.""" @@ -170,32 +177,35 @@ class AbstractC2(Application, identifier="AbstractC2"): :return: True if successfully handled, false otherwise. :rtype: Bool """ - self.sys_log.info(f"{self.name}: Keep Alive Received from {self.c2_remote_connection}") - # Using this guard clause to prevent packet storms and recognise that we've achieved a connection. - if self.keep_alive_sent: - self.sys_log.info(f"{self.name}: Connection successfully established with {self.c2_remote_connection}") - self.c2_connection_active = True # Sets the connection to active - self.keep_alive_inactivity = 0 # Sets the keep alive inactivity to zero + self.sys_log.info(f"{self.name}: Keep Alive Received from {self.c2_remote_connection}.") + + self.c2_connection_active = True # Sets the connection to active + self.keep_alive_inactivity = 0 # Sets the keep alive inactivity to zero + self.current_c2_session = self.software_manager.session_manager.sessions_by_uuid[session_id] + + # Using this guard clause to prevent packet storms and recognise that we've achieved a connection. + # This guard clause triggers on the c2 suite that establishes connection. + if self.keep_alive_sent == True: # Return early without sending another keep alive and then setting keep alive_sent false for next timestep. self.keep_alive_sent = False return True # If we've reached this part of the method then we've received a keep alive but haven't sent a reply. # Therefore we also need to configure the masquerade attributes based off the keep alive sent. - if not self._resolve_keep_alive(self, payload): + if self._resolve_keep_alive(payload, session_id) == False: + self.sys_log.warning(f"{self.name}: Keep Alive Could not be resolved correctly. Refusing Keep Alive.") return False # If this method returns true then we have sent successfully sent a keep alive. - if self._send_keep_alive(self, session_id): - self.keep_alive_sent = True # Setting the guard clause to true (prevents packet storms.) - return True - # Return false if we're unable to send handle the keep alive correctly. - else: - return False + self.sys_log.info(f"{self.name}: Connection successfully established with {self.c2_remote_connection}.") + + return self._send_keep_alive(session_id) - def receive(self, payload: MasqueradePacket, session_id: Optional[str] = None) -> bool: + + # from_network_interface=from_network_interface + def receive(self, payload: MasqueradePacket, session_id: Optional[str] = None, **kwargs) -> bool: """Receives masquerade packets. Used by both c2 server and c2 client. :param payload: The Masquerade Packet to be received. @@ -220,20 +230,20 @@ class AbstractC2(Application, identifier="AbstractC2"): masquerade_protocol=self.current_masquerade_protocol, masquerade_port=self.current_masquerade_port, payload_type=C2Payload.KEEP_ALIVE, + command=None ) - - # C2 Server will need to c2_remote_connection after it receives it's first keep alive. + # We need to set this guard clause to true before sending the keep alive (prevents packet storms.) + self.keep_alive_sent = True + # C2 Server will need to configure c2_remote_connection after it receives it's first keep alive. if self.send( - self, payload=keep_alive_packet, dest_ip_address=self.c2_remote_connection, - port=self.current_masquerade_port, - protocol=self.current_masquerade_protocol, + dest_port=self.current_masquerade_port, + ip_protocol=self.current_masquerade_protocol, session_id=session_id, ): self.sys_log.info(f"{self.name}: Keep Alive sent to {self.c2_remote_connection}") self.sys_log.debug(f"{self.name}: on {self.current_masquerade_port} via {self.current_masquerade_protocol}") - self.receive(payload=keep_alive_packet) return True else: self.sys_log.warning( @@ -242,7 +252,7 @@ class AbstractC2(Application, identifier="AbstractC2"): return False - def _resolve_keep_alive(self, payload: MasqueradePacket) -> bool: + def _resolve_keep_alive(self, payload: MasqueradePacket, session_id: Optional[str]) -> bool: """ Parses the Masquerade Port/Protocol within the received Keep Alive packet. @@ -257,8 +267,8 @@ class AbstractC2(Application, identifier="AbstractC2"): :rtype: bool """ # Validating that they are valid Enums. - if payload.masquerade_port or payload.masquerade_protocol != Enum: - self.sys_log.warning(f"{self.name}: Received invalid Masquerade type. Port: {type(payload.masquerade_port)} Protocol: {type(payload.masquerade_protocol)}") + if not isinstance(payload.masquerade_port, Port) or not isinstance(payload.masquerade_protocol, IPProtocol): + self.sys_log.warning(f"{self.name}: Received invalid Masquerade type. Port: {type(payload.masquerade_port)} Protocol: {type(payload.masquerade_protocol)}.") return False # TODO: Validation on Ports (E.g only allow HTTP, FTP etc) # Potentially compare to IPProtocol & Port children (Same way that abstract TAP does it with kill chains) @@ -266,4 +276,14 @@ class AbstractC2(Application, identifier="AbstractC2"): # Setting the Ports self.current_masquerade_port = payload.masquerade_port self.current_masquerade_protocol = payload.masquerade_protocol - return True \ No newline at end of file + + # This statement is intended to catch on the C2 Application that is listening for connection. (C2 Beacon) + if self.c2_remote_connection == None: + self.sys_log.debug(f"{self.name}: Attempting to configure remote C2 connection based off received output.") + self.c2_remote_connection = self.current_c2_session.with_ip_address + + self.c2_connection_active = True # Sets the connection to active + self.keep_alive_inactivity = 0 # Sets the keep alive inactivity to zeroW + + return True + \ No newline at end of file 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 1d61e3b1..14c7af02 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 @@ -1,6 +1,7 @@ # © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from primaite.simulator.system.applications.red_applications.c2.abstract_c2 import AbstractC2, C2Command #from primaite.simulator.system.services.terminal.terminal import Terminal +from prettytable import MARKDOWN, PrettyTable from primaite.simulator.core import RequestManager, RequestType from primaite.interface.request import RequestFormat, RequestResponse from primaite.simulator.network.protocols.masquerade import C2Payload, MasqueradePacket @@ -12,7 +13,7 @@ from enum import Enum from primaite.simulator.system.software import SoftwareHealthState from primaite.simulator.system.applications.application import ApplicationOperatingState -class C2Beacon(AbstractC2, identifier="C2Beacon"): +class C2Beacon(AbstractC2, identifier="C2 Beacon"): """ C2 Beacon Application. @@ -128,11 +129,10 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): def establish(self) -> bool: """Establishes connection to the C2 server via a send alive. Must be called after the C2 Beacon is configured.""" self.run() - self._send_keep_alive() self.num_executions += 1 - + return self._send_keep_alive(session_id=None) - def _handle_command_input(self, payload: MasqueradePacket) -> bool: + def _handle_command_input(self, payload: MasqueradePacket, session_id: Optional[str]) -> bool: """ Handles the parsing of C2 Commands from C2 Traffic (Masquerade Packets) as well as then calling the relevant method dependant on the C2 Command. @@ -149,22 +149,22 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): if command == C2Command.RANSOMWARE_CONFIGURE: self.sys_log.info(f"{self.name}: Received a ransomware configuration C2 command.") - return self._return_command_output(self._command_ransomware_config(payload)) + return self._return_command_output(command_output=self._command_ransomware_config(payload), session_id=session_id) elif command == C2Command.RANSOMWARE_LAUNCH: self.sys_log.info(f"{self.name}: Received a ransomware launch C2 command.") - return self._return_command_output(self._command_ransomware_launch(payload)) + return self._return_command_output(command_output=self._command_ransomware_launch(payload), session_id=session_id) elif command == C2Command.TERMINAL: self.sys_log.info(f"{self.name}: Received a terminal C2 command.") - return self._return_command_output(self._command_terminal(payload)) + return self._return_command_output(command_output=self._command_terminal(payload), session_id=session_id) else: 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 _return_command_output(self, command_output: RequestResponse) -> bool: + def _return_command_output(self, command_output: RequestResponse, session_id) -> bool: """Responsible for responding to the C2 Server with the output of the given command.""" output_packet = MasqueradePacket( masquerade_protocol=self.current_masquerade_protocol, @@ -173,11 +173,10 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): 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, + dest_port=self.current_masquerade_port, + ip_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}") @@ -256,15 +255,16 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): if not self._check_c2_connection(timestep): self.sys_log.error(f"{self.name}: Connection Severed - Application Closing.") self.clear_connections() + # TODO: Shouldn't this close() method also set the health state to 'UNUSED'? self.close() return def _check_c2_connection(self, timestep) -> bool: """Checks the C2 Server connection. If a connection cannot be confirmed then this method will return false otherwise true.""" - 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 == self.keep_alive_frequency: + self.sys_log.info(f"{self.name}: Attempting to Send Keep Alive to {self.c2_remote_connection} at timestep {timestep}.") + self._send_keep_alive(session_id=self.current_c2_session.uuid) 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 @@ -274,6 +274,19 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): # Defining this abstract method from Abstract C2 def _handle_command_output(self, payload): """C2 Beacons currently do not need to handle output commands coming from the C2 Servers.""" - self.sys_log.warning(f"{self.name}: C2 Beacon received an unexpected OUTPUT payload: {payload}") + self.sys_log.warning(f"{self.name}: C2 Beacon received an unexpected OUTPUT payload: {payload}.") pass - \ No newline at end of file + + def show(self, markdown: bool = False): + """ + Prints a table of the current C2 attributes on a C2 Beacon. + + :param markdown: If True, outputs the table in markdown format. Default is False. + """ + table = PrettyTable(["C2 Connection Active", "C2 Remote Connection", "Keep Alive Inactivity", "Keep Alive Frequency"]) + if markdown: + table.set_style(MARKDOWN) + table.align = "l" + table.title = f"{self.name} Running Status" + table.add_row([self.c2_connection_active, self.c2_remote_connection, self.keep_alive_inactivity, self.keep_alive_frequency]) + print(table) diff --git a/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py b/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py index 6cff1972..8fe8c00c 100644 --- a/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py +++ b/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py @@ -3,6 +3,7 @@ from primaite.simulator.system.applications.red_applications.c2.abstract_c2 impo from primaite.simulator.network.protocols.masquerade import C2Payload, MasqueradePacket from primaite.simulator.core import RequestManager, RequestType from primaite.interface.request import RequestFormat, RequestResponse +from prettytable import MARKDOWN, PrettyTable from typing import Dict,Optional class C2Server(AbstractC2, identifier="C2 Server"): @@ -94,6 +95,7 @@ class C2Server(AbstractC2, identifier="C2 Server"): return RequestResponse(status="failure", data={"Received unexpected C2 Response."}) return command_output + def _send_command(self, given_command: C2Command, command_options: Dict) -> RequestResponse: """ Sends a command to the C2 Beacon. @@ -115,8 +117,12 @@ class C2Server(AbstractC2, identifier="C2 Server"): command_packet = self._craft_packet(given_command=given_command, command_options=command_options) # Need to investigate if this is correct. - if self.send(self, payload=command_packet,dest_ip_address=self.c2_remote_connection, - port=self.current_masquerade_port, protocol=self.current_masquerade_protocol,session_id=None): + if self.send(payload=command_packet, + dest_ip_address=self.c2_remote_connection, + src_port=self.current_masquerade_port, + dst_port=self.current_masquerade_port, + ip_protocol=self.current_masquerade_protocol, + session_id=None): self.sys_log.info(f"{self.name}: Successfully sent {given_command}.") self.sys_log.info(f"{self.name}: Awaiting command response {given_command}.") return self._handle_command_output(command_packet) @@ -150,3 +156,18 @@ class C2Server(AbstractC2, identifier="C2 Server"): """C2 Servers currently do not receive input commands coming from the C2 Beacons.""" self.sys_log.warning(f"{self.name}: C2 Server received an unexpected INPUT payload: {payload}") pass + + + def show(self, markdown: bool = False): + """ + Prints a table of the current C2 attributes on a C2 Server. + + :param markdown: If True, outputs the table in markdown format. Default is False. + """ + table = PrettyTable(["C2 Connection Active", "C2 Remote Connection", "Keep Alive Inactivity"]) + if markdown: + table.set_style(MARKDOWN) + table.align = "l" + table.title = f"{self.name} Running Status" + table.add_row([self.c2_connection_active, self.c2_remote_connection, self.keep_alive_inactivity]) + print(table) diff --git a/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_c2_suite.py b/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_c2_suite.py index 20da4140..3014cd19 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_c2_suite.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_c2_suite.py @@ -48,19 +48,25 @@ def basic_network() -> Network: node_a = Computer(hostname="node_a", ip_address="192.168.0.10", subnet_mask="255.255.255.0", start_up_duration=0) node_a.power_on() node_a.software_manager.get_open_ports() - + node_a.software_manager.install(software_class=C2Server) node_b = Computer(hostname="node_b", ip_address="192.168.0.11", subnet_mask="255.255.255.0", start_up_duration=0) + node_b.software_manager.install(software_class=C2Beacon) node_b.power_on() network.connect(node_a.network_interface[1], node_b.network_interface[1]) return network def test_c2_suite_setup_receive(basic_network): - """Test that C2 Beacon can successfully establish connection with the c2 Server""" + """Test that C2 Beacon can successfully establish connection with the c2 Server.""" network: Network = basic_network computer_a: Computer = network.get_node_by_hostname("node_a") c2_server: C2Server = computer_a.software_manager.software.get("C2Server") computer_b: Computer = network.get_node_by_hostname("node_b") - c2_beacon: C2Server = computer_a.software_manager.software.get("C2Beacon") + c2_beacon: C2Beacon = computer_b.software_manager.software.get("C2Beacon") + + c2_beacon.configure(c2_server_ip_address="192.168.0.10") + c2_beacon.establish() + + c2_beacon.sys_log.show() \ No newline at end of file From 2339dabac13b348998479e6f512ba0e449fa97ee Mon Sep 17 00:00:00 2001 From: "Archer.Bowen" Date: Fri, 2 Aug 2024 13:25:08 +0100 Subject: [PATCH 10/46] #2689 Overhauled .receive method. Keep Alive and initial implementation of commands working. (also Updated docustrings + pre-commit) --- src/primaite/game/game.py | 2 - .../simulator/network/protocols/masquerade.py | 17 +- .../red_applications/c2/abstract_c2.py | 109 +++---- .../red_applications/c2/c2_beacon.py | 302 +++++++++++++----- .../red_applications/c2/c2_server.py | 152 ++++++--- .../_red_applications/test_c2_suite.py | 12 +- 6 files changed, 382 insertions(+), 212 deletions(-) diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index 8cddbcda..5ef8c14c 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -36,8 +36,6 @@ from primaite.simulator.system.applications.red_applications.data_manipulation_b ) from primaite.simulator.system.applications.red_applications.dos_bot import DoSBot # noqa: F401 from primaite.simulator.system.applications.red_applications.ransomware_script import RansomwareScript # noqa: F401 -from primaite.simulator.system.applications.red_applications.c2.c2_beacon import C2Beacon -from primaite.simulator.system.applications.red_applications.c2.c2_server import C2Server from primaite.simulator.system.applications.web_browser import WebBrowser # noqa: F401 from primaite.simulator.system.services.database.database_service import DatabaseService from primaite.simulator.system.services.dns.dns_client import DNSClient diff --git a/src/primaite/simulator/network/protocols/masquerade.py b/src/primaite/simulator/network/protocols/masquerade.py index 93554f57..7ef17fc0 100644 --- a/src/primaite/simulator/network/protocols/masquerade.py +++ b/src/primaite/simulator/network/protocols/masquerade.py @@ -5,19 +5,6 @@ from typing import Optional from primaite.simulator.network.protocols.packet import DataPacket -class C2Payload(Enum): - """Represents the different types of command and control payloads.""" - - KEEP_ALIVE = "keep_alive" - """C2 Keep Alive payload. Used by the C2 beacon and C2 Server to confirm their connection.""" - - INPUT = "input_command" - """C2 Input Command payload. Used by the C2 Server to send a command to the c2 beacon.""" - - OUTPUT = "output_command" - """C2 Output Command. Used by the C2 Beacon to send the results of a Input command to the c2 server.""" - - class MasqueradePacket(DataPacket): """Represents an generic malicious packet that is masquerading as another protocol.""" @@ -25,6 +12,6 @@ class MasqueradePacket(DataPacket): masquerade_port: Enum # The 'Masquerade' port that is currently in use - payload_type: C2Payload # The type of C2 traffic (e.g keep alive, command or command out) + payload_type: Enum # The type of C2 traffic (e.g keep alive, command or command out) - command: Optional[str] # Used to pass the actual C2 Command in C2 INPUT + command: Optional[Enum] = None # Used to pass the actual C2 Command in C2 INPUT diff --git a/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py b/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py index 9c840616..af701e8c 100644 --- a/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py +++ b/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py @@ -6,7 +6,7 @@ from typing import Dict, Optional from pydantic import validate_call -from primaite.simulator.network.protocols.masquerade import C2Payload, MasqueradePacket +from primaite.simulator.network.protocols.masquerade import MasqueradePacket from primaite.simulator.network.transmission.network_layer import IPProtocol from primaite.simulator.network.transmission.transport_layer import Port from primaite.simulator.system.applications.application import Application @@ -20,9 +20,7 @@ from primaite.simulator.system.core.session_manager import Session class C2Command(Enum): - """ - Enumerations representing the different commands the C2 suite currently supports. - """ + """Enumerations representing the different commands the C2 suite currently supports.""" RANSOMWARE_CONFIGURE = "Ransomware Configure" "Instructs the c2 beacon to configure the ransomware with the provided options." @@ -36,12 +34,25 @@ class C2Command(Enum): # The terminal command should also be able to pass a session which can be used for remote connections. +class C2Payload(Enum): + """Represents the different types of command and control payloads.""" + + KEEP_ALIVE = "keep_alive" + """C2 Keep Alive payload. Used by the C2 beacon and C2 Server to confirm their connection.""" + + INPUT = "input_command" + """C2 Input Command payload. Used by the C2 Server to send a command to the c2 beacon.""" + + OUTPUT = "output_command" + """C2 Output Command. Used by the C2 Beacon to send the results of a Input command to the c2 server.""" + + class AbstractC2(Application, identifier="AbstractC2"): """ An abstract command and control (c2) application. Extends the application class to provide base functionality for c2 suite applications - such as c2 beacons and c2 servers. + such as c2 beacons and c2 servers. Provides the base methods for handling ``Keep Alive`` connections, configuring masquerade ports and protocols as well as providing the abstract methods for sending, receiving and parsing commands. @@ -55,13 +66,6 @@ class AbstractC2(Application, identifier="AbstractC2"): c2_remote_connection: IPv4Address = None """The IPv4 Address of the remote c2 connection. (Either the IP of the beacon or the server).""" - keep_alive_sent: bool = False - """Indicates if a keep alive has been sent this timestep. Used to prevent packet storms.""" - - # We should set the application to NOT_RUNNING if the inactivity count reaches a certain thresh hold. - keep_alive_inactivity: int = 0 - """Indicates how many timesteps since the last time the c2 application received a keep alive.""" - # These two attributes are set differently in the c2 server and c2 beacon. # The c2 server parses the keep alive and sets these accordingly. # The c2 beacon will set this attributes upon installation and configuration @@ -75,9 +79,6 @@ class AbstractC2(Application, identifier="AbstractC2"): current_c2_session: Session = None """The currently active session that the C2 Traffic is using. Set after establishing connection.""" - # TODO: Create a attribute call 'LISTENER' which indicates whenever the c2 application should listen for incoming connections or establish connections. - # This in order to simulate a blind shell (the current implementation is more akin to a reverse shell) - # TODO: Move this duplicate method from NMAP class into 'Application' to adhere to DRY principle. def _can_perform_network_action(self) -> bool: """ @@ -104,10 +105,10 @@ class AbstractC2(Application, identifier="AbstractC2"): :rtype: Dict """ return super().describe_state() - + def __init__(self, **kwargs): - kwargs["port"] = Port.HTTP # TODO: Update this post application/services requiring to listen to multiple ports - kwargs["protocol"] = IPProtocol.TCP # Update this as well + kwargs["port"] = Port.HTTP # TODO: Update this post application/services requiring to listen to multiple ports + kwargs["protocol"] = IPProtocol.TCP # Update this as well super().__init__(**kwargs) # Validate call ensures we are only handling Masquerade Packets. @@ -145,7 +146,7 @@ class AbstractC2(Application, identifier="AbstractC2"): elif payload.payload_type == C2Payload.OUTPUT: self.sys_log.info(f"{self.name} received an OUTPUT COMMAND payload.") - return self._handle_command_input(payload, session_id) + return self._handle_command_output(payload) else: self.sys_log.warning( @@ -168,41 +169,7 @@ class AbstractC2(Application, identifier="AbstractC2"): pass def _handle_keep_alive(self, payload: MasqueradePacket, session_id: Optional[str]) -> bool: - """ - Handles receiving and sending keep alive payloads. This method is only called if we receive a keep alive. - - Returns False if a keep alive was unable to be sent. - Returns True if a keep alive was successfully sent or already has been sent this timestep. - - :return: True if successfully handled, false otherwise. - :rtype: Bool - """ - self.sys_log.info(f"{self.name}: Keep Alive Received from {self.c2_remote_connection}.") - - self.c2_connection_active = True # Sets the connection to active - self.keep_alive_inactivity = 0 # Sets the keep alive inactivity to zero - self.current_c2_session = self.software_manager.session_manager.sessions_by_uuid[session_id] - - - # Using this guard clause to prevent packet storms and recognise that we've achieved a connection. - # This guard clause triggers on the c2 suite that establishes connection. - if self.keep_alive_sent == True: - # Return early without sending another keep alive and then setting keep alive_sent false for next timestep. - self.keep_alive_sent = False - return True - - # If we've reached this part of the method then we've received a keep alive but haven't sent a reply. - # Therefore we also need to configure the masquerade attributes based off the keep alive sent. - if self._resolve_keep_alive(payload, session_id) == False: - self.sys_log.warning(f"{self.name}: Keep Alive Could not be resolved correctly. Refusing Keep Alive.") - return False - - # If this method returns true then we have sent successfully sent a keep alive. - - self.sys_log.info(f"{self.name}: Connection successfully established with {self.c2_remote_connection}.") - - return self._send_keep_alive(session_id) - + """Abstract Method: The C2 Server and the C2 Beacon handle the KEEP ALIVEs differently.""" # from_network_interface=from_network_interface def receive(self, payload: MasqueradePacket, session_id: Optional[str] = None, **kwargs) -> bool: @@ -217,23 +184,23 @@ class AbstractC2(Application, identifier="AbstractC2"): """Sends a C2 keep alive payload to the self.remote_connection IPv4 Address.""" # Checking that the c2 application is capable of performing both actions and has an enabled NIC # (Using NOT to improve code readability) - if self.c2_remote_connection == None: - self.sys_log.error(f"{self.name}: Unable to Establish connection as the C2 Server's IP Address has not been given.") - + if self.c2_remote_connection is None: + self.sys_log.error( + f"{self.name}: Unable to Establish connection as the C2 Server's IP Address has not been given." + ) + if not self._can_perform_network_action(): self.sys_log.warning(f"{self.name}: Unable to perform network actions.") return False - # We also Pass masquerade protocol/port so that the c2 server can reply on the correct protocol/port. + # We also Pass masquerade proto`col/port so that the c2 server can reply on the correct protocol/port. # (This also lays the foundations for switching masquerade port/protocols mid episode.) keep_alive_packet = MasqueradePacket( masquerade_protocol=self.current_masquerade_protocol, masquerade_port=self.current_masquerade_port, payload_type=C2Payload.KEEP_ALIVE, - command=None + command=None, ) - # We need to set this guard clause to true before sending the keep alive (prevents packet storms.) - self.keep_alive_sent = True # C2 Server will need to configure c2_remote_connection after it receives it's first keep alive. if self.send( payload=keep_alive_packet, @@ -242,16 +209,16 @@ class AbstractC2(Application, identifier="AbstractC2"): ip_protocol=self.current_masquerade_protocol, session_id=session_id, ): + self.keep_alive_sent = True self.sys_log.info(f"{self.name}: Keep Alive 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 Keep Alive. The node may be unable to access the network." + f"{self.name}: failed to send a Keep Alive. The node may be unable to access the ``network." ) return False - def _resolve_keep_alive(self, payload: MasqueradePacket, session_id: Optional[str]) -> bool: """ Parses the Masquerade Port/Protocol within the received Keep Alive packet. @@ -260,7 +227,7 @@ class AbstractC2(Application, identifier="AbstractC2"): Returns True on successfully extracting and configuring the masquerade port/protocols. Returns False otherwise. - + :param payload: The Keep Alive payload received. :type payload: MasqueradePacket :return: True on successful configuration, false otherwise. @@ -268,22 +235,24 @@ class AbstractC2(Application, identifier="AbstractC2"): """ # Validating that they are valid Enums. if not isinstance(payload.masquerade_port, Port) or not isinstance(payload.masquerade_protocol, IPProtocol): - self.sys_log.warning(f"{self.name}: Received invalid Masquerade type. Port: {type(payload.masquerade_port)} Protocol: {type(payload.masquerade_protocol)}.") + self.sys_log.warning( + f"{self.name}: Received invalid Masquerade Values within Keep Alive." + f"Port: {payload.masquerade_port} Protocol: {payload.masquerade_protocol}." + ) return False # TODO: Validation on Ports (E.g only allow HTTP, FTP etc) # Potentially compare to IPProtocol & Port children (Same way that abstract TAP does it with kill chains) - + # Setting the Ports self.current_masquerade_port = payload.masquerade_port self.current_masquerade_protocol = payload.masquerade_protocol # This statement is intended to catch on the C2 Application that is listening for connection. (C2 Beacon) - if self.c2_remote_connection == None: + if self.c2_remote_connection is None: self.sys_log.debug(f"{self.name}: Attempting to configure remote C2 connection based off received output.") self.c2_remote_connection = self.current_c2_session.with_ip_address - + self.c2_connection_active = True # Sets the connection to active self.keep_alive_inactivity = 0 # Sets the keep alive inactivity to zeroW - + return True - \ No newline at end of file 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 14c7af02..b00b7c57 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 @@ -1,34 +1,44 @@ # © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK -from primaite.simulator.system.applications.red_applications.c2.abstract_c2 import AbstractC2, C2Command -#from primaite.simulator.system.services.terminal.terminal import Terminal -from prettytable import MARKDOWN, PrettyTable -from primaite.simulator.core import RequestManager, RequestType -from primaite.interface.request import RequestFormat, RequestResponse -from primaite.simulator.network.protocols.masquerade import C2Payload, MasqueradePacket -from primaite.simulator.network.transmission.network_layer import IPProtocol -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 ipaddress import IPv4Address +from typing import Dict, Optional + +# from primaite.simulator.system.services.terminal.terminal import Terminal +from prettytable import MARKDOWN, PrettyTable + +from primaite.interface.request import RequestFormat, RequestResponse +from primaite.simulator.core import RequestManager, RequestType +from primaite.simulator.network.protocols.masquerade import MasqueradePacket +from primaite.simulator.network.transmission.network_layer import IPProtocol +from primaite.simulator.network.transmission.transport_layer import Port from primaite.simulator.system.applications.application import ApplicationOperatingState +from primaite.simulator.system.applications.red_applications.c2.abstract_c2 import AbstractC2, C2Command, C2Payload +from primaite.simulator.system.software import SoftwareHealthState + class C2Beacon(AbstractC2, identifier="C2 Beacon"): """ C2 Beacon Application. - Represents a generic C2 beacon which can be used in conjunction with the C2 Server - to simulate malicious communications within primAITE. + Represents a vendor generic C2 beacon is used in conjunction with the C2 Server + to simulate malicious communications and infrastructure within primAITE. + + Must be configured with the C2 Server's IP Address upon installation. - Must be configured with the C2 Server's Ip Address upon installation. - Extends the Abstract C2 application to include the following: 1. Receiving commands from the C2 Server (Command input) 2. Leveraging the terminal application to execute requests (dependant on the command given) 3. Sending the RequestResponse back to the C2 Server (Command output) """ - + + keep_alive_attempted: bool = False + """Indicates if a keep alive has been attempted to be sent this timestep. Used to prevent packet storms.""" + + # We should set the application to NOT_RUNNING if the inactivity count reaches a certain thresh hold. + keep_alive_inactivity: int = 0 + """Indicates how many timesteps since the last time the c2 application received a keep alive.""" + keep_alive_frequency: int = 5 "The frequency at which ``Keep Alive`` packets are sent to the C2 Server from the C2 Beacon." @@ -38,8 +48,8 @@ class C2Beacon(AbstractC2, identifier="C2 Beacon"): # Implement a command output method that sends the RequestResponse to the C2 server. # Uncomment the terminal Import and the terminal property after terminal PR - #@property - #def _host_terminal(self) -> Terminal: + # @property + # 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: @@ -69,27 +79,33 @@ class C2Beacon(AbstractC2, identifier="C2 Beacon"): :return: RequestResponse object with a success code reflecting whether the configuration could be applied. :rtype: RequestResponse """ - server_ip = request[-1].get("c2_server_ip_address") - if server_ip == None: + c2_remote_ip = request[-1].get("c2_server_ip_address") + if c2_remote_ip is None: self.sys_log.error(f"{self.name}: Did not receive C2 Server IP in configuration parameters.") - RequestResponse(status="failure", data={"No C2 Server IP given to C2 beacon. Unable to configure C2 Beacon"}) + RequestResponse( + status="failure", data={"No C2 Server IP given to C2 beacon. Unable to configure C2 Beacon"} + ) c2_remote_ip = IPv4Address(c2_remote_ip) frequency = request[-1].get("keep_alive_frequency") - protocol= request[-1].get("masquerade_protocol") + protocol = request[-1].get("masquerade_protocol") port = request[-1].get("masquerade_port") - return RequestResponse.from_bool(self.configure(c2_server_ip_address=server_ip, - keep_alive_frequency=frequency, - masquerade_protocol=protocol, - masquerade_port=port)) + return RequestResponse.from_bool( + self.configure( + c2_server_ip_address=c2_remote_ip, + keep_alive_frequency=frequency, + masquerade_protocol=protocol, + masquerade_port=port, + ) + ) rm.add_request("configure", request_type=RequestType(func=_configure)) return rm - + def __init__(self, **kwargs): kwargs["name"] = "C2Beacon" super().__init__(**kwargs) - + def configure( self, c2_server_ip_address: IPv4Address = None, @@ -100,6 +116,7 @@ class C2Beacon(AbstractC2, identifier="C2 Beacon"): """ Configures the C2 beacon to communicate with the C2 server with following additional parameters. + # TODO: Expand docustring. :param c2_server_ip_address: The IP Address of the C2 Server. Used to establish connection. :type c2_server_ip_address: IPv4Address @@ -117,43 +134,70 @@ class C2Beacon(AbstractC2, identifier="C2 Beacon"): self.sys_log.info( f"{self.name}: Configured {self.name} with remote C2 server connection: {c2_server_ip_address=}." ) - self.sys_log.debug(f"{self.name}: configured with the following settings:" - f"Remote C2 Server: {c2_server_ip_address}" - f"Keep Alive Frequency {keep_alive_frequency}" - f"Masquerade Protocol: {masquerade_protocol}" - f"Masquerade Port: {masquerade_port}") + self.sys_log.debug( + f"{self.name}: configured with the following settings:" + f"Remote C2 Server: {c2_server_ip_address}" + f"Keep Alive Frequency {keep_alive_frequency}" + f"Masquerade Protocol: {masquerade_protocol}" + f"Masquerade Port: {masquerade_port}" + ) 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.""" + """Establishes connection to the C2 server via a send alive. The C2 Beacon must already be configured.""" + if self.c2_remote_connection is None: + self.sys_log.info(f"{self.name}: Failed to establish connection. C2 Beacon has not been configured.") + return False self.run() self.num_executions += 1 return self._send_keep_alive(session_id=None) def _handle_command_input(self, payload: MasqueradePacket, session_id: Optional[str]) -> bool: """ - 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 + Handles the parsing of C2 Commands from C2 Traffic (Masquerade Packets). + + Dependant the C2 Command contained within the payload. + The following methods are called and returned. + + C2 Command | Internal Method + ---------------------|------------------------ + RANSOMWARE_CONFIGURE | self._command_ransomware_config() + RANSOMWARE_LAUNCH | self._command_ransomware_launch() + Terminal | self._command_terminal() + + Please see each method individually for further information regarding + the implementation of these commands. + + :param payload: The INPUT C2 Payload :type payload: MasqueradePacket :return: The Request Response provided by the terminal execute method. :rtype Request Response: """ - command = payload.payload_type - if command != C2Payload: + # TODO: Probably could refactor this to be a more clean. + # The elif's are a bit ugly when they are all calling the same method. + command = payload.command + if not isinstance(command, C2Command): self.sys_log.warning(f"{self.name}: Received unexpected C2 command. Unable to resolve command") - return self._return_command_output(RequestResponse(status="failure", data={"Received unexpected C2Command. Unable to resolve command."})) + return self._return_command_output( + command_output=RequestResponse( + status="failure", + data={"Reason": "C2 Beacon received unexpected C2Command. Unable to resolve command."}, + ), + session_id=session_id, + ) if command == C2Command.RANSOMWARE_CONFIGURE: self.sys_log.info(f"{self.name}: Received a ransomware configuration C2 command.") - return self._return_command_output(command_output=self._command_ransomware_config(payload), session_id=session_id) + return self._return_command_output( + command_output=self._command_ransomware_config(payload), session_id=session_id + ) elif command == C2Command.RANSOMWARE_LAUNCH: self.sys_log.info(f"{self.name}: Received a ransomware launch C2 command.") - return self._return_command_output(command_output=self._command_ransomware_launch(payload), session_id=session_id) + return self._return_command_output( + command_output=self._command_ransomware_launch(payload), session_id=session_id + ) elif command == C2Command.TERMINAL: self.sys_log.info(f"{self.name}: Received a terminal C2 command.") @@ -161,22 +205,30 @@ class C2Beacon(AbstractC2, identifier="C2 Beacon"): else: 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."})) + return self._return_command_output( + RequestResponse(status="failure", data={"Reason": "Unexpected Behaviour. Unable to resolve command."}) + ) + def _return_command_output(self, command_output: RequestResponse, session_id: Optional[str] = None) -> bool: + """Responsible for responding to the C2 Server with the output of the given command. - def _return_command_output(self, command_output: RequestResponse, session_id) -> bool: - """Responsible for responding to the C2 Server with the output of the given command.""" + :param command_output: The RequestResponse returned by the terminal application's execute method. + :type command_output: Request Response + :param session_id: The current session established with the C2 Server. + :type session_id: Str + """ output_packet = MasqueradePacket( masquerade_protocol=self.current_masquerade_protocol, masquerade_port=self.current_masquerade_port, payload_type=C2Payload.OUTPUT, - payload=command_output + payload=command_output, ) if self.send( payload=output_packet, dest_ip_address=self.c2_remote_connection, dest_port=self.current_masquerade_port, ip_protocol=self.current_masquerade_protocol, + session_id=session_id, ): 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}") @@ -189,68 +241,121 @@ class C2Beacon(AbstractC2, identifier="C2 Beacon"): def _command_ransomware_config(self, payload: MasqueradePacket) -> RequestResponse: """ - C2 Command: Ransomware Configuration + 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. + :payload MasqueradePacket: The incoming INPUT command. + :type Masquerade Packet: MasqueradePacket. :return: Returns the Request Response returned by the Terminal execute method. :rtype: Request Response """ - pass - #return self._host_terminal.execute(command) + # TODO: replace and use terminal + return RequestResponse(status="success", data={"Reason": "Placeholder."}) def _command_ransomware_launch(self, payload: MasqueradePacket) -> RequestResponse: """ - C2 Command: Ransomware Execute + C2 Command: Ransomware Launch. Creates a request that executes the ransomware script. This request is then sent to the terminal service in order to be executed. + + :payload MasqueradePacket: The incoming INPUT command. + :type Masquerade Packet: MasqueradePacket. :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) + # TODO: replace and use terminal + return RequestResponse(status="success", data={"Reason": "Placeholder."}) def _command_terminal(self, payload: MasqueradePacket) -> RequestResponse: """ - C2 Command: Ransomware Execute + C2 Command: Terminal. - Creates a request that executes the ransomware script. + Creates a request that executes a terminal command. This request is then sent to the terminal service in order to be executed. + :payload MasqueradePacket: The incoming INPUT command. + :type Masquerade Packet: MasqueradePacket. :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) + # TODO: uncomment and replace (uses terminal) + return RequestResponse(status="success", data={"Reason": "Placeholder."}) + # return self._host_terminal.execute(command) + def _handle_keep_alive(self, payload: MasqueradePacket, session_id: Optional[str]) -> bool: + """ + Handles receiving and sending keep alive payloads. This method is only called if we receive a keep alive. + + In the C2 Beacon implementation of this method the c2 connection active boolean + is set to true and the keep alive inactivity is reset only after sending a keep alive + as wel as receiving a response back from the C2 Server. + + This is because the C2 Server is the listener and thus will only ever receive packets from + the C2 Beacon rather than the other way around. (The C2 Beacon is akin to a reverse shell) + + Therefore, we need a response back from the listener (C2 Server) + before the C2 beacon is able to confirm it's connection. + + Returns False if a keep alive was unable to be sent. + Returns True if a keep alive was successfully sent or already has been sent this timestep. + + :return: True if successfully handled, false otherwise. + :rtype: Bool + """ + self.sys_log.info(f"{self.name}: Keep Alive Received from {self.c2_remote_connection}.") + + # Using this guard clause to prevent packet storms and recognise that we've achieved a connection. + # This guard clause triggers on the c2 suite that establishes connection. + if self.keep_alive_attempted is True: + self.c2_connection_active = True # Sets the connection to active + self.keep_alive_inactivity = 0 # Sets the keep alive inactivity to zero + self.current_c2_session = self.software_manager.session_manager.sessions_by_uuid[session_id] + + # We set keep alive_attempted here to show that we've achieved connection. + self.keep_alive_attempted = False + self.sys_log.warning(f"{self.name}: Connection successfully Established with C2 Server.") + return True + + # If we've reached this part of the method then we've received a keep alive but haven't sent a reply. + # Therefore we also need to configure the masquerade attributes based off the keep alive sent. + if self._resolve_keep_alive(payload, session_id) is False: + self.sys_log.warning(f"{self.name}: Keep Alive Could not be resolved correctly. Refusing Keep Alive.") + return False + + self.keep_alive_attempted = True + # If this method returns true then we have sent successfully sent a keep alive. + return self._send_keep_alive(session_id) - # 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. + """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. - + 3. If a keep alive response packet is received then the ``keep_alive_inactivity`` attribute is reset. + + Therefore, if ``keep_alive_inactivity`` attribute is not 0 after a keep alive is sent + then the connection is considered severed and c2 beacon will shut down. + :param timestep: The current timestep of the simulation. + :type timestep: Int + :return bool: Returns false if connection was lost. Returns True if connection is active or re-established. + :rtype bool: """ super().apply_timestep(timestep=timestep) - if self.operating_state is ApplicationOperatingState.RUNNING and self.health_state_actual is SoftwareHealthState.GOOD: + self.keep_alive_attempted = False # Resetting keep alive sent. + 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.") @@ -259,34 +364,67 @@ class C2Beacon(AbstractC2, identifier="C2 Beacon"): self.close() return + def _check_c2_connection(self, timestep: int) -> bool: + """Checks the suitability of the current C2 Server connection. - def _check_c2_connection(self, timestep) -> bool: - """Checks the C2 Server connection. If a connection cannot be confirmed then this method will return false otherwise true.""" + If a connection cannot be confirmed then this method will return false otherwise true. + + :param timestep: The current timestep of the simulation. + :type timestep: Int + :return: Returns False if connection was lost. Returns True if connection is active or re-established. + :rtype bool: + """ if self.keep_alive_inactivity == self.keep_alive_frequency: - self.sys_log.info(f"{self.name}: Attempting to Send Keep Alive to {self.c2_remote_connection} at timestep {timestep}.") + self.sys_log.info( + f"{self.name}: Attempting to Send Keep Alive to {self.c2_remote_connection} at timestep {timestep}." + ) self._send_keep_alive(session_id=self.current_c2_session.uuid) if self.keep_alive_inactivity != 0: - self.sys_log.warning(f"{self.name}: Did not receive keep alive from c2 Server. Connection considered severed.") + self.sys_log.warning( + f"{self.name}: Did not receive keep alive from c2 Server. Connection considered severed." + ) return False return True - # Defining this abstract method from Abstract C2 - def _handle_command_output(self, payload): - """C2 Beacons currently do not need to handle output commands coming from the C2 Servers.""" + def _handle_command_output(self, payload: MasqueradePacket): + """C2 Beacons currently does not need to handle output commands coming from the C2 Servers.""" self.sys_log.warning(f"{self.name}: C2 Beacon received an unexpected OUTPUT payload: {payload}.") pass - + def show(self, markdown: bool = False): """ - Prints a table of the current C2 attributes on a C2 Beacon. + Prints a table of the current status of the C2 Beacon. + + Displays the current values of the following C2 attributes: + + ``C2 Connection Active``: + If the C2 Beacon is currently connected to the C2 Server + + ``C2 Remote Connection``: + The IP of the C2 Server. (Configured by upon installation) + + ``Keep Alive Inactivity``: + How many timesteps have occurred since the last keep alive. + + ``Keep Alive Frequency``: + How often should the C2 Beacon attempt a keep alive? :param markdown: If True, outputs the table in markdown format. Default is False. """ - table = PrettyTable(["C2 Connection Active", "C2 Remote Connection", "Keep Alive Inactivity", "Keep Alive Frequency"]) + table = PrettyTable( + ["C2 Connection Active", "C2 Remote Connection", "Keep Alive Inactivity", "Keep Alive Frequency"] + ) if markdown: table.set_style(MARKDOWN) table.align = "l" table.title = f"{self.name} Running Status" - table.add_row([self.c2_connection_active, self.c2_remote_connection, self.keep_alive_inactivity, self.keep_alive_frequency]) + table.add_row( + [ + self.c2_connection_active, + self.c2_remote_connection, + self.keep_alive_inactivity, + self.keep_alive_frequency, + ] + ) print(table) diff --git a/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py b/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py index 8fe8c00c..9d02224e 100644 --- a/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py +++ b/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py @@ -1,15 +1,34 @@ # © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK -from primaite.simulator.system.applications.red_applications.c2.abstract_c2 import AbstractC2, C2Command -from primaite.simulator.network.protocols.masquerade import C2Payload, MasqueradePacket -from primaite.simulator.core import RequestManager, RequestType -from primaite.interface.request import RequestFormat, RequestResponse +from typing import Dict, Optional + from prettytable import MARKDOWN, PrettyTable -from typing import Dict,Optional +from pydantic import validate_call + +from primaite.interface.request import RequestFormat, RequestResponse +from primaite.simulator.core import RequestManager, RequestType +from primaite.simulator.network.protocols.masquerade import MasqueradePacket +from primaite.simulator.system.applications.red_applications.c2.abstract_c2 import AbstractC2, C2Command, C2Payload + class C2Server(AbstractC2, identifier="C2 Server"): - # TODO: - # Implement the request manager and agent actions. - # Implement the output handling methods. (These need to interface with the actions) + """ + C2 Server Application. + + Represents a vendor generic C2 Server is used in conjunction with the C2 beacon + to simulate malicious communications and infrastructure within primAITE. + + The C2 Server must be installed and be in a running state before it's able to receive + red agent actions and send commands to the C2 beacon. + + Extends the Abstract C2 application to include the following: + + 1. Sending commands to the C2 Beacon. (Command input) + 2. Parsing terminal RequestResponses back to the Agent. + """ + + current_command_output: RequestResponse = None + """The Request Response by the last command send. This attribute is updated by the method _handle_command_output.""" + def _init_request_manager(self) -> RequestManager: """ Initialise the request manager. @@ -34,7 +53,7 @@ class C2Server(AbstractC2, identifier="C2 Server"): def _launch_ransomware_action(request: RequestFormat, context: Dict) -> RequestResponse: """Agent Action - Sends a RANSOMWARE_LAUNCH C2Command to the C2 Beacon with the given parameters. - + :param request: Request with one element containing a dict of parameters for the configure method. :type request: RequestFormat :param context: additional context for resolving this action, currently unused @@ -48,7 +67,7 @@ class C2Server(AbstractC2, identifier="C2 Server"): def _remote_terminal_action(request: RequestFormat, context: Dict) -> RequestResponse: """Agent Action - Sends a TERMINAL C2Command to the C2 Beacon with the given parameters. - + :param request: Request with one element containing a dict of parameters for the configure method. :type request: RequestFormat :param context: additional context for resolving this action, currently unused @@ -78,28 +97,69 @@ class C2Server(AbstractC2, identifier="C2 Server"): kwargs["name"] = "C2Server" super().__init__(**kwargs) - def _handle_command_output(self, payload: MasqueradePacket) -> RequestResponse: + def _handle_command_output(self, payload: MasqueradePacket) -> bool: """ - Handles the parsing of C2 Command Output from C2 Traffic (Masquerade Packets) - as well as then calling the relevant method dependant on the C2 Command. - - :param payload: The OUTPUT C2 Payload + Handles the parsing of C2 Command Output from C2 Traffic (Masquerade Packets). + + Parses the Request Response within MasqueradePacket's payload attribute (Inherited from Data packet). + The class attribute self.current_command_output is then set to this Request Response. + + If the payload attribute does not contain a RequestResponse, then an error will be raised in syslog and + the self.current_command_output is updated to reflect the error. + + :param payload: The OUTPUT C2 Payload :type payload: MasqueradePacket - :return: Returns the Request Response of the C2 Beacon's host terminal service execute method. - :rtype Request Response: + :return: Returns True if the self.current_command_output is currently updated, false otherwise. + :rtype Bool: """ self.sys_log.info(f"{self.name}: Received command response from C2 Beacon: {payload}.") command_output = payload.payload - if command_output != MasqueradePacket: - self.sys_log.warning(f"{self.name}: Received invalid command response: {command_output}.") - return RequestResponse(status="failure", data={"Received unexpected C2 Response."}) - return command_output - + if not isinstance(command_output, RequestResponse): + self.sys_log.warning(f"{self.name}: C2 Server received invalid command response: {command_output}.") + self.current_command_output = RequestResponse( + status="failure", data={"Reason": "Received unexpected C2 Response."} + ) + return False + self.current_command_output = command_output + return True + + def _handle_keep_alive(self, payload: MasqueradePacket, session_id: Optional[str]) -> bool: + """ + Handles receiving and sending keep alive payloads. This method is only called if we receive a keep alive. + + In the C2 Server implementation of this method the c2 connection active boolean + is set to true and the keep alive inactivity is reset after receiving one keep alive. + + This is because the C2 Server is the listener and thus will only ever receive packets from + the C2 Beacon rather than the other way around. (The C2 Beacon is akin to a reverse shell) + + Returns False if a keep alive was unable to be sent. + Returns True if a keep alive was successfully sent or already has been sent this timestep. + + :return: True if successfully handled, false otherwise. + :rtype: Bool + """ + self.sys_log.info(f"{self.name}: Keep Alive Received. Attempting to resolve the remote connection details.") + + self.c2_connection_active = True # Sets the connection to active + self.current_c2_session = self.software_manager.session_manager.sessions_by_uuid[session_id] + + if self._resolve_keep_alive(payload, session_id) == False: + self.sys_log.warning(f"{self.name}: Keep Alive Could not be resolved correctly. Refusing Keep Alive.") + return False + + # If this method returns true then we have sent successfully sent a keep alive. + self.sys_log.info(f"{self.name}: Remote connection successfully established: {self.c2_remote_connection}.") + self.sys_log.debug(f"{self.name}: Attempting to send Keep Alive response back to {self.c2_remote_connection}.") + + return self._send_keep_alive(session_id) + + @validate_call def _send_command(self, given_command: C2Command, command_options: Dict) -> RequestResponse: """ Sends a command to the C2 Beacon. - + # TODO: Expand this docustring. :param given_command: The C2 command to be sent to the C2 Beacon. @@ -109,31 +169,41 @@ class C2Server(AbstractC2, identifier="C2 Server"): :return: Returns the Request Response of the C2 Beacon's host terminal service execute method. :rtype: RequestResponse """ - if given_command != C2Payload: + if not isinstance(given_command, C2Command): self.sys_log.warning(f"{self.name}: Received unexpected C2 command. Unable to send command.") - return RequestResponse(status="failure", data={"Received unexpected C2Command. Unable to send command."}) + return RequestResponse( + status="failure", data={"Reason": "Received unexpected C2Command. Unable to send command."} + ) + + if self._can_perform_network_action == False: + self.sys_log.warning(f"{self.name}: Unable to make leverage networking resources. Rejecting Command.") + return RequestResponse( + status="failure", data={"Reason": "Unable to access networking resources. Unable to send command."} + ) self.sys_log.info(f"{self.name}: Attempting to send command {given_command}.") command_packet = self._craft_packet(given_command=given_command, command_options=command_options) # Need to investigate if this is correct. - if self.send(payload=command_packet, + if self.send( + payload=command_packet, dest_ip_address=self.c2_remote_connection, - src_port=self.current_masquerade_port, - dst_port=self.current_masquerade_port, + session_id=self.current_c2_session.uuid, + dest_port=self.current_masquerade_port, ip_protocol=self.current_masquerade_protocol, - session_id=None): + ): self.sys_log.info(f"{self.name}: Successfully sent {given_command}.") self.sys_log.info(f"{self.name}: Awaiting command response {given_command}.") - return self._handle_command_output(command_packet) + # If the command output was handled currently, the self.current_command_output will contain the RequestResponse. + return self.current_command_output # TODO: Perhaps make a new pydantic base model for command_options? # TODO: Perhaps make the return optional? Returns False is the packet was unable to be crafted. def _craft_packet(self, given_command: C2Command, command_options: Dict) -> MasqueradePacket: """ Creates a Masquerade Packet based off the command parameter and the arguments given. - + :param given_command: The C2 command to be sent to the C2 Beacon. :type given_command: C2Command. :param command_options: The relevant C2 Beacon parameters.F @@ -147,27 +217,33 @@ class C2Server(AbstractC2, identifier="C2 Server"): masquerade_port=self.current_masquerade_port, payload_type=C2Payload.INPUT, command=given_command, - payload=command_options + payload=command_options, ) return constructed_packet - + + # TODO: I think I can just overload the methods rather than setting it as abstract_method? # Defining this abstract method - def _handle_command_input(self, payload): - """C2 Servers currently do not receive input commands coming from the C2 Beacons.""" + def _handle_command_input(self, payload: MasqueradePacket): + """Defining this method (Abstract method inherited from abstract C2) in order to instantiate the class. + + C2 Servers currently do not receive input commands coming from the C2 Beacons. + + :param payload: The incoming MasqueradePacket + :type payload: MasqueradePacket. + """ self.sys_log.warning(f"{self.name}: C2 Server received an unexpected INPUT payload: {payload}") pass - def show(self, markdown: bool = False): """ Prints a table of the current C2 attributes on a C2 Server. :param markdown: If True, outputs the table in markdown format. Default is False. """ - table = PrettyTable(["C2 Connection Active", "C2 Remote Connection", "Keep Alive Inactivity"]) + table = PrettyTable(["C2 Connection Active", "C2 Remote Connection"]) if markdown: table.set_style(MARKDOWN) table.align = "l" table.title = f"{self.name} Running Status" - table.add_row([self.c2_connection_active, self.c2_remote_connection, self.keep_alive_inactivity]) + table.add_row([self.c2_connection_active, self.c2_remote_connection]) print(table) diff --git a/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_c2_suite.py b/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_c2_suite.py index 3014cd19..7f869e92 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_c2_suite.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_c2_suite.py @@ -12,14 +12,15 @@ from primaite.simulator.network.hardware.nodes.network.router import ACLAction, from primaite.simulator.network.hardware.nodes.network.switch import Switch from primaite.simulator.network.transmission.network_layer import IPProtocol from primaite.simulator.network.transmission.transport_layer import Port +from primaite.simulator.system.applications.red_applications.c2.c2_beacon import C2Beacon +from primaite.simulator.system.applications.red_applications.c2.c2_server import C2Server from primaite.simulator.system.services.dns.dns_server import DNSServer from primaite.simulator.system.services.service import ServiceOperatingState from primaite.simulator.system.services.web_server.web_server import WebServer -from primaite.simulator.system.applications.red_applications.c2.c2_beacon import C2Beacon -from primaite.simulator.system.applications.red_applications.c2.c2_server import C2Server # TODO: Update these tests. + @pytest.fixture(scope="function") def c2_server_on_computer() -> Tuple[C2Beacon, Computer]: computer: Computer = Computer( @@ -30,6 +31,7 @@ def c2_server_on_computer() -> Tuple[C2Beacon, Computer]: return [c2_beacon, computer] + @pytest.fixture(scope="function") def c2_server_on_computer() -> Tuple[C2Server, Computer]: computer: Computer = Computer( @@ -41,7 +43,6 @@ def c2_server_on_computer() -> Tuple[C2Server, Computer]: return [c2_server, computer] - @pytest.fixture(scope="function") def basic_network() -> Network: network = Network() @@ -57,6 +58,7 @@ def basic_network() -> Network: return network + def test_c2_suite_setup_receive(basic_network): """Test that C2 Beacon can successfully establish connection with the c2 Server.""" network: Network = basic_network @@ -68,5 +70,5 @@ def test_c2_suite_setup_receive(basic_network): c2_beacon.configure(c2_server_ip_address="192.168.0.10") c2_beacon.establish() - - c2_beacon.sys_log.show() \ No newline at end of file + + c2_beacon.sys_log.show() From 1933522e8928d948ff081529ad774da48e638ffc Mon Sep 17 00:00:00 2001 From: "Archer.Bowen" Date: Fri, 2 Aug 2024 16:13:59 +0100 Subject: [PATCH 11/46] #2689 Updated docustrings and general quality improvements. --- .../system/applications/application.py | 18 ++++++ .../red_applications/c2/abstract_c2.py | 51 ++++++++-------- .../red_applications/c2/c2_server.py | 61 +++++++++++++------ 3 files changed, 88 insertions(+), 42 deletions(-) diff --git a/src/primaite/simulator/system/applications/application.py b/src/primaite/simulator/system/applications/application.py index dc16a725..741f491d 100644 --- a/src/primaite/simulator/system/applications/application.py +++ b/src/primaite/simulator/system/applications/application.py @@ -214,3 +214,21 @@ class Application(IOSoftware): f"Cannot perform request on application '{self.application.name}' because it is not in the " f"{self.state.name} state." ) + + def _can_perform_network_action(self) -> bool: + """ + Checks if the application can perform outbound network actions. + + First confirms application suitability via the can_perform_action method. + Then confirms that the host has an enabled NIC that can be used for outbound traffic. + + :return: True if outbound network actions can be performed, otherwise False. + :rtype bool: + """ + if not super()._can_perform_action(): + return False + + for nic in self.software_manager.node.network_interface.values(): + if nic.enabled: + return True + return False diff --git a/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py b/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py index af701e8c..89ab7953 100644 --- a/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py +++ b/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py @@ -13,7 +13,6 @@ from primaite.simulator.system.applications.application import Application from primaite.simulator.system.core.session_manager import Session # TODO: -# Complete C2 Server and C2 Beacon TODOs # Create test that leverage all the functionality needed for the different TAPs # Create a .RST doc # Potentially? A notebook which demonstrates a custom red agent using the c2 server for various means. @@ -79,24 +78,6 @@ class AbstractC2(Application, identifier="AbstractC2"): current_c2_session: Session = None """The currently active session that the C2 Traffic is using. Set after establishing connection.""" - # TODO: Move this duplicate method from NMAP class into 'Application' to adhere to DRY principle. - def _can_perform_network_action(self) -> bool: - """ - Checks if the C2 application can perform outbound network actions. - - This is done by checking the parent application can_per_action functionality. - Then checking if there is an enabled NIC that can be used for outbound traffic. - - :return: True if outbound network actions can be performed, otherwise False. - """ - if not super()._can_perform_action(): - return False - - for nic in self.software_manager.node.network_interface.values(): - if nic.enabled: - return True - return False - def describe_state(self) -> Dict: """ Describe the state of the C2 application. @@ -106,9 +87,11 @@ class AbstractC2(Application, identifier="AbstractC2"): """ return super().describe_state() + # TODO: Update this post application/services requiring to listen to multiple ports def __init__(self, **kwargs): + """Initialise the C2 applications to by default listen for HTTP traffic.""" kwargs["port"] = Port.HTTP # TODO: Update this post application/services requiring to listen to multiple ports - kwargs["protocol"] = IPProtocol.TCP # Update this as well + kwargs["protocol"] = IPProtocol.TCP super().__init__(**kwargs) # Validate call ensures we are only handling Masquerade Packets. @@ -173,15 +156,30 @@ class AbstractC2(Application, identifier="AbstractC2"): # from_network_interface=from_network_interface def receive(self, payload: MasqueradePacket, session_id: Optional[str] = None, **kwargs) -> bool: - """Receives masquerade packets. Used by both c2 server and c2 client. + """Receives masquerade packets. Used by both c2 server and c2 beacon. + + Defining the `Receive` method so that the application can receive packets via the session manager. + These packets are then immediately handed to ._handle_c2_payload. :param payload: The Masquerade Packet to be received. - :param session: The transport session that the payload is originating from. + :type payload: MasqueradePacket + :param session_id: The transport session_id that the payload is originating from. + :type session_id: str """ return self._handle_c2_payload(payload, session_id) def _send_keep_alive(self, session_id: Optional[str]) -> bool: - """Sends a C2 keep alive payload to the self.remote_connection IPv4 Address.""" + """Sends a C2 keep alive payload to the self.remote_connection IPv4 Address. + + Used by both the c2 client and the s2 server for establishing and confirming connection. + This method also contains some additional validation to ensure that the C2 applications + are correctly configured before sending any traffic. + + :param session_id: The transport session_id that the payload is originating from. + :type session_id: str + :returns: Returns True if a send alive was successfully sent. False otherwise. + :rtype bool: + """ # Checking that the c2 application is capable of performing both actions and has an enabled NIC # (Using NOT to improve code readability) if self.c2_remote_connection is None: @@ -230,6 +228,8 @@ class AbstractC2(Application, identifier="AbstractC2"): :param payload: The Keep Alive payload received. :type payload: MasqueradePacket + :param session_id: The transport session_id that the payload is originating from. + :type session_id: str :return: True on successful configuration, false otherwise. :rtype: bool """ @@ -240,8 +240,9 @@ class AbstractC2(Application, identifier="AbstractC2"): f"Port: {payload.masquerade_port} Protocol: {payload.masquerade_protocol}." ) return False + # TODO: Validation on Ports (E.g only allow HTTP, FTP etc) - # Potentially compare to IPProtocol & Port children (Same way that abstract TAP does it with kill chains) + # Potentially compare to IPProtocol & Port children? Depends on how listening on multiple ports is implemented. # Setting the Ports self.current_masquerade_port = payload.masquerade_port @@ -253,6 +254,6 @@ class AbstractC2(Application, identifier="AbstractC2"): self.c2_remote_connection = self.current_c2_session.with_ip_address self.c2_connection_active = True # Sets the connection to active - self.keep_alive_inactivity = 0 # Sets the keep alive inactivity to zeroW + self.keep_alive_inactivity = 0 # Sets the keep alive inactivity to zero return True diff --git a/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py b/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py index 9d02224e..c29cd271 100644 --- a/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py +++ b/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py @@ -137,6 +137,10 @@ class C2Server(AbstractC2, identifier="C2 Server"): Returns False if a keep alive was unable to be sent. Returns True if a keep alive was successfully sent or already has been sent this timestep. + :param payload: The Keep Alive payload received. + :type payload: MasqueradePacket + :param session_id: The transport session_id that the payload is originating from. + :type session_id: str :return: True if successfully handled, false otherwise. :rtype: Bool """ @@ -160,7 +164,22 @@ class C2Server(AbstractC2, identifier="C2 Server"): """ Sends a command to the C2 Beacon. - # TODO: Expand this docustring. + Currently, these commands leverage the pre-existing capability of other applications. + However, the commands are sent via the network rather than the game layer which + grants more opportunity to the blue agent to prevent attacks. + + Additionally, future editions of primAITE may expand the C2 repertoire to allow for + more complex red agent behaviour such as file extraction, establishing further fall back channels + or introduce red applications that are only installable via C2 Servers. (T1105) + + C2 Command | Meaning + ---------------------|------------------------ + RANSOMWARE_CONFIGURE | Configures an installed ransomware script based on the passed parameters. + RANSOMWARE_LAUNCH | Launches the installed ransomware script. + Terminal | Executes a command via the terminal installed on the C2 Beacons Host. + + For more information on the impact of these commands please refer to the terminal + and the ransomware applications. :param given_command: The C2 command to be sent to the C2 Beacon. :type given_command: C2Command. @@ -198,11 +217,12 @@ class C2Server(AbstractC2, identifier="C2 Server"): # If the command output was handled currently, the self.current_command_output will contain the RequestResponse. return self.current_command_output - # TODO: Perhaps make a new pydantic base model for command_options? - # TODO: Perhaps make the return optional? Returns False is the packet was unable to be crafted. + # TODO: Probably could move this as a class method in MasqueradePacket. def _craft_packet(self, given_command: C2Command, command_options: Dict) -> MasqueradePacket: """ - Creates a Masquerade Packet based off the command parameter and the arguments given. + Creates and returns a Masquerade Packet using the arguments given. + + Creates Masquerade Packet with a payload_type INPUT C2Payload :param given_command: The C2 command to be sent to the C2 Beacon. :type given_command: C2Command. @@ -221,23 +241,18 @@ class C2Server(AbstractC2, identifier="C2 Server"): ) return constructed_packet - # TODO: I think I can just overload the methods rather than setting it as abstract_method? - # Defining this abstract method - def _handle_command_input(self, payload: MasqueradePacket): - """Defining this method (Abstract method inherited from abstract C2) in order to instantiate the class. - - C2 Servers currently do not receive input commands coming from the C2 Beacons. - - :param payload: The incoming MasqueradePacket - :type payload: MasqueradePacket. - """ - self.sys_log.warning(f"{self.name}: C2 Server received an unexpected INPUT payload: {payload}") - pass - def show(self, markdown: bool = False): """ Prints a table of the current C2 attributes on a C2 Server. + Displays the current values of the following C2 attributes: + + ``C2 Connection Active``: + If the C2 Server has established connection with a C2 Beacon. + + ``C2 Remote Connection``: + The IP of the C2 Beacon. (Configured by upon receiving a keep alive.) + :param markdown: If True, outputs the table in markdown format. Default is False. """ table = PrettyTable(["C2 Connection Active", "C2 Remote Connection"]) @@ -247,3 +262,15 @@ class C2Server(AbstractC2, identifier="C2 Server"): table.title = f"{self.name} Running Status" table.add_row([self.c2_connection_active, self.c2_remote_connection]) print(table) + + # Abstract method inherited from abstract C2 - Not currently utilised. + def _handle_command_input(self, payload: MasqueradePacket) -> None: + """Defining this method (Abstract method inherited from abstract C2) in order to instantiate the class. + + C2 Servers currently do not receive input commands coming from the C2 Beacons. + + :param payload: The incoming MasqueradePacket + :type payload: MasqueradePacket. + """ + self.sys_log.warning(f"{self.name}: C2 Server received an unexpected INPUT payload: {payload}") + pass From 4ae0275dc9e6b6a46ab4aa6515d056f0d4f96c88 Mon Sep 17 00:00:00 2001 From: "Archer.Bowen" Date: Mon, 5 Aug 2024 16:53:48 +0100 Subject: [PATCH 12/46] #2689 Implemented initial agent actions and started on documentations. A few TODO's left to do such as validation and expanding unit tests. --- .../system/applications/c2_suite.rst | 145 ++++++++++++ .../red_applications/c2/abstract_c2.py | 4 +- .../red_applications/c2/c2_beacon.py | 19 +- .../red_applications/c2/c2_server.py | 17 +- .../red_applications/ransomware_script.py | 24 ++ .../_red_applications/test_c2_suite.py | 224 +++++++++++++++++- 6 files changed, 412 insertions(+), 21 deletions(-) create mode 100644 docs/source/simulation_components/system/applications/c2_suite.rst diff --git a/docs/source/simulation_components/system/applications/c2_suite.rst b/docs/source/simulation_components/system/applications/c2_suite.rst new file mode 100644 index 00000000..c360d0be --- /dev/null +++ b/docs/source/simulation_components/system/applications/c2_suite.rst @@ -0,0 +1,145 @@ +.. only:: comment + + © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + +.. _C2_Suite: + +Command and Control Application Suite +##################################### + +Comprising of two applications, the command and control (C2) suites intends to introduce +malicious network architecture and begin to further the realism of red agents within primAITE. + +Overview: +========= + +These two new classes intend to Red Agents a cyber realistic way of leveraging the capabilities of the ``Terminal`` application. +Whilst introducing both more oppourtinies for the blue agent to notice and subvert Red Agents during an episode. + +For a more in-depth look at the command and control applications then please refer to the ``C2-E2E-Notebook``. + +``C2 Server`` +"""""""""""" + +The C2 Server application is intended to represent the malicious infrastructure already under the control of an adversary. + +The C2 Server is configured to listen and await ``keep alive`` traffic from a c2 beacon. Once received the C2 Server is able to send and receive c2 commands. + +Currently, the C2 Server offers three commands: + ++---------------------+---------------------------------------------------------------------------+ +|C2 Command | Meaning | ++=====================+===========================================================================+ +|RANSOMWARE_CONFIGURE | Configures an installed ransomware script based on the passed parameters. | ++---------------------+---------------------------------------------------------------------------+ +|RANSOMWARE_LAUNCH | Launches the installed ransomware script. | ++---------------------+---------------------------------------------------------------------------+ +|TERMINAL_COMMAND | Executes a command via the terminal installed on the C2 Beacons Host. | ++---------------------+---------------------------------------------------------------------------+ + + +It's important to note that in order to keep the PrimAITE realistic from a cyber perspective, +The C2 Server application should never be visible or actionable upon directly by the blue agent. + +This is because in the real world, C2 servers are hosted on ephemeral public domains that would not be accessible by private network blue agent. +Therefore granting a blue agent's the ability to perform counter measures directly against the application would be unrealistic. + +It is more accurate to see the host that the C2 Server is installed on as being able to route to the C2 Server (Internet Access). + +``C2 Beacon`` +""""""""""""" + +The C2 Beacon application is intended to represent malware that is used to establish and maintain contact to a C2 Server within a compromised network. + +A C2 Beacon will need to be first configured with the C2 Server IP Address which can be done via the ``configure`` method. + +Once installed and configured; the c2 beacon can establish connection with the C2 Server via executing the application. + +This will send an initial ``keep alive`` to the given C2 Server (The C2 Server IPv4Address must be given upon C2 Beacon configuration). +Which is then resolved and responded by another ``Keep Alive`` by the c2 server back to the C2 beacon to confirm connection. + +The C2 Beacon will send out periodic keep alive based on it's configuration parameters to configure it's active connection with the c2 server. + +It's recommended that a C2 Beacon is installed and configured mid episode by a Red Agent for a more cyber realistic simulation. + +Usage +===== + +As mentioned, the C2 Suite is intended to grant Red Agents further flexibility whilst also expanding a blue agent's observation_space. + +Adding to this, the following behaviour of the C2 beacon can be configured by users for increased domain randomisation: + +- Frequency of C2 ``Keep Alive `` Communication`` +- C2 Communication Port +- C2 Communication Protocol + + +Implementation +============== + +Both applications inherit from an abstract C2 which handles the keep alive functionality and main logic. +However, each host implements it's receive methods individually. + +- The ``C2 Beacon`` is responsible for the following logic: + - Establishes and confirms connection to the C2 Server via sending ``C2Payload.KEEP_ALIVE``. + - Receives and executes C2 Commands given by the C2 Server via ``C2Payload.INPUT``. + - Returns the RequestResponse of the C2 Commands executed back the C2 Server via ``C2Payload.OUTPUT``. + +- The ``C2 Server`` is responsible for the following logic: + - Listens and resolves connection to a C2 Beacon via responding to ``C2Payload.KEEP_ALIVE``. + - Sends C2 Commands to the C2 Beacon via ``C2Payload.INPUT``. + - Receives the RequestResponse of the C2 Commands executed by C2 Beacon via ``C2Payload.OUTPUT``. + + + +Examples +======== + +Python +"""""" +.. code-block:: python + from primaite.simulator.system.applications.red_applications.c2.c2_beacon import C2Beacon + from primaite.simulator.system.applications.red_applications.c2.c2_server import C2Server + from primaite.simulator.system.applications.red_applications.c2.c2_server import C2Command + from primaite.simulator.network.hardware.nodes.host.computer import Computer + + # Network Setup + + node_a = Computer(hostname="node_a", ip_address="192.168.0.10", subnet_mask="255.255.255.0", start_up_duration=0) + node_a.power_on() + node_a.software_manager.install(software_class=C2Server) + node_a.software_manager.get_open_ports() + + + node_b = Computer(hostname="node_b", ip_address="192.168.0.11", subnet_mask="255.255.255.0", start_up_duration=0) + node_b.power_on() + node_b.software_manager.install(software_class=C2Beacon) + node_b.software_manager.install(software_class=RansomwareScript) + network.connect(node_a.network_interface[1], node_b.network_interface[1]) + + + # C2 Application objects + + c2_server_host = simulation_testing_network.get_node_by_hostname("node_a") + c2_beacon_host = simulation_testing_network.get_node_by_hostname("node_b") + + + c2_server: C2Server = c2_server_host.software_manager.software["C2Server"] + c2_beacon: C2Beacon = c2_beacon_host.software_manager.software["C2Beacon"] + + # Configuring the C2 Beacon + c2_beacon.configure(c2_server_ip_address="192.168.0.10", keep_alive_frequency=5) + + # Launching the C2 Server (Needs to be running in order to listen for connections) + c2_server.run() + + # Establishing connection + c2_beacon.establish() + + # Example command: Configuring Ransomware + + ransomware_config = {"server_ip_address": "1.1.1.1"} + c2_server._send_command(given_command=C2Command.RANSOMWARE_CONFIGURE, command_options=ransomware_config) + + +For a more in-depth look at the command and control applications then please refer to the ``C2-Suite-E2E-Notebook``. diff --git a/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py b/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py index 89ab7953..9158d80f 100644 --- a/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py +++ b/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py @@ -213,7 +213,7 @@ class AbstractC2(Application, identifier="AbstractC2"): return True else: self.sys_log.warning( - f"{self.name}: failed to send a Keep Alive. The node may be unable to access the ``network." + f"{self.name}: failed to send a Keep Alive. The node may be unable to access networking resources." ) return False @@ -251,7 +251,7 @@ class AbstractC2(Application, identifier="AbstractC2"): # This statement is intended to catch on the C2 Application that is listening for connection. (C2 Beacon) if self.c2_remote_connection is None: self.sys_log.debug(f"{self.name}: Attempting to configure remote C2 connection based off received output.") - self.c2_remote_connection = self.current_c2_session.with_ip_address + self.c2_remote_connection = IPv4Address(self.current_c2_session.with_ip_address) self.c2_connection_active = True # Sets the connection to active self.keep_alive_inactivity = 0 # Sets the keep alive inactivity to zero 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 b00b7c57..16420164 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 @@ -13,6 +13,7 @@ from primaite.simulator.network.transmission.network_layer import IPProtocol from primaite.simulator.network.transmission.transport_layer import Port from primaite.simulator.system.applications.application import ApplicationOperatingState from primaite.simulator.system.applications.red_applications.c2.abstract_c2 import AbstractC2, C2Command, C2Payload +from primaite.simulator.system.applications.red_applications.ransomware_script import RansomwareScript from primaite.simulator.system.software import SoftwareHealthState @@ -44,8 +45,6 @@ class C2Beacon(AbstractC2, identifier="C2 Beacon"): # TODO: # Implement the placeholder command methods - # Implement the keep alive frequency. - # Implement a command output method that sends the RequestResponse to the C2 server. # Uncomment the terminal Import and the terminal property after terminal PR # @property @@ -56,6 +55,14 @@ class C2Beacon(AbstractC2, identifier="C2 Beacon"): # self.sys_log.warning(f"{self.__class__.__name__} cannot find a terminal on its host.") # return host_terminal + @property + def _host_ransomware_script(self) -> RansomwareScript: + """Return the RansomwareScript that is installed on the same machine as the C2 Beacon.""" + ransomware_script: RansomwareScript = self.software_manager.software.get("RansomwareScript") + if ransomware_script is None: + self.sys_log.warning(f"{self.__class__.__name__} cannot find installed ransomware on its host.") + return ransomware_script + def _init_request_manager(self) -> RequestManager: """ Initialise the request manager. @@ -87,6 +94,7 @@ class C2Beacon(AbstractC2, identifier="C2 Beacon"): ) c2_remote_ip = IPv4Address(c2_remote_ip) + # TODO: validation. frequency = request[-1].get("keep_alive_frequency") protocol = request[-1].get("masquerade_protocol") port = request[-1].get("masquerade_port") @@ -127,7 +135,7 @@ class C2Beacon(AbstractC2, identifier="C2 Beacon"): :param masquerade_port: The Port that the C2 Traffic will masquerade as. Defaults to FTP. :type masquerade_port: Enum (Port) """ - self.c2_remote_connection = c2_server_ip_address + self.c2_remote_connection = IPv4Address(c2_server_ip_address) self.keep_alive_frequency = keep_alive_frequency self.current_masquerade_port = masquerade_port self.current_masquerade_protocol = masquerade_protocol @@ -252,7 +260,10 @@ class C2Beacon(AbstractC2, identifier="C2 Beacon"): :rtype: Request Response """ # TODO: replace and use terminal - return RequestResponse(status="success", data={"Reason": "Placeholder."}) + # return RequestResponse(status="success", data={"Reason": "Placeholder."}) + given_config = payload.payload + host_ransomware = self._host_ransomware_script + return RequestResponse.from_bool(host_ransomware.configure(server_ip_address=given_config["server_ip_address"])) def _command_ransomware_launch(self, payload: MasqueradePacket) -> RequestResponse: """ diff --git a/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py b/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py index c29cd271..d01cd412 100644 --- a/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py +++ b/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py @@ -48,8 +48,8 @@ class C2Server(AbstractC2, identifier="C2 Server"): :rtype: RequestResponse """ # TODO: Parse the parameters from the request to get the parameters - placeholder: dict = {} - return self._send_command(given_command=C2Command.RANSOMWARE_CONFIGURE, command_options=placeholder) + ransomware_config = {"server_ip_address": request[-1].get("server_ip_address")} + return self._send_command(given_command=C2Command.RANSOMWARE_CONFIGURE, command_options=ransomware_config) def _launch_ransomware_action(request: RequestFormat, context: Dict) -> RequestResponse: """Agent Action - Sends a RANSOMWARE_LAUNCH C2Command to the C2 Beacon with the given parameters. @@ -61,9 +61,7 @@ class C2Server(AbstractC2, identifier="C2 Server"): :return: RequestResponse object with a success code reflecting whether the ransomware was launched. :rtype: RequestResponse """ - # TODO: Parse the parameters from the request to get the parameters - placeholder: dict = {} - return self._send_command(given_command=C2Command.RANSOMWARE_LAUNCH, command_options=placeholder) + return self._send_command(given_command=C2Command.RANSOMWARE_LAUNCH, command_options={}) def _remote_terminal_action(request: RequestFormat, context: Dict) -> RequestResponse: """Agent Action - Sends a TERMINAL C2Command to the C2 Beacon with the given parameters. @@ -77,18 +75,18 @@ class C2Server(AbstractC2, identifier="C2 Server"): """ # TODO: Parse the parameters from the request to get the parameters placeholder: dict = {} - return self._send_command(given_command=C2Command.RANSOMWARE_LAUNCH, command_options=placeholder) + return self._send_command(given_command=C2Command.TERMINAL, command_options=placeholder) rm.add_request( - name="c2_ransomware_configure", + name="ransomware_configure", request_type=RequestType(func=_configure_ransomware_action), ) rm.add_request( - name="c2_ransomware_launch", + name="ransomware_launch", request_type=RequestType(func=_launch_ransomware_action), ) rm.add_request( - name="c2_terminal_command", + name="terminal_command", request_type=RequestType(func=_remote_terminal_action), ) return rm @@ -203,7 +201,6 @@ class C2Server(AbstractC2, identifier="C2 Server"): self.sys_log.info(f"{self.name}: Attempting to send command {given_command}.") command_packet = self._craft_packet(given_command=given_command, command_options=command_options) - # Need to investigate if this is correct. if self.send( payload=command_packet, dest_ip_address=self.c2_remote_connection, diff --git a/src/primaite/simulator/system/applications/red_applications/ransomware_script.py b/src/primaite/simulator/system/applications/red_applications/ransomware_script.py index 77a6bf2c..2046affc 100644 --- a/src/primaite/simulator/system/applications/red_applications/ransomware_script.py +++ b/src/primaite/simulator/system/applications/red_applications/ransomware_script.py @@ -2,6 +2,8 @@ from ipaddress import IPv4Address from typing import Dict, Optional +from prettytable import MARKDOWN, PrettyTable + from primaite.interface.request import RequestFormat, RequestResponse from primaite.simulator.core import RequestManager, RequestType from primaite.simulator.network.transmission.network_layer import IPProtocol @@ -169,3 +171,25 @@ class RansomwareScript(Application, identifier="RansomwareScript"): else: self.sys_log.warning("Attack Attempted to launch too quickly") return False + + def show(self, markdown: bool = False): + """ + Prints a table of the current status of the Ransomware Script. + + Displays the current values of the following Ransomware Attributes: + + ``server_ip_address`: + The IP of the target database. + + ``payload``: + The payload (type of attack) to be sent to the database. + + :param markdown: If True, outputs the table in markdown format. Default is False. + """ + table = PrettyTable(["Target Server IP Address", "Payload"]) + if markdown: + table.set_style(MARKDOWN) + table.align = "l" + table.title = f"{self.name} Running Status" + table.add_row([self.server_ip_address, self.payload]) + print(table) diff --git a/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_c2_suite.py b/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_c2_suite.py index 7f869e92..064ef57d 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_c2_suite.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_c2_suite.py @@ -1,4 +1,5 @@ # © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +from ipaddress import IPv4Address from typing import Tuple import pytest @@ -12,14 +13,13 @@ from primaite.simulator.network.hardware.nodes.network.router import ACLAction, from primaite.simulator.network.hardware.nodes.network.switch import Switch from primaite.simulator.network.transmission.network_layer import IPProtocol from primaite.simulator.network.transmission.transport_layer import Port +from primaite.simulator.system.applications.application import ApplicationOperatingState from primaite.simulator.system.applications.red_applications.c2.c2_beacon import C2Beacon from primaite.simulator.system.applications.red_applications.c2.c2_server import C2Server +from primaite.simulator.system.applications.red_applications.ransomware_script import RansomwareScript from primaite.simulator.system.services.dns.dns_server import DNSServer -from primaite.simulator.system.services.service import ServiceOperatingState from primaite.simulator.system.services.web_server.web_server import WebServer -# TODO: Update these tests. - @pytest.fixture(scope="function") def c2_server_on_computer() -> Tuple[C2Beacon, Computer]: @@ -60,7 +60,7 @@ def basic_network() -> Network: def test_c2_suite_setup_receive(basic_network): - """Test that C2 Beacon can successfully establish connection with the c2 Server.""" + """Test that C2 Beacon can successfully establish connection with the C2 Server.""" network: Network = basic_network computer_a: Computer = network.get_node_by_hostname("node_a") c2_server: C2Server = computer_a.software_manager.software.get("C2Server") @@ -68,7 +68,221 @@ def test_c2_suite_setup_receive(basic_network): computer_b: Computer = network.get_node_by_hostname("node_b") c2_beacon: C2Beacon = computer_b.software_manager.software.get("C2Beacon") + # Assert that the c2 beacon configure correctly. c2_beacon.configure(c2_server_ip_address="192.168.0.10") + assert c2_beacon.c2_remote_connection == IPv4Address("192.168.0.10") + + c2_server.run() c2_beacon.establish() - c2_beacon.sys_log.show() + # Asserting that the c2 beacon has established a c2 connection + assert c2_beacon.c2_connection_active is True + + # Asserting that the c2 server has established a c2 connection. + assert c2_server.c2_connection_active is True + assert c2_server.c2_remote_connection == IPv4Address("192.168.0.11") + + +def test_c2_suite_keep_alive_inactivity(basic_network): + """Tests that C2 Beacon disconnects from the C2 Server after inactivity.""" + network: Network = basic_network + computer_a: Computer = network.get_node_by_hostname("node_a") + c2_server: C2Server = computer_a.software_manager.software.get("C2Server") + + computer_b: Computer = network.get_node_by_hostname("node_b") + c2_beacon: C2Beacon = computer_b.software_manager.software.get("C2Beacon") + + # Initial config (#TODO: Make this a function) + c2_beacon.configure(c2_server_ip_address="192.168.0.10", keep_alive_frequency=2) + c2_server.run() + c2_beacon.establish() + + c2_beacon.apply_timestep(0) + assert c2_beacon.keep_alive_inactivity == 1 + + # Keep Alive successfully sent and received upon the 2nd timestep. + c2_beacon.apply_timestep(1) + assert c2_beacon.keep_alive_inactivity == 0 + assert c2_beacon.c2_connection_active == True + + # Now we turn off the c2 server (Thus preventing a keep alive) + c2_server.close() + c2_beacon.apply_timestep(2) + c2_beacon.apply_timestep(3) + assert c2_beacon.keep_alive_inactivity == 2 + assert c2_beacon.c2_connection_active == False + assert c2_beacon.health_state_actual == ApplicationOperatingState.CLOSED + + +# TODO: Flesh out these tests. +def test_c2_suite_configure_via_actions(basic_network): + """Tests that a red agent is able to configure the c2 beacon and c2 server via Actions.""" + # Setting up the network: + network: Network = basic_network + computer_a: Computer = network.get_node_by_hostname("node_a") + c2_server: C2Server = computer_a.software_manager.software.get("C2Server") + + computer_b: Computer = network.get_node_by_hostname("node_b") + c2_beacon: C2Beacon = computer_b.software_manager.software.get("C2Beacon") + + # Testing Via Requests: + network.apply_request(["node", "node_a", "application", "C2Server", "run"]) + + c2_beacon_config = { + "c2_server_ip_address": "192.168.0.10", + "keep_alive_frequency": 5, + "masquerade_protocol": IPProtocol.TCP, + "masquerade_port": Port.HTTP, + } + + network.apply_request(["node", "node_b", "application", "C2Beacon", "configure", c2_beacon_config]) + network.apply_request(["node", "node_b", "application", "C2Beacon", "execute"]) + + assert c2_beacon.c2_connection_active is True + assert c2_server.c2_connection_active is True + assert c2_server.c2_remote_connection == IPv4Address("192.168.0.11") + + # Testing Via Agents: + # TODO: + + +def test_c2_suite_configure_ransomware(basic_network): + """Tests that a red agent is able to configure ransomware via C2 Server Actions.""" + # Setting up the network: + network: Network = basic_network + computer_a: Computer = network.get_node_by_hostname("node_a") + c2_server: C2Server = computer_a.software_manager.software.get("C2Server") + + computer_b: Computer = network.get_node_by_hostname("node_b") + c2_beacon: C2Beacon = computer_b.software_manager.software.get("C2Beacon") + + c2_beacon.configure(c2_server_ip_address="192.168.0.10", keep_alive_frequency=2) + c2_server.run() + c2_beacon.establish() + + # Testing Via Requests: + computer_b.software_manager.install(software_class=RansomwareScript) + ransomware_config = {"server_ip_address": "1.1.1.1"} + network.apply_request(["node", "node_a", "application", "C2Server", "ransomware_configure", ransomware_config]) + + ransomware_script: RansomwareScript = computer_b.software_manager.software["RansomwareScript"] + + assert ransomware_script.server_ip_address == "1.1.1.1" + + # Testing Via Agents: + # TODO: + + +def test_c2_suite_terminal(basic_network): + """Tests that a red agent is able to execute terminal commands via C2 Server Actions.""" + + +@pytest.fixture(scope="function") +def acl_network() -> Network: + # 0: Pull out the network + network = Network() + + # 1: Set up network hardware + # 1.1: Configure the router + router = Router(hostname="router", num_ports=3, start_up_duration=0) + router.power_on() + router.configure_port(port=1, ip_address="10.0.1.1", subnet_mask="255.255.255.0") + router.configure_port(port=2, ip_address="10.0.2.1", subnet_mask="255.255.255.0") + + # 1.2: Create and connect switches + switch_1 = Switch(hostname="switch_1", num_ports=6, start_up_duration=0) + switch_1.power_on() + network.connect(endpoint_a=router.network_interface[1], endpoint_b=switch_1.network_interface[6]) + router.enable_port(1) + switch_2 = Switch(hostname="switch_2", num_ports=6, start_up_duration=0) + switch_2.power_on() + network.connect(endpoint_a=router.network_interface[2], endpoint_b=switch_2.network_interface[6]) + router.enable_port(2) + + # 1.3: Create and connect computer + client_1 = Computer( + hostname="client_1", + ip_address="10.0.1.2", + subnet_mask="255.255.255.0", + default_gateway="10.0.1.1", + start_up_duration=0, + ) + client_1.power_on() + client_1.software_manager.install(software_class=C2Server) + network.connect( + endpoint_a=client_1.network_interface[1], + endpoint_b=switch_1.network_interface[1], + ) + + client_2 = Computer( + hostname="client_2", + ip_address="10.0.1.3", + subnet_mask="255.255.255.0", + default_gateway="10.0.1.1", + start_up_duration=0, + ) + client_2.power_on() + client_2.software_manager.install(software_class=C2Beacon) + network.connect(endpoint_a=client_2.network_interface[1], endpoint_b=switch_2.network_interface[1]) + + # 1.4: Create and connect servers + server_1 = Server( + hostname="server_1", + ip_address="10.0.2.2", + subnet_mask="255.255.255.0", + default_gateway="10.0.2.1", + start_up_duration=0, + ) + server_1.power_on() + network.connect(endpoint_a=server_1.network_interface[1], endpoint_b=switch_2.network_interface[1]) + + server_2 = Server( + hostname="server_2", + ip_address="10.0.2.3", + subnet_mask="255.255.255.0", + default_gateway="10.0.2.1", + start_up_duration=0, + ) + server_2.power_on() + network.connect(endpoint_a=server_2.network_interface[1], endpoint_b=switch_2.network_interface[2]) + + return network + + +# TODO: Fix this test: Not sure why this isn't working + + +def test_c2_suite_acl_block(acl_network): + """Tests that C2 Beacon disconnects from the C2 Server after blocking ACL rules.""" + network: Network = acl_network + computer_a: Computer = network.get_node_by_hostname("client_1") + c2_server: C2Server = computer_a.software_manager.software.get("C2Server") + + computer_b: Computer = network.get_node_by_hostname("client_2") + c2_beacon: C2Beacon = computer_b.software_manager.software.get("C2Beacon") + + router: Router = network.get_node_by_hostname("router") + + network.apply_timestep(0) + # Initial config (#TODO: Make this a function) + c2_beacon.configure(c2_server_ip_address="10.0.1.2", keep_alive_frequency=2) + + c2_server.run() + c2_beacon.establish() + + assert c2_beacon.keep_alive_inactivity == 0 + assert c2_beacon.c2_connection_active == True + assert c2_server.c2_connection_active == True + + # Now we add a HTTP blocking acl (Thus preventing a keep alive) + router.acl.add_rule(action=ACLAction.DENY, src_port=Port.HTTP, dst_port=Port.HTTP, position=1) + + c2_beacon.apply_timestep(1) + c2_beacon.apply_timestep(2) + assert c2_beacon.keep_alive_inactivity == 2 + assert c2_beacon.c2_connection_active == False + assert c2_beacon.health_state_actual == ApplicationOperatingState.CLOSED + + +def test_c2_suite_launch_ransomware(basic_network): + """Tests that a red agent is able to launch ransomware via C2 Server Actions.""" From 9c68cd4bd0ef86fe3dc7a126613198f227a17b1e Mon Sep 17 00:00:00 2001 From: "Archer.Bowen" Date: Tue, 6 Aug 2024 17:05:00 +0100 Subject: [PATCH 13/46] #2689 Agent Actions Implemented, E2E Demo notebook started and a couple of general fixes and improvements. --- src/primaite/game/agent/actions.py | 101 +++++ src/primaite/game/game.py | 2 + .../Command-&-Control-E2E-Demonstration.ipynb | 367 ++++++++++++++++++ .../red_applications/c2/c2_beacon.py | 37 +- .../red_applications/c2/c2_server.py | 25 +- 5 files changed, 516 insertions(+), 16 deletions(-) create mode 100644 src/primaite/notebooks/Command-&-Control-E2E-Demonstration.ipynb diff --git a/src/primaite/game/agent/actions.py b/src/primaite/game/agent/actions.py index 9a5fedc9..d2752459 100644 --- a/src/primaite/game/agent/actions.py +++ b/src/primaite/game/agent/actions.py @@ -1071,6 +1071,103 @@ class NodeNetworkServiceReconAction(AbstractAction): ] +class ConfigureC2BeaconAction(AbstractAction): + """Action which configures a C2 Beacon based on the parameters given.""" + + class _Opts(BaseModel): + """Schema for options that can be passed to this action.""" + + c2_server_ip_address: str + keep_alive_frequency: int = Field(default=5, ge=1) + masquerade_protocol: str = Field(default="TCP") + masquerade_port: str = Field(default="HTTP") + + @field_validator( + "c2_server_ip_address", + "keep_alive_frequency", + "masquerade_protocol", + "masquerade_port", + mode="before", + ) + @classmethod + def not_none(cls, v: str, info: ValidationInfo) -> int: + """If None is passed, use the default value instead.""" + if v is None: + return cls.model_fields[info.field_name].default + return v + + def __init__(self, manager: "ActionManager", **kwargs) -> None: + super().__init__(manager=manager) + + def form_request(self, node_id: int, config: Dict) -> RequestFormat: + """Return the action formatted as a request that can be ingested by the simulation.""" + node_name = self.manager.get_node_name_by_idx(node_id) + if node_name is None: + return ["do_nothing"] + config = ConfigureC2BeaconAction._Opts( + c2_server_ip_address=config["c2_server_ip_address"], + keep_alive_frequency=config["keep_alive_frequency"], + masquerade_port=config["masquerade_protocol"], + masquerade_protocol=config["masquerade_port"], + ) + + ConfigureC2BeaconAction._Opts.model_validate(config) # check that options adhere to schema + + return ["network", "node", node_name, "application", "C2Beacon", "configure", config.__dict__] + + +class RansomwareConfigureC2ServerAction(AbstractAction): + """Action which sends a command from the C2 Server to the C2 Beacon which configures a local RansomwareScript.""" + + def __init__(self, manager: "ActionManager", **kwargs) -> None: + super().__init__(manager=manager) + + def form_request(self, node_id: int, config: Dict) -> RequestFormat: + """Return the action formatted as a request that can be ingested by the simulation.""" + node_name = self.manager.get_node_name_by_idx(node_id) + if node_name is None: + return ["do_nothing"] + # Using the ransomware scripts model to validate. + ConfigureRansomwareScriptAction._Opts.model_validate(config) # check that options adhere to schema + return ["network", "node", node_name, "application", "C2Server", "ransomware_configure", config] + + +class RansomwareLaunchC2ServerAction(AbstractAction): + """Action which causes the C2 Server to send a command to the C2 Beacon to launch the RansomwareScript.""" + + def __init__(self, manager: "ActionManager", **kwargs) -> None: + super().__init__(manager=manager) + + def form_request(self, node_id: int) -> RequestFormat: + """Return the action formatted as a request that can be ingested by the simulation.""" + node_name = self.manager.get_node_name_by_idx(node_id) + if node_name is None: + return ["do_nothing"] + # Not options needed for this action. + return ["network", "node", node_name, "application", "C2Server", "ransomware_launch"] + + +class TerminalC2ServerAction(AbstractAction): + """Action which causes the C2 Server to send a command to the C2 Beacon to execute the terminal command passed.""" + + class _Opts(BaseModel): + """Schema for options that can be passed to this action.""" + + model_config = ConfigDict(extra="forbid") + commands: RequestFormat + + def __init__(self, manager: "ActionManager", **kwargs) -> None: + super().__init__(manager=manager) + + def form_request(self, node_id: int, config: Dict) -> RequestFormat: + """Return the action formatted as a request that can be ingested by the simulation.""" + node_name = self.manager.get_node_name_by_idx(node_id) + if node_name is None: + return ["do_nothing"] + TerminalC2ServerAction._Opts.model_validate(config) # check that options adhere to schema + return ["network", "node", node_name, "application", "C2Server", "terminal_command", config] + + class ActionManager: """Class which manages the action space for an agent.""" @@ -1122,6 +1219,10 @@ class ActionManager: "CONFIGURE_DATABASE_CLIENT": ConfigureDatabaseClientAction, "CONFIGURE_RANSOMWARE_SCRIPT": ConfigureRansomwareScriptAction, "CONFIGURE_DOSBOT": ConfigureDoSBotAction, + "CONFIGURE_C2_BEACON": ConfigureC2BeaconAction, + "C2_SERVER_RANSOMWARE_LAUNCH": RansomwareLaunchC2ServerAction, + "C2_SERVER_RANSOMWARE_CONFIGURE": RansomwareConfigureC2ServerAction, + "C2_SERVER_TERMINAL_COMMAND": TerminalC2ServerAction, } """Dictionary which maps action type strings to the corresponding action class.""" diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index 5ef8c14c..831dab2b 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -31,6 +31,8 @@ from primaite.simulator.network.transmission.transport_layer import Port from primaite.simulator.sim_container import Simulation from primaite.simulator.system.applications.application import Application from primaite.simulator.system.applications.database_client import DatabaseClient # noqa: F401 +from primaite.simulator.system.applications.red_applications.c2.c2_beacon import C2Beacon # noqa: F401 +from primaite.simulator.system.applications.red_applications.c2.c2_server import C2Server # noqa: F401 from primaite.simulator.system.applications.red_applications.data_manipulation_bot import ( # noqa: F401 DataManipulationBot, ) diff --git a/src/primaite/notebooks/Command-&-Control-E2E-Demonstration.ipynb b/src/primaite/notebooks/Command-&-Control-E2E-Demonstration.ipynb new file mode 100644 index 00000000..60ea756d --- /dev/null +++ b/src/primaite/notebooks/Command-&-Control-E2E-Demonstration.ipynb @@ -0,0 +1,367 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Command and Control Application Suite E2E Demonstration\n", + "\n", + "© Crown-owned copyright 2024, Defence Science and Technology Laboratory UK\n", + "\n", + "This notebook demonstrates the current implementation of the command and control (C2) server and beacon applications in primAITE." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Imports\n", + "from primaite.config.load import data_manipulation_config_path\n", + "from primaite.session.environment import PrimaiteGymEnv\n", + "from primaite.game.agent.interface import AgentHistoryItem\n", + "import yaml\n", + "from pprint import pprint\n", + "from primaite.simulator.network.container import Network\n", + "from primaite.game.game import PrimaiteGame\n", + "from primaite.simulator.system.applications.red_applications.c2.c2_beacon import C2Beacon\n", + "from primaite.simulator.system.applications.red_applications.c2.c2_server import C2Server\n", + "from primaite.simulator.system.applications.red_applications.c2.abstract_c2 import C2Command, C2Payload\n", + "from primaite.simulator.system.applications.red_applications.ransomware_script import RansomwareScript\n", + "from primaite.simulator.network.hardware.nodes.host.computer import Computer\n", + "from primaite.simulator.network.hardware.nodes.host.server import Server" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## **Notebook Setup** | **Network Configuration:**\n", + "\n", + "This notebook uses the same network setup as UC2. Please refer to the main [UC2-E2E-Demo notebook for further reference](./Data-Manipulation-E2E-Demonstration.ipynb).\n", + "\n", + "However, this notebook will replaces with the red agent used in UC2 with a custom proxy red agent built for this notebook." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "custom_c2_agent = \"\"\"\n", + " - ref: CustomC2Agent\n", + " team: RED\n", + " type: ProxyAgent\n", + " observation_space: null\n", + " action_space:\n", + " action_list:\n", + " - type: DONOTHING\n", + " - type: NODE_APPLICATION_INSTALL\n", + " - type: NODE_APPLICATION_EXECUTE\n", + " - type: CONFIGURE_C2_BEACON\n", + " - type: C2_SERVER_RANSOMWARE_LAUNCH\n", + " - type: C2_SERVER_RANSOMWARE_CONFIGURE\n", + " - type: C2_SERVER_TERMINAL_COMMAND\n", + " options:\n", + " nodes:\n", + " - node_name: client_1\n", + " applications: \n", + " - application_name: C2Beacon\n", + " - node_name: domain_controller\n", + " applications: \n", + " - application_name: C2Server\n", + " max_folders_per_node: 1\n", + " max_files_per_folder: 1\n", + " max_services_per_node: 2\n", + " max_nics_per_node: 8\n", + " max_acl_rules: 10\n", + " ip_list:\n", + " - 192.168.1.10\n", + " - 192.168.1.14\n", + " action_map:\n", + " 0:\n", + " action: DONOTHING\n", + " options: {}\n", + " 1:\n", + " action: NODE_APPLICATION_INSTALL\n", + " options:\n", + " node_id: 0\n", + " application_name: C2Beacon\n", + " 2:\n", + " action: CONFIGURE_C2_BEACON\n", + " options:\n", + " node_id: 0\n", + " config:\n", + " c2_server_ip_address: 192.168.1.10\n", + " keep_alive_frequency:\n", + " masquerade_protocol:\n", + " masquerade_port:\n", + " 3:\n", + " action: NODE_APPLICATION_EXECUTE\n", + " options:\n", + " node_id: 0\n", + " application_id: 0 \n", + " 4:\n", + " action: NODE_APPLICATION_INSTALL\n", + " options:\n", + " node_id: 0\n", + " application_name: RansomwareScript \n", + " 5:\n", + " action: C2_SERVER_RANSOMWARE_CONFIGURE\n", + " options:\n", + " node_id: 1\n", + " config:\n", + " server_ip_address: 192.168.1.14\n", + " payload: ENCRYPT\n", + " 6:\n", + " action: C2_SERVER_RANSOMWARE_LAUNCH\n", + " options:\n", + " node_id: 1\n", + " 7:\n", + " action: C2_SERVER_TERMINAL_COMMAND\n", + " options:\n", + " node_id: 1\n", + " application_id: 0 \n", + "\n", + " reward_function:\n", + " reward_components:\n", + " - type: DUMMY\n", + "\"\"\"\n", + "c2_agent_yaml = yaml.safe_load(custom_c2_agent)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "with open(data_manipulation_config_path()) as f:\n", + " cfg = yaml.safe_load(f)\n", + " # removing all agents & adding the custom agent.\n", + " cfg['agents'] = {}\n", + " cfg['agents'] = c2_agent_yaml\n", + " \n", + "\n", + "env = PrimaiteGymEnv(env_config=cfg)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## **Notebook Setup** | Network Prerequisites\n", + "\n", + "Before the Red Agent is able to perform any C2 specific actions, the C2 Server needs to be installed and run before the episode begins.\n", + "\n", + "This is because higher fidelity environments (and the real-world) a C2 server would not be accessible by private network blue agent and the C2 Server would already be in place before the an adversary (Red Agent) before the narrative of the use case.\n", + "\n", + "The cells below installs and runs the C2 Server on the domain controller server directly via the simulation API." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "domain_controller: Server = env.game.simulation.network.get_node_by_hostname(\"domain_controller\")\n", + "domain_controller.software_manager.install(C2Server)\n", + "c2_server: C2Server = domain_controller.software_manager.software[\"C2Server\"]\n", + "c2_server.run()\n", + "domain_controller.software_manager.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## **Command and Control** | C2 Beacon Actions\n", + "\n", + "Before the Red Agent is able to perform any C2 Server commands, it must first establish connection with a C2 beacon.\n", + "\n", + "This can be done by installing, configuring and then executing a C2 Beacon. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "c2_red_agent = env.game.agents[\"CustomC2Agent\"]\n", + "client_1: Computer = env.game.simulation.network.get_node_by_hostname(\"client_1\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### **Command and Control** | C2 Beacon Actions | Installation" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "env.step(1)\n", + "client_1.software_manager.show()\n", + "c2_beacon: C2Beacon = client_1.software_manager.software[\"C2Beacon\"]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### **Command and Control** | C2 Beacon Actions | Configuration" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "env.step(2)\n", + "c2_beacon.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### **Command and Control** | C2 Beacon Actions | Establishing Connection" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "env.step(3)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "c2_beacon.show()\n", + "c2_server.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## **Command and Control** | C2 Server Actions" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### **Command and Control** | C2 Server Actions | Configuring Ransomware" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "env.step(4)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "env.step(5)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "ransomware_script: RansomwareScript = client_1.software_manager.software[\"RansomwareScript\"]\n", + "client_1.software_manager.show()\n", + "ransomware_script.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### **Command and Control** | C2 Server Actions | Launching Ransomware" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "env.step(6)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "database_server: Server = env.game.simulation.network.get_node_by_hostname(\"database_server\")\n", + "database_server.software_manager.file_system.show(full=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### **Command and Control** | C2 Server Actions | Executing Terminal Commands" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# TODO: Post Terminal.\n", + "#env.step(7)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.8" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} 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 16420164..c73799da 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 @@ -5,6 +5,7 @@ from typing import Dict, Optional # from primaite.simulator.system.services.terminal.terminal import Terminal from prettytable import MARKDOWN, PrettyTable +from pydantic import validate_call from primaite.interface.request import RequestFormat, RequestResponse from primaite.simulator.core import RequestManager, RequestType @@ -17,7 +18,7 @@ from primaite.simulator.system.applications.red_applications.ransomware_script i from primaite.simulator.system.software import SoftwareHealthState -class C2Beacon(AbstractC2, identifier="C2 Beacon"): +class C2Beacon(AbstractC2, identifier="C2Beacon"): """ C2 Beacon Application. @@ -94,16 +95,16 @@ class C2Beacon(AbstractC2, identifier="C2 Beacon"): ) c2_remote_ip = IPv4Address(c2_remote_ip) - # TODO: validation. frequency = request[-1].get("keep_alive_frequency") protocol = request[-1].get("masquerade_protocol") port = request[-1].get("masquerade_port") + return RequestResponse.from_bool( self.configure( c2_server_ip_address=c2_remote_ip, keep_alive_frequency=frequency, - masquerade_protocol=protocol, - masquerade_port=port, + masquerade_protocol=IPProtocol[protocol], + masquerade_port=Port[port], ) ) @@ -114,12 +115,13 @@ class C2Beacon(AbstractC2, identifier="C2 Beacon"): kwargs["name"] = "C2Beacon" super().__init__(**kwargs) + @validate_call def configure( self, c2_server_ip_address: IPv4Address = None, - keep_alive_frequency: Optional[int] = 5, - masquerade_protocol: Optional[Enum] = IPProtocol.TCP, - masquerade_port: Optional[Enum] = Port.HTTP, + keep_alive_frequency: int = 5, + masquerade_protocol: Enum = IPProtocol.TCP, + masquerade_port: Enum = Port.HTTP, ) -> bool: """ Configures the C2 beacon to communicate with the C2 server with following additional parameters. @@ -278,8 +280,7 @@ class C2Beacon(AbstractC2, identifier="C2 Beacon"): :return: Returns the Request Response returned by the Terminal execute method. :rtype: Request Response """ - # TODO: replace and use terminal - return RequestResponse(status="success", data={"Reason": "Placeholder."}) + return RequestResponse.from_bool(self._host_ransomware_script.attack()) def _command_terminal(self, payload: MasqueradePacket) -> RequestResponse: """ @@ -295,7 +296,6 @@ class C2Beacon(AbstractC2, identifier="C2 Beacon"): """ # TODO: uncomment and replace (uses terminal) return RequestResponse(status="success", data={"Reason": "Placeholder."}) - # return self._host_terminal.execute(command) def _handle_keep_alive(self, payload: MasqueradePacket, session_id: Optional[str]) -> bool: """ @@ -421,10 +421,23 @@ class C2Beacon(AbstractC2, identifier="C2 Beacon"): ``Keep Alive Frequency``: How often should the C2 Beacon attempt a keep alive? + ``Current Masquerade Protocol``: + The current protocol that the C2 Traffic is using. (e.g TCP/UDP) + + ``Current Masquerade Port``: + The current port that the C2 Traffic is using. (e.g HTTP (Port 80)) + :param markdown: If True, outputs the table in markdown format. Default is False. """ table = PrettyTable( - ["C2 Connection Active", "C2 Remote Connection", "Keep Alive Inactivity", "Keep Alive Frequency"] + [ + "C2 Connection Active", + "C2 Remote Connection", + "Keep Alive Inactivity", + "Keep Alive Frequency", + "Current Masquerade Protocol", + "Current Masquerade Port", + ] ) if markdown: table.set_style(MARKDOWN) @@ -436,6 +449,8 @@ class C2Beacon(AbstractC2, identifier="C2 Beacon"): self.c2_remote_connection, self.keep_alive_inactivity, self.keep_alive_frequency, + self.current_masquerade_protocol, + self.current_masquerade_port, ] ) print(table) diff --git a/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py b/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py index d01cd412..c381403e 100644 --- a/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py +++ b/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py @@ -10,7 +10,7 @@ from primaite.simulator.network.protocols.masquerade import MasqueradePacket from primaite.simulator.system.applications.red_applications.c2.abstract_c2 import AbstractC2, C2Command, C2Payload -class C2Server(AbstractC2, identifier="C2 Server"): +class C2Server(AbstractC2, identifier="C2Server"): """ C2 Server Application. @@ -74,8 +74,8 @@ class C2Server(AbstractC2, identifier="C2 Server"): :rtype: RequestResponse """ # TODO: Parse the parameters from the request to get the parameters - placeholder: dict = {} - return self._send_command(given_command=C2Command.TERMINAL, command_options=placeholder) + terminal_commands = {"commands": request[-1].get("commands")} + return self._send_command(given_command=C2Command.TERMINAL, command_options=terminal_commands) rm.add_request( name="ransomware_configure", @@ -250,14 +250,29 @@ class C2Server(AbstractC2, identifier="C2 Server"): ``C2 Remote Connection``: The IP of the C2 Beacon. (Configured by upon receiving a keep alive.) + ``Current Masquerade Protocol``: + The current protocol that the C2 Traffic is using. (e.g TCP/UDP) + + ``Current Masquerade Port``: + The current port that the C2 Traffic is using. (e.g HTTP (Port 80)) + :param markdown: If True, outputs the table in markdown format. Default is False. """ - table = PrettyTable(["C2 Connection Active", "C2 Remote Connection"]) + table = PrettyTable( + ["C2 Connection Active", "C2 Remote Connection", "Current Masquerade Protocol", "Current Masquerade Port"] + ) if markdown: table.set_style(MARKDOWN) table.align = "l" table.title = f"{self.name} Running Status" - table.add_row([self.c2_connection_active, self.c2_remote_connection]) + table.add_row( + [ + self.c2_connection_active, + self.c2_remote_connection, + self.current_masquerade_protocol, + self.current_masquerade_port, + ] + ) print(table) # Abstract method inherited from abstract C2 - Not currently utilised. From afa4d2b946ea479efc80be10362a52881f5f939d Mon Sep 17 00:00:00 2001 From: "Archer.Bowen" Date: Wed, 7 Aug 2024 10:34:30 +0100 Subject: [PATCH 14/46] #2689 Address a couple of TODOs and other misc changes. --- .../Command-&-Control-E2E-Demonstration.ipynb | 33 ++++- .../red_applications/c2/c2_beacon.py | 26 ++-- .../red_applications/c2/c2_server.py | 3 +- .../_red_applications/test_c2_suite.py | 136 +++++------------- 4 files changed, 84 insertions(+), 114 deletions(-) diff --git a/src/primaite/notebooks/Command-&-Control-E2E-Demonstration.ipynb b/src/primaite/notebooks/Command-&-Control-E2E-Demonstration.ipynb index 60ea756d..1df85bb6 100644 --- a/src/primaite/notebooks/Command-&-Control-E2E-Demonstration.ipynb +++ b/src/primaite/notebooks/Command-&-Control-E2E-Demonstration.ipynb @@ -180,7 +180,7 @@ "source": [ "## **Command and Control** | C2 Beacon Actions\n", "\n", - "Before the Red Agent is able to perform any C2 Server commands, it must first establish connection with a C2 beacon.\n", + "Before any C2 Server commands is able to accept any commands, it must first establish connection with a C2 beacon.\n", "\n", "This can be done by installing, configuring and then executing a C2 Beacon. " ] @@ -341,6 +341,37 @@ "# TODO: Post Terminal.\n", "#env.step(7)" ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## **Command and Control** | Blue Agent Relevance" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### **Command and Control** | Blue Agent Relevance | Observation Space" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### **Command and Control** | Blue Agent Relevance | Action Space" + ] } ], "metadata": { 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 c73799da..6dd1a873 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 @@ -184,8 +184,6 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): :return: The Request Response provided by the terminal execute method. :rtype Request Response: """ - # TODO: Probably could refactor this to be a more clean. - # The elif's are a bit ugly when they are all calling the same method. command = payload.command if not isinstance(command, C2Command): self.sys_log.warning(f"{self.name}: Received unexpected C2 command. Unable to resolve command") @@ -253,19 +251,26 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): """ 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. + Calls the locally installed RansomwareScript application's configure method + and passes the given parameters. + + The class attribute self._host_ransomware_script will return None if the host + does not have an instance of the RansomwareScript. :payload MasqueradePacket: The incoming INPUT command. :type Masquerade Packet: MasqueradePacket. :return: Returns the Request Response returned by the Terminal execute method. :rtype: Request Response """ - # TODO: replace and use terminal - # return RequestResponse(status="success", data={"Reason": "Placeholder."}) given_config = payload.payload - host_ransomware = self._host_ransomware_script - return RequestResponse.from_bool(host_ransomware.configure(server_ip_address=given_config["server_ip_address"])) + if self._host_ransomware_script is None: + return RequestResponse( + status="failure", + data={"Reason": "Cannot find any instances of a RansomwareScript. Have you installed one?"}, + ) + return RequestResponse.from_bool( + self._host_ransomware_script.configure(server_ip_address=given_config["server_ip_address"]) + ) def _command_ransomware_launch(self, payload: MasqueradePacket) -> RequestResponse: """ @@ -280,6 +285,11 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): :return: Returns the Request Response returned by the Terminal execute method. :rtype: Request Response """ + if self._host_ransomware_script is None: + return RequestResponse( + status="failure", + data={"Reason": "Cannot find any instances of a RansomwareScript. Have you installed one?"}, + ) return RequestResponse.from_bool(self._host_ransomware_script.attack()) def _command_terminal(self, payload: MasqueradePacket) -> RequestResponse: diff --git a/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py b/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py index c381403e..85009cec 100644 --- a/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py +++ b/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py @@ -219,7 +219,7 @@ class C2Server(AbstractC2, identifier="C2Server"): """ Creates and returns a Masquerade Packet using the arguments given. - Creates Masquerade Packet with a payload_type INPUT C2Payload + Creates Masquerade Packet with a payload_type INPUT C2Payload. :param given_command: The C2 command to be sent to the C2 Beacon. :type given_command: C2Command. @@ -228,7 +228,6 @@ class C2Server(AbstractC2, identifier="C2Server"): :return: Returns the construct MasqueradePacket :rtype: MasqueradePacket """ - # TODO: Validation on command_options. constructed_packet = MasqueradePacket( masquerade_protocol=self.current_masquerade_protocol, masquerade_port=self.current_masquerade_port, diff --git a/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_c2_suite.py b/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_c2_suite.py index 064ef57d..7e4df4f1 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_c2_suite.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_c2_suite.py @@ -21,40 +21,44 @@ from primaite.simulator.system.services.dns.dns_server import DNSServer from primaite.simulator.system.services.web_server.web_server import WebServer -@pytest.fixture(scope="function") -def c2_server_on_computer() -> Tuple[C2Beacon, Computer]: - computer: Computer = Computer( - hostname="node_a", ip_address="192.168.0.10", subnet_mask="255.255.255.0", start_up_duration=0 - ) - computer.power_on() - c2_beacon = computer.software_manager.software.get("C2Beacon") - - return [c2_beacon, computer] - - -@pytest.fixture(scope="function") -def c2_server_on_computer() -> Tuple[C2Server, Computer]: - computer: Computer = Computer( - hostname="node_b", ip_address="192.168.0.11", subnet_mask="255.255.255.0", start_up_duration=0 - ) - computer.power_on() - c2_server = computer.software_manager.software.get("C2Server") - - return [c2_server, computer] - - @pytest.fixture(scope="function") def basic_network() -> Network: network = Network() - node_a = Computer(hostname="node_a", ip_address="192.168.0.10", subnet_mask="255.255.255.0", start_up_duration=0) + + # Creating two generic nodes for the C2 Server and the C2 Beacon. + node_a = Computer(hostname="node_a", ip_address="192.168.0.2", subnet_mask="255.255.255.252", start_up_duration=0) node_a.power_on() node_a.software_manager.get_open_ports() node_a.software_manager.install(software_class=C2Server) - node_b = Computer(hostname="node_b", ip_address="192.168.0.11", subnet_mask="255.255.255.0", start_up_duration=0) + node_b = Computer(hostname="node_b", ip_address="192.168.255.2", subnet_mask="255.255.255.252", start_up_duration=0) node_b.software_manager.install(software_class=C2Beacon) node_b.power_on() - network.connect(node_a.network_interface[1], node_b.network_interface[1]) + + # Creating a router to sit between node 1 and node 2. + router = Router(hostname="router", num_ports=3, start_up_duration=0) + router.power_on() + router.configure_port(port=1, ip_address="192.168.0.1", subnet_mask="255.255.255.252") + router.configure_port(port=2, ip_address="192.168.255.1", subnet_mask="255.255.255.252") + + # Creating switches for each client. + switch_1 = Switch(hostname="switch_1", num_ports=6, start_up_duration=0) + switch_1.power_on() + + # Connecting the switches to the router. + network.connect(endpoint_a=router.network_interface[1], endpoint_b=switch_1.network_interface[6]) + router.enable_port(1) + + switch_2 = Switch(hostname="switch_2", num_ports=6, start_up_duration=0) + switch_2.power_on() + + network.connect(endpoint_a=router.network_interface[2], endpoint_b=switch_2.network_interface[6]) + router.enable_port(2) + + # Connecting the node to each switch + network.connect(node_a.network_interface[1], switch_1.network_interface[1]) + + network.connect(node_b.network_interface[1], switch_2.network_interface[1]) return network @@ -68,9 +72,10 @@ def test_c2_suite_setup_receive(basic_network): computer_b: Computer = network.get_node_by_hostname("node_b") c2_beacon: C2Beacon = computer_b.software_manager.software.get("C2Beacon") + computer_a.ping("192.168.255.1") # Assert that the c2 beacon configure correctly. - c2_beacon.configure(c2_server_ip_address="192.168.0.10") - assert c2_beacon.c2_remote_connection == IPv4Address("192.168.0.10") + c2_beacon.configure(c2_server_ip_address="192.168.0.2") + assert c2_beacon.c2_remote_connection == IPv4Address("192.168.0.2") c2_server.run() c2_beacon.establish() @@ -80,7 +85,7 @@ def test_c2_suite_setup_receive(basic_network): # Asserting that the c2 server has established a c2 connection. assert c2_server.c2_connection_active is True - assert c2_server.c2_remote_connection == IPv4Address("192.168.0.11") + assert c2_server.c2_remote_connection == IPv4Address("192.168.255.2") def test_c2_suite_keep_alive_inactivity(basic_network): @@ -177,81 +182,6 @@ def test_c2_suite_terminal(basic_network): """Tests that a red agent is able to execute terminal commands via C2 Server Actions.""" -@pytest.fixture(scope="function") -def acl_network() -> Network: - # 0: Pull out the network - network = Network() - - # 1: Set up network hardware - # 1.1: Configure the router - router = Router(hostname="router", num_ports=3, start_up_duration=0) - router.power_on() - router.configure_port(port=1, ip_address="10.0.1.1", subnet_mask="255.255.255.0") - router.configure_port(port=2, ip_address="10.0.2.1", subnet_mask="255.255.255.0") - - # 1.2: Create and connect switches - switch_1 = Switch(hostname="switch_1", num_ports=6, start_up_duration=0) - switch_1.power_on() - network.connect(endpoint_a=router.network_interface[1], endpoint_b=switch_1.network_interface[6]) - router.enable_port(1) - switch_2 = Switch(hostname="switch_2", num_ports=6, start_up_duration=0) - switch_2.power_on() - network.connect(endpoint_a=router.network_interface[2], endpoint_b=switch_2.network_interface[6]) - router.enable_port(2) - - # 1.3: Create and connect computer - client_1 = Computer( - hostname="client_1", - ip_address="10.0.1.2", - subnet_mask="255.255.255.0", - default_gateway="10.0.1.1", - start_up_duration=0, - ) - client_1.power_on() - client_1.software_manager.install(software_class=C2Server) - network.connect( - endpoint_a=client_1.network_interface[1], - endpoint_b=switch_1.network_interface[1], - ) - - client_2 = Computer( - hostname="client_2", - ip_address="10.0.1.3", - subnet_mask="255.255.255.0", - default_gateway="10.0.1.1", - start_up_duration=0, - ) - client_2.power_on() - client_2.software_manager.install(software_class=C2Beacon) - network.connect(endpoint_a=client_2.network_interface[1], endpoint_b=switch_2.network_interface[1]) - - # 1.4: Create and connect servers - server_1 = Server( - hostname="server_1", - ip_address="10.0.2.2", - subnet_mask="255.255.255.0", - default_gateway="10.0.2.1", - start_up_duration=0, - ) - server_1.power_on() - network.connect(endpoint_a=server_1.network_interface[1], endpoint_b=switch_2.network_interface[1]) - - server_2 = Server( - hostname="server_2", - ip_address="10.0.2.3", - subnet_mask="255.255.255.0", - default_gateway="10.0.2.1", - start_up_duration=0, - ) - server_2.power_on() - network.connect(endpoint_a=server_2.network_interface[1], endpoint_b=switch_2.network_interface[2]) - - return network - - -# TODO: Fix this test: Not sure why this isn't working - - def test_c2_suite_acl_block(acl_network): """Tests that C2 Beacon disconnects from the C2 Server after blocking ACL rules.""" network: Network = acl_network From b1baf023d64f44e399706e0f5402e9ad798c4de0 Mon Sep 17 00:00:00 2001 From: Archer Bowen Date: Wed, 7 Aug 2024 14:16:50 +0100 Subject: [PATCH 15/46] #2689 Fixed up Pytests and confirmed functionality before merging from dev. --- .../Command-&-Control-E2E-Demonstration.ipynb | 2 +- .../red_applications/c2/c2_beacon.py | 1 + .../integration_tests/network/test_routing.py | 18 +-- .../system/red_applications}/test_c2_suite.py | 114 ++++++++++-------- 4 files changed, 77 insertions(+), 58 deletions(-) rename tests/{unit_tests/_primaite/_simulator/_system/_applications/_red_applications => integration_tests/system/red_applications}/test_c2_suite.py (72%) diff --git a/src/primaite/notebooks/Command-&-Control-E2E-Demonstration.ipynb b/src/primaite/notebooks/Command-&-Control-E2E-Demonstration.ipynb index 1df85bb6..0810871b 100644 --- a/src/primaite/notebooks/Command-&-Control-E2E-Demonstration.ipynb +++ b/src/primaite/notebooks/Command-&-Control-E2E-Demonstration.ipynb @@ -390,7 +390,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.8" + "version": "3.10.12" } }, "nbformat": 4, 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 6dd1a873..1dde28a2 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 @@ -380,6 +380,7 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): self.keep_alive_inactivity += 1 if not self._check_c2_connection(timestep): self.sys_log.error(f"{self.name}: Connection Severed - Application Closing.") + self.c2_connection_active = False self.clear_connections() # TODO: Shouldn't this close() method also set the health state to 'UNUSED'? self.close() diff --git a/tests/integration_tests/network/test_routing.py b/tests/integration_tests/network/test_routing.py index 62b58cbd..5f9e03ef 100644 --- a/tests/integration_tests/network/test_routing.py +++ b/tests/integration_tests/network/test_routing.py @@ -33,18 +33,18 @@ def pc_a_pc_b_router_1() -> Tuple[Computer, Computer, Router]: ) pc_b.power_on() - router_1 = Router(hostname="router_1", start_up_duration=0) - router_1.power_on() + router = Router(hostname="router", start_up_duration=0) + router.power_on() - router_1.configure_port(1, "192.168.0.1", "255.255.255.0") - router_1.configure_port(2, "192.168.1.1", "255.255.255.0") + router.configure_port(1, "192.168.0.1", "255.255.255.0") + router.configure_port(2, "192.168.1.1", "255.255.255.0") - network.connect(endpoint_a=pc_a.network_interface[1], endpoint_b=router_1.network_interface[1]) - network.connect(endpoint_a=pc_b.network_interface[1], endpoint_b=router_1.network_interface[2]) - router_1.enable_port(1) - router_1.enable_port(2) + network.connect(endpoint_a=pc_a.network_interface[1], endpoint_b=router.network_interface[1]) + network.connect(endpoint_a=pc_b.network_interface[1], endpoint_b=router.network_interface[2]) + router.enable_port(1) + router.enable_port(2) - return pc_a, pc_b, router_1 + return pc_a, pc_b, router @pytest.fixture(scope="function") diff --git a/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_c2_suite.py b/tests/integration_tests/system/red_applications/test_c2_suite.py similarity index 72% rename from tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_c2_suite.py rename to tests/integration_tests/system/red_applications/test_c2_suite.py index 7e4df4f1..9d66f3c1 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_c2_suite.py +++ b/tests/integration_tests/system/red_applications/test_c2_suite.py @@ -6,6 +6,7 @@ import pytest from primaite.game.agent.interface import ProxyAgent from primaite.game.game import PrimaiteGame +from primaite.simulator.file_system.file_system_item_abc import FileSystemItemHealthStatus from primaite.simulator.network.container import Network from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.network.hardware.nodes.host.server import Server @@ -14,9 +15,11 @@ from primaite.simulator.network.hardware.nodes.network.switch import Switch from primaite.simulator.network.transmission.network_layer import IPProtocol from primaite.simulator.network.transmission.transport_layer import Port from primaite.simulator.system.applications.application import ApplicationOperatingState +from primaite.simulator.system.applications.database_client import DatabaseClient from primaite.simulator.system.applications.red_applications.c2.c2_beacon import C2Beacon from primaite.simulator.system.applications.red_applications.c2.c2_server import C2Server from primaite.simulator.system.applications.red_applications.ransomware_script import RansomwareScript +from primaite.simulator.system.services.database.database_service import DatabaseService from primaite.simulator.system.services.dns.dns_server import DNSServer from primaite.simulator.system.services.web_server.web_server import WebServer @@ -26,33 +29,46 @@ def basic_network() -> Network: network = Network() # Creating two generic nodes for the C2 Server and the C2 Beacon. - node_a = Computer(hostname="node_a", ip_address="192.168.0.2", subnet_mask="255.255.255.252", start_up_duration=0) + node_a = Computer( + hostname="node_a", + ip_address="192.168.0.2", + subnet_mask="255.255.255.252", + default_gateway="192.168.0.1", + start_up_duration=0, + ) node_a.power_on() node_a.software_manager.get_open_ports() node_a.software_manager.install(software_class=C2Server) - node_b = Computer(hostname="node_b", ip_address="192.168.255.2", subnet_mask="255.255.255.252", start_up_duration=0) - node_b.software_manager.install(software_class=C2Beacon) + node_b = Computer( + hostname="node_b", + ip_address="192.168.255.2", + subnet_mask="255.255.255.252", + default_gateway="192.168.255.1", + start_up_duration=0, + ) node_b.power_on() - + node_b.software_manager.install(software_class=C2Beacon) # Creating a router to sit between node 1 and node 2. router = Router(hostname="router", num_ports=3, start_up_duration=0) + # Default allow all. + router.acl.add_rule(action=ACLAction.PERMIT) router.power_on() - router.configure_port(port=1, ip_address="192.168.0.1", subnet_mask="255.255.255.252") - router.configure_port(port=2, ip_address="192.168.255.1", subnet_mask="255.255.255.252") - # Creating switches for each client. switch_1 = Switch(hostname="switch_1", num_ports=6, start_up_duration=0) switch_1.power_on() # Connecting the switches to the router. + router.configure_port(port=1, ip_address="192.168.0.1", subnet_mask="255.255.255.252") network.connect(endpoint_a=router.network_interface[1], endpoint_b=switch_1.network_interface[6]) - router.enable_port(1) switch_2 = Switch(hostname="switch_2", num_ports=6, start_up_duration=0) switch_2.power_on() network.connect(endpoint_a=router.network_interface[2], endpoint_b=switch_2.network_interface[6]) + router.configure_port(port=2, ip_address="192.168.255.1", subnet_mask="255.255.255.252") + + router.enable_port(1) router.enable_port(2) # Connecting the node to each switch @@ -72,7 +88,6 @@ def test_c2_suite_setup_receive(basic_network): computer_b: Computer = network.get_node_by_hostname("node_b") c2_beacon: C2Beacon = computer_b.software_manager.software.get("C2Beacon") - computer_a.ping("192.168.255.1") # Assert that the c2 beacon configure correctly. c2_beacon.configure(c2_server_ip_address="192.168.0.2") assert c2_beacon.c2_remote_connection == IPv4Address("192.168.0.2") @@ -97,8 +112,7 @@ def test_c2_suite_keep_alive_inactivity(basic_network): computer_b: Computer = network.get_node_by_hostname("node_b") c2_beacon: C2Beacon = computer_b.software_manager.software.get("C2Beacon") - # Initial config (#TODO: Make this a function) - c2_beacon.configure(c2_server_ip_address="192.168.0.10", keep_alive_frequency=2) + c2_beacon.configure(c2_server_ip_address="192.168.0.2", keep_alive_frequency=2) c2_server.run() c2_beacon.establish() @@ -116,12 +130,11 @@ def test_c2_suite_keep_alive_inactivity(basic_network): c2_beacon.apply_timestep(3) assert c2_beacon.keep_alive_inactivity == 2 assert c2_beacon.c2_connection_active == False - assert c2_beacon.health_state_actual == ApplicationOperatingState.CLOSED + assert c2_beacon.operating_state == ApplicationOperatingState.CLOSED -# TODO: Flesh out these tests. -def test_c2_suite_configure_via_actions(basic_network): - """Tests that a red agent is able to configure the c2 beacon and c2 server via Actions.""" +def test_c2_suite_configure_request(basic_network): + """Tests that the request system can be used to successfully setup a c2 suite.""" # Setting up the network: network: Network = basic_network computer_a: Computer = network.get_node_by_hostname("node_a") @@ -131,88 +144,93 @@ def test_c2_suite_configure_via_actions(basic_network): c2_beacon: C2Beacon = computer_b.software_manager.software.get("C2Beacon") # Testing Via Requests: - network.apply_request(["node", "node_a", "application", "C2Server", "run"]) + c2_server.run() + network.apply_timestep(0) c2_beacon_config = { - "c2_server_ip_address": "192.168.0.10", + "c2_server_ip_address": "192.168.0.2", "keep_alive_frequency": 5, - "masquerade_protocol": IPProtocol.TCP, - "masquerade_port": Port.HTTP, + "masquerade_protocol": "TCP", + "masquerade_port": "HTTP", } network.apply_request(["node", "node_b", "application", "C2Beacon", "configure", c2_beacon_config]) + network.apply_timestep(0) network.apply_request(["node", "node_b", "application", "C2Beacon", "execute"]) assert c2_beacon.c2_connection_active is True assert c2_server.c2_connection_active is True - assert c2_server.c2_remote_connection == IPv4Address("192.168.0.11") - - # Testing Via Agents: - # TODO: + assert c2_server.c2_remote_connection == IPv4Address("192.168.255.2") -def test_c2_suite_configure_ransomware(basic_network): - """Tests that a red agent is able to configure ransomware via C2 Server Actions.""" +def test_c2_suite_ransomware_commands(basic_network): + """Tests the Ransomware commands can be used to configure & launch ransomware via Requests.""" # Setting up the network: network: Network = basic_network computer_a: Computer = network.get_node_by_hostname("node_a") c2_server: C2Server = computer_a.software_manager.software.get("C2Server") + computer_a.software_manager.install(DatabaseService) + computer_a.software_manager.software["DatabaseService"].start() computer_b: Computer = network.get_node_by_hostname("node_b") c2_beacon: C2Beacon = computer_b.software_manager.software.get("C2Beacon") + computer_b.software_manager.install(DatabaseClient) + computer_b.software_manager.software["DatabaseClient"].configure(server_ip_address=IPv4Address("192.168.0.2")) + computer_b.software_manager.software["DatabaseClient"].run() - c2_beacon.configure(c2_server_ip_address="192.168.0.10", keep_alive_frequency=2) + c2_beacon.configure(c2_server_ip_address="192.168.0.2", keep_alive_frequency=2) c2_server.run() c2_beacon.establish() # Testing Via Requests: computer_b.software_manager.install(software_class=RansomwareScript) - ransomware_config = {"server_ip_address": "1.1.1.1"} + ransomware_config = {"server_ip_address": "192.168.0.2"} network.apply_request(["node", "node_a", "application", "C2Server", "ransomware_configure", ransomware_config]) ransomware_script: RansomwareScript = computer_b.software_manager.software["RansomwareScript"] - assert ransomware_script.server_ip_address == "1.1.1.1" + assert ransomware_script.server_ip_address == "192.168.0.2" - # Testing Via Agents: - # TODO: + network.apply_request(["node", "node_a", "application", "C2Server", "ransomware_launch"]) + + database_file = computer_a.software_manager.file_system.get_file("database", "database.db") + + assert database_file.health_status == FileSystemItemHealthStatus.CORRUPT -def test_c2_suite_terminal(basic_network): - """Tests that a red agent is able to execute terminal commands via C2 Server Actions.""" - - -def test_c2_suite_acl_block(acl_network): +def test_c2_suite_acl_block(basic_network): """Tests that C2 Beacon disconnects from the C2 Server after blocking ACL rules.""" - network: Network = acl_network - computer_a: Computer = network.get_node_by_hostname("client_1") + + network: Network = basic_network + computer_a: Computer = network.get_node_by_hostname("node_a") c2_server: C2Server = computer_a.software_manager.software.get("C2Server") - computer_b: Computer = network.get_node_by_hostname("client_2") + computer_b: Computer = network.get_node_by_hostname("node_b") c2_beacon: C2Beacon = computer_b.software_manager.software.get("C2Beacon") router: Router = network.get_node_by_hostname("router") - network.apply_timestep(0) - # Initial config (#TODO: Make this a function) - c2_beacon.configure(c2_server_ip_address="10.0.1.2", keep_alive_frequency=2) - + c2_beacon.configure(c2_server_ip_address="192.168.0.2", keep_alive_frequency=2) c2_server.run() c2_beacon.establish() + c2_beacon.apply_timestep(0) + assert c2_beacon.keep_alive_inactivity == 1 + + # Keep Alive successfully sent and received upon the 2nd timestep. + c2_beacon.apply_timestep(1) assert c2_beacon.keep_alive_inactivity == 0 assert c2_beacon.c2_connection_active == True - assert c2_server.c2_connection_active == True # Now we add a HTTP blocking acl (Thus preventing a keep alive) - router.acl.add_rule(action=ACLAction.DENY, src_port=Port.HTTP, dst_port=Port.HTTP, position=1) + router.acl.add_rule(action=ACLAction.DENY, src_port=Port.HTTP, dst_port=Port.HTTP, position=0) - c2_beacon.apply_timestep(1) c2_beacon.apply_timestep(2) + c2_beacon.apply_timestep(3) assert c2_beacon.keep_alive_inactivity == 2 assert c2_beacon.c2_connection_active == False - assert c2_beacon.health_state_actual == ApplicationOperatingState.CLOSED + assert c2_beacon.operating_state == ApplicationOperatingState.CLOSED -def test_c2_suite_launch_ransomware(basic_network): - """Tests that a red agent is able to launch ransomware via C2 Server Actions.""" +def test_c2_suite_terminal(basic_network): + """Tests the Ransomware commands can be used to configure & launch ransomware via Requests.""" From 6ec575d18ec1c3db269d5a01711c655ebd311973 Mon Sep 17 00:00:00 2001 From: Archer Bowen Date: Fri, 9 Aug 2024 09:58:44 +0100 Subject: [PATCH 16/46] #2689 Updated actions E2E notebook and other additions --- src/primaite/game/agent/actions.py | 20 +- .../Command-&-Control-E2E-Demonstration.ipynb | 727 +++++++++++++++++- .../red_applications/c2/c2_beacon.py | 93 ++- .../red_applications/c2/c2_server.py | 25 +- 4 files changed, 797 insertions(+), 68 deletions(-) diff --git a/src/primaite/game/agent/actions.py b/src/primaite/game/agent/actions.py index 7b07c660..5f045ccb 100644 --- a/src/primaite/game/agent/actions.py +++ b/src/primaite/game/agent/actions.py @@ -1153,19 +1153,29 @@ class TerminalC2ServerAction(AbstractAction): class _Opts(BaseModel): """Schema for options that can be passed to this action.""" - model_config = ConfigDict(extra="forbid") - commands: RequestFormat + commands: List[RequestFormat] + ip_address: Optional[str] + username: Optional[str] + password: Optional[str] def __init__(self, manager: "ActionManager", **kwargs) -> None: super().__init__(manager=manager) - def form_request(self, node_id: int, config: Dict) -> RequestFormat: + def form_request(self, node_id: int, commands: List, ip_address: Optional[str], account: dict) -> RequestFormat: """Return the action formatted as a request that can be ingested by the simulation.""" node_name = self.manager.get_node_name_by_idx(node_id) if node_name is None: return ["do_nothing"] - TerminalC2ServerAction._Opts.model_validate(config) # check that options adhere to schema - return ["network", "node", node_name, "application", "C2Server", "terminal_command", config] + + command_model = { + "commands": commands, + "ip_address": ip_address, + "username": account["username"], + "password": account["password"], + } + + TerminalC2ServerAction._Opts.model_validate(command_model) + return ["network", "node", node_name, "application", "C2Server", "terminal_command", command_model] class ActionManager: diff --git a/src/primaite/notebooks/Command-&-Control-E2E-Demonstration.ipynb b/src/primaite/notebooks/Command-&-Control-E2E-Demonstration.ipynb index 0810871b..3cdb3324 100644 --- a/src/primaite/notebooks/Command-&-Control-E2E-Demonstration.ipynb +++ b/src/primaite/notebooks/Command-&-Control-E2E-Demonstration.ipynb @@ -20,15 +20,18 @@ "# Imports\n", "from primaite.config.load import data_manipulation_config_path\n", "from primaite.session.environment import PrimaiteGymEnv\n", + "from primaite.simulator.network.hardware.nodes.network.router import Router\n", "from primaite.game.agent.interface import AgentHistoryItem\n", "import yaml\n", "from pprint import pprint\n", "from primaite.simulator.network.container import Network\n", "from primaite.game.game import PrimaiteGame\n", + "from primaite.simulator.system.applications.application import ApplicationOperatingState\n", "from primaite.simulator.system.applications.red_applications.c2.c2_beacon import C2Beacon\n", "from primaite.simulator.system.applications.red_applications.c2.c2_server import C2Server\n", "from primaite.simulator.system.applications.red_applications.c2.abstract_c2 import C2Command, C2Payload\n", "from primaite.simulator.system.applications.red_applications.ransomware_script import RansomwareScript\n", + "from primaite.simulator.system.software import SoftwareHealthState\n", "from primaite.simulator.network.hardware.nodes.host.computer import Computer\n", "from primaite.simulator.network.hardware.nodes.host.server import Server" ] @@ -66,10 +69,10 @@ " - type: C2_SERVER_TERMINAL_COMMAND\n", " options:\n", " nodes:\n", - " - node_name: client_1\n", + " - node_name: web_server\n", " applications: \n", " - application_name: C2Beacon\n", - " - node_name: domain_controller\n", + " - node_name: client_1\n", " applications: \n", " - application_name: C2Server\n", " max_folders_per_node: 1\n", @@ -78,7 +81,7 @@ " max_nics_per_node: 8\n", " max_acl_rules: 10\n", " ip_list:\n", - " - 192.168.1.10\n", + " - 192.168.1.21\n", " - 192.168.1.14\n", " action_map:\n", " 0:\n", @@ -94,7 +97,7 @@ " options:\n", " node_id: 0\n", " config:\n", - " c2_server_ip_address: 192.168.1.10\n", + " c2_server_ip_address: 192.168.10.21\n", " keep_alive_frequency:\n", " masquerade_protocol:\n", " masquerade_port:\n", @@ -104,10 +107,19 @@ " node_id: 0\n", " application_id: 0 \n", " 4:\n", - " action: NODE_APPLICATION_INSTALL\n", + " action: C2_SERVER_TERMINAL_COMMAND\n", " options:\n", - " node_id: 0\n", - " application_name: RansomwareScript \n", + " node_id: 1\n", + " ip_address:\n", + " account:\n", + " username: test123\n", + " password: pass123\n", + " commands:\n", + " - \n", + " - software_manager\n", + " - application\n", + " - install\n", + " - RansomwareScript\n", " 5:\n", " action: C2_SERVER_RANSOMWARE_CONFIGURE\n", " options:\n", @@ -119,11 +131,8 @@ " action: C2_SERVER_RANSOMWARE_LAUNCH\n", " options:\n", " node_id: 1\n", - " 7:\n", - " action: C2_SERVER_TERMINAL_COMMAND\n", - " options:\n", - " node_id: 1\n", - " application_id: 0 \n", + "\n", + "\n", "\n", " reward_function:\n", " reward_components:\n", @@ -158,7 +167,7 @@ "\n", "This is because higher fidelity environments (and the real-world) a C2 server would not be accessible by private network blue agent and the C2 Server would already be in place before the an adversary (Red Agent) before the narrative of the use case.\n", "\n", - "The cells below installs and runs the C2 Server on the domain controller server directly via the simulation API." + "The cells below installs and runs the C2 Server on the client_1 directly via the simulation API." ] }, { @@ -167,11 +176,11 @@ "metadata": {}, "outputs": [], "source": [ - "domain_controller: Server = env.game.simulation.network.get_node_by_hostname(\"domain_controller\")\n", - "domain_controller.software_manager.install(C2Server)\n", - "c2_server: C2Server = domain_controller.software_manager.software[\"C2Server\"]\n", + "client_1: Computer = env.game.simulation.network.get_node_by_hostname(\"client_1\")\n", + "client_1.software_manager.install(C2Server)\n", + "c2_server: C2Server = client_1.software_manager.software[\"C2Server\"]\n", "c2_server.run()\n", - "domain_controller.software_manager.show()" + "client_1.software_manager.show()" ] }, { @@ -185,16 +194,6 @@ "This can be done by installing, configuring and then executing a C2 Beacon. " ] }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "c2_red_agent = env.game.agents[\"CustomC2Agent\"]\n", - "client_1: Computer = env.game.simulation.network.get_node_by_hostname(\"client_1\")\n" - ] - }, { "cell_type": "markdown", "metadata": {}, @@ -209,8 +208,8 @@ "outputs": [], "source": [ "env.step(1)\n", - "client_1.software_manager.show()\n", - "c2_beacon: C2Beacon = client_1.software_manager.software[\"C2Beacon\"]" + "web_server: Computer = env.game.simulation.network.get_node_by_hostname(\"web_server\")\n", + "web_server.software_manager.show()" ] }, { @@ -227,6 +226,8 @@ "outputs": [], "source": [ "env.step(2)\n", + "c2_beacon: C2Beacon = web_server.software_manager.software[\"C2Beacon\"]\n", + "web_server.software_manager.show()\n", "c2_beacon.show()" ] }, @@ -243,7 +244,7 @@ "metadata": {}, "outputs": [], "source": [ - "env.step(3)" + "env.step(3) " ] }, { @@ -267,7 +268,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### **Command and Control** | C2 Server Actions | Configuring Ransomware" + "### **Command and Control** | C2 Server Actions | Executing Terminal Commands" ] }, { @@ -279,6 +280,22 @@ "env.step(4)" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "client_1.software_manager.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### **Command and Control** | C2 Server Actions | Configuring Ransomware" + ] + }, { "cell_type": "code", "execution_count": null, @@ -294,8 +311,17 @@ "metadata": {}, "outputs": [], "source": [ - "ransomware_script: RansomwareScript = client_1.software_manager.software[\"RansomwareScript\"]\n", - "client_1.software_manager.show()\n", + "env.step(6)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "ransomware_script: RansomwareScript = web_server.software_manager.software[\"RansomwareScript\"]\n", + "web_server.software_manager.show()\n", "ransomware_script.show()" ] }, @@ -329,7 +355,11 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### **Command and Control** | C2 Server Actions | Executing Terminal Commands" + "## **Command and Control** | Blue Agent Relevance\n", + "\n", + "The next section of the notebook will demonstrate the impact that the command and control suite has to the Blue Agent's observation space as well as some potential actions that can be used to prevent the attack from being successfully.\n", + "\n", + "The code cell below re-creates the UC2 network and swaps out the previous custom red agent with a custom blue agent. \n" ] }, { @@ -338,39 +368,652 @@ "metadata": {}, "outputs": [], "source": [ - "# TODO: Post Terminal.\n", - "#env.step(7)" + "custom_blue_agent_yaml = \"\"\" \n", + " - ref: defender\n", + " team: BLUE\n", + " type: ProxyAgent\n", + "\n", + " observation_space:\n", + " type: CUSTOM\n", + " options:\n", + " components:\n", + " - type: NODES\n", + " label: NODES\n", + " options:\n", + " hosts:\n", + " - hostname: web_server\n", + " applications:\n", + " - application_name: C2Beacon\n", + " - application_name: RansomwareScript\n", + " - hostname: database_server\n", + " folders:\n", + " - folder_name: database\n", + " files:\n", + " - file_name: database.db\n", + " - hostname: client_1\n", + " - hostname: client_2\n", + " num_services: 0\n", + " num_applications: 2\n", + " num_folders: 1\n", + " num_files: 1\n", + " num_nics: 0\n", + " include_num_access: false\n", + " include_nmne: false\n", + " monitored_traffic:\n", + " icmp:\n", + " - NONE\n", + " tcp:\n", + " - HTTP\n", + " routers:\n", + " - hostname: router_1\n", + " num_ports: 1\n", + " ip_list:\n", + " - 192.168.10.21\n", + " - 192.168.1.12\n", + " wildcard_list:\n", + " - 0.0.0.1\n", + " port_list:\n", + " - 80\n", + " protocol_list:\n", + " - ICMP\n", + " - TCP\n", + " - UDP\n", + " num_rules: 10\n", + "\n", + " - type: LINKS\n", + " label: LINKS\n", + " options:\n", + " link_references:\n", + " - router_1:eth-1<->switch_1:eth-8\n", + " - router_1:eth-2<->switch_2:eth-8\n", + " - switch_1:eth-1<->web_server:eth-1\n", + " - switch_1:eth-2<->web_server:eth-1\n", + " - switch_1:eth-3<->database_server:eth-1\n", + " - switch_1:eth-4<->backup_server:eth-1\n", + " - switch_1:eth-7<->security_suite:eth-1\n", + " - switch_2:eth-1<->client_1:eth-1\n", + " - switch_2:eth-2<->client_2:eth-1\n", + " - switch_2:eth-7<->security_suite:eth-2\n", + " - type: \"NONE\"\n", + " label: ICS\n", + " options: {}\n", + " \n", + " action_space:\n", + " action_list:\n", + " - type: NODE_APPLICATION_REMOVE\n", + " - type: NODE_SHUTDOWN\n", + " - type: ROUTER_ACL_ADDRULE\n", + " - type: DONOTHING\n", + " action_map:\n", + " 0:\n", + " action: DONOTHING\n", + " options: {}\n", + " 1:\n", + " action: NODE_APPLICATION_REMOVE\n", + " options:\n", + " node_id: 0\n", + " application_name: C2Beacon\n", + " 2:\n", + " action: NODE_SHUTDOWN\n", + " options:\n", + " node_id: 0\n", + " 3:\n", + " action: ROUTER_ACL_ADDRULE\n", + " options:\n", + " target_router: router_1\n", + " position: 1\n", + " permission: 2\n", + " source_ip_id: 2\n", + " dest_ip_id: 3\n", + " source_port_id: 2\n", + " dest_port_id: 2\n", + " protocol_id: 1\n", + " source_wildcard_id: 0\n", + " dest_wildcard_id: 0\n", + "\n", + " options:\n", + " nodes:\n", + " - node_name: web_server\n", + " applications:\n", + " - application_name: C2Beacon\n", + "\n", + " - node_name: database_server\n", + " folders:\n", + " - folder_name: database\n", + " files:\n", + " - file_name: database.db\n", + " services:\n", + " - service_name: DatabaseService\n", + " - node_name: router_1\n", + "\n", + " max_folders_per_node: 2\n", + " max_files_per_folder: 2\n", + " max_services_per_node: 2\n", + " max_nics_per_node: 8\n", + " max_acl_rules: 10\n", + " ip_list:\n", + " - 192.168.10.21\n", + " - 192.168.1.12\n", + " wildcard_list:\n", + " - 0.0.0.1\n", + "\n", + " reward_function:\n", + " reward_components:\n", + " - type: DUMMY\n", + "\n", + " agent_settings:\n", + " flatten_obs: False\n", + "\"\"\"\n", + "custom_blue = yaml.safe_load(custom_blue_agent_yaml)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "with open(data_manipulation_config_path()) as f:\n", + " cfg = yaml.safe_load(f)\n", + " # removing all agents & adding the custom agent.\n", + " cfg['agents'] = {}\n", + " cfg['agents'] = custom_blue\n", + " \n", + "\n", + "blue_env = PrimaiteGymEnv(env_config=cfg)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Utility function for showing OBS changes between each time step.\n", + "\n", + "from deepdiff.diff import DeepDiff\n", + "\n", + "def display_obs_diffs(old, new, step_counter):\n", + " \"\"\"\n", + " Use DeepDiff to extract and display differences in old and new instances of\n", + " the observation space.\n", + "\n", + " :param old: observation space instance.\n", + " :param new: observation space instance.\n", + " :param step_counter: current step counter.\n", + " \"\"\"\n", + " print(\"\\nObservation space differences\")\n", + " print(\"-----------------------------\")\n", + " diff = DeepDiff(old, new)\n", + " print(f\"Step {step_counter}\")\n", + " for d,v in diff.get('values_changed', {}).items():\n", + " print(f\"{d}: {v['old_value']} -> {v['new_value']}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## **Command and Control** | Blue Agent Relevance" + "### **Command and Control** | Blue Agent Relevance | Observation Space\n", + "\n", + "This section demonstrates the OBS impact if the C2 suite is successfully installed and then used to install, configure and launch the ransomwarescript." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Resetting the environment and capturing the default observation space.\n", + "blue_env.reset()\n", + "default_obs, _, _, _, _ = blue_env.step(0)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Setting up the C2 Suite via the simulation API.\n", + "\n", + "client_1: Computer = blue_env.game.simulation.network.get_node_by_hostname(\"client_1\")\n", + "web_server: Server = blue_env.game.simulation.network.get_node_by_hostname(\"web_server\")\n", + "\n", + "# Installing the C2 Server.\n", + "client_1.software_manager.install(C2Server)\n", + "c2_server: C2Server = client_1.software_manager.software[\"C2Server\"]\n", + "c2_server.run()\n", + "\n", + "# Installing the C2 Beacon.\n", + "web_server.software_manager.install(C2Beacon)\n", + "c2_beacon: C2Beacon = web_server.software_manager.software[\"C2Beacon\"]\n", + "c2_beacon.configure(c2_server_ip_address=\"192.168.10.21\")\n", + "c2_beacon.establish()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Capturing the observation impacts of the previous code cell: C2 Suite setup.\n", + "c2_configuration_obs, _, _, _, _ = blue_env.step(0)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "display_obs_diffs(default_obs, c2_configuration_obs, blue_env.game.step_counter)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Installing RansomwareScript via C2 Terminal Commands\n", + "ransomware_install_command = {\"commands\":[[\"software_manager\", \"application\", \"install\", \"RansomwareScript\"]],\n", + " \"username\": \"pass123\",\n", + " \"password\": \"password123\"}\n", + "c2_server._send_command(C2Command.TERMINAL, command_options=ransomware_install_command)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Configuring the RansomwareScript\n", + "ransomware_config = {\"server_ip_address\": \"192.168.1.14\", \"payload\": \"ENCRYPT\"}\n", + "c2_server._send_command(C2Command.RANSOMWARE_CONFIGURE, command_options=ransomware_config)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Capturing the observation impacts of the previous code cell: Ransomware installation & configuration.\n", + "c2_ransomware_obs, _, _, _, _ = blue_env.step(0)" ] }, { "cell_type": "markdown", "metadata": {}, - "source": [] + "source": [ + "The code cell below demonstrates the differences between the default observation space and the configuration of the C2 Server and the Ransomware installation." + ] }, { - "cell_type": "markdown", + "cell_type": "code", + "execution_count": null, "metadata": {}, + "outputs": [], "source": [ - "### **Command and Control** | Blue Agent Relevance | Observation Space" + "display_obs_diffs(default_obs, c2_ransomware_obs, env.game.step_counter)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Waiting for the ransomware to finish installing and then launching the RansomwareScript.\n", + "blue_env.step(0)\n", + "c2_server._send_command(C2Command.RANSOMWARE_LAUNCH, command_options={})" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Capturing the observation impacts of the previous code cell: Launching the RansomwareScript.\n", + "c2_final_obs, _, _, _, _ = blue_env.step(0)" ] }, { "cell_type": "markdown", "metadata": {}, - "source": [] + "source": [ + "The code cell below demonstrates the differences between the default observation space and the configuration of the C2 Server, the ransomware script installation as well as the impact of RansomwareScript upon the database." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "display_obs_diffs(c2_ransomware_obs, c2_final_obs, blue_env.game.step_counter)" + ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "### **Command and Control** | Blue Agent Relevance | Action Space" + "### **Command and Control** | Blue Agent Relevance | Action Space\n", + "\n", + "The next section of this notebook will go over some potential blue agent actions that could be use to thwart the previously demonstrated attack." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# This method is used to shorthand setting up the C2Server and the C2 Beacon.\n", + "def c2_setup(given_env: PrimaiteGymEnv):\n", + " client_1: Computer = given_env.game.simulation.network.get_node_by_hostname(\"client_1\")\n", + " web_server: Server = given_env.game.simulation.network.get_node_by_hostname(\"web_server\")\n", + "\n", + " client_1.software_manager.install(C2Server)\n", + " c2_server: C2Server = client_1.software_manager.software[\"C2Server\"]\n", + " c2_server.run()\n", + "\n", + " web_server.software_manager.install(C2Beacon)\n", + " c2_beacon: C2Beacon = web_server.software_manager.software[\"C2Beacon\"]\n", + " c2_beacon.configure(c2_server_ip_address=\"192.168.10.21\")\n", + " c2_beacon.establish()\n", + "\n", + " return given_env, c2_server, c2_beacon" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Removing the C2 Beacon.\n", + "\n", + "The simplest way a blue agent could prevent the C2 suite is by simply removing the C2 beacon from it's installation point. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "blue_env.reset()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Setting up the C2 Suite using the c2_setup method & capturing the OBS impacts\n", + "\n", + "blue_env, c2_server, c2_beacon = c2_setup(blue_env=blue_env)\n", + "pre_blue_action_obs, _, _, _, _ = blue_env.step(0)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The code cell below uses the custom blue agent defined at the start of this section perform NODE_APPLICATION_REMOVE on the C2 beacon" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Using CAOS ACTION: NODE_APPLICATION_REMOVE & capturing the OBS\n", + "post_blue_action_obs, _, _, _, _ = blue_env.step(1)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Which we can see after the effects of after stepping another timestep and looking at the web_servers software manager and the OBS differences." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "blue_env.step(0)\n", + "web_server.software_manager.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "display_obs_diffs(pre_blue_action_obs, post_blue_action_obs, blue_env.game.step_counter)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now we are unable to do so as the C2 Server is unable has lost it's connection to the C2 Beacon:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Attempting to install the C2 RansomwareScript\n", + "ransomware_install_command = {\"commands\":[[\"software_manager\", \"application\", \"install\", \"RansomwareScript\"]],\n", + " \"username\": \"pass123\",\n", + " \"password\": \"password123\"}\n", + "\n", + "c2_server: C2Server = client_1.software_manager.software[\"C2Server\"]\n", + "c2_server._send_command(C2Command.TERMINAL, command_options=ransomware_install_command)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Shutting down the node infected with a C2 Beacon.\n", + "\n", + "Another way a blue agent can prevent the C2 suite is via shutting down the C2 beacon's host node. Whilst not as effective as the previous option, dependant on situation (such as multiple malicious applications) or other scenarios it may be more timestep efficient for a blue agent to shut down a node directly." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "blue_env.reset()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Setting up the C2 Suite using the c2_setup method & capturing the OBS impacts\n", + "\n", + "blue_env, c2_server, c2_beacon = c2_setup(blue_env=blue_env)\n", + "pre_blue_action_obs, _, _, _, _ = blue_env.step(0)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The code cell below uses the custom blue agent defined at the start of this section perform NODE_SHUT_DOWN on the web server." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Using CAOS ACTION: NODE_SHUT_DOWN & capturing the OBS\n", + "post_blue_action_obs, _, _, _, _ = blue_env.step(2)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Which we can see after the effects of after stepping another timestep and looking at the web_servers operating state & the OBS differences." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "web_server = blue_env.game.simulation.network.get_node_by_hostname(\"web_server\")\n", + "print(web_server.operating_state)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "display_obs_diffs(pre_blue_action_obs, post_blue_action_obs, blue_env.game.step_counter)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Attempting to install the C2 RansomwareScript\n", + "ransomware_install_command = {\"commands\":[[\"software_manager\", \"application\", \"install\", \"RansomwareScript\"]],\n", + " \"username\": \"pass123\",\n", + " \"password\": \"password123\"}\n", + "\n", + "c2_server: C2Server = client_1.software_manager.software[\"C2Server\"]\n", + "c2_server._send_command(C2Command.TERMINAL, command_options=ransomware_install_command)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Blocking C2 Traffic via ACL.\n", + "\n", + "Another potential option a blue agent could take is by placing an ACL rule which blocks traffic between the C2 Server can C2 Beacon.\n", + "\n", + "It's worth noting the potential effectiveness of approach is also linked by the current green agent traffic on the network. The same applies for the previous example." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "blue_env.reset()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Setting up the C2 Suite using the c2_setup method & capturing the OBS impacts\n", + "\n", + "blue_env, c2_server, c2_beacon = c2_setup(blue_env=blue_env)\n", + "pre_blue_action_obs, _, _, _, _ = blue_env.step(0)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The code cell below uses the custom blue agent defined at the start of this section to perform a ROUTER_ACL_ADDRULE on router 1." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Using CAOS ACTION: ROUTER_ACL_ADDRULE & capturing the OBS\n", + "post_blue_action_obs, _, _, _, _ = blue_env.step(3)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Which we can see after the effects of after stepping another timestep and looking at router 1's ACLs and the OBS differences." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "router_1: Router = blue_env.game.simulation.network.get_node_by_hostname(\"router_1\")\n", + "router_1.acl.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "display_obs_diffs(default_obs, c2_ransomware_obs, env.game.step_counter)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now we can see that the C2 applications are unable to maintain connection - thus being unable to execute correctly." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Waiting for the ransomware to finish installing and then launching the RansomwareScript.\n", + "blue_env.step(0)\n", + "c2_server._send_command(C2Command.RANSOMWARE_LAUNCH, command_options={})" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "router_1.acl.show()" ] } ], 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 1dde28a2..1bb4d70f 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 @@ -3,7 +3,6 @@ from enum import Enum from ipaddress import IPv4Address from typing import Dict, Optional -# from primaite.simulator.system.services.terminal.terminal import Terminal from prettytable import MARKDOWN, PrettyTable from pydantic import validate_call @@ -15,6 +14,11 @@ from primaite.simulator.network.transmission.transport_layer import Port from primaite.simulator.system.applications.application import ApplicationOperatingState from primaite.simulator.system.applications.red_applications.c2.abstract_c2 import AbstractC2, C2Command, C2Payload from primaite.simulator.system.applications.red_applications.ransomware_script import RansomwareScript +from primaite.simulator.system.services.terminal.terminal import ( + LocalTerminalConnection, + RemoteTerminalConnection, + Terminal, +) from primaite.simulator.system.software import SoftwareHealthState @@ -44,17 +48,19 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): keep_alive_frequency: int = 5 "The frequency at which ``Keep Alive`` packets are sent to the C2 Server from the C2 Beacon." - # TODO: - # Implement the placeholder command methods - # Uncomment the terminal Import and the terminal property after terminal PR + local_terminal_session: LocalTerminalConnection = None + """#TODO""" - # @property - # 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 + remote_terminal_session: RemoteTerminalConnection = None + """#TODO""" + + @property + def _host_terminal(self) -> Optional[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 @property def _host_ransomware_script(self) -> RansomwareScript: @@ -64,6 +70,26 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): self.sys_log.warning(f"{self.__class__.__name__} cannot find installed ransomware on its host.") return ransomware_script + def get_terminal_session(self, username: str, password: str) -> Optional[LocalTerminalConnection]: + """Return an instance of a Local Terminal Connection upon successful login. Otherwise returns None.""" + if self.local_terminal_session is None: + host_terminal: Terminal = self._host_terminal + self.local_terminal_session = host_terminal.login(username=username, password=password) + + return self.local_terminal_session + + def get_remote_terminal_session( + self, username: str, password: str, ip_address: IPv4Address + ) -> Optional[RemoteTerminalConnection]: + """Return an instance of a Local Terminal Connection upon successful login. Otherwise returns None.""" + if self.remote_terminal_session is None: + host_terminal: Terminal = self._host_terminal + self.remote_terminal_session = host_terminal.login( + username=username, password=password, ip_address=ip_address + ) + + return self.remote_terminal_session + def _init_request_manager(self) -> RequestManager: """ Initialise the request manager. @@ -153,7 +179,6 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): ) 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. The C2 Beacon must already be configured.""" if self.c2_remote_connection is None: @@ -269,7 +294,9 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): data={"Reason": "Cannot find any instances of a RansomwareScript. Have you installed one?"}, ) return RequestResponse.from_bool( - self._host_ransomware_script.configure(server_ip_address=given_config["server_ip_address"]) + self._host_ransomware_script.configure( + server_ip_address=given_config["server_ip_address"], payload=given_config["payload"] + ) ) def _command_ransomware_launch(self, payload: MasqueradePacket) -> RequestResponse: @@ -304,8 +331,44 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): :return: Returns the Request Response returned by the Terminal execute method. :rtype: Request Response """ - # TODO: uncomment and replace (uses terminal) - return RequestResponse(status="success", data={"Reason": "Placeholder."}) + terminal_output: Dict[int, RequestResponse] = {} + given_commands: list[RequestFormat] + + if self._host_terminal is None: + return RequestResponse( + status="failure", + data={"Reason": "Host does not seem to have terminal installed. Unable to resolve command."}, + ) + + # TODO: Placeholder until further details on handling user sessions. + given_commands = payload.payload.get("commands") + given_username = payload.payload.get("username") + given_password = payload.payload.get("password") + remote_ip = payload.payload.get("ip_address") + + # Creating a remote terminal session if given an IP Address, otherwise using a local terminal session. + if payload.payload.get("ip_address") is None: + terminal_session = self.get_terminal_session(username=given_username, password=given_password) + else: + terminal_session = self.get_remote_terminal_session( + username=given_username, password=given_password, ip_address=remote_ip + ) + + if terminal_session is None: + RequestResponse( + status="failure", + data={"Reason": "Host cannot is unable to connect to terminal. Unable to resolve command."}, + ) + + for index, given_command in enumerate(given_commands): + # A try catch exception ladder was used but was considered not the best approach + # as it can end up obscuring visibility of actual bugs (Not the expected ones) and was a temporary solution. + # TODO: Refactor + add further validation to ensure that a request is correct. (maybe a pydantic method?) + terminal_output[index] = terminal_session.execute(given_command) + + # Reset our remote terminal session. + self.remote_terminal_session is None + return RequestResponse(status="success", data=terminal_output) def _handle_keep_alive(self, payload: MasqueradePacket, session_id: Optional[str]) -> bool: """ diff --git a/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py b/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py index 85009cec..211da210 100644 --- a/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py +++ b/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py @@ -47,8 +47,10 @@ class C2Server(AbstractC2, identifier="C2Server"): :return: RequestResponse object with a success code reflecting whether the configuration could be applied. :rtype: RequestResponse """ - # TODO: Parse the parameters from the request to get the parameters - ransomware_config = {"server_ip_address": request[-1].get("server_ip_address")} + ransomware_config = { + "server_ip_address": request[-1].get("server_ip_address"), + "payload": request[-1].get("payload"), + } return self._send_command(given_command=C2Command.RANSOMWARE_CONFIGURE, command_options=ransomware_config) def _launch_ransomware_action(request: RequestFormat, context: Dict) -> RequestResponse: @@ -73,9 +75,8 @@ class C2Server(AbstractC2, identifier="C2Server"): :return: RequestResponse object with a success code reflecting whether the ransomware was launched. :rtype: RequestResponse """ - # TODO: Parse the parameters from the request to get the parameters - terminal_commands = {"commands": request[-1].get("commands")} - return self._send_command(given_command=C2Command.TERMINAL, command_options=terminal_commands) + command_payload = request[-1] + return self._send_command(given_command=C2Command.TERMINAL, command_options=command_payload) rm.add_request( name="ransomware_configure", @@ -174,7 +175,7 @@ class C2Server(AbstractC2, identifier="C2Server"): ---------------------|------------------------ RANSOMWARE_CONFIGURE | Configures an installed ransomware script based on the passed parameters. RANSOMWARE_LAUNCH | Launches the installed ransomware script. - Terminal | Executes a command via the terminal installed on the C2 Beacons Host. + TERMINAL | Executes a command via the terminal installed on the C2 Beacons Host. For more information on the impact of these commands please refer to the terminal and the ransomware applications. @@ -198,6 +199,18 @@ class C2Server(AbstractC2, identifier="C2Server"): status="failure", data={"Reason": "Unable to access networking resources. Unable to send command."} ) + if self.c2_remote_connection is False: + self.sys_log.warning(f"{self.name}: C2 Beacon has yet to establish connection. Rejecting command.") + return RequestResponse( + status="failure", data={"Reason": "C2 Beacon has yet to establish connection. Unable to send command."} + ) + + if self.current_c2_session is None: + self.sys_log.warning(f"{self.name}: C2 Beacon cannot be reached. Rejecting command.") + return RequestResponse( + status="failure", data={"Reason": "C2 Beacon cannot be reached. Unable to send command."} + ) + self.sys_log.info(f"{self.name}: Attempting to send command {given_command}.") command_packet = self._craft_packet(given_command=given_command, command_options=command_options) From ddc9acd03a2765725e7e33a4fbedf26316278041 Mon Sep 17 00:00:00 2001 From: Archer Bowen Date: Fri, 9 Aug 2024 11:04:12 +0100 Subject: [PATCH 17/46] #2689 Fix notebook blue agent actions not functioning correctly. --- .../Command-&-Control-E2E-Demonstration.ipynb | 62 ++++++++++++++----- .../red_applications/c2/abstract_c2.py | 6 +- .../system/red_applications/test_c2_suite.py | 6 ++ 3 files changed, 54 insertions(+), 20 deletions(-) diff --git a/src/primaite/notebooks/Command-&-Control-E2E-Demonstration.ipynb b/src/primaite/notebooks/Command-&-Control-E2E-Demonstration.ipynb index 3cdb3324..e41b6e08 100644 --- a/src/primaite/notebooks/Command-&-Control-E2E-Demonstration.ipynb +++ b/src/primaite/notebooks/Command-&-Control-E2E-Demonstration.ipynb @@ -83,6 +83,8 @@ " ip_list:\n", " - 192.168.1.21\n", " - 192.168.1.14\n", + " wildcard_list:\n", + " - 0.0.0.1\n", " action_map:\n", " 0:\n", " action: DONOTHING\n", @@ -469,7 +471,8 @@ " dest_port_id: 2\n", " protocol_id: 1\n", " source_wildcard_id: 0\n", - " dest_wildcard_id: 0\n", + " dest_wildcard_id: 0 \n", + "\n", "\n", " options:\n", " nodes:\n", @@ -496,7 +499,6 @@ " - 192.168.1.12\n", " wildcard_list:\n", " - 0.0.0.1\n", - "\n", " reward_function:\n", " reward_components:\n", " - type: DUMMY\n", @@ -728,7 +730,7 @@ " c2_beacon.configure(c2_server_ip_address=\"192.168.10.21\")\n", " c2_beacon.establish()\n", "\n", - " return given_env, c2_server, c2_beacon" + " return given_env, c2_server, c2_beacon, client_1, web_server" ] }, { @@ -757,7 +759,7 @@ "source": [ "# Setting up the C2 Suite using the c2_setup method & capturing the OBS impacts\n", "\n", - "blue_env, c2_server, c2_beacon = c2_setup(blue_env=blue_env)\n", + "blue_env, c2_server, c2_beacon, client_1, web_server = c2_setup(given_env=blue_env)\n", "pre_blue_action_obs, _, _, _, _ = blue_env.step(0)" ] }, @@ -852,7 +854,7 @@ "source": [ "# Setting up the C2 Suite using the c2_setup method & capturing the OBS impacts\n", "\n", - "blue_env, c2_server, c2_beacon = c2_setup(blue_env=blue_env)\n", + "blue_env, c2_server, c2_beacon, client_1, web_server = c2_setup(given_env=blue_env)\n", "pre_blue_action_obs, _, _, _, _ = blue_env.step(0)" ] }, @@ -942,7 +944,7 @@ "source": [ "# Setting up the C2 Suite using the c2_setup method & capturing the OBS impacts\n", "\n", - "blue_env, c2_server, c2_beacon = c2_setup(blue_env=blue_env)\n", + "blue_env, c2_server, c2_beacon, client_1, web_server = c2_setup(given_env=blue_env)\n", "pre_blue_action_obs, _, _, _, _ = blue_env.step(0)" ] }, @@ -980,15 +982,6 @@ "router_1.acl.show()" ] }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "display_obs_diffs(default_obs, c2_ransomware_obs, env.game.step_counter)" - ] - }, { "cell_type": "markdown", "metadata": {}, @@ -1002,8 +995,10 @@ "metadata": {}, "outputs": [], "source": [ - "# Waiting for the ransomware to finish installing and then launching the RansomwareScript.\n", "blue_env.step(0)\n", + "\n", + "# Attempting to install and execute the ransomware script\n", + "c2_server._send_command(C2Command.TERMINAL, command_options=ransomware_install_command)\n", "c2_server._send_command(C2Command.RANSOMWARE_LAUNCH, command_options={})" ] }, @@ -1015,6 +1010,41 @@ "source": [ "router_1.acl.show()" ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Because of the ACL rule the C2 beacon never received the ransomware installation and execute commands from the C2 server:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "web_server.software_manager.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "database_server: Server = blue_env.game.simulation.network.get_node_by_hostname(\"database_server\")\n", + "database_server.software_manager.file_system.show(full=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "display_obs_diffs(pre_blue_action_obs, post_blue_action_obs, blue_env.game.step_counter)" + ] } ], "metadata": { diff --git a/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py b/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py index 9158d80f..47944633 100644 --- a/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py +++ b/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py @@ -90,7 +90,8 @@ class AbstractC2(Application, identifier="AbstractC2"): # TODO: Update this post application/services requiring to listen to multiple ports def __init__(self, **kwargs): """Initialise the C2 applications to by default listen for HTTP traffic.""" - kwargs["port"] = Port.HTTP # TODO: Update this post application/services requiring to listen to multiple ports + kwargs["listen_on_ports"] = {Port.HTTP, Port.FTP, Port.DNS} + kwargs["port"] = Port.HTTP kwargs["protocol"] = IPProtocol.TCP super().__init__(**kwargs) @@ -241,9 +242,6 @@ class AbstractC2(Application, identifier="AbstractC2"): ) return False - # TODO: Validation on Ports (E.g only allow HTTP, FTP etc) - # Potentially compare to IPProtocol & Port children? Depends on how listening on multiple ports is implemented. - # Setting the Ports self.current_masquerade_port = payload.masquerade_port self.current_masquerade_protocol = payload.masquerade_protocol diff --git a/tests/integration_tests/system/red_applications/test_c2_suite.py b/tests/integration_tests/system/red_applications/test_c2_suite.py index 9d66f3c1..9b799ff5 100644 --- a/tests/integration_tests/system/red_applications/test_c2_suite.py +++ b/tests/integration_tests/system/red_applications/test_c2_suite.py @@ -102,6 +102,12 @@ def test_c2_suite_setup_receive(basic_network): assert c2_server.c2_connection_active is True assert c2_server.c2_remote_connection == IPv4Address("192.168.255.2") + for i in range(50): + network.apply_timestep(i) + + assert c2_beacon.c2_connection_active is True + assert c2_server.c2_connection_active is True + def test_c2_suite_keep_alive_inactivity(basic_network): """Tests that C2 Beacon disconnects from the C2 Server after inactivity.""" From 4241118d260fadf05d43e7e4e69fb6e6fd716944 Mon Sep 17 00:00:00 2001 From: Archer Bowen Date: Fri, 9 Aug 2024 12:14:57 +0100 Subject: [PATCH 18/46] #2689 Adding slight changes to c2_Beacon & terminal that appeared when merging from dev. --- .../Command-&-Control-E2E-Demonstration.ipynb | 4 ++-- .../applications/red_applications/c2/c2_beacon.py | 14 ++++++-------- .../simulator/system/services/terminal/terminal.py | 2 +- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/src/primaite/notebooks/Command-&-Control-E2E-Demonstration.ipynb b/src/primaite/notebooks/Command-&-Control-E2E-Demonstration.ipynb index e41b6e08..10077cd4 100644 --- a/src/primaite/notebooks/Command-&-Control-E2E-Demonstration.ipynb +++ b/src/primaite/notebooks/Command-&-Control-E2E-Demonstration.ipynb @@ -114,8 +114,8 @@ " node_id: 1\n", " ip_address:\n", " account:\n", - " username: test123\n", - " password: pass123\n", + " username: admin\n", + " password: admin\n", " commands:\n", " - \n", " - software_manager\n", 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 1bb4d70f..d8911622 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 @@ -49,10 +49,10 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): "The frequency at which ``Keep Alive`` packets are sent to the C2 Server from the C2 Beacon." local_terminal_session: LocalTerminalConnection = None - """#TODO""" + "The currently in use local terminal session." remote_terminal_session: RemoteTerminalConnection = None - """#TODO""" + "The currently in use remote terminal session" @property def _host_terminal(self) -> Optional[Terminal]: @@ -199,7 +199,7 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): ---------------------|------------------------ RANSOMWARE_CONFIGURE | self._command_ransomware_config() RANSOMWARE_LAUNCH | self._command_ransomware_launch() - Terminal | self._command_terminal() + TERMINAL | self._command_terminal() Please see each method individually for further information regarding the implementation of these commands. @@ -340,14 +340,13 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): data={"Reason": "Host does not seem to have terminal installed. Unable to resolve command."}, ) - # TODO: Placeholder until further details on handling user sessions. given_commands = payload.payload.get("commands") given_username = payload.payload.get("username") given_password = payload.payload.get("password") remote_ip = payload.payload.get("ip_address") # Creating a remote terminal session if given an IP Address, otherwise using a local terminal session. - if payload.payload.get("ip_address") is None: + if remote_ip is None: terminal_session = self.get_terminal_session(username=given_username, password=given_password) else: terminal_session = self.get_remote_terminal_session( @@ -355,9 +354,8 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): ) if terminal_session is None: - RequestResponse( - status="failure", - data={"Reason": "Host cannot is unable to connect to terminal. Unable to resolve command."}, + return RequestResponse( + status="failure", data={"reason": "Terminal Login failed. Cannot create a terminal session."} ) for index, given_command in enumerate(given_commands): diff --git a/src/primaite/simulator/system/services/terminal/terminal.py b/src/primaite/simulator/system/services/terminal/terminal.py index 876b1694..df2098df 100644 --- a/src/primaite/simulator/system/services/terminal/terminal.py +++ b/src/primaite/simulator/system/services/terminal/terminal.py @@ -92,7 +92,7 @@ class LocalTerminalConnection(TerminalClientConnection): if not self.is_active: self.parent_terminal.sys_log.warning("Connection inactive, cannot execute") return None - return self.parent_terminal.execute(command, connection_id=self.connection_uuid) + return self.parent_terminal.execute(command) class RemoteTerminalConnection(TerminalClientConnection): From ab91f993a52e916e2d1f9a88aa74b4e3bb661ccc Mon Sep 17 00:00:00 2001 From: Archer Bowen Date: Fri, 9 Aug 2024 12:45:15 +0100 Subject: [PATCH 19/46] #2689 Initial Implementation of multi-port listeners. --- src/primaite/game/agent/actions.py | 4 +- .../Command-&-Control-E2E-Demonstration.ipynb | 130 ++++++++++++++++++ .../red_applications/c2/abstract_c2.py | 5 +- 3 files changed, 134 insertions(+), 5 deletions(-) diff --git a/src/primaite/game/agent/actions.py b/src/primaite/game/agent/actions.py index 5f045ccb..92b175a9 100644 --- a/src/primaite/game/agent/actions.py +++ b/src/primaite/game/agent/actions.py @@ -1107,8 +1107,8 @@ class ConfigureC2BeaconAction(AbstractAction): config = ConfigureC2BeaconAction._Opts( c2_server_ip_address=config["c2_server_ip_address"], keep_alive_frequency=config["keep_alive_frequency"], - masquerade_port=config["masquerade_protocol"], - masquerade_protocol=config["masquerade_port"], + masquerade_port=config["masquerade_port"], + masquerade_protocol=config["masquerade_protocol"], ) ConfigureC2BeaconAction._Opts.model_validate(config) # check that options adhere to schema diff --git a/src/primaite/notebooks/Command-&-Control-E2E-Demonstration.ipynb b/src/primaite/notebooks/Command-&-Control-E2E-Demonstration.ipynb index 10077cd4..bbe29cd6 100644 --- a/src/primaite/notebooks/Command-&-Control-E2E-Demonstration.ipynb +++ b/src/primaite/notebooks/Command-&-Control-E2E-Demonstration.ipynb @@ -133,6 +133,15 @@ " action: C2_SERVER_RANSOMWARE_LAUNCH\n", " options:\n", " node_id: 1\n", + " 7:\n", + " action: CONFIGURE_C2_BEACON\n", + " options:\n", + " node_id: 0\n", + " config:\n", + " c2_server_ip_address: 192.168.10.21\n", + " keep_alive_frequency: 10\n", + " masquerade_protocol: TCP\n", + " masquerade_port: DNS\n", "\n", "\n", "\n", @@ -1045,6 +1054,127 @@ "source": [ "display_obs_diffs(pre_blue_action_obs, post_blue_action_obs, blue_env.game.step_counter)" ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## **Command and Control** | C2 Beacon Actions\n", + "\n", + "Before any C2 Server commands is able to accept any commands, it must first establish connection with a C2 beacon.\n", + "\n", + "This can be done by installing, configuring and then executing a C2 Beacon. " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## **Command and Control** | Configurability \n", + "\n", + "TODO: Fleshout" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "with open(data_manipulation_config_path()) as f:\n", + " cfg = yaml.safe_load(f)\n", + " # removing all agents & adding the custom agent.\n", + " cfg['agents'] = {}\n", + " cfg['agents'] = c2_agent_yaml\n", + " \n", + "\n", + "c2_config_env = PrimaiteGymEnv(env_config=cfg)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Installing the C2 Server" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "client_1: Computer = c2_config_env.game.simulation.network.get_node_by_hostname(\"client_1\")\n", + "client_1.software_manager.install(C2Server)\n", + "c2_server: C2Server = client_1.software_manager.software[\"C2Server\"]\n", + "c2_server.run()\n", + "client_1.software_manager.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Installing the C2 Beacon via NODE_APPLICATION_INSTALL" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "c2_config_env.step(1)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Configuring the C2 Beacon using different parameters:\n", + "\n", + "``` yaml\n", + " action: CONFIGURE_C2_BEACON\n", + " options:\n", + " node_id: 0\n", + " config:\n", + " c2_server_ip_address: 192.168.10.21\n", + " keep_alive_frequency: 10\n", + " masquerade_protocol: TCP\n", + " masquerade_port: DNS\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "c2_config_env.step(7)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Establishing connection to the C2 Server.\n", + "c2_config_env.step(3)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "web_server: Server = c2_config_env.game.simulation.network.get_node_by_hostname(\"web_server\")\n", + "c2_beacon: C2Beacon = web_server.software_manager.software[\"C2Beacon\"]\n", + "c2_beacon.show()\n", + "c2_server.show()" + ] } ], "metadata": { diff --git a/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py b/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py index 47944633..c7b0d32c 100644 --- a/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py +++ b/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py @@ -87,11 +87,10 @@ class AbstractC2(Application, identifier="AbstractC2"): """ return super().describe_state() - # TODO: Update this post application/services requiring to listen to multiple ports def __init__(self, **kwargs): """Initialise the C2 applications to by default listen for HTTP traffic.""" kwargs["listen_on_ports"] = {Port.HTTP, Port.FTP, Port.DNS} - kwargs["port"] = Port.HTTP + kwargs["port"] = Port.NONE kwargs["protocol"] = IPProtocol.TCP super().__init__(**kwargs) @@ -242,7 +241,7 @@ class AbstractC2(Application, identifier="AbstractC2"): ) return False - # Setting the Ports + # Setting the masquerade_port/protocol attribute: self.current_masquerade_port = payload.masquerade_port self.current_masquerade_protocol = payload.masquerade_protocol From 53433ce7b6bbefb46fdd0d288defd1468a81477b Mon Sep 17 00:00:00 2001 From: Archer Bowen Date: Fri, 9 Aug 2024 17:53:47 +0100 Subject: [PATCH 20/46] #2689 General improvements. 1. Abstract TAP now handles .apply_timestep 2. Expanded tests 3. Added pydantic model for c2 configuration. --- .../system/applications/c2_suite.rst | 4 +- .../Command-&-Control-E2E-Demonstration.ipynb | 12 +- .../simulator/network/protocols/masquerade.py | 6 + .../red_applications/c2/abstract_c2.py | 120 ++++++++++---- .../red_applications/c2/c2_beacon.py | 110 +++++-------- .../red_applications/c2/c2_server.py | 72 ++++++--- ..._suite.py => test_c2_suite_integration.py} | 153 +++++++++++------- .../_red_applications/test_c2_suite.py | 138 ++++++++++++++++ 8 files changed, 421 insertions(+), 194 deletions(-) rename tests/integration_tests/system/red_applications/{test_c2_suite.py => test_c2_suite_integration.py} (71%) create mode 100644 tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_c2_suite.py diff --git a/docs/source/simulation_components/system/applications/c2_suite.rst b/docs/source/simulation_components/system/applications/c2_suite.rst index c360d0be..e299bb0e 100644 --- a/docs/source/simulation_components/system/applications/c2_suite.rst +++ b/docs/source/simulation_components/system/applications/c2_suite.rst @@ -14,7 +14,7 @@ Overview: ========= These two new classes intend to Red Agents a cyber realistic way of leveraging the capabilities of the ``Terminal`` application. -Whilst introducing both more oppourtinies for the blue agent to notice and subvert Red Agents during an episode. +Whilst introducing both more opportunities for the blue agent to notice and subvert Red Agents during an episode. For a more in-depth look at the command and control applications then please refer to the ``C2-E2E-Notebook``. @@ -42,7 +42,7 @@ It's important to note that in order to keep the PrimAITE realistic from a cyber The C2 Server application should never be visible or actionable upon directly by the blue agent. This is because in the real world, C2 servers are hosted on ephemeral public domains that would not be accessible by private network blue agent. -Therefore granting a blue agent's the ability to perform counter measures directly against the application would be unrealistic. +Therefore granting blue agent(s) the ability to perform counter measures directly against the application would be unrealistic. It is more accurate to see the host that the C2 Server is installed on as being able to route to the C2 Server (Internet Access). diff --git a/src/primaite/notebooks/Command-&-Control-E2E-Demonstration.ipynb b/src/primaite/notebooks/Command-&-Control-E2E-Demonstration.ipynb index bbe29cd6..b41b9f2e 100644 --- a/src/primaite/notebooks/Command-&-Control-E2E-Demonstration.ipynb +++ b/src/primaite/notebooks/Command-&-Control-E2E-Demonstration.ipynb @@ -631,8 +631,8 @@ "source": [ "# Installing RansomwareScript via C2 Terminal Commands\n", "ransomware_install_command = {\"commands\":[[\"software_manager\", \"application\", \"install\", \"RansomwareScript\"]],\n", - " \"username\": \"pass123\",\n", - " \"password\": \"password123\"}\n", + " \"username\": \"admin\",\n", + " \"password\": \"admin\"}\n", "c2_server._send_command(C2Command.TERMINAL, command_options=ransomware_install_command)\n" ] }, @@ -830,8 +830,8 @@ "source": [ "# Attempting to install the C2 RansomwareScript\n", "ransomware_install_command = {\"commands\":[[\"software_manager\", \"application\", \"install\", \"RansomwareScript\"]],\n", - " \"username\": \"pass123\",\n", - " \"password\": \"password123\"}\n", + " \"username\": \"admin\",\n", + " \"password\": \"admin\"}\n", "\n", "c2_server: C2Server = client_1.software_manager.software[\"C2Server\"]\n", "c2_server._send_command(C2Command.TERMINAL, command_options=ransomware_install_command)" @@ -918,8 +918,8 @@ "source": [ "# Attempting to install the C2 RansomwareScript\n", "ransomware_install_command = {\"commands\":[[\"software_manager\", \"application\", \"install\", \"RansomwareScript\"]],\n", - " \"username\": \"pass123\",\n", - " \"password\": \"password123\"}\n", + " \"username\": \"admin\",\n", + " \"password\": \"admin\"}\n", "\n", "c2_server: C2Server = client_1.software_manager.software[\"C2Server\"]\n", "c2_server._send_command(C2Command.TERMINAL, command_options=ransomware_install_command)" diff --git a/src/primaite/simulator/network/protocols/masquerade.py b/src/primaite/simulator/network/protocols/masquerade.py index 7ef17fc0..e2a7b6a0 100644 --- a/src/primaite/simulator/network/protocols/masquerade.py +++ b/src/primaite/simulator/network/protocols/masquerade.py @@ -12,6 +12,12 @@ class MasqueradePacket(DataPacket): masquerade_port: Enum # The 'Masquerade' port that is currently in use + +class C2Packet(MasqueradePacket): + """Represents C2 suite communications packets.""" + payload_type: Enum # The type of C2 traffic (e.g keep alive, command or command out) command: Optional[Enum] = None # Used to pass the actual C2 Command in C2 INPUT + + keep_alive_frequency: int diff --git a/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py b/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py index c7b0d32c..a00b8570 100644 --- a/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py +++ b/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py @@ -4,13 +4,14 @@ from enum import Enum from ipaddress import IPv4Address from typing import Dict, Optional -from pydantic import validate_call +from pydantic import BaseModel, Field, validate_call -from primaite.simulator.network.protocols.masquerade import MasqueradePacket +from primaite.simulator.network.protocols.masquerade import C2Packet from primaite.simulator.network.transmission.network_layer import IPProtocol from primaite.simulator.network.transmission.transport_layer import Port -from primaite.simulator.system.applications.application import Application +from primaite.simulator.system.applications.application import Application, ApplicationOperatingState from primaite.simulator.system.core.session_manager import Session +from primaite.simulator.system.software import SoftwareHealthState # TODO: # Create test that leverage all the functionality needed for the different TAPs @@ -65,19 +66,30 @@ class AbstractC2(Application, identifier="AbstractC2"): c2_remote_connection: IPv4Address = None """The IPv4 Address of the remote c2 connection. (Either the IP of the beacon or the server).""" - # These two attributes are set differently in the c2 server and c2 beacon. - # The c2 server parses the keep alive and sets these accordingly. - # The c2 beacon will set this attributes upon installation and configuration - - current_masquerade_protocol: IPProtocol = IPProtocol.TCP - """The currently chosen protocol that the C2 traffic is masquerading as. Defaults as TCP.""" - - current_masquerade_port: Port = Port.HTTP - """The currently chosen port that the C2 traffic is masquerading as. Defaults at HTTP.""" - - current_c2_session: Session = None + c2_session: Session = None """The currently active session that the C2 Traffic is using. Set after establishing connection.""" + keep_alive_inactivity: int = 0 + """Indicates how many timesteps since the last time the c2 application received a keep alive.""" + + class _C2_Opts(BaseModel): + """A Pydantic Schema for the different C2 configuration options.""" + + keep_alive_frequency: int = Field(default=5, ge=1) + """The frequency at which ``Keep Alive`` packets are sent to the C2 Server from the C2 Beacon.""" + + masquerade_protocol: IPProtocol = Field(default=IPProtocol.TCP) + """The currently chosen protocol that the C2 traffic is masquerading as. Defaults as TCP.""" + + masquerade_port: Port = Field(default=Port.HTTP) + """The currently chosen port that the C2 traffic is masquerading as. Defaults at HTTP.""" + + # The c2 beacon sets the c2_config through it's own internal method - configure (which is also used by agents) + # and then passes the config attributes to the c2 server via keep alives + # The c2 server parses the C2 configurations from keep alive traffic and sets the c2_config accordingly. + c2_config: _C2_Opts = _C2_Opts() + """Holds the current configuration settings of the C2 Suite.""" + def describe_state(self) -> Dict: """ Describe the state of the C2 application. @@ -96,7 +108,7 @@ class AbstractC2(Application, identifier="AbstractC2"): # Validate call ensures we are only handling Masquerade Packets. @validate_call - def _handle_c2_payload(self, payload: MasqueradePacket, session_id: Optional[str] = None) -> bool: + def _handle_c2_payload(self, payload: C2Packet, session_id: Optional[str] = None) -> bool: """Handles masquerade payloads for both c2 beacons and c2 servers. Currently, the C2 application suite can handle the following payloads: @@ -151,18 +163,18 @@ class AbstractC2(Application, identifier="AbstractC2"): """Abstract Method: Used in C2 beacon to parse and handle commands received from the c2 server.""" pass - def _handle_keep_alive(self, payload: MasqueradePacket, session_id: Optional[str]) -> bool: + def _handle_keep_alive(self, payload: C2Packet, session_id: Optional[str]) -> bool: """Abstract Method: The C2 Server and the C2 Beacon handle the KEEP ALIVEs differently.""" # from_network_interface=from_network_interface - def receive(self, payload: MasqueradePacket, session_id: Optional[str] = None, **kwargs) -> bool: + def receive(self, payload: C2Packet, session_id: Optional[str] = None, **kwargs) -> bool: """Receives masquerade packets. Used by both c2 server and c2 beacon. Defining the `Receive` method so that the application can receive packets via the session manager. These packets are then immediately handed to ._handle_c2_payload. :param payload: The Masquerade Packet to be received. - :type payload: MasqueradePacket + :type payload: C2Packet :param session_id: The transport session_id that the payload is originating from. :type session_id: str """ @@ -193,9 +205,10 @@ class AbstractC2(Application, identifier="AbstractC2"): # We also Pass masquerade proto`col/port so that the c2 server can reply on the correct protocol/port. # (This also lays the foundations for switching masquerade port/protocols mid episode.) - keep_alive_packet = MasqueradePacket( - masquerade_protocol=self.current_masquerade_protocol, - masquerade_port=self.current_masquerade_port, + keep_alive_packet = C2Packet( + masquerade_protocol=self.c2_config.masquerade_protocol, + masquerade_port=self.c2_config.masquerade_port, + keep_alive_frequency=self.c2_config.keep_alive_frequency, payload_type=C2Payload.KEEP_ALIVE, command=None, ) @@ -203,13 +216,15 @@ class AbstractC2(Application, identifier="AbstractC2"): if self.send( payload=keep_alive_packet, dest_ip_address=self.c2_remote_connection, - dest_port=self.current_masquerade_port, - ip_protocol=self.current_masquerade_protocol, + dest_port=self.c2_config.masquerade_port, + ip_protocol=self.c2_config.masquerade_protocol, session_id=session_id, ): self.keep_alive_sent = True self.sys_log.info(f"{self.name}: Keep Alive sent to {self.c2_remote_connection}") - self.sys_log.debug(f"{self.name}: on {self.current_masquerade_port} via {self.current_masquerade_protocol}") + self.sys_log.debug( + f"{self.name}: on {self.c2_config.masquerade_port} via {self.c2_config.masquerade_protocol}" + ) return True else: self.sys_log.warning( @@ -217,7 +232,7 @@ class AbstractC2(Application, identifier="AbstractC2"): ) return False - def _resolve_keep_alive(self, payload: MasqueradePacket, session_id: Optional[str]) -> bool: + def _resolve_keep_alive(self, payload: C2Packet, session_id: Optional[str]) -> bool: """ Parses the Masquerade Port/Protocol within the received Keep Alive packet. @@ -227,7 +242,7 @@ class AbstractC2(Application, identifier="AbstractC2"): Returns False otherwise. :param payload: The Keep Alive payload received. - :type payload: MasqueradePacket + :type payload: C2Packet :param session_id: The transport session_id that the payload is originating from. :type session_id: str :return: True on successful configuration, false otherwise. @@ -241,16 +256,61 @@ class AbstractC2(Application, identifier="AbstractC2"): ) return False - # Setting the masquerade_port/protocol attribute: - self.current_masquerade_port = payload.masquerade_port - self.current_masquerade_protocol = payload.masquerade_protocol + # Updating the C2 Configuration attribute. + + self.c2_config.masquerade_port = payload.masquerade_port + self.c2_config.masquerade_protocol = payload.masquerade_protocol + self.c2_config.keep_alive_frequency = payload.keep_alive_frequency # This statement is intended to catch on the C2 Application that is listening for connection. (C2 Beacon) if self.c2_remote_connection is None: self.sys_log.debug(f"{self.name}: Attempting to configure remote C2 connection based off received output.") - self.c2_remote_connection = IPv4Address(self.current_c2_session.with_ip_address) + self.c2_remote_connection = IPv4Address(self.c2_session.with_ip_address) self.c2_connection_active = True # Sets the connection to active self.keep_alive_inactivity = 0 # Sets the keep alive inactivity to zero return True + + def _reset_c2_connection(self) -> None: + """Resets all currently established C2 communications to their default setting.""" + self.c2_connection_active = False + self.c2_session = None + self.keep_alive_inactivity = 0 + self.keep_alive_frequency = 5 + self.c2_remote_connection = None + self.c2_config.masquerade_port = Port.HTTP + self.c2_config.masquerade_protocol = IPProtocol.TCP + + @abstractmethod + def _confirm_connection(self, timestep: int) -> bool: + """Abstract method - Checks the suitability of the current C2 Server/Beacon connection.""" + + def apply_timestep(self, timestep: int) -> None: + """Apply a timestep to the c2_server & c2 beacon. + + Used to keep track of when the c2 server should consider a beacon dead + and set it's c2_remote_connection attribute to false. + + 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 a keep alive response packet is received then the ``keep_alive_inactivity`` attribute is reset. + + Therefore, if ``keep_alive_inactivity`` attribute is not 0 after a keep alive is sent + then the connection is considered severed and c2 beacon will shut down. + + :param timestep: The current timestep of the simulation. + :type timestep: Int + :return bool: Returns false if connection was lost. Returns True if connection is active or re-established. + :rtype bool: + """ + 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 + self._confirm_connection(timestep) + return 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 d8911622..55dd1474 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 @@ -8,10 +8,9 @@ from pydantic import validate_call from primaite.interface.request import RequestFormat, RequestResponse from primaite.simulator.core import RequestManager, RequestType -from primaite.simulator.network.protocols.masquerade import MasqueradePacket +from primaite.simulator.network.protocols.masquerade import C2Packet from primaite.simulator.network.transmission.network_layer import IPProtocol from primaite.simulator.network.transmission.transport_layer import Port -from primaite.simulator.system.applications.application import ApplicationOperatingState from primaite.simulator.system.applications.red_applications.c2.abstract_c2 import AbstractC2, C2Command, C2Payload from primaite.simulator.system.applications.red_applications.ransomware_script import RansomwareScript from primaite.simulator.system.services.terminal.terminal import ( @@ -19,7 +18,6 @@ from primaite.simulator.system.services.terminal.terminal import ( RemoteTerminalConnection, Terminal, ) -from primaite.simulator.system.software import SoftwareHealthState class C2Beacon(AbstractC2, identifier="C2Beacon"): @@ -41,13 +39,6 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): keep_alive_attempted: bool = False """Indicates if a keep alive has been attempted to be sent this timestep. Used to prevent packet storms.""" - # We should set the application to NOT_RUNNING if the inactivity count reaches a certain thresh hold. - keep_alive_inactivity: int = 0 - """Indicates how many timesteps since the last time the c2 application received a keep alive.""" - - keep_alive_frequency: int = 5 - "The frequency at which ``Keep Alive`` packets are sent to the C2 Server from the C2 Beacon." - local_terminal_session: LocalTerminalConnection = None "The currently in use local terminal session." @@ -164,9 +155,9 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): :type masquerade_port: Enum (Port) """ self.c2_remote_connection = IPv4Address(c2_server_ip_address) - self.keep_alive_frequency = keep_alive_frequency - self.current_masquerade_port = masquerade_port - self.current_masquerade_protocol = masquerade_protocol + self.c2_config.keep_alive_frequency = keep_alive_frequency + self.c2_config.masquerade_port = masquerade_port + self.c2_config.masquerade_protocol = masquerade_protocol self.sys_log.info( f"{self.name}: Configured {self.name} with remote C2 server connection: {c2_server_ip_address=}." ) @@ -188,7 +179,7 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): self.num_executions += 1 return self._send_keep_alive(session_id=None) - def _handle_command_input(self, payload: MasqueradePacket, session_id: Optional[str]) -> bool: + def _handle_command_input(self, payload: C2Packet, session_id: Optional[str]) -> bool: """ Handles the parsing of C2 Commands from C2 Traffic (Masquerade Packets). @@ -205,7 +196,7 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): the implementation of these commands. :param payload: The INPUT C2 Payload - :type payload: MasqueradePacket + :type payload: C2Packet :return: The Request Response provided by the terminal execute method. :rtype Request Response: """ @@ -250,21 +241,24 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): :param session_id: The current session established with the C2 Server. :type session_id: Str """ - output_packet = MasqueradePacket( - masquerade_protocol=self.current_masquerade_protocol, - masquerade_port=self.current_masquerade_port, + output_packet = C2Packet( + masquerade_protocol=self.c2_config.masquerade_protocol, + masquerade_port=self.c2_config.masquerade_port, + keep_alive_frequency=self.c2_config.keep_alive_frequency, payload_type=C2Payload.OUTPUT, payload=command_output, ) if self.send( payload=output_packet, dest_ip_address=self.c2_remote_connection, - dest_port=self.current_masquerade_port, - ip_protocol=self.current_masquerade_protocol, + dest_port=self.c2_config.masquerade_port, + ip_protocol=self.c2_config.masquerade_protocol, session_id=session_id, ): 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}") + self.sys_log.debug( + f"{self.name}: on {self.c2_config.masquerade_port} via {self.c2_config.masquerade_protocol}" + ) return True else: self.sys_log.warning( @@ -272,7 +266,7 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): ) return False - def _command_ransomware_config(self, payload: MasqueradePacket) -> RequestResponse: + def _command_ransomware_config(self, payload: C2Packet) -> RequestResponse: """ C2 Command: Ransomware Configuration. @@ -282,8 +276,8 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): The class attribute self._host_ransomware_script will return None if the host does not have an instance of the RansomwareScript. - :payload MasqueradePacket: The incoming INPUT command. - :type Masquerade Packet: MasqueradePacket. + :payload C2Packet: The incoming INPUT command. + :type Masquerade Packet: C2Packet. :return: Returns the Request Response returned by the Terminal execute method. :rtype: Request Response """ @@ -299,7 +293,7 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): ) ) - def _command_ransomware_launch(self, payload: MasqueradePacket) -> RequestResponse: + def _command_ransomware_launch(self, payload: C2Packet) -> RequestResponse: """ C2 Command: Ransomware Launch. @@ -307,8 +301,8 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): This request is then sent to the terminal service in order to be executed. - :payload MasqueradePacket: The incoming INPUT command. - :type Masquerade Packet: MasqueradePacket. + :payload C2Packet: The incoming INPUT command. + :type Masquerade Packet: C2Packet. :return: Returns the Request Response returned by the Terminal execute method. :rtype: Request Response """ @@ -319,15 +313,15 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): ) return RequestResponse.from_bool(self._host_ransomware_script.attack()) - def _command_terminal(self, payload: MasqueradePacket) -> RequestResponse: + def _command_terminal(self, payload: C2Packet) -> RequestResponse: """ C2 Command: Terminal. Creates a request that executes a terminal command. This request is then sent to the terminal service in order to be executed. - :payload MasqueradePacket: The incoming INPUT command. - :type Masquerade Packet: MasqueradePacket. + :payload C2Packet: The incoming INPUT command. + :type Masquerade Packet: C2Packet. :return: Returns the Request Response returned by the Terminal execute method. :rtype: Request Response """ @@ -368,7 +362,7 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): self.remote_terminal_session is None return RequestResponse(status="success", data=terminal_output) - def _handle_keep_alive(self, payload: MasqueradePacket, session_id: Optional[str]) -> bool: + def _handle_keep_alive(self, payload: C2Packet, session_id: Optional[str]) -> bool: """ Handles receiving and sending keep alive payloads. This method is only called if we receive a keep alive. @@ -395,7 +389,7 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): if self.keep_alive_attempted is True: self.c2_connection_active = True # Sets the connection to active self.keep_alive_inactivity = 0 # Sets the keep alive inactivity to zero - self.current_c2_session = self.software_manager.session_manager.sessions_by_uuid[session_id] + self.c2_session = self.software_manager.session_manager.sessions_by_uuid[session_id] # We set keep alive_attempted here to show that we've achieved connection. self.keep_alive_attempted = False @@ -412,42 +406,7 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): # If this method returns true then we have sent successfully sent a keep alive. return self._send_keep_alive(session_id) - 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 a keep alive response packet is received then the ``keep_alive_inactivity`` attribute is reset. - - Therefore, if ``keep_alive_inactivity`` attribute is not 0 after a keep alive is sent - then the connection is considered severed and c2 beacon will shut down. - - :param timestep: The current timestep of the simulation. - :type timestep: Int - :return bool: Returns false if connection was lost. Returns True if connection is active or re-established. - :rtype bool: - """ - super().apply_timestep(timestep=timestep) - self.keep_alive_attempted = False # Resetting keep alive sent. - 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.c2_connection_active = False - self.clear_connections() - # TODO: Shouldn't this close() method also set the health state to 'UNUSED'? - self.close() - return - - def _check_c2_connection(self, timestep: int) -> bool: + def _confirm_connection(self, timestep: int) -> bool: """Checks the suitability of the current C2 Server connection. If a connection cannot be confirmed then this method will return false otherwise true. @@ -457,20 +416,23 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): :return: Returns False if connection was lost. Returns True if connection is active or re-established. :rtype bool: """ - if self.keep_alive_inactivity == self.keep_alive_frequency: + self.keep_alive_attempted = False # Resetting keep alive sent. + if self.keep_alive_inactivity == self.c2_config.keep_alive_frequency: self.sys_log.info( f"{self.name}: Attempting to Send Keep Alive to {self.c2_remote_connection} at timestep {timestep}." ) - self._send_keep_alive(session_id=self.current_c2_session.uuid) + self._send_keep_alive(session_id=self.c2_session.uuid) if self.keep_alive_inactivity != 0: self.sys_log.warning( f"{self.name}: Did not receive keep alive from c2 Server. Connection considered severed." ) + self._reset_c2_connection() + self.close() return False return True # Defining this abstract method from Abstract C2 - def _handle_command_output(self, payload: MasqueradePacket): + def _handle_command_output(self, payload: C2Packet): """C2 Beacons currently does not need to handle output commands coming from the C2 Servers.""" self.sys_log.warning(f"{self.name}: C2 Beacon received an unexpected OUTPUT payload: {payload}.") pass @@ -520,9 +482,9 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): self.c2_connection_active, self.c2_remote_connection, self.keep_alive_inactivity, - self.keep_alive_frequency, - self.current_masquerade_protocol, - self.current_masquerade_port, + self.c2_config.keep_alive_frequency, + self.c2_config.masquerade_protocol, + self.c2_config.masquerade_port, ] ) print(table) diff --git a/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py b/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py index 211da210..e4bf3302 100644 --- a/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py +++ b/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py @@ -6,7 +6,7 @@ from pydantic import validate_call from primaite.interface.request import RequestFormat, RequestResponse from primaite.simulator.core import RequestManager, RequestType -from primaite.simulator.network.protocols.masquerade import MasqueradePacket +from primaite.simulator.network.protocols.masquerade import C2Packet from primaite.simulator.system.applications.red_applications.c2.abstract_c2 import AbstractC2, C2Command, C2Payload @@ -96,18 +96,18 @@ class C2Server(AbstractC2, identifier="C2Server"): kwargs["name"] = "C2Server" super().__init__(**kwargs) - def _handle_command_output(self, payload: MasqueradePacket) -> bool: + def _handle_command_output(self, payload: C2Packet) -> bool: """ Handles the parsing of C2 Command Output from C2 Traffic (Masquerade Packets). - Parses the Request Response within MasqueradePacket's payload attribute (Inherited from Data packet). + Parses the Request Response within C2Packet's payload attribute (Inherited from Data packet). The class attribute self.current_command_output is then set to this Request Response. If the payload attribute does not contain a RequestResponse, then an error will be raised in syslog and the self.current_command_output is updated to reflect the error. :param payload: The OUTPUT C2 Payload - :type payload: MasqueradePacket + :type payload: C2Packet :return: Returns True if the self.current_command_output is currently updated, false otherwise. :rtype Bool: """ @@ -123,7 +123,7 @@ class C2Server(AbstractC2, identifier="C2Server"): self.current_command_output = command_output return True - def _handle_keep_alive(self, payload: MasqueradePacket, session_id: Optional[str]) -> bool: + def _handle_keep_alive(self, payload: C2Packet, session_id: Optional[str]) -> bool: """ Handles receiving and sending keep alive payloads. This method is only called if we receive a keep alive. @@ -137,7 +137,7 @@ class C2Server(AbstractC2, identifier="C2Server"): Returns True if a keep alive was successfully sent or already has been sent this timestep. :param payload: The Keep Alive payload received. - :type payload: MasqueradePacket + :type payload: C2Packet :param session_id: The transport session_id that the payload is originating from. :type session_id: str :return: True if successfully handled, false otherwise. @@ -146,7 +146,7 @@ class C2Server(AbstractC2, identifier="C2Server"): self.sys_log.info(f"{self.name}: Keep Alive Received. Attempting to resolve the remote connection details.") self.c2_connection_active = True # Sets the connection to active - self.current_c2_session = self.software_manager.session_manager.sessions_by_uuid[session_id] + self.c2_session = self.software_manager.session_manager.sessions_by_uuid[session_id] if self._resolve_keep_alive(payload, session_id) == False: self.sys_log.warning(f"{self.name}: Keep Alive Could not be resolved correctly. Refusing Keep Alive.") @@ -205,7 +205,7 @@ class C2Server(AbstractC2, identifier="C2Server"): status="failure", data={"Reason": "C2 Beacon has yet to establish connection. Unable to send command."} ) - if self.current_c2_session is None: + if self.c2_session is None: self.sys_log.warning(f"{self.name}: C2 Beacon cannot be reached. Rejecting command.") return RequestResponse( status="failure", data={"Reason": "C2 Beacon cannot be reached. Unable to send command."} @@ -217,18 +217,22 @@ class C2Server(AbstractC2, identifier="C2Server"): if self.send( payload=command_packet, dest_ip_address=self.c2_remote_connection, - session_id=self.current_c2_session.uuid, - dest_port=self.current_masquerade_port, - ip_protocol=self.current_masquerade_protocol, + session_id=self.c2_session.uuid, + dest_port=self.c2_config.masquerade_port, + ip_protocol=self.c2_config.masquerade_protocol, ): self.sys_log.info(f"{self.name}: Successfully sent {given_command}.") self.sys_log.info(f"{self.name}: Awaiting command response {given_command}.") # If the command output was handled currently, the self.current_command_output will contain the RequestResponse. + if self.current_command_output is None: + return RequestResponse( + status="failure", data={"Reason": "Command sent to the C2 Beacon but no response was ever received."} + ) return self.current_command_output - # TODO: Probably could move this as a class method in MasqueradePacket. - def _craft_packet(self, given_command: C2Command, command_options: Dict) -> MasqueradePacket: + # TODO: Probably could move this as a class method in C2Packet. + def _craft_packet(self, given_command: C2Command, command_options: Dict) -> C2Packet: """ Creates and returns a Masquerade Packet using the arguments given. @@ -238,12 +242,13 @@ class C2Server(AbstractC2, identifier="C2Server"): :type given_command: C2Command. :param command_options: The relevant C2 Beacon parameters.F :type command_options: Dict - :return: Returns the construct MasqueradePacket - :rtype: MasqueradePacket + :return: Returns the construct C2Packet + :rtype: C2Packet """ - constructed_packet = MasqueradePacket( - masquerade_protocol=self.current_masquerade_protocol, - masquerade_port=self.current_masquerade_port, + constructed_packet = C2Packet( + masquerade_protocol=self.c2_config.masquerade_protocol, + masquerade_port=self.c2_config.masquerade_port, + keep_alive_frequency=self.c2_config.keep_alive_frequency, payload_type=C2Payload.INPUT, command=given_command, payload=command_options, @@ -281,20 +286,41 @@ class C2Server(AbstractC2, identifier="C2Server"): [ self.c2_connection_active, self.c2_remote_connection, - self.current_masquerade_protocol, - self.current_masquerade_port, + self.c2_config.masquerade_protocol, + self.c2_config.masquerade_port, ] ) print(table) # Abstract method inherited from abstract C2 - Not currently utilised. - def _handle_command_input(self, payload: MasqueradePacket) -> None: + def _handle_command_input(self, payload: C2Packet) -> None: """Defining this method (Abstract method inherited from abstract C2) in order to instantiate the class. C2 Servers currently do not receive input commands coming from the C2 Beacons. - :param payload: The incoming MasqueradePacket - :type payload: MasqueradePacket. + :param payload: The incoming C2Packet + :type payload: C2Packet. """ self.sys_log.warning(f"{self.name}: C2 Server received an unexpected INPUT payload: {payload}") pass + + def _confirm_connection(self, timestep: int) -> bool: + """Checks the suitability of the current C2 Beacon connection. + + If a C2 Server has not received a keep alive within the current set + keep alive frequency (self._keep_alive_frequency) then the C2 beacons + connection is considered dead and any commands will be rejected. + + :param timestep: The current timestep of the simulation. + :type timestep: Int + :return: Returns False if the C2 beacon is considered dead. Otherwise True. + :rtype bool: + """ + if self.keep_alive_inactivity > self.c2_config.keep_alive_frequency: + self.sys_log.debug( + f"{self.name}: Failed to receive expected keep alive from {self.c2_remote_connection} at {timestep}." + ) + self.sys_log.info(f"{self.name}: C2 Beacon connection considered dead due to inactivity.") + self._reset_c2_connection() + return False + return True diff --git a/tests/integration_tests/system/red_applications/test_c2_suite.py b/tests/integration_tests/system/red_applications/test_c2_suite_integration.py similarity index 71% rename from tests/integration_tests/system/red_applications/test_c2_suite.py rename to tests/integration_tests/system/red_applications/test_c2_suite_integration.py index 9b799ff5..ab609cb0 100644 --- a/tests/integration_tests/system/red_applications/test_c2_suite.py +++ b/tests/integration_tests/system/red_applications/test_c2_suite_integration.py @@ -17,7 +17,7 @@ from primaite.simulator.network.transmission.transport_layer import Port from primaite.simulator.system.applications.application import ApplicationOperatingState from primaite.simulator.system.applications.database_client import DatabaseClient from primaite.simulator.system.applications.red_applications.c2.c2_beacon import C2Beacon -from primaite.simulator.system.applications.red_applications.c2.c2_server import C2Server +from primaite.simulator.system.applications.red_applications.c2.c2_server import C2Command, C2Server from primaite.simulator.system.applications.red_applications.ransomware_script import RansomwareScript from primaite.simulator.system.services.database.database_service import DatabaseService from primaite.simulator.system.services.dns.dns_server import DNSServer @@ -29,6 +29,7 @@ def basic_network() -> Network: network = Network() # Creating two generic nodes for the C2 Server and the C2 Beacon. + node_a = Computer( hostname="node_a", ip_address="192.168.0.2", @@ -43,12 +44,24 @@ def basic_network() -> Network: node_b = Computer( hostname="node_b", ip_address="192.168.255.2", - subnet_mask="255.255.255.252", + subnet_mask="255.255.255.248", default_gateway="192.168.255.1", start_up_duration=0, ) + node_b.power_on() node_b.software_manager.install(software_class=C2Beacon) + + # Creating a generic computer for testing remote terminal connections. + node_c = Computer( + hostname="node_c", + ip_address="192.168.255.3", + subnet_mask="255.255.255.248", + default_gateway="192.168.255.1", + start_up_duration=0, + ) + node_c.power_on() + # Creating a router to sit between node 1 and node 2. router = Router(hostname="router", num_ports=3, start_up_duration=0) # Default allow all. @@ -66,35 +79,43 @@ def basic_network() -> Network: switch_2.power_on() network.connect(endpoint_a=router.network_interface[2], endpoint_b=switch_2.network_interface[6]) - router.configure_port(port=2, ip_address="192.168.255.1", subnet_mask="255.255.255.252") + router.configure_port(port=2, ip_address="192.168.255.1", subnet_mask="255.255.255.248") router.enable_port(1) router.enable_port(2) # Connecting the node to each switch network.connect(node_a.network_interface[1], switch_1.network_interface[1]) - network.connect(node_b.network_interface[1], switch_2.network_interface[1]) + network.connect(node_c.network_interface[1], switch_2.network_interface[2]) return network +def setup_c2(given_network: Network): + """Installs the C2 Beacon & Server, configures and then returns.""" + computer_a: Computer = given_network.get_node_by_hostname("node_a") + c2_server: C2Server = computer_a.software_manager.software.get("C2Server") + computer_a.software_manager.install(DatabaseService) + computer_a.software_manager.software["DatabaseService"].start() + + computer_b: Computer = given_network.get_node_by_hostname("node_b") + c2_beacon: C2Beacon = computer_b.software_manager.software.get("C2Beacon") + computer_b.software_manager.install(DatabaseClient) + computer_b.software_manager.software["DatabaseClient"].configure(server_ip_address=IPv4Address("192.168.0.2")) + computer_b.software_manager.software["DatabaseClient"].run() + + c2_beacon.configure(c2_server_ip_address="192.168.0.2", keep_alive_frequency=2) + c2_server.run() + c2_beacon.establish() + + return given_network, computer_a, c2_server, computer_b, c2_beacon + + def test_c2_suite_setup_receive(basic_network): """Test that C2 Beacon can successfully establish connection with the C2 Server.""" network: Network = basic_network - computer_a: Computer = network.get_node_by_hostname("node_a") - c2_server: C2Server = computer_a.software_manager.software.get("C2Server") - - computer_b: Computer = network.get_node_by_hostname("node_b") - c2_beacon: C2Beacon = computer_b.software_manager.software.get("C2Beacon") - - # Assert that the c2 beacon configure correctly. - c2_beacon.configure(c2_server_ip_address="192.168.0.2") - assert c2_beacon.c2_remote_connection == IPv4Address("192.168.0.2") - - c2_server.run() - c2_beacon.establish() - + network, computer_a, c2_server, computer_b, c2_beacon = setup_c2(network) # Asserting that the c2 beacon has established a c2 connection assert c2_beacon.c2_connection_active is True @@ -112,15 +133,7 @@ def test_c2_suite_setup_receive(basic_network): def test_c2_suite_keep_alive_inactivity(basic_network): """Tests that C2 Beacon disconnects from the C2 Server after inactivity.""" network: Network = basic_network - computer_a: Computer = network.get_node_by_hostname("node_a") - c2_server: C2Server = computer_a.software_manager.software.get("C2Server") - - computer_b: Computer = network.get_node_by_hostname("node_b") - c2_beacon: C2Beacon = computer_b.software_manager.software.get("C2Beacon") - - c2_beacon.configure(c2_server_ip_address="192.168.0.2", keep_alive_frequency=2) - c2_server.run() - c2_beacon.establish() + network, computer_a, c2_server, computer_b, c2_beacon = setup_c2(network) c2_beacon.apply_timestep(0) assert c2_beacon.keep_alive_inactivity == 1 @@ -133,21 +146,21 @@ def test_c2_suite_keep_alive_inactivity(basic_network): # Now we turn off the c2 server (Thus preventing a keep alive) c2_server.close() c2_beacon.apply_timestep(2) + + assert c2_beacon.keep_alive_inactivity == 1 + c2_beacon.apply_timestep(3) - assert c2_beacon.keep_alive_inactivity == 2 + + # C2 Beacon resets it's connections back to default. + assert c2_beacon.keep_alive_inactivity == 0 assert c2_beacon.c2_connection_active == False assert c2_beacon.operating_state == ApplicationOperatingState.CLOSED def test_c2_suite_configure_request(basic_network): """Tests that the request system can be used to successfully setup a c2 suite.""" - # Setting up the network: network: Network = basic_network - computer_a: Computer = network.get_node_by_hostname("node_a") - c2_server: C2Server = computer_a.software_manager.software.get("C2Server") - - computer_b: Computer = network.get_node_by_hostname("node_b") - c2_beacon: C2Beacon = computer_b.software_manager.software.get("C2Beacon") + network, computer_a, c2_server, computer_b, c2_beacon = setup_c2(network) # Testing Via Requests: c2_server.run() @@ -173,20 +186,7 @@ def test_c2_suite_ransomware_commands(basic_network): """Tests the Ransomware commands can be used to configure & launch ransomware via Requests.""" # Setting up the network: network: Network = basic_network - computer_a: Computer = network.get_node_by_hostname("node_a") - c2_server: C2Server = computer_a.software_manager.software.get("C2Server") - computer_a.software_manager.install(DatabaseService) - computer_a.software_manager.software["DatabaseService"].start() - - computer_b: Computer = network.get_node_by_hostname("node_b") - c2_beacon: C2Beacon = computer_b.software_manager.software.get("C2Beacon") - computer_b.software_manager.install(DatabaseClient) - computer_b.software_manager.software["DatabaseClient"].configure(server_ip_address=IPv4Address("192.168.0.2")) - computer_b.software_manager.software["DatabaseClient"].run() - - c2_beacon.configure(c2_server_ip_address="192.168.0.2", keep_alive_frequency=2) - c2_server.run() - c2_beacon.establish() + network, computer_a, c2_server, computer_b, c2_beacon = setup_c2(network) # Testing Via Requests: computer_b.software_manager.install(software_class=RansomwareScript) @@ -208,18 +208,10 @@ def test_c2_suite_acl_block(basic_network): """Tests that C2 Beacon disconnects from the C2 Server after blocking ACL rules.""" network: Network = basic_network - computer_a: Computer = network.get_node_by_hostname("node_a") - c2_server: C2Server = computer_a.software_manager.software.get("C2Server") - - computer_b: Computer = network.get_node_by_hostname("node_b") - c2_beacon: C2Beacon = computer_b.software_manager.software.get("C2Beacon") + network, computer_a, c2_server, computer_b, c2_beacon = setup_c2(network) router: Router = network.get_node_by_hostname("router") - c2_beacon.configure(c2_server_ip_address="192.168.0.2", keep_alive_frequency=2) - c2_server.run() - c2_beacon.establish() - c2_beacon.apply_timestep(0) assert c2_beacon.keep_alive_inactivity == 1 @@ -233,10 +225,53 @@ def test_c2_suite_acl_block(basic_network): c2_beacon.apply_timestep(2) c2_beacon.apply_timestep(3) - assert c2_beacon.keep_alive_inactivity == 2 + + # C2 Beacon resets after unable to maintain contact. + + assert c2_beacon.keep_alive_inactivity == 0 assert c2_beacon.c2_connection_active == False assert c2_beacon.operating_state == ApplicationOperatingState.CLOSED -def test_c2_suite_terminal(basic_network): - """Tests the Ransomware commands can be used to configure & launch ransomware via Requests.""" +def test_c2_suite_terminal_command_file_creation(basic_network): + """Tests the C2 Terminal command can be used on local and remote.""" + network: Network = basic_network + network, computer_a, c2_server, computer_b, c2_beacon = setup_c2(network) + computer_c: Computer = network.get_node_by_hostname("node_c") + + # Asserting to demonstrate that the test files don't exist: + assert ( + computer_c.software_manager.file_system.access_file(folder_name="test_folder", file_name="test_file") == False + ) + + assert ( + computer_b.software_manager.file_system.access_file(folder_name="test_folder", file_name="test_file") == False + ) + + # Testing that we can create the test file and folders via the terminal command (Local C2 Terminal). + + # Local file/folder creation commands. + file_create_command = { + "commands": [ + ["file_system", "create", "folder", "test_folder"], + ["file_system", "create", "file", "test_folder", "test_file", "True"], + ], + "username": "admin", + "password": "admin", + "ip_address": None, + } + + c2_server._send_command(C2Command.TERMINAL, command_options=file_create_command) + + assert computer_b.software_manager.file_system.access_file(folder_name="test_folder", file_name="test_file") == True + assert c2_beacon.local_terminal_session is not None + + # Testing that we can create the same test file/folders via on node 3 via a remote terminal. + + # node_c's IP is 192.168.255.3 + file_create_command.update({"ip_address": "192.168.255.3"}) + + c2_server._send_command(C2Command.TERMINAL, command_options=file_create_command) + + assert computer_c.software_manager.file_system.access_file(folder_name="test_folder", file_name="test_file") == True + assert c2_beacon.remote_terminal_session is not None diff --git a/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_c2_suite.py b/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_c2_suite.py new file mode 100644 index 00000000..a790081f --- /dev/null +++ b/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_c2_suite.py @@ -0,0 +1,138 @@ +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +import pytest + +from primaite.simulator.network.container import Network +from primaite.simulator.network.hardware.nodes.host.computer import Computer +from primaite.simulator.network.hardware.nodes.host.server import Server +from primaite.simulator.system.applications.application import ApplicationOperatingState +from primaite.simulator.system.applications.red_applications.c2.c2_beacon import C2Beacon +from primaite.simulator.system.applications.red_applications.c2.c2_server import C2Command, C2Server + + +@pytest.fixture(scope="function") +def basic_c2_network() -> Network: + network = Network() + + # Creating two generic nodes for the C2 Server and the C2 Beacon. + + computer_a = Computer( + hostname="computer_a", + ip_address="192.168.0.1", + subnet_mask="255.255.255.252", + start_up_duration=0, + ) + computer_a.power_on() + computer_a.software_manager.install(software_class=C2Server) + + computer_b = Computer( + hostname="computer_b", ip_address="192.168.0.2", subnet_mask="255.255.255.252", start_up_duration=0 + ) + + computer_b.power_on() + computer_b.software_manager.install(software_class=C2Beacon) + + network.connect(endpoint_a=computer_a.network_interface[1], endpoint_b=computer_b.network_interface[1]) + return network + + +def setup_c2(given_network: Network): + """Installs the C2 Beacon & Server, configures and then returns.""" + network: Network = given_network + + computer_a: Computer = network.get_node_by_hostname("computer_a") + computer_b: Computer = network.get_node_by_hostname("computer_b") + + c2_beacon: C2Beacon = computer_b.software_manager.software.get("C2Beacon") + c2_server: C2Server = computer_a.software_manager.software.get("C2Server") + + c2_beacon.configure(c2_server_ip_address="192.168.0.1", keep_alive_frequency=2) + c2_server.run() + c2_beacon.establish() + + return network, computer_a, c2_server, computer_b, c2_beacon + + +def test_c2_handle_server_disconnect(basic_c2_network): + """Tests that the C2 suite will be able handle the c2 server application closing.""" + network: Network = basic_c2_network + network, computer_a, c2_server, computer_b, c2_beacon = setup_c2(network) + + assert c2_beacon.c2_connection_active is True + + ##### C2 Server disconnecting. + + # Closing the C2 Server + c2_server.close() + + # Applying 10 timesteps to trigger C2 beacon keep alive + + for i in range(10): + network.apply_timestep(i) + + assert c2_beacon.c2_connection_active is False + assert c2_beacon.operating_state is ApplicationOperatingState.CLOSED + + # C2 Beacon disconnected. + + network: Network = basic_c2_network + + network, computer_a, c2_server, computer_b, c2_beacon = setup_c2(network) + + +def test_c2_handle_beacon_disconnect(basic_c2_network): + """Tests that the C2 suite will be able handle the c2 beacon application closing.""" + network: Network = basic_c2_network + network, computer_a, c2_server, computer_b, c2_beacon = setup_c2(network) + + assert c2_server.c2_connection_active is True + + # Closing the C2 beacon + + c2_beacon.close() + + assert c2_beacon.operating_state is ApplicationOperatingState.CLOSED + + # Attempting a simple C2 Server command: + file_create_command = { + "commands": [["file_system", "create", "folder", "test_folder"]], + "username": "admin", + "password": "admin", + "ip_address": None, + } + + command_request_response = c2_server._send_command(C2Command.TERMINAL, command_options=file_create_command) + + assert command_request_response.status == "failure" + + # Despite the command failing - The C2 Server will still consider the beacon alive + # Until it does not respond within the keep alive frequency set in the last keep_alive. + assert c2_server.c2_connection_active is True + + # Stepping 6 timesteps in order for the C2 server to consider the beacon dead. + for i in range(6): + network.apply_timestep(i) + + assert c2_server.c2_connection_active is False + + +# TODO: Finalise and complete these tests. + + +def test_c2_handle_switching_port(basic_c2_network): + """Tests that the C2 suite will be able handle switching destination/src port.""" + network: Network = basic_c2_network + + network, computer_a, c2_server, computer_b, c2_beacon = setup_c2(network) + + # Asserting that the c2 beacon has established a c2 connection + assert c2_beacon.c2_connection_active is True + + +def test_c2_handle_switching_frequency(basic_c2_network): + """Tests that the C2 suite will be able handle switching keep alive frequency.""" + network: Network = basic_c2_network + + network, computer_a, c2_server, computer_b, c2_beacon = setup_c2(network) + + # Asserting that the c2 beacon has established a c2 connection + assert c2_beacon.c2_connection_active is True From ce3805cd15c0bdece670ef42fbd34b2f91de5f5d Mon Sep 17 00:00:00 2001 From: Archer Bowen Date: Mon, 12 Aug 2024 10:47:56 +0100 Subject: [PATCH 21/46] #2689 Updated c2 tests significantly and improved quality of debug logging. --- .../red_applications/c2/abstract_c2.py | 22 +- .../red_applications/c2/c2_beacon.py | 4 + .../red_applications/c2/c2_server.py | 8 +- .../test_c2_suite_integration.py | 190 +++++++++++++++++- .../_red_applications/test_c2_suite.py | 71 ++++++- 5 files changed, 281 insertions(+), 14 deletions(-) diff --git a/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py b/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py index a00b8570..3c9080b3 100644 --- a/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py +++ b/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py @@ -13,11 +13,6 @@ from primaite.simulator.system.applications.application import Application, Appl from primaite.simulator.system.core.session_manager import Session from primaite.simulator.system.software import SoftwareHealthState -# TODO: -# Create test that leverage all the functionality needed for the different TAPs -# Create a .RST doc -# Potentially? A notebook which demonstrates a custom red agent using the c2 server for various means. - class C2Command(Enum): """Enumerations representing the different commands the C2 suite currently supports.""" @@ -196,11 +191,11 @@ class AbstractC2(Application, identifier="AbstractC2"): # (Using NOT to improve code readability) if self.c2_remote_connection is None: self.sys_log.error( - f"{self.name}: Unable to Establish connection as the C2 Server's IP Address has not been given." + f"{self.name}: Unable to establish connection as the C2 Server's IP Address has not been configured." ) if not self._can_perform_network_action(): - self.sys_log.warning(f"{self.name}: Unable to perform network actions.") + self.sys_log.warning(f"{self.name}: Unable to perform network actions. Unable to send Keep Alive.") return False # We also Pass masquerade proto`col/port so that the c2 server can reply on the correct protocol/port. @@ -223,12 +218,14 @@ class AbstractC2(Application, identifier="AbstractC2"): self.keep_alive_sent = True self.sys_log.info(f"{self.name}: Keep Alive sent to {self.c2_remote_connection}") self.sys_log.debug( - f"{self.name}: on {self.c2_config.masquerade_port} via {self.c2_config.masquerade_protocol}" + f"{self.name}: Keep Alive sent to {self.c2_remote_connection}" + f"Using Masquerade Port: {self.c2_config.masquerade_port}" + f"Using Masquerade Protocol: {self.c2_config.masquerade_protocol}" ) return True else: self.sys_log.warning( - f"{self.name}: failed to send a Keep Alive. The node may be unable to access networking resources." + f"{self.name}: Failed to send a Keep Alive. The node may be unable to access networking resources." ) return False @@ -262,6 +259,13 @@ class AbstractC2(Application, identifier="AbstractC2"): self.c2_config.masquerade_protocol = payload.masquerade_protocol self.c2_config.keep_alive_frequency = payload.keep_alive_frequency + self.sys_log.debug( + f"{self.name}: C2 Config Resolved Config from Keep Alive:" + f"Masquerade Port: {self.c2_config.masquerade_port}" + f"Masquerade Protocol: {self.c2_config.masquerade_protocol}" + f"Keep Alive Frequency: {self.c2_config.keep_alive_frequency}" + ) + # This statement is intended to catch on the C2 Application that is listening for connection. (C2 Beacon) if self.c2_remote_connection is None: self.sys_log.debug(f"{self.name}: Attempting to configure remote C2 connection based off received output.") 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 55dd1474..8052d0f2 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 @@ -168,6 +168,10 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): f"Masquerade Protocol: {masquerade_protocol}" f"Masquerade Port: {masquerade_port}" ) + # Send a keep alive to the C2 Server if we already have a keep alive. + if self.c2_connection_active is True: + self.sys_log.info(f"{self.name}: Updating C2 Server with updated C2 configuration.") + self._send_keep_alive(self.c2_session.uuid if not None else None) return True def establish(self) -> bool: diff --git a/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py b/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py index e4bf3302..6b51f8c7 100644 --- a/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py +++ b/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py @@ -317,10 +317,12 @@ class C2Server(AbstractC2, identifier="C2Server"): :rtype bool: """ if self.keep_alive_inactivity > self.c2_config.keep_alive_frequency: - self.sys_log.debug( - f"{self.name}: Failed to receive expected keep alive from {self.c2_remote_connection} at {timestep}." - ) self.sys_log.info(f"{self.name}: C2 Beacon connection considered dead due to inactivity.") + self.sys_log.debug( + f"{self.name}: Did not receive expected keep alive connection from {self.c2_remote_connection}" + f"{self.name}: Expected at timestep: {timestep} due to frequency: {self.c2_config.keep_alive_frequency}" + f"{self.name}: Last Keep Alive received at {(timestep - self.keep_alive_inactivity)}" + ) self._reset_c2_connection() return False return True diff --git a/tests/integration_tests/system/red_applications/test_c2_suite_integration.py b/tests/integration_tests/system/red_applications/test_c2_suite_integration.py index ab609cb0..56b354d7 100644 --- a/tests/integration_tests/system/red_applications/test_c2_suite_integration.py +++ b/tests/integration_tests/system/red_applications/test_c2_suite_integration.py @@ -10,7 +10,7 @@ from primaite.simulator.file_system.file_system_item_abc import FileSystemItemHe from primaite.simulator.network.container import Network from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.network.hardware.nodes.host.server import Server -from primaite.simulator.network.hardware.nodes.network.router import ACLAction, Router +from primaite.simulator.network.hardware.nodes.network.router import AccessControlList, ACLAction, Router from primaite.simulator.network.hardware.nodes.network.switch import Switch from primaite.simulator.network.transmission.network_layer import IPProtocol from primaite.simulator.network.transmission.transport_layer import Port @@ -209,6 +209,8 @@ def test_c2_suite_acl_block(basic_network): network: Network = basic_network network, computer_a, c2_server, computer_b, c2_beacon = setup_c2(network) + computer_b.software_manager.install(software_class=RansomwareScript) + ransomware_config = {"server_ip_address": "192.168.0.2"} router: Router = network.get_node_by_hostname("router") @@ -275,3 +277,189 @@ def test_c2_suite_terminal_command_file_creation(basic_network): assert computer_c.software_manager.file_system.access_file(folder_name="test_folder", file_name="test_file") == True assert c2_beacon.remote_terminal_session is not None + + +def test_c2_suite_acl_bypass(basic_network): + """Tests that C2 Beacon can be reconfigured to connect C2 Server to bypass blocking ACL rules. + + 1. This Test first configures a router to block HTTP traffic and asserts the following: + 1. C2 Beacon and C2 Server are unable to maintain connection + 2. Traffic is confirmed to be blocked by the ACL rule. + + 2. Next the C2 Beacon is re-configured to use FTP which is permitted by the ACL and asserts the following; + 1. The C2 Beacon and C2 Server re-establish connection + 2. The ACL rule has not prevent any further traffic. + 3. A test file create command is sent & it's output confirmed + + 3. The ACL is then re-configured to block FTP traffic and asserts the following: + 1. C2 Beacon and C2 Server are unable to maintain connection + 2. Traffic is confirmed to be blocked by the ACL rule. + + 4. Next the C2 Beacon is re-configured to use HTTP which is permitted by the ACL and asserts the following; + 1. The C2 Beacon and C2 Server re-establish connection + 2. The ACL rule has not prevent any further traffic. + 3. A test file create command is sent & it's output confirmed + """ + + network: Network = basic_network + network, computer_a, c2_server, computer_b, c2_beacon = setup_c2(network) + router: Router = network.get_node_by_hostname("router") + + ################ Confirm Default Setup ######################### + + # Permitting all HTTP & FTP traffic + router.acl.add_rule(action=ACLAction.PERMIT, src_port=Port.HTTP, dst_port=Port.HTTP, position=0) + router.acl.add_rule(action=ACLAction.PERMIT, src_port=Port.FTP, dst_port=Port.FTP, position=1) + + c2_beacon.apply_timestep(0) + assert c2_beacon.keep_alive_inactivity == 1 + + # Keep Alive successfully sent and received upon the 2nd timestep. + c2_beacon.apply_timestep(1) + + assert c2_beacon.keep_alive_inactivity == 0 + assert c2_beacon.c2_connection_active == True + + ################ Denying HTTP Traffic ######################### + + # Now we add a HTTP blocking acl (Thus preventing a keep alive) + router.acl.add_rule(action=ACLAction.DENY, src_port=Port.HTTP, dst_port=Port.HTTP, position=0) + blocking_acl: AccessControlList = router.acl.acl[0] + + # Asserts to show the C2 Suite is unable to maintain connection: + + network.apply_timestep(2) + network.apply_timestep(3) + + c2_packets_blocked = blocking_acl.match_count + assert c2_packets_blocked != 0 + assert c2_beacon.c2_connection_active is False + + # Stepping one more time to confirm that the C2 server drops its connection + network.apply_timestep(4) + assert c2_server.c2_connection_active is False + + ################ Configuring C2 to use FTP ##################### + + # Reconfiguring the c2 beacon to now use FTP + c2_beacon.configure( + c2_server_ip_address="192.168.0.2", + keep_alive_frequency=2, + masquerade_port=Port.FTP, + masquerade_protocol=IPProtocol.TCP, + ) + + c2_beacon.establish() + + ################ Confirming connection via FTP ##################### + + # Confirming we've re-established connection + + assert c2_beacon.c2_connection_active is True + assert c2_server.c2_connection_active is True + + # Confirming that we can send commands: + + ftp_file_create_command = { + "commands": [ + ["file_system", "create", "folder", "test_folder"], + ["file_system", "create", "file", "test_folder", "ftp_test_file", "True"], + ], + "username": "admin", + "password": "admin", + "ip_address": None, + } + c2_server._send_command(C2Command.TERMINAL, command_options=ftp_file_create_command) + assert ( + computer_b.software_manager.file_system.access_file(folder_name="test_folder", file_name="ftp_test_file") + == True + ) + + # Confirming we can maintain connection + + # Stepping twenty timesteps in the network + i = 4 # We're already at the 4th timestep (starting at timestep 4) + + for i in range(20): + network.apply_timestep(i) + + # Confirming HTTP ACL ineffectiveness (C2 Bypass) + + # Asserting that the ACL hasn't caught more traffic and the c2 connection is still active + assert c2_packets_blocked == blocking_acl.match_count + assert c2_server.c2_connection_active is True + assert c2_beacon.c2_connection_active is True + + ################ Denying FTP Traffic & Enable HTTP ######################### + + # Blocking FTP and re-permitting HTTP: + router.acl.add_rule(action=ACLAction.PERMIT, src_port=Port.HTTP, dst_port=Port.HTTP, position=0) + router.acl.add_rule(action=ACLAction.DENY, src_port=Port.FTP, dst_port=Port.FTP, position=1) + blocking_acl: AccessControlList = router.acl.acl[1] + + # Asserts to show the C2 Suite is unable to maintain connection: + + network.apply_timestep(25) + network.apply_timestep(26) + + c2_packets_blocked = blocking_acl.match_count + assert c2_packets_blocked != 0 + assert c2_beacon.c2_connection_active is False + + # Stepping one more time to confirm that the C2 server drops its connection + network.apply_timestep(27) + assert c2_server.c2_connection_active is False + + ################ Configuring C2 to use HTTP ##################### + + # Reconfiguring the c2 beacon to now use HTTP Again + c2_beacon.configure( + c2_server_ip_address="192.168.0.2", + keep_alive_frequency=2, + masquerade_port=Port.HTTP, + masquerade_protocol=IPProtocol.TCP, + ) + + c2_beacon.establish() + + ################ Confirming connection via HTTP ##################### + + # Confirming we've re-established connection + + assert c2_beacon.c2_connection_active is True + assert c2_server.c2_connection_active is True + + # Confirming that we can send commands + + http_file_create_command = { + "commands": [ + ["file_system", "create", "folder", "test_folder"], + ["file_system", "create", "file", "test_folder", "http_test_file", "True"], + ], + "username": "admin", + "password": "admin", + "ip_address": None, + } + c2_server._send_command(C2Command.TERMINAL, command_options=http_file_create_command) + assert ( + computer_b.software_manager.file_system.access_file(folder_name="test_folder", file_name="http_test_file") + == True + ) + + assert c2_beacon.c2_connection_active is True + assert c2_server.c2_connection_active is True + + # Confirming we can maintain connection + + # Stepping twenty timesteps in the network + i = 28 # We're already at the 28th timestep + + for i in range(20): + network.apply_timestep(i) + + # Confirming FTP ACL ineffectiveness (C2 Bypass) + + # Asserting that the ACL hasn't caught more traffic and the c2 connection is still active + assert c2_packets_blocked == blocking_acl.match_count + assert c2_server.c2_connection_active is True + assert c2_beacon.c2_connection_active is True diff --git a/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_c2_suite.py b/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_c2_suite.py index a790081f..ed408d14 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_c2_suite.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_c2_suite.py @@ -4,6 +4,8 @@ import pytest from primaite.simulator.network.container import Network from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.network.hardware.nodes.host.server import Server +from primaite.simulator.network.transmission.network_layer import IPProtocol +from primaite.simulator.network.transmission.transport_layer import Port from primaite.simulator.system.applications.application import ApplicationOperatingState from primaite.simulator.system.applications.red_applications.c2.c2_beacon import C2Beacon from primaite.simulator.system.applications.red_applications.c2.c2_server import C2Command, C2Server @@ -124,8 +126,38 @@ def test_c2_handle_switching_port(basic_c2_network): network, computer_a, c2_server, computer_b, c2_beacon = setup_c2(network) - # Asserting that the c2 beacon has established a c2 connection + # Asserting that the c2 applications have established a c2 connection assert c2_beacon.c2_connection_active is True + assert c2_server.c2_connection_active is True + + # Assert to confirm that both the C2 server and the C2 beacon are configured correctly. + assert c2_beacon.c2_config.keep_alive_frequency is 2 + assert c2_beacon.c2_config.masquerade_port is Port.HTTP + assert c2_beacon.c2_config.masquerade_protocol is IPProtocol.TCP + + assert c2_server.c2_config.keep_alive_frequency is 2 + assert c2_server.c2_config.masquerade_port is Port.HTTP + assert c2_server.c2_config.masquerade_protocol is IPProtocol.TCP + + # Configuring the C2 Beacon. + c2_beacon.configure( + c2_server_ip_address="192.168.0.1", + keep_alive_frequency=2, + masquerade_port=Port.FTP, + masquerade_protocol=IPProtocol.TCP, + ) + + # Asserting that the c2 applications have established a c2 connection + assert c2_beacon.c2_connection_active is True + assert c2_server.c2_connection_active is True + + # Assert to confirm that both the C2 server and the C2 beacon + # Have reconfigured their C2 settings. + assert c2_beacon.c2_config.masquerade_port is Port.FTP + assert c2_beacon.c2_config.masquerade_protocol is IPProtocol.TCP + + assert c2_server.c2_config.masquerade_port is Port.FTP + assert c2_server.c2_config.masquerade_protocol is IPProtocol.TCP def test_c2_handle_switching_frequency(basic_c2_network): @@ -136,3 +168,40 @@ def test_c2_handle_switching_frequency(basic_c2_network): # Asserting that the c2 beacon has established a c2 connection assert c2_beacon.c2_connection_active is True + network: Network = basic_c2_network + + network, computer_a, c2_server, computer_b, c2_beacon = setup_c2(network) + + # Asserting that the c2 applications have established a c2 connection + assert c2_beacon.c2_connection_active is True + assert c2_server.c2_connection_active is True + + # Assert to confirm that both the C2 server and the C2 beacon are configured correctly. + assert c2_beacon.c2_config.keep_alive_frequency is 2 + assert c2_server.c2_config.keep_alive_frequency is 2 + + # Configuring the C2 Beacon. + c2_beacon.configure(c2_server_ip_address="192.168.0.1", keep_alive_frequency=10) + + # Asserting that the c2 applications have established a c2 connection + assert c2_beacon.c2_connection_active is True + assert c2_server.c2_connection_active is True + + # Assert to confirm that both the C2 server and the C2 beacon + # Have reconfigured their C2 settings. + assert c2_beacon.c2_config.keep_alive_frequency is 10 + assert c2_server.c2_config.keep_alive_frequency is 10 + + # Now skipping 9 time steps to confirm keep alive inactivity + for i in range(9): + network.apply_timestep(i) + + # If the keep alive reconfiguration failed then the keep alive inactivity could never reach 9 + # As another keep alive would have already been sent. + assert c2_beacon.keep_alive_inactivity is 9 + assert c2_server.keep_alive_inactivity is 9 + + network.apply_timestep(10) + + assert c2_beacon.keep_alive_inactivity is 0 + assert c2_server.keep_alive_inactivity is 0 From cbf02ebf3224f30fbf85dd8ea2e12bf4f33ba41d Mon Sep 17 00:00:00 2001 From: Archer Bowen Date: Mon, 12 Aug 2024 14:16:21 +0100 Subject: [PATCH 22/46] #2689 Updated documentation and moved _craft_packet into abstract C2 --- .../system/applications/c2_suite.rst | 144 ++++++++++++++++-- .../Command-&-Control-E2E-Demonstration.ipynb | 14 +- .../red_applications/c2/abstract_c2.py | 123 +++++++++++---- .../red_applications/c2/c2_beacon.py | 26 ++-- .../red_applications/c2/c2_server.py | 68 +++------ .../test_c2_suite_integration.py | 8 +- .../_red_applications/test_c2_suite.py | 32 +++- 7 files changed, 306 insertions(+), 109 deletions(-) diff --git a/docs/source/simulation_components/system/applications/c2_suite.rst b/docs/source/simulation_components/system/applications/c2_suite.rst index e299bb0e..4d5f685a 100644 --- a/docs/source/simulation_components/system/applications/c2_suite.rst +++ b/docs/source/simulation_components/system/applications/c2_suite.rst @@ -34,7 +34,7 @@ Currently, the C2 Server offers three commands: +---------------------+---------------------------------------------------------------------------+ |RANSOMWARE_LAUNCH | Launches the installed ransomware script. | +---------------------+---------------------------------------------------------------------------+ -|TERMINAL_COMMAND | Executes a command via the terminal installed on the C2 Beacons Host. | +|TERMINAL | Executes a command via the terminal installed on the C2 Beacons Host. | +---------------------+---------------------------------------------------------------------------+ @@ -69,9 +69,17 @@ As mentioned, the C2 Suite is intended to grant Red Agents further flexibility w Adding to this, the following behaviour of the C2 beacon can be configured by users for increased domain randomisation: -- Frequency of C2 ``Keep Alive `` Communication`` -- C2 Communication Port -- C2 Communication Protocol ++---------------------+---------------------------------------------------------------------------+ +|Configuration Option | Option Meaning | ++=====================+===========================================================================+ +|c2_server_ip_address | The IP Address of the C2 Server. (The C2 Server must be running) | ++---------------------+---------------------------------------------------------------------------+ +|keep_alive_frequency | How often should the C2 Beacon confirm it's connection in timesteps. | ++---------------------+---------------------------------------------------------------------------+ +|masquerade_protocol | What protocol should the C2 traffic masquerade as? (HTTP, FTP or DNS) | ++---------------------+---------------------------------------------------------------------------+ +|masquerade_port | What port should the C2 traffic use? (TCP or UDP) | ++---------------------+---------------------------------------------------------------------------+ Implementation @@ -91,6 +99,7 @@ However, each host implements it's receive methods individually. - Receives the RequestResponse of the C2 Commands executed by C2 Beacon via ``C2Payload.OUTPUT``. +For further details and more in-depth examples please refer to the ``Command-&-Control notebook`` Examples ======== @@ -120,8 +129,8 @@ Python # C2 Application objects - c2_server_host = simulation_testing_network.get_node_by_hostname("node_a") - c2_beacon_host = simulation_testing_network.get_node_by_hostname("node_b") + c2_server_host: computer = simulation_testing_network.get_node_by_hostname("node_a") + c2_beacon_host: computer = simulation_testing_network.get_node_by_hostname("node_b") c2_server: C2Server = c2_server_host.software_manager.software["C2Server"] @@ -136,10 +145,125 @@ Python # Establishing connection c2_beacon.establish() - # Example command: Configuring Ransomware + # Example command: Creating a file - ransomware_config = {"server_ip_address": "1.1.1.1"} - c2_server._send_command(given_command=C2Command.RANSOMWARE_CONFIGURE, command_options=ransomware_config) + file_create_command = { + "commands": [ + ["file_system", "create", "folder", "test_folder"], + ["file_system", "create", "file", "test_folder", "example_file", "True"], + ], + "username": "admin", + "password": "admin", + "ip_address": None, + } + + c2_server.send_command(C2Command.TERMINAL, command_options=file_create_command) + + # Example commands: Installing and configuring Ransomware: + + ransomware_installation_command = { "commands": [ + ["software_manager","application","install","RansomwareScript"], + ], + "username": "admin", + "password": "admin", + "ip_address": None, + } + c2_server.send_command(given_command=C2Command.TERMINAL, command_options=ransomware_config) + + ransomware_config = {"server_ip_address": "192.168.0.10"} + + c2_server.send_command(given_command=C2Command.RANSOMWARE_CONFIGURE, command_options=ransomware_config) + + c2_beacon_host.software_manager.show() -For a more in-depth look at the command and control applications then please refer to the ``C2-Suite-E2E-Notebook``. +Via Configuration +""""""""""""""""" + +.. code-block:: yaml + + simulation: + network: + nodes: + - ref: example_computer_1 + hostname: computer_a + type: computer + ... + applications: + type: C2Server + ... + hostname: computer_b + type: computer + ... + # A C2 Beacon will not automatically connection to a C2 Server. + # Either an agent must use application_execute. + # Or a user must use .establish(). + applications: + type: C2Beacon + options: + c2_server_ip_address: ... + keep_alive_frequency: 5 + masquerade_protocol: tcp + masquerade_port: http + + + +C2 Beacon Configuration +======================= + +.. include:: ../common/common_configuration.rst + +.. |SOFTWARE_NAME| replace:: C2Beacon +.. |SOFTWARE_NAME_BACKTICK| replace:: ``C2Beacon`` + +``c2_server_ip_address`` +""""""""""""""""""""""" + +IP address of the ``C2Server`` that the C2 Beacon will use to establish connection. + +This must be a valid octet i.e. in the range of ``0.0.0.0`` and ``255.255.255.255``. + + +``Keep Alive Frequency`` +""""""""""""""""""""""" + +How often should the C2 Beacon confirm it's connection in timesteps. + +For example, if the keep alive Frequency is set to one then every single timestep +the C2 connection will be confirmed. + +It's worth noting that this may be useful option when investigating +network blue agent observation space. + +This must be a valid integer i.e ``10``. Defaults to ``5``. + + +``Masquerade Protocol`` +""""""""""""""""""""""" + +The protocol that the C2 Beacon will use to communicate to the C2 Server with. + +Currently only ``tcp`` and ``udp`` are valid masquerade protocol options. + +It's worth noting that this may be useful option to bypass ACL rules. + +This must be a string i.e ``udp``. Defaults to ``tcp``. + +_Please refer to the ``IPProtocol`` class for further reference._ + +``Masquerade Port`` +""""""""""""""""""" + +What port that the C2 Beacon will use to communicate to the C2 Server with. + +Currently only ``FTP``, ``HTTP`` and ``DNS`` are valid masquerade port options. + +It's worth noting that this may be useful option to bypass ACL rules. + +This must be a string i.e ``DNS``. Defaults to ``HTTP``. + +_Please refer to the ``IPProtocol`` class for further reference._ + + + +_The C2 Server does not currently offer any unique configuration options and will configure itself to match the C2 beacon's network behaviour._ diff --git a/src/primaite/notebooks/Command-&-Control-E2E-Demonstration.ipynb b/src/primaite/notebooks/Command-&-Control-E2E-Demonstration.ipynb index b41b9f2e..7ee1c5cf 100644 --- a/src/primaite/notebooks/Command-&-Control-E2E-Demonstration.ipynb +++ b/src/primaite/notebooks/Command-&-Control-E2E-Demonstration.ipynb @@ -633,7 +633,7 @@ "ransomware_install_command = {\"commands\":[[\"software_manager\", \"application\", \"install\", \"RansomwareScript\"]],\n", " \"username\": \"admin\",\n", " \"password\": \"admin\"}\n", - "c2_server._send_command(C2Command.TERMINAL, command_options=ransomware_install_command)\n" + "c2_server.send_command(C2Command.TERMINAL, command_options=ransomware_install_command)\n" ] }, { @@ -644,7 +644,7 @@ "source": [ "# Configuring the RansomwareScript\n", "ransomware_config = {\"server_ip_address\": \"192.168.1.14\", \"payload\": \"ENCRYPT\"}\n", - "c2_server._send_command(C2Command.RANSOMWARE_CONFIGURE, command_options=ransomware_config)" + "c2_server.send_command(C2Command.RANSOMWARE_CONFIGURE, command_options=ransomware_config)" ] }, { @@ -681,7 +681,7 @@ "source": [ "# Waiting for the ransomware to finish installing and then launching the RansomwareScript.\n", "blue_env.step(0)\n", - "c2_server._send_command(C2Command.RANSOMWARE_LAUNCH, command_options={})" + "c2_server.send_command(C2Command.RANSOMWARE_LAUNCH, command_options={})" ] }, { @@ -834,7 +834,7 @@ " \"password\": \"admin\"}\n", "\n", "c2_server: C2Server = client_1.software_manager.software[\"C2Server\"]\n", - "c2_server._send_command(C2Command.TERMINAL, command_options=ransomware_install_command)" + "c2_server.send_command(C2Command.TERMINAL, command_options=ransomware_install_command)" ] }, { @@ -922,7 +922,7 @@ " \"password\": \"admin\"}\n", "\n", "c2_server: C2Server = client_1.software_manager.software[\"C2Server\"]\n", - "c2_server._send_command(C2Command.TERMINAL, command_options=ransomware_install_command)" + "c2_server.send_command(C2Command.TERMINAL, command_options=ransomware_install_command)" ] }, { @@ -1007,8 +1007,8 @@ "blue_env.step(0)\n", "\n", "# Attempting to install and execute the ransomware script\n", - "c2_server._send_command(C2Command.TERMINAL, command_options=ransomware_install_command)\n", - "c2_server._send_command(C2Command.RANSOMWARE_LAUNCH, command_options={})" + "c2_server.send_command(C2Command.TERMINAL, command_options=ransomware_install_command)\n", + "c2_server.send_command(C2Command.RANSOMWARE_LAUNCH, command_options={})" ] }, { diff --git a/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py b/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py index 3c9080b3..f5fb0929 100644 --- a/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py +++ b/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py @@ -6,6 +6,7 @@ from typing import Dict, Optional from pydantic import BaseModel, Field, validate_call +from primaite.interface.request import RequestResponse from primaite.simulator.network.protocols.masquerade import C2Packet from primaite.simulator.network.transmission.network_layer import IPProtocol from primaite.simulator.network.transmission.transport_layer import Port @@ -53,6 +54,8 @@ class AbstractC2(Application, identifier="AbstractC2"): as well as providing the abstract methods for sending, receiving and parsing commands. Defaults to masquerading as HTTP (Port 80) via TCP. + + Please refer to the Command-&-Control notebook for an in-depth example of the C2 Suite. """ c2_connection_active: bool = False @@ -79,11 +82,46 @@ class AbstractC2(Application, identifier="AbstractC2"): masquerade_port: Port = Field(default=Port.HTTP) """The currently chosen port that the C2 traffic is masquerading as. Defaults at HTTP.""" - # The c2 beacon sets the c2_config through it's own internal method - configure (which is also used by agents) - # and then passes the config attributes to the c2 server via keep alives - # The c2 server parses the C2 configurations from keep alive traffic and sets the c2_config accordingly. + def _craft_packet( + self, c2_payload: C2Payload, c2_command: Optional[C2Command] = None, command_options: Optional[Dict] = {} + ) -> C2Packet: + """ + Creates and returns a Masquerade Packet using the parameters given. + + The packet uses the current c2 configuration and parameters given + to construct a C2 Packet. + + :param c2_payload: The type of C2 Traffic ot be sent + :type c2_payload: C2Payload + :param c2_command: The C2 command to be sent to the C2 Beacon. + :type c2_command: C2Command. + :param command_options: The relevant C2 Beacon parameters.F + :type command_options: Dict + :return: Returns the construct C2Packet + :rtype: C2Packet + """ + constructed_packet = C2Packet( + masquerade_protocol=self.c2_config.masquerade_protocol, + masquerade_port=self.c2_config.masquerade_port, + keep_alive_frequency=self.c2_config.keep_alive_frequency, + payload_type=c2_payload, + command=c2_command, + payload=command_options, + ) + return constructed_packet + c2_config: _C2_Opts = _C2_Opts() - """Holds the current configuration settings of the C2 Suite.""" + """ + Holds the current configuration settings of the C2 Suite. + + The C2 beacon initialise this class through it's internal configure method. + + The C2 Server when receiving a keep alive will initialise it's own configuration + to match that of the configuration settings passed in the keep alive through _resolve keep alive. + + If the C2 Beacon is reconfigured then a new keep alive is set which causes the + C2 beacon to reconfigure it's configuration settings. + """ def describe_state(self) -> Dict: """ @@ -187,27 +225,18 @@ class AbstractC2(Application, identifier="AbstractC2"): :returns: Returns True if a send alive was successfully sent. False otherwise. :rtype bool: """ - # Checking that the c2 application is capable of performing both actions and has an enabled NIC - # (Using NOT to improve code readability) - if self.c2_remote_connection is None: - self.sys_log.error( - f"{self.name}: Unable to establish connection as the C2 Server's IP Address has not been configured." + # Checking that the c2 application is capable of connecting to remote. + # Purely a safety guard clause. + if not (connection_status := self._check_connection()[0]): + self.sys_log.warning( + f"{self.name}: Unable to send keep alive due to c2 connection status: {connection_status}." ) - - if not self._can_perform_network_action(): - self.sys_log.warning(f"{self.name}: Unable to perform network actions. Unable to send Keep Alive.") return False - # We also Pass masquerade proto`col/port so that the c2 server can reply on the correct protocol/port. - # (This also lays the foundations for switching masquerade port/protocols mid episode.) - keep_alive_packet = C2Packet( - masquerade_protocol=self.c2_config.masquerade_protocol, - masquerade_port=self.c2_config.masquerade_port, - keep_alive_frequency=self.c2_config.keep_alive_frequency, - payload_type=C2Payload.KEEP_ALIVE, - command=None, - ) - # C2 Server will need to configure c2_remote_connection after it receives it's first keep alive. + # Passing our current C2 configuration in remain in sync. + keep_alive_packet = self._craft_packet(c2_payload=C2Payload.KEEP_ALIVE) + + # Sending the keep alive via the .send() method (as with all other applications.) if self.send( payload=keep_alive_packet, dest_ip_address=self.c2_remote_connection, @@ -215,6 +244,8 @@ class AbstractC2(Application, identifier="AbstractC2"): ip_protocol=self.c2_config.masquerade_protocol, session_id=session_id, ): + # Setting the keep_alive_sent guard condition to True. This is used to prevent packet storms. + # This prevents the _resolve_keep_alive method from calling this method again (until the next timestep.) self.keep_alive_sent = True self.sys_log.info(f"{self.name}: Keep Alive sent to {self.c2_remote_connection}") self.sys_log.debug( @@ -266,7 +297,7 @@ class AbstractC2(Application, identifier="AbstractC2"): f"Keep Alive Frequency: {self.c2_config.keep_alive_frequency}" ) - # This statement is intended to catch on the C2 Application that is listening for connection. (C2 Beacon) + # This statement is intended to catch on the C2 Application that is listening for connection. if self.c2_remote_connection is None: self.sys_log.debug(f"{self.name}: Attempting to configure remote C2 connection based off received output.") self.c2_remote_connection = IPv4Address(self.c2_session.with_ip_address) @@ -287,8 +318,14 @@ class AbstractC2(Application, identifier="AbstractC2"): self.c2_config.masquerade_protocol = IPProtocol.TCP @abstractmethod - def _confirm_connection(self, timestep: int) -> bool: - """Abstract method - Checks the suitability of the current C2 Server/Beacon connection.""" + def _confirm_remote_connection(self, timestep: int) -> bool: + """ + Abstract method - Confirms the suitability of the current C2 application remote connection. + + Each application will have perform different behaviour to confirm the remote connection. + + :return: Boolean. True if remote connection is confirmed, false otherwise. + """ def apply_timestep(self, timestep: int) -> None: """Apply a timestep to the c2_server & c2 beacon. @@ -316,5 +353,39 @@ class AbstractC2(Application, identifier="AbstractC2"): and self.health_state_actual is SoftwareHealthState.GOOD ): self.keep_alive_inactivity += 1 - self._confirm_connection(timestep) + self._confirm_remote_connection(timestep) return + + def _check_connection(self) -> tuple[bool, RequestResponse]: + """ + Validation method: Checks that the C2 application is capable of sending C2 Command input/output. + + Performs a series of connection validation to ensure that the C2 application is capable of + sending and responding to the remote c2 connection. + + :return: A tuple containing a boolean True/False and a corresponding Request Response + :rtype: tuple[bool, RequestResponse] + """ + if self._can_perform_network_action == False: + self.sys_log.warning(f"{self.name}: Unable to make leverage networking resources. Rejecting Command.") + return [ + False, + RequestResponse( + status="failure", data={"Reason": "Unable to access networking resources. Unable to send command."} + ), + ] + + if self.c2_remote_connection is False: + self.sys_log.warning(f"{self.name}: C2 Application has yet to establish connection. Rejecting command.") + return [ + False, + RequestResponse( + status="failure", + data={"Reason": "C2 Application has yet to establish connection. Unable to send command."}, + ), + ] + else: + return [ + True, + RequestResponse(status="success", data={"Reason": "C2 Application is able to send connections."}), + ] 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 8052d0f2..d256be42 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 @@ -28,12 +28,15 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): to simulate malicious communications and infrastructure within primAITE. Must be configured with the C2 Server's IP Address upon installation. + Please refer to the _configure method for further information. Extends the Abstract C2 application to include the following: 1. Receiving commands from the C2 Server (Command input) 2. Leveraging the terminal application to execute requests (dependant on the command given) 3. Sending the RequestResponse back to the C2 Server (Command output) + + Please refer to the Command-&-Control notebook for an in-depth example of the C2 Suite. """ keep_alive_attempted: bool = False @@ -141,9 +144,18 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): masquerade_port: Enum = Port.HTTP, ) -> bool: """ - Configures the C2 beacon to communicate with the C2 server with following additional parameters. + Configures the C2 beacon to communicate with the C2 server. + + The C2 Beacon has four different configuration options which can be used to + modify the networking behaviour between the C2 Server and the C2 Beacon. + + Configuration Option | Option Meaning + ---------------------|------------------------ + c2_server_ip_address | The IP Address of the C2 Server. (The C2 Server must be running) + keep_alive_frequency | How often should the C2 Beacon confirm it's connection in timesteps. + masquerade_protocol | What protocol should the C2 traffic masquerade as? (HTTP, FTP or DNS) + masquerade_port | What port should the C2 traffic use? (TCP or UDP) - # TODO: Expand docustring. :param c2_server_ip_address: The IP Address of the C2 Server. Used to establish connection. :type c2_server_ip_address: IPv4Address @@ -245,13 +257,7 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): :param session_id: The current session established with the C2 Server. :type session_id: Str """ - output_packet = C2Packet( - masquerade_protocol=self.c2_config.masquerade_protocol, - masquerade_port=self.c2_config.masquerade_port, - keep_alive_frequency=self.c2_config.keep_alive_frequency, - payload_type=C2Payload.OUTPUT, - payload=command_output, - ) + output_packet = self._craft_packet(c2_payload=C2Payload.OUTPUT, command_options=command_output) if self.send( payload=output_packet, dest_ip_address=self.c2_remote_connection, @@ -410,7 +416,7 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): # If this method returns true then we have sent successfully sent a keep alive. return self._send_keep_alive(session_id) - def _confirm_connection(self, timestep: int) -> bool: + def _confirm_remote_connection(self, timestep: int) -> bool: """Checks the suitability of the current C2 Server connection. If a connection cannot be confirmed then this method will return false otherwise true. diff --git a/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py b/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py index 6b51f8c7..577a13cb 100644 --- a/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py +++ b/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py @@ -24,6 +24,8 @@ class C2Server(AbstractC2, identifier="C2Server"): 1. Sending commands to the C2 Beacon. (Command input) 2. Parsing terminal RequestResponses back to the Agent. + + Please refer to the Command-&-Control notebook for an in-depth example of the C2 Suite. """ current_command_output: RequestResponse = None @@ -51,7 +53,7 @@ class C2Server(AbstractC2, identifier="C2Server"): "server_ip_address": request[-1].get("server_ip_address"), "payload": request[-1].get("payload"), } - return self._send_command(given_command=C2Command.RANSOMWARE_CONFIGURE, command_options=ransomware_config) + return self.send_command(given_command=C2Command.RANSOMWARE_CONFIGURE, command_options=ransomware_config) def _launch_ransomware_action(request: RequestFormat, context: Dict) -> RequestResponse: """Agent Action - Sends a RANSOMWARE_LAUNCH C2Command to the C2 Beacon with the given parameters. @@ -63,7 +65,7 @@ class C2Server(AbstractC2, identifier="C2Server"): :return: RequestResponse object with a success code reflecting whether the ransomware was launched. :rtype: RequestResponse """ - return self._send_command(given_command=C2Command.RANSOMWARE_LAUNCH, command_options={}) + return self.send_command(given_command=C2Command.RANSOMWARE_LAUNCH, command_options={}) def _remote_terminal_action(request: RequestFormat, context: Dict) -> RequestResponse: """Agent Action - Sends a TERMINAL C2Command to the C2 Beacon with the given parameters. @@ -76,7 +78,7 @@ class C2Server(AbstractC2, identifier="C2Server"): :rtype: RequestResponse """ command_payload = request[-1] - return self._send_command(given_command=C2Command.TERMINAL, command_options=command_payload) + return self.send_command(given_command=C2Command.TERMINAL, command_options=command_payload) rm.add_request( name="ransomware_configure", @@ -159,7 +161,7 @@ class C2Server(AbstractC2, identifier="C2Server"): return self._send_keep_alive(session_id) @validate_call - def _send_command(self, given_command: C2Command, command_options: Dict) -> RequestResponse: + def send_command(self, given_command: C2Command, command_options: Dict) -> RequestResponse: """ Sends a command to the C2 Beacon. @@ -193,26 +195,17 @@ class C2Server(AbstractC2, identifier="C2Server"): status="failure", data={"Reason": "Received unexpected C2Command. Unable to send command."} ) - if self._can_perform_network_action == False: - self.sys_log.warning(f"{self.name}: Unable to make leverage networking resources. Rejecting Command.") - return RequestResponse( - status="failure", data={"Reason": "Unable to access networking resources. Unable to send command."} - ) - - if self.c2_remote_connection is False: - self.sys_log.warning(f"{self.name}: C2 Beacon has yet to establish connection. Rejecting command.") - return RequestResponse( - status="failure", data={"Reason": "C2 Beacon has yet to establish connection. Unable to send command."} - ) - - if self.c2_session is None: - self.sys_log.warning(f"{self.name}: C2 Beacon cannot be reached. Rejecting command.") - return RequestResponse( - status="failure", data={"Reason": "C2 Beacon cannot be reached. Unable to send command."} - ) + # Lambda method used to return a failure RequestResponse if we're unable to confirm a connection. + # If _check_connection returns false then connection_status will return reason (A 'failure' Request Response) + if connection_status := (lambda return_bool, reason: reason if return_bool is False else None)( + *self._check_connection() + ): + return connection_status self.sys_log.info(f"{self.name}: Attempting to send command {given_command}.") - command_packet = self._craft_packet(given_command=given_command, command_options=command_options) + command_packet = self._craft_packet( + c2_payload=C2Payload.INPUT, c2_command=given_command, command_options=command_options + ) if self.send( payload=command_packet, @@ -231,30 +224,6 @@ class C2Server(AbstractC2, identifier="C2Server"): ) return self.current_command_output - # TODO: Probably could move this as a class method in C2Packet. - def _craft_packet(self, given_command: C2Command, command_options: Dict) -> C2Packet: - """ - Creates and returns a Masquerade Packet using the arguments given. - - Creates Masquerade Packet with a payload_type INPUT C2Payload. - - :param given_command: The C2 command to be sent to the C2 Beacon. - :type given_command: C2Command. - :param command_options: The relevant C2 Beacon parameters.F - :type command_options: Dict - :return: Returns the construct C2Packet - :rtype: C2Packet - """ - constructed_packet = C2Packet( - masquerade_protocol=self.c2_config.masquerade_protocol, - masquerade_port=self.c2_config.masquerade_port, - keep_alive_frequency=self.c2_config.keep_alive_frequency, - payload_type=C2Payload.INPUT, - command=given_command, - payload=command_options, - ) - return constructed_packet - def show(self, markdown: bool = False): """ Prints a table of the current C2 attributes on a C2 Server. @@ -292,7 +261,8 @@ class C2Server(AbstractC2, identifier="C2Server"): ) print(table) - # Abstract method inherited from abstract C2 - Not currently utilised. + # Abstract method inherited from abstract C2. + # C2 Servers do not currently receive any input commands from the C2 beacon. def _handle_command_input(self, payload: C2Packet) -> None: """Defining this method (Abstract method inherited from abstract C2) in order to instantiate the class. @@ -304,13 +274,15 @@ class C2Server(AbstractC2, identifier="C2Server"): self.sys_log.warning(f"{self.name}: C2 Server received an unexpected INPUT payload: {payload}") pass - def _confirm_connection(self, timestep: int) -> bool: + def _confirm_remote_connection(self, timestep: int) -> bool: """Checks the suitability of the current C2 Beacon connection. If a C2 Server has not received a keep alive within the current set keep alive frequency (self._keep_alive_frequency) then the C2 beacons connection is considered dead and any commands will be rejected. + This method is used to + :param timestep: The current timestep of the simulation. :type timestep: Int :return: Returns False if the C2 beacon is considered dead. Otherwise True. diff --git a/tests/integration_tests/system/red_applications/test_c2_suite_integration.py b/tests/integration_tests/system/red_applications/test_c2_suite_integration.py index 56b354d7..42091ec2 100644 --- a/tests/integration_tests/system/red_applications/test_c2_suite_integration.py +++ b/tests/integration_tests/system/red_applications/test_c2_suite_integration.py @@ -263,7 +263,7 @@ def test_c2_suite_terminal_command_file_creation(basic_network): "ip_address": None, } - c2_server._send_command(C2Command.TERMINAL, command_options=file_create_command) + c2_server.send_command(C2Command.TERMINAL, command_options=file_create_command) assert computer_b.software_manager.file_system.access_file(folder_name="test_folder", file_name="test_file") == True assert c2_beacon.local_terminal_session is not None @@ -273,7 +273,7 @@ def test_c2_suite_terminal_command_file_creation(basic_network): # node_c's IP is 192.168.255.3 file_create_command.update({"ip_address": "192.168.255.3"}) - c2_server._send_command(C2Command.TERMINAL, command_options=file_create_command) + c2_server.send_command(C2Command.TERMINAL, command_options=file_create_command) assert computer_c.software_manager.file_system.access_file(folder_name="test_folder", file_name="test_file") == True assert c2_beacon.remote_terminal_session is not None @@ -369,7 +369,7 @@ def test_c2_suite_acl_bypass(basic_network): "password": "admin", "ip_address": None, } - c2_server._send_command(C2Command.TERMINAL, command_options=ftp_file_create_command) + c2_server.send_command(C2Command.TERMINAL, command_options=ftp_file_create_command) assert ( computer_b.software_manager.file_system.access_file(folder_name="test_folder", file_name="ftp_test_file") == True @@ -440,7 +440,7 @@ def test_c2_suite_acl_bypass(basic_network): "password": "admin", "ip_address": None, } - c2_server._send_command(C2Command.TERMINAL, command_options=http_file_create_command) + c2_server.send_command(C2Command.TERMINAL, command_options=http_file_create_command) assert ( computer_b.software_manager.file_system.access_file(folder_name="test_folder", file_name="http_test_file") == True diff --git a/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_c2_suite.py b/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_c2_suite.py index ed408d14..813fb810 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_c2_suite.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_c2_suite.py @@ -102,7 +102,7 @@ def test_c2_handle_beacon_disconnect(basic_c2_network): "ip_address": None, } - command_request_response = c2_server._send_command(C2Command.TERMINAL, command_options=file_create_command) + command_request_response = c2_server.send_command(C2Command.TERMINAL, command_options=file_create_command) assert command_request_response.status == "failure" @@ -117,9 +117,6 @@ def test_c2_handle_beacon_disconnect(basic_c2_network): assert c2_server.c2_connection_active is False -# TODO: Finalise and complete these tests. - - def test_c2_handle_switching_port(basic_c2_network): """Tests that the C2 suite will be able handle switching destination/src port.""" network: Network = basic_c2_network @@ -205,3 +202,30 @@ def test_c2_handle_switching_frequency(basic_c2_network): assert c2_beacon.keep_alive_inactivity is 0 assert c2_server.keep_alive_inactivity is 0 + + +def test_c2_handles_1_timestep_keep_alive(basic_c2_network): + """Tests that the C2 suite will be able handle a C2 Beacon will a keep alive of 1 timestep.""" + network: Network = basic_c2_network + + network, computer_a, c2_server, computer_b, c2_beacon = setup_c2(network) + + c2_beacon.configure(c2_server_ip_address="192.168.0.1", keep_alive_frequency=1) + c2_server.run() + c2_beacon.establish() + + for i in range(50): + network.apply_timestep(i) + + assert c2_beacon.c2_connection_active is True + assert c2_server.c2_connection_active is True + + +def test_c2_server_runs_on_default(basic_c2_network): + """Tests that the C2 Server begins running by default.""" + network: Network = basic_c2_network + + computer_a: Computer = network.get_node_by_hostname("computer_a") + c2_server: C2Server = computer_a.software_manager.software.get("C2Server") + + assert c2_server.operating_state == ApplicationOperatingState.RUNNING From 27ec06658fde17d04e49f13d0bb482cdf6356c82 Mon Sep 17 00:00:00 2001 From: Archer Bowen Date: Mon, 12 Aug 2024 19:25:30 +0100 Subject: [PATCH 23/46] #2689 Majorly updated the command and control notebook to demonstrate more configuration options and more text to explain the code cells. --- .../Command-&-Control-E2E-Demonstration.ipynb | 629 +++++++++++++++--- .../red_applications/c2/abstract_c2.py | 2 - 2 files changed, 532 insertions(+), 99 deletions(-) diff --git a/src/primaite/notebooks/Command-&-Control-E2E-Demonstration.ipynb b/src/primaite/notebooks/Command-&-Control-E2E-Demonstration.ipynb index 7ee1c5cf..46fbe886 100644 --- a/src/primaite/notebooks/Command-&-Control-E2E-Demonstration.ipynb +++ b/src/primaite/notebooks/Command-&-Control-E2E-Demonstration.ipynb @@ -142,8 +142,15 @@ " keep_alive_frequency: 10\n", " masquerade_protocol: TCP\n", " masquerade_port: DNS\n", - "\n", - "\n", + " 8:\n", + " action: CONFIGURE_C2_BEACON\n", + " options:\n", + " node_id: 0\n", + " config:\n", + " c2_server_ip_address: 192.168.10.22\n", + " keep_alive_frequency:\n", + " masquerade_protocol:\n", + " masquerade_port:\n", "\n", " reward_function:\n", " reward_components:\n", @@ -202,14 +209,39 @@ "\n", "Before any C2 Server commands is able to accept any commands, it must first establish connection with a C2 beacon.\n", "\n", - "This can be done by installing, configuring and then executing a C2 Beacon. " + "A red agent is able to install, configure and establish a C2 beacon at any point of an episode. The code cells below demonstrate what actions and option parameters are needed to perform this." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "### **Command and Control** | C2 Beacon Actions | Installation" + "### **Command and Control** | C2 Beacon Actions | NODE_APPLICATION_INSTALL\n", + "\n", + "The custom proxy red agent defined at the start of this notebook has been configured to install the C2 Beacon as action ``1`` on it's action map. \n", + "\n", + "The below yaml snippet shows all the relevant agent options for this action:\n", + "\n", + "```yaml\n", + " action_space:\n", + " action_list:\n", + " ...\n", + " - type: NODE_APPLICATION_INSTALL\n", + " ...\n", + " options:\n", + " nodes: # Node List\n", + " - node_name: web_server\n", + " applications: \n", + " - application_name: C2Beacon\n", + " ...\n", + " ...\n", + " action_map:\n", + " 1:\n", + " action: NODE_APPLICATION_INSTALL \n", + " options:\n", + " node_id: 0 # Index 0 at the node list.\n", + " application_name: C2Beacon\n", + "```" ] }, { @@ -227,7 +259,35 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### **Command and Control** | C2 Beacon Actions | Configuration" + "### **Command and Control** | C2 Beacon Actions | CONFIGURE_C2_BEACON \n", + "\n", + "The custom proxy red agent defined at the start of this notebook can configure the C2 Beacon via action ``2`` on it's action map. \n", + "\n", + "The below yaml snippet shows all the relevant agent options for this action:\n", + "\n", + "```yaml\n", + " action_space:\n", + " action_list:\n", + " ...\n", + " - type: CONFIGURE_C2_BEACON\n", + " ...\n", + " options:\n", + " nodes: # Node List\n", + " - node_name: web_server\n", + " ...\n", + " ...\n", + " action_map:\n", + " ...\n", + " 2:\n", + " action: CONFIGURE_C2_BEACON\n", + " options:\n", + " node_id: 0 # Node Index\n", + " config: # Further information about these config options can be found at the bottom of this notebook.\n", + " c2_server_ip_address: 192.168.10.21\n", + " keep_alive_frequency:\n", + " masquerade_protocol:\n", + " masquerade_port:\n", + "```" ] }, { @@ -246,7 +306,33 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### **Command and Control** | C2 Beacon Actions | Establishing Connection" + "### **Command and Control** | C2 Beacon Actions | NODE_APPLICATION_EXECUTE\n", + "\n", + "The final action is ``NODE_APPLICATION_EXECUTE`` which is used to establish connection for the C2 application. This action can be called by the Red Agent via action ``3`` on it's action map. \n", + "\n", + "The below yaml snippet shows all the relevant agent options for this action:\n", + "\n", + "```yaml\n", + " action_space:\n", + " action_list:\n", + " ...\n", + " - type: NODE_APPLICATION_EXECUTE\n", + " ...\n", + " options:\n", + " nodes: # Node List\n", + " - node_name: web_server\n", + " applications: \n", + " - application_name: C2Beacon\n", + " ...\n", + " ...\n", + " action_map:\n", + " ...\n", + " 3:\n", + " action: NODE_APPLICATION_EXECUTE\n", + " options:\n", + " node_id: 0\n", + " application_id: 0\n", + "```" ] }, { @@ -272,14 +358,59 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## **Command and Control** | C2 Server Actions" + "## **Command and Control** | C2 Server Actions\n", + "\n", + "Once the C2 suite has been successfully established, the C2 Server based actions become available to the Red Agent. \n", + "\n", + "\n", + "This next section will demonstrate the different actions that become available to a red agent after establishing C2 connection:" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "### **Command and Control** | C2 Server Actions | Executing Terminal Commands" + "### **Command and Control** | C2 Server Actions | C2_SERVER_TERMINAL_COMMAND\n", + "\n", + "The C2 Server's terminal action is indexed at ``4`` on the custom red agent action map. \n", + "\n", + "This action leverages the terminal service that is installed by default on all nodes to grant red agents a lot more configurability. If you're unfamiliar with terminals then it's recommended that you refer to the ``Terminal Processing`` notebook.\n", + "\n", + "It's worth noting that an additional benefit that a red agent has when using terminal via the C2 Server is that you can execute multiple commands in one action. \n", + "\n", + "In this notebook, the ``C2_SERVER_TERMINAL_COMMAND`` is used to install a RansomwareScript application on the ``web_server`` node.\n", + "\n", + "The below yaml snippet shows all the relevant agent options for this action:\n", + "\n", + "``` yaml\n", + " action_space:\n", + " action_list:\n", + " ...\n", + " - type: C2_SERVER_TERMINAL_COMMAND\n", + " ...\n", + " options:\n", + " nodes: # Node List\n", + " ...\n", + " - node_name: client_1\n", + " applications: \n", + " - application_name: C2Server\n", + " ...\n", + " action_map:\n", + " 4:\n", + " action: C2_SERVER_TERMINAL_COMMAND\n", + " options:\n", + " node_id: 1\n", + " ip_address:\n", + " account:\n", + " username: admin\n", + " password: admin\n", + " commands:\n", + " - \n", + " - software_manager\n", + " - application\n", + " - install\n", + " - RansomwareScript\n", + "```" ] }, { @@ -304,7 +435,36 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### **Command and Control** | C2 Server Actions | Configuring Ransomware" + "### **Command and Control** | C2 Server Actions | C2_SERVER_RANSOMWARE_CONFIGURE\n", + "\n", + "Another action that the C2 Server grants is the ability for a Red Agent to configure ransomware via the C2 Server. \n", + "\n", + "This action is indexed as action ``5``.\n", + "\n", + "The below yaml snippet shows all the relevant agent options for this action:\n", + "\n", + "``` yaml\n", + " action_space:\n", + " action_list:\n", + " ...\n", + " - type: C2_SERVER_RANSOMWARE_CONFIGURE\n", + " ...\n", + " options:\n", + " nodes: # Node List\n", + " ...\n", + " - node_name: client_1\n", + " applications: \n", + " - application_name: C2Server\n", + " ...\n", + " action_map:\n", + " 5:\n", + " action: C2_SERVER_RANSOMWARE_CONFIG\n", + " options:\n", + " node_id: 1\n", + " config:\n", + " server_ip_address: 192.168.1.14\n", + " payload: ENCRYPT\n", + "```\n" ] }, { @@ -316,15 +476,6 @@ "env.step(5)" ] }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "env.step(6)" - ] - }, { "cell_type": "code", "execution_count": null, @@ -340,7 +491,33 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### **Command and Control** | C2 Server Actions | Launching Ransomware" + "### **Command and Control** | C2 Server Actions | C2_SERVER_RANSOMWARE_LAUNCH\n", + "\n", + "Finally, currently the last action available is the ``C2_SERVER_RANSOMWARE_LAUNCH`` which quite simply launches the ransomware script installed on the same node as the C2 beacon.\n", + "\n", + "This action is indexed as action ``6``.\n", + "\n", + "The below yaml snippet shows all the relevant agent options for this actio\n", + "\n", + "``` yaml\n", + " action_space:\n", + " action_list:\n", + " ...\n", + " - type: C2_SERVER_RANSOMWARE_LAUNCH\n", + " ...\n", + " options:\n", + " nodes: # Node List\n", + " ...\n", + " - node_name: client_1\n", + " applications: \n", + " - application_name: C2Server\n", + " ...\n", + " action_map:\n", + " 6:\n", + " action: C2_SERVER_RANSOMWARE_LAUNCH\n", + " options:\n", + " node_id: 1\n", + "```\n" ] }, { @@ -407,7 +584,7 @@ " num_applications: 2\n", " num_folders: 1\n", " num_files: 1\n", - " num_nics: 0\n", + " num_nics: 1\n", " include_num_access: false\n", " include_nmne: false\n", " monitored_traffic:\n", @@ -415,16 +592,26 @@ " - NONE\n", " tcp:\n", " - HTTP\n", + " - DNS\n", + " - FTP\n", " routers:\n", " - hostname: router_1\n", - " num_ports: 1\n", + " num_ports: 3\n", " ip_list:\n", - " - 192.168.10.21\n", + " - 192.168.1.10\n", " - 192.168.1.12\n", + " - 192.168.1.14\n", + " - 192.168.1.16\n", + " - 192.168.1.110\n", + " - 192.168.10.21\n", + " - 192.168.10.22\n", + " - 192.168.10.110\n", " wildcard_list:\n", " - 0.0.0.1\n", " port_list:\n", " - 80\n", + " - 53\n", + " - 21\n", " protocol_list:\n", " - ICMP\n", " - TCP\n", @@ -776,7 +963,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "The code cell below uses the custom blue agent defined at the start of this section perform NODE_APPLICATION_REMOVE on the C2 beacon" + "The code cell below uses the custom blue agent defined at the start of this section perform a NODE_APPLICATION_REMOVE on the C2 beacon:" ] }, { @@ -1059,20 +1246,57 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## **Command and Control** | C2 Beacon Actions\n", + "## **Command and Control** | Configurability \n", "\n", - "Before any C2 Server commands is able to accept any commands, it must first establish connection with a C2 beacon.\n", - "\n", - "This can be done by installing, configuring and then executing a C2 Beacon. " + "This section of the notebook demonstrates the C2 configuration options and their impact on the simulation layer and the game layer." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## **Command and Control** | Configurability \n", + "The table below is the currently offered C2 Beacon configuration options:\n", "\n", - "TODO: Fleshout" + "|Configuration Option | Option Meaning |Default Option | Type | _Optional_ |\n", + "|---------------------|---------------------------------------------------------------------------|---------------|---------|------------|\n", + "|c2_server_ip_address | The IP Address of the C2 Server. (The C2 Server must be running) |_None_ |str (IP) | _No_ |\n", + "|keep_alive_frequency | How often should the C2 Beacon confirm it's connection in timesteps. |5 |Int | _Yes_ |\n", + "|masquerade_port | What port should the C2 traffic use? (TCP or UDP) |TCP |Str | _Yes_ |\n", + "|masquerade_protocol | What protocol should the C2 traffic masquerade as? (HTTP, FTP or DNS) |HTTP |Str | _Yes_ |\n", + "\n", + "The C2 Server currently does not offer any unique configuration options. The C2 Server aligns itself with the C2 Beacon's configuration options once connection is established." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As demonstrated earlier, red agents can use the ``CONFIGURE_C2_BEACON`` action to configure these settings mid episode through the configuration options:\n", + "\n", + "``` YAML\n", + "...\n", + " action: CONFIGURE_C2_BEACON\n", + " options:\n", + " node_id: 0\n", + " config:\n", + " c2_server_ip_address: 192.168.10.21\n", + " keep_alive_frequency: 10\n", + " masquerade_protocol: TCP\n", + " masquerade_port: DNS\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### **Command and Control** | Configurability | C2 Server IP Address\n", + "\n", + "As with a majority of client and server based application configuration in primaite, the remote IP of server must be supplied.\n", + "\n", + "In the case of the C2 Beacon, the C2 Server's IP must be supplied before the C2 beacon will be able to perform any other actions (including ``APPLICATION EXECUTE``).\n", + "\n", + "If the network contains multiple C2 Servers then it's also possible to switch to different C2 servers mid episode which is demonstrated in the below code cells." ] }, { @@ -1095,73 +1319,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Installing the C2 Server" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "client_1: Computer = c2_config_env.game.simulation.network.get_node_by_hostname(\"client_1\")\n", - "client_1.software_manager.install(C2Server)\n", - "c2_server: C2Server = client_1.software_manager.software[\"C2Server\"]\n", - "c2_server.run()\n", - "client_1.software_manager.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Installing the C2 Beacon via NODE_APPLICATION_INSTALL" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "c2_config_env.step(1)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Configuring the C2 Beacon using different parameters:\n", - "\n", - "``` yaml\n", - " action: CONFIGURE_C2_BEACON\n", - " options:\n", - " node_id: 0\n", - " config:\n", - " c2_server_ip_address: 192.168.10.21\n", - " keep_alive_frequency: 10\n", - " masquerade_protocol: TCP\n", - " masquerade_port: DNS\n", - "```" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "c2_config_env.step(7)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Establishing connection to the C2 Server.\n", - "c2_config_env.step(3)" + "Installing the C2 Server on both client 1 and client 2." ] }, { @@ -1171,9 +1329,286 @@ "outputs": [], "source": [ "web_server: Server = c2_config_env.game.simulation.network.get_node_by_hostname(\"web_server\")\n", + "web_server.software_manager.install(C2Beacon)\n", "c2_beacon: C2Beacon = web_server.software_manager.software[\"C2Beacon\"]\n", + "\n", + "client_1: Computer = c2_config_env.game.simulation.network.get_node_by_hostname(\"client_1\")\n", + "client_1.software_manager.install(C2Server)\n", + "c2_server_1: C2Server = client_1.software_manager.software[\"C2Server\"]\n", + "c2_server_1.run()\n", + "\n", + "client_2: Computer = c2_config_env.game.simulation.network.get_node_by_hostname(\"client_2\")\n", + "client_2.software_manager.install(C2Server)\n", + "c2_server_2: C2Server = client_2.software_manager.software[\"C2Server\"]\n", + "c2_server_2.run()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Configuring the C2 Beacon to establish connection to the C2 Server on client_1 (192.168.10.21)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "env.step(2) # Agent Action Equivalent to c2_beacon.configure(c2_server_ip_address=\"192.168.10.21\")\n", + "env.step(3) # Agent action Equivalent to c2_beacon.establish()\n", "c2_beacon.show()\n", - "c2_server.show()" + "c2_server_1.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now reconfiguring the C2 Beacon to establish connection to the C2 Server on client_2 (192.168.10.22)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "env.step(8) # Equivalent of to c2_beacon.configure(c2_server_ip_address=\"192.168.10.22\")\n", + "env.step(3)\n", + "\n", + "c2_beacon.show()\n", + "c2_server_2.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "After six timesteps the client_1 server will recognise the c2 beacon previous connection as dead and clear it's connections. (This is dependant o the ``Keep Alive Frequency`` setting.)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "for i in range(6):\n", + " env.step(0)\n", + " \n", + "c2_server_1.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### **Command and Control** | Configurability | Keep Alive Frequency\n", + "\n", + "In order to confirm it's connection the C2 Beacon will send out a ``Keep Alive`` to the C2 Server and receive a keep alive back. \n", + "\n", + "By default, this occurs at a rate of 5 timesteps. However, this setting can be configured to be much more infrequent or as frequent as every timestep. \n", + "\n", + "The next set of code cells below demonstrate the impact that this setting has on blue agent observation space." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "with open(data_manipulation_config_path()) as f:\n", + " cfg = yaml.safe_load(f)\n", + " # removing all agents & adding the custom agent.\n", + " cfg['agents'] = {}\n", + " cfg['agents'] = custom_blue\n", + " cfg['agents'][0]['observation_space']['options']['components'][0]['options']['num_ports'] = 3\n", + " cfg['agents'][0]['observation_space']['options']['components'][0]['options']['monitored_traffic'].update({\"tcp\": [\"HTTP\",\"FTP\"]})\n", + " cfg['agents'][0]['observation_space']['options']['components'][0]['options']['monitored_traffic'].update({\"udp\": [\"DNS\"]})\n", + "\n", + "blue_config_env = PrimaiteGymEnv(env_config=cfg)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Performing the usual c2 setup:\n", + "blue_config_env, c2_server, c2_beacon, client_1, web_server = c2_setup(given_env=blue_config_env)\n", + "\n", + "# Flushing out the OBS impacts from setting up the C2 suite.\n", + "blue_config_env.step(0)\n", + "blue_config_env.step(0)\n", + "\n", + "# Capturing the 'default' obs (Post C2 installation and configuration):\n", + "default_obs, _, _, _, _ = blue_config_env.step(0)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The next code cells capture the obs impact of the default Keep Alive Frequency which is 5 timesteps:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "c2_beacon.configure(c2_server_ip_address=\"192.168.10.21\")\n", + "c2_beacon.establish()\n", + "c2_beacon.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The code cell below goes through 10 timesteps and displays the differences between the default and the current timestep.\n", + "\n", + "You will notice that the only observation space differences after 10 timesteps. This is due to the C2 Suite confirming their connection through sending ``Keep Alive`` traffic across the network." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "for i in range(10):\n", + " keep_alive_obs, _, _, _, _ = blue_config_env.step(0)\n", + " display_obs_diffs(default_obs, keep_alive_obs, blue_config_env.game.step_counter)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Next, the code cells below configuring the C2 Beacon's Keep Alive Frequency to confirm connection on every timestep." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "c2_beacon.configure(c2_server_ip_address=\"192.168.10.21\", keep_alive_frequency=1)\n", + "c2_beacon.establish()\n", + "c2_beacon.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The code cells below demonstrate that the observation impacts of the Keep Alive can be seen on every timestep. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Comparing the OBS of the default frequency to a timestep frequency of 1 \n", + "for i in range(2):\n", + " keep_alive_obs, _, _, _, _ = blue_config_env.step(0)\n", + " display_obs_diffs(default_obs, keep_alive_obs, blue_config_env.game.step_counter)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### **Command and Control** | Configurability | Masquerade Port & Masquerade Protocol\n", + "\n", + "The final configurable options are ``Masquerade Port`` & ``Masquerade Protocol``. These options can be used to control what networking IP Protocol and Port the C2 traffic is currently using.\n", + "\n", + "In the real world, Adversaries take defensive steps to reduce the chance that an installed C2 Beacon is discovered. One of the most commonly used methods is to masquerade c2 traffic as other commonly used networking protocols.\n", + "\n", + "In primAITE, red agents can begin to simulate stealth behaviour by configuring C2 traffic to use different protocols mid episode or between episodes. \n", + "Currently, red agent actions are limited to using ports: ``DNS``, ``FTP`` and ``HTTP`` and protocols: ``UDP`` and ``TCP``.\n", + "\n", + "The next set of code cells will demonstrate the impact this option from a blue agent perspective." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "blue_config_env.reset()\n", + "\n", + "# Performing the usual c2 setup:\n", + "blue_config_env, c2_server, c2_beacon, client_1, web_server = c2_setup(given_env=blue_config_env)\n", + "\n", + "blue_config_env.step(0)\n", + "\n", + "# Capturing the 'default' obs (Post C2 installation and configuration):\n", + "default_obs, _, _, _, _ = blue_config_env.step(0)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "By default, the C2 suite will masquerade a Web Browser, meaning C2 Traffic will opt to use ``TCP`` and ``HTTP`` (Port 80):" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Capturing default C2 Traffic \n", + "for i in range(3):\n", + " tcp_c2_obs, _, _, _, _ = blue_config_env.step(0)\n", + "\n", + "display_obs_diffs(default_obs, tcp_c2_obs, blue_config_env.game.step_counter)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "However, C2 Beacon can be configured to use UDP (``Masquerade Protocol``) and we can also configure the C2 Beacon to use a different Port (``Masquerade Port``) for example ``DNS``. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from primaite.simulator.network.transmission.network_layer import IPProtocol\n", + "from primaite.simulator.network.transmission.transport_layer import Port\n", + "# As we're configuring via the PrimAITE API we need to pass the actual IPProtocol/Port (Agents leverage the simulation via the game layer and thus can pass strings).\n", + "c2_beacon.configure(c2_server_ip_address=\"192.168.10.21\", masquerade_protocol=IPProtocol.UDP, masquerade_port=Port.DNS)\n", + "c2_beacon.establish()\n", + "c2_beacon.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Capturing UDP C2 Traffic\n", + "for i in range(5):\n", + " udp_c2_obs, _, _, _, _ = blue_config_env.step(0)\n", + "\n", + "display_obs_diffs(tcp_c2_obs, udp_c2_obs, blue_config_env.game.step_counter)" ] } ], diff --git a/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py b/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py index f5fb0929..2a3e78bb 100644 --- a/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py +++ b/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py @@ -27,8 +27,6 @@ class C2Command(Enum): TERMINAL = "Terminal" "Instructs the c2 beacon to execute the provided terminal command." - # The terminal command should also be able to pass a session which can be used for remote connections. - class C2Payload(Enum): """Represents the different types of command and control payloads.""" From 6c7376ab4b5c55d23af9715a5bd5ed66cd08759b Mon Sep 17 00:00:00 2001 From: Archer Bowen Date: Tue, 13 Aug 2024 09:37:11 +0100 Subject: [PATCH 24/46] #2681 Updated to include yaml file tests + include listening on multiports. --- .../system/applications/c2_suite.rst | 15 +++- src/primaite/game/game.py | 16 ++++ .../red_applications/c2/abstract_c2.py | 6 +- .../red_applications/c2/c2_server.py | 1 + tests/assets/configs/basic_c2_setup.yaml | 76 +++++++++++++++++++ .../test_c2_suite_integration.py | 34 +++++++++ 6 files changed, 143 insertions(+), 5 deletions(-) create mode 100644 tests/assets/configs/basic_c2_setup.yaml diff --git a/docs/source/simulation_components/system/applications/c2_suite.rst b/docs/source/simulation_components/system/applications/c2_suite.rst index 4d5f685a..55f58ff4 100644 --- a/docs/source/simulation_components/system/applications/c2_suite.rst +++ b/docs/source/simulation_components/system/applications/c2_suite.rst @@ -197,7 +197,7 @@ Via Configuration ... # A C2 Beacon will not automatically connection to a C2 Server. # Either an agent must use application_execute. - # Or a user must use .establish(). + # Or a if using the simulation layer - .establish(). applications: type: C2Beacon options: @@ -205,6 +205,10 @@ Via Configuration keep_alive_frequency: 5 masquerade_protocol: tcp masquerade_port: http + listen_on_ports: + - 80 + - 53 + - 21 @@ -264,6 +268,13 @@ This must be a string i.e ``DNS``. Defaults to ``HTTP``. _Please refer to the ``IPProtocol`` class for further reference._ - +C2 Server Configuration +======================= _The C2 Server does not currently offer any unique configuration options and will configure itself to match the C2 beacon's network behaviour._ + + +.. include:: ../common/common_configuration.rst + +.. |SOFTWARE_NAME| replace:: C2Server +.. |SOFTWARE_NAME_BACKTICK| replace:: ``C2Server`` diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index 7f7f69eb..f4722514 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -27,6 +27,7 @@ from primaite.simulator.network.hardware.nodes.network.router import Router from primaite.simulator.network.hardware.nodes.network.switch import Switch from primaite.simulator.network.hardware.nodes.network.wireless_router import WirelessRouter from primaite.simulator.network.nmne import NMNEConfig +from primaite.simulator.network.transmission.network_layer import IPProtocol from primaite.simulator.network.transmission.transport_layer import Port from primaite.simulator.sim_container import Simulation from primaite.simulator.system.applications.application import Application @@ -455,6 +456,21 @@ class PrimaiteGame: dos_intensity=float(opt.get("dos_intensity", "1.0")), max_sessions=int(opt.get("max_sessions", "1000")), ) + elif application_type == "C2Beacon": + if "options" in application_cfg: + opt = application_cfg["options"] + new_application.configure( + c2_server_ip_address=IPv4Address(opt.get("c2_server_ip_address")), + keep_alive_frequency=(opt.get("keep_alive_frequency")) + if opt.get("keep_alive_frequency") + else 5, + masquerade_protocol=IPProtocol[(opt.get("masquerade_protocol"))] + if opt.get("masquerade_protocol") + else IPProtocol.TCP, + masquerade_port=Port[(opt.get("masquerade_port"))] + if opt.get("masquerade_port") + else Port.HTTP, + ) if "network_interfaces" in node_cfg: for nic_num, nic_cfg in node_cfg["network_interfaces"].items(): new_node.connect_nic(NIC(ip_address=nic_cfg["ip_address"], subnet_mask=nic_cfg["subnet_mask"])) diff --git a/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py b/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py index 2a3e78bb..e6740d9f 100644 --- a/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py +++ b/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py @@ -247,9 +247,9 @@ class AbstractC2(Application, identifier="AbstractC2"): self.keep_alive_sent = True self.sys_log.info(f"{self.name}: Keep Alive sent to {self.c2_remote_connection}") self.sys_log.debug( - f"{self.name}: Keep Alive sent to {self.c2_remote_connection}" - f"Using Masquerade Port: {self.c2_config.masquerade_port}" - f"Using Masquerade Protocol: {self.c2_config.masquerade_protocol}" + f"{self.name}: Keep Alive sent to {self.c2_remote_connection} " + f"Masquerade Port: {self.c2_config.masquerade_port} " + f"Masquerade Protocol: {self.c2_config.masquerade_protocol} " ) return True else: diff --git a/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py b/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py index 577a13cb..a93fd8b6 100644 --- a/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py +++ b/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py @@ -97,6 +97,7 @@ class C2Server(AbstractC2, identifier="C2Server"): def __init__(self, **kwargs): kwargs["name"] = "C2Server" super().__init__(**kwargs) + self.run() def _handle_command_output(self, payload: C2Packet) -> bool: """ diff --git a/tests/assets/configs/basic_c2_setup.yaml b/tests/assets/configs/basic_c2_setup.yaml new file mode 100644 index 00000000..0cae2ba0 --- /dev/null +++ b/tests/assets/configs/basic_c2_setup.yaml @@ -0,0 +1,76 @@ +# Basic Switched network +# +# -------------- -------------- -------------- +# | node_a |------| switch_1 |------| node_b | +# -------------- -------------- -------------- +# +io_settings: + save_step_metadata: false + save_pcap_logs: true + save_sys_logs: true + sys_log_level: WARNING + agent_log_level: INFO + save_agent_logs: true + write_agent_log_to_terminal: True + + +game: + max_episode_length: 256 + ports: + - ARP + - DNS + - HTTP + - POSTGRES_SERVER + protocols: + - ICMP + - TCP + - UDP + +simulation: + network: + nodes: + + - type: switch + hostname: switch_1 + num_ports: 8 + + - hostname: node_a + type: computer + ip_address: 192.168.10.21 + subnet_mask: 255.255.255.0 + default_gateway: 192.168.10.1 + applications: + - type: C2Server + options: + listen_on_ports: + - 80 + - 53 + - 21 + - hostname: node_b + type: computer + ip_address: 192.168.10.22 + subnet_mask: 255.255.255.0 + default_gateway: 192.168.10.1 + applications: + - type: C2Beacon + options: + c2_server_ip_address: 192.168.10.21 + keep_alive_frequency: 5 + masquerade_protocol: TCP + masquerade_port: HTTP + listen_on_ports: + - 80 + - 53 + - 21 + + links: + - endpoint_a_hostname: switch_1 + endpoint_a_port: 1 + endpoint_b_hostname: node_a + endpoint_b_port: 1 + bandwidth: 200 + - endpoint_a_hostname: switch_1 + endpoint_a_port: 2 + endpoint_b_hostname: node_b + endpoint_b_port: 1 + bandwidth: 200 diff --git a/tests/integration_tests/system/red_applications/test_c2_suite_integration.py b/tests/integration_tests/system/red_applications/test_c2_suite_integration.py index 42091ec2..904fb449 100644 --- a/tests/integration_tests/system/red_applications/test_c2_suite_integration.py +++ b/tests/integration_tests/system/red_applications/test_c2_suite_integration.py @@ -3,6 +3,7 @@ from ipaddress import IPv4Address from typing import Tuple import pytest +import yaml from primaite.game.agent.interface import ProxyAgent from primaite.game.game import PrimaiteGame @@ -22,6 +23,7 @@ from primaite.simulator.system.applications.red_applications.ransomware_script i from primaite.simulator.system.services.database.database_service import DatabaseService from primaite.simulator.system.services.dns.dns_server import DNSServer from primaite.simulator.system.services.web_server.web_server import WebServer +from tests import TEST_ASSETS_ROOT @pytest.fixture(scope="function") @@ -463,3 +465,35 @@ def test_c2_suite_acl_bypass(basic_network): assert c2_packets_blocked == blocking_acl.match_count assert c2_server.c2_connection_active is True assert c2_beacon.c2_connection_active is True + + +def test_c2_suite_yaml(): + """Tests that the C2 Suite is can be configured correctly via the Yaml.""" + with open(TEST_ASSETS_ROOT / "configs" / "basic_c2_setup.yaml") as f: + cfg = yaml.safe_load(f) + game = PrimaiteGame.from_config(cfg) + + yaml_network = game.simulation.network + computer_a: Computer = yaml_network.get_node_by_hostname("node_a") + c2_server: C2Server = computer_a.software_manager.software.get("C2Server") + + computer_b: Computer = yaml_network.get_node_by_hostname("node_b") + c2_beacon: C2Beacon = computer_b.software_manager.software.get("C2Beacon") + + assert c2_server.operating_state == ApplicationOperatingState.RUNNING + + assert c2_beacon.c2_remote_connection == IPv4Address("192.168.10.21") + + c2_beacon.establish() + + # Asserting that the c2 beacon has established a c2 connection + assert c2_beacon.c2_connection_active is True + # Asserting that the c2 server has established a c2 connection. + assert c2_server.c2_connection_active is True + assert c2_server.c2_remote_connection == IPv4Address("192.168.10.22") + + for i in range(50): + yaml_network.apply_timestep(i) + + assert c2_beacon.c2_connection_active is True + assert c2_server.c2_connection_active is True From 845a4c6bd65b35ebfc1b9280ef0e7f67ab5c13ab Mon Sep 17 00:00:00 2001 From: Archer Bowen Date: Tue, 13 Aug 2024 10:18:56 +0100 Subject: [PATCH 25/46] #2689 Final docustring updates before PR. --- .../red_applications/c2/abstract_c2.py | 19 ++- .../red_applications/c2/c2_beacon.py | 12 +- .../red_applications/c2/c2_server.py | 117 ++++++++++-------- .../_red_applications/test_c2_suite.py | 10 -- 4 files changed, 88 insertions(+), 70 deletions(-) diff --git a/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py b/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py index e6740d9f..fc383837 100644 --- a/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py +++ b/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py @@ -87,7 +87,10 @@ class AbstractC2(Application, identifier="AbstractC2"): Creates and returns a Masquerade Packet using the parameters given. The packet uses the current c2 configuration and parameters given - to construct a C2 Packet. + to construct the base networking information such as the masquerade + protocol/port. Additionally all C2 Traffic packets pass the currently + in use C2 configuration. This ensures that the all C2 applications + can keep their configuration in sync. :param c2_payload: The type of C2 Traffic ot be sent :type c2_payload: C2Payload @@ -208,6 +211,8 @@ class AbstractC2(Application, identifier="AbstractC2"): :type payload: C2Packet :param session_id: The transport session_id that the payload is originating from. :type session_id: str + :return: Returns a bool if the traffic was received correctly (See _handle_c2_payload.) + :rtype: bool """ return self._handle_c2_payload(payload, session_id) @@ -306,7 +311,13 @@ class AbstractC2(Application, identifier="AbstractC2"): return True def _reset_c2_connection(self) -> None: - """Resets all currently established C2 communications to their default setting.""" + """ + Resets all currently established C2 communications to their default setting. + + This method is called once a C2 application considers their remote connection + severed and reverts back to default settings. Worth noting that that this will + revert any non-default configuration that a user/agent may have set. + """ self.c2_connection_active = False self.c2_session = None self.keep_alive_inactivity = 0 @@ -359,7 +370,9 @@ class AbstractC2(Application, identifier="AbstractC2"): Validation method: Checks that the C2 application is capable of sending C2 Command input/output. Performs a series of connection validation to ensure that the C2 application is capable of - sending and responding to the remote c2 connection. + sending and responding to the remote c2 connection. This method is used to confirm connection + before carrying out Agent Commands hence why this method also returns a tuple + containing both a success boolean as well as RequestResponse. :return: A tuple containing a boolean True/False and a corresponding Request Response :rtype: tuple[bool, RequestResponse] 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 d256be42..e66bedc5 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 @@ -156,6 +156,11 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): masquerade_protocol | What protocol should the C2 traffic masquerade as? (HTTP, FTP or DNS) masquerade_port | What port should the C2 traffic use? (TCP or UDP) + These configuration options are used to reassign the fields in the inherited inner class + ``c2_config``. + + If a connection is already in progress then this method also sends a keep alive to the C2 + Server in order for the C2 Server to sync with the new configuration settings. :param c2_server_ip_address: The IP Address of the C2 Server. Used to establish connection. :type c2_server_ip_address: IPv4Address @@ -165,6 +170,7 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): :type masquerade_protocol: Enum (IPProtocol) :param masquerade_port: The Port that the C2 Traffic will masquerade as. Defaults to FTP. :type masquerade_port: Enum (Port) + :return: Returns True if the configuration was successful, False otherwise. """ self.c2_remote_connection = IPv4Address(c2_server_ip_address) self.c2_config.keep_alive_frequency = keep_alive_frequency @@ -183,7 +189,7 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): # Send a keep alive to the C2 Server if we already have a keep alive. if self.c2_connection_active is True: self.sys_log.info(f"{self.name}: Updating C2 Server with updated C2 configuration.") - self._send_keep_alive(self.c2_session.uuid if not None else None) + return self._send_keep_alive(self.c2_session.uuid if not None else None) return True def establish(self) -> bool: @@ -193,14 +199,14 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): return False self.run() self.num_executions += 1 + # Creates a new session if using the establish method. return self._send_keep_alive(session_id=None) def _handle_command_input(self, payload: C2Packet, session_id: Optional[str]) -> bool: """ Handles the parsing of C2 Commands from C2 Traffic (Masquerade Packets). - Dependant the C2 Command contained within the payload. - The following methods are called and returned. + Dependant the C2 Command parsed from the payload, the following methods are called and returned: C2 Command | Internal Method ---------------------|------------------------ diff --git a/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py b/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py index a93fd8b6..3d71b881 100644 --- a/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py +++ b/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py @@ -103,15 +103,15 @@ class C2Server(AbstractC2, identifier="C2Server"): """ Handles the parsing of C2 Command Output from C2 Traffic (Masquerade Packets). - Parses the Request Response within C2Packet's payload attribute (Inherited from Data packet). - The class attribute self.current_command_output is then set to this Request Response. + Parses the Request Response from the given C2Packet's payload attribute (Inherited from Data packet). + This RequestResponse is then stored in the C2 Server class attribute self.current_command_output. If the payload attribute does not contain a RequestResponse, then an error will be raised in syslog and the self.current_command_output is updated to reflect the error. :param payload: The OUTPUT C2 Payload :type payload: C2Packet - :return: Returns True if the self.current_command_output is currently updated, false otherwise. + :return: Returns True if the self.current_command_output was updated, false otherwise. :rtype Bool: """ self.sys_log.info(f"{self.name}: Received command response from C2 Beacon: {payload}.") @@ -130,20 +130,27 @@ class C2Server(AbstractC2, identifier="C2Server"): """ Handles receiving and sending keep alive payloads. This method is only called if we receive a keep alive. - In the C2 Server implementation of this method the c2 connection active boolean - is set to true and the keep alive inactivity is reset after receiving one keep alive. + Abstract method inherited from abstract C2. + + In the C2 Server implementation of this method the following logic is performed: + + 1. The ``self.c2_connection_active`` is set to True. (Indicates that we're received a connection) + 2. The received keep alive (Payload parameter) is then resolved by _resolve_keep_alive. + 3. After the keep alive is resolved, a keep alive is sent back to confirm connection. This is because the C2 Server is the listener and thus will only ever receive packets from - the C2 Beacon rather than the other way around. (The C2 Beacon is akin to a reverse shell) + the C2 Beacon rather than the other way around. + + The C2 Beacon/Server communication is akin to that of a real-world reverse shells. Returns False if a keep alive was unable to be sent. Returns True if a keep alive was successfully sent or already has been sent this timestep. :param payload: The Keep Alive payload received. :type payload: C2Packet - :param session_id: The transport session_id that the payload is originating from. + :param session_id: The transport session_id that the payload originates from. :type session_id: str - :return: True if successfully handled, false otherwise. + :return: True if the keep alive was successfully handled, false otherwise. :rtype: Bool """ self.sys_log.info(f"{self.name}: Keep Alive Received. Attempting to resolve the remote connection details.") @@ -155,16 +162,22 @@ class C2Server(AbstractC2, identifier="C2Server"): self.sys_log.warning(f"{self.name}: Keep Alive Could not be resolved correctly. Refusing Keep Alive.") return False - # If this method returns true then we have sent successfully sent a keep alive. self.sys_log.info(f"{self.name}: Remote connection successfully established: {self.c2_remote_connection}.") self.sys_log.debug(f"{self.name}: Attempting to send Keep Alive response back to {self.c2_remote_connection}.") + # If this method returns true then we have sent successfully sent a keep alive response back. return self._send_keep_alive(session_id) @validate_call def send_command(self, given_command: C2Command, command_options: Dict) -> RequestResponse: """ - Sends a command to the C2 Beacon. + Sends a C2 command to the C2 Beacon using the given parameters. + + C2 Command | Command Synopsis + ---------------------|------------------------ + RANSOMWARE_CONFIGURE | Configures an installed ransomware script based on the passed parameters. + RANSOMWARE_LAUNCH | Launches the installed ransomware script. + TERMINAL | Executes a command via the terminal installed on the C2 Beacons Host. Currently, these commands leverage the pre-existing capability of other applications. However, the commands are sent via the network rather than the game layer which @@ -174,12 +187,6 @@ class C2Server(AbstractC2, identifier="C2Server"): more complex red agent behaviour such as file extraction, establishing further fall back channels or introduce red applications that are only installable via C2 Servers. (T1105) - C2 Command | Meaning - ---------------------|------------------------ - RANSOMWARE_CONFIGURE | Configures an installed ransomware script based on the passed parameters. - RANSOMWARE_LAUNCH | Launches the installed ransomware script. - TERMINAL | Executes a command via the terminal installed on the C2 Beacons Host. - For more information on the impact of these commands please refer to the terminal and the ransomware applications. @@ -225,6 +232,46 @@ class C2Server(AbstractC2, identifier="C2Server"): ) return self.current_command_output + def _confirm_remote_connection(self, timestep: int) -> bool: + """Checks the suitability of the current C2 Beacon connection. + + Inherited Abstract Method. + + If a C2 Server has not received a keep alive within the current set + keep alive frequency (self._keep_alive_frequency) then the C2 beacons + connection is considered dead and any commands will be rejected. + + This method is called on each timestep (Called by .apply_timestep) + + :param timestep: The current timestep of the simulation. + :type timestep: Int + :return: Returns False if the C2 beacon is considered dead. Otherwise True. + :rtype bool: + """ + if self.keep_alive_inactivity > self.c2_config.keep_alive_frequency: + self.sys_log.info(f"{self.name}: C2 Beacon connection considered dead due to inactivity.") + self.sys_log.debug( + f"{self.name}: Did not receive expected keep alive connection from {self.c2_remote_connection}" + f"{self.name}: Expected at timestep: {timestep} due to frequency: {self.c2_config.keep_alive_frequency}" + f"{self.name}: Last Keep Alive received at {(timestep - self.keep_alive_inactivity)}" + ) + self._reset_c2_connection() + return False + return True + + # Abstract method inherited from abstract C2. + # C2 Servers do not currently receive any input commands from the C2 beacon. + def _handle_command_input(self, payload: C2Packet) -> None: + """Defining this method (Abstract method inherited from abstract C2) in order to instantiate the class. + + C2 Servers currently do not receive input commands coming from the C2 Beacons. + + :param payload: The incoming C2Packet + :type payload: C2Packet. + """ + self.sys_log.warning(f"{self.name}: C2 Server received an unexpected INPUT payload: {payload}") + pass + def show(self, markdown: bool = False): """ Prints a table of the current C2 attributes on a C2 Server. @@ -261,41 +308,3 @@ class C2Server(AbstractC2, identifier="C2Server"): ] ) print(table) - - # Abstract method inherited from abstract C2. - # C2 Servers do not currently receive any input commands from the C2 beacon. - def _handle_command_input(self, payload: C2Packet) -> None: - """Defining this method (Abstract method inherited from abstract C2) in order to instantiate the class. - - C2 Servers currently do not receive input commands coming from the C2 Beacons. - - :param payload: The incoming C2Packet - :type payload: C2Packet. - """ - self.sys_log.warning(f"{self.name}: C2 Server received an unexpected INPUT payload: {payload}") - pass - - def _confirm_remote_connection(self, timestep: int) -> bool: - """Checks the suitability of the current C2 Beacon connection. - - If a C2 Server has not received a keep alive within the current set - keep alive frequency (self._keep_alive_frequency) then the C2 beacons - connection is considered dead and any commands will be rejected. - - This method is used to - - :param timestep: The current timestep of the simulation. - :type timestep: Int - :return: Returns False if the C2 beacon is considered dead. Otherwise True. - :rtype bool: - """ - if self.keep_alive_inactivity > self.c2_config.keep_alive_frequency: - self.sys_log.info(f"{self.name}: C2 Beacon connection considered dead due to inactivity.") - self.sys_log.debug( - f"{self.name}: Did not receive expected keep alive connection from {self.c2_remote_connection}" - f"{self.name}: Expected at timestep: {timestep} due to frequency: {self.c2_config.keep_alive_frequency}" - f"{self.name}: Last Keep Alive received at {(timestep - self.keep_alive_inactivity)}" - ) - self._reset_c2_connection() - return False - return True diff --git a/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_c2_suite.py b/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_c2_suite.py index 813fb810..30defe8b 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_c2_suite.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_c2_suite.py @@ -219,13 +219,3 @@ def test_c2_handles_1_timestep_keep_alive(basic_c2_network): assert c2_beacon.c2_connection_active is True assert c2_server.c2_connection_active is True - - -def test_c2_server_runs_on_default(basic_c2_network): - """Tests that the C2 Server begins running by default.""" - network: Network = basic_c2_network - - computer_a: Computer = network.get_node_by_hostname("computer_a") - c2_server: C2Server = computer_a.software_manager.software.get("C2Server") - - assert c2_server.operating_state == ApplicationOperatingState.RUNNING From c36af13a665d5bcf41e5f6464e6836db947c656b Mon Sep 17 00:00:00 2001 From: Archer Bowen Date: Tue, 13 Aug 2024 10:30:44 +0100 Subject: [PATCH 26/46] #2689 Updated changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c354aa14..f06301a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added `User`, `UserManager` and `UserSessionManager` to enable the creation of user accounts and login on Nodes. - Added a `listen_on_ports` set in the `IOSoftware` class to enable software listening on ports in addition to the main port they're assigned. +- Added two new red applications: ``C2Beacon`` and ``C2Server`` which aim to simulate malicious network infrastructure. + Refer to the ``Command and Control Application Suite E2E Demonstration`` notebook for more information. ### Changed - Updated `SoftwareManager` `install` and `uninstall` to handle all functionality that was being done at the `install` From 1138605e2bf5be8e93ad04918215c3ff1451ec7d Mon Sep 17 00:00:00 2001 From: Archer Bowen Date: Tue, 13 Aug 2024 10:48:17 +0100 Subject: [PATCH 27/46] #2689 Fixing mistakenly altered test file. --- .../integration_tests/network/test_routing.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/integration_tests/network/test_routing.py b/tests/integration_tests/network/test_routing.py index 5f9e03ef..1ef38e15 100644 --- a/tests/integration_tests/network/test_routing.py +++ b/tests/integration_tests/network/test_routing.py @@ -33,18 +33,18 @@ def pc_a_pc_b_router_1() -> Tuple[Computer, Computer, Router]: ) pc_b.power_on() - router = Router(hostname="router", start_up_duration=0) - router.power_on() + router_1 = Router(hostname="router", start_up_duration=0) + router_1.power_on() - router.configure_port(1, "192.168.0.1", "255.255.255.0") - router.configure_port(2, "192.168.1.1", "255.255.255.0") + router_1.configure_port(1, "192.168.0.1", "255.255.255.0") + router_1.configure_port(2, "192.168.1.1", "255.255.255.0") - network.connect(endpoint_a=pc_a.network_interface[1], endpoint_b=router.network_interface[1]) - network.connect(endpoint_a=pc_b.network_interface[1], endpoint_b=router.network_interface[2]) - router.enable_port(1) - router.enable_port(2) + network.connect(endpoint_a=pc_a.network_interface[1], endpoint_b=router_1.network_interface[1]) + network.connect(endpoint_a=pc_b.network_interface[1], endpoint_b=router_1.network_interface[2]) + router_1.enable_port(1) + router_1.enable_port(2) - return pc_a, pc_b, router + return pc_a, pc_b, router_1 @pytest.fixture(scope="function") From 57dcd325a034a349c72fbfcdb8fe7924bfa28258 Mon Sep 17 00:00:00 2001 From: Archer Bowen Date: Tue, 13 Aug 2024 10:49:10 +0100 Subject: [PATCH 28/46] #2689 missed the hostname... --- tests/integration_tests/network/test_routing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration_tests/network/test_routing.py b/tests/integration_tests/network/test_routing.py index 1ef38e15..62b58cbd 100644 --- a/tests/integration_tests/network/test_routing.py +++ b/tests/integration_tests/network/test_routing.py @@ -33,7 +33,7 @@ def pc_a_pc_b_router_1() -> Tuple[Computer, Computer, Router]: ) pc_b.power_on() - router_1 = Router(hostname="router", start_up_duration=0) + router_1 = Router(hostname="router_1", start_up_duration=0) router_1.power_on() router_1.configure_port(1, "192.168.0.1", "255.255.255.0") From ead302c95de3ae00643e10a1bffe155a8b9d90b9 Mon Sep 17 00:00:00 2001 From: Archer Bowen Date: Tue, 13 Aug 2024 12:33:41 +0100 Subject: [PATCH 29/46] #2689 Added Tests for the C2 actions (Was previously covered via the notebook - now explicitly in a test.) --- tests/conftest.py | 8 +- .../actions/test_c2_suite_actions.py | 152 ++++++++++++++++++ 2 files changed, 159 insertions(+), 1 deletion(-) create mode 100644 tests/integration_tests/game_layer/actions/test_c2_suite_actions.py diff --git a/tests/conftest.py b/tests/conftest.py index 2d605c94..b6375acd 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -458,6 +458,10 @@ def game_and_agent(): {"type": "HOST_NIC_DISABLE"}, {"type": "NETWORK_PORT_ENABLE"}, {"type": "NETWORK_PORT_DISABLE"}, + {"type": "CONFIGURE_C2_BEACON"}, + {"type": "C2_SERVER_RANSOMWARE_LAUNCH"}, + {"type": "C2_SERVER_RANSOMWARE_CONFIGURE"}, + {"type": "C2_SERVER_TERMINAL_COMMAND"}, ] action_space = ActionManager( @@ -468,12 +472,14 @@ def game_and_agent(): "applications": [ {"application_name": "WebBrowser"}, {"application_name": "DoSBot"}, + {"application_name": "C2Server"}, ], "folders": [{"folder_name": "downloads", "files": [{"file_name": "cat.png"}]}], }, { "node_name": "server_1", "services": [{"service_name": "DNSServer"}], + "applications": [{"application_name": "C2Beacon"}], }, {"node_name": "server_2", "services": [{"service_name": "WebServer"}]}, {"node_name": "router"}, @@ -481,7 +487,7 @@ def game_and_agent(): max_folders_per_node=2, max_files_per_folder=2, max_services_per_node=2, - max_applications_per_node=2, + max_applications_per_node=3, max_nics_per_node=2, max_acl_rules=10, protocols=["TCP", "UDP", "ICMP"], diff --git a/tests/integration_tests/game_layer/actions/test_c2_suite_actions.py b/tests/integration_tests/game_layer/actions/test_c2_suite_actions.py new file mode 100644 index 00000000..990c6363 --- /dev/null +++ b/tests/integration_tests/game_layer/actions/test_c2_suite_actions.py @@ -0,0 +1,152 @@ +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +from ipaddress import IPv4Address +from typing import Tuple + +import pytest + +from primaite.game.agent.interface import ProxyAgent +from primaite.game.game import PrimaiteGame +from primaite.simulator.file_system.file_system_item_abc import FileSystemItemHealthStatus +from primaite.simulator.network.hardware.base import UserManager +from primaite.simulator.network.hardware.nodes.host.computer import Computer +from primaite.simulator.network.hardware.nodes.host.server import Server +from primaite.simulator.network.hardware.nodes.network.router import ACLAction +from primaite.simulator.network.transmission.transport_layer import Port +from primaite.simulator.system.applications.red_applications.c2.c2_beacon import C2Beacon +from primaite.simulator.system.applications.red_applications.c2.c2_server import C2Command, C2Server +from primaite.simulator.system.services.database.database_service import DatabaseService +from primaite.simulator.system.services.service import ServiceOperatingState + + +@pytest.fixture +def game_and_agent_fixture(game_and_agent): + """Create a game with a simple agent that can be controlled by the tests.""" + game, agent = game_and_agent + + router = game.simulation.network.get_node_by_hostname("router") + router.acl.add_rule(action=ACLAction.PERMIT, src_port=Port.HTTP, dst_port=Port.HTTP, position=4) + router.acl.add_rule(action=ACLAction.PERMIT, src_port=Port.DNS, dst_port=Port.DNS, position=5) + router.acl.add_rule(action=ACLAction.PERMIT, src_port=Port.FTP, dst_port=Port.FTP, position=6) + + c2_server_host = game.simulation.network.get_node_by_hostname("client_1") + c2_server_host.software_manager.install(software_class=C2Server) + c2_server: C2Server = c2_server_host.software_manager.software["C2Server"] + c2_server.run() + + return (game, agent) + + +def test_c2_beacon_default(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyAgent]): + """Tests that a Red Agent can install, configure and establish a C2 Beacon (default params).""" + game, agent = game_and_agent_fixture + + # Installing C2 Beacon on Server_1 + server_1: Server = game.simulation.network.get_node_by_hostname("server_1") + + action = ( + "NODE_APPLICATION_INSTALL", + {"node_id": 1, "application_name": "C2Beacon"}, + ) + agent.store_action(action) + game.step() + assert agent.history[-1].response.status == "success" + + action = ( + "CONFIGURE_C2_BEACON", + { + "node_id": 1, + "config": { + "c2_server_ip_address": "10.0.1.2", + "keep_alive_frequency": 5, + "masquerade_protocol": "TCP", + "masquerade_port": "HTTP", + }, + }, + ) + agent.store_action(action) + game.step() + assert agent.history[-1].response.status == "success" + + action = ( + "NODE_APPLICATION_EXECUTE", + {"node_id": 1, "application_id": 0}, + ) + agent.store_action(action) + game.step() + assert agent.history[-1].response.status == "success" + + # Asserting that we've confirmed our connection + c2_beacon: C2Beacon = server_1.software_manager.software["C2Beacon"] + assert c2_beacon.c2_connection_active == True + + +def test_c2_server_ransomware(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyAgent]): + """Tests that a Red Agent can install a RansomwareScript, Configure and launch all via C2 Server actions.""" + game, agent = game_and_agent_fixture + + # Installing a C2 Beacon on server_1 + server_1: Server = game.simulation.network.get_node_by_hostname("server_1") + server_1.software_manager.install(C2Beacon) + + # Installing a database on Server_2 for the ransomware to attack + server_2: Server = game.simulation.network.get_node_by_hostname("server_2") + server_2.software_manager.install(DatabaseService) + server_2.software_manager.software["DatabaseService"].start() + # Configuring the C2 to connect to client 1 (C2 Server) + c2_beacon: C2Beacon = server_1.software_manager.software["C2Beacon"] + c2_beacon.configure(c2_server_ip_address=IPv4Address("10.0.1.2")) + c2_beacon.establish() + assert c2_beacon.c2_connection_active == True + + # C2 Action 1: Installing the RansomwareScript & Database client via Terminal + + action = ( + "C2_SERVER_TERMINAL_COMMAND", + { + "node_id": 0, + "ip_address": None, + "account": { + "username": "admin", + "password": "admin", + }, + "commands": [ + ["software_manager", "application", "install", "RansomwareScript"], + ["software_manager", "application", "install", "DatabaseClient"], + ], + }, + ) + agent.store_action(action) + game.step() + assert agent.history[-1].response.status == "success" + + action = ( + "C2_SERVER_RANSOMWARE_CONFIGURE", + { + "node_id": 0, + "config": {"server_ip_address": "10.0.2.3", "payload": "ENCRYPT"}, + }, + ) + agent.store_action(action) + game.step() + assert agent.history[-1].response.status == "success" + + # Stepping a few timesteps to allow for the RansowmareScript to finish installing. + + action = ("DONOTHING", {}) + agent.store_action(action) + game.step() + game.step() + game.step() + + action = ( + "C2_SERVER_RANSOMWARE_LAUNCH", + { + "node_id": 0, + }, + ) + agent.store_action(action) + game.step() + assert agent.history[-1].response.status == "success" + + database_file = server_2.software_manager.file_system.get_file("database", "database.db") + assert database_file.health_status == FileSystemItemHealthStatus.CORRUPT From d6e2994d6b5426273e7cc181bd01b6b0e12bc76d Mon Sep 17 00:00:00 2001 From: Archer Bowen Date: Tue, 13 Aug 2024 15:43:21 +0000 Subject: [PATCH 30/46] Apply suggestions from code review --- src/primaite/game/game.py | 2 +- .../system/applications/red_applications/c2/abstract_c2.py | 2 +- .../system/applications/red_applications/c2/c2_server.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index f4722514..d3035a5a 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -461,7 +461,7 @@ class PrimaiteGame: opt = application_cfg["options"] new_application.configure( c2_server_ip_address=IPv4Address(opt.get("c2_server_ip_address")), - keep_alive_frequency=(opt.get("keep_alive_frequency")) + keep_alive_frequency=(opt.get("keep_alive_frequency", 5)) if opt.get("keep_alive_frequency") else 5, masquerade_protocol=IPProtocol[(opt.get("masquerade_protocol"))] diff --git a/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py b/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py index fc383837..7fa8f9ad 100644 --- a/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py +++ b/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py @@ -187,7 +187,7 @@ class AbstractC2(Application, identifier="AbstractC2"): # Used in C2 server to parse and receive the output of commands sent to the c2 beacon. @abstractmethod def _handle_command_output(payload): - """Abstract Method: Used in C2 server to prase and receive the output of commands sent to the c2 beacon.""" + """Abstract Method: Used in C2 server to parse and receive the output of commands sent to the c2 beacon.""" pass # Abstract method diff --git a/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py b/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py index 3d71b881..f413a4b7 100644 --- a/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py +++ b/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py @@ -14,7 +14,7 @@ class C2Server(AbstractC2, identifier="C2Server"): """ C2 Server Application. - Represents a vendor generic C2 Server is used in conjunction with the C2 beacon + Represents a vendor generic C2 Server used in conjunction with the C2 beacon to simulate malicious communications and infrastructure within primAITE. The C2 Server must be installed and be in a running state before it's able to receive From 559f48006255d34c6b32b2980decae948c687ce0 Mon Sep 17 00:00:00 2001 From: Archer Bowen Date: Tue, 13 Aug 2024 16:47:40 +0100 Subject: [PATCH 31/46] #2689 Fixed .rst formatting issues and removed unnecessary comments. --- .../simulation_components/system/applications/c2_suite.rst | 6 +++--- .../system/applications/red_applications/c2/abstract_c2.py | 4 ---- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/docs/source/simulation_components/system/applications/c2_suite.rst b/docs/source/simulation_components/system/applications/c2_suite.rst index 55f58ff4..fff670e7 100644 --- a/docs/source/simulation_components/system/applications/c2_suite.rst +++ b/docs/source/simulation_components/system/applications/c2_suite.rst @@ -253,7 +253,7 @@ It's worth noting that this may be useful option to bypass ACL rules. This must be a string i.e ``udp``. Defaults to ``tcp``. -_Please refer to the ``IPProtocol`` class for further reference._ +*Please refer to the ``IPProtocol`` class for further reference.* ``Masquerade Port`` """"""""""""""""""" @@ -266,12 +266,12 @@ It's worth noting that this may be useful option to bypass ACL rules. This must be a string i.e ``DNS``. Defaults to ``HTTP``. -_Please refer to the ``IPProtocol`` class for further reference._ +*Please refer to the ``IPProtocol`` class for further reference.* C2 Server Configuration ======================= -_The C2 Server does not currently offer any unique configuration options and will configure itself to match the C2 beacon's network behaviour._ +*The C2 Server does not currently offer any unique configuration options and will configure itself to match the C2 beacon's network behaviour.* .. include:: ../common/common_configuration.rst diff --git a/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py b/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py index 7fa8f9ad..8a03491e 100644 --- a/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py +++ b/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py @@ -183,15 +183,11 @@ class AbstractC2(Application, identifier="AbstractC2"): ) return False - # Abstract method - # Used in C2 server to parse and receive the output of commands sent to the c2 beacon. @abstractmethod def _handle_command_output(payload): """Abstract Method: Used in C2 server to parse and receive the output of commands sent to the c2 beacon.""" pass - # Abstract method - # Used in C2 beacon to parse and handle commands received from the c2 server. @abstractmethod def _handle_command_input(payload): """Abstract Method: Used in C2 beacon to parse and handle commands received from the c2 server.""" From 192ca814e0284e1a6246c98c0e231acdfec3bef0 Mon Sep 17 00:00:00 2001 From: Archer Bowen Date: Tue, 13 Aug 2024 15:49:52 +0000 Subject: [PATCH 32/46] Apply suggestions from code review --- .../simulation_components/system/applications/c2_suite.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/simulation_components/system/applications/c2_suite.rst b/docs/source/simulation_components/system/applications/c2_suite.rst index fff670e7..c3044d1d 100644 --- a/docs/source/simulation_components/system/applications/c2_suite.rst +++ b/docs/source/simulation_components/system/applications/c2_suite.rst @@ -38,7 +38,7 @@ Currently, the C2 Server offers three commands: +---------------------+---------------------------------------------------------------------------+ -It's important to note that in order to keep the PrimAITE realistic from a cyber perspective, +It's important to note that in order to keep PrimAITE realistic from a cyber perspective, The C2 Server application should never be visible or actionable upon directly by the blue agent. This is because in the real world, C2 servers are hosted on ephemeral public domains that would not be accessible by private network blue agent. From 6a28f17f1be8ba50226b51019b07f47e96d77fcd Mon Sep 17 00:00:00 2001 From: Archer Bowen Date: Wed, 14 Aug 2024 19:49:58 +0100 Subject: [PATCH 33/46] #2689 Initial draft of File exfiltration. --- src/primaite/game/agent/actions.py | 43 ++++++ .../Command-&-Control-E2E-Demonstration.ipynb | 100 ++++++++++++-- .../red_applications/c2/abstract_c2.py | 121 ++++++++++++++--- .../red_applications/c2/c2_beacon.py | 126 +++++++++++++++++- .../red_applications/c2/c2_server.py | 61 +++++++++ .../system/services/ftp/ftp_client.py | 53 +++++++- tests/conftest.py | 1 + .../actions/test_c2_suite_actions.py | 51 +++++++ .../test_c2_suite_integration.py | 41 ++++++ 9 files changed, 568 insertions(+), 29 deletions(-) diff --git a/src/primaite/game/agent/actions.py b/src/primaite/game/agent/actions.py index 92b175a9..aa74399e 100644 --- a/src/primaite/game/agent/actions.py +++ b/src/primaite/game/agent/actions.py @@ -1147,6 +1147,48 @@ class RansomwareLaunchC2ServerAction(AbstractAction): return ["network", "node", node_name, "application", "C2Server", "ransomware_launch"] +class ExfiltrationC2ServerAction(AbstractAction): + """Action which exfiltrates a target file from a certain node onto the C2 beacon and then the C2 Server.""" + + class _Opts(BaseModel): + """Schema for options that can be passed to this action.""" + + username: Optional[str] + password: Optional[str] + target_ip_address: str + target_file_name: str + target_folder_name: str + exfiltration_folder_name: Optional[str] + + def __init__(self, manager: "ActionManager", **kwargs) -> None: + super().__init__(manager=manager) + + def form_request( + self, + node_id: int, + account: dict, + target_ip_address: str, + target_file_name: str, + target_folder_name: str, + exfiltration_folder_name: Optional[str], + ) -> RequestFormat: + """Return the action formatted as a request that can be ingested by the simulation.""" + node_name = self.manager.get_node_name_by_idx(node_id) + if node_name is None: + return ["do_nothing"] + + command_model = { + "target_file_name": target_file_name, + "target_folder_name": target_folder_name, + "exfiltration_folder_name": exfiltration_folder_name, + "target_ip_address": target_ip_address, + "username": account["username"], + "password": account["password"], + } + ExfiltrationC2ServerAction._Opts.model_validate(command_model) + return ["network", "node", node_name, "application", "C2Server", "exfiltrate", command_model] + + class TerminalC2ServerAction(AbstractAction): """Action which causes the C2 Server to send a command to the C2 Beacon to execute the terminal command passed.""" @@ -1233,6 +1275,7 @@ class ActionManager: "C2_SERVER_RANSOMWARE_LAUNCH": RansomwareLaunchC2ServerAction, "C2_SERVER_RANSOMWARE_CONFIGURE": RansomwareConfigureC2ServerAction, "C2_SERVER_TERMINAL_COMMAND": TerminalC2ServerAction, + "C2_SERVER_DATA_EXFILTRATE": ExfiltrationC2ServerAction, } """Dictionary which maps action type strings to the corresponding action class.""" diff --git a/src/primaite/notebooks/Command-&-Control-E2E-Demonstration.ipynb b/src/primaite/notebooks/Command-&-Control-E2E-Demonstration.ipynb index 46fbe886..052136f8 100644 --- a/src/primaite/notebooks/Command-&-Control-E2E-Demonstration.ipynb +++ b/src/primaite/notebooks/Command-&-Control-E2E-Demonstration.ipynb @@ -67,6 +67,7 @@ " - type: C2_SERVER_RANSOMWARE_LAUNCH\n", " - type: C2_SERVER_RANSOMWARE_CONFIGURE\n", " - type: C2_SERVER_TERMINAL_COMMAND\n", + " - type: C2_SERVER_DATA_EXFILTRATE\n", " options:\n", " nodes:\n", " - node_name: web_server\n", @@ -130,10 +131,22 @@ " server_ip_address: 192.168.1.14\n", " payload: ENCRYPT\n", " 6:\n", + " action: C2_SERVER_DATA_EXFILTRATE\n", + " options:\n", + " node_id: 1\n", + " target_file_name: \"database.db\"\n", + " target_folder_name: \"database\"\n", + " exfiltration_folder_name: \"spoils\"\n", + " target_ip_address: 192.168.1.14\n", + " account:\n", + " username: admin\n", + " password: admin \n", + "\n", + " 7:\n", " action: C2_SERVER_RANSOMWARE_LAUNCH\n", " options:\n", " node_id: 1\n", - " 7:\n", + " 8:\n", " action: CONFIGURE_C2_BEACON\n", " options:\n", " node_id: 0\n", @@ -142,7 +155,7 @@ " keep_alive_frequency: 10\n", " masquerade_protocol: TCP\n", " masquerade_port: DNS\n", - " 8:\n", + " 9:\n", " action: CONFIGURE_C2_BEACON\n", " options:\n", " node_id: 0\n", @@ -487,17 +500,88 @@ "ransomware_script.show()" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### **Command and Control** | C2 Server Actions | C2_SERVER_DATA_EXFILTRATE\n", + "\n", + "Finally, currently the last action available is the ``C2_SERVER_DATA_EXFILTRATE`` which can be used to exfiltrate a target_file on a remote node to the C2 Beacon & Server's host file system via the ``FTP`` services.\n", + "\n", + "This action is indexed as action ``9``. # TODO: Update.\n", + "\n", + "The below yaml snippet shows all the relevant agent options for this action\n", + "\n", + "``` yaml\n", + " action_space:\n", + " action_list:\n", + " ...\n", + " - type: C2_SERVER_DATA_EXFILTRATE\n", + " ...\n", + " options:\n", + " nodes: # Node List\n", + " ...\n", + " - node_name: client_1\n", + " applications: \n", + " - application_name: C2Server\n", + " ...\n", + " action_map:\n", + " 6:\n", + " action: C2_SERVER_DATA_EXFILTRATE\n", + " options:\n", + " node_id: 1\n", + " target_file_name: \"database.db\"\n", + " target_folder_name: \"database\"\n", + " exfiltration_folder_name: \"spoils\"\n", + " target_ip_address: \"192.168.1.14\"\n", + " account:\n", + " username: \"admin\",\n", + " password: \"admin\"\n", + "\n", + "```\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "env.step(6)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "client_1: Computer = env.game.simulation.network.get_node_by_hostname(\"client_1\")\n", + "client_1.software_manager.file_system.show(full=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "web_server: Computer = env.game.simulation.network.get_node_by_hostname(\"web_server\")\n", + "web_server.software_manager.file_system.show(full=True)" + ] + }, { "cell_type": "markdown", "metadata": {}, "source": [ "### **Command and Control** | C2 Server Actions | C2_SERVER_RANSOMWARE_LAUNCH\n", "\n", - "Finally, currently the last action available is the ``C2_SERVER_RANSOMWARE_LAUNCH`` which quite simply launches the ransomware script installed on the same node as the C2 beacon.\n", + "Finally, to the ransomware configuration action, there is also the ``C2_SERVER_RANSOMWARE_LAUNCH`` which quite simply launches the ransomware script installed on the same node as the C2 beacon.\n", "\n", - "This action is indexed as action ``6``.\n", + "This action is indexed as action ``7``.\n", "\n", - "The below yaml snippet shows all the relevant agent options for this actio\n", + "The below yaml snippet shows all the relevant agent options for this action\n", "\n", "``` yaml\n", " action_space:\n", @@ -513,7 +597,7 @@ " - application_name: C2Server\n", " ...\n", " action_map:\n", - " 6:\n", + " 7:\n", " action: C2_SERVER_RANSOMWARE_LAUNCH\n", " options:\n", " node_id: 1\n", @@ -526,7 +610,7 @@ "metadata": {}, "outputs": [], "source": [ - "env.step(6)" + "env.step(7)" ] }, { @@ -1375,7 +1459,7 @@ "metadata": {}, "outputs": [], "source": [ - "env.step(8) # Equivalent of to c2_beacon.configure(c2_server_ip_address=\"192.168.10.22\")\n", + "env.step(9) # Equivalent of to c2_beacon.configure(c2_server_ip_address=\"192.168.10.22\")\n", "env.step(3)\n", "\n", "c2_beacon.show()\n", diff --git a/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py b/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py index 8a03491e..6fa34fd6 100644 --- a/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py +++ b/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py @@ -2,16 +2,20 @@ from abc import abstractmethod from enum import Enum from ipaddress import IPv4Address -from typing import Dict, Optional +from typing import Dict, Optional, Union from pydantic import BaseModel, Field, validate_call from primaite.interface.request import RequestResponse +from primaite.simulator.file_system.file_system import FileSystem, Folder from primaite.simulator.network.protocols.masquerade import C2Packet from primaite.simulator.network.transmission.network_layer import IPProtocol from primaite.simulator.network.transmission.transport_layer import Port from primaite.simulator.system.applications.application import Application, ApplicationOperatingState from primaite.simulator.system.core.session_manager import Session +from primaite.simulator.system.services.ftp.ftp_client import FTPClient +from primaite.simulator.system.services.ftp.ftp_server import FTPServer +from primaite.simulator.system.services.service import ServiceOperatingState from primaite.simulator.system.software import SoftwareHealthState @@ -24,6 +28,9 @@ class C2Command(Enum): RANSOMWARE_LAUNCH = "Ransomware Launch" "Instructs the c2 beacon to execute the installed ransomware." + DATA_EXFILTRATION = "Data Exfiltration" + "Instructs the c2 beacon to attempt to return a file to the C2 Server." + TERMINAL = "Terminal" "Instructs the c2 beacon to execute the provided terminal command." @@ -80,6 +87,19 @@ class AbstractC2(Application, identifier="AbstractC2"): masquerade_port: Port = Field(default=Port.HTTP) """The currently chosen port that the C2 traffic is masquerading as. Defaults at HTTP.""" + c2_config: _C2_Opts = _C2_Opts() + """ + Holds the current configuration settings of the C2 Suite. + + The C2 beacon initialise this class through it's internal configure method. + + The C2 Server when receiving a keep alive will initialise it's own configuration + to match that of the configuration settings passed in the keep alive through _resolve keep alive. + + If the C2 Beacon is reconfigured then a new keep alive is set which causes the + C2 beacon to reconfigure it's configuration settings. + """ + def _craft_packet( self, c2_payload: C2Payload, c2_command: Optional[C2Command] = None, command_options: Optional[Dict] = {} ) -> C2Packet: @@ -111,19 +131,6 @@ class AbstractC2(Application, identifier="AbstractC2"): ) return constructed_packet - c2_config: _C2_Opts = _C2_Opts() - """ - Holds the current configuration settings of the C2 Suite. - - The C2 beacon initialise this class through it's internal configure method. - - The C2 Server when receiving a keep alive will initialise it's own configuration - to match that of the configuration settings passed in the keep alive through _resolve keep alive. - - If the C2 Beacon is reconfigured then a new keep alive is set which causes the - C2 beacon to reconfigure it's configuration settings. - """ - def describe_state(self) -> Dict: """ Describe the state of the C2 application. @@ -140,6 +147,82 @@ class AbstractC2(Application, identifier="AbstractC2"): kwargs["protocol"] = IPProtocol.TCP super().__init__(**kwargs) + # TODO: We may need to disable the ftp_server/client when using the opposite service. (To test) + @property + def _host_ftp_client(self) -> Optional[FTPClient]: + """Return the FTPClient that is installed C2 Application's host. + + This method confirms that the FTP Client is functional via the ._can_perform_action + method. If the FTP Client service is not in a suitable state (e.g disabled/pause) + then this method will return None. + + (The FTP Client service is installed by default) + + :return: An FTPClient object is successful, else None + :rtype: union[FTPClient, None] + """ + ftp_client: Union[FTPClient, None] = self.software_manager.software.get("FTPClient") + if ftp_client is None: + self.sys_log.warning(f"{self.__class__.__name__}: No FTPClient. Attempting to install.") + self.software_manager.install(FTPClient) + ftp_client = self.software_manager.software.get("FTPClient") + + # Force start if the service is stopped. + if ftp_client.operating_state == ServiceOperatingState.STOPPED: + if not ftp_client.start(): + self.sys_log.warning(f"{self.__class__.__name__}: cannot start the FTP Client.") + + if not ftp_client._can_perform_action(): + self.sys_log.error(f"{self.__class__.__name__}: is unable to use the FTP service on its host.") + return + + return ftp_client + + @property + def _host_ftp_server(self) -> Optional[FTPServer]: + """ + Returns the FTP Server that is installed C2 Application's host. + + If a FTPServer is not installed then this method will attempt to install one. + + :return: An FTPServer object is successful, else None + :rtype: union[FTPServer, None] + """ + ftp_server: Union[FTPServer, None] = self.software_manager.software.get("FTPServer") + if ftp_server is None: + self.sys_log.warning(f"{self.__class__.__name__}:No FTPServer installed. Attempting to install FTPServer.") + self.software_manager.install(FTPServer) + ftp_server = self.software_manager.software.get("FTPServer") + + # Force start if the service is stopped. + if ftp_server.operating_state == ServiceOperatingState.STOPPED: + if not ftp_server.start(): + self.sys_log.warning(f"{self.__class__.__name__}: cannot start the FTP Server.") + + if not ftp_server._can_perform_action(): + self.sys_log.error(f"{self.__class__.__name__}: is unable use FTP Server service on its host.") + return + + return ftp_server + + # Getter property for the get_exfiltration_folder method () + @property + def _host_file_system(self) -> FileSystem: + """Return the C2 Host's filesystem (Used for exfiltration related commands) .""" + host_file_system: FileSystem = self.software_manager.file_system + if host_file_system is None: + self.sys_log.error(f"{self.__class__.__name__}: does not seem to have a file system!") + return host_file_system + + def get_exfiltration_folder(self, folder_name: str) -> Optional[Folder]: + """Return a folder used for storing exfiltrated data. Otherwise returns None.""" + exfiltration_folder: Union[Folder, None] = self._host_file_system.get_folder(folder_name) + if exfiltration_folder is None: + self.sys_log.info(f"{self.__class__.__name__}: Creating a exfiltration folder.") + return self._host_file_system.create_folder(folder_name=folder_name) + + return exfiltration_folder + # Validate call ensures we are only handling Masquerade Packets. @validate_call def _handle_c2_payload(self, payload: C2Packet, session_id: Optional[str] = None) -> bool: @@ -197,7 +280,7 @@ class AbstractC2(Application, identifier="AbstractC2"): """Abstract Method: The C2 Server and the C2 Beacon handle the KEEP ALIVEs differently.""" # from_network_interface=from_network_interface - def receive(self, payload: C2Packet, session_id: Optional[str] = None, **kwargs) -> bool: + def receive(self, payload: any, session_id: Optional[str] = None, **kwargs) -> bool: """Receives masquerade packets. Used by both c2 server and c2 beacon. Defining the `Receive` method so that the application can receive packets via the session manager. @@ -210,6 +293,11 @@ class AbstractC2(Application, identifier="AbstractC2"): :return: Returns a bool if the traffic was received correctly (See _handle_c2_payload.) :rtype: bool """ + if not isinstance(payload, C2Packet): + self.sys_log.warning(f"{self.name}: Payload is not an C2Packet") + self.sys_log.debug(f"{self.name}: {payload}") + return False + return self._handle_c2_payload(payload, session_id) def _send_keep_alive(self, session_id: Optional[str]) -> bool: @@ -352,14 +440,13 @@ class AbstractC2(Application, identifier="AbstractC2"): :return bool: Returns false if connection was lost. Returns True if connection is active or re-established. :rtype bool: """ - 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 self._confirm_remote_connection(timestep) - return + return super().apply_timestep(timestep=timestep) def _check_connection(self) -> tuple[bool, RequestResponse]: """ 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 e66bedc5..b3bf1902 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 @@ -135,6 +135,7 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): kwargs["name"] = "C2Beacon" super().__init__(**kwargs) + # Configure is practically setter method for the ``c2.config`` attribute that also ties into the request manager. @validate_call def configure( self, @@ -212,6 +213,7 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): ---------------------|------------------------ RANSOMWARE_CONFIGURE | self._command_ransomware_config() RANSOMWARE_LAUNCH | self._command_ransomware_launch() + DATA_EXFILTRATION | self._command_data_exfiltration() TERMINAL | self._command_terminal() Please see each method individually for further information regarding @@ -249,6 +251,12 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): self.sys_log.info(f"{self.name}: Received a terminal C2 command.") return self._return_command_output(command_output=self._command_terminal(payload), session_id=session_id) + elif command == C2Command.DATA_EXFILTRATION: + self.sys_log.info(f"{self.name}: Received a Data Exfiltration C2 command.") + return self._return_command_output( + command_output=self._command_data_exfiltration(payload), session_id=session_id + ) + else: self.sys_log.error(f"{self.name}: Received an C2 command: {command} but was unable to resolve command.") return self._return_command_output( @@ -313,9 +321,8 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): """ C2 Command: Ransomware Launch. - Creates a request that executes the ransomware script. - This request is then sent to the terminal service in order to be executed. - + Uses the RansomwareScript's public method .attack() to carry out the + ransomware attack and uses the .from_bool method to return a RequestResponse :payload C2Packet: The incoming INPUT command. :type Masquerade Packet: C2Packet. @@ -329,6 +336,119 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): ) return RequestResponse.from_bool(self._host_ransomware_script.attack()) + def _command_data_exfiltration(self, payload: C2Packet) -> RequestResponse: + """ + C2 Command: Data Exfiltration. + + Uses the FTP Client & Server services to perform the data exfiltration. + + Similar to the terminal, the logic of this command is dependant on if a remote_ip + is present within the payload. + + If a payload does contain an IP address then the C2 Beacon will ssh into the target ip + and execute a command which causes the FTPClient service to send a + + target file will be moved from the target IP address onto the C2 Beacon's host + file system. + + However, if no IP is given, then the command will move the target file from this + machine onto the C2 server. (This logic is performed on the C2) + + :payload C2Packet: The incoming INPUT command. + :type Masquerade Packet: C2Packet. + :return: Returns the Request Response returned by the Terminal execute method. + :rtype: Request Response + """ + if self._host_ftp_server is None: + self.sys_log.warning(f"{self.name}: C2 Beacon unable to the FTP Server. Unable to resolve command.") + return RequestResponse( + status="failure", + data={"Reason": "Cannot find any instances of both a FTP Server & Client. Are they installed?"}, + ) + + remote_ip = payload.payload.get("target_ip_address") + target_folder = payload.payload.get("target_folder_name") + dest_folder = payload.payload.get("exfiltration_folder_name") + + # Using the same name for both the target/destination file for clarity. + file_name = payload.payload.get("target_file_name") + + # TODO: Split Remote file extraction and local file extraction into different methods. + # if remote_ip is None: + # self._host_ftp_client.start() + # self.sys_log.info(f"{self.name}: No Remote IP given. Returning target file from local file system.") + # return RequestResponse.from_bool(self._host_ftp_client.send_file( + # dest_ip_address=self.c2_remote_connection, + # src_folder_name=target_folder, + # src_file_name=file_name, + # dest_folder_name=dest_folder, + # dest_file_name=file_name, + # session_id=self.c2_session.uuid + # )) + + # Parsing remote login credentials + given_username = payload.payload.get("username") + given_password = payload.payload.get("password") + + # Setting up the terminal session and the ftp server + terminal_session = self.get_remote_terminal_session( + username=given_username, password=given_password, ip_address=remote_ip + ) + + # Initialising the exfiltration folder. + exfiltration_folder = self.get_exfiltration_folder(dest_folder) + + # Using the terminal to start the FTP Client on the remote machine. + # This can fail if the FTP Client is already enabled. + terminal_session.execute(command=["service", "start", "FTPClient"]) + host_network_interfaces = self.software_manager.node.network_interfaces + local_ip = host_network_interfaces.get(next(iter(host_network_interfaces))).ip_address + # Creating the FTP creation options. + remote_ftp_options = { + "dest_ip_address": str(local_ip), + "src_folder_name": target_folder, + "src_file_name": file_name, + "dest_folder_name": dest_folder, + "dest_file_name": file_name, + } + + # Using the terminal to send the target data back to the C2 Beacon. + remote_ftp_response: RequestResponse = RequestResponse.from_bool( + terminal_session.execute(command=["service", "FTPClient", "send", remote_ftp_options]) + ) + + # Validating that we successfully received the target data. + + if remote_ftp_response.status == "failure": + self.sys_log.warning( + f"{self.name}: Remote connection: {remote_ip} failed to transfer the target data via FTP." + ) + return remote_ftp_response + + if exfiltration_folder.get_file(file_name) is None: + self.sys_log.warning(f"{self.name}: Unable to locate exfiltrated file on local filesystem.") + return RequestResponse( + status="failure", data={"reason": "Unable to locate exfiltrated data on file system."} + ) + + if self._host_ftp_client is None: + self.sys_log.warning(f"{self.name}: C2 Beacon unable to the FTP Server. Unable to resolve command.") + return RequestResponse( + status="failure", + data={"Reason": "Cannot find any instances of both a FTP Server & Client. Are they installed?"}, + ) + + # Sending the transferred target data back to the C2 Server to successfully exfiltrate the data out the network. + return RequestResponse.from_bool( + self._host_ftp_client.send_file( + dest_ip_address=self.c2_remote_connection, + src_folder_name=dest_folder, # TODO: Clarify this - Dest folder has the same name on c2server/c2beacon. + src_file_name=file_name, + dest_folder_name=dest_folder, + dest_file_name=file_name, + ) + ) + def _command_terminal(self, payload: C2Packet) -> RequestResponse: """ C2 Command: Terminal. diff --git a/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py b/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py index f413a4b7..3441a86b 100644 --- a/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py +++ b/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py @@ -67,6 +67,19 @@ class C2Server(AbstractC2, identifier="C2Server"): """ return self.send_command(given_command=C2Command.RANSOMWARE_LAUNCH, command_options={}) + def _data_exfiltration_action(request: RequestFormat, context: Dict) -> RequestResponse: + """Agent Action - Sends a Data Exfiltration C2Command to the C2 Beacon with the given parameters. + + :param request: Request with one element containing a dict of parameters for the configure method. + :type request: RequestFormat + :param context: additional context for resolving this action, currently unused + :type context: dict + :return: RequestResponse object with a success code reflecting whether the ransomware was launched. + :rtype: RequestResponse + """ + command_payload = request[-1] + return self.send_command(given_command=C2Command.DATA_EXFILTRATION, command_options=command_payload) + def _remote_terminal_action(request: RequestFormat, context: Dict) -> RequestResponse: """Agent Action - Sends a TERMINAL C2Command to the C2 Beacon with the given parameters. @@ -92,6 +105,10 @@ class C2Server(AbstractC2, identifier="C2Server"): name="terminal_command", request_type=RequestType(func=_remote_terminal_action), ) + rm.add_request( + name="exfiltrate", + request_type=RequestType(func=_data_exfiltration_action), + ) return rm def __init__(self, **kwargs): @@ -177,6 +194,7 @@ class C2Server(AbstractC2, identifier="C2Server"): ---------------------|------------------------ RANSOMWARE_CONFIGURE | Configures an installed ransomware script based on the passed parameters. RANSOMWARE_LAUNCH | Launches the installed ransomware script. + DATA_EXFILTRATION | Utilises the FTP Service to exfiltrate data back to the C2 Server. TERMINAL | Executes a command via the terminal installed on the C2 Beacons Host. Currently, these commands leverage the pre-existing capability of other applications. @@ -210,6 +228,14 @@ class C2Server(AbstractC2, identifier="C2Server"): ): return connection_status + if not self._command_setup(given_command, command_options): + self.sys_log.warning( + f"{self.name}: Failed to perform necessary C2 Server setup for given command: {given_command}." + ) + return RequestResponse( + status="failure", data={"Reason": "Failed to perform necessary C2 Server setup for given command."} + ) + self.sys_log.info(f"{self.name}: Attempting to send command {given_command}.") command_packet = self._craft_packet( c2_payload=C2Payload.INPUT, c2_command=given_command, command_options=command_options @@ -232,6 +258,41 @@ class C2Server(AbstractC2, identifier="C2Server"): ) return self.current_command_output + def _command_setup(self, given_command: C2Command, command_options: dict) -> bool: + """ + Performs any necessary C2 Server setup needed to perform certain commands. + + The following table details any C2 Server prequisites for following commands. + + C2 Command | Command Service/Application Requirements + ---------------------|----------------------------------------- + RANSOMWARE_CONFIGURE | N/A + RANSOMWARE_LAUNCH | N/A + DATA_EXFILTRATION | FTP Server & File system folder + TERMINAL | N/A + + Currently, only the data exfiltration command require the C2 Server + to perform any necessary setup. Specifically, the Data Exfiltration command requires + the C2 Server to have an running FTP Server service as well as a folder for + storing any exfiltrated data. + + :param given_command: Any C2 Command. + :type given_command: C2Command. + :param command_options: The relevant command parameters. + :type command_options: Dict + :returns: True the setup was successful, false otherwise. + :rtype: bool + """ + if given_command == C2Command.DATA_EXFILTRATION: # Data exfiltration setup + if self._host_ftp_server is None: + self.sys_log.warning(f"{self.name}: Unable to setup the FTP Server for data exfiltration") + return False + if not self.get_exfiltration_folder(command_options.get("exfiltration_folder_name", "exfil")): + self.sys_log.warning(f"{self.name}: Unable to create a folder for storing exfiltration data.") + return False + + return True + def _confirm_remote_connection(self, timestep: int) -> bool: """Checks the suitability of the current C2 Beacon connection. diff --git a/src/primaite/simulator/system/services/ftp/ftp_client.py b/src/primaite/simulator/system/services/ftp/ftp_client.py index 28a591dd..79216deb 100644 --- a/src/primaite/simulator/system/services/ftp/ftp_client.py +++ b/src/primaite/simulator/system/services/ftp/ftp_client.py @@ -1,8 +1,10 @@ # © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address -from typing import Optional +from typing import Dict, Optional from primaite import getLogger +from primaite.interface.request import RequestFormat, RequestResponse +from primaite.simulator.core import RequestManager, RequestType from primaite.simulator.file_system.file_system import File from primaite.simulator.network.protocols.ftp import FTPCommand, FTPPacket, FTPStatusCode from primaite.simulator.network.transmission.network_layer import IPProtocol @@ -28,6 +30,55 @@ class FTPClient(FTPServiceABC): super().__init__(**kwargs) self.start() + def _init_request_manager(self) -> RequestManager: + """ + Initialise the request manager. + + More information in user guide and docstring for SimComponent._init_request_manager. + """ + rm = super()._init_request_manager() + + def _send_data_request(request: RequestFormat, context: Dict) -> RequestResponse: + """ + Request for sending data via the ftp_client using the request options parameters. + + :param request: Request with one element containing a dict of parameters for the send method. + :type request: RequestFormat + :param context: additional context for resolving this action, currently unused + :type context: dict + :return: RequestResponse object with a success code reflecting whether the configuration could be applied. + :rtype: RequestResponse + """ + dest_ip = request[-1].get("dest_ip_address") + dest_ip = None if dest_ip is None else IPv4Address(dest_ip) + + # TODO: Confirm that the default values lead to a safe failure. + src_folder = request[-1].get("src_folder_name", None) + src_file_name = request[-1].get("src_file_name", None) + dest_folder = request[-1].get("dest_folder_name", None) + dest_file_name = request[-1].get("dest_file_name", None) + + if not self.file_system.access_file(folder_name=src_folder, file_name=src_file_name): + self.sys_log.debug( + f"{self.name}: Received a FTP Request to transfer file: {src_file_name} to Remote IP: {dest_ip}." + ) + return RequestResponse( + status="failure", data={"reason": "Unable to locate requested file on local file system."} + ) + + return RequestResponse.from_bool( + self.send_file( + dest_ip_address=dest_ip, + src_folder_name=src_folder, + src_file_name=src_file_name, + dest_folder_name=dest_folder, + dest_file_name=dest_file_name, + ) + ) + + rm.add_request("send", request_type=RequestType(func=_send_data_request)), + return rm + def _process_ftp_command(self, payload: FTPPacket, session_id: Optional[str] = None, **kwargs) -> FTPPacket: """ Process the command in the FTP Packet. diff --git a/tests/conftest.py b/tests/conftest.py index b6375acd..1328bc9c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -462,6 +462,7 @@ def game_and_agent(): {"type": "C2_SERVER_RANSOMWARE_LAUNCH"}, {"type": "C2_SERVER_RANSOMWARE_CONFIGURE"}, {"type": "C2_SERVER_TERMINAL_COMMAND"}, + {"type": "C2_SERVER_DATA_EXFILTRATE"}, ] action_space = ActionManager( diff --git a/tests/integration_tests/game_layer/actions/test_c2_suite_actions.py b/tests/integration_tests/game_layer/actions/test_c2_suite_actions.py index 990c6363..806ce063 100644 --- a/tests/integration_tests/game_layer/actions/test_c2_suite_actions.py +++ b/tests/integration_tests/game_layer/actions/test_c2_suite_actions.py @@ -15,6 +15,8 @@ from primaite.simulator.network.transmission.transport_layer import Port from primaite.simulator.system.applications.red_applications.c2.c2_beacon import C2Beacon from primaite.simulator.system.applications.red_applications.c2.c2_server import C2Command, C2Server from primaite.simulator.system.services.database.database_service import DatabaseService +from primaite.simulator.system.services.ftp.ftp_client import FTPClient +from primaite.simulator.system.services.ftp.ftp_server import FTPServer from primaite.simulator.system.services.service import ServiceOperatingState @@ -150,3 +152,52 @@ def test_c2_server_ransomware(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyA database_file = server_2.software_manager.file_system.get_file("database", "database.db") assert database_file.health_status == FileSystemItemHealthStatus.CORRUPT + + +def test_c2_server_data_exfiltration(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyAgent]): + """Tests that a Red Agent can extract a database.db file via C2 Server actions.""" + game, agent = game_and_agent_fixture + + # Installing a C2 Beacon on server_1 + server_1: Server = game.simulation.network.get_node_by_hostname("server_1") + server_1.software_manager.install(C2Beacon) + + # Installing a database on Server_2 (creates a database.db file.) + server_2: Server = game.simulation.network.get_node_by_hostname("server_2") + server_2.software_manager.install(DatabaseService) + server_2.software_manager.software["DatabaseService"].start() + + # Configuring the C2 to connect to client 1 (C2 Server) + c2_beacon: C2Beacon = server_1.software_manager.software["C2Beacon"] + c2_beacon.configure(c2_server_ip_address=IPv4Address("10.0.1.2")) + c2_beacon.establish() + assert c2_beacon.c2_connection_active == True + + # Selecting a target file to steal: database.db + # Server 2 ip : 10.0.2.3 + database_file = server_2.software_manager.file_system.get_file(folder_name="database", file_name="database.db") + assert database_file is not None + + # C2 Action: Data exfiltrate. + + action = ( + "C2_SERVER_DATA_EXFILTRATE", + { + "node_id": 0, + "target_file_name": "database.db", + "target_folder_name": "database", + "exfiltration_folder_name": "spoils", + "target_ip_address": "10.0.2.3", + "account": { + "username": "admin", + "password": "admin", + }, + }, + ) + agent.store_action(action) + game.step() + + assert server_1.file_system.access_file(folder_name="spoils", file_name="database.db") + + client_1 = game.simulation.network.get_node_by_hostname("client_1") + assert client_1.file_system.access_file(folder_name="spoils", file_name="database.db") diff --git a/tests/integration_tests/system/red_applications/test_c2_suite_integration.py b/tests/integration_tests/system/red_applications/test_c2_suite_integration.py index 904fb449..4d6432f3 100644 --- a/tests/integration_tests/system/red_applications/test_c2_suite_integration.py +++ b/tests/integration_tests/system/red_applications/test_c2_suite_integration.py @@ -22,6 +22,8 @@ from primaite.simulator.system.applications.red_applications.c2.c2_server import from primaite.simulator.system.applications.red_applications.ransomware_script import RansomwareScript from primaite.simulator.system.services.database.database_service import DatabaseService from primaite.simulator.system.services.dns.dns_server import DNSServer +from primaite.simulator.system.services.ftp.ftp_client import FTPClient +from primaite.simulator.system.services.ftp.ftp_server import FTPServer from primaite.simulator.system.services.web_server.web_server import WebServer from tests import TEST_ASSETS_ROOT @@ -497,3 +499,42 @@ def test_c2_suite_yaml(): assert c2_beacon.c2_connection_active is True assert c2_server.c2_connection_active is True + + +def test_c2_suite_file_extraction(basic_network): + """Test that C2 Beacon can successfully exfiltrate a target file.""" + network: Network = basic_network + network, computer_a, c2_server, computer_b, c2_beacon = setup_c2(network) + # Asserting that the c2 beacon has established a c2 connection + assert c2_beacon.c2_connection_active is True + + # Asserting that the c2 server has established a c2 connection. + assert c2_server.c2_connection_active is True + assert c2_server.c2_remote_connection == IPv4Address("192.168.255.2") + + # Creating the target file on computer_c + computer_c: Computer = network.get_node_by_hostname("node_c") + computer_c.file_system.create_folder("important_files") + computer_c.file_system.create_file(file_name="secret.txt", folder_name="important_files") + assert computer_c.file_system.access_file(folder_name="important_files", file_name="secret.txt") + + # Installing an FTP Server on the same node as C2 Beacon via the terminal: + + # Attempting to exfiltrate secret.txt from computer c to the C2 Server + c2_server.send_command( + given_command=C2Command.DATA_EXFILTRATION, + command_options={ + "username": "admin", + "password": "admin", + "target_ip_address": "192.168.255.3", + "target_folder_name": "important_files", + "exfiltration_folder_name": "yoinked_files", + "target_file_name": "secret.txt", + }, + ) + + # Asserting that C2 Beacon has managed to get the file + assert c2_beacon._host_file_system.access_file(folder_name="yoinked_files", file_name="secret.txt") + + # Asserting that the C2 Beacon can relay it back to the C2 Server + assert c2_server._host_file_system.access_file(folder_name="yoinked_files", file_name="secret.txt") From e53ac846660a149fb398abde7c2c8b708edc7bcb Mon Sep 17 00:00:00 2001 From: Archer Bowen Date: Thu, 15 Aug 2024 11:36:55 +0100 Subject: [PATCH 34/46] #2689 Fixed small bugs, added pydantic class validation and divided the data_Exfil command on c2 beacon into two separate methods. --- src/primaite/game/agent/actions.py | 2 +- .../simulator/network/hardware/base.py | 9 +- .../red_applications/c2/__init__.py | 55 +++++++ .../red_applications/c2/abstract_c2.py | 5 +- .../red_applications/c2/c2_beacon.py | 139 +++++++++--------- .../red_applications/c2/c2_server.py | 2 +- 6 files changed, 138 insertions(+), 74 deletions(-) diff --git a/src/primaite/game/agent/actions.py b/src/primaite/game/agent/actions.py index aa74399e..07bb039e 100644 --- a/src/primaite/game/agent/actions.py +++ b/src/primaite/game/agent/actions.py @@ -1195,7 +1195,7 @@ class TerminalC2ServerAction(AbstractAction): class _Opts(BaseModel): """Schema for options that can be passed to this action.""" - commands: List[RequestFormat] + commands: Union[List[RequestFormat], RequestFormat] ip_address: Optional[str] username: Optional[str] password: Optional[str] diff --git a/src/primaite/simulator/network/hardware/base.py b/src/primaite/simulator/network/hardware/base.py index 1441c93b..08164f22 100644 --- a/src/primaite/simulator/network/hardware/base.py +++ b/src/primaite/simulator/network/hardware/base.py @@ -1275,13 +1275,18 @@ class UserSessionManager(Service): def pre_timestep(self, timestep: int) -> None: """Apply any pre-timestep logic that helps make sure we have the correct observations.""" self.current_timestep = timestep + inactive_sessions: list = [] if self.local_session: if self.local_session.last_active_step + self.local_session_timeout_steps <= timestep: - self._timeout_session(self.local_session) + inactive_sessions.append(self.local_session) + for session in self.remote_sessions: remote_session = self.remote_sessions[session] if remote_session.last_active_step + self.remote_session_timeout_steps <= timestep: - self._timeout_session(remote_session) + inactive_sessions.append(remote_session) + + for sessions in inactive_sessions: + self._timeout_session(sessions) def _timeout_session(self, session: UserSession) -> None: """ diff --git a/src/primaite/simulator/system/applications/red_applications/c2/__init__.py b/src/primaite/simulator/system/applications/red_applications/c2/__init__.py index be6c00e7..97923284 100644 --- a/src/primaite/simulator/system/applications/red_applications/c2/__init__.py +++ b/src/primaite/simulator/system/applications/red_applications/c2/__init__.py @@ -1 +1,56 @@ # © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +from typing import Optional, Union + +from pydantic import BaseModel, Field + +from primaite.interface.request import RequestFormat + + +class Command_Opts(BaseModel): + """A C2 Pydantic Schema acting as a base class for all C2 Commands.""" + + +class Ransomware_Opts(Command_Opts): + """A Pydantic Schema for the Ransomware Configuration command options.""" + + server_ip_address: str + """""" + + payload: Optional[str] = Field(default="ENCRYPT") + """""" + + +class Remote_Opts(Command_Opts): + """A base C2 Pydantic Schema for all C2 Commands that require a remote terminal connection.""" + + ip_address: Optional[str] = Field(default=None) + """""" + + username: str + """""" + + password: str + """""" + + +class Exfil_Opts(Remote_Opts): + """A Pydantic Schema for the C2 Data Exfiltration command options.""" + + target_ip_address: str + """""" + + target_folder_name: str + """""" + + target_file_name: str + """""" + + exfiltration_folder_name: Optional[str] = Field(default="exfiltration_folder") + """""" + + +class Terminal_Opts(Remote_Opts): + """A Pydantic Schema for the C2 Terminal command options.""" + + commands: Union[list[RequestFormat], RequestFormat] + """""" diff --git a/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py b/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py index 6fa34fd6..d273ea23 100644 --- a/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py +++ b/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py @@ -147,7 +147,6 @@ class AbstractC2(Application, identifier="AbstractC2"): kwargs["protocol"] = IPProtocol.TCP super().__init__(**kwargs) - # TODO: We may need to disable the ftp_server/client when using the opposite service. (To test) @property def _host_ftp_client(self) -> Optional[FTPClient]: """Return the FTPClient that is installed C2 Application's host. @@ -214,8 +213,10 @@ class AbstractC2(Application, identifier="AbstractC2"): self.sys_log.error(f"{self.__class__.__name__}: does not seem to have a file system!") return host_file_system - def get_exfiltration_folder(self, folder_name: str) -> Optional[Folder]: + def get_exfiltration_folder(self, folder_name: Optional[str] = "exfiltration_folder") -> Optional[Folder]: """Return a folder used for storing exfiltrated data. Otherwise returns None.""" + if self._host_file_system is None: + return exfiltration_folder: Union[Folder, None] = self._host_file_system.get_folder(folder_name) if exfiltration_folder is None: self.sys_log.info(f"{self.__class__.__name__}: Creating a exfiltration folder.") 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 b3bf1902..47e4f902 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 @@ -11,6 +11,7 @@ from primaite.simulator.core import RequestManager, RequestType from primaite.simulator.network.protocols.masquerade import C2Packet from primaite.simulator.network.transmission.network_layer import IPProtocol from primaite.simulator.network.transmission.transport_layer import Port +from primaite.simulator.system.applications.red_applications.c2 import Exfil_Opts, Ransomware_Opts, Terminal_Opts from primaite.simulator.system.applications.red_applications.c2.abstract_c2 import AbstractC2, C2Command, C2Payload from primaite.simulator.system.applications.red_applications.ransomware_script import RansomwareScript from primaite.simulator.system.services.terminal.terminal import ( @@ -305,7 +306,7 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): :return: Returns the Request Response returned by the Terminal execute method. :rtype: Request Response """ - given_config = payload.payload + command_opts = Ransomware_Opts.model_validate(payload.payload) if self._host_ransomware_script is None: return RequestResponse( status="failure", @@ -313,7 +314,7 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): ) return RequestResponse.from_bool( self._host_ransomware_script.configure( - server_ip_address=given_config["server_ip_address"], payload=given_config["payload"] + server_ip_address=command_opts.server_ip_address, payload=command_opts.payload ) ) @@ -342,10 +343,7 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): Uses the FTP Client & Server services to perform the data exfiltration. - Similar to the terminal, the logic of this command is dependant on if a remote_ip - is present within the payload. - - If a payload does contain an IP address then the C2 Beacon will ssh into the target ip + This command instructs the C2 Beacon to ssh into the target ip and execute a command which causes the FTPClient service to send a target file will be moved from the target IP address onto the C2 Beacon's host @@ -366,67 +364,74 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): data={"Reason": "Cannot find any instances of both a FTP Server & Client. Are they installed?"}, ) - remote_ip = payload.payload.get("target_ip_address") - target_folder = payload.payload.get("target_folder_name") - dest_folder = payload.payload.get("exfiltration_folder_name") - - # Using the same name for both the target/destination file for clarity. - file_name = payload.payload.get("target_file_name") - - # TODO: Split Remote file extraction and local file extraction into different methods. - # if remote_ip is None: - # self._host_ftp_client.start() - # self.sys_log.info(f"{self.name}: No Remote IP given. Returning target file from local file system.") - # return RequestResponse.from_bool(self._host_ftp_client.send_file( - # dest_ip_address=self.c2_remote_connection, - # src_folder_name=target_folder, - # src_file_name=file_name, - # dest_folder_name=dest_folder, - # dest_file_name=file_name, - # session_id=self.c2_session.uuid - # )) - - # Parsing remote login credentials - given_username = payload.payload.get("username") - given_password = payload.payload.get("password") + command_opts = Exfil_Opts.model_validate(payload.payload) # Setting up the terminal session and the ftp server terminal_session = self.get_remote_terminal_session( - username=given_username, password=given_password, ip_address=remote_ip + username=command_opts.username, password=command_opts.password, ip_address=command_opts.target_ip_address ) - # Initialising the exfiltration folder. - exfiltration_folder = self.get_exfiltration_folder(dest_folder) - # Using the terminal to start the FTP Client on the remote machine. - # This can fail if the FTP Client is already enabled. terminal_session.execute(command=["service", "start", "FTPClient"]) + + # Need to supply to the FTP Client the C2 Beacon's host IP. host_network_interfaces = self.software_manager.node.network_interfaces local_ip = host_network_interfaces.get(next(iter(host_network_interfaces))).ip_address + # Creating the FTP creation options. - remote_ftp_options = { + exfil_opts = { "dest_ip_address": str(local_ip), - "src_folder_name": target_folder, - "src_file_name": file_name, - "dest_folder_name": dest_folder, - "dest_file_name": file_name, + "src_folder_name": command_opts.target_folder_name, + "src_file_name": command_opts.target_file_name, + "dest_folder_name": command_opts.exfiltration_folder_name, + "dest_file_name": command_opts.target_file_name, } + # Lambda method used to return a failure RequestResponse if we're unable to perform the exfiltration. + # If _check_connection returns false then connection_status will return reason (A 'failure' Request Response) + if attempt_exfiltration := (lambda return_bool, reason: reason if return_bool is False else None)( + *self._perform_target_exfiltration(exfil_opts, terminal_session) + ): + return attempt_exfiltration + + # Sending the transferred target data back to the C2 Server to successfully exfiltrate the data out the network. + + return RequestResponse.from_bool( + self._host_ftp_client.send_file( + dest_ip_address=self.c2_remote_connection, + src_folder_name=command_opts.exfiltration_folder_name, # The Exfil folder is inherited attribute. + src_file_name=command_opts.target_file_name, + dest_folder_name=command_opts.exfiltration_folder_name, + dest_file_name=command_opts.target_file_name, + ) + ) + + def _perform_target_exfiltration( + self, exfil_opts: dict, terminal_session: RemoteTerminalConnection + ) -> tuple[bool, RequestResponse]: + """Confirms that the target data is currently present within the C2 Beacon's hosts file system.""" # Using the terminal to send the target data back to the C2 Beacon. - remote_ftp_response: RequestResponse = RequestResponse.from_bool( - terminal_session.execute(command=["service", "FTPClient", "send", remote_ftp_options]) + exfil_response: RequestResponse = RequestResponse.from_bool( + terminal_session.execute(command=["service", "FTPClient", "send", exfil_opts]) ) # Validating that we successfully received the target data. - if remote_ftp_response.status == "failure": - self.sys_log.warning( - f"{self.name}: Remote connection: {remote_ip} failed to transfer the target data via FTP." - ) - return remote_ftp_response + if exfil_response.status == "failure": + self.sys_log.warning(f"{self.name}: Remote connection failure. failed to transfer the target data via FTP.") + return [False, exfil_response] - if exfiltration_folder.get_file(file_name) is None: - self.sys_log.warning(f"{self.name}: Unable to locate exfiltrated file on local filesystem.") + # Target file: + target_file: str = exfil_opts.get("src_file_name") + + # Creating the exfiltration folder . + exfiltration_folder = self.get_exfiltration_folder(exfil_opts.get("src_folder_name")) + + if exfiltration_folder.get_file(target_file) is None: + self.sys_log.warning( + f"{self.name}: Unable to locate exfiltrated file on local filesystem." + f"Perhaps the file transfer failed?" + ) return RequestResponse( status="failure", data={"reason": "Unable to locate exfiltrated data on file system."} ) @@ -438,16 +443,13 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): data={"Reason": "Cannot find any instances of both a FTP Server & Client. Are they installed?"}, ) - # Sending the transferred target data back to the C2 Server to successfully exfiltrate the data out the network. - return RequestResponse.from_bool( - self._host_ftp_client.send_file( - dest_ip_address=self.c2_remote_connection, - src_folder_name=dest_folder, # TODO: Clarify this - Dest folder has the same name on c2server/c2beacon. - src_file_name=file_name, - dest_folder_name=dest_folder, - dest_file_name=file_name, - ) - ) + return [ + True, + RequestResponse( + status="success", + data={"Reason": "Located the target file on local file system. Data exfiltration successful."}, + ), + ] def _command_terminal(self, payload: C2Packet) -> RequestResponse: """ @@ -461,8 +463,7 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): :return: Returns the Request Response returned by the Terminal execute method. :rtype: Request Response """ - terminal_output: Dict[int, RequestResponse] = {} - given_commands: list[RequestFormat] + command_opts = Terminal_Opts.model_validate(payload.payload) if self._host_terminal is None: return RequestResponse( @@ -470,17 +471,14 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): data={"Reason": "Host does not seem to have terminal installed. Unable to resolve command."}, ) - given_commands = payload.payload.get("commands") - given_username = payload.payload.get("username") - given_password = payload.payload.get("password") - remote_ip = payload.payload.get("ip_address") + terminal_output: Dict[int, RequestResponse] = {} # Creating a remote terminal session if given an IP Address, otherwise using a local terminal session. - if remote_ip is None: - terminal_session = self.get_terminal_session(username=given_username, password=given_password) + if command_opts.ip_address is None: + terminal_session = self.get_terminal_session(username=command_opts.username, password=command_opts.password) else: terminal_session = self.get_remote_terminal_session( - username=given_username, password=given_password, ip_address=remote_ip + username=command_opts.username, password=command_opts.password, ip_address=command_opts.ip_address ) if terminal_session is None: @@ -488,7 +486,12 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): status="failure", data={"reason": "Terminal Login failed. Cannot create a terminal session."} ) - for index, given_command in enumerate(given_commands): + # Converts a singular terminal command: [RequestFormat] into a list with one element [[RequestFormat]] + command_opts.commands = ( + [command_opts.commands] if not isinstance(command_opts.commands, list) else command_opts.commands + ) + + for index, given_command in enumerate(command_opts.commands): # A try catch exception ladder was used but was considered not the best approach # as it can end up obscuring visibility of actual bugs (Not the expected ones) and was a temporary solution. # TODO: Refactor + add further validation to ensure that a request is correct. (maybe a pydantic method?) diff --git a/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py b/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py index 3441a86b..f4cc7aa6 100644 --- a/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py +++ b/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py @@ -287,7 +287,7 @@ class C2Server(AbstractC2, identifier="C2Server"): if self._host_ftp_server is None: self.sys_log.warning(f"{self.name}: Unable to setup the FTP Server for data exfiltration") return False - if not self.get_exfiltration_folder(command_options.get("exfiltration_folder_name", "exfil")): + if self.get_exfiltration_folder(command_options.get("exfiltration_folder_name")) is None: self.sys_log.warning(f"{self.name}: Unable to create a folder for storing exfiltration data.") return False From c50b005c375faee7b425d5aae7a7429904e8a066 Mon Sep 17 00:00:00 2001 From: Archer Bowen Date: Thu, 15 Aug 2024 13:10:47 +0100 Subject: [PATCH 35/46] #2689 Improved terminal session handling. --- .../Command-&-Control-E2E-Demonstration.ipynb | 42 ++++++- .../red_applications/c2/__init__.py | 24 ++-- .../red_applications/c2/c2_beacon.py | 118 ++++++++++-------- 3 files changed, 112 insertions(+), 72 deletions(-) diff --git a/src/primaite/notebooks/Command-&-Control-E2E-Demonstration.ipynb b/src/primaite/notebooks/Command-&-Control-E2E-Demonstration.ipynb index 052136f8..6fa91c04 100644 --- a/src/primaite/notebooks/Command-&-Control-E2E-Demonstration.ipynb +++ b/src/primaite/notebooks/Command-&-Control-E2E-Demonstration.ipynb @@ -1557,7 +1557,7 @@ "source": [ "The code cell below goes through 10 timesteps and displays the differences between the default and the current timestep.\n", "\n", - "You will notice that the only observation space differences after 10 timesteps. This is due to the C2 Suite confirming their connection through sending ``Keep Alive`` traffic across the network." + "You will notice that the only two timesteps displayed observation space differences. This is due to the C2 Suite confirming their connection through sending ``Keep Alive`` traffic across the network every 5 timesteps." ] }, { @@ -1575,7 +1575,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Next, the code cells below configuring the C2 Beacon's Keep Alive Frequency to confirm connection on every timestep." + "Next, the code cells below configure the C2 Beacon to confirm connection on every timestep via changing the ``keep_alive_frequency`` to ``1``." ] }, { @@ -1593,7 +1593,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "The code cells below demonstrate that the observation impacts of the Keep Alive can be seen on every timestep. " + "Demonstrating that the observation impacts of the Keep Alive can be seen on every timestep:" ] }, { @@ -1608,6 +1608,29 @@ " display_obs_diffs(default_obs, keep_alive_obs, blue_config_env.game.step_counter)" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Additionally, the keep_alive_frequency can also be used to configure the C2 Beacon to confirm connection less frequently. \n", + "\n", + "The code cells below demonstrate the impacts of changing the frequency rate to ``7`` timesteps." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "c2_beacon.configure(c2_server_ip_address=\"192.168.10.21\", keep_alive_frequency=7)\n", + "\n", + "# Comparing the OBS of the default frequency to a timestep frequency of 7\n", + "for i in range(7):\n", + " keep_alive_obs, _, _, _, _ = blue_config_env.step(0)\n", + " display_obs_diffs(default_obs, keep_alive_obs, blue_config_env.game.step_counter)" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -1618,8 +1641,17 @@ "\n", "In the real world, Adversaries take defensive steps to reduce the chance that an installed C2 Beacon is discovered. One of the most commonly used methods is to masquerade c2 traffic as other commonly used networking protocols.\n", "\n", - "In primAITE, red agents can begin to simulate stealth behaviour by configuring C2 traffic to use different protocols mid episode or between episodes. \n", - "Currently, red agent actions are limited to using ports: ``DNS``, ``FTP`` and ``HTTP`` and protocols: ``UDP`` and ``TCP``.\n", + "In primAITE, red agents can begin to simulate stealth behaviour by configuring C2 traffic to use different protocols mid episode or between episodes.\n", + "\n", + "Currently, red agent actions support the following port and protocol options:\n", + "\n", + "| Supported Ports | Supported Protocols |\n", + "|------------------|---------------------|\n", + "|``DNS`` | ``UDP`` |\n", + "|``FTP`` | ``TCP`` |\n", + "|``HTTP`` | |\n", + "\n", + "\n", "\n", "The next set of code cells will demonstrate the impact this option from a blue agent perspective." ] diff --git a/src/primaite/simulator/system/applications/red_applications/c2/__init__.py b/src/primaite/simulator/system/applications/red_applications/c2/__init__.py index 97923284..919b1bf5 100644 --- a/src/primaite/simulator/system/applications/red_applications/c2/__init__.py +++ b/src/primaite/simulator/system/applications/red_applications/c2/__init__.py @@ -14,36 +14,36 @@ class Ransomware_Opts(Command_Opts): """A Pydantic Schema for the Ransomware Configuration command options.""" server_ip_address: str - """""" + """The IP Address of the target database that the RansomwareScript will attack.""" payload: Optional[str] = Field(default="ENCRYPT") - """""" + """The malicious payload to be used to attack the target database.""" class Remote_Opts(Command_Opts): - """A base C2 Pydantic Schema for all C2 Commands that require a remote terminal connection.""" + """A base C2 Pydantic Schema for all C2 Commands that require a terminal connection.""" ip_address: Optional[str] = Field(default=None) - """""" + """The IP address of a remote host. If this field defaults to None then a local session is used.""" username: str - """""" + """A Username of a valid user account. Used to login into both remote and local hosts.""" password: str - """""" + """A Password of a valid user account. Used to login into both remote and local hosts.""" class Exfil_Opts(Remote_Opts): """A Pydantic Schema for the C2 Data Exfiltration command options.""" target_ip_address: str - """""" - - target_folder_name: str - """""" + """The IP address of the target host that will be the target of the exfiltration.""" target_file_name: str - """""" + """The name of the file that is attempting to be exfiltrated.""" + + target_folder_name: str + """The name of the remote folder which contains the target file.""" exfiltration_folder_name: Optional[str] = Field(default="exfiltration_folder") """""" @@ -53,4 +53,4 @@ class Terminal_Opts(Remote_Opts): """A Pydantic Schema for the C2 Terminal command options.""" commands: Union[list[RequestFormat], RequestFormat] - """""" + """A list or individual Terminal Command. Please refer to the RequestResponse system for further info.""" 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 47e4f902..71703500 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 @@ -14,11 +14,7 @@ from primaite.simulator.network.transmission.transport_layer import Port from primaite.simulator.system.applications.red_applications.c2 import Exfil_Opts, Ransomware_Opts, Terminal_Opts from primaite.simulator.system.applications.red_applications.c2.abstract_c2 import AbstractC2, C2Command, C2Payload from primaite.simulator.system.applications.red_applications.ransomware_script import RansomwareScript -from primaite.simulator.system.services.terminal.terminal import ( - LocalTerminalConnection, - RemoteTerminalConnection, - Terminal, -) +from primaite.simulator.system.services.terminal.terminal import Terminal, TerminalClientConnection class C2Beacon(AbstractC2, identifier="C2Beacon"): @@ -43,11 +39,8 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): keep_alive_attempted: bool = False """Indicates if a keep alive has been attempted to be sent this timestep. Used to prevent packet storms.""" - local_terminal_session: LocalTerminalConnection = None - "The currently in use local terminal session." - - remote_terminal_session: RemoteTerminalConnection = None - "The currently in use remote terminal session" + terminal_session: TerminalClientConnection = None + "The currently in use terminal session." @property def _host_terminal(self) -> Optional[Terminal]: @@ -65,25 +58,20 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): self.sys_log.warning(f"{self.__class__.__name__} cannot find installed ransomware on its host.") return ransomware_script - def get_terminal_session(self, username: str, password: str) -> Optional[LocalTerminalConnection]: - """Return an instance of a Local Terminal Connection upon successful login. Otherwise returns None.""" - if self.local_terminal_session is None: - host_terminal: Terminal = self._host_terminal - self.local_terminal_session = host_terminal.login(username=username, password=password) + def _set_terminal_session(self, username: str, password: str, ip_address: Optional[IPv4Address] = None) -> bool: + """ + Attempts to create and a terminal session using the parameters given. - return self.local_terminal_session + If an IP Address is passed then this method will attempt to create a remote terminal + session. Otherwise a local terminal session will be created. - def get_remote_terminal_session( - self, username: str, password: str, ip_address: IPv4Address - ) -> Optional[RemoteTerminalConnection]: - """Return an instance of a Local Terminal Connection upon successful login. Otherwise returns None.""" - if self.remote_terminal_session is None: - host_terminal: Terminal = self._host_terminal - self.remote_terminal_session = host_terminal.login( - username=username, password=password, ip_address=ip_address - ) - - return self.remote_terminal_session + :return: Returns true if a terminal session was successfully set. False otherwise. + :rtype: Bool + """ + self.terminal_session is None + host_terminal: Terminal = self._host_terminal + self.terminal_session = host_terminal.login(username=username, password=password, ip_address=ip_address) + return self.terminal_session is not None def _init_request_manager(self) -> RequestManager: """ @@ -354,7 +342,7 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): :payload C2Packet: The incoming INPUT command. :type Masquerade Packet: C2Packet. - :return: Returns the Request Response returned by the Terminal execute method. + :return: Returns a tuple containing Request Response returned by the Terminal execute method. :rtype: Request Response """ if self._host_ftp_server is None: @@ -367,12 +355,16 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): command_opts = Exfil_Opts.model_validate(payload.payload) # Setting up the terminal session and the ftp server - terminal_session = self.get_remote_terminal_session( + if not self._set_terminal_session( username=command_opts.username, password=command_opts.password, ip_address=command_opts.target_ip_address - ) + ): + return RequestResponse( + status="failure", + data={"Reason": "Cannot create a terminal session. Are the credentials correct?"}, + ) # Using the terminal to start the FTP Client on the remote machine. - terminal_session.execute(command=["service", "start", "FTPClient"]) + self.terminal_session.execute(command=["service", "start", "FTPClient"]) # Need to supply to the FTP Client the C2 Beacon's host IP. host_network_interfaces = self.software_manager.node.network_interfaces @@ -390,7 +382,7 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): # Lambda method used to return a failure RequestResponse if we're unable to perform the exfiltration. # If _check_connection returns false then connection_status will return reason (A 'failure' Request Response) if attempt_exfiltration := (lambda return_bool, reason: reason if return_bool is False else None)( - *self._perform_target_exfiltration(exfil_opts, terminal_session) + *self._perform_exfiltration(exfil_opts) ): return attempt_exfiltration @@ -406,13 +398,29 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): ) ) - def _perform_target_exfiltration( - self, exfil_opts: dict, terminal_session: RemoteTerminalConnection - ) -> tuple[bool, RequestResponse]: - """Confirms that the target data is currently present within the C2 Beacon's hosts file system.""" + def _perform_exfiltration(self, exfil_opts: Exfil_Opts) -> tuple[bool, RequestResponse]: + """ + Attempts to exfiltrate a target file from a target using the parameters given. + + Uses the current terminal_session to send a command to the + remote host's FTP Client passing the exfil_opts as command options. + + This will instruct the FTP client to send the target file to the + dest_ip_address's destination folder. + + This method assumes that the following: + 1. The self.terminal_session is the remote target. + 2. The target has a functioning FTP Client Service. + + + :exfil_opts: A Pydantic model containing the require configuration options + :type exfil_opts: Exfil_Opts + :return: Returns a tuple containing a success boolean and a Request Response.. + :rtype: tuple[bool, RequestResponse + """ # Using the terminal to send the target data back to the C2 Beacon. exfil_response: RequestResponse = RequestResponse.from_bool( - terminal_session.execute(command=["service", "FTPClient", "send", exfil_opts]) + self.terminal_session.execute(command=["service", "FTPClient", "send", exfil_opts]) ) # Validating that we successfully received the target data. @@ -432,16 +440,20 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): f"{self.name}: Unable to locate exfiltrated file on local filesystem." f"Perhaps the file transfer failed?" ) - return RequestResponse( - status="failure", data={"reason": "Unable to locate exfiltrated data on file system."} - ) + return [ + False, + RequestResponse(status="failure", data={"reason": "Unable to locate exfiltrated data on file system."}), + ] if self._host_ftp_client is None: self.sys_log.warning(f"{self.name}: C2 Beacon unable to the FTP Server. Unable to resolve command.") - return RequestResponse( - status="failure", - data={"Reason": "Cannot find any instances of both a FTP Server & Client. Are they installed?"}, - ) + return [ + False, + RequestResponse( + status="failure", + data={"Reason": "Cannot find any instances of both a FTP Server & Client. Are they installed?"}, + ), + ] return [ True, @@ -474,16 +486,12 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): terminal_output: Dict[int, RequestResponse] = {} # Creating a remote terminal session if given an IP Address, otherwise using a local terminal session. - if command_opts.ip_address is None: - terminal_session = self.get_terminal_session(username=command_opts.username, password=command_opts.password) - else: - terminal_session = self.get_remote_terminal_session( - username=command_opts.username, password=command_opts.password, ip_address=command_opts.ip_address - ) - - if terminal_session is None: + if not self._set_terminal_session( + username=command_opts.username, password=command_opts.password, ip_address=command_opts.ip_address + ): return RequestResponse( - status="failure", data={"reason": "Terminal Login failed. Cannot create a terminal session."} + status="failure", + data={"Reason": "Cannot create a terminal session. Are the credentials correct?"}, ) # Converts a singular terminal command: [RequestFormat] into a list with one element [[RequestFormat]] @@ -495,10 +503,10 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): # A try catch exception ladder was used but was considered not the best approach # as it can end up obscuring visibility of actual bugs (Not the expected ones) and was a temporary solution. # TODO: Refactor + add further validation to ensure that a request is correct. (maybe a pydantic method?) - terminal_output[index] = terminal_session.execute(given_command) + terminal_output[index] = self.terminal_session.execute(given_command) # Reset our remote terminal session. - self.remote_terminal_session is None + self.terminal_session is None return RequestResponse(status="success", data=terminal_output) def _handle_keep_alive(self, payload: C2Packet, session_id: Optional[str]) -> bool: From f32b3a931f62bdaa39364d3afc48566af31f24cd Mon Sep 17 00:00:00 2001 From: Archer Bowen Date: Thu, 15 Aug 2024 14:41:35 +0100 Subject: [PATCH 36/46] #2689 Addressed failing tests + updated c2_suite.rst to include the Data exfil command. --- .../system/applications/c2_suite.rst | 42 +++++++++++++++---- .../Command-&-Control-E2E-Demonstration.ipynb | 10 +---- .../red_applications/c2/c2_beacon.py | 19 ++++----- .../red_applications/c2/c2_server.py | 10 ++--- .../test_c2_suite_integration.py | 4 +- 5 files changed, 52 insertions(+), 33 deletions(-) diff --git a/docs/source/simulation_components/system/applications/c2_suite.rst b/docs/source/simulation_components/system/applications/c2_suite.rst index c3044d1d..974bb6ce 100644 --- a/docs/source/simulation_components/system/applications/c2_suite.rst +++ b/docs/source/simulation_components/system/applications/c2_suite.rst @@ -34,6 +34,8 @@ Currently, the C2 Server offers three commands: +---------------------+---------------------------------------------------------------------------+ |RANSOMWARE_LAUNCH | Launches the installed ransomware script. | +---------------------+---------------------------------------------------------------------------+ +|DATA_EXFILTRATION | Copies a target file from a remote node to the C2 Beacon & Server via FTP | ++---------------------+---------------------------------------------------------------------------+ |TERMINAL | Executes a command via the terminal installed on the C2 Beacons Host. | +---------------------+---------------------------------------------------------------------------+ @@ -111,21 +113,28 @@ Python from primaite.simulator.system.applications.red_applications.c2.c2_server import C2Server from primaite.simulator.system.applications.red_applications.c2.c2_server import C2Command from primaite.simulator.network.hardware.nodes.host.computer import Computer - + from primaite.simulator.system.services.database.database_service import DatabaseService + from primaite.simulator.system.applications.database_client import DatabaseClient # Network Setup + switch = Switch(hostname="switch", start_up_duration=0, num_ports=4) + switch.power_on() + node_a = Computer(hostname="node_a", ip_address="192.168.0.10", subnet_mask="255.255.255.0", start_up_duration=0) node_a.power_on() node_a.software_manager.install(software_class=C2Server) - node_a.software_manager.get_open_ports() - + network.connect(node_a.network_interface[1], switch.network_interface[1]) node_b = Computer(hostname="node_b", ip_address="192.168.0.11", subnet_mask="255.255.255.0", start_up_duration=0) node_b.power_on() node_b.software_manager.install(software_class=C2Beacon) - node_b.software_manager.install(software_class=RansomwareScript) - network.connect(node_a.network_interface[1], node_b.network_interface[1]) + node_b.software_manager.install(software_class=DatabaseClient) + network.connect(node_b.network_interface[1], switch.network_interface[2]) + node_c = Computer(hostname="node_c", ip_address="192.168.0.12", subnet_mask="255.255.255.0", start_up_duration=0) + node_c.power_on() + node_c.software_manager.install(software_class=DatabaseServer) + network.connect(node_c.network_interface[1], switch.network_interface[3]) # C2 Application objects @@ -159,7 +168,7 @@ Python c2_server.send_command(C2Command.TERMINAL, command_options=file_create_command) - # Example commands: Installing and configuring Ransomware: + # Example command: Installing and configuring Ransomware: ransomware_installation_command = { "commands": [ ["software_manager","application","install","RansomwareScript"], @@ -170,12 +179,31 @@ Python } c2_server.send_command(given_command=C2Command.TERMINAL, command_options=ransomware_config) - ransomware_config = {"server_ip_address": "192.168.0.10"} + ransomware_config = {"server_ip_address": "192.168.0.12"} c2_server.send_command(given_command=C2Command.RANSOMWARE_CONFIGURE, command_options=ransomware_config) c2_beacon_host.software_manager.show() + # Example command: File Exfiltration + + data_exfil_options = { + "username": "admin", + "password": "admin", + "ip_address": None, + "target_ip_address": "192.168.0.12", + "target_file_name": "database.db" + "target_folder_name": "database" + "exfiltration_folder_name": + } + + c2_server.send_command(given_command=C2Command.DATA_EXFILTRATION, command_options=data_exfil_options) + + # Example command: Launching Ransomware + + c2_server.send_command(given_command=C2Command.RANSOMWARE_LAUNCH, command_options={}) + + Via Configuration """"""""""""""""" diff --git a/src/primaite/notebooks/Command-&-Control-E2E-Demonstration.ipynb b/src/primaite/notebooks/Command-&-Control-E2E-Demonstration.ipynb index 6fa91c04..0e8c8931 100644 --- a/src/primaite/notebooks/Command-&-Control-E2E-Demonstration.ipynb +++ b/src/primaite/notebooks/Command-&-Control-E2E-Demonstration.ipynb @@ -18,20 +18,14 @@ "outputs": [], "source": [ "# Imports\n", + "import yaml\n", "from primaite.config.load import data_manipulation_config_path\n", "from primaite.session.environment import PrimaiteGymEnv\n", "from primaite.simulator.network.hardware.nodes.network.router import Router\n", - "from primaite.game.agent.interface import AgentHistoryItem\n", - "import yaml\n", - "from pprint import pprint\n", - "from primaite.simulator.network.container import Network\n", - "from primaite.game.game import PrimaiteGame\n", - "from primaite.simulator.system.applications.application import ApplicationOperatingState\n", "from primaite.simulator.system.applications.red_applications.c2.c2_beacon import C2Beacon\n", "from primaite.simulator.system.applications.red_applications.c2.c2_server import C2Server\n", - "from primaite.simulator.system.applications.red_applications.c2.abstract_c2 import C2Command, C2Payload\n", + "from primaite.simulator.system.applications.red_applications.c2.abstract_c2 import C2Command\n", "from primaite.simulator.system.applications.red_applications.ransomware_script import RansomwareScript\n", - "from primaite.simulator.system.software import SoftwareHealthState\n", "from primaite.simulator.network.hardware.nodes.host.computer import Computer\n", "from primaite.simulator.network.hardware.nodes.host.server import Server" ] 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 71703500..e2393ff1 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 @@ -379,12 +379,11 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): "dest_file_name": command_opts.target_file_name, } - # Lambda method used to return a failure RequestResponse if we're unable to perform the exfiltration. - # If _check_connection returns false then connection_status will return reason (A 'failure' Request Response) - if attempt_exfiltration := (lambda return_bool, reason: reason if return_bool is False else None)( - *self._perform_exfiltration(exfil_opts) - ): - return attempt_exfiltration + attempt_exfiltration: tuple[bool, RequestResponse] = self._perform_exfiltration(exfil_opts) + + if attempt_exfiltration[0] is False: + self.sys_log.error(f"{self.name}: File Exfiltration Attempt Failed: {attempt_exfiltration[1].data}") + return attempt_exfiltration[1] # Sending the transferred target data back to the C2 Server to successfully exfiltrate the data out the network. @@ -418,6 +417,9 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): :return: Returns a tuple containing a success boolean and a Request Response.. :rtype: tuple[bool, RequestResponse """ + # Creating the exfiltration folder . + exfiltration_folder = self.get_exfiltration_folder(exfil_opts.get("dest_folder_name")) + # Using the terminal to send the target data back to the C2 Beacon. exfil_response: RequestResponse = RequestResponse.from_bool( self.terminal_session.execute(command=["service", "FTPClient", "send", exfil_opts]) @@ -432,12 +434,9 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): # Target file: target_file: str = exfil_opts.get("src_file_name") - # Creating the exfiltration folder . - exfiltration_folder = self.get_exfiltration_folder(exfil_opts.get("src_folder_name")) - if exfiltration_folder.get_file(target_file) is None: self.sys_log.warning( - f"{self.name}: Unable to locate exfiltrated file on local filesystem." + f"{self.name}: Unable to locate exfiltrated file on local filesystem. " f"Perhaps the file transfer failed?" ) return [ diff --git a/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py b/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py index f4cc7aa6..9816bb15 100644 --- a/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py +++ b/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py @@ -221,12 +221,10 @@ class C2Server(AbstractC2, identifier="C2Server"): status="failure", data={"Reason": "Received unexpected C2Command. Unable to send command."} ) - # Lambda method used to return a failure RequestResponse if we're unable to confirm a connection. - # If _check_connection returns false then connection_status will return reason (A 'failure' Request Response) - if connection_status := (lambda return_bool, reason: reason if return_bool is False else None)( - *self._check_connection() - ): - return connection_status + connection_status: tuple[bool, RequestResponse] = self._check_connection() + + if connection_status[0] is False: + return connection_status[1] if not self._command_setup(given_command, command_options): self.sys_log.warning( diff --git a/tests/integration_tests/system/red_applications/test_c2_suite_integration.py b/tests/integration_tests/system/red_applications/test_c2_suite_integration.py index 4d6432f3..910f4760 100644 --- a/tests/integration_tests/system/red_applications/test_c2_suite_integration.py +++ b/tests/integration_tests/system/red_applications/test_c2_suite_integration.py @@ -270,7 +270,7 @@ def test_c2_suite_terminal_command_file_creation(basic_network): c2_server.send_command(C2Command.TERMINAL, command_options=file_create_command) assert computer_b.software_manager.file_system.access_file(folder_name="test_folder", file_name="test_file") == True - assert c2_beacon.local_terminal_session is not None + assert c2_beacon.terminal_session is not None # Testing that we can create the same test file/folders via on node 3 via a remote terminal. @@ -280,7 +280,7 @@ def test_c2_suite_terminal_command_file_creation(basic_network): c2_server.send_command(C2Command.TERMINAL, command_options=file_create_command) assert computer_c.software_manager.file_system.access_file(folder_name="test_folder", file_name="test_file") == True - assert c2_beacon.remote_terminal_session is not None + assert c2_beacon.terminal_session is not None def test_c2_suite_acl_bypass(basic_network): From 7d086ec35e2a5cd3f0a3a9f9b4e25005a598942a Mon Sep 17 00:00:00 2001 From: Archer Bowen Date: Thu, 15 Aug 2024 17:08:10 +0100 Subject: [PATCH 37/46] #2689 Implemented pydantic model validation on C2 Server setup method + updated E2E notebook with data exfiltration. --- .../Command-&-Control-E2E-Demonstration.ipynb | 110 +++++++++++++++--- .../red_applications/c2/__init__.py | 14 ++- .../red_applications/c2/abstract_c2.py | 4 +- .../red_applications/c2/c2_beacon.py | 15 ++- .../red_applications/c2/c2_server.py | 51 ++++++-- .../_red_applications/test_c2_suite.py | 27 +++++ 6 files changed, 182 insertions(+), 39 deletions(-) diff --git a/src/primaite/notebooks/Command-&-Control-E2E-Demonstration.ipynb b/src/primaite/notebooks/Command-&-Control-E2E-Demonstration.ipynb index 0e8c8931..f8c550e0 100644 --- a/src/primaite/notebooks/Command-&-Control-E2E-Demonstration.ipynb +++ b/src/primaite/notebooks/Command-&-Control-E2E-Demonstration.ipynb @@ -500,9 +500,9 @@ "source": [ "### **Command and Control** | C2 Server Actions | C2_SERVER_DATA_EXFILTRATE\n", "\n", - "Finally, currently the last action available is the ``C2_SERVER_DATA_EXFILTRATE`` which can be used to exfiltrate a target_file on a remote node to the C2 Beacon & Server's host file system via the ``FTP`` services.\n", + "The second to last action available is the ``C2_SERVER_DATA_EXFILTRATE`` which can be used to exfiltrate a target file on a remote node to the C2 Beacon & Server's host file system via the ``FTP`` services.\n", "\n", - "This action is indexed as action ``9``. # TODO: Update.\n", + "This action is indexed as action ``6``..\n", "\n", "The below yaml snippet shows all the relevant agent options for this action\n", "\n", @@ -651,9 +651,13 @@ " applications:\n", " - application_name: C2Beacon\n", " - application_name: RansomwareScript\n", + " folders:\n", + " - folder_name: exfiltration_folder\n", + " files:\n", + " - file_name: database.db\n", " - hostname: database_server\n", " folders:\n", - " - folder_name: database\n", + " - folder_name: exfiltration_folder\n", " files:\n", " - file_name: database.db\n", " - hostname: client_1\n", @@ -663,7 +667,7 @@ " num_folders: 1\n", " num_files: 1\n", " num_nics: 1\n", - " include_num_access: false\n", + " include_num_access: true\n", " include_nmne: false\n", " monitored_traffic:\n", " icmp:\n", @@ -832,7 +836,14 @@ "source": [ "### **Command and Control** | Blue Agent Relevance | Observation Space\n", "\n", - "This section demonstrates the OBS impact if the C2 suite is successfully installed and then used to install, configure and launch the ransomwarescript." + "This section demonstrates the impacts that each of that the C2 Beacon and the C2 Server's commands cause on the observation space (OBS)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### **Command and Control** | OBS Impact | C2 Beacon | Installation & Configuration" ] }, { @@ -888,6 +899,19 @@ "display_obs_diffs(default_obs, c2_configuration_obs, blue_env.game.step_counter)" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### **Command and Control** | OBS Impact | C2 Server | Terminal Command\n", + "\n", + "Using the C2 Server's ``TERMINAL`` command it is possible to install a ``RansomwareScript`` application onto the C2 Beacon's host.\n", + "\n", + "The below code cells perform this as well as capturing the OBS impacts.\n", + "\n", + "It's important to note that the ``TERMINAL`` command is not limited to just installing software." + ] + }, { "cell_type": "code", "execution_count": null, @@ -922,11 +946,22 @@ "c2_ransomware_obs, _, _, _, _ = blue_env.step(0)" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "display_obs_diffs(default_obs, c2_ransomware_obs, env.game.step_counter)" + ] + }, { "cell_type": "markdown", "metadata": {}, "source": [ - "The code cell below demonstrates the differences between the default observation space and the configuration of the C2 Server and the Ransomware installation." + "#### **Command and Control** | OBS Impact | C2 Server | Data Exfiltration\n", + "\n", + "Before encrypting the database.db file, the ``DATA_EXFILTRATION`` command can be used to copy the database.db file onto both the C2 Server and the C2 Beacon's file systems:" ] }, { @@ -935,7 +970,61 @@ "metadata": {}, "outputs": [], "source": [ - "display_obs_diffs(default_obs, c2_ransomware_obs, env.game.step_counter)" + "exfil_options={\n", + " \"username\": \"admin\",\n", + " \"password\": \"admin\",\n", + " \"target_ip_address\": \"192.168.1.14\",\n", + " \"target_folder_name\": \"database\",\n", + " \"exfiltration_folder_name\": \"exfiltration_folder\",\n", + " \"target_file_name\": \"database.db\",\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "c2_server.send_command(given_command=C2Command.DATA_EXFILTRATION, command_options=exfil_options)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "c2_exfil_obs, _, _, _, _ = blue_env.step(0)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "display_obs_diffs(c2_ransomware_obs, c2_exfil_obs, env.game.step_counter)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### **Command and Control** | OBS Impact | C2 Server | Ransomware Commands\n", + "\n", + "The code cell below demonstrates the differences between the ransomware script installation obs and the impact of RansomwareScript upon the database." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Configuring the RansomwareScript\n", + "ransomware_config = {\"server_ip_address\": \"192.168.1.14\", \"payload\": \"ENCRYPT\"}\n", + "c2_server.send_command(C2Command.RANSOMWARE_CONFIGURE, command_options=ransomware_config)" ] }, { @@ -959,13 +1048,6 @@ "c2_final_obs, _, _, _, _ = blue_env.step(0)" ] }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The code cell below demonstrates the differences between the default observation space and the configuration of the C2 Server, the ransomware script installation as well as the impact of RansomwareScript upon the database." - ] - }, { "cell_type": "code", "execution_count": null, diff --git a/src/primaite/simulator/system/applications/red_applications/c2/__init__.py b/src/primaite/simulator/system/applications/red_applications/c2/__init__.py index 919b1bf5..23dfeb31 100644 --- a/src/primaite/simulator/system/applications/red_applications/c2/__init__.py +++ b/src/primaite/simulator/system/applications/red_applications/c2/__init__.py @@ -1,7 +1,7 @@ # © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from typing import Optional, Union -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field, field_validator, ValidationInfo from primaite.interface.request import RequestFormat @@ -9,6 +9,14 @@ from primaite.interface.request import RequestFormat class Command_Opts(BaseModel): """A C2 Pydantic Schema acting as a base class for all C2 Commands.""" + @field_validator("payload", "exfiltration_folder_name", "ip_address", mode="before", check_fields=False) + @classmethod + def not_none(cls, v: str, info: ValidationInfo) -> int: + """If None is passed, use the default value instead.""" + if v is None: + return cls.model_fields[info.field_name].default + return v + class Ransomware_Opts(Command_Opts): """A Pydantic Schema for the Ransomware Configuration command options.""" @@ -16,7 +24,7 @@ class Ransomware_Opts(Command_Opts): server_ip_address: str """The IP Address of the target database that the RansomwareScript will attack.""" - payload: Optional[str] = Field(default="ENCRYPT") + payload: str = Field(default="ENCRYPT") """The malicious payload to be used to attack the target database.""" @@ -45,7 +53,7 @@ class Exfil_Opts(Remote_Opts): target_folder_name: str """The name of the remote folder which contains the target file.""" - exfiltration_folder_name: Optional[str] = Field(default="exfiltration_folder") + exfiltration_folder_name: str = Field(default="exfiltration_folder") """""" diff --git a/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py b/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py index d273ea23..b21a996d 100644 --- a/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py +++ b/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py @@ -185,9 +185,9 @@ class AbstractC2(Application, identifier="AbstractC2"): If a FTPServer is not installed then this method will attempt to install one. :return: An FTPServer object is successful, else None - :rtype: union[FTPServer, None] + :rtype: Optional[FTPServer] """ - ftp_server: Union[FTPServer, None] = self.software_manager.software.get("FTPServer") + ftp_server: Optional[FTPServer] = self.software_manager.software.get("FTPServer") if ftp_server is None: self.sys_log.warning(f"{self.__class__.__name__}:No FTPServer installed. Attempting to install FTPServer.") self.software_manager.install(FTPServer) 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 e2393ff1..9c63bb53 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 @@ -359,8 +359,7 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): username=command_opts.username, password=command_opts.password, ip_address=command_opts.target_ip_address ): return RequestResponse( - status="failure", - data={"Reason": "Cannot create a terminal session. Are the credentials correct?"}, + status="failure", data={"Reason": "Cannot create a terminal session. Are the credentials correct?"} ) # Using the terminal to start the FTP Client on the remote machine. @@ -371,7 +370,7 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): local_ip = host_network_interfaces.get(next(iter(host_network_interfaces))).ip_address # Creating the FTP creation options. - exfil_opts = { + ftp_opts = { "dest_ip_address": str(local_ip), "src_folder_name": command_opts.target_folder_name, "src_file_name": command_opts.target_file_name, @@ -379,7 +378,7 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): "dest_file_name": command_opts.target_file_name, } - attempt_exfiltration: tuple[bool, RequestResponse] = self._perform_exfiltration(exfil_opts) + attempt_exfiltration: tuple[bool, RequestResponse] = self._perform_exfiltration(ftp_opts) if attempt_exfiltration[0] is False: self.sys_log.error(f"{self.name}: File Exfiltration Attempt Failed: {attempt_exfiltration[1].data}") @@ -397,7 +396,7 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): ) ) - def _perform_exfiltration(self, exfil_opts: Exfil_Opts) -> tuple[bool, RequestResponse]: + def _perform_exfiltration(self, ftp_opts: dict) -> tuple[bool, RequestResponse]: """ Attempts to exfiltrate a target file from a target using the parameters given. @@ -418,11 +417,11 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): :rtype: tuple[bool, RequestResponse """ # Creating the exfiltration folder . - exfiltration_folder = self.get_exfiltration_folder(exfil_opts.get("dest_folder_name")) + exfiltration_folder = self.get_exfiltration_folder(ftp_opts.get("dest_folder_name")) # Using the terminal to send the target data back to the C2 Beacon. exfil_response: RequestResponse = RequestResponse.from_bool( - self.terminal_session.execute(command=["service", "FTPClient", "send", exfil_opts]) + self.terminal_session.execute(command=["service", "FTPClient", "send", ftp_opts]) ) # Validating that we successfully received the target data. @@ -432,7 +431,7 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): return [False, exfil_response] # Target file: - target_file: str = exfil_opts.get("src_file_name") + target_file: str = ftp_opts.get("src_file_name") if exfiltration_folder.get_file(target_file) is None: self.sys_log.warning( diff --git a/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py b/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py index 9816bb15..8384d922 100644 --- a/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py +++ b/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py @@ -7,6 +7,12 @@ from pydantic import validate_call from primaite.interface.request import RequestFormat, RequestResponse from primaite.simulator.core import RequestManager, RequestType from primaite.simulator.network.protocols.masquerade import C2Packet +from primaite.simulator.system.applications.red_applications.c2 import ( + Command_Opts, + Exfil_Opts, + Ransomware_Opts, + Terminal_Opts, +) from primaite.simulator.system.applications.red_applications.c2.abstract_c2 import AbstractC2, C2Command, C2Payload @@ -49,11 +55,11 @@ class C2Server(AbstractC2, identifier="C2Server"): :return: RequestResponse object with a success code reflecting whether the configuration could be applied. :rtype: RequestResponse """ - ransomware_config = { + command_payload = { "server_ip_address": request[-1].get("server_ip_address"), "payload": request[-1].get("payload"), } - return self.send_command(given_command=C2Command.RANSOMWARE_CONFIGURE, command_options=ransomware_config) + return self.send_command(given_command=C2Command.RANSOMWARE_CONFIGURE, command_options=command_payload) def _launch_ransomware_action(request: RequestFormat, context: Dict) -> RequestResponse: """Agent Action - Sends a RANSOMWARE_LAUNCH C2Command to the C2 Beacon with the given parameters. @@ -226,7 +232,9 @@ class C2Server(AbstractC2, identifier="C2Server"): if connection_status[0] is False: return connection_status[1] - if not self._command_setup(given_command, command_options): + setup_success, command_options = self._command_setup(given_command, command_options) + + if setup_success is False: self.sys_log.warning( f"{self.name}: Failed to perform necessary C2 Server setup for given command: {given_command}." ) @@ -236,7 +244,7 @@ class C2Server(AbstractC2, identifier="C2Server"): self.sys_log.info(f"{self.name}: Attempting to send command {given_command}.") command_packet = self._craft_packet( - c2_payload=C2Payload.INPUT, c2_command=given_command, command_options=command_options + c2_payload=C2Payload.INPUT, c2_command=given_command, command_options=command_options.model_dump() ) if self.send( @@ -256,10 +264,12 @@ class C2Server(AbstractC2, identifier="C2Server"): ) return self.current_command_output - def _command_setup(self, given_command: C2Command, command_options: dict) -> bool: + def _command_setup(self, given_command: C2Command, command_options: dict) -> tuple[bool, Command_Opts]: """ Performs any necessary C2 Server setup needed to perform certain commands. + This includes any option validation and any other required setup. + The following table details any C2 Server prequisites for following commands. C2 Command | Command Service/Application Requirements @@ -278,18 +288,35 @@ class C2Server(AbstractC2, identifier="C2Server"): :type given_command: C2Command. :param command_options: The relevant command parameters. :type command_options: Dict - :returns: True the setup was successful, false otherwise. - :rtype: bool + :returns: Tuple containing a success bool if the setup was successful and the validated c2 opts. + :rtype: tuple[bool, Command_Opts] """ + server_setup_success: bool = True + if given_command == C2Command.DATA_EXFILTRATION: # Data exfiltration setup + # Validating command options + command_options = Exfil_Opts.model_validate(command_options) if self._host_ftp_server is None: self.sys_log.warning(f"{self.name}: Unable to setup the FTP Server for data exfiltration") - return False - if self.get_exfiltration_folder(command_options.get("exfiltration_folder_name")) is None: - self.sys_log.warning(f"{self.name}: Unable to create a folder for storing exfiltration data.") - return False + server_setup_success = False - return True + if self.get_exfiltration_folder(command_options.exfiltration_folder_name) is None: + self.sys_log.warning(f"{self.name}: Unable to create a folder for storing exfiltration data.") + server_setup_success = False + + if given_command == C2Command.TERMINAL: + # Validating command options + command_options = Terminal_Opts.model_validate(command_options) + + if given_command == C2Command.RANSOMWARE_CONFIGURE: + # Validating command options + command_options = Ransomware_Opts.model_validate(command_options) + + if given_command == C2Command.RANSOMWARE_LAUNCH: + # Validating command options + command_options = Command_Opts.model_validate(command_options) + + return [server_setup_success, command_options] def _confirm_remote_connection(self, timestep: int) -> bool: """Checks the suitability of the current C2 Beacon connection. diff --git a/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_c2_suite.py b/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_c2_suite.py index 30defe8b..885a3cb6 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_c2_suite.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_c2_suite.py @@ -219,3 +219,30 @@ def test_c2_handles_1_timestep_keep_alive(basic_c2_network): assert c2_beacon.c2_connection_active is True assert c2_server.c2_connection_active is True + + +def test_c2_exfil_folder(basic_c2_network): + """Tests that the C2 suite correctly default and setup their exfiltration_folders.""" + network: Network = basic_c2_network + + network, computer_a, c2_server, computer_b, c2_beacon = setup_c2(network) + + c2_beacon.get_exfiltration_folder() + c2_server.get_exfiltration_folder() + assert c2_beacon.file_system.get_folder("exfiltration_folder") + assert c2_server.file_system.get_folder("exfiltration_folder") + + c2_server.file_system.create_file(folder_name="test_folder", file_name="test_file") + + # asserting to check that by default the c2 exfil will use "exfiltration_folder" + exfil_options = { + "username": "admin", + "password": "admin", + "target_ip_address": "192.168.0.1", + "target_folder_name": "test_folder", + "exfiltration_folder_name": None, + "target_file_name": "test_file", + } + c2_server.send_command(given_command=C2Command.DATA_EXFILTRATION, command_options=exfil_options) + + assert c2_beacon.file_system.get_file(folder_name="exfiltration_folder", file_name="test_file") From e5be392ea8d8bf534d0c11d51eb89ff585989b5d Mon Sep 17 00:00:00 2001 From: Archer Bowen Date: Thu, 15 Aug 2024 17:47:33 +0100 Subject: [PATCH 38/46] #2689 Updated documentation and docustrings following PR comments. --- .../system/applications/c2_suite.rst | 13 ++- src/primaite/game/agent/actions.py | 2 +- .../Command-&-Control-E2E-Demonstration.ipynb | 82 +++++++++---------- .../red_applications/c2/__init__.py | 12 +-- .../red_applications/c2/abstract_c2.py | 4 +- .../red_applications/c2/c2_beacon.py | 18 ++-- .../red_applications/c2/c2_server.py | 20 ++--- 7 files changed, 75 insertions(+), 76 deletions(-) diff --git a/docs/source/simulation_components/system/applications/c2_suite.rst b/docs/source/simulation_components/system/applications/c2_suite.rst index 974bb6ce..ab6a49e2 100644 --- a/docs/source/simulation_components/system/applications/c2_suite.rst +++ b/docs/source/simulation_components/system/applications/c2_suite.rst @@ -7,14 +7,13 @@ Command and Control Application Suite ##################################### -Comprising of two applications, the command and control (C2) suites intends to introduce +Comprising of two applications, the Command and Control (C2) suites intends to introduce malicious network architecture and begin to further the realism of red agents within primAITE. Overview: ========= -These two new classes intend to Red Agents a cyber realistic way of leveraging the capabilities of the ``Terminal`` application. -Whilst introducing both more opportunities for the blue agent to notice and subvert Red Agents during an episode. +These two new classes give red agents a cyber realistic way of leveraging the capabilities of the ``Terminal`` application whilst introducing more opportunities for the blue agent to notice and subvert a red agent during an episode. For a more in-depth look at the command and control applications then please refer to the ``C2-E2E-Notebook``. @@ -23,7 +22,7 @@ For a more in-depth look at the command and control applications then please ref The C2 Server application is intended to represent the malicious infrastructure already under the control of an adversary. -The C2 Server is configured to listen and await ``keep alive`` traffic from a c2 beacon. Once received the C2 Server is able to send and receive c2 commands. +The C2 Server is configured to listen and await ``keep alive`` traffic from a C2 beacon. Once received the C2 Server is able to send and receive C2 commands. Currently, the C2 Server offers three commands: @@ -88,7 +87,7 @@ Implementation ============== Both applications inherit from an abstract C2 which handles the keep alive functionality and main logic. -However, each host implements it's receive methods individually. +However, each host implements it's own receive methods. - The ``C2 Beacon`` is responsible for the following logic: - Establishes and confirms connection to the C2 Server via sending ``C2Payload.KEEP_ALIVE``. @@ -275,11 +274,11 @@ This must be a valid integer i.e ``10``. Defaults to ``5``. The protocol that the C2 Beacon will use to communicate to the C2 Server with. -Currently only ``tcp`` and ``udp`` are valid masquerade protocol options. +Currently only ``TCP`` and ``UDP`` are valid masquerade protocol options. It's worth noting that this may be useful option to bypass ACL rules. -This must be a string i.e ``udp``. Defaults to ``tcp``. +This must be a string i.e *UDP*. Defaults to ``TCP``. *Please refer to the ``IPProtocol`` class for further reference.* diff --git a/src/primaite/game/agent/actions.py b/src/primaite/game/agent/actions.py index 07bb039e..654ac0ac 100644 --- a/src/primaite/game/agent/actions.py +++ b/src/primaite/game/agent/actions.py @@ -1143,7 +1143,7 @@ class RansomwareLaunchC2ServerAction(AbstractAction): node_name = self.manager.get_node_name_by_idx(node_id) if node_name is None: return ["do_nothing"] - # Not options needed for this action. + # This action currently doesn't require any further configuration options. return ["network", "node", node_name, "application", "C2Server", "ransomware_launch"] diff --git a/src/primaite/notebooks/Command-&-Control-E2E-Demonstration.ipynb b/src/primaite/notebooks/Command-&-Control-E2E-Demonstration.ipynb index f8c550e0..9da39e32 100644 --- a/src/primaite/notebooks/Command-&-Control-E2E-Demonstration.ipynb +++ b/src/primaite/notebooks/Command-&-Control-E2E-Demonstration.ipynb @@ -38,7 +38,7 @@ "\n", "This notebook uses the same network setup as UC2. Please refer to the main [UC2-E2E-Demo notebook for further reference](./Data-Manipulation-E2E-Demonstration.ipynb).\n", "\n", - "However, this notebook will replaces with the red agent used in UC2 with a custom proxy red agent built for this notebook." + "However, this notebook replaces the red agent used in UC2 with a custom proxy red agent built for this notebook." ] }, { @@ -188,11 +188,10 @@ "source": [ "## **Notebook Setup** | Network Prerequisites\n", "\n", - "Before the Red Agent is able to perform any C2 specific actions, the C2 Server needs to be installed and run before the episode begins.\n", + "Before the Red Agent is able to perform any C2 specific actions, the C2 Server needs to be installed and run before the Red Agent can perform any C2 specific action.\n", + "This is because in higher fidelity environments (and the real-world) a C2 server would not be accessible by a private network blue agent and the C2 Server would already be in place before the an adversary (Red Agent) starts.\n", "\n", - "This is because higher fidelity environments (and the real-world) a C2 server would not be accessible by private network blue agent and the C2 Server would already be in place before the an adversary (Red Agent) before the narrative of the use case.\n", - "\n", - "The cells below installs and runs the C2 Server on the client_1 directly via the simulation API." + "The cells below install and runs the C2 Server on client_1 directly via the simulation API." ] }, { @@ -214,9 +213,9 @@ "source": [ "## **Command and Control** | C2 Beacon Actions\n", "\n", - "Before any C2 Server commands is able to accept any commands, it must first establish connection with a C2 beacon.\n", + "Before a C2 Server can accept any commands it must first establish connection with a C2 Beacon.\n", "\n", - "A red agent is able to install, configure and establish a C2 beacon at any point of an episode. The code cells below demonstrate what actions and option parameters are needed to perform this." + "A red agent is able to install, configure and establish a C2 beacon at any point in an episode. The code cells below demonstrate the actions and option parameters that are needed to perform this." ] }, { @@ -225,7 +224,7 @@ "source": [ "### **Command and Control** | C2 Beacon Actions | NODE_APPLICATION_INSTALL\n", "\n", - "The custom proxy red agent defined at the start of this notebook has been configured to install the C2 Beacon as action ``1`` on it's action map. \n", + "The custom proxy red agent defined at the start of this notebook has been configured to install the C2 Beacon as action ``1`` in it's action map. \n", "\n", "The below yaml snippet shows all the relevant agent options for this action:\n", "\n", @@ -268,9 +267,9 @@ "source": [ "### **Command and Control** | C2 Beacon Actions | CONFIGURE_C2_BEACON \n", "\n", - "The custom proxy red agent defined at the start of this notebook can configure the C2 Beacon via action ``2`` on it's action map. \n", + "The custom proxy red agent defined at the start of this notebook can configure the C2 Beacon via action ``2`` in it's action map. \n", "\n", - "The below yaml snippet shows all the relevant agent options for this action:\n", + "The yaml snippet below shows all the relevant agent options for this action:\n", "\n", "```yaml\n", " action_space:\n", @@ -315,9 +314,9 @@ "source": [ "### **Command and Control** | C2 Beacon Actions | NODE_APPLICATION_EXECUTE\n", "\n", - "The final action is ``NODE_APPLICATION_EXECUTE`` which is used to establish connection for the C2 application. This action can be called by the Red Agent via action ``3`` on it's action map. \n", + "The final action is ``NODE_APPLICATION_EXECUTE`` which is used to establish a connection for the C2 application. This action can be called by the Red Agent via action ``3`` in it's action map. \n", "\n", - "The below yaml snippet shows all the relevant agent options for this action:\n", + "The yaml snippet below shows all the relevant agent options for this action:\n", "\n", "```yaml\n", " action_space:\n", @@ -370,7 +369,7 @@ "Once the C2 suite has been successfully established, the C2 Server based actions become available to the Red Agent. \n", "\n", "\n", - "This next section will demonstrate the different actions that become available to a red agent after establishing C2 connection:" + "This next section will demonstrate the different actions that become available to a red agent after establishing a C2 connection:" ] }, { @@ -379,15 +378,15 @@ "source": [ "### **Command and Control** | C2 Server Actions | C2_SERVER_TERMINAL_COMMAND\n", "\n", - "The C2 Server's terminal action is indexed at ``4`` on the custom red agent action map. \n", + "The C2 Server's terminal action: ``C2_SERVER_TERMINAL_COMMAND`` is indexed at ``4`` in it's action map. \n", "\n", "This action leverages the terminal service that is installed by default on all nodes to grant red agents a lot more configurability. If you're unfamiliar with terminals then it's recommended that you refer to the ``Terminal Processing`` notebook.\n", "\n", - "It's worth noting that an additional benefit that a red agent has when using terminal via the C2 Server is that you can execute multiple commands in one action. \n", + "It's worth noting that an additional benefit a red agent has when using the terminal service via the C2 Server is that you can execute multiple commands in one action. \n", "\n", "In this notebook, the ``C2_SERVER_TERMINAL_COMMAND`` is used to install a RansomwareScript application on the ``web_server`` node.\n", "\n", - "The below yaml snippet shows all the relevant agent options for this action:\n", + "The yaml snippet below shows all the relevant agent options for this action:\n", "\n", "``` yaml\n", " action_space:\n", @@ -444,11 +443,11 @@ "source": [ "### **Command and Control** | C2 Server Actions | C2_SERVER_RANSOMWARE_CONFIGURE\n", "\n", - "Another action that the C2 Server grants is the ability for a Red Agent to configure ransomware via the C2 Server. \n", + "Another action the C2 Server grants is the ability for a Red Agent to configure the RansomwareScript via the C2 Server rather than the note directly.\n", "\n", "This action is indexed as action ``5``.\n", "\n", - "The below yaml snippet shows all the relevant agent options for this action:\n", + "The yaml snippet below shows all the relevant agent options for this action:\n", "\n", "``` yaml\n", " action_space:\n", @@ -500,11 +499,11 @@ "source": [ "### **Command and Control** | C2 Server Actions | C2_SERVER_DATA_EXFILTRATE\n", "\n", - "The second to last action available is the ``C2_SERVER_DATA_EXFILTRATE`` which can be used to exfiltrate a target file on a remote node to the C2 Beacon & Server's host file system via the ``FTP`` services.\n", + "The second to last action available is the ``C2_SERVER_DATA_EXFILTRATE`` which is indexed as action ``6`` in the action map.\n", "\n", - "This action is indexed as action ``6``..\n", + "This action can be used to exfiltrate a target file on a remote node to the C2 Beacon and the C2 Server's host file system via the ``FTP`` services.\n", "\n", - "The below yaml snippet shows all the relevant agent options for this action\n", + "The below yaml snippet shows all the relevant agent options for this action:\n", "\n", "``` yaml\n", " action_space:\n", @@ -532,8 +531,7 @@ " username: \"admin\",\n", " password: \"admin\"\n", "\n", - "```\n", - "\n" + "```" ] }, { @@ -571,11 +569,11 @@ "source": [ "### **Command and Control** | C2 Server Actions | C2_SERVER_RANSOMWARE_LAUNCH\n", "\n", - "Finally, to the ransomware configuration action, there is also the ``C2_SERVER_RANSOMWARE_LAUNCH`` which quite simply launches the ransomware script installed on the same node as the C2 beacon.\n", + "Finally, the last available action is for the C2_SERVER_RANSOMWARE_LAUNCH to start the ransomware script installed on the same node as the C2 beacon.\n", "\n", "This action is indexed as action ``7``.\n", "\n", - "The below yaml snippet shows all the relevant agent options for this action\n", + "\"The yaml snippet below shows all the relevant agent options for this action:\n", "\n", "``` yaml\n", " action_space:\n", @@ -623,9 +621,9 @@ "source": [ "## **Command and Control** | Blue Agent Relevance\n", "\n", - "The next section of the notebook will demonstrate the impact that the command and control suite has to the Blue Agent's observation space as well as some potential actions that can be used to prevent the attack from being successfully.\n", + "The next section of the notebook will demonstrate the impact the command and control suite has on the Blue Agent's observation space as well as some potential actions that can be used to prevent the attack from being successful.\n", "\n", - "The code cell below re-creates the UC2 network and swaps out the previous custom red agent with a custom blue agent. \n" + "The code cell below recreates the UC2 network and swaps out the previous custom red agent with a custom blue agent. " ] }, { @@ -1072,7 +1070,7 @@ "metadata": {}, "outputs": [], "source": [ - "# This method is used to shorthand setting up the C2Server and the C2 Beacon.\n", + "# This method is used to simplify setting up the C2Server and the C2 Beacon.\n", "def c2_setup(given_env: PrimaiteGymEnv):\n", " client_1: Computer = given_env.game.simulation.network.get_node_by_hostname(\"client_1\")\n", " web_server: Server = given_env.game.simulation.network.get_node_by_hostname(\"web_server\")\n", @@ -1190,7 +1188,7 @@ "source": [ "#### Shutting down the node infected with a C2 Beacon.\n", "\n", - "Another way a blue agent can prevent the C2 suite is via shutting down the C2 beacon's host node. Whilst not as effective as the previous option, dependant on situation (such as multiple malicious applications) or other scenarios it may be more timestep efficient for a blue agent to shut down a node directly." + "Another way a blue agent can prevent the C2 suite is by shutting down the C2 beacon's host node. Whilst not as effective as the previous option, dependant on the situation (such as multiple malicious applications) or other scenarios it may be more timestep efficient for a blue agent to shut down a node directly." ] }, { @@ -1218,7 +1216,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "The code cell below uses the custom blue agent defined at the start of this section perform NODE_SHUT_DOWN on the web server." + "The code cell below uses the custom blue agent defined at the start of this section perform a ``NODE_SHUT_DOWN`` action on the web server." ] }, { @@ -1235,7 +1233,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Which we can see after the effects of after stepping another timestep and looking at the web_servers operating state & the OBS differences." + "Which we can see the effects of after another timestep and looking at the web_server's operating state & the OBS differences." ] }, { @@ -1264,7 +1262,7 @@ "outputs": [], "source": [ "# Attempting to install the C2 RansomwareScript\n", - "ransomware_install_command = {\"commands\":[[\"software_manager\", \"application\", \"install\", \"RansomwareScript\"]],\n", + "ransomware_install_command = {\"commands\":[\"software_manager\", \"application\", \"install\", \"RansomwareScript\"],\n", " \"username\": \"admin\",\n", " \"password\": \"admin\"}\n", "\n", @@ -1280,7 +1278,7 @@ "\n", "Another potential option a blue agent could take is by placing an ACL rule which blocks traffic between the C2 Server can C2 Beacon.\n", "\n", - "It's worth noting the potential effectiveness of approach is also linked by the current green agent traffic on the network. The same applies for the previous example." + "It's worth noting the potential effectiveness of this approach is connected to the current green agent traffic on the network. For example, if there are multiple green agents using the C2 Beacon's host node then blocking all traffic would lead to a negative reward. The same applies for the previous example." ] }, { @@ -1325,7 +1323,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Which we can see after the effects of after stepping another timestep and looking at router 1's ACLs and the OBS differences." + "Which we can see the effects of after another timestep and looking at router 1's ACLs and the OBS differences." ] }, { @@ -1454,9 +1452,9 @@ "\n", "As with a majority of client and server based application configuration in primaite, the remote IP of server must be supplied.\n", "\n", - "In the case of the C2 Beacon, the C2 Server's IP must be supplied before the C2 beacon will be able to perform any other actions (including ``APPLICATION EXECUTE``).\n", + "In the case of the C2 Beacon, the C2 Server's IP address must be supplied before the C2 beacon will be able to perform any other actions (including ``APPLICATION EXECUTE``).\n", "\n", - "If the network contains multiple C2 Servers then it's also possible to switch to different C2 servers mid episode which is demonstrated in the below code cells." + "If the network contains multiple C2 Servers then it's also possible to switch to different C2 servers mid-episode which is demonstrated in the below code cells." ] }, { @@ -1546,7 +1544,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "After six timesteps the client_1 server will recognise the c2 beacon previous connection as dead and clear it's connections. (This is dependant o the ``Keep Alive Frequency`` setting.)" + "After six timesteps the client_1 server will recognise the C2 beacon's previous connection as dead and clear its connections. (This is dependant on the ``Keep Alive Frequency`` setting.)" ] }, { @@ -1569,7 +1567,7 @@ "\n", "In order to confirm it's connection the C2 Beacon will send out a ``Keep Alive`` to the C2 Server and receive a keep alive back. \n", "\n", - "By default, this occurs at a rate of 5 timesteps. However, this setting can be configured to be much more infrequent or as frequent as every timestep. \n", + "By default, this occurs every 5 timesteps. However, this setting can be configured to be much more infrequent or as frequent as every timestep. \n", "\n", "The next set of code cells below demonstrate the impact that this setting has on blue agent observation space." ] @@ -1631,7 +1629,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "The code cell below goes through 10 timesteps and displays the differences between the default and the current timestep.\n", + "The code cell below executes 10 timesteps and displays the differences between the default and the current timestep.\n", "\n", "You will notice that the only two timesteps displayed observation space differences. This is due to the C2 Suite confirming their connection through sending ``Keep Alive`` traffic across the network every 5 timesteps." ] @@ -1688,7 +1686,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Additionally, the keep_alive_frequency can also be used to configure the C2 Beacon to confirm connection less frequently. \n", + "Lastly, the keep_alive_frequency can also be used to configure the C2 Beacon to confirm connection less frequently. \n", "\n", "The code cells below demonstrate the impacts of changing the frequency rate to ``7`` timesteps." ] @@ -1713,9 +1711,9 @@ "source": [ "### **Command and Control** | Configurability | Masquerade Port & Masquerade Protocol\n", "\n", - "The final configurable options are ``Masquerade Port`` & ``Masquerade Protocol``. These options can be used to control what networking IP Protocol and Port the C2 traffic is currently using.\n", + "The final configurable options are ``Masquerade Port`` & ``Masquerade Protocol``. These options can be used to control the networking IP Protocol and Port the C2 traffic is currently using.\n", "\n", - "In the real world, Adversaries take defensive steps to reduce the chance that an installed C2 Beacon is discovered. One of the most commonly used methods is to masquerade c2 traffic as other commonly used networking protocols.\n", + "In the real world, adversaries take defensive steps to reduce the chance that an installed C2 Beacon is discovered. One of the most commonly used methods is to masquerade C2 traffic as other commonly used networking protocols.\n", "\n", "In primAITE, red agents can begin to simulate stealth behaviour by configuring C2 traffic to use different protocols mid episode or between episodes.\n", "\n", diff --git a/src/primaite/simulator/system/applications/red_applications/c2/__init__.py b/src/primaite/simulator/system/applications/red_applications/c2/__init__.py index 23dfeb31..60e39743 100644 --- a/src/primaite/simulator/system/applications/red_applications/c2/__init__.py +++ b/src/primaite/simulator/system/applications/red_applications/c2/__init__.py @@ -6,7 +6,7 @@ from pydantic import BaseModel, Field, field_validator, ValidationInfo from primaite.interface.request import RequestFormat -class Command_Opts(BaseModel): +class CommandOpts(BaseModel): """A C2 Pydantic Schema acting as a base class for all C2 Commands.""" @field_validator("payload", "exfiltration_folder_name", "ip_address", mode="before", check_fields=False) @@ -18,7 +18,7 @@ class Command_Opts(BaseModel): return v -class Ransomware_Opts(Command_Opts): +class RansomwareOpts(CommandOpts): """A Pydantic Schema for the Ransomware Configuration command options.""" server_ip_address: str @@ -28,7 +28,7 @@ class Ransomware_Opts(Command_Opts): """The malicious payload to be used to attack the target database.""" -class Remote_Opts(Command_Opts): +class RemoteOpts(CommandOpts): """A base C2 Pydantic Schema for all C2 Commands that require a terminal connection.""" ip_address: Optional[str] = Field(default=None) @@ -41,7 +41,7 @@ class Remote_Opts(Command_Opts): """A Password of a valid user account. Used to login into both remote and local hosts.""" -class Exfil_Opts(Remote_Opts): +class ExfilOpts(RemoteOpts): """A Pydantic Schema for the C2 Data Exfiltration command options.""" target_ip_address: str @@ -54,10 +54,10 @@ class Exfil_Opts(Remote_Opts): """The name of the remote folder which contains the target file.""" exfiltration_folder_name: str = Field(default="exfiltration_folder") - """""" + """The name of C2 Suite folder used to store the target file. Defaults to ``exfiltration_folder``""" -class Terminal_Opts(Remote_Opts): +class TerminalOpts(RemoteOpts): """A Pydantic Schema for the C2 Terminal command options.""" commands: Union[list[RequestFormat], RequestFormat] diff --git a/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py b/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py index b21a996d..0d7bbf1f 100644 --- a/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py +++ b/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py @@ -277,8 +277,10 @@ class AbstractC2(Application, identifier="AbstractC2"): """Abstract Method: Used in C2 beacon to parse and handle commands received from the c2 server.""" pass + @abstractmethod def _handle_keep_alive(self, payload: C2Packet, session_id: Optional[str]) -> bool: - """Abstract Method: The C2 Server and the C2 Beacon handle the KEEP ALIVEs differently.""" + """Abstract Method: Each C2 suite handles ``C2Payload.KEEP_ALIVE`` differently.""" + pass # from_network_interface=from_network_interface def receive(self, payload: any, session_id: Optional[str] = None, **kwargs) -> bool: 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 9c63bb53..393512db 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 @@ -11,7 +11,7 @@ from primaite.simulator.core import RequestManager, RequestType from primaite.simulator.network.protocols.masquerade import C2Packet from primaite.simulator.network.transmission.network_layer import IPProtocol from primaite.simulator.network.transmission.transport_layer import Port -from primaite.simulator.system.applications.red_applications.c2 import Exfil_Opts, Ransomware_Opts, Terminal_Opts +from primaite.simulator.system.applications.red_applications.c2 import ExfilOpts, RansomwareOpts, TerminalOpts from primaite.simulator.system.applications.red_applications.c2.abstract_c2 import AbstractC2, C2Command, C2Payload from primaite.simulator.system.applications.red_applications.ransomware_script import RansomwareScript from primaite.simulator.system.services.terminal.terminal import Terminal, TerminalClientConnection @@ -30,7 +30,7 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): Extends the Abstract C2 application to include the following: 1. Receiving commands from the C2 Server (Command input) - 2. Leveraging the terminal application to execute requests (dependant on the command given) + 2. Leveraging the terminal application to execute requests (dependent on the command given) 3. Sending the RequestResponse back to the C2 Server (Command output) Please refer to the Command-&-Control notebook for an in-depth example of the C2 Suite. @@ -156,7 +156,7 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): :type c2_server_ip_address: IPv4Address :param keep_alive_frequency: The frequency (timesteps) at which the C2 beacon will send keep alive(s). :type keep_alive_frequency: Int - :param masquerade_protocol: The Protocol that C2 Traffic will masquerade as. Defaults as TCP. + :param masquerade_protocol: The Protocol that C2 Traffic will masquerade as. Defaults to TCP. :type masquerade_protocol: Enum (IPProtocol) :param masquerade_port: The Port that the C2 Traffic will masquerade as. Defaults to FTP. :type masquerade_port: Enum (Port) @@ -294,7 +294,7 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): :return: Returns the Request Response returned by the Terminal execute method. :rtype: Request Response """ - command_opts = Ransomware_Opts.model_validate(payload.payload) + command_opts = RansomwareOpts.model_validate(payload.payload) if self._host_ransomware_script is None: return RequestResponse( status="failure", @@ -352,7 +352,7 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): data={"Reason": "Cannot find any instances of both a FTP Server & Client. Are they installed?"}, ) - command_opts = Exfil_Opts.model_validate(payload.payload) + command_opts = ExfilOpts.model_validate(payload.payload) # Setting up the terminal session and the ftp server if not self._set_terminal_session( @@ -401,7 +401,7 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): Attempts to exfiltrate a target file from a target using the parameters given. Uses the current terminal_session to send a command to the - remote host's FTP Client passing the exfil_opts as command options. + remote host's FTP Client passing the ExfilOpts as command options. This will instruct the FTP client to send the target file to the dest_ip_address's destination folder. @@ -411,8 +411,8 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): 2. The target has a functioning FTP Client Service. - :exfil_opts: A Pydantic model containing the require configuration options - :type exfil_opts: Exfil_Opts + :ExfilOpts: A Pydantic model containing the require configuration options + :type ExfilOpts: ExfilOpts :return: Returns a tuple containing a success boolean and a Request Response.. :rtype: tuple[bool, RequestResponse """ @@ -473,7 +473,7 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): :return: Returns the Request Response returned by the Terminal execute method. :rtype: Request Response """ - command_opts = Terminal_Opts.model_validate(payload.payload) + command_opts = TerminalOpts.model_validate(payload.payload) if self._host_terminal is None: return RequestResponse( diff --git a/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py b/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py index 8384d922..53552e6e 100644 --- a/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py +++ b/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py @@ -8,10 +8,10 @@ from primaite.interface.request import RequestFormat, RequestResponse from primaite.simulator.core import RequestManager, RequestType from primaite.simulator.network.protocols.masquerade import C2Packet from primaite.simulator.system.applications.red_applications.c2 import ( - Command_Opts, - Exfil_Opts, - Ransomware_Opts, - Terminal_Opts, + CommandOpts, + ExfilOpts, + RansomwareOpts, + TerminalOpts, ) from primaite.simulator.system.applications.red_applications.c2.abstract_c2 import AbstractC2, C2Command, C2Payload @@ -264,7 +264,7 @@ class C2Server(AbstractC2, identifier="C2Server"): ) return self.current_command_output - def _command_setup(self, given_command: C2Command, command_options: dict) -> tuple[bool, Command_Opts]: + def _command_setup(self, given_command: C2Command, command_options: dict) -> tuple[bool, CommandOpts]: """ Performs any necessary C2 Server setup needed to perform certain commands. @@ -289,13 +289,13 @@ class C2Server(AbstractC2, identifier="C2Server"): :param command_options: The relevant command parameters. :type command_options: Dict :returns: Tuple containing a success bool if the setup was successful and the validated c2 opts. - :rtype: tuple[bool, Command_Opts] + :rtype: tuple[bool, CommandOpts] """ server_setup_success: bool = True if given_command == C2Command.DATA_EXFILTRATION: # Data exfiltration setup # Validating command options - command_options = Exfil_Opts.model_validate(command_options) + command_options = ExfilOpts.model_validate(command_options) if self._host_ftp_server is None: self.sys_log.warning(f"{self.name}: Unable to setup the FTP Server for data exfiltration") server_setup_success = False @@ -306,15 +306,15 @@ class C2Server(AbstractC2, identifier="C2Server"): if given_command == C2Command.TERMINAL: # Validating command options - command_options = Terminal_Opts.model_validate(command_options) + command_options = TerminalOpts.model_validate(command_options) if given_command == C2Command.RANSOMWARE_CONFIGURE: # Validating command options - command_options = Ransomware_Opts.model_validate(command_options) + command_options = RansomwareOpts.model_validate(command_options) if given_command == C2Command.RANSOMWARE_LAUNCH: # Validating command options - command_options = Command_Opts.model_validate(command_options) + command_options = CommandOpts.model_validate(command_options) return [server_setup_success, command_options] From 849cb20f3526a4bea8f74d5b599e69e4039867e3 Mon Sep 17 00:00:00 2001 From: Archer Bowen Date: Fri, 16 Aug 2024 10:24:53 +0100 Subject: [PATCH 39/46] #2689 Addressed more PR comments & fixed an bug with command parsing in _command_terminal (c2 beacon) --- .../system/applications/c2_suite.rst | 2 +- src/primaite/game/game.py | 12 ++---- .../Command-&-Control-E2E-Demonstration.ipynb | 12 +++--- .../red_applications/c2/abstract_c2.py | 4 +- .../red_applications/c2/c2_beacon.py | 3 +- .../test_c2_suite_integration.py | 40 +++++++++++++------ 6 files changed, 41 insertions(+), 32 deletions(-) diff --git a/docs/source/simulation_components/system/applications/c2_suite.rst b/docs/source/simulation_components/system/applications/c2_suite.rst index ab6a49e2..28bb1bf8 100644 --- a/docs/source/simulation_components/system/applications/c2_suite.rst +++ b/docs/source/simulation_components/system/applications/c2_suite.rst @@ -13,7 +13,7 @@ malicious network architecture and begin to further the realism of red agents wi Overview: ========= -These two new classes give red agents a cyber realistic way of leveraging the capabilities of the ``Terminal`` application whilst introducing more opportunities for the blue agent to notice and subvert a red agent during an episode. +These two new classes give red agents a cyber realistic way of leveraging the capabilities of the ``Terminal`` application whilst introducing more opportunities for the blue agent(s) to notice and subvert a red agent during an episode. For a more in-depth look at the command and control applications then please refer to the ``C2-E2E-Notebook``. diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index d3035a5a..045b2467 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -461,15 +461,9 @@ class PrimaiteGame: opt = application_cfg["options"] new_application.configure( c2_server_ip_address=IPv4Address(opt.get("c2_server_ip_address")), - keep_alive_frequency=(opt.get("keep_alive_frequency", 5)) - if opt.get("keep_alive_frequency") - else 5, - masquerade_protocol=IPProtocol[(opt.get("masquerade_protocol"))] - if opt.get("masquerade_protocol") - else IPProtocol.TCP, - masquerade_port=Port[(opt.get("masquerade_port"))] - if opt.get("masquerade_port") - else Port.HTTP, + keep_alive_frequency=(opt.get("keep_alive_frequency", 5)), + masquerade_protocol=IPProtocol[(opt.get("masquerade_protocol", IPProtocol.TCP))], + masquerade_port=Port[(opt.get("masquerade_port", Port.HTTP))], ) if "network_interfaces" in node_cfg: for nic_num, nic_cfg in node_cfg["network_interfaces"].items(): diff --git a/src/primaite/notebooks/Command-&-Control-E2E-Demonstration.ipynb b/src/primaite/notebooks/Command-&-Control-E2E-Demonstration.ipynb index 9da39e32..03e50ae4 100644 --- a/src/primaite/notebooks/Command-&-Control-E2E-Demonstration.ipynb +++ b/src/primaite/notebooks/Command-&-Control-E2E-Demonstration.ipynb @@ -191,7 +191,7 @@ "Before the Red Agent is able to perform any C2 specific actions, the C2 Server needs to be installed and run before the Red Agent can perform any C2 specific action.\n", "This is because in higher fidelity environments (and the real-world) a C2 server would not be accessible by a private network blue agent and the C2 Server would already be in place before the an adversary (Red Agent) starts.\n", "\n", - "The cells below install and runs the C2 Server on client_1 directly via the simulation API." + "The cells below install and run the C2 Server on client_1 directly via the simulation API." ] }, { @@ -1188,7 +1188,7 @@ "source": [ "#### Shutting down the node infected with a C2 Beacon.\n", "\n", - "Another way a blue agent can prevent the C2 suite is by shutting down the C2 beacon's host node. Whilst not as effective as the previous option, dependant on the situation (such as multiple malicious applications) or other scenarios it may be more timestep efficient for a blue agent to shut down a node directly." + "Another way a blue agent can prevent the C2 suite is by shutting down the C2 beacon's host node. Whilst not as effective as the previous option, depending on the situation (such as multiple malicious applications) or other scenarios it may be more timestep efficient for a blue agent to shut down a node directly." ] }, { @@ -1216,7 +1216,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "The code cell below uses the custom blue agent defined at the start of this section perform a ``NODE_SHUT_DOWN`` action on the web server." + "The code cell below uses the custom blue agent defined at the start of this section to perform a ``NODE_SHUT_DOWN`` action on the web server." ] }, { @@ -1233,7 +1233,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Which we can see the effects of after another timestep and looking at the web_server's operating state & the OBS differences." + "Which we can see the effects of after another timestep and looking at the web server's operating state & the OBS differences." ] }, { @@ -1454,7 +1454,7 @@ "\n", "In the case of the C2 Beacon, the C2 Server's IP address must be supplied before the C2 beacon will be able to perform any other actions (including ``APPLICATION EXECUTE``).\n", "\n", - "If the network contains multiple C2 Servers then it's also possible to switch to different C2 servers mid-episode which is demonstrated in the below code cells." + "If the network contains multiple C2 Servers then it's also possible to switch to a different C2 servers mid-episode which is demonstrated in the below code cells." ] }, { @@ -1544,7 +1544,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "After six timesteps the client_1 server will recognise the C2 beacon's previous connection as dead and clear its connections. (This is dependant on the ``Keep Alive Frequency`` setting.)" + "After six timesteps the client_1 server will recognise the C2 beacon's previous connection as dead and clear its connections. (This is dependent on the ``Keep Alive Frequency`` setting.)" ] }, { diff --git a/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py b/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py index 0d7bbf1f..7e9de77e 100644 --- a/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py +++ b/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py @@ -75,7 +75,7 @@ class AbstractC2(Application, identifier="AbstractC2"): keep_alive_inactivity: int = 0 """Indicates how many timesteps since the last time the c2 application received a keep alive.""" - class _C2_Opts(BaseModel): + class C2_Opts(BaseModel): """A Pydantic Schema for the different C2 configuration options.""" keep_alive_frequency: int = Field(default=5, ge=1) @@ -87,7 +87,7 @@ class AbstractC2(Application, identifier="AbstractC2"): masquerade_port: Port = Field(default=Port.HTTP) """The currently chosen port that the C2 traffic is masquerading as. Defaults at HTTP.""" - c2_config: _C2_Opts = _C2_Opts() + c2_config: C2_Opts = C2_Opts() """ Holds the current configuration settings of the C2 Suite. 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 393512db..fa0271e5 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 @@ -493,8 +493,9 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): ) # Converts a singular terminal command: [RequestFormat] into a list with one element [[RequestFormat]] + # Checks the first element - if this element is a str then there must be multiple commands. command_opts.commands = ( - [command_opts.commands] if not isinstance(command_opts.commands, list) else command_opts.commands + [command_opts.commands] if isinstance(command_opts.commands[0], str) else command_opts.commands ) for index, given_command in enumerate(command_opts.commands): diff --git a/tests/integration_tests/system/red_applications/test_c2_suite_integration.py b/tests/integration_tests/system/red_applications/test_c2_suite_integration.py index 910f4760..9d12f2cf 100644 --- a/tests/integration_tests/system/red_applications/test_c2_suite_integration.py +++ b/tests/integration_tests/system/red_applications/test_c2_suite_integration.py @@ -257,27 +257,37 @@ def test_c2_suite_terminal_command_file_creation(basic_network): # Testing that we can create the test file and folders via the terminal command (Local C2 Terminal). # Local file/folder creation commands. - file_create_command = { - "commands": [ - ["file_system", "create", "folder", "test_folder"], - ["file_system", "create", "file", "test_folder", "test_file", "True"], - ], + folder_create_command = { + "commands": ["file_system", "create", "folder", "test_folder"], "username": "admin", "password": "admin", "ip_address": None, } + c2_server.send_command(C2Command.TERMINAL, command_options=folder_create_command) + file_create_command = { + "commands": ["file_system", "create", "file", "test_folder", "test_file", "True"], + "username": "admin", + "password": "admin", + "ip_address": None, + } c2_server.send_command(C2Command.TERMINAL, command_options=file_create_command) assert computer_b.software_manager.file_system.access_file(folder_name="test_folder", file_name="test_file") == True assert c2_beacon.terminal_session is not None # Testing that we can create the same test file/folders via on node 3 via a remote terminal. + file_remote_create_command = { + "commands": [ + ["file_system", "create", "folder", "test_folder"], + ["file_system", "create", "file", "test_folder", "test_file", "True"], + ], + "username": "admin", + "password": "admin", + "ip_address": "192.168.255.3", + } - # node_c's IP is 192.168.255.3 - file_create_command.update({"ip_address": "192.168.255.3"}) - - c2_server.send_command(C2Command.TERMINAL, command_options=file_create_command) + c2_server.send_command(C2Command.TERMINAL, command_options=file_remote_create_command) assert computer_c.software_manager.file_system.access_file(folder_name="test_folder", file_name="test_file") == True assert c2_beacon.terminal_session is not None @@ -435,11 +445,15 @@ def test_c2_suite_acl_bypass(basic_network): # Confirming that we can send commands + http_folder_create_command = { + "commands": ["file_system", "create", "folder", "test_folder"], + "username": "admin", + "password": "admin", + "ip_address": None, + } + c2_server.send_command(C2Command.TERMINAL, command_options=http_folder_create_command) http_file_create_command = { - "commands": [ - ["file_system", "create", "folder", "test_folder"], - ["file_system", "create", "file", "test_folder", "http_test_file", "True"], - ], + "commands": ["file_system", "create", "file", "test_folder", "http_test_file", "true"], "username": "admin", "password": "admin", "ip_address": None, From 83b8206ce0254e6bfa7df3b9b635533ea06872a0 Mon Sep 17 00:00:00 2001 From: Archer Bowen Date: Fri, 16 Aug 2024 11:51:38 +0100 Subject: [PATCH 40/46] #2689 Added C2 Sequence diagram to docs and added additional ftp_client request tests. --- docs/_static/c2_sequence.png | Bin 0 -> 55723 bytes .../system/applications/c2_suite.rst | 6 ++ .../Command-&-Control-E2E-Demonstration.ipynb | 2 +- .../red_applications/c2/abstract_c2.py | 4 +- .../red_applications/c2/c2_server.py | 2 +- .../system/services/ftp/ftp_client.py | 7 +- .../system/test_ftp_client_server.py | 63 ++++++++++++++++++ 7 files changed, 78 insertions(+), 6 deletions(-) create mode 100644 docs/_static/c2_sequence.png diff --git a/docs/_static/c2_sequence.png b/docs/_static/c2_sequence.png new file mode 100644 index 0000000000000000000000000000000000000000..9c7ba397b8dddf6d61ebe7f7d6114849705c16d4 GIT binary patch literal 55723 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 Date: Fri, 16 Aug 2024 15:47:41 +0100 Subject: [PATCH 41/46] #2689 Fixed issues with .rst (fixed terminal as well) --- .../system/applications/c2_suite.rst | 46 +++++++++++-------- .../system/services/terminal.rst | 17 +++---- 2 files changed, 35 insertions(+), 28 deletions(-) diff --git a/docs/source/simulation_components/system/applications/c2_suite.rst b/docs/source/simulation_components/system/applications/c2_suite.rst index 1fa05466..034158d7 100644 --- a/docs/source/simulation_components/system/applications/c2_suite.rst +++ b/docs/source/simulation_components/system/applications/c2_suite.rst @@ -18,7 +18,7 @@ These two new classes give red agents a cyber realistic way of leveraging the ca For a more in-depth look at the command and control applications then please refer to the ``C2-E2E-Notebook``. ``C2 Server`` -"""""""""""" +""""""""""""" The C2 Server application is intended to represent the malicious infrastructure already under the control of an adversary. @@ -101,8 +101,8 @@ However, each host implements it's own receive methods. The sequence diagram below clarifies the functionality of both applications: -.. image:: ../_static/c2_sequence.png - :width: 500 +.. image:: ../../../../_static/c2_sequence.png + :width: 1000 :align: center @@ -114,38 +114,45 @@ Examples Python """""" .. code-block:: python - from primaite.simulator.system.applications.red_applications.c2.c2_beacon import C2Beacon - from primaite.simulator.system.applications.red_applications.c2.c2_server import C2Server - from primaite.simulator.system.applications.red_applications.c2.c2_server import C2Command + + from primaite.simulator.network.container import Network from primaite.simulator.network.hardware.nodes.host.computer import Computer - from primaite.simulator.system.services.database.database_service import DatabaseService + from primaite.simulator.network.hardware.nodes.network.switch import Switch from primaite.simulator.system.applications.database_client import DatabaseClient + from primaite.simulator.system.applications.red_applications.ransomware_script import RansomwareScript + from primaite.simulator.system.services.database.database_service import DatabaseService + from primaite.simulator.system.applications.red_applications.c2.c2_server import C2Command, C2Server + from primaite.simulator.system.applications.red_applications.c2.c2_beacon import C2Beacon + # Network Setup + network = Network() + switch = Switch(hostname="switch", start_up_duration=0, num_ports=4) switch.power_on() node_a = Computer(hostname="node_a", ip_address="192.168.0.10", subnet_mask="255.255.255.0", start_up_duration=0) node_a.power_on() - node_a.software_manager.install(software_class=C2Server) network.connect(node_a.network_interface[1], switch.network_interface[1]) node_b = Computer(hostname="node_b", ip_address="192.168.0.11", subnet_mask="255.255.255.0", start_up_duration=0) node_b.power_on() - node_b.software_manager.install(software_class=C2Beacon) - node_b.software_manager.install(software_class=DatabaseClient) + network.connect(node_b.network_interface[1], switch.network_interface[2]) node_c = Computer(hostname="node_c", ip_address="192.168.0.12", subnet_mask="255.255.255.0", start_up_duration=0) node_c.power_on() - node_c.software_manager.install(software_class=DatabaseServer) network.connect(node_c.network_interface[1], switch.network_interface[3]) + node_c.software_manager.install(software_class=DatabaseService) + node_b.software_manager.install(software_class=DatabaseClient) + node_b.software_manager.install(software_class=RansomwareScript) + node_a.software_manager.install(software_class=C2Server) + # C2 Application objects - c2_server_host: computer = simulation_testing_network.get_node_by_hostname("node_a") - c2_beacon_host: computer = simulation_testing_network.get_node_by_hostname("node_b") - + c2_server_host: Computer = network.get_node_by_hostname("node_a") + c2_beacon_host: Computer = network.get_node_by_hostname("node_b") c2_server: C2Server = c2_server_host.software_manager.software["C2Server"] c2_beacon: C2Beacon = c2_beacon_host.software_manager.software["C2Beacon"] @@ -182,7 +189,7 @@ Python "password": "admin", "ip_address": None, } - c2_server.send_command(given_command=C2Command.TERMINAL, command_options=ransomware_config) + c2_server.send_command(given_command=C2Command.TERMINAL, command_options=ransomware_installation_command) ransomware_config = {"server_ip_address": "192.168.0.12"} @@ -197,9 +204,8 @@ Python "password": "admin", "ip_address": None, "target_ip_address": "192.168.0.12", - "target_file_name": "database.db" - "target_folder_name": "database" - "exfiltration_folder_name": + "target_file_name": "database.db", + "target_folder_name": "database", } c2_server.send_command(given_command=C2Command.DATA_EXFILTRATION, command_options=data_exfil_options) @@ -254,7 +260,7 @@ C2 Beacon Configuration .. |SOFTWARE_NAME_BACKTICK| replace:: ``C2Beacon`` ``c2_server_ip_address`` -""""""""""""""""""""""" +"""""""""""""""""""""""" IP address of the ``C2Server`` that the C2 Beacon will use to establish connection. @@ -262,7 +268,7 @@ This must be a valid octet i.e. in the range of ``0.0.0.0`` and ``255.255.255.25 ``Keep Alive Frequency`` -""""""""""""""""""""""" +"""""""""""""""""""""""" How often should the C2 Beacon confirm it's connection in timesteps. diff --git a/docs/source/simulation_components/system/services/terminal.rst b/docs/source/simulation_components/system/services/terminal.rst index 5097f213..f982145d 100644 --- a/docs/source/simulation_components/system/services/terminal.rst +++ b/docs/source/simulation_components/system/services/terminal.rst @@ -5,26 +5,26 @@ .. _Terminal: Terminal -======== +######## The ``Terminal.py`` class provides a generic terminal simulation, by extending the base Service class within PrimAITE. The aim of this is to act as the primary entrypoint for Nodes within the environment. Overview --------- +======== The Terminal service uses Secure Socket (SSH) as the communication method between terminals. They operate on port 22, and are part of the services automatically installed on Nodes when they are instantiated. Key capabilities -================ +"""""""""""""""" - Ensures packets are matched to an existing session - Simulates common Terminal processes/commands. - Leverages the Service base class for install/uninstall, status tracking etc. Usage -===== +""""" - Pre-Installs on any `Node` (component with the exception of `Switches`). - Terminal Clients connect, execute commands and disconnect from remote nodes. @@ -32,7 +32,7 @@ Usage - Service runs on SSH port 22 by default. Implementation -============== +"""""""""""""" - Manages remote connections in a dictionary by session ID. - Processes commands, forwarding to the ``RequestManager`` or ``SessionManager`` where appropriate. @@ -67,7 +67,7 @@ Python terminal: Terminal = client.software_manager.software.get("Terminal") Creating Remote Terminal Connection -""""""""""""""""""""""""""" +""""""""""""""""""""""""""""""""""" .. code-block:: python @@ -93,7 +93,7 @@ Creating Remote Terminal Connection Executing a basic application install command -""""""""""""""""""""""""""""""""" +""""""""""""""""""""""""""""""""""""""""""""" .. code-block:: python @@ -121,7 +121,7 @@ Executing a basic application install command Creating a folder on a remote node -"""""""""""""""""""""""""""""""" +"""""""""""""""""""""""""""""""""" .. code-block:: python @@ -148,6 +148,7 @@ Creating a folder on a remote node Disconnect from Remote Node +""""""""""""""""""""""""""" .. code-block:: python From f595f44ce97d7f6aa2648ab274c6bdf6a7ed172e Mon Sep 17 00:00:00 2001 From: Archer Bowen Date: Mon, 19 Aug 2024 13:08:31 +0100 Subject: [PATCH 42/46] #2689 Implemented fixes to _check_connection following PR --- .../red_applications/c2/abstract_c2.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py b/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py index 82e740c5..3d096209 100644 --- a/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py +++ b/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py @@ -463,26 +463,26 @@ class AbstractC2(Application, identifier="AbstractC2"): :return: A tuple containing a boolean True/False and a corresponding Request Response :rtype: tuple[bool, RequestResponse] """ - if self._can_perform_network_action == False: + if not self._can_perform_network_action: self.sys_log.warning(f"{self.name}: Unable to make leverage networking resources. Rejecting Command.") - return [ + return ( False, RequestResponse( status="failure", data={"Reason": "Unable to access networking resources. Unable to send command."} ), - ] + ) if self.c2_remote_connection is False: self.sys_log.warning(f"{self.name}: C2 Application has yet to establish connection. Rejecting command.") - return [ + return ( False, RequestResponse( status="failure", data={"Reason": "C2 Application has yet to establish connection. Unable to send command."}, ), - ] + ) else: - return [ + return ( True, RequestResponse(status="success", data={"Reason": "C2 Application is able to send connections."}), - ] + ) From 2413a2f6a8d3f12815cd142f46eff3b5e2d02999 Mon Sep 17 00:00:00 2001 From: Archer Bowen Date: Mon, 19 Aug 2024 13:10:35 +0100 Subject: [PATCH 43/46] #2689 Fixing oversight on method call --- .../system/applications/red_applications/c2/abstract_c2.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py b/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py index 3d096209..354976b7 100644 --- a/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py +++ b/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py @@ -463,7 +463,7 @@ class AbstractC2(Application, identifier="AbstractC2"): :return: A tuple containing a boolean True/False and a corresponding Request Response :rtype: tuple[bool, RequestResponse] """ - if not self._can_perform_network_action: + if not self._can_perform_network_action(): self.sys_log.warning(f"{self.name}: Unable to make leverage networking resources. Rejecting Command.") return ( False, From 7b1584ccb7a33fc7854d223805a81c786b2935a5 Mon Sep 17 00:00:00 2001 From: Archer Bowen Date: Mon, 19 Aug 2024 15:24:24 +0100 Subject: [PATCH 44/46] #2689 Updated following PR --- .../applications/red_applications/c2/abstract_c2.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py b/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py index 354976b7..7316dd63 100644 --- a/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py +++ b/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py @@ -472,7 +472,7 @@ class AbstractC2(Application, identifier="AbstractC2"): ), ) - if self.c2_remote_connection is False: + if self.c2_remote_connection is None: self.sys_log.warning(f"{self.name}: C2 Application has yet to establish connection. Rejecting command.") return ( False, @@ -481,8 +481,7 @@ class AbstractC2(Application, identifier="AbstractC2"): data={"Reason": "C2 Application has yet to establish connection. Unable to send command."}, ), ) - else: - return ( - True, - RequestResponse(status="success", data={"Reason": "C2 Application is able to send connections."}), - ) + return ( + True, + RequestResponse(status="success", data={"Reason": "C2 Application is able to send connections."}), + ) From b8767da61ec9d9d11e2528d817dbeedeb1d63be0 Mon Sep 17 00:00:00 2001 From: Archer Bowen Date: Tue, 20 Aug 2024 10:51:29 +0100 Subject: [PATCH 45/46] #2689 Fixed merging errors with actions.py --- src/primaite/game/agent/actions.py | 116 +++++++++++++++++++---------- 1 file changed, 76 insertions(+), 40 deletions(-) diff --git a/src/primaite/game/agent/actions.py b/src/primaite/game/agent/actions.py index 713c4eb2..42ba25b4 100644 --- a/src/primaite/game/agent/actions.py +++ b/src/primaite/game/agent/actions.py @@ -1096,10 +1096,6 @@ class ConfigureC2BeaconAction(AbstractAction): return cls.model_fields[info.field_name].default return v - -class NodeAccountsChangePasswordAction(AbstractAction): - """Action which changes the password for a user.""" - def __init__(self, manager: "ActionManager", **kwargs) -> None: super().__init__(manager=manager) @@ -1120,8 +1116,11 @@ class NodeAccountsChangePasswordAction(AbstractAction): return ["network", "node", node_name, "application", "C2Beacon", "configure", config.__dict__] -class RansomwareConfigureC2ServerAction(AbstractAction): - """Action which sends a command from the C2 Server to the C2 Beacon which configures a local RansomwareScript.""" +class NodeAccountsChangePasswordAction(AbstractAction): + """Action which changes the password for a user.""" + + def __init__(self, manager: "ActionManager", **kwargs) -> None: + super().__init__(manager=manager) def form_request(self, node_id: str, username: str, current_password: str, new_password: str) -> RequestFormat: """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" @@ -1145,19 +1144,6 @@ class NodeSessionsRemoteLoginAction(AbstractAction): def __init__(self, manager: "ActionManager", **kwargs) -> None: super().__init__(manager=manager) - def form_request(self, node_id: int, config: Dict) -> RequestFormat: - """Return the action formatted as a request that can be ingested by the simulation.""" - node_name = self.manager.get_node_name_by_idx(node_id) - if node_name is None: - return ["do_nothing"] - # Using the ransomware scripts model to validate. - ConfigureRansomwareScriptAction._Opts.model_validate(config) # check that options adhere to schema - return ["network", "node", node_name, "application", "C2Server", "ransomware_configure", config] - - -class RansomwareLaunchC2ServerAction(AbstractAction): - """Action which causes the C2 Server to send a command to the C2 Beacon to launch the RansomwareScript.""" - def form_request(self, node_id: str, username: str, password: str, remote_ip: str) -> RequestFormat: """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" node_name = self.manager.get_node_name_by_idx(node_id) @@ -1177,6 +1163,43 @@ class RansomwareLaunchC2ServerAction(AbstractAction): class NodeSessionsRemoteLogoutAction(AbstractAction): """Action which performs a remote session logout.""" + def __init__(self, manager: "ActionManager", **kwargs) -> None: + super().__init__(manager=manager) + + def form_request(self, node_id: int, remote_ip: str, command: RequestFormat) -> RequestFormat: + """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" + node_name = self.manager.get_node_name_by_idx(node_id) + return [ + "network", + "node", + node_name, + "service", + "Terminal", + "send_remote_command", + remote_ip, + {"command": command}, + ] + + +class RansomwareConfigureC2ServerAction(AbstractAction): + """Action which sends a command from the C2 Server to the C2 Beacon which configures a local RansomwareScript.""" + + def __init__(self, manager: "ActionManager", **kwargs) -> None: + super().__init__(manager=manager) + + def form_request(self, node_id: int, config: Dict) -> RequestFormat: + """Return the action formatted as a request that can be ingested by the simulation.""" + node_name = self.manager.get_node_name_by_idx(node_id) + if node_name is None: + return ["do_nothing"] + # Using the ransomware scripts model to validate. + ConfigureRansomwareScriptAction._Opts.model_validate(config) # check that options adhere to schema + return ["network", "node", node_name, "application", "C2Server", "ransomware_configure", config] + + +class RansomwareLaunchC2ServerAction(AbstractAction): + """Action which causes the C2 Server to send a command to the C2 Beacon to launch the RansomwareScript.""" + def __init__(self, manager: "ActionManager", **kwargs) -> None: super().__init__(manager=manager) @@ -1202,15 +1225,6 @@ class ExfiltrationC2ServerAction(AbstractAction): target_folder_name: str exfiltration_folder_name: Optional[str] - def form_request(self, node_id: str, remote_ip: str) -> RequestFormat: - """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" - node_name = self.manager.get_node_name_by_idx(node_id) - return ["network", "node", node_name, "service", "Terminal", "remote_logoff", remote_ip] - - -class NodeSendRemoteCommandAction(AbstractAction): - """Action which sends a terminal command to a remote node via SSH.""" - def __init__(self, manager: "ActionManager", **kwargs) -> None: super().__init__(manager=manager) @@ -1240,6 +1254,27 @@ class NodeSendRemoteCommandAction(AbstractAction): return ["network", "node", node_name, "application", "C2Server", "exfiltrate", command_model] +class NodeSendRemoteCommandAction(AbstractAction): + """Action which sends a terminal command to a remote node via SSH.""" + + def __init__(self, manager: "ActionManager", **kwargs) -> None: + super().__init__(manager=manager) + + def form_request(self, node_id: int, remote_ip: str, command: RequestFormat) -> RequestFormat: + """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" + node_name = self.manager.get_node_name_by_idx(node_id) + return [ + "network", + "node", + node_name, + "service", + "Terminal", + "send_remote_command", + remote_ip, + {"command": command}, + ] + + class TerminalC2ServerAction(AbstractAction): """Action which causes the C2 Server to send a command to the C2 Beacon to execute the terminal command passed.""" @@ -1270,19 +1305,20 @@ class TerminalC2ServerAction(AbstractAction): TerminalC2ServerAction._Opts.model_validate(command_model) return ["network", "node", node_name, "application", "C2Server", "terminal_command", command_model] - def form_request(self, node_id: int, remote_ip: str, command: RequestFormat) -> RequestFormat: - """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" + +class RansomwareLaunchC2ServerAction(AbstractAction): + """Action which causes the C2 Server to send a command to the C2 Beacon to launch the RansomwareScript.""" + + def __init__(self, manager: "ActionManager", **kwargs) -> None: + super().__init__(manager=manager) + + def form_request(self, node_id: int) -> RequestFormat: + """Return the action formatted as a request that can be ingested by the simulation.""" node_name = self.manager.get_node_name_by_idx(node_id) - return [ - "network", - "node", - node_name, - "service", - "Terminal", - "send_remote_command", - remote_ip, - {"command": command}, - ] + if node_name is None: + return ["do_nothing"] + # This action currently doesn't require any further configuration options. + return ["network", "node", node_name, "application", "C2Server", "ransomware_launch"] class ActionManager: From c9d62d512c174ef4edca44832ef7b0c60bd2e8d3 Mon Sep 17 00:00:00 2001 From: Archer Bowen Date: Tue, 20 Aug 2024 11:15:04 +0100 Subject: [PATCH 46/46] #2689 fixed mismerge --- src/primaite/game/agent/actions.py | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/src/primaite/game/agent/actions.py b/src/primaite/game/agent/actions.py index 42ba25b4..2e6189c0 100644 --- a/src/primaite/game/agent/actions.py +++ b/src/primaite/game/agent/actions.py @@ -1166,19 +1166,10 @@ class NodeSessionsRemoteLogoutAction(AbstractAction): def __init__(self, manager: "ActionManager", **kwargs) -> None: super().__init__(manager=manager) - def form_request(self, node_id: int, remote_ip: str, command: RequestFormat) -> RequestFormat: + def form_request(self, node_id: str, remote_ip: str) -> RequestFormat: """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" node_name = self.manager.get_node_name_by_idx(node_id) - return [ - "network", - "node", - node_name, - "service", - "Terminal", - "send_remote_command", - remote_ip, - {"command": command}, - ] + return ["network", "node", node_name, "service", "Terminal", "remote_logoff", remote_ip] class RansomwareConfigureC2ServerAction(AbstractAction):