Fake GitHub Security Tools: Hunt the Visual Studio Build Trap


What this means for your clients: A free tool downloaded from the internet can take over the computer of the person who builds it, and that person often holds the keys to your network.
Most of us read a script before running it, but few read the project file before pressing Build. Doctor Web's November 5 research found repositories posing as fake GitHub security tools for scanning, testing and log analysis, and reports more than 2,000 infected PCs at the time of its research. One claims to clean Fscan output, though the real Fscan project is not involved. The two-year campaign began with blockchain game projects. Doctor Web names no actor, so neither do I.
How pressing Build runs someone else's code
A .csproj file is an MSBuild script, and MSBuild runs whatever commands it contains. PreBuildEvent is a legitimate feature for running steps before compiling. In these repositories it launches the attacker's code with your rights when you build in Visual Studio or run dotnet build or msbuild. The reviewed source never has to be malicious, which is why this catches careful people.
The .suo file holds Visual Studio's per-user options. Doctor Web found some carrying a PowerShell command. Newer projects also hide code in Python and Go files behind long runs of whitespace that push it off screen. Doctor Web does not detail every trigger, and Security Affairs says the script runs once the project is opened, so treat opening and building as equally risky.
A PowerShell stage lands as t.ps1 or L.ps1 in C:\Users\Public\Pictures. It pulls an encrypted pointer from services like Telegram and Pastebin, downloads Quixo.7z, and unpacks it into C:\ProgramData\Windows.Microsoft.Photos, a name chosen to look like Windows. Inside is Microsoft.exe, an Electron loader Doctor Web tracks as Trojan.Loader.3261.
That loader fetches Nexara.7z, reports to a Telegram bot, keeps itself running, and then attacks your defenses. It disables Microsoft Defender, UAC, System Restore and shadow copies, adds Defender exclusions including regasm.exe and dwm.exe, and changes VerifiedAndReputablePolicyState to get past Smart App Control. It patches ETW and AMSI, spoofs explorer.exe as its parent, injects into RegAsm.exe, dwwin.exe and dwm.exe, tries to remove ESET, McAfee and Malwarebytes, and edits the hosts file to block antivirus vendors.
The payloads that follow are two AsyncRAT backdoors, a crypto clipper and an Electron stealer called JS.Stealer.191. Because the console is blinded before they arrive, a quiet console proves nothing here.
What one infected analyst laptop means
An analyst laptop usually has admin rights, VPN profiles and saved sessions into client systems. Doctor Web says the stealer takes browser data, FileZilla XML files, MobaXterm.ini, VPN settings and Discord tokens. One build turns the defender's machine into an initial access point. For an MSP, that reaches every client whose credentials sit in those tools.
Reduce the risk before anyone presses Build
Build unvetted repositories only in a disposable VM with no corporate credentials or saved sessions. Before building, read the .csproj and any imported .targets or .props files for PreBuildEvent, PostBuildEvent and Exec entries. Delete any .suo file that arrives with a download, and turn on word wrap when reviewing Python and Go.
Keep Defender tamper protection on, limit and alert on exclusion changes, test attack surface reduction rules, and give staff an approved source for internal tools. If a machine was hit, rotate every password and key saved in its SSH, FTP and VPN clients and browsers, revoke sessions, and rebuild it, because this loader blinds the tools you would use to confirm a cleanup.
Before you hunt
We wrote these from vendor documentation and haven't run them in a live SIEM, so test each one in your environment before you trust it.
Prepare in order. First, list the machines that build code from your asset system or EDR console, not the SIEM. Second, confirm each sends what its platform needs, which is Sysmon, Elastic Defend, Falcon sensor events or Defender for Endpoint tables. Third, reconcile against sensor check-ins and collector health before any time filter, because counts taken after event-time filtering cannot prove completeness. Then run the coverage test. A build machine that returns nothing is a gap, not a clean result.
All 20 blocks share one window, 2026-10-10T07:00:00Z inclusive to 2026-11-09T07:00:00Z exclusive, which is midnight October 10 to midnight November 9 Phoenix time (MST). Widen it as retention allows, changing every block and time picker together.
Coverage test: can you see your build machines
Every build machine on your list should appear here, by host and event type.
Field mapping: Splunk assumes the Sysmon add-on in YOUR_SYSMON_INDEX. CrowdStrike file write event names are an assumption.
Splunk
index=YOUR_SYSMON_INDEX sourcetype="XmlWinEventLog:Microsoft-Windows-Sysmon/Operational" EventCode IN (1, 3, 11, 13, 22) earliest=1791615600 latest=1794207600
| where _time >= 1791615600 AND _time < 1794207600
| stats count AS events min(_time) AS first_seen max(_time) AS last_seen BY host EventCodeElastic
FROM logs-endpoint.events.*
| WHERE @timestamp >= TO_DATETIME("2026-10-10T07:00:00Z") AND @timestamp < TO_DATETIME("2026-11-09T07:00:00Z")
| STATS events = COUNT(*), first_seen = MIN(@timestamp), last_seen = MAX(@timestamp) BY host.name, data_stream.datasetCrowdStrike
#event_simpleName=/^(ProcessRollup2|DnsRequest|NetworkConnectIP4|RegGenericValueUpdate|AsepValueUpdate|\w+Written)$/
| test(@timestamp >= 1791615600000)
| test(@timestamp < 1794207600000)
| groupBy([aid, #event_simpleName], function=[count(as=events), min(@timestamp, as=first_seen), max(@timestamp, as=last_seen)])Microsoft KQL
union withsource=SourceTable DeviceProcessEvents, DeviceFileEvents, DeviceRegistryEvents, DeviceNetworkEvents
| where Timestamp >= datetime(2026-10-10T07:00:00Z) and Timestamp < datetime(2026-11-09T07:00:00Z)
| summarize Events = count(), FirstSeen = min(Timestamp), LastSeen = max(Timestamp) by DeviceName, SourceTableHunt 1: a build tool that launches a shell
The most direct signal is devenv.exe, MSBuild.exe or dotnet.exe starting PowerShell or cmd. Legitimate pre-build steps do this too, so judge each hit by its command line and repository.
Field mapping: Elastic assumes event.action "start". CrowdStrike ParentBaseFileName is an assumption.
Splunk
index=YOUR_SYSMON_INDEX sourcetype="XmlWinEventLog:Microsoft-Windows-Sysmon/Operational" EventCode=1 earliest=1791615600 latest=1794207600
| where _time >= 1791615600 AND _time < 1794207600
| where (like(lower(ParentImage), "%devenv.exe") OR like(lower(ParentImage), "%msbuild.exe") OR like(lower(ParentImage), "%dotnet.exe")) AND (like(lower(Image), "%powershell.exe") OR like(lower(Image), "%pwsh.exe") OR like(lower(Image), "%cmd.exe"))
| table _time host User ParentImage Image CommandLine CurrentDirectoryElastic
FROM logs-endpoint.events.process-*
| WHERE @timestamp >= TO_DATETIME("2026-10-10T07:00:00Z") AND @timestamp < TO_DATETIME("2026-11-09T07:00:00Z")
| WHERE event.action == "start"
| WHERE TO_LOWER(process.parent.name) IN ("devenv.exe", "msbuild.exe", "dotnet.exe") AND TO_LOWER(process.name) IN ("powershell.exe", "pwsh.exe", "cmd.exe")
| KEEP @timestamp, host.name, user.name, process.parent.name, process.command_line, process.working_directory
| SORT @timestampCrowdStrike
#event_simpleName=ProcessRollup2
| test(@timestamp >= 1791615600000)
| test(@timestamp < 1794207600000)
| in(field="ParentBaseFileName", values=["devenv.exe", "msbuild.exe", "dotnet.exe"], ignoreCase=true)
| in(field="FileName", values=["powershell.exe", "pwsh.exe", "cmd.exe"], ignoreCase=true)
| table([@timestamp, aid, UserName, ParentBaseFileName, FileName, CommandLine])Microsoft KQL
DeviceProcessEvents
| where Timestamp >= datetime(2026-10-10T07:00:00Z) and Timestamp < datetime(2026-11-09T07:00:00Z)
| where InitiatingProcessFileName in~ ("devenv.exe", "msbuild.exe", "dotnet.exe")
| where FileName in~ ("powershell.exe", "pwsh.exe", "cmd.exe")
| project Timestamp, DeviceName, AccountName, InitiatingProcessFileName, InitiatingProcessCommandLine, FileName, ProcessCommandLineHunt 2: dropped files and the loader they start
This looks for scripts written to C:\Users\Public\Pictures, files under C:\ProgramData\Windows.Microsoft.Photos, and changes in drivers\etc where the hosts file lives. It also catches the loader running, meaning any process launched from Windows.Microsoft.Photos or a Microsoft.exe outside Program Files and WindowsApps.
Field mapping: Sysmon logs only what your configuration includes, and Falcon does not record every file write, so hosts edits may be missed.
Splunk
index=YOUR_SYSMON_INDEX sourcetype="XmlWinEventLog:Microsoft-Windows-Sysmon/Operational" EventCode IN (1, 11) earliest=1791615600 latest=1794207600
| where _time >= 1791615600 AND _time < 1794207600
| eval f=lower(coalesce(TargetFilename, "")), img=lower(coalesce(Image, ""))
| where (EventCode=11 AND (like(f, "c:_users_public_pictures_%.ps1") OR like(f, "c:_programdata_windows.microsoft.photos_%") OR like(f, "%_system32_drivers_etc_%"))) OR (EventCode=1 AND (like(img, "c:_programdata_windows.microsoft.photos_%") OR (like(img, "%_microsoft.exe") AND NOT like(img, "c:_program files%") AND NOT like(img, "%_windowsapps_%"))))
| table _time host User EventCode Image CommandLine TargetFilenameElastic
FROM logs-endpoint.events.file-*, logs-endpoint.events.process-*
| WHERE @timestamp >= TO_DATETIME("2026-10-10T07:00:00Z") AND @timestamp < TO_DATETIME("2026-11-09T07:00:00Z")
| EVAL f = TO_LOWER(COALESCE(file.path, "")), img = TO_LOWER(COALESCE(process.executable, ""))
| WHERE (data_stream.dataset == "endpoint.events.file" AND (f LIKE "c:?users?public?pictures?*.ps1" OR f LIKE "c:?programdata?windows.microsoft.photos?*" OR f LIKE "*?system32?drivers?etc?*")) OR (data_stream.dataset == "endpoint.events.process" AND event.action == "start" AND (img LIKE "c:?programdata?windows.microsoft.photos?*" OR (img LIKE "*?microsoft.exe" AND img NOT LIKE "c:?program files*" AND img NOT LIKE "*?windowsapps?*")))
| KEEP @timestamp, host.name, user.name, data_stream.dataset, event.action, process.executable, process.command_line, file.path
| SORT @timestampCrowdStrike
#event_simpleName=/^(ProcessRollup2|\w+Written)$/
| test(@timestamp >= 1791615600000)
| test(@timestamp < 1794207600000)
| TargetFileName=/\\Users\\Public\\Pictures\\[^\\]+\.ps1$|\\ProgramData\\Windows\.Microsoft\.Photos\\|\\System32\\drivers\\etc\\/i or (#event_simpleName=ProcessRollup2 and (ImageFileName=/\\ProgramData\\Windows\.Microsoft\.Photos\\/i or (ImageFileName=/\\Microsoft\.exe$/i and ImageFileName!=/\\(Program Files|Program Files \(x86\)|WindowsApps)\\/i)))
| table([@timestamp, aid, #event_simpleName, ImageFileName, CommandLine, TargetFileName, ContextProcessId])Microsoft KQL
union withsource=SourceTable DeviceFileEvents, DeviceProcessEvents
| where Timestamp >= datetime(2026-10-10T07:00:00Z) and Timestamp < datetime(2026-11-09T07:00:00Z)
| where (SourceTable == "DeviceFileEvents" and ((FolderPath contains @"\Users\Public\Pictures\" and FileName endswith ".ps1")
or FolderPath contains @"\ProgramData\Windows.Microsoft.Photos"
or FolderPath contains @"\System32\drivers\etc"))
or (SourceTable == "DeviceProcessEvents" and (FolderPath contains @"\ProgramData\Windows.Microsoft.Photos\"
or (FileName =~ "Microsoft.exe" and FolderPath !contains @"\Program Files" and FolderPath !contains @"\WindowsApps\")))
| project Timestamp, DeviceName, SourceTable, ActionType, FolderPath, FileName, ProcessCommandLine, InitiatingProcessFileName, InitiatingProcessCommandLineHunt 3: tampering with Defender, recovery and antivirus
This looks for exclusion and disable commands, shadow copy deletion, registry writes to Defender exclusions, VerifiedAndReputablePolicyState, DisableSR and EnableLUA, plus uninstall commands naming ESET, McAfee or Malwarebytes. Doctor Web describes outcomes, not commands, so these are common methods, not confirmed ones. Deployment tools add exclusions legitimately.
Field mapping: CrowdStrike registry event names are an assumption. An uninstall run by product code alone will not name the vendor, so check installer logs too.
Splunk
index=YOUR_SYSMON_INDEX sourcetype="XmlWinEventLog:Microsoft-Windows-Sysmon/Operational" EventCode IN (1, 13) earliest=1791615600 latest=1794207600
| where _time >= 1791615600 AND _time < 1794207600
| eval cmd=lower(coalesce(CommandLine, "")), reg=lower(coalesce(TargetObject, ""))
| where like(cmd, "%add-mppreference%exclusion%") OR like(cmd, "%disablerealtimemonitoring%") OR like(cmd, "%vssadmin%delete%shadows%") OR like(cmd, "%shadowcopy%delete%") OR like(reg, "%windows defender_exclusions%") OR like(reg, "%verifiedandreputablepolicystate%") OR like(reg, "%disablesr%") OR like(reg, "%enablelua%") OR (match(cmd, "(^|[^a-z])(eset|mcafee|malwarebytes)([^a-z]|$)") AND match(cmd, "unins|/x"))
| table _time host User EventCode Image CommandLine TargetObject DetailsElastic
FROM logs-endpoint.events.process-*, logs-endpoint.events.registry-*
| WHERE @timestamp >= TO_DATETIME("2026-10-10T07:00:00Z") AND @timestamp < TO_DATETIME("2026-11-09T07:00:00Z")
| EVAL cmd = TO_LOWER(COALESCE(process.command_line, "")), reg = TO_LOWER(COALESCE(registry.path, ""))
| WHERE cmd LIKE "*add-mppreference*exclusion*" OR cmd LIKE "*disablerealtimemonitoring*" OR cmd LIKE "*vssadmin*delete*shadows*" OR cmd LIKE "*shadowcopy*delete*" OR reg LIKE "*windows defender?exclusions*" OR reg LIKE "*verifiedandreputablepolicystate*" OR reg LIKE "*disablesr*" OR reg LIKE "*enablelua*" OR (cmd RLIKE "(.*[^a-z])?(eset|mcafee|malwarebytes)([^a-z].*)?" AND (cmd LIKE "*unins*" OR cmd LIKE "*/x*"))
| KEEP @timestamp, host.name, user.name, process.executable, process.command_line, registry.path, registry.data.strings
| SORT @timestampCrowdStrike
#event_simpleName=/^(ProcessRollup2|RegGenericValueUpdate|AsepValueUpdate)$/
| test(@timestamp >= 1791615600000)
| test(@timestamp < 1794207600000)
| CommandLine=/add-mppreference.*exclusion|disablerealtimemonitoring|vssadmin.*delete\s+shadows|shadowcopy.*delete/i or RegObjectName=/windows defender\\exclusions/i or RegValueName=/^(VerifiedAndReputablePolicyState|DisableSR|EnableLUA)$/i or (CommandLine=/(^|[^a-zA-Z])(eset|mcafee|malwarebytes)([^a-zA-Z]|$)/i and CommandLine=/unins|\/x/i)
| table([@timestamp, aid, #event_simpleName, FileName, CommandLine, RegObjectName, RegValueName])Microsoft KQL
union withsource=SourceTable DeviceProcessEvents, DeviceRegistryEvents
| where Timestamp >= datetime(2026-10-10T07:00:00Z) and Timestamp < datetime(2026-11-09T07:00:00Z)
| where ProcessCommandLine matches regex @"(?i)add-mppreference.*exclusion|disablerealtimemonitoring|vssadmin.*delete\s+shadows|shadowcopy.*delete"
or RegistryKey contains @"Windows Defender\Exclusions"
or RegistryValueName in~ ("VerifiedAndReputablePolicyState", "DisableSR", "EnableLUA")
or (ProcessCommandLine matches regex @"(?i)(^|[^a-zA-Z])(eset|mcafee|malwarebytes)([^a-zA-Z]|$)" and ProcessCommandLine matches regex @"(?i)unins|/x")
| project Timestamp, DeviceName, SourceTable, ActionType, InitiatingProcessFileName, ProcessCommandLine, RegistryKey, RegistryValueName, RegistryValueDataHunt 4: network contact from the wrong processes
This hunt matches the published domains muckanthropic[.]com, muckduck.duckdns[.]org, muckcoding[.]com and muckdeveloper[.]com with their subdomains, plus the IP 130.12.182[.]172. It also flags api.telegram.org lookups from non-browser processes and public connections from RegAsm.exe, dwwin.exe or dwm.exe, which the loader injects into. Doctor Web says only "Telegram bot," so that host is my inference. I defang indicators here so nobody clicks a live bad link, but the queries need real values.
Field mapping: Outside KQL, which uses RemoteIPType, public excludes private, loopback and link-local IPv4 ranges. CrowdStrike ContextBaseFileName is an assumption, so join on ContextProcessId if missing. Telegram lookups whose process name field is missing stay in the results rather than being dropped by the browser filter. Defender does not always populate RemoteUrl.
Splunk
index=YOUR_SYSMON_INDEX sourcetype="XmlWinEventLog:Microsoft-Windows-Sysmon/Operational" EventCode IN (3, 22) earliest=1791615600 latest=1794207600
| where _time >= 1791615600 AND _time < 1794207600
| eval q=lower(coalesce(QueryName, DestinationHostname, "")), proc=lower(mvindex(split(Image, "\\"), -1))
| where match(q, "(^|\.)(muckanthropic\.com|muckcoding\.com|muckdeveloper\.com|muckduck\.duckdns\.org)$") OR DestinationIp="130.12.182.172" OR (q="api.telegram.org" AND NOT in(proc, "chrome.exe", "msedge.exe", "firefox.exe", "brave.exe", "telegram.exe")) OR (EventCode=3 AND in(proc, "regasm.exe", "dwwin.exe", "dwm.exe") AND NOT cidrmatch("10.0.0.0/8", DestinationIp) AND NOT cidrmatch("172.16.0.0/12", DestinationIp) AND NOT cidrmatch("192.168.0.0/16", DestinationIp) AND NOT cidrmatch("127.0.0.0/8", DestinationIp) AND NOT cidrmatch("169.254.0.0/16", DestinationIp))
| table _time host EventCode Image QueryName DestinationIp DestinationPortElastic
FROM logs-endpoint.events.network-*
| WHERE @timestamp >= TO_DATETIME("2026-10-10T07:00:00Z") AND @timestamp < TO_DATETIME("2026-11-09T07:00:00Z")
| EVAL q = TO_LOWER(COALESCE(dns.question.name, "")), proc = TO_LOWER(COALESCE(process.name, ""))
| WHERE q RLIKE """(.*\.)?(muckanthropic\.com|muckcoding\.com|muckdeveloper\.com|muckduck\.duckdns\.org)""" OR destination.ip == TO_IP("130.12.182.172") OR (q == "api.telegram.org" AND proc NOT IN ("chrome.exe", "msedge.exe", "firefox.exe", "brave.exe", "telegram.exe")) OR (proc IN ("regasm.exe", "dwwin.exe", "dwm.exe") AND destination.ip IS NOT NULL AND NOT CIDR_MATCH(destination.ip, "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "127.0.0.0/8", "169.254.0.0/16"))
| KEEP @timestamp, host.name, process.name, process.executable, dns.question.name, destination.ip, destination.port
| SORT @timestampCrowdStrike
#event_simpleName=/^(DnsRequest|NetworkConnectIP4)$/
| test(@timestamp >= 1791615600000)
| test(@timestamp < 1794207600000)
| case {
DomainName=/(^|\.)(muckanthropic\.com|muckcoding\.com|muckdeveloper\.com|muckduck\.duckdns\.org)$/i ;
RemoteAddressIP4="130.12.182.172" ;
DomainName=/^api\.telegram\.org$/i | default(field=ContextBaseFileName, value="missing") | ContextBaseFileName!=/^(chrome|msedge|firefox|brave|telegram)\.exe$/i ;
#event_simpleName=NetworkConnectIP4 | ContextBaseFileName=/^(regasm|dwwin|dwm)\.exe$/i | cidr(RemoteAddressIP4, subnet=["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "127.0.0.0/8", "169.254.0.0/16"], negate=true) ;
}
| table([@timestamp, aid, #event_simpleName, ContextBaseFileName, ContextProcessId, DomainName, RemoteAddressIP4, RemotePort])Microsoft KQL
DeviceNetworkEvents
| where Timestamp >= datetime(2026-10-10T07:00:00Z) and Timestamp < datetime(2026-11-09T07:00:00Z)
| where RemoteUrl has_any ("muckanthropic.com", "muckcoding.com", "muckdeveloper.com", "muckduck.duckdns.org")
or RemoteIP == "130.12.182.172"
or (RemoteUrl has "api.telegram.org" and InitiatingProcessFileName !in~ ("chrome.exe", "msedge.exe", "firefox.exe", "brave.exe", "telegram.exe"))
or (InitiatingProcessFileName in~ ("regasm.exe", "dwwin.exe", "dwm.exe") and RemoteIPType == "Public")
| project Timestamp, DeviceName, ActionType, InitiatingProcessFileName, InitiatingProcessCommandLine, RemoteUrl, RemoteIP, RemotePortWhat the results mean and when to escalate
A build tool starting a shell proves only that a project ran a command. Read the project file and close it if the step matches the documented build. Escalate when the shell reached the network or wrote to Public\Pictures.
Scripts in Public\Pictures, files under Windows.Microsoft.Photos, a process running from that folder or a stray Microsoft.exe are strong evidence alone, so isolate and escalate. Escalate a hosts file change only when antivirus domains are blocked in it, since VPN clients edit it too.
I know of no business reason to exclude regasm.exe or dwm.exe from Defender, so treat that as compromise. Close other exclusions only when an admin and a change record explain them. Escalate any ESET, McAfee or Malwarebytes uninstall that no admin requested. A published domain or IP match justifies isolation. Trace unexpected Telegram lookups and RegAsm.exe, dwwin.exe or dwm.exe connections through the process tree, since explorer.exe parent spoofing means a normal tree proves nothing.
After a confirmed hit, scope needs proving. Find which credentials sat on the machine, where they were used afterward, and who else built that repository. Doctor Web has not published how the loader persists, so review all startup entries and services. A build machine missing from the coverage test is unproven.
Keep the people with the most access protected
Inception Protection helps teams review the endpoint and Microsoft telemetry behind signals like these and treats a silent sensor as a gap, not a clean result. Start with a free Inception Foresight M365 Assessment.



