Windows Defender Application Control(WDAC)을 우회하기 위한 브라우저 익스플로잇 운영

배경에 외부 모니터가 비추는 어두운 사무실에서 책상에 앉아 노트북에 기대어 있는 남성 클로즈업

작성자

Valentina Palmiotti

Head of X-Force Offensive Research (XOR)

IBM

Windows Defender Application Control(WDAC)는 무단 코드(예: 멀웨어 또는 신뢰할 수 없는 실행 파일 및 스크립트)가 시스템에서 실행되는 것을 방지하는 Windows 보안 기능입니다. 이는 명시적으로 신뢰할 수 있는 실행 파일, 스크립트 및 드라이버만 시스템에서 실행되도록 허용하는 정책을 시행하는 애플리케이션 화이트리스팅 메커니즘입니다. X-Force 레드 적대적 시뮬레이션 팀이 테스트하는 환경과 같이 보안 및 시스템 무결성이 중요한 높은 보증 환경 또는 엄격하게 제어되는 환경에서 자주 사용됩니다.

몇 주 전, 제 동료인 Bobby Cooke은 신뢰할 수 있는 Electron 애플리케이션을 백도어링하여 가장 엄격한 WDAC 정책을 우회하는 방법을 자세히 설명하는 블로그 게시물을 게시했습니다. 그의 블로그 게시물을 읽고 Electron 애플리케이션이 Node.js를 사용하는 방법과 백도어링하는 방법을 알아보는 것이 좋습니다.

이 연구의 일환으로 그는 Node.js 기반 명령 및 제어 프레임워크인 Loki C2를 오픈소스화하기도 했습니다. Loki C2를 개발한 Bobby와 Dylan Tran의 뛰어난 작업 덕분에 X-Force Adversary Simulation 팀은 WDAC를 사용하는 강화된 환경에서 교전에서 코드를 실행할 수 있었습니다.

그렇다면 이 연구의 목적은 무엇일까요? 앞서 언급한 기술에는 한 가지 단점이 있습니다. JavaScript 코드만 실행할 수 있고, DLL 로드나 EXE 실행과 같은 네이티브 코드는 실행할 수 없다는 단점이 있습니다. 또한 2단계 C2 페이로드를 발사하기 위해 셸코드를 실행할 수도 없습니다. 이 블로그 게시물에서는 이러한 제한을 우회하기 위해 사용한 기법에 대해 설명합니다.

우선 Bobby와 저는 Electron 애플리케이션에서 로드하는 서명된 Node.js 모듈을 리버스 엔지니어링하여 낮은 수준의 명령 수준 코드 실행을 부여할 수 있는 취약점을 찾기 시작했습니다. 초기 탐색과 jeffssh의 제안에 따라 Node.js와 Chrome에서 사용하는 V8 엔진으로 관심을 돌렸습니다.

취약한 애플리케이션 가져오기

Node.js 모듈에서 취약점을 찾는 대신 N-day로 V8 엔진을 악용하는 것은 어떨까요?

취약하지만 신뢰할 수 있는 바이너리를 가져와 시스템에 발판을 마련할 수 있다는 사실을 악용하는 공격 시나리오는 익숙한 공격 시나리오입니다. 이 경우 취약한 V8 버전이 포함된 신뢰할 수 있는 Electron 애플리케이션을 사용하여 main.js를 페이로드로 2단계를 실행하는 V8 익스플로잇으로 대체하고, 네이티브 셸코드를 실행합니다. 악용된 애플리케이션이 신뢰할 수 있는 기관(예: Microsoft)에 의해 화이트리스트에 포함/서명되고 일반적으로 채택된 WDAC 정책에 따라 실행이 허용되는 경우, 이는 악성 페이로드의 그릇으로 사용될 수 있습니다.

이 접근 방식은 셸코드를 자유롭게 실행할 수 있을 뿐만 아니라 브라우저와 같은 프로세스의 컨텍스트에서 셸코드를 실행할 수 있다는 이점이 있어 이점이 있습니다. JIT(Just-In-Time) 코드에 대해 RWX 메모리를 매핑하는 것과 같이 브라우저에서는 EDR에서 의심스러운 것으로 플래그가 지정될 수 있는 동작을 정상으로 보입니다.

