#2689 Updated documentation and moved _craft_packet into abstract C2
This commit is contained in:
@@ -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={})"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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."}),
|
||||
]
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user