Skip to content

Commit a142d7f

Browse files
committed
Merge branch '3411-cpu-utilization-component' into develop
Issue #3411 PR #3563
2 parents 4698407 + 0a6153a commit a142d7f

File tree

5 files changed

+158
-17
lines changed

5 files changed

+158
-17
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
CRYPTOJACKER_PAYLOAD_TAG = "cryptojacker-payload"
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,153 @@
1+
import hashlib
2+
import logging
3+
import threading
4+
import time
5+
from random import randbytes # noqa: DUO102 (this isn't for cryptographic use)
16
from typing import Optional
27

8+
import psutil
9+
10+
from common import OperatingSystem
11+
from common.agent_events import CPUConsumptionEvent
312
from common.event_queue import IAgentEventPublisher
4-
from common.types import AgentID, PercentLimited
13+
from common.tags import RESOURCE_HIJACKING_T1496_TAG
14+
from common.types import AgentID, NonNegativeFloat, PercentLimited
15+
from common.utils.environment import get_os
16+
from infection_monkey.utils.threading import create_daemon_thread
17+
18+
from .consts import CRYPTOJACKER_PAYLOAD_TAG
19+
20+
logger = logging.getLogger(__name__)
21+
22+
23+
ACCURACY_THRESHOLD = 0.02
24+
INITIAL_SLEEP_SECONDS = 0.001
25+
AVERAGE_BLOCK_SIZE_BYTES = int(
26+
3.25 * 1024 * 1024
27+
) # 3.25 MB - Source: https://trustmachines.co/blog/bitcoin-ordinals-reignited-block-size-debate/
28+
OPERATION_COUNT = 250
29+
OPERATION_COUNT_MODIFIER_START = int(OPERATION_COUNT / 10)
30+
OPERATION_COUNT_MODIFIER_FACTOR = 1.5
31+
MINIMUM_SLEEP = 0.000001
32+
MINIMUM_CPU_UTILIZATION_TARGET = 1 # 1%
533

634

735
class CPUUtilizer:
836
def __init__(
937
self,
10-
cpu_utilization: PercentLimited,
38+
target_cpu_utilization_percent: PercentLimited,
1139
agent_id: AgentID,
1240
agent_event_publisher: IAgentEventPublisher,
1341
):
14-
self._cpu_utilization = cpu_utilization
42+
# Target CPU utilization can never be zero, otherwise divide by zero errors could occur or
43+
# sleeps could become so large that this process will hang indefinitely.
44+
self._target_cpu_utilization = max(
45+
target_cpu_utilization_percent, MINIMUM_CPU_UTILIZATION_TARGET
46+
)
1547
self._agent_id = agent_id
1648
self._agent_event_publisher = agent_event_publisher
1749

50+
self._should_stop_cpu_utilization = threading.Event()
51+
self._cpu_utilizer_thread = create_daemon_thread(
52+
target=self._utilize_cpu,
53+
name="Cryptojacker.CPUUtilizer",
54+
)
55+
1856
def start(self):
19-
pass
57+
logger.info("Starting CPUUtilizer")
58+
59+
self._cpu_utilizer_thread.start()
60+
61+
def _utilize_cpu(self):
62+
operation_count_modifier = OPERATION_COUNT_MODIFIER_START
63+
sleep_seconds = INITIAL_SLEEP_SECONDS if self._target_cpu_utilization < 100 else 0
64+
block = randbytes(AVERAGE_BLOCK_SIZE_BYTES)
65+
nonce = 0
66+
67+
process = psutil.Process()
68+
# This is a throw-away call. The first call to cpu_percent() always returns 0, even
69+
# after generating some hashes.
70+
# https://psutil.readthedocs.io/en/latest/#psutil.cpu_percent
71+
process.cpu_percent()
72+
73+
os = get_os()
74+
if os == OperatingSystem.LINUX:
75+
get_current_process_cpu_number = process.cpu_num
76+
elif os == OperatingSystem.WINDOWS:
77+
get_current_process_cpu_number = self._get_windows_process_cpu_number
78+
79+
while not self._should_stop_cpu_utilization.is_set():
80+
# The operation_count_modifier decreases the number of hashes per iteration.
81+
# The modifier, itself, decreases by a factor of 1.5 each iteration, until
82+
# it reaches 1. This allows a higher sample rate of the CPU utilization
83+
# early on to help the sleep time to converge quicker.
84+
for _ in range(0, int(OPERATION_COUNT / operation_count_modifier)):
85+
digest = hashlib.sha256()
86+
digest.update(nonce.to_bytes(8))
87+
digest.update(block)
88+
nonce += 1
89+
90+
time.sleep(sleep_seconds)
91+
92+
measured_cpu_utilization = process.cpu_percent()
93+
process_cpu_number = get_current_process_cpu_number()
94+
95+
self._publish_cpu_consumption_event(measured_cpu_utilization, process_cpu_number)
96+
97+
cpu_utilization_percent_error = self._calculate_percent_error(
98+
measured=measured_cpu_utilization
99+
)
100+
sleep_seconds = CPUUtilizer._calculate_new_sleep(
101+
sleep_seconds, cpu_utilization_percent_error
102+
)
103+
104+
operation_count_modifier = max(
105+
int(operation_count_modifier / OPERATION_COUNT_MODIFIER_FACTOR), 1
106+
)
107+
108+
def _publish_cpu_consumption_event(
109+
self, measured_cpu_utilization: NonNegativeFloat, process_cpu_number: int
110+
):
111+
cpu_consumption_event = CPUConsumptionEvent(
112+
source=self._agent_id,
113+
utilization=measured_cpu_utilization,
114+
cpu_number=process_cpu_number,
115+
tags=frozenset({CRYPTOJACKER_PAYLOAD_TAG, RESOURCE_HIJACKING_T1496_TAG}),
116+
)
117+
self._agent_event_publisher.publish(cpu_consumption_event)
118+
119+
def _get_windows_process_cpu_number(self) -> int:
120+
from ctypes import windll
121+
from ctypes.wintypes import DWORD
122+
123+
get_current_processor_number = windll.kernel32.GetCurrentProcessorNumber
124+
get_current_processor_number.argtypes = []
125+
get_current_processor_number.restype = DWORD
126+
127+
return get_current_processor_number()
128+
129+
def _calculate_percent_error(self, measured: float) -> float:
130+
# `self._target_cpu_utilization` can never be 0 because we prevent this in __init__()
131+
return (measured - self._target_cpu_utilization) / self._target_cpu_utilization
132+
133+
@staticmethod
134+
def _calculate_new_sleep(current_sleep: float, percent_error: float):
135+
if abs(percent_error) < ACCURACY_THRESHOLD:
136+
return current_sleep
137+
138+
# Since our multiplication is based on sleep_seconds, don't ever let sleep_seconds == 0,
139+
# otherwise it will never equal anything else. CAVEAT: If the target utilization is 100%,
140+
# current_sleep will be initialized to 0.
141+
return current_sleep * max((1 + percent_error), MINIMUM_SLEEP)
20142

