Every automated inspection step in your environment has a maximum file size. The sandbox has one. The mail gateway has one. The EDR agent's cloud submission path has one. VirusTotal has one. On a small security team, nobody has read any of those four numbers in years.
Forescout's Vedere Labs published the number that makes them worth reading again. On October 15 its researchers documented a malware delivery platform they call ROMULUS, and the executable at the end of one of its two chains is 882,716,672 bytes. About 881.7 MB of that sits in a single .reloc section whose relocation directory describes 108 bytes of real relocation data. The rest is one 16-byte value, repeated. It reaches the endpoint inside a 7-Zip archive under a megabyte, and inflates there.
That is MITRE's T1027.001, Binary Padding, used exactly as the technique page describes: padding that increases "the size of the binary beyond what some security tools are capable of handling." The limits it targets are configuration values you own, and you can go read them this afternoon.
Whether this one is yours
ROMULUS delivers to Windows endpoints through ClickFix. Someone looking for software lands on a page that tells them to paste a command into the Run dialog, and they paste it. Forescout identified six IP addresses running the platform, analysed two of them, and found a web panel at 86.109.75[.]7/stats.htm that counts unique installs and runs by country. The operator is unknown. One delivery node, 96.62.224[.]81, is the same host Huntress documented on September 28 serving ClickFix lures from attacker-built ChatGPT Custom GPTs.
If you run Windows desktops where users can open the Run dialog, launch PowerShell and install MSI packages, this one is yours. If you have already taken the Run dialog away from standard users, constrained PowerShell to a signed-script policy, and blocked msiexec from user-writable paths, the delivery half fails before any of the padding matters, and the size-threshold section below is the only part you need. On a Mac or Linux fleet you can skip the whole thing. Every stage of this chain is Windows-specific.
The sandbox you assume detonated the file
Test the first belief: anything suspicious that lands on an endpoint gets detonated somewhere. Usually it does. It stops happening above a size written down in a settings page that nobody opens.
VirusTotal states its ceiling plainly in the upload documentation: "the actual size limit is 650MBs." Files under 32 MB go to the standard endpoint; anything larger needs a separate one-time upload URL. The same page advises against submitting bundles over 200 MB at all, because "engines tend to have performance issues on big files (timeouts, some may not even scan them)." An 882 MB executable clears none of that. Nothing hands it to the analysers in the first place.
Your own stack carries the same shape of limit in more places than VirusTotal does. Read these four numbers before you do anything else with this article.
Four thresholds to look up today
- The maximum file size your EDR will upload for cloud analysis or detonation.
- The maximum attachment size your mail gateway will scan, which is a different number from the maximum it will deliver.
- The maximum object size your web proxy or TLS-inspecting firewall buffers for inspection rather than streaming straight through.
- The maximum file size your on-access antivirus reads, plus its separate archive-extraction limit.
Each of those is a number, and in most products it ships defaulted low enough to matter. Write all four on the same page. That page is an inventory of what an attacker gets for free by making a file large.
The archive is small, the file is enormous
The second belief: a very large payload means a very large download, and a 900 MB transfer out of nowhere would show up on the network. ROMULUS breaks that at the archive layer.
One route starts with a PowerShell staging script that hides its own console window, fetches orbitflownetwork[.]lol/dl/J8QkZUVJgV.7z, and separately downloads the legitimate 7zr.exe from GitHub. It verifies the archive begins with the 7z signature and that 7zr.exe starts with an MZ header before using it. The outer archive opens with a password hardcoded in the script. Inside sits a second archive and a 20-byte text file holding that archive's password. The second archive yields five high-entropy .bin files the final payload never reads, and one randomly named executable: the 882,716,672-byte carrier.
Nothing oversized crosses the wire. A repeated 16-byte value compresses to almost nothing, so the inflation happens during extraction on the endpoint. Encrypting the 7-Zip headers hides the member names as well, so a scanner that peeks inside an archive sees neither a filename nor a size worth alerting on. The carrier's first 2 MB or so holds the real PE headers, the code, the configuration and two packed stages. Everything after that is filler.
The stages themselves are ordinary once you reach them. Stage one uses CryptoAPI with a plaintext AES-256 key blob to decrypt an embedded buffer, strips the PKCS#7 padding and reverses the bytes to produce 542,356 bytes of shellcode. That shellcode repeats the decrypt-and-reverse step, then manually maps a 475,648-byte PE: LegionLoader. The loader registers with smashshape[.]com on /nfront.php, reports results to /nback.php, derives an 8-character machine ID by running CRC32 over the volume serial five times, and reaches for Chrome's Local State key material.
The PowerShell you assume you would notice
The third belief: a staging script that long and that obfuscated would trip something on the way past. Both ROMULUS staging scripts rebuild their real code from a large array of integers using a per-sample key, run it through [scriptblock]::Create, store their own path in $env:__ST, and delete themselves on the way out. There is no stable string to signature, and the variable names and keys change per sample.
The structure does not change, and script block logging records structure. Event 4104 in the Microsoft-Windows-PowerShell/Operational log captures the content of every script block PowerShell processes once script block logging is turned on. A deobfuscation call and a URL written as a bare decimal integer both survive the obfuscation, because they are what the script has to do to work.
That decimal notation is the cheapest indicator in the whole report. One route downloads from hxxp://1450003207/app/..., which is 86.109.75[.]7 written as a 32-bit integer. 1614733393 is 96.62.224[.]81. Browsers and .NET's WebClient both resolve it happily. Almost nothing legitimate writes a URL that way.
// Microsoft Defender advanced hunting: the three ROMULUS delivery steps.
// Decimal-notation URLs, password-flagged 7-Zip extraction, silent MSI from TEMP.
DeviceProcessEvents
| where Timestamp > ago(30d)
| where ProcessCommandLine matches regex @"https?://\d{8,10}([/:?]|$)"
or (FileName =~ "7zr.exe" and ProcessCommandLine has "-p")
or (FileName =~ "msiexec.exe"
and ProcessCommandLine has "/qn"
and ProcessCommandLine has_any (@"\Temp\", @"\AppData\Local\"))
| project Timestamp, DeviceName, AccountName, FileName,
ProcessCommandLine, InitiatingProcessFileName, InitiatingProcessCommandLine
| order by Timestamp desc
Without Defender for Endpoint, the same three signals are in the local event logs. This reads the script block log for the deobfuscation structure rather than for any one sample's strings.
# Script block logs: match the shape of the stager, not its variable names.
Get-WinEvent -FilterHashtable @{
LogName = 'Microsoft-Windows-PowerShell/Operational'
Id = 4104
StartTime = (Get-Date).AddDays(-30)
} -ErrorAction SilentlyContinue |
Where-Object {
$_.Message -match '\[scriptblock\]::Create' -or
$_.Message -match '\$env:__ST' -or
$_.Message -match 'https?://\d{8,10}'
} |
Select-Object TimeCreated,
@{ n = 'Snippet'; e = { $_.Message.Substring(0, [Math]::Min(240, $_.Message.Length)) } } |
Sort-Object TimeCreated -Descending
Hunt the shape, not the sample
Forescout's hashes are worth blocking today and will be stale next week, because the panel mints fresh 12-character tokens and fresh filenames per campaign. The shape outlives them. Three sequences deserve standing detections.
Three sequences worth a rule
- PowerShell to 7-Zip to a new executable. A
powershell.exeparent spawning7zr.exewith-pand-y, then a process launched from the extraction directory seconds later. - Silent MSI from a user-writable path.
msiexec.exe /i <path> /qn /norestartwhere the path is under%TEMP%, especially whenUnblock-Fileran first. The other ROMULUS route installs a counterfeit "Stardock Smart DeElevation Tool" that sideloads a patchedDeElevator64.dllinto the genuineDeElevate64.exe. - A large PE written where users can write. Then look at its section table.
The third one is the check almost nobody has. An 842 MB file is unremarkable on its own; installers and game assets get that big. An 842 MB PE whose single .reloc section carries 881.7 MB of raw data while the header declares 108 bytes of relocations is damning with no other context. That ratio costs microseconds to compute and does not care which malware family you are looking at.
# Step 1: oversized executables recently written to user-writable paths.
$roots = @($env:TEMP, "$env:LOCALAPPDATA\Programs", "$env:USERPROFILE\Downloads")
$big = Get-ChildItem -Path $roots -Recurse -Include *.exe,*.dll -File -ErrorAction SilentlyContinue |
Where-Object { $_.Length -gt 200MB -and $_.LastWriteTime -gt (Get-Date).AddDays(-30) }
# Step 2: print the section table. Binary padding shows up as one section
# holding nearly the whole file. The ROMULUS carrier put 881.7 MB in .reloc.
function Get-PeSectionSizes {
param([Parameter(Mandatory)][string]$Path)
$fs = [IO.File]::OpenRead($Path)
try {
$br = [IO.BinaryReader]::new($fs)
$fs.Position = 0x3C; $pe = $br.ReadUInt32()
$fs.Position = $pe
if ($br.ReadUInt32() -ne 0x00004550) { return } # "PE\0\0"
$fs.Position = $pe + 6; $sections = $br.ReadUInt16()
$fs.Position = $pe + 20; $optSize = $br.ReadUInt16()
$fs.Position = $pe + 24 + $optSize # section table
for ($i = 0; $i -lt $sections; $i++) {
$name = [Text.Encoding]::ASCII.GetString($br.ReadBytes(8)).TrimEnd([char]0)
$null = $br.ReadUInt32() # VirtualSize
$null = $br.ReadUInt32() # VirtualAddress
$raw = $br.ReadUInt32() # SizeOfRawData
$null = $br.ReadBytes(16); $null = $br.ReadUInt32()
[pscustomobject]@{
File = Split-Path $Path -Leaf
Section = $name
RawMB = [math]::Round($raw / 1MB, 1)
}
}
} finally { $fs.Dispose() }
}
$big | ForEach-Object { Get-PeSectionSizes -Path $_.FullName } | Format-Table -AutoSize
Run that across %TEMP%, %LOCALAPPDATA%\Programs and the Downloads folder on a dozen machines and you will learn what large executables normally look like in your estate. The baseline is the point. The ROMULUS carrier is one instance of a shape any family can adopt in an afternoon, and once you know your own numbers the next one is a five-line query rather than a research project.
Go read the four size limits before the next sample arrives
Pull the four thresholds out of their settings pages and write them into the triage runbook, next to the submission steps, so the analyst holding a 900 MB executable at 2 a.m. already knows the sandbox will refuse it and the work has to happen on the endpoint. Add the section-table ratio to whatever you run locally on a suspicious file. Blocking the six ROMULUS addresses and the two domains takes five minutes and buys you this campaign; knowing your own inspection ceilings buys you the next one. If you want a second pair of eyes on the detection pipeline that has to carry all of this, that is work we do.
Want these checks running without you?
We build open-source security tooling and help teams turn one-off hunts into standing detections. Browse our tools on GitHub, or book a session to work through your detection pipeline with us.