컴퓨터를 바라보는 남성

보안 인텔리전스 강화 


매주 Think 뉴스레터에서 보안, AI 등에 대한 뉴스와 인사이트를 확인하고 위협에 한발 앞서 대처하세요. 


이전 작업

이 접근 방식은 충분히 간단해 보였지만 몇 가지 미해결 질문이 있었습니다. 공개된 Chrome V8 N-day 익스플로잇이 Electron 앱 내에서 실제로 작동하나요? Chrome에서 사용되는 V8 엔진은 Node.js의 엔진과 어떻게 다른가요? 익스플로잇에 어떤 수정이 필요하나요? 이 문제를 어떻게 디버깅할 수 있나요?

알고 보니 Electron 앱의 V8 익스플로잇에 대한 기존 공개 연구가 있었는데, 저는 매우 안타깝게도 이를 완성하기 전까지 찾지 못했습니다. Turb0는 공개 v8 익스플로잇과 해당 읽기/쓰기 프리미티브를 Electron 애플리케이션 내에서 작동하도록 조정하는 (다소 고통스러운) 프로세스를 훌륭하게 처리합니다. Turb0의 블로그 게시물에 이미 제가 겪은 일에 대한 심층적인 기술적 세부 사항이 많이 나와 있으니 꼭 확인해 보시기 바랍니다. 이 블로그 게시물의 나머지 부분에서는 WDAC 우회를 구축한다는 구체적인 목표와 실제 사용을 위해 익스플로잇을 운영하면서 발생한 문제를 대상으로 하는 익스플로잇 개발 주기의 나머지 단계에 중점을 둘 것입니다.

버전 타겟팅

가장 먼저 해야 할 일은 정확한 타겟을 파악하는 것이었습니다. 신뢰할 수 있는 Electron 애플리케이션을 선택하고 이를 악용할 취약점을 선택해야 했습니다. 이전에는 브라우저 악용 경험이 거의 없었기 때문에 선택한 취약점은 시작점으로 사용할 수 있는 공개 악용 사례가 있어야 합니다.

V8 버전이 V8 Electron이 사용하는 버전에 어떻게 매핑되는지 또는 실제로 취약한지 확인하는 방법을 알지 못했습니다. Electron의 V8 버전은 최신 버전의 Chrome V8보다 뒤처지는 경우가 많습니다. Electron 관리자는 최신 버전의 중요한 보안 패치를 특정 Electron 릴리스를 위해 고정한 버전으로 백포트합니다. 즉, Electron이 이전 버전의 V8을 사용하더라도 수정 사항이 백포팅되었을 수 있기 때문에 반드시 버그에 취약하다는 의미는 아닙니다. 그들이 적용한 엄선된 패치는 여기에 저장됩니다.

가장 쉬운 방법은 애플리케이션 버전이 출시된 후 패치된 취약점을 사용하는 것이라고 판단했습니다. 이렇게 하면 해당 앱 버전이 아직 패치될 가능성이 전혀 없었습니다. 약간의 조사 끝에 지난 2년간의 VSCode 릴리스에 대한 다운로드를 찾아냈습니다. Microsoft에서 서명한 취약한 애플리케이션을 다양하게 선택할 수 있었습니다.

빌드 및 디버깅

먼저, 최근 공개된 V8 익스플로잇 PoC를 가져와서 취약한 Electron 앱을 백도어에 넣고, main.js를 익스플로잇으로 대체하고는 그냥 지나쳤습니다. 어쩌면 그렇게 쉬울 수도 있겠죠? 적어도 충돌이 일어나기를 바랐습니다. 놀랍게도 앱을 시작했을 때 아무 일도 일어나지 않았습니다. 마지못해 저는 더 깊은 수준에서 무슨 일이 일어나고 있는지 이해하기 위해 V8을 구축해야 한다는 것을 알았습니다. V8을 직접 빌드함으로써 디버그 버전(d8)을 빌드하고, 익스플로잇의 깊이를 살펴본 다음, 대상으로 하는 특정 버전에 맞게 조정할 수 있었습니다.

언제 어디서 무엇을? - 다양한 OS 및 버전에서 익스플로잇

