Analyzing an active vishing campaign using a Teams and email-bombing combination

I should be driving to Vegas but instead I am sitting here fucking around with some asshole’s C2 infrastructure.

An e-mail bomb was launched against LA County users together with a coordinated vishing attempt via Teams. The intent was to execute the malware package described here.

Exhaustive report below including the IOCs. Reach out if you want access to the full malware and investigation package but since this is currently active you should be able to fully reproduce everything based on the report below.

How the Malware Was Intended to Work

The malware was designed to turn a single PowerShell command entered by the user into a persistent remote-access foothold. Its workflow consisted of several stages: downloading an installer, unpacking a concealed Python backdoor, connecting to command-and-control servers, profiling the victim’s computer and Active Directory environment, returning the collected information, and establishing persistence for future operator commands.

1. Initial PowerShell Execution

The attack began when the user was instructed to enter or run a PowerShell command. That command downloaded a file named DefenderUpdate.msi into the user’s Downloads folder and launched it through msiexec.exe, the legitimate Windows Installer service.

The filename was intended to make the package resemble a Microsoft Defender update. Using an MSI package also made the activity look more like a normal software installation and allowed the attacker to organize the remaining malware components inside a standard Windows installer.

The PowerShell command was therefore only the entry point. Its purpose was to retrieve and start the installation package that contained the actual malware chain.

2. Installation and Unpacking

When executed, DefenderUpdate.msi created the following working directory inside the user’s profile:

%LOCALAPPDATA%\PowershellCleaner\scripts

The installer placed at least two important files in this directory:

archive6.zip
cleaner.ps1

It then launched cleaner.ps1 with PowerShell profile loading disabled and execution-policy restrictions bypassed. These options allowed the script to run without being affected by the user’s normal PowerShell configuration or local script-execution policy.

The decoded PowerShell wrapper was designed to generate a random 16-character folder name under %APPDATA% and extract archive6.zip into that folder. The resulting installation would resemble:

%APPDATA%\<16 random letters>\fl\ang\pythonw.exe
%APPDATA%\<16 random letters>\fl\ang\ss.py

The archive contained its own Python runtime and supporting DLLs. Consequently, the malware did not depend on Python already being installed on the victim’s computer.

After extraction, the script waited approximately five seconds and launched the bundled pythonw.exe with ss.py. The pythonw.exe version of Python does not display a normal console window, allowing the backdoor to operate with little visible indication to the user.

3. Identifying the Compromised Installation

The random 16-character installation folder was more than an attempt to conceal the files. The Python backdoor used that folder name as both a unique client identifier and part of its encryption mechanism.

The identifier also provided the value required to encrypt and decrypt communications. Anyone inspecting the network traffic without recovering the installation folder and malware code would have difficulty interpreting the exchanged information.

4. Connection to the Command-and-Control Service

After starting, ss.py selected one of three hardcoded command-and-control domains:

neversoftmain.net
rootfarmapp.net
tripinupdate.net

The malware generated randomized URL paths and sent an HTTPS POST request to the selected server. Its messages contained an eight-byte nonce followed by encrypted data. The first message registered the installation identifier and the time at which the malware started.

The backdoor disabled normal TLS certificate and hostname validation. The malware would accept whatever certificate the server supplied as long as an encrypted connection could be established.

After registering, the malware contacted the infrastructure every 90 to 120 seconds. It randomly selected among the three domains for each attempt. This supplied both redundancy and less predictable network traffic. If one domain stopped responding, the malware could continue through either of the other two.

5. Receiving and Executing Commands

The most important behavior occurred after the server responded. The malware decrypted the response and passed the resulting Python code directly to Python’s exec() function.

This gave the attacker a general remote-code-execution mechanism. The copy of ss.py stored on the computer did not need to contain every malicious feature. It functioned as a loader that repeatedly asked the servers what it should execute next.

6. System and Active Directory Reconnaissance

The first recovered server-supplied stage was designed to profile the compromised computer. It gathered:

the computer name, username, and Windows domain; the privilege level available to the malware; detailed Windows system information; running processes, including identifiers, resource usage, and executable paths; installed and running services and their startup configurations; the user’s security groups, access token, and Windows privileges; and the number of computer objects visible through Active Directory.

The reconnaissance stage used a bundled helper DLL and its RunPowerShell function to execute the required Windows commands. The results were placed into the backdoor’s outbound queue. During the next connection, the malware would encrypt the collected information and return it to the command-and-control service.

