#2689 Updated documentation and moved _craft_packet into abstract C2
This commit is contained in:
@@ -34,7 +34,7 @@ Currently, the C2 Server offers three commands:
|
|||||||
+---------------------+---------------------------------------------------------------------------+
|
+---------------------+---------------------------------------------------------------------------+
|
||||||
|RANSOMWARE_LAUNCH | Launches the installed ransomware script. |
|
|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:
|
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
|
|Configuration Option | Option Meaning |
|
||||||
- C2 Communication Protocol
|
+=====================+===========================================================================+
|
||||||
|
|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
|
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``.
|
- 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
|
Examples
|
||||||
========
|
========
|
||||||
@@ -120,8 +129,8 @@ Python
|
|||||||
|
|
||||||
# C2 Application objects
|
# C2 Application objects
|
||||||
|
|
||||||
c2_server_host = simulation_testing_network.get_node_by_hostname("node_a")
|
c2_server_host: computer = simulation_testing_network.get_node_by_hostname("node_a")
|
||||||
c2_beacon_host = simulation_testing_network.get_node_by_hostname("node_b")
|
c2_beacon_host: computer = simulation_testing_network.get_node_by_hostname("node_b")
|
||||||
|
|
||||||
|
|
||||||
c2_server: C2Server = c2_server_host.software_manager.software["C2Server"]
|
c2_server: C2Server = c2_server_host.software_manager.software["C2Server"]
|
||||||
@@ -136,10 +145,125 @@ Python
|
|||||||
# Establishing connection
|
# Establishing connection
|
||||||
c2_beacon.establish()
|
c2_beacon.establish()
|
||||||
|
|
||||||
# Example command: Configuring Ransomware
|
# Example command: Creating a file
|
||||||
|
|
||||||
ransomware_config = {"server_ip_address": "1.1.1.1"}
|
file_create_command = {
|
||||||
c2_server._send_command(given_command=C2Command.RANSOMWARE_CONFIGURE, command_options=ransomware_config)
|
"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._
|
||||||
|
|||||||
@@ -633,7 +633,7 @@
|
|||||||
"ransomware_install_command = {\"commands\":[[\"software_manager\", \"application\", \"install\", \"RansomwareScript\"]],\n",
|
"ransomware_install_command = {\"commands\":[[\"software_manager\", \"application\", \"install\", \"RansomwareScript\"]],\n",
|
||||||
" \"username\": \"admin\",\n",
|
" \"username\": \"admin\",\n",
|
||||||
" \"password\": \"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": [
|
"source": [
|
||||||
"# Configuring the RansomwareScript\n",
|
"# Configuring the RansomwareScript\n",
|
||||||
"ransomware_config = {\"server_ip_address\": \"192.168.1.14\", \"payload\": \"ENCRYPT\"}\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": [
|
"source": [
|
||||||
"# Waiting for the ransomware to finish installing and then launching the RansomwareScript.\n",
|
"# Waiting for the ransomware to finish installing and then launching the RansomwareScript.\n",
|
||||||
"blue_env.step(0)\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",
|
" \"password\": \"admin\"}\n",
|
||||||
"\n",
|
"\n",
|
||||||
"c2_server: C2Server = client_1.software_manager.software[\"C2Server\"]\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",
|
" \"password\": \"admin\"}\n",
|
||||||
"\n",
|
"\n",
|
||||||
"c2_server: C2Server = client_1.software_manager.software[\"C2Server\"]\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",
|
"blue_env.step(0)\n",
|
||||||
"\n",
|
"\n",
|
||||||
"# Attempting to install and execute the ransomware script\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.TERMINAL, command_options=ransomware_install_command)\n",
|
||||||
"c2_server._send_command(C2Command.RANSOMWARE_LAUNCH, command_options={})"
|
"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 pydantic import BaseModel, Field, validate_call
|
||||||
|
|
||||||
|
from primaite.interface.request import RequestResponse
|
||||||
from primaite.simulator.network.protocols.masquerade import C2Packet
|
from primaite.simulator.network.protocols.masquerade import C2Packet
|
||||||
from primaite.simulator.network.transmission.network_layer import IPProtocol
|
from primaite.simulator.network.transmission.network_layer import IPProtocol
|
||||||
from primaite.simulator.network.transmission.transport_layer import Port
|
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.
|
as well as providing the abstract methods for sending, receiving and parsing commands.
|
||||||
|
|
||||||
Defaults to masquerading as HTTP (Port 80) via TCP.
|
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
|
c2_connection_active: bool = False
|
||||||
@@ -79,11 +82,46 @@ class AbstractC2(Application, identifier="AbstractC2"):
|
|||||||
masquerade_port: Port = Field(default=Port.HTTP)
|
masquerade_port: Port = Field(default=Port.HTTP)
|
||||||
"""The currently chosen port that the C2 traffic is masquerading as. Defaults at 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)
|
def _craft_packet(
|
||||||
# and then passes the config attributes to the c2 server via keep alives
|
self, c2_payload: C2Payload, c2_command: Optional[C2Command] = None, command_options: Optional[Dict] = {}
|
||||||
# The c2 server parses the C2 configurations from keep alive traffic and sets the c2_config accordingly.
|
) -> 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()
|
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:
|
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.
|
:returns: Returns True if a send alive was successfully sent. False otherwise.
|
||||||
:rtype bool:
|
:rtype bool:
|
||||||
"""
|
"""
|
||||||
# Checking that the c2 application is capable of performing both actions and has an enabled NIC
|
# Checking that the c2 application is capable of connecting to remote.
|
||||||
# (Using NOT to improve code readability)
|
# Purely a safety guard clause.
|
||||||
if self.c2_remote_connection is None:
|
if not (connection_status := self._check_connection()[0]):
|
||||||
self.sys_log.error(
|
self.sys_log.warning(
|
||||||
f"{self.name}: Unable to establish connection as the C2 Server's IP Address has not been configured."
|
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
|
return False
|
||||||
|
|
||||||
# We also Pass masquerade proto`col/port so that the c2 server can reply on the correct protocol/port.
|
# Passing our current C2 configuration in remain in sync.
|
||||||
# (This also lays the foundations for switching masquerade port/protocols mid episode.)
|
keep_alive_packet = self._craft_packet(c2_payload=C2Payload.KEEP_ALIVE)
|
||||||
keep_alive_packet = C2Packet(
|
|
||||||
masquerade_protocol=self.c2_config.masquerade_protocol,
|
# Sending the keep alive via the .send() method (as with all other applications.)
|
||||||
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.
|
|
||||||
if self.send(
|
if self.send(
|
||||||
payload=keep_alive_packet,
|
payload=keep_alive_packet,
|
||||||
dest_ip_address=self.c2_remote_connection,
|
dest_ip_address=self.c2_remote_connection,
|
||||||
@@ -215,6 +244,8 @@ class AbstractC2(Application, identifier="AbstractC2"):
|
|||||||
ip_protocol=self.c2_config.masquerade_protocol,
|
ip_protocol=self.c2_config.masquerade_protocol,
|
||||||
session_id=session_id,
|
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.keep_alive_sent = True
|
||||||
self.sys_log.info(f"{self.name}: Keep Alive sent to {self.c2_remote_connection}")
|
self.sys_log.info(f"{self.name}: Keep Alive sent to {self.c2_remote_connection}")
|
||||||
self.sys_log.debug(
|
self.sys_log.debug(
|
||||||
@@ -266,7 +297,7 @@ class AbstractC2(Application, identifier="AbstractC2"):
|
|||||||
f"Keep Alive Frequency: {self.c2_config.keep_alive_frequency}"
|
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:
|
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.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)
|
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
|
self.c2_config.masquerade_protocol = IPProtocol.TCP
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def _confirm_connection(self, timestep: int) -> bool:
|
def _confirm_remote_connection(self, timestep: int) -> bool:
|
||||||
"""Abstract method - Checks the suitability of the current C2 Server/Beacon connection."""
|
"""
|
||||||
|
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:
|
def apply_timestep(self, timestep: int) -> None:
|
||||||
"""Apply a timestep to the c2_server & c2 beacon.
|
"""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
|
and self.health_state_actual is SoftwareHealthState.GOOD
|
||||||
):
|
):
|
||||||
self.keep_alive_inactivity += 1
|
self.keep_alive_inactivity += 1
|
||||||
self._confirm_connection(timestep)
|
self._confirm_remote_connection(timestep)
|
||||||
return
|
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.
|
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.
|
||||||
|
Please refer to the _configure method for further information.
|
||||||
|
|
||||||
Extends the Abstract C2 application to include the following:
|
Extends the Abstract C2 application to include the following:
|
||||||
|
|
||||||
1. Receiving commands from the C2 Server (Command input)
|
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 (dependant on the command given)
|
||||||
3. Sending the RequestResponse back to the C2 Server (Command output)
|
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
|
keep_alive_attempted: bool = False
|
||||||
@@ -141,9 +144,18 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"):
|
|||||||
masquerade_port: Enum = Port.HTTP,
|
masquerade_port: Enum = Port.HTTP,
|
||||||
) -> bool:
|
) -> 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.
|
:param c2_server_ip_address: The IP Address of the C2 Server. Used to establish connection.
|
||||||
:type c2_server_ip_address: IPv4Address
|
: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.
|
:param session_id: The current session established with the C2 Server.
|
||||||
:type session_id: Str
|
:type session_id: Str
|
||||||
"""
|
"""
|
||||||
output_packet = C2Packet(
|
output_packet = self._craft_packet(c2_payload=C2Payload.OUTPUT, command_options=command_output)
|
||||||
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(
|
if self.send(
|
||||||
payload=output_packet,
|
payload=output_packet,
|
||||||
dest_ip_address=self.c2_remote_connection,
|
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.
|
# If this method returns true then we have sent successfully sent a keep alive.
|
||||||
return self._send_keep_alive(session_id)
|
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.
|
"""Checks the suitability of the current 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.
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ class C2Server(AbstractC2, identifier="C2Server"):
|
|||||||
|
|
||||||
1. Sending commands to the C2 Beacon. (Command input)
|
1. Sending commands to the C2 Beacon. (Command input)
|
||||||
2. Parsing terminal RequestResponses back to the Agent.
|
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
|
current_command_output: RequestResponse = None
|
||||||
@@ -51,7 +53,7 @@ class C2Server(AbstractC2, identifier="C2Server"):
|
|||||||
"server_ip_address": request[-1].get("server_ip_address"),
|
"server_ip_address": request[-1].get("server_ip_address"),
|
||||||
"payload": request[-1].get("payload"),
|
"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:
|
def _launch_ransomware_action(request: RequestFormat, context: Dict) -> RequestResponse:
|
||||||
"""Agent Action - Sends a RANSOMWARE_LAUNCH C2Command to the C2 Beacon with the given parameters.
|
"""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.
|
:return: RequestResponse object with a success code reflecting whether the ransomware was launched.
|
||||||
:rtype: RequestResponse
|
: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:
|
def _remote_terminal_action(request: RequestFormat, context: Dict) -> RequestResponse:
|
||||||
"""Agent Action - Sends a TERMINAL C2Command to the C2 Beacon with the given parameters.
|
"""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
|
:rtype: RequestResponse
|
||||||
"""
|
"""
|
||||||
command_payload = request[-1]
|
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(
|
rm.add_request(
|
||||||
name="ransomware_configure",
|
name="ransomware_configure",
|
||||||
@@ -159,7 +161,7 @@ class C2Server(AbstractC2, identifier="C2Server"):
|
|||||||
return self._send_keep_alive(session_id)
|
return self._send_keep_alive(session_id)
|
||||||
|
|
||||||
@validate_call
|
@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.
|
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."}
|
status="failure", data={"Reason": "Received unexpected C2Command. Unable to send command."}
|
||||||
)
|
)
|
||||||
|
|
||||||
if self._can_perform_network_action == False:
|
# Lambda method used to return a failure RequestResponse if we're unable to confirm a connection.
|
||||||
self.sys_log.warning(f"{self.name}: Unable to make leverage networking resources. Rejecting Command.")
|
# If _check_connection returns false then connection_status will return reason (A 'failure' Request Response)
|
||||||
return RequestResponse(
|
if connection_status := (lambda return_bool, reason: reason if return_bool is False else None)(
|
||||||
status="failure", data={"Reason": "Unable to access networking resources. Unable to send command."}
|
*self._check_connection()
|
||||||
)
|
):
|
||||||
|
return connection_status
|
||||||
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."}
|
|
||||||
)
|
|
||||||
|
|
||||||
self.sys_log.info(f"{self.name}: Attempting to send command {given_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)
|
command_packet = self._craft_packet(
|
||||||
|
c2_payload=C2Payload.INPUT, c2_command=given_command, command_options=command_options
|
||||||
|
)
|
||||||
|
|
||||||
if self.send(
|
if self.send(
|
||||||
payload=command_packet,
|
payload=command_packet,
|
||||||
@@ -231,30 +224,6 @@ class C2Server(AbstractC2, identifier="C2Server"):
|
|||||||
)
|
)
|
||||||
return self.current_command_output
|
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):
|
def show(self, markdown: bool = False):
|
||||||
"""
|
"""
|
||||||
Prints a table of the current C2 attributes on a C2 Server.
|
Prints a table of the current C2 attributes on a C2 Server.
|
||||||
@@ -292,7 +261,8 @@ class C2Server(AbstractC2, identifier="C2Server"):
|
|||||||
)
|
)
|
||||||
print(table)
|
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:
|
def _handle_command_input(self, payload: C2Packet) -> None:
|
||||||
"""Defining this method (Abstract method inherited from abstract C2) in order to instantiate the class.
|
"""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}")
|
self.sys_log.warning(f"{self.name}: C2 Server received an unexpected INPUT payload: {payload}")
|
||||||
pass
|
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.
|
"""Checks the suitability of the current C2 Beacon connection.
|
||||||
|
|
||||||
If a C2 Server has not received a keep alive within the current set
|
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
|
keep alive frequency (self._keep_alive_frequency) then the C2 beacons
|
||||||
connection is considered dead and any commands will be rejected.
|
connection is considered dead and any commands will be rejected.
|
||||||
|
|
||||||
|
This method is used to
|
||||||
|
|
||||||
:param timestep: The current timestep of the simulation.
|
:param timestep: The current timestep of the simulation.
|
||||||
:type timestep: Int
|
:type timestep: Int
|
||||||
:return: Returns False if the C2 beacon is considered dead. Otherwise True.
|
:return: Returns False if the C2 beacon is considered dead. Otherwise True.
|
||||||
|
|||||||
@@ -263,7 +263,7 @@ def test_c2_suite_terminal_command_file_creation(basic_network):
|
|||||||
"ip_address": None,
|
"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 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.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
|
# node_c's IP is 192.168.255.3
|
||||||
file_create_command.update({"ip_address": "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 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.remote_terminal_session is not None
|
||||||
@@ -369,7 +369,7 @@ def test_c2_suite_acl_bypass(basic_network):
|
|||||||
"password": "admin",
|
"password": "admin",
|
||||||
"ip_address": None,
|
"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 (
|
assert (
|
||||||
computer_b.software_manager.file_system.access_file(folder_name="test_folder", file_name="ftp_test_file")
|
computer_b.software_manager.file_system.access_file(folder_name="test_folder", file_name="ftp_test_file")
|
||||||
== True
|
== True
|
||||||
@@ -440,7 +440,7 @@ def test_c2_suite_acl_bypass(basic_network):
|
|||||||
"password": "admin",
|
"password": "admin",
|
||||||
"ip_address": None,
|
"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 (
|
assert (
|
||||||
computer_b.software_manager.file_system.access_file(folder_name="test_folder", file_name="http_test_file")
|
computer_b.software_manager.file_system.access_file(folder_name="test_folder", file_name="http_test_file")
|
||||||
== True
|
== True
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ def test_c2_handle_beacon_disconnect(basic_c2_network):
|
|||||||
"ip_address": None,
|
"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"
|
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
|
assert c2_server.c2_connection_active is False
|
||||||
|
|
||||||
|
|
||||||
# TODO: Finalise and complete these tests.
|
|
||||||
|
|
||||||
|
|
||||||
def test_c2_handle_switching_port(basic_c2_network):
|
def test_c2_handle_switching_port(basic_c2_network):
|
||||||
"""Tests that the C2 suite will be able handle switching destination/src port."""
|
"""Tests that the C2 suite will be able handle switching destination/src port."""
|
||||||
network: Network = basic_c2_network
|
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_beacon.keep_alive_inactivity is 0
|
||||||
assert c2_server.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
|
||||||
|
|||||||
Reference in New Issue
Block a user