첫 번째 목표는 익스플로잇이 작동하는 것으로 알려진 정확한 환경을 복제하여 '근거 자료'를 확보하는 것이었습니다. 그런 다음 해당 버전과 제가 목표로 하는 버전 간의 차이점을 검토하여 무엇이 잘못되었는지 파악할 수 있었습니다.

제가 발견한 공개 V8 익스플로잇의 대부분은 Linux를 표적으로 삼았습니다. 그래서 Linux에서 V8을 컴파일하는 것부터 시작했고, 제가 선택한 공개 익스플로잇의 정확한 커밋을 확인했습니다. 그런 다음 익스플로잇을 실행하여 작동하는지 확인했습니다. 다행히도 성공했습니다. 이제 저는 제 진실을 알게 되었습니다.

거기에서 대상으로 하는 V8 버전(Electron 앱에서 사용하는 것과 동일)을 Linux에서 컴파일했습니다. 이 익스플로잇은 처음부터 제대로 작동하지 않았습니다. 프로젝트를 직접 빌드하면 필요한 만큼 코드에 대한 성찰을 할 수 있다는 장점이 있습니다. 특히 V8에는 V8 JavaScript 엔진의 독립형 셸인 d8이 있으며, 주로 브라우저 또는 Node.js 환경 외부에서 JavaScript 및 WebAssembly 코드를 테스트, 디버깅 및 실행하는 데 사용됩니다. d8에는 내부 디버그 기능이 플래그로 활성화되어 --allow-natives-syntax 있습니다. 특히,%DebugPrint(value) , 메모리 주소를 포함하여 V8 엔진 내부에 태그가 지정된 값의 내부 표현을 인쇄합니다.

이를 통해 관심 객체의 주소를 인쇄하고 공개 익스플로잇의 하드코딩된 오프셋을 조정할 수 있습니다. 이제 어느 정도 진전이 있었습니다. 제가 해야 할 일은 제 익스플로잇을 Windows로 이식하는 것뿐입니다.

Windows에서 이전 버전의 V8을 컴파일하는 것은 많은 골치 아픈 일이었습니다. 종속성 관련 문제를 많이 해결해야 했기 때문에 의심스러운 내부 코드를 수정했습니다. 이제 세부적인 내용은 기억나지 않습니다. 제 뇌가 스스로를 보호하기 위해 차단해 버렸기 때문입니다. 몇 시간 동안 고군분투한 끝에 마침내 필요한 버전을 컴파일할 수 있었습니다! 놀랍게도 Linux 수정 익스플로잇은 조정 없이 Windows에서 작동했습니다.

이제 남은 것은 Electron 앱에서 익스플로잇을 테스트하고 숨을 참는 것뿐이었습니다... 앗, 성공하지 못했습니다! 하지만 그 이유는 무엇일까요?

처음에는 타겟이 충돌했기 때문에 희망을 가졌습니다. 결국 저는 Linux 페이로드를 Windows에 맞게 조정하지 않았기 때문에 흥미로운 일이 일어날 것이라고 기대할 수 없었습니다. 동작을 확인하기 위해 익스플로잇 페이로드를 0x4141414141 주소에서 실행되도록 변경했습니다. 이것은 익스플로잇 작성자가 명령어 포인터 주소를 제어하여 프로그램에 대한 제어권을 얻었음을 확인/증명하기 위해 사용하는 일반적인 기술입니다. 그러나 WinDbg의 충돌을 살펴본 후 원하는 것을 볼 수 없었습니다. 타겟 함수 포인터를 덮어쓸 때 세그멘테이션 오류가 발생했습니다.

Electron Cherry-Picking V8이 이전에 이야기한 내용을 커밋한다는 것을 기억하시나요? 알고보니 제가 악용할 때 사용하던 버그에 앱이 취약했는데도 퍼블릭 익스플로잇이 사용한 샌드박스 이스케이프 방식은 이미 체리 픽을 통해 패치된 상태였습니다. V8 샌드박스/메모리 케이지에 대해 잘 모르신다면 여기에서 자세히 알아보세요. 본질적으로 이는 취약점이 발생할 경우 V8 악용을 더 어렵게 만드는 방법입니다.

