Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cryptojacker payload #3411

Closed
31 tasks done
mssalvatore opened this issue Jun 7, 2023 · 2 comments
Closed
31 tasks done

Cryptojacker payload #3411

mssalvatore opened this issue Jun 7, 2023 · 2 comments
Labels
Complexity: Low Feature Issue that describes a new feature to be implemented. Impact: High Payloads Plugins sp/13
Milestone

Comments

@mssalvatore
Copy link
Collaborator

mssalvatore commented Jun 7, 2023

Description

Build a payload plugin that allows Infection Monkey to simulate a cryptominer. The user should be able to configure:

  • CPU utilization (percentage) (single-core only)
  • Memory utilization (percentage)
  • Time limit (None for unlimited)
  • Send getblocktemplate requests (boolean)

Notes

  • The cryptominer should generate sha256 hashes in order to consume CPU usage
  • The getblocktemplate requests can be sent to the Island (the payload doesn't actually need a response).
  • See Cryptominer Research #3389 for more details

Tasks

  • Create plugin skeleton (0d) - @shreyamalviya
    • options (pydantic)
      • determine what options cryptominer should have
      • create a pydantic model for cryptominer options
    • Create plugin manifest
    • Create config schema
  • Research and define events that the payload will generate (0d) @mssalvatore
  • Modify the payload interface to accept the server (Island) (0d) - @shreyamalviya
    • Modify the puppet to construct payloads with the server
  • Build a basic cryptominer with stubbed components (0d) - @shreyamalviya
    • plugin.py constructs and executes the cryptominer
    • implement and call stubbed memory utilization component
    • implement and call stubbed sha256 CPU utilization component
    • implement and call stubbed getblocktemplate request generator
    • implement the logic that respects the time limit option
  • implement the memory utilization component (0d) @mssalvatore
  • implement the cpu utilization component (0d) - See comments below - @shreyamalviya
  • implement the getblocktemplate component (0d) - @shreyamalviya @mssalvatore @ilija-lazoroski
  • Add a jenkins job to build the plugin (0d) @mssalvatore
    • Update the island build jobs on Jenkins to copy the artifacts from the ransomware plugin build job
  • Final testing (0d)
  • Add documentation (0d) @mssalvatore
  • update changelog @mssalvatore
  • Remove Vulture entries @mssalvatore
  • Manual test procedure
@mssalvatore mssalvatore added Feature Issue that describes a new feature to be implemented. Impact: High Complexity: Low Plugins Payloads labels Jun 7, 2023
@mssalvatore mssalvatore added this to the v2.3.0 milestone Jun 7, 2023
@mssalvatore
Copy link
Collaborator Author

mssalvatore commented Aug 3, 2023

I've prototyped the CPU utilization algorithm. Below are included two versions. The first is the basic algoritm. The second one produces some useful output and graphs for performance tuning the algorithm's parameters.

import hashlib
from random import randbytes
from time import sleep

import psutil
from egg_timer import EggTimer

TARGET_CPU_UTILIZATION = .10
ACCURACY_THRESHOLD = 0.01
INITIAL_SLEEP_SECONDS = 0.001
AVERAGE_BLOCK_SIZE_BYTES = int(
    3.25 * 1024 * 1024
)  # 3.25 MB - Source: https://trustmachines.co/blog/bitcoin-ordinals-reignited-block-size-debate/
OPERATION_COUNT = 500
OPERATION_COUNT_MODIFIER_START = int(OPERATION_COUNT / 10)
OPERATION_COUNT_MODIFIER_FACTOR = 1.5
SIMULATION_TIME = 60
MINIMUM_SLEEP = 0.000001


def main():
    timer = EggTimer()
    process = psutil.Process()
    sleep_seconds = INITIAL_SLEEP_SECONDS
    nonce = 0
    block = randbytes(AVERAGE_BLOCK_SIZE_BYTES)
    operation_count_modifier = OPERATION_COUNT_MODIFIER_START

    # This is a throw-away call. The first call to cpu_percent() always seems to return 0, even
    # after generating some hashes. This could be due to some implementation detail in psutil, which
    # performs some additional operations the first time it's called, causing utilization to drop
    # back down to 0. In other words, my guess is that the first time cpu_percent() is called, the
    # act of measurement has a large effect on what's being measured (i.e. the Heisenberg
    # uncertainty principle is in play), where as subsequent calls have less of an effect.
    #
    # UPDATE: Mystery solved! See https://psutil.readthedocs.io/en/latest/#psutil.cpu_percent
    process.cpu_percent()

    timer.set(SIMULATION_TIME)
    while not timer.is_expired():
        # The operation_count_modifier decreases the number of hashes per iteration. The modifier,
        # itself, decreases by a factor of 1.5 each iteration, until it reaches 1. This allows a
        # higher sample rate of the CPU utilization early on to help the sleep time to converge
        # quicker.
        for _ in range(0, int(OPERATION_COUNT / operation_count_modifier)):
            digest = hashlib.sha256()
            digest.update(nonce.to_bytes(8))
            digest.update(block)
            nonce += 1

            sleep(sleep_seconds)

        operation_count_modifier = max(
            operation_count_modifier / OPERATION_COUNT_MODIFIER_FACTOR, 1
        )

        cpu_utilization = process.cpu_percent() / 100
        cpu_utilization_percent_error = calculate_percent_error(cpu_utilization)

        sleep_seconds = calculate_new_sleep(sleep_seconds, cpu_utilization_percent_error)


def calculate_percent_error(measured: float) -> float:
    return (measured - TARGET_CPU_UTILIZATION) / TARGET_CPU_UTILIZATION


def calculate_new_sleep(current_sleep: float, percent_error: float):
    if abs(percent_error) < ACCURACY_THRESHOLD:
        return current_sleep

    # Since our multiplication is based on sleep_seconds, don't ever let
    # sleep_seconds == 0, otherwise it will never equal anything else.
    return current_sleep * max((1 + percent_error), MINIMUM_SLEEP)


if __name__ == "__main__":
    main()
import hashlib
import hashlib
from random import randbytes
from time import sleep
from typing import List

import matplotlib.pyplot as plt
import psutil
from egg_timer import EggTimer

TARGET_CPU_UTILIZATION = 0.05
ACCURACY_THRESHOLD = 0.02
INITIAL_SLEEP_SECONDS = 0.001
AVERAGE_BLOCK_SIZE_BYTES = int(
    3.25 * 1024 * 1024
)  # 3.25 MB - Source: https://trustmachines.co/blog/bitcoin-ordinals-reignited-block-size-debate/
OPERATION_COUNT = 250
OPERATION_COUNT_MODIFIER_START = int(OPERATION_COUNT / 10)
OPERATION_COUNT_MODIFIER_FACTOR = 1.5
SIMULATION_TIME = 60


def main():
    timer = EggTimer()
    process = psutil.Process()
    sleep_seconds = INITIAL_SLEEP_SECONDS
    nonce = 0
    block = randbytes(AVERAGE_BLOCK_SIZE_BYTES)
    operation_count_modifier = OPERATION_COUNT_MODIFIER_START

    sleep_times = [sleep_seconds]
    cpu_utilizations = []
    percent_errors = []


    # This is a throw-away call. The first call to cpu_percent() always seems to return 0, even
    # after generating some hashes. This could be due to some implementation detail in psutil, which
    # performs some additional operations the first time it's called, causing utilization to drop
    # back down to 0. In other words, my guess is that the first time cpu_percent() is called, the
    # act of measurement has a large effect on what's being measured (i.e. the Heisenberg
    # uncertainty principle is in play), where as subsequent calls have less of an effect.
    process.cpu_percent()

    print(f"First sleep: {sleep_seconds}\n")

    timer.set(SIMULATION_TIME)
    while not timer.is_expired():
        # The operation_count_modifier decreases the number of hashes per iteration. The modifier,
        # itself, decreases by a factor of 1.5 each iteration, until it reaches 1. This allows a
        # higher sample rate of the CPU utilization early on to help the sleep time to converge
        # quicker.
        for _ in range(0, int(OPERATION_COUNT / operation_count_modifier)):
            digest = hashlib.sha256()
            digest.update(nonce.to_bytes(8))
            digest.update(block)
            sleep(sleep_seconds)
            nonce += 1

        operation_count_modifier = max(
            operation_count_modifier / OPERATION_COUNT_MODIFIER_FACTOR, 1
        )

        cpu_utilization = process.cpu_percent() / 100
        cpu_utilizations.append(cpu_utilization)

        cpu_utilization_percent_error = (
            cpu_utilization - TARGET_CPU_UTILIZATION
        ) / TARGET_CPU_UTILIZATION
        percent_errors.append(cpu_utilization_percent_error)

        if abs(cpu_utilization_percent_error) >= ACCURACY_THRESHOLD:
            # Since our multiplication is based on sleep_seconds, don't ever let
            # sleep_seconds == 0, otherwise it will never equal anything else.
            sleep_seconds *= max((1 + cpu_utilization_percent_error), 0.000001)

        sleep_times.append(sleep_seconds)

        print(f"CPU Utilization: {cpu_utilization}")
        print(f"Percent error: {cpu_utilization_percent_error}")
        print(f"New sleep: {sleep_seconds}")
        print()

    plot_sleep_times(sleep_times)
    plot_cpu_utilization(cpu_utilizations)
    plot_percent_error(percent_errors)


def plot_sleep_times(sleep_times: List[float]):
    x = range(0, len(sleep_times))
    y = sleep_times

    plt.clf()
    plt.plot(x, y)
    plt.xlabel("Iteration")
    plt.ylabel("Sleep time (seconds)")
    plt.title("Sleep time per iteration")
    plt.savefig("sleep_times.png")


def plot_cpu_utilization(cpu_utilizations: List[float]):
    x = range(0, len(cpu_utilizations))
    y = [utilization * 100 for utilization in cpu_utilizations]

    plt.clf()
    plt.plot(x, y)
    plt.xlabel("Iteration")
    plt.ylabel("CPU Utilization (percent)")
    plt.title("CPU Utilization per iteration")
    plt.savefig("cpu_utilization.png")


def plot_percent_error(percent_errors: List[float]):
    x = range(0, len(percent_errors))
    y = [utilization * 100 for utilization in percent_errors]

    plt.clf()
    plt.plot(x, y)
    plt.xlabel("Iteration")
    plt.ylabel("Percent Error (percent)")
    plt.title("Percent Error per iteration")
    plt.savefig("percent_error.png")

if __name__ == "__main__":
    main()

@shreyamalviya shreyamalviya changed the title Cryptominer payload Cryptojacker payload Aug 4, 2023
@mssalvatore mssalvatore mentioned this issue Aug 4, 2023
10 tasks
mssalvatore added a commit that referenced this issue Aug 6, 2023
@mssalvatore
Copy link
Collaborator Author

mssalvatore commented Aug 7, 2023

You may need https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-getcurrentprocessornumber to get the processor number on Windows.

Example:

def GetCurrentProcessorNumber():
    _GetCurrentProcessorNumber = windll.kernel32.GetCurrentProcessorNumber
    _GetCurrentProcessorNumber.argtypes = []
    _GetCurrentProcessorNumber.restype  = DWORD
    _GetCurrentProcessorNumber.errcheck = RaiseIfZero
    return _GetCurrentProcessorNumber()

# VOID WINAPI FlushProcessWriteBuffers(void); 

Source: https://www.programcreek.com/python/?CodeExample=get+processor

mssalvatore added a commit that referenced this issue Aug 9, 2023
mssalvatore added a commit that referenced this issue Aug 9, 2023
mssalvatore added a commit that referenced this issue Aug 9, 2023
mssalvatore added a commit that referenced this issue Aug 9, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Complexity: Low Feature Issue that describes a new feature to be implemented. Impact: High Payloads Plugins sp/13
Projects
None yet
Development

No branches or pull requests

2 participants