This information would allow the operator to determine whether the computer was worth further attention. In particular, it would reveal whether the user had administrative privileges, whether the system belonged to a Windows domain, which security or management products were running, and approximately how large the visible Active Directory environment was.

7. Establishing Persistence

After the reconnaissance stage, the command-and-control service delivered a second encrypted Python stage containing an embedded Windows DLL. The Python code used another bundled helper DLL, msvcp160.dll, to load the embedded DLL directly into memory and invoke its nm export.

The embedded DLL was a persistence module. Its purpose was to register a Windows scheduled task with a random 8-to-12-character name. That task would launch the private copy of pythonw.exe and run ss.py from the random %APPDATA% installation directory.

The scheduled task was configured to: run immediately after being registered; run approximately 30 seconds after the user logged on; run every day at 10:00 AM local time; start later if a scheduled execution was missed; allow manual or demand-based execution; and continue operating across changes in battery state.

The task would run using the current user’s interactive security token. The recovered persistence component did not contain a separate privilege-escalation mechanism. It would inherit whatever access the compromised user already possessed.

The immediate trigger would restart the backdoor after persistence was installed. The logon and daily triggers would provide additional opportunities for it to reconnect following a reboot, logoff, process termination, or temporary network interruption.

8. Intended End State

The attacker’s immediate objective was to establish a concealed and persistent remote-access backdoor. Once the complete workflow finished, the attacker would have:

a private Python environment hidden inside the user’s profile; an encrypted connection to three interchangeable command-and-control servers; detailed information about the computer, user privileges, processes, services, and Active Directory environment; a scheduled task that repeatedly restarted the backdoor; and the ability to send and execute additional Python code whenever the victim reconnected. The recovered stages establish reconnaissance, encrypted information transfer, remote tasking, in-memory DLL execution, and scheduled-task persistence. They do not establish what final operation the attacker planned after evaluating the victim.

Stage 1 : How cleaner.ps1 Worked

cleaner.ps1 was a decrypt-and-launch bootstrapper. Its job was not to communicate with the attacker directly. It unpacked and started the Python backdoor that handled command-and-control communications.

1. Started a Hidden PowerShell Process

The script began by launching another PowerShell process with:

powershell.exe -NoProfile -WindowStyle Hidden

-NoProfile prevented the user’s normal PowerShell configuration from affecting execution. -WindowStyle Hidden concealed the new PowerShell window.

The command contained a large array of hexadecimal character values. PowerShell converted those values back into text, joined them together, and passed the reconstructed code to Invoke-Expression, abbreviated as iex.

In effect, this outer layer concealed the actual script as thousands of values such as:

0x24, 0x62, 0x3D, 0x22 …

This was obfuscation, not encryption. Its purpose was to make the script difficult to read and reduce the chance that simple text-based security detections would recognize its contents.

2. Reconstructed the Decryption Routine

The reconstructed script contained: an encrypted Base64 blob; a hardcoded password; an eight-byte salt; an expected SHA-256 hash; and the code needed to decrypt and validate the next stage.

The relevant embedded values were:
Password: KFHEJBKFHEIFOndwovfdeiuf
Salt: 5A 1F 3C 99 0B E2 7D 44
Expected decrypted-script SHA-256: 1493819C060FCA7B3C00C5F3B7BE4D07C64FB31EF56C53305A0CC88935F26FAD

The password in this layer was a cryptographic key embedded by the malware author. It was not a stolen user password.

3. Derived the AES Key

The script passed the password and salt through the .NET Rfc2898DeriveBytes function using 5,000 iterations.

It requested:
32 bytes for the AES key; and
16 additional bytes for the initialization vector.
The malware then configured AES with:

Cipher mode: AES-CBC
Padding: PKCS7
Key size: 256 bits

It decoded the Base64 ciphertext, decrypted it in memory, and read the resulting plaintext as another PowerShell script. This prevented the actual deployment commands from appearing directly in the original file.

4. Verified the Decrypted Payload

Before executing the decrypted script, cleaner.ps1 calculated its SHA-256 hash and compared it with the hardcoded expected value:

1493819C060FCA7B3C00C5F3B7BE4D07C64FB31EF56C53305A0CC88935F26FAD\

If the hash did not match, the script displayed:

**Blocked - Hash mismatch **