무슨 일이 일어나고 있는지 파악하기 위해 대상 버전의 V8을 다시 구축해야 했는데, 이번에는 엄선된 패치를 적용해야 했습니다. 보안 패치 외에도 Node.js 는 Electron이 사용하는 V8 버전에 특정 Node.js 패치를 적용합니다. Electron과 Node.js가 다양한 종속성을 처리하는 방법이 명확하지 않았기 때문에 이렇게 해야 한다는 사실을 깨닫는 데 오랜 시간이 걸렸습니다.

하루나 이틀 동안 제가 컴파일하는 V8 버전이 목표와 '동일'한지 확인하고 최근 샌드박스 탈출 기법에 대해 읽은 끝에 진전을 이루었습니다. 저는 제 목표에 맞는 탈출 기술을 찾을 수 있었습니다. 익스플로잇을 조정한 후, 마침내 명령어 포인터를 제어하여 앱을 충돌시킬 수 있었습니다. 감미로운 승리였습니다. 끝이 보이는 것 같습니다...

실제 페이로드로 익스플로잇 조정하기

이 시점에서 남은 일은 대중의 익스플로잇 페이로드를 수정해 우리 C2 페이로드를 실행하는 것뿐이었습니다.  간단해 보이는 이 변경은 생각보다 성가신 일이었습니다. 공개 익스플로잇의 Linux 페이로드는 몇 바이트 크기에 불과한 셸을 터뜨리는 간단한 것이었습니다. C2의 페이로드는 그보다 훨씬 더 컸습니다.

셸코드로 코딩하는 것에 대해 알고 있다면 Windows 셸코드를 작성하는 것이 Linux의 셸코드보다 더 번거롭다는 것을 알 것입니다. Linux에서처럼 위치 독립적인 방식으로 직접 시스템 호출을 할 수 있는 간단한 방법이 없기 때문입니다. 페이로드는 또한 부동 소수점 배열 안에 'JOP 밀수'되어야 했습니다.

부동 소수점 배열에서 JOP 밀반입을 보여주는 코드, 셸코드는 /bin/sh를 실행합니다.
그림 1: 부동 소수점 배열에서 JOP 밀반입, 셸코드가 /bin/sh 실행

수천 바이트에 달하는 전체 C2 단계 페이로드는 당연히 이렇게 실행할 수 없습니다. 그래서 실행 가능한 페이지를 매핑하고 최종 페이로드를 복사한 다음 해당 페이지로 이동하는 부트스트랩 페이로드를 작성해야 했습니다.

인수 밀수

부트스트랩 페이로드의 문제는 프로그램 제어 권한이 있는 동안 실행된 페이로드에 인수를 전달할 방법이 없다는 것입니다. 따라서 내가 밀수한 셸코드는 복사할 최종 페이로드의 주소를 알 수 없습니다. 저는 이 문제를 제가 "논리 밀수"라고 명명한 방법으로 해결했습니다.

덮어 쓴 JSFunction 객체의 주소가 rcx 레지스터에 저장된다는 것을 알고 있었습니다. 그래서 임의의 쓰기 원시 함수를 사용해, 매핑된 페이지를 필요 없는 객체의 필드 중 하나에 저장했습니다. 일부 오프셋을 덮어쓰면 충돌이 발생했기 때문에 약간의 시행착오가 필요했습니다. 복사할 값과 복사할 오프셋에 대해서도 같은 작업을 했습니다. 필드의 오프셋을 셸코드에 하드코딩하여 페이로드를 복사할 위치를 알 수 있습니다. 페이로드를 n번 호출했는데, 여기서 n은 복사할 바이트 수입니다.

밀수된 셸코드의 코드 스니펫

TurboFan JIT 최적화

V8의 최적화 컴파일러인 TurboFan은 제 계획에 어려움을 주었습니다. TurboFan의 최적화로 인해 동일한 값의 여러 부동 소수점으로 변환된 명령어 시퀀스를 밀수하면 메모리에 해당 값의 인스턴스가 하나만 생성됩니다. 이로 인해 지침을 반복할 수 있는 빈도에 제한이 생겼습니다. 셸코드를 최대한 컴팩트하게 만들고, 명령을 절대적으로 반복해야 하는 경우에는 밀수된 명령의 위치를 변경하여 부동 소수점 값이 달라지고 반복 항목이 없도록 함으로써 이 문제를 해결했습니다.

