JavaScript DRM의 허상: HotAudio 복사 보호를 3라운드 만에 무력화한 과정
(therantydev.com)- 브라우저에서 실행되는 JavaScript 기반 DRM은 복호화된 오디오 데이터가 결국 JavaScript 접근 가능한 영역을 통과해야 하므로 근본적으로 우회 가능함
- HotAudio는 NSFW ASMR 오디오 호스팅 플랫폼으로, MediaSource Extensions API를 활용한 자체 암호화·청크 전송 방식의 복사 보호를 구현
- 개발자의 반복적인 패치(전역 변수 제거, 해시 검증,
.toString()무결성 검사, iframe/Shadow DOM 격리)에 대해 공격자가 매번 프로토타입 후킹과 위장 기법으로 대응하는 3단계 공방 기록 - 실질적인 DRM은 Trusted Execution Environment(TEE) 기반의 하드웨어 보호(Widevine, FairPlay 등)가 필요하나, 소규모 플랫폼은 라이선스 비용과 인프라 문제로 접근 불가
- JavaScript DRM은 일반 사용자에게는 유효한 마찰(friction) 역할을 하지만, 숙련된 공격자를 막을 수 없으므로 "DRM"이라 부르기엔 기대치와 현실 사이에 큰 괴리 존재
배경: HotAudio와 JavaScript DRM의 태생적 한계
- HotAudio는 NSFW ASMR 오디오 호스팅 사이트로, 크리에이터를 위한 DRM 보호 기능을 제공한다고 주장하는 플랫폼
- 기존 Soundgasm, Mega 등의 호스팅 서비스가 ToS 강화로 제한되면서 대안 플랫폼으로 등장
- 개발자 fermaw가 Reddit에서 DRM 구현을 "재미있었다"고 언급한 것이 분석의 시작점
- JavaScript 코드는 본질적으로 "userland" 영역에 존재하며, 사용자가 접근·수정 가능한 코드를 배포하는 구조
- 아무리 정교한 키, nonce, 암호화 파일 포맷을 사용해도 결국 JavaScript 복호화 로직을 거친 데이터는 평문 상태로 브라우저 오디오 엔진에 전달되어야 함
Trusted Execution Environment(TEE)의 역할
- Microsoft 정의에 따르면 TEE는 "암호화로 보호된 CPU와 메모리의 격리 영역"으로, 외부 코드가 내부 데이터를 읽거나 변조할 수 없는 구조
- TEE는 하드웨어 기반 보안 영역(ARM TrustZone, Intel SGX 등)이며, 그 위에서 Content Decryption Module(CDM) 인 Widevine, FairPlay, PlayReady가 동작
- 이들 CDM은 암호화 키와 복호화된 미디어 버퍼가 호스트 OS에 노출되지 않도록 보장
- Widevine 라이선스 취득에는 Google과의 라이선스 계약, 네이티브 바이너리 통합, 인프라, 법적 절차, 상당한 비용이 필요
- 소규모 NSFW 오디오 플랫폼이 Widevine 라이선스를 확보하는 것은 현실적으로 불가능
HotAudio의 구현 방식과 "PCM 경계"
- HotAudio는 오디오를 암호화된 형태로 전송하고, MediaSource Extensions(MSE) API를 통해 청크 단위로 복호화·재생하는 JavaScript 기반 커스텀 복호화 방식 채택
- 이 방식은 일반 사용자의 우클릭 저장이나 네트워크 탭에서의 직접 다운로드를 차단하는 데는 효과적
- PCM(Pulse-Code Modulation) 은 스피커로 전달되는 최종 비압축 디지털 오디오 포맷으로, 모든 오디오 파이프라인의 종착점
- 실제 공격에서는 PCM까지 추적할 필요 없이, JavaScript가 접근 가능한 마지막 지점인
SourceBuffer.appendBuffer()메서드가 핵심 공격 대상 -
appendBuffer가 호출되는 시점에 데이터는 이미 JavaScript에 의해 복호화된 상태이며, 브라우저의 AAC/Opus 디코더는 HotAudio의 독자 암호화를 이해하지 못하므로 표준 코덱 형태의 복호화된 데이터만 수용 - 복호화 완료와 브라우저 미디어 엔진 전달 사이의 순간이 바로 인터셉트 가능한 "골든 모먼트"
Act 1: V1.0 — 전역 변수 노출과 프로토타입 후킹
- HotAudio 플레이어가
window.as라는 전역 변수로 오디오 소스 객체를 노출하고 있었음 - V1 확장 프로그램은 HotAudio가 항상 전송하는
nozzle.js파일을 네트워크 요청 단계에서 가로채 수정된 코드를 주입 -
SourceBuffer.prototype.appendBuffer를 몽키패치하여 복호화된 청크를 배열에 저장하면서 원래 함수도 정상 호출 -
window.as.el을 음소거하고 재생 속도를 16배(브라우저 최대치)로 설정하여 빠르게 전체 오디오를 버퍼링한 후,ended이벤트 발생 시Blob으로 결합해.m4a파일로 다운로드 - 브라우저 확장 API를 활용한 클라이언트 사이드 중간자 공격(MITM) 으로, HotAudio 서버는 변조 사실을 인지할 수 없음
-
fermaw의 첫 번째 대응
- 공개 릴리스 약 2주 후 fermaw가 패치 적용
-
window.as전역 변수 노출을 제거하고 초기화 코드를 클로저로 감싸 외부 접근 차단 -
nozzle.js에 대한 해시 검증 체크 도입(SRI, 커스텀 자체 해싱, 서버 사이드 nonce 시스템 중 하나로 추정)- 수정된 파일이 정규 해시와 불일치하면 플레이어가 초기화되지 않는 구조
Act 2: V2.0 — 위장 기법과 범용 후킹
-
fermaw의 인메모리 방어
- JavaScript에서 네이티브 함수에
.toString()을 호출하면"function appendBuffer() { [native code] }"를 반환하지만, 몽키패치된 함수는 실제 소스 코드를 반환하는 특성 활용 - fermaw는
SourceBuffer.prototype.appendBuffer.toString()에'[native code]'가 포함되지 않으면 재생을 거부하는 무결성 검사 추가 - 플레이어 초기화 과정도 난독화하여
AudioSource클래스를 폴링 루프로 찾기 어렵게 변경
- JavaScript에서 네이티브 함수에
-
mockToString — 무결성 검사를 속이는 위장 함수
- 후킹된 함수의
.toString()이"function 이름() { [native code] }"를 반환하도록 오버라이드 - fermaw의 무결성 검사가 false negative를 반환하게 만들어, 후킹 여부를 탐지 불가능하게 만듦
- 후킹된 함수의
-
HTMLMediaElement.prototype.play 후킹
-
window.as나 특정 클래스명을 찾는 대신,HTMLMediaElement.prototype.play를 후킹하는 범용 접근 채택 - 플레이어 객체의 이름이나 클로저 깊이에 관계없이
.play()호출 시점에 오디오 엘리먼트를 자동 포착 - 모바일 기기는 일반적으로 하나의 플레이어만 활성화하므로, 다수의
.play()로 역분석을 방해하기 어려움
-
-
Object.defineProperty를 통한 영구 고정
-
window.Audio를 하이재킹한 생성자로 교체한 뒤writable: false,configurable: false로 설정 - fermaw의 코드가 원래
Audio생성자를 복원하려 해도 브라우저가 TypeError를 발생시키는 구조 - 후킹이 페이지 수명 동안 영구적으로 유지
-
Act 3: V3.0 — 프로퍼티 디스크립터 레벨의 전면 후킹
-
fermaw의 iframe 및 Shadow DOM 격리 시도
-
<iframe>은 자체window,document, 독립된 프로토타입 체인을 가지므로 부모 window의 후킹이 iframe 내부에 적용되지 않음 -
Shadow DOM은 메인 문서의
querySelector로 내부 엘리먼트를 탐색할 수 없는 격리된 DOM 서브트리 -
srcObject를 통해MediaStream/MediaSource객체를 직접 할당하여 URL 기반 인터셉트를 우회하는 방식도 시도
-
-
V3의 대응: 브라우저 프로퍼티 디스크립터 수준 후킹
-
Object.getOwnPropertyDescriptor를 사용하여HTMLMediaElement.prototype의src와srcObjectsetter를 직접 후킹- 오디오 엘리먼트가 메인 문서, iframe, 웹 컴포넌트 어디에 존재하든 소스 할당 시 후킹 발동
-
document_start주입을 통해 iframe 초기화 이전에 후킹 설치
-
-
addSourceBuffer 후킹: 레이스 컨디션 해결
- 이전 버전에서
SourceBuffer.prototype.appendBuffer를 프로토타입 수준에서 후킹할 경우, fermaw의 코드가 후킹 설치 전에appendBuffer참조를 캐시하면 우회 가능했음 - V3에서는
MediaSource.prototype.addSourceBuffer를 후킹하여SourceBuffer인스턴스 생성 시점을 가로챔- 인스턴스가 반환되는 즉시 해당 인스턴스에 직접
appendBuffer후킹을 own property로 설치 - 페이지 코드가 인스턴스를 보기 전에 후킹이 완료되므로 캐시 우회가 원천적으로 불가능
- 인스턴스가 반환되는 즉시 해당 인스턴스에 직접
- 이전 버전에서
-
캡처 단계 이벤트 리스너 — 최후의 안전망
-
document.addEventListener에서useCapture: true(캡처 단계)로play,loadedmetadata이벤트 감시 - 브라우저 이벤트는 캡처 단계(루트→타겟)에서 먼저 전파되므로, HotAudio 코드의 이벤트 리스너보다 항상 먼저 실행
-
addSourceBuffer프로토타입 후킹 +src/srcObject프로퍼티 디스크립터 후킹 +play()후킹 + 캡처 단계 이벤트 리스너의 4중 레이어로 브라우저의 모든 미디어 재생 경로를 커버
-
자동화: 고속 다운로드 프로세스
- 포착된 오디오 엘리먼트를 음소거하고
playbackRate를 16배로 설정 후 처음부터 재생 - 브라우저가 재생 위치 앞의 버퍼를 채우기 위해 빠르게 fetch→복호화→
SourceBuffer전달을 반복하고, 모든 청크가 후킹된appendBuffer를 통해 수집됨 - Chrome은 재생 속도를 16배로 제한(HTML 스펙에 명시된 상한은 없으나 Chromium 구현 제약)
- fermaw는 버스트 트래픽에 대한 스로틀링(수백 KB/s → 약 50 KB/s)을 적용하나, 실시간 청취 대비 여전히 수배 빠른 속도
- 더 심한 제한은 정상 사용자의 스트리밍에도 끊김을 유발하므로 현실적으로 어려움
-
적응형 속도 제어
- V3에서 추가된 기능으로,
buffered타임 레인지를 모니터링하여 버퍼 상태에 따라 재생 속도를 동적 조절- 버퍼 여유가 15초 이상이면 속도 증가, 3초 미만이면 감속
- 느린 연결에서 브라우저 멈춤(stall)과
ended이벤트 미발생 문제 방지
- V3에서 추가된 기능으로,
-
최종 파일 생성
- 재생 완료(
ended이벤트 또는currentTime이duration에 근접) 시 수집된 청크를Blob으로 결합하여.m4a다운로드 - 버퍼 경계의 불완전한 청크로 인한 무음 패딩 아티팩트가 발생할 수 있으며,
ffmpeg후처리로 정리 가능
- 재생 완료(
V3의 spoof() 함수: 더 정교한 위장
- V2의
mockToString은 네이티브 코드 문자열을 하드코딩하여 반환했으나, 브라우저/플랫폼별로[native code]문자열의 공백·포맷이 미세하게 다를 수 있는 취약점 존재 - V3의
spoof()는 후킹 전 원본 함수에서 실제 네이티브 코드 문자열을 캡처한 뒤 그대로 반환하는 방식으로 완벽한 위조 달성 -
_call.call(_toString, original)형태로 스크립트 시작 시 캐시해둔Function.prototype.call과Function.prototype.toString참조를 사용- 이후 다른 코드에 의해
.toString이 변조되더라도 영향을 받지 않는 구조
- 이후 다른 코드에 의해
DRM의 본질적 한계와 윤리적 고찰
- DRM 역사 전체가 "잠긴 상자를 주면서 동시에 열쇠를 건네는" 문제의 반복
- 1999년 최초의 CSS 암호화 DVD 크래킹 이후, 영화·음악 산업은 이 싸움에서 계속 패배
- 가장 정교한 게임 DRM인 Denuvo도 대부분의 주요 게임에서 출시 수주 내에 크래킹됨
- 한때 유명 크래커 Empress 은퇴 후 크래킹 속도가 둔화되었으나, 하이퍼바이저 스타일 익스플로잇 등장으로 다시 크래킹이 활발해진 상황
- 콘텐츠와 복호화 키가 모두 클라이언트 머신에 존재하는 한, 충분한 동기와 도구를 가진 사용자의 인터셉트는 불가피
결론: JavaScript DRM은 "정교한 마찰"이지 진정한 DRM이 아님
- HotAudio의 DRM은 fermaw의 능력 부족 때문이 아니라, JavaScript 기반 DRM이 도달할 수 있는 최선
- 클라이언트 사이드 복호화, 청크 전송, 능동적 안티탬퍼 체크를 모두 구현했으며, 브라우저 확장을 모르는 대다수 사용자에게는 완전한 차단 효과
- 그러나 이를 "DRM"이라 부르면 하드웨어 TEE 기반의 진정한 DRM과 동일한 기대치를 설정하게 되어 문제
- ASMR 크리에이터의 열성 팬은 오프라인 복사본을 원할 정도로 헌신적이며, Patreon 같은 유료 채널이 제공되면 기꺼이 구매할 가능성이 있는 층
- 어떤 형태의 보호든 콘텐츠 제작자가 필요로 하는 것은 이해할 수 있으나, JavaScript로 구현하는 것은 근본적으로 부적합한 접근
와 서로 정말 재미있는 공방이었을듯.
저도 예전에 api 응답이 갑자기 암호화된 채로 오길래 암호화된 값을 받았으면 클라이언트 어디선가 복호화를 하겠지라는 생각으로 번들링된 자바스크립트 코드 통짜 그대로 복사해서 복호화 코드 앞에 console.log 한줄 추가하고 그대로 개발자 콘솔에 붙여 넣었죠. 의외로 그냥 작동하더라고요? 아무튼 그렇게 암호화 키를 알아내니까 다음은 쉽더라고요. api의 다른 응답 속에서 키를 받아 쓰고 있었음ㅎㅎ
NSFW (Not Safe For Work) ASMR 이면..
성인 사이트 해킹한 이야기를 아주 기술적으로 딥하게 풀어쓴거네요 ㅡ.ㅡ;
역시 기술의 진보는 모두 성인쪽에서 이뤄집니다...?
생각해보니 오디오에 drm을 건다는 건... 정말 어렵지 않나요?
복잡한 해킹을 하는 게 아니라 오디오를 가상 케이블로만 돌려도 뭔가 될 거 같은 느낌이에요