The Notepad++ Supply Chain Breach: Technical Overview and Response for the Chrysalis Backdoor
Author
Joshua Rogers
Date Published

The Bottom Line: Between June and December 2025, multiple independent reports described a highly selective Notepad++ update hijack affecting a small number of targets. Reports attribute the attack to the Chinese APT group Lotus Blossom, and the attack was used to deliver a custom backdoor to victims which has been named Chrysalis. The operational pattern fits a precision strike, with security professionals describing impacts primarily across Southeast Asia. In this post, we discuss the vulnerabilities which lead up to this incident, and include detection strategies with IOCs, KQL queries, at the bottom of this post.
In early February 2026, Notepad++ developers publicly stated the update infrastructure compromise stemmed from a hosting-provider-level incident (June to September 2025), with attacker access to internal services persisting until December 2025. Kaspersky then published a telemetry-driven breakdown showing the operators rotated delivery chains, staging infrastructure, and payloads over roughly July to October 2025, and included "previously unpublished IoCs" that expand defender coverage beyond the first public writeups.
Two things made the whole chain possible, and they are worth stating plainly up front:
- The shared hosting used by the Notepad++ update infrastructure was compromised, so the attacker could selectively change what the update server returned (for example, returning a different download URL only to targeted organizations).
- The signing trust model was brittle because the auto-updater did not properly verify authenticity of the update payload. In addition to this, following an update to Notepad++ in July (v8.8.2) until the newest release (v8.8.9), Notepad++ was deliberately changed to use a self-signed certificate whose private key was public in its GitHub repository. Notepad++ moved back to a DigiCert certificate with proper verification after the attack.
Immediate Action for Security Teams
- Update Now: Deploy Notepad++ v8.8.9 or later. This hardens the update path by verifying both the digital signature and the certificate of downloaded installers during the update process, aborting on failure.
- Audit Your Fleet: Identify all instances of Notepad++ and move toward managed deployments rather than allowing in-app auto-updates.
- Scan for Intrusion: Use the IOCs and KQL queries at the bottom of this page to hunt for the Chrysalis tooling and for anomalous updater behavior.
If your environment has Windows desktops, you likely have Notepad++ installed somewhere. In 2025, that ubiquity became a tactical entry point. This incident did not need a user to click a suspicious link. It used the software update mechanism itself.
The reporting that makes this incident worth studying is the selectivity. In the cases described by responders, most users received normal update behavior, while a small set of organizations saw update-related processes preceding hands-on-keyboard activity. That selectivity kept the signal weak in most enterprise telemetry and slowed down community consensus about what was real.
This also reflects a common modern supply chain tradeoff: instead of poisoning source code, compromise the infrastructure that answers a simpler question: "Where should this updater download the next executable from?"
Kaspersky's Securelist quantified just how selective this was from their vantage point: three distinct infection chains across roughly a dozen machines, belonging to:
- Individuals located in Vietnam, El Salvador, and Australia
- A government organization located in the Philippines
- A financial organization located in El Salvador
- An IT service provider organization located in Vietnam
That victim mix is a useful reminder: even when an intrusion set is strongly associated with a region, supply chain access can produce out-of-pattern geography when targets travel, work remotely, or share upstream infrastructure.
The Mechanics of a Selective Redirect
Notepad++ updates via WinGUp (commonly gup.exe). The basic flow is:
gup.execallshttps://notepad-plus-plus.org/update/getDownloadUrl.phpwith the current version.- The server returns
gup.xml, which includes a<Location>URL for the installer. - The installer is downloaded into
%TEMP%and executed.
The weak point is not a vulnerability in the application itself. It is the decision step that supplies the <Location> URL. If an attacker can influence that XML response, they can redirect the updater to an attacker-controlled server.
The key operational detail is how this can be done selectively. In their article, Rapid7 describes a compromise of the infrastructure hosting Notepad++, and notes the confirmed chain: execution of notepad++.exe, then GUP.exe, then a suspicious update.exe downloaded from 95.179.213.0. If the update infrastructure is compromised, an attacker can return malicious XML only to requests from specific source IP ranges (for example, target organizations), while returning benign XML to everyone else. No TLS interception is required if the server itself is the one sending the malicious response.
Rapid7 are not the only that have reported on this incident. Way back in December, Kevin Beaumont reported on these security woes, and the security issues which allowed for this attack to occur. Kaspersky's writeup is also valuable for understanding the operator playbook around it: the attackers repeatedly changed the entire execution chain (and not just the hosting) about once a month across July to October 2025. More information about that can be read in their post.
The Signing Trap and Brittle Trust
Two separate trust assumptions failed in ways defenders should internalize:
1) "It is HTTPS so it cannot be altered." If the update endpoint or shared hosting is compromised, the attacker can return malicious XML over a valid TLS connection. HTTPS protects transport from tampering in transit, not integrity of a server that has already been modified.
2) "The updater verifies signatures so the downloaded file is safe." Signature verification answers “is the signature valid.” It does not answer “is this signed by the identity we intended to trust.”
Indeed, Notepad++'s signature verification did not actually verify the code signing's certificate (this has been fixed in v8.8.9). To make matters worse, Notepad++ deliberately moved from a private DigiCert certificate for its code signing, to a public (hosted on GitHub) certificate for codesigning, allowing anybody to sign code appearing to come from the Notepad++ developers. Ironically, this was mentioned in the July-release's announcement:
There were - and still are - many false-positives reported in the previous version v8.8.2, by the antivirus software due to the absence of Windows code signing certificate. To prevent this issue from recurring in future releases, from this version the Notepad++ release is signed with a certificate issued by a self-signed Certificate Authority (CA). The root certificate is published on the Notepad++ website, GitHub repository & Notepad++ User Manual, allowing antivirus vendors, IT teams and users to verify the authenticity of each release.
Because removing the signing by the DigiCert resulted in so many security reports, Notepad++ moved to the self-signed certificate (instead of no signing at all), whose private key was hosted in GitHub. This resulted in zero net benefit in terms of security.
What the Malware Did: The Chrysalis Backdoor
Rapid7 also provided a deep reverse engineering of the malware which was used in this attack. The operationally relevant part is the execution chain and the few stable artifacts it produces. They confirmed that the suspicious update.exe was an NSIS installer that dropped a DLL sideloading setup into a hidden %AppData%\Bluetooth directory.
The chain:
- The wrapper:
update.exe(NSIS installer) runs. - Hidden directory: Creates
%AppData%\Bluetooth, sets it to hidden. - Sideload host + DLL: Drops a renamed Bitdefender binary (
BluetoothService.exe) and a maliciouslog.dll, plus an encrypted payload blob namedBluetoothService. - Execution:
BluetoothService.exeloadslog.dll(DLL sideloading), which decrypts and executes shellcode in memory that becomes the Chrysalis backdoor.
Once active, Chrysalis is a feature-rich backdoor that performs host discovery and communicates with C2 over HTTPS using WinINet.
The AI Slop Problem: Why the Signal Got Buried
This incident landed in an ecosystem saturated with low-quality, LLM-generated "security research": bogus vulnerability claims, hallucinated screenshots, and GitHub issue spam that had little to do with the actual update hijack. This is directly relevant and points out that prior "supply chain" claims about Notepad++ were sometimes nonsense DLL sideloading stories that did not reflect a real upstream compromise. Indeed, in October, Notepad++ discussed these incorrect security signals, discussing how a public CVE was "one of the most absurd entries we've ever seen".
There is also a human cost that matters for outcomes. Spam reports and false positives create fatigue for maintainers: attention gets dragged toward debunking, issue triage queues swell, and real incident communication slows down. Over time, developers learn to distrust inbound security reports, which makes the next real report harder to surface quickly. In incidents like this, that is not just annoying, it is a risk. AISLE cuts the noise, and actually adds values for developers and security teams looking to secure their systems.
AISLE is built to fix the reliability gap of other security products. Our product is two-fold and end-to-end: first, it automatically ingests third-party security tooling findings into a single system of record, verifying which findings are true positives with auto-triage, and automatically fixing them. Secondly, we also find new true positives with our AI-based analyzer. Combined, this allows developers to make good decisions, and more secure. In January 2026, AISLEs autonomous analyzer identified all 12 vulnerabilities disclosed in OpenSSLs coordinated release and recommended fixes that were incorporated upstream, a concrete example of high-signal AI improving outcomes while the broader ecosystem is flooded with low-quality reports.
For defenders, the practical takeaway is simple: treat community claims as leads, but prioritize telemetry-backed signals. Hashes, process ancestry, file paths, and network destinations are what you can validate quickly and at scale.
Indicators of Compromise (IOCs)
All of the values below come from Rapid7’s analysis, Kaspersky's analysis, and X users UK_Daniel_Card and cyb3rops.
File hashes (SHA-256)
Observed drop components extracted from the NSIS installer:
- [NSIS].nsi (installation script):
8ea8b83645fba6e23d48075a0d3fc73ad2ba515b4536710cda4f1f232718f53e - BluetoothService.exe (renamed Bitdefender Submission Wizard used for sideloading):
2da00de67720f5f13b17e9d985fe70f10f153da60c9ab1086fe58f069a156924 - BluetoothService (encrypted shellcode blob):
77bfea78def679aa1117f569a35e8fd1542df21f7e00e27f192c907e61d63a2e - log.dll (malicious DLL):
3bdc4c0637591533f1d4198a72a33426c01f69bd2e15ceee547866f65e26b7ad
Hashes from Kaspersky's analysis:
JavaScript18e6e505438c21f3d281e1cc257abdbf7223b7f5a290e677d7ff5844407b9c073e3b7e896e078e11cd3573549869e84544e3ef253bdba79851dcde4963a413179c8f19fbf3d8473c49983a199e6cb4f318f054c9aac447bf732acc97992290aa7a187b967ee2c6821c0cafb2aab0f063ef7e313f64313fc81d46cd706a6a5a39193075734a32e0235bde0e979c2722889c3ba38890ed984a25abb6a094b5dbf052f22fa79ca4b6fe0c69472cd3d63b212eb805b7f65710d33100d0f315fd8cf408a483f8e2dd1e69422629ed9fd112a476cfb85fbf012fdbe63a37642c11afa5cf020
Network indicators
- Confirmed download source for suspicious
update.exe:95.179.213.0,install.exe:95.179.213.0,AutoUpdater.exe:95.179.213.0 - C2 domain:
api.skycloudcenter.com - Additional domains:
api.wiresguard.com,cdncheck.it.com,safe-dns.it.com - C2-related IPs often used for sweeping hunts:
61.4.102.97,59.110.7.32,124.222.137.114,45.76.155.202,95.179.213.0
Detection Queries
The queries below assume Microsoft Defender for Endpoint advanced hunting tables (DeviceProcessEvents, DeviceFileEvents, DeviceNetworkEvents, DeviceTvmSoftwareInventory).
0) Quick fleet inventory: where is Notepad++ installed?
This finds endpoints where processes executed from a Notepad++ folder path.
JavaScript1DeviceProcessEvents2| where TimeGenerated > ago(90d)3| where FolderPath has "notepad++"4| summarize ProcEvents=count(), FirstSeen=min(TimeGenerated), LastSeen=max(TimeGenerated) by DeviceName5| order by ProcEvents desc
To enrich that with software inventory (version, vendor), use the join pattern:
JavaScript1DeviceProcessEvents2| where TimeGenerated > ago(90d)3| where FolderPath has "notepad++"4| summarize ProcEvents=count(), AnySHA256=any(SHA256), AnyFile=any(FileName) by DeviceId, DeviceName, AccountName5| join kind=leftouter (6 DeviceTvmSoftwareInventory7 | where SoftwareName has "notepad++"8 | project DeviceId, SoftwareName, SoftwareVersion, SoftwareVendor9) on DeviceId10| project DeviceName, AccountName, SoftwareName, SoftwareVersion, SoftwareVendor, ProcEvents, AnyFile, AnySHA25611| order by ProcEvents desc
1) Hunt for the Chrysalis Bluetooth directory and dropped files
This looks for the specific residency path and filenames.
JavaScript1DeviceFileEvents2| where TimeGenerated > ago(180d)3| where FolderPath has @"\AppData\Roaming\Bluetooth"4| where FileName in~ ("BluetoothService.exe", "log.dll", "BluetoothService")5| project TimeGenerated, DeviceName, ActionType, FileName, FolderPath, SHA256,6 InitiatingProcessFileName, InitiatingProcessFolderPath, InitiatingProcessCommandLine7| order by TimeGenerated desc
2) Hunt for file hash IOCs directly
JavaScript1let Rapid7_SHA256 = dynamic([2 "8ea8b83645fba6e23d48075a0d3fc73ad2ba515b4536710cda4f1f232718f53e",3 "2da00de67720f5f13b17e9d985fe70f10f153da60c9ab1086fe58f069a156924",4 "77bfea78def679aa1117f569a35e8fd1542df21f7e00e27f192c907e61d63a2e",5 "3bdc4c0637591533f1d4198a72a33426c01f69bd2e15ceee547866f65e26b7ad"6]);78// Securelist (Kaspersky) published SHA1s for multiple chains (malicious updaters + dropped components)9let Kaspersky_SHA1 = dynamic([10 // Malicious update.exe (NSIS) samples11 "8e6e505438c21f3d281e1cc257abdbf7223b7f5a",12 "90e677d7ff5844407b9c073e3b7e896e078e11cd",13 "573549869e84544e3ef253bdba79851dcde4963a",14 "13179c8f19fbf3d8473c49983a199e6cb4f318f0",15 "4c9aac447bf732acc97992290aa7a187b967ee2c",16 "d7ffd7b588880cf61b603346a3557e7cce648c93",17 "821c0cafb2aab0f063ef7e313f64313fc81d46cd",1819 // Chain #3 dropped components (Bluetooth sideload set)20 "21a942273c14e4b9d3faa58e4de1fd4d5014a1ed", // BluetoothService.exe (legit)21 "f7910d943a013eede24ac89d6388c1b98f8b3717", // log.dll22 "7e0790226ea461bcc9ecd4be3c315ace41e1c122", // BluetoothService (encrypted shellcode)2324 // Chain #1 dropped components (ProShow staging)25 "defb05d5a91e4920c9e22de2d81c5dc9b95a9a7c", // ProShow.exe26 "259cd3542dea998c57f67ffdd4543ab836e3d2a3", // defscr27 "46654a7ad6bc809b623c51938954de48e27a5618", // if.dnt28 "9df6ecc47b192260826c247bf8d40384aa6e6fd6" // proshow_e.bmp29]);3031DeviceFileEvents32| where TimeGenerated > ago(365d)33| where (SHA256 in~ (Rapid7_SHA256)) or (SHA1 in~ (Kaspersky_SHA1))34| extend MatchedHash = case(35 SHA256 in~ (Rapid7_SHA256), SHA256,36 SHA1 in~ (Kaspersky_SHA1), SHA1,37 ""38 )39| project TimeGenerated, DeviceName, ActionType, FileName, FolderPath, SHA1, SHA256, MatchedHash,40 InitiatingProcessFileName, InitiatingProcessCommandLine41| order by TimeGenerated desc
3) Identify suspicious updater behavior: gup.exe writing or executing unexpected installers from %TEMP%
Look for files like update.exe and AutoUpdater.exe in %TEMP% which were used in the update chain, especially when written or executed by gup.exe.
JavaScript1DeviceProcessEvents2| where TimeGenerated > ago(180d)3| where InitiatingProcessFileName =~ "gup.exe"4| where FolderPath has @"\Temp\" or ProcessCommandLine has @"\Temp\"5| where FileName in~ ("update.exe", "AutoUpdater.exe")6| project TimeGenerated, DeviceName, AccountName, InitiatingProcessFileName,7 FileName, FolderPath, ProcessCommandLine, InitiatingProcessCommandLine8| order by TimeGenerated desc
To catch gup.exe writing these files:
JavaScript1DeviceFileEvents2| where TimeGenerated > ago(180d)3| where InitiatingProcessFileName =~ "gup.exe"4| where FolderPath has @"\Temp\"5| where FileName in~ ("update.exe", "AutoUpdater.exe")6| project TimeGenerated, DeviceName, ActionType, FileName, FolderPath, SHA256,7 InitiatingProcessFileName, InitiatingProcessCommandLine8| order by TimeGenerated desc
4) Detect gup.exe network egress outside expected destinations
gup.exe should not be making arbitrary outbound connections. This query looks for gup.exe connections not involving known-good domains used by the updater path.
JavaScript1DeviceNetworkEvents2| where TimeGenerated > ago(180d)3| where InitiatingProcessFileName =~ "gup.exe"4| where ActionType == "ConnectionSuccess"5| extend RemoteHost = tostring(parse_url(RemoteUrl).Host)6| where isnotempty(RemoteHost)7| where RemoteHost !in~ (8 "notepad-plus-plus.org",9 "github.com",10 "release-assets.githubusercontent.com"11)12| project TimeGenerated, DeviceName, AccountName, RemoteUrl, RemoteIP, RemotePort,13 InitiatingProcessFileName, InitiatingProcessFolderPath, InitiatingProcessCommandLine14| order by TimeGenerated desc
5) Detect connections to known Chrysalis infrastructure
Perform a simple sweep for the IPs often used for first-pass scoping, or C2 domains.
JavaScript1let SuspiciousIPs = dynamic([2 // Rapid7 sweep IPs + Securelist/Kaspersky infra3 "95.179.213.0",4 "61.4.102.97",5 "59.110.7.32",6 "124.222.137.114",7 "45.76.155.202",8 "45.32.144.255",9 "45.77.31.210"10]);1112let SuspiciousDomains = dynamic([13 // Rapid7 / Chrysalis infra14 "api.skycloudcenter.com",15 "api.wiresguard.com",1617 // Securelist/Kaspersky infra used in chains #1/#218 "self-dns.it.com",19 "cdncheck.it.com",20 "safe-dns.it.com",2122 // Called out as unusual DNS/UA artifact in chains #1/#223 "temp.sh"24]);2526DeviceNetworkEvents27| where TimeGenerated > ago(365d)28| where ActionType == "ConnectionSuccess"29| extend RemoteHost = tostring(parse_url(RemoteUrl).Host)30| where RemoteIP in (SuspiciousIPs)31 or RemoteHost in~ (SuspiciousDomains)32 or RemoteUrl has_any (SuspiciousDomains)33| project TimeGenerated, DeviceName, AccountName, RemoteUrl, RemoteHost, RemoteIP, RemotePort,34 InitiatingProcessFileName, InitiatingProcessCommandLine35| order by TimeGenerated desc
If you want to be more specific to the C2 URL shape Rapid7 shows (/a/chat/s/<guid>):
JavaScript1DeviceNetworkEvents2| where TimeGenerated > ago(365d)3| where ActionType == "ConnectionSuccess"4| where RemoteUrl has "api.skycloudcenter.com/a/chat/s/"5| project TimeGenerated, DeviceName, AccountName, RemoteUrl, RemoteIP,6 InitiatingProcessFileName, InitiatingProcessCommandLine7| order by TimeGenerated desc
6) Known-good baseline: compare endpoint hashes to official Notepad++ release hashes
A collector repo that extracts and aggregates official Notepad++ release hashes is available, as well as a KQL pattern to ingest the CSV as a lookup table using externaldata. The repository can be found at https://github.com/Neo23x0/notepad-plus-plus-hashes.
In some environments, externaldata over raw GitHub works. In others, it will fail due to tenant restrictions. If it fails, download the CSV internally and host it somewhere your Sentinel/Defender environment can read.
Step 1: Load the release hash table
JavaScript1let NotepadHashes =2externaldata(3 version_tag: string,4 release_title: string,5 release_date: datetime,6 prerelease: bool,7 release_url: string,8 hash_algorithm: string,9 hash_value: string,10 inferred_asset_name: string,11 source_location: string,12 checksum_asset_name: string,13 checksum_asset_url: string14)15[h@"https://raw.githubusercontent.com/Neo23x0/notepad-plus-plus-hashes/refs/heads/main/notepadpp_release_hashes.csv"]16with(format="csv", ignoreFirstRecord=true);17NotepadHashes
Step 2: Pull observed Notepad++-related file hashes from endpoints
JavaScript1let ObservedNpp =2DeviceFileEvents3| where TimeGenerated > ago(180d)4| where FolderPath has "Notepad++" or FileName has "notepad++"5| where isnotempty(SHA256)6| summarize LastSeen=max(TimeGenerated) by DeviceName, FileName, FolderPath, SHA256;7ObservedNpp
Step 3: Flag binaries that do not match known-good release hashes
JavaScript1let NotepadHashes =2externaldata(3 version_tag: string,4 release_title: string,5 release_date: datetime,6 prerelease: bool,7 release_url: string,8 hash_algorithm: string,9 hash_value: string,10 inferred_asset_name: string,11 source_location: string,12 checksum_asset_name: string,13 checksum_asset_url: string14)15[h@"https://raw.githubusercontent.com/Neo23x0/notepad-plus-plus-hashes/refs/heads/main/notepadpp_release_hashes.csv"]16with(format="csv", ignoreFirstRecord=true);1718let KnownGoodSHA256 =19NotepadHashes20| where hash_algorithm =~ "SHA256"21| project KnownGoodSHA256 = tolower(hash_value);2223let ObservedNpp =24DeviceFileEvents25| where TimeGenerated > ago(180d)26| where FolderPath has "Notepad++" or FileName has "notepad++"27| where isnotempty(SHA256)28| summarize LastSeen=max(TimeGenerated) by DeviceName, FileName, FolderPath, SHA256_lower=tolower(SHA256);2930ObservedNpp31| join kind=leftanti KnownGoodSHA256 on $left.SHA256_lower == $right.KnownGoodSHA25632| project DeviceName, FileName, FolderPath, SHA256=SHA256_lower, LastSeen33| order by LastSeen desc
Note: this is not a malware detector by itself. It is a fast way to surface outliers (trojanized installers, tampered binaries, or local repacks) so you can prioritize deeper review.
Summary and Next Steps
This incident is a forcing function for corporate security policy. It requires a clear decision: do you want a self-updating desktop utility to have direct code-execution reach across your fleet, or do you want updates to flow through managed deployment controls?
Practical next steps:
- Move to managed deployments (Intune, SCCM, your software catalog) and treat in-app updaters as exceptions that require explicit review.
- Constrain updater egress: consider blocking
gup.exefrom direct internet access, or at minimum alert ongup.execonnections outside expected domains. - Baseline and hunt: use the file and network IOCs for direct matching, then use the release-hash baseline to catch unusual Notepad++ artifacts that do not belong on endpoints.