또한 2단계 페이로드가 너무 큰 경우 셸코드를 복사하는 데 문제가 발생했습니다. 아마도 이를 최적화하기 위해 동일한 JSFunction과 TurboFan을 호출해야 하는 횟수가 많기 때문일 것입니다. 결국 하나의 큰 루프가 아닌 'WriteShellcode'에 여러 루프를 복사하여 붙여넣어 이 문제를 해결했습니다. 끔찍하게 못생겼지만 효과가 있었습니다! 나중에 Bobby와 Dylan은 C2 페이로드를 스토리지에서 더 큰 페이로드를 검색하는 스테이저로 교체했기 때문에 최종 페이로드를 디스크에 저장할 필요가 없었습니다. 이는 또한 main.js의 파일 크기를 합리적인 수준으로 유지하는 데 도움이 되었습니다.

일관성 없는 오프셋

익스플로잇의 실제 운영에 대비하려면 항상 다양한 환경에 대한 테스트를 포함해야 합니다. 공격 당시에는 페이로드가 어떤 환경에서 실행될지 알 수 없었지만, WDAC이 활성화된 Windows 시스템일 가능성이 높다는 점만 알 수 있었습니다. 그러므로 이 공격은 OS에 관계없이 작동해야 합니다. V8 버전과 모든 종속성이 앱 내에 포함되어 있기 때문에 변동성이 크지 않을 것이라고 확신했습니다. 그 가정은 틀렸습니다.

제가 이해할 수 없는 이유로 덮어쓰기에 취약한 함수 포인터의 오프셋이 Windows 버전에 따라 변경되었습니다. 제가 알기로는 오프셋 거리는 애플리케이션 패키지에서 라이브러리가 직접 로드되는 V8 JIT 엔진에 의해 결정되기 때문에 이해가 되지 않았습니다. 즉, OS에 관계없이 정확히 동일한 V8 라이브러리가 로드됩니다. 문제를 더욱 혼란스럽게 하는 것은, 그 변화가 어떤 패턴도 따르지 않는 것처럼 보인다는 점입니다. 일부 Windows 버전(이전 버전과 최신 버전 모두)에서 오프셋이 4바이트씩 벗어나는 경우가 있었습니다. 특히 짜증났던 점은, (제가 보기엔) JavaScript 취약점 내에서 적절한 오프셋을 얻을 방법이 없었기 때문입니다. 이를 계산하는 유일한 방법은 디버깅 셸을 사용하여 메모리 주소를 읽고 계산을 수행하는 것이었는데, 이는 프로덕션 Electron 애플리케이션 내에서는 분명히 옵션이 아니었습니다. TLDR: 익스플로잇 런타임에는 오프셋 변동을 계산할 수 없습니다.

JIT(Just-In-Time) 익스플로잇 엔지니어링

일관되지 않은 오프셋 문제를 해결하기 위해 Bobby와 Dylan은 main.js가 공격을 여러 번 시작하고 성공할 때까지 가능한 다양한 오프셋을 시도하도록 공격을 재설계했습니다. 이는 초기 코드 프로세스가 루프를 수행하도록 하여 이루어졌습니다. 이 루프는 고유한 오프셋으로 악용을 시도하는 자식 프로세스를 생성합니다. 만약 공격이 실패하면 자식 프로세스는 종료됩니다. 익스플로잇이 성공하면 셸코드가 실행되어 2단계 C2를 배포하기 전에 Mutex 파일을 작성합니다. 일단 공격이 성공하면, 초기 프로세스는 루프에서 빠져나와 영원히 잠자게 됩니다.

조작된 익스플로잇 실행 흐름을 보여주는 흐름 차트
그림 2: 조작된 익스플로잇 실행 흐름

이는 잘못된 오프셋 시도로 인해 충돌이 발생한다는 것을 의미하지만, 테스트 결과 사용자에게는 눈에 띄는 오류가 없었고 애플리케이션 기능은 여전히 원활하게 작동하는 것으로 나타났습니다. 가장 깔끔한 해결책은 아니었고 충돌로 인해 다소 시끄러웠지만 시간이 가장 중요했습니다. 이를 우리는 비즈니스에서 'JIT xdev'라고 부르며, 우리의 요구 사항에 완벽하게 부합했습니다.

JS 난독화 및 CI/CD 페이로드