21143
def stop(self, timeout: Optional[float] = None):
22-
pass
144+
logger.info("Stopping CPUUtilizer")
145+
146+
self._should_stop_cpu_utilization.set()
147+
148+
self._cpu_utilizer_thread.join(timeout)
149+
if self._cpu_utilizer_thread.is_alive():
150+
logger.warning(
151+
"Timed out while waiting for CPU utilization thread to stop, "
152+
"it will be stopped forcefully when the parent process terminates"
153+
)

monkey/agent_plugins/payloads/cryptojacker/src/cryptojacker.py

+16-9
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@
1111

1212
logger = logging.getLogger(__name__)
1313

14-
CRYPTOJACKER_PAYLOAD_TAG = "cryptojacker-payload"
15-
1614
COMPONENT_STOP_TIMEOUT = 30 # seconds
1715
CHECK_DURATION_TIMER_INTERVAL = 5 # seconds
1816

@@ -31,22 +29,31 @@ def __init__(
3129
self._bitcoin_mining_network_traffic_simulator = bitcoin_mining_network_traffic_simulator
3230

3331
def run(self, interrupt: Event):
34-
logger.info("Running cryptojacker payload")
32+
self._start()
3533

3634
timer = EggTimer()
3735
timer.set(self._options.duration)
36+
while not timer.is_expired() and not interrupt.is_set():
37+
interrupt.wait(CHECK_DURATION_TIMER_INTERVAL)
38+
39+
self._stop()
40+
41+
def _start(self):
42+
logger.info("Starting the cryptojacker payload")
43+
if self._options.cpu_utilization > 0:
44+
self._cpu_utilizer.start()
3845

39-
self._cpu_utilizer.start()
4046
self._memory_utilizer.start()
47+
4148
if self._options.simulate_bitcoin_mining_network_traffic:
4249
self._bitcoin_mining_network_traffic_simulator.start()
4350

44-
while not timer.is_expired() and not interrupt.is_set():
45-
interrupt.wait(CHECK_DURATION_TIMER_INTERVAL)
51+
def _stop(self):
52+
logger.info("Stopping the cryptojacker payload")
53+
if self._options.cpu_utilization > 0:
54+
self._cpu_utilizer.stop(timeout=COMPONENT_STOP_TIMEOUT)
4655

47-
logger.info("Stopping cryptojacker payload")
48-
49-
self._cpu_utilizer.stop(timeout=COMPONENT_STOP_TIMEOUT)
5056
self._memory_utilizer.stop(timeout=COMPONENT_STOP_TIMEOUT)
57+
5158
if self._options.simulate_bitcoin_mining_network_traffic:
5259
self._bitcoin_mining_network_traffic_simulator.stop(timeout=COMPONENT_STOP_TIMEOUT)

monkey/common/types/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,4 @@
55
from .networking import NetworkService, NetworkPort, PortStatus, SocketAddress, NetworkProtocol
66
from .secrets import OTP, Token
77
from .file_extension import FileExtension
8-
from .percent import Percent, PercentLimited
8+
from .percent import Percent, PercentLimited, NonNegativeFloat

monkey/common/types/percent.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
from typing import Any, Self
1+
from typing import Any, Self, TypeAlias
22

3-
from pydantic import NonNegativeFloat
3+
from pydantic import NonNegativeFloat as PydanticNonNegativeFloat
4+
5+
NonNegativeFloat: TypeAlias = PydanticNonNegativeFloat
46

57

68
class Percent(NonNegativeFloat):

0 commit comments

Comments
 (0)