If it matched, the script converted the plaintext into a PowerShell script block and executed it directly from memory. This integrity check was probably intended to ensure that the encrypted payload had been decrypted correctly and had not been corrupted or modified.

5. Created a Random Installation Directory

The decrypted stage generated a random 16-character name from uppercase and lowercase letters. It used that name to create a directory under the current user’s roaming application-data folder:

%APPDATA%\<16 random letters>

For example:

C:\Users\<user>\AppData\Roaming\aBcDeFgHiJkLmNoP

The random name made the installation path different on each victim. The Python backdoor later used this same folder name as its client identifier and cryptographic value.

The script also issued Clear, which would erase the visible contents of an interactive PowerShell window. Because the child PowerShell process was hidden, this was primarily an additional attempt to conceal activity.

6. Extracted the Python Backdoor

The script extracted:

%LOCALAPPDATA%\PowershellCleaner\scripts\archive6.zip

into the newly created random %APPDATA% directory.

archive6.zip contained a complete portable Python environment, including:

pythonw.exe
ss.py
msvcp150.dll
msvcp160.dll
Python libraries and supporting DLLs

Bundling the Python runtime meant the malware would work even if Python was not installed on the computer.

7. Launched ss.py

After extraction, the script waited five seconds and started:

%APPDATA%\<random name>\fl\ang\pythonw.exe

with the following script as its argument:

%APPDATA%\<random name>\fl\ang\ss.py

Using pythonw.exe prevented a Python console window from appearing.

At this point, cleaner.ps1 had completed its primary function. The newly launched ss.py process took over and began communicating with the three command-and-control domains.

Role in the Malware Chain

cleaner.ps1 performed four essential functions:

Concealed its instructions behind hexadecimal obfuscation and AES encryption.
Created a randomized installation directory.
Unpacked the self-contained Python backdoor.
Started the backdoor without displaying a console window.\

It did not itself perform reconnaissance, contact the C2 servers, exfiltrate information, or create scheduled-task persistence. Those behaviors belonged to ss.py and the encrypted stages subsequently delivered by the C2 service.

What Did the Embedded DLL Do?

The embedded DLL was a dedicated persistence component. Its purpose was to ensure that the Python backdoor, ss.py, continued running after the original PowerShell process ended, the user logged off, or the computer restarted.

The DLL did not contain command-and-control addresses, perform reconnaissance, steal credentials, or encrypt files. Its function was narrower but operationally critical: it created a concealed Windows scheduled task that repeatedly launched the installed Python backdoor.

5D823EBA3FD52BF5BD4A9D41E5EBE047

The recovered debug information contained this PDB path:

C:\Users\genry\source\repos\schdtaks_atlg_dll\x64\Release\schdtaks_atlg_dll.pdb

This suggests that the developer’s project was named schdtaks_atlg_dll and may have been built under a Windows account named genry. Both the PDB path and PE timestamp are useful correlation leads, but they can be deliberately altered and should not be treated as attribution by themselves.

1. Reflective Loading

The persistence stage loaded the locally bundled helper:

<payload directory>\msvcp160.dll

It configured and called the helper’s RunDllFunction export with three arguments:

Pointer to decoded DLL bytes Size of the decoded DLL Export name: nm

Conceptually, the call was:

RunDllFunction(dll_buffer, 173568, “nm”)

msvcp160.dll mapped the embedded PE image directly into the Python process and invoked its nm export.

This technique is known as reflective code loading. It bypassed the normal LoadLibrary workflow and allowed the persistence DLL to run without first being written to disk under a conventional DLL filename.

The DLL’s behavior therefore occurred inside the existing pythonw.exe process.

2. Located the Installed Backdoor

After its nm export began running, the DLL determined the directory from which the malware components had been loaded.

It then constructed paths to:

<payload directory>\pythonw.exe <payload directory>\ss.py

In the observed installation, those paths followed this pattern:

%APPDATA%\<16 random letters>\fl\ang\pythonw.exe %APPDATA%\<16 random letters>\fl\ang\ss.py

These became the executable and argument for the scheduled task.

The DLL did not install another backdoor. It established persistence for the existing ss.py implant.

3. Generated a Random Task Name

The DLL generated a random task name between eight and twelve characters long.

The available characters were:

ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz 0123456789

The resulting task name matched:

^[A-Za-z0-9]{8,12}$

A possible example would be:

aB7kP2xQ9

The name was generated using a Mersenne Twister pseudorandom-number generator. Because the name was randomized, defenders could not reliably hunt for one fixed scheduled-task name.