우리는 우리가 발견되어 누군가 애플리케이션의 main.js 진입점을 분석하는 경우 악용이 분명해지는 것을 원하지 않았습니다. 이를 방지하기 위해 익스플로잇 코드에 JavaScript Obfuscator를 적용하여 사람의 눈으로는 사실상 이해할 수 없게 만들었습니다. 팀의 페이로드 CI/CD 파이프라인을 관리하는 Chris Spehn의 재능과 헌신 덕분에 이 페이로드의 전달을 간소화하고 페이로드가 생성될 때마다 코드를 다시 난독화하여 매번 다른 익스플로잇 코드를 사용하여 애플리케이션을 무기한 재사용할 수 있었습니다. 이렇게 하면 페이로드가 서명되지 않습니다. 안타깝게도 처음 해당 기능을 사용하려고 했을 때 사용자가 피싱 이메일을 신고하여 적발되었기 때문에 이 기능은 특히 유용했습니다🙁. 흥미롭게도 고객의 블루팀은 피싱 이메일에서 애플리케이션을 분석했지만 애플리케이션의 목적을 알아내지 못했고 내장된 V8 익스플로잇을 식별하지도 못했습니다.

남아있는 미해결 질문

모든 관련 V8 라이브러리가 Electron 애플리케이션 내에 번들로 제공되어야하기 때문에 JITD 함수 오프셋이 OS에 따라 달라지는 이유를 아직 잘 이해하지 못합니다. 그 이유를 아시는 분이 계시면 알려주세요!

향후 보안 고려 사항

Electron은 런타임에 모든 애플리케이션 파일의 무결성을 확인하는 실험적인 무결성 기능을 출시했습니다. macOS에서는 버전 16부터, Windows에서는 버전 30부터 사용할 수 있습니다. 애플리케이션 개발자는 이 Electron 퓨즈를 활성화하여 애플리케이션 파일이 변조되지 않도록 할 수 있습니다. 이 경우 프로세스가 자동으로 종료되고 아무 것도 실행되지 않습니다.

이 기능은 main.js를 포함하여 Electron 앱의 패키지 파일을 수정하는 것을 방지하고 논의된 기술을 방해합니다. 그러나 가장 많이 사용되는 애플리케이션에는 아직 구현되지 않았습니다. 이 기능이 더 널리 사용되더라도 이전 버전의 애플리케이션인 사전 무결성 퓨즈는 여전히 취약한 상태로 이러한 공격에 사용할 수 있다는 점에 유의해야 합니다.

감사의 말

Bobby Cooke, Dylan Tran - 익스플로잇 운영 지원

Dylan Tran - 다이어그램 생성

Chris Spehn - 이 페이로드를 CI/CD 파이프라인에 통합(및 팀을 위해 수행한 다른 모든 DevOps 작업)

jeffssh - 영감

jj - 많은 V8 PoC를 통해 큰 도움을 받은 마스터 V8 해커

Mixture of Experts | 12월 12일, 에피소드 85

AI 디코딩: 주간 뉴스 요약

세계적인 수준의 엔지니어, 연구원, 제품 리더 등으로 구성된 패널과 함께 불필요한 AI 잡음을 차단하고 실질적인 AI 최신 소식과 인사이트를 확인해 보세요.

관련 솔루션
IBM Guardium

가장 중요한 데이터를 보호하세요. 환경 전반에서 민감한 정보를 탐색, 모니터링 및 보호하는 동시에 규정 준수를 자동화하고 위험을 줄이세요.

IBM Guardium 살펴보기
엔터프라이즈 보안 솔루션

세계 최대 엔터프라이즈 보안 제공업체의 솔루션을 사용해 보안 프로그램을 혁신하세요.

    보안 솔루션 살펴보기
    사이버 보안 서비스

    사이버 보안 컨설팅, 클라우드 및 관리형 보안 서비스를 통해 비즈니스를 혁신하고 위험을 관리하세요.

    사이버 보안 서비스 살펴보기
    다음 단계 안내

    클라우드 및 온프레미스 환경 전반에서 기업을 보호하기 위해 데이터 보호, 위협 탐지 및 규정 준수를 자동화하세요.

    1. IBM Guardium 살펴보기
    2. 사이버 보안 솔루션 알아보기