The task’s action and trigger configuration provide much stronger detection characteristics than its name alone.

4. Connected to Windows Task Scheduler

The DLL used the legitimate Windows Task Scheduler COM interfaces.

It connected to the Task Scheduler service and opened the root task folder():

It created a new task definition and registered it using:

TASK_CREATE_OR_UPDATE
This setting meant that the task would be created if it did not exist or updated if a task with the selected name already existed.

The task was registered using:

TASK_LOGON_INTERACTIVE_TOKEN

The DLL called GetUserNameW to obtain the current username. It did not provide a password when registering the task.

As a result, the scheduled task ran with the security rights of the currently compromised user. The DLL did not contain a separate privilege-escalation mechanism.

5. Configured the Task Action

The scheduled task launched:

%APPDATA%\<random folder>\fl\ang\pythonw.exe

with this argument:

%APPDATA%\<random folder>\fl\ang\ss.py

The working directory was:

%APPDATA%\<random folder>\fl\ang

Using pythonw.exe allowed the backdoor to start without presenting a console window to the user.

The resulting command was effectively:

pythonw.exe ss.py

The scheduled task did not launch cleaner.ps1 again. It directly restarted the already installed Python backdoor.

6. Created Multiple Execution Triggers

The DLL configured two recurring triggers and one immediate execution. The first trigger ran when the current user logged on and included a 30 second delay.

The delay allowed the user’s desktop and normal startup processes to initialize before the backdoor started. The second trigger ran every day at: 10:00:00 AM local time The recurrence interval was one day. This gave the malware another opportunity to restart even if the user remained logged on and the earlier backdoor process had terminated.

After registering the task, the DLL called:

IRegisteredTask::Run

This started the new task immediately.

7. Persistence Created by the DLL

Once registration was complete, the backdoor had several opportunities to return:

Immediately after task registration 30 seconds after the user logged on Every day at 10:00 AM After a missed trigger when the computer became available Whenever the task was started on demand

Every execution restarted pythonw.exe with ss.py, which resumed the encrypted 90-to-120-second C2 beacon cycle.

This converted the original one-time launch by cleaner.ps1 into a durable backdoor installation.

8. Windows Artifacts It Would Have Created

The scheduled task would normally produce artifacts in:

C:\Windows\System32\Tasks\<random task name>

and the Task Scheduler registry cache:

HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Schedule\TaskCache\Tree\<random task name>

HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Schedule\TaskCache\Tasks{task GUID}

Relevant event sources include:

Security event 4698 Microsoft-Windows-TaskScheduler/Operational

Stage 2 : How ss.py Worked

ss.py was the core command-and-control backdoor. Unlike cleaner.ps1, which only installed and started the payload, ss.py maintained communication with the attacker, transmitted results, received encrypted Python instructions, and executed those instructions on the compromised computer.

1. Determined Its Installation Identifier

When ss.py started, it examined its own location. The expected path was:

%APPDATA%\<16-character name>\fl\ang\ss.py

It moved three directory levels upward and extracted the random 16-character directory name created by cleaner.ps1.

That value became the malware’s local_id. It served three purposes: It uniquely identified the infected installation. It appeared inside every C2 request URL. It became the encryption key for communications with the C2 service.

Consequently, each installation had its own identifier and encryption key without requiring a separate configuration file.

2. Created Its Initial Registration Message

At startup, the malware constructed a small JSON message:

{ “ping”: { “local_id”: “<16-character installation identifier>”, “timestamp”: “<local startup time>”, “ping”: true } }

This was the initial registration or check-in message. It told the server that a particular installation was active and supplied the local time at which ss.py started.

The message did not contain detailed computer information. That information was collected later by code returned from the server.

3. Maintained an Outbound Results Queue

ss.py created a global list named results.

Initially, that list was empty, so the first C2 request contained the registration message. Server-supplied Python could subsequently place collected information into results.

On the next beacon, ss.py would take the first queued result, encrypt it, and send it instead of the registration message. This provided a simple two-way tasking cycle:

Beacon to server Receive and execute task Task places output in results Next beacon returns encrypted output

The queue was shared with downloaded code because C2 instructions were executed in the global Python namespace.

4. Encrypted the Outbound Message

For each request, ss.py generated a new random eight-byte nonce using os.urandom().

It then initialized a custom Salsa20-like stream cipher using:

Key: 16-character local installation ID Nonce: Eight random bytes

The implementation used the characteristic Salsa20 rotation sequence of 7, 9, 13, and 18 bits, but substituted custom internal constants. It should therefore be described as a modified or custom Salsa20-style cipher rather than a standard library implementation.

The cipher generated a pseudorandom byte stream. Each plaintext byte was XORed with the corresponding stream byte to produce ciphertext.

The final HTTP request body was:

[8-byte nonce][encrypted message]

The nonce was not secret. It was included so the server could initialize the same cipher and decrypt the remainder of the message.

Because this was a stream cipher, the same operation was used for encryption and decryption.

5. Constructed a Randomized C2 URL

For every connection attempt, the malware randomly selected one of three hardcoded domains:

neversoftmain.net
rootfarmapp.net
tripinupdate.net

It also selected two values from a list of ten hardcoded, randomly generated-looking strings.

The resulting URL had this format:

https://<C2 domain>/<random token>/<local_id>/<random token>

For example:

https://neversoftmain.net/<token>/<16-character ID>/<token>

The changing tokens caused the URL to vary between requests. The installation identifier remained in the middle of the path, allowing the server to identify the client before decrypting the request body.

The use of three domains provided redundancy. If one server became unavailable, later connection attempts could use either of the other two.

6. Sent an HTTPS POST Request

The malware sent the nonce and encrypted data using an HTTPS POST request with:

Content-Type: application/octet-stream Timeout: 30 seconds

Although the traffic used HTTPS, ss.py explicitly disabled certificate validation:

ssl_context.check_hostname = False ssl_context.verify_mode = ssl.CERT_NONE

The malware therefore did not verify that the certificate was valid, trusted, or issued for the requested domain. This made the implant tolerant of self-signed, expired, mismatched, or rapidly replaced certificates.

If the server returned an HTTP error response, the malware still read the response body. This allowed the C2 service to deliver commands through either a successful HTTP response or an HTTP error page.

If the connection failed entirely, ss.py abandoned that attempt and continued its beacon loop.

7. Decrypted the Server Response

A valid response was expected to contain at least eight bytes:

[8-byte server nonce][encrypted Python code]

ss.py separated the first eight bytes from the encrypted content. It then initialized the same custom cipher using:

Key: The installation’s 16-character local ID Nonce: The eight-byte nonce supplied by the server

The decrypted bytes were decoded as UTF-8 text.

If the response contained fewer than eight bytes, was empty, or could not be decrypted, it was ignored.

8. Executed the Returned Python

After decrypting the server response, ss.py executed the plaintext with:

exec(decrypted_code, globals())

This was the backdoor’s central capability.

The server response was not interpreted as a restricted command such as “list files” or “run process.” It was treated as unrestricted Python source code and executed inside the running implant.

The downloaded code could therefore:

call Python functions and operating-system APIs; load bundled DLLs; run PowerShell through helper components; collect information; add data to the results queue; create or modify files; start additional processes; and establish persistence.

Because the code ran in the global namespace, downloaded stages could access and modify ss.py’s existing variables, including results and other shared state.

This design allowed the attacker to change the implant’s capabilities from the server without modifying the installed ss.py file.

9. Repeated the Beacon Cycle

After each connection attempt, successful or unsuccessful, the malware waited for a random interval:

90 to 120 seconds

It then selected another domain and another pair of URL tokens and repeated the process.

The random delay reduced the regularity of the traffic. Random domain selection also meant that consecutive requests from the same infection could be sent to different C2 servers.

The loop had no normal termination condition:

Select domain and URL Encrypt registration or queued results Send HTTPS POST Receive encrypted Python Decrypt and execute it Wait 90–120 seconds Repeat indefinitely

Errors were generally ignored so a temporary connection failure, malformed response, or unavailable server would not permanently stop the implant.

10. Capabilities Supplied by the C2

ss.py itself did not contain the host-reconnaissance or scheduled-task code. Those capabilities were supplied dynamically by the C2 servers.

The recovered C2 responses instructed it to:

Collect system, process, service, privilege, and Active Directory information.

Place that information into the outbound results queue.

Return the encrypted results during a later beacon.

Load an embedded DLL in memory.

Create a scheduled task that restarted pythonw.exe ss.py.

This separation was intentional. Keeping capabilities on the server made the local implant smaller and allowed the operator to deliver different code to different victims.

Tags: