# JavaScript DRM의 허상: HotAudio 복사 보호를 3라운드 만에 무력화한 과정

> Clean Markdown view of GeekNews topic #27427. Use the original source for factual precision when an external source URL is present.

## Metadata

- GeekNews HTML: [https://news.hada.io/topic?id=27427](https://news.hada.io/topic?id=27427)
- GeekNews Markdown: [https://news.hada.io/topic/27427.md](https://news.hada.io/topic/27427.md)
- Type: GN+
- Author: [neo](https://news.hada.io/@neo)
- Published: 2026-03-12T10:26:02+09:00
- Updated: 2026-03-12T10:26:02+09:00
- Original source: [therantydev.com](https://www.therantydev.com/javascript-drms-are-stupid)
- Points: 17
- Comments: 4

## Summary

브라우저에서 동작하는 **JavaScript DRM**은 복호화된 오디오 데이터가 결국 사용자 영역을 통과하기 때문에 구조적으로 완전한 보호가 불가능합니다. HotAudio가 MediaSource Extensions API를 이용해 자체 암호화·청크 전송 방식을 구현했지만, 공격자는 프로토타입 후킹과 위장 기법으로 세 차례에 걸쳐 이를 무력화했습니다. 하드웨어 기반 **Trusted Execution Environment(TEE)** 가 없는 한 이런 DRM은 숙련된 사용자를 막을 수 없으며, 실질적으로는 단지 복제 시도를 늦추는 ‘정교한 마찰’에 그칩니다.

## Topic Body

- 브라우저에서 실행되는 **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` 클래스를 폴링 루프로 찾기 어렵게 변경  
- ## 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 격리 시도  
  - `&lt;iframe&gt;`은 자체 `window`, `document`, **독립된 프로토타입 체인**을 가지므로 부모 window의 후킹이 iframe 내부에 적용되지 않음  
  - **Shadow DOM**은 메인 문서의 `querySelector`로 내부 엘리먼트를 탐색할 수 없는 격리된 DOM 서브트리  
  - `srcObject`를 통해 `MediaStream`/`MediaSource` 객체를 직접 할당하여 URL 기반 인터셉트를 우회하는 방식도 시도  
- ## V3의 대응: 브라우저 프로퍼티 디스크립터 수준 후킹  
  - `Object.getOwnPropertyDescriptor`를 사용하여 `HTMLMediaElement.prototype`의 **`src`와 `srcObject` setter**를 직접 후킹  
    - 오디오 엘리먼트가 메인 문서, 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` 이벤트 미발생 문제 방지  
- ## 최종 파일 생성  
  - 재생 완료(`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로 구현하는 것은 근본적으로 **부적합한 접근**

## Comments



### Comment 52929

- Author: joyfui
- Created: 2026-03-13T01:24:53+09:00
- Points: 1

와 서로 정말 재미있는 공방이었을듯.  
저도 예전에 api 응답이 갑자기 암호화된 채로 오길래 암호화된 값을 받았으면 클라이언트 어디선가 복호화를 하겠지라는 생각으로 번들링된 자바스크립트 코드 통짜 그대로 복사해서 복호화 코드 앞에 console.log 한줄 추가하고 그대로 개발자 콘솔에 붙여 넣었죠. 의외로 그냥 작동하더라고요? 아무튼 그렇게 암호화 키를 알아내니까 다음은 쉽더라고요. api의 다른 응답 속에서 키를 받아 쓰고 있었음ㅎㅎ

### Comment 52875

- Author: xguru
- Created: 2026-03-12T11:31:03+09:00
- Points: 1

NSFW (Not Safe For Work) ASMR 이면..   
성인 사이트 해킹한 이야기를 아주 기술적으로 딥하게 풀어쓴거네요 ㅡ.ㅡ;   
역시 기술의 진보는 모두 성인쪽에서 이뤄집니다...?

### Comment 52865

- Author: crawler
- Created: 2026-03-12T10:31:40+09:00
- Points: 1

생각해보니 오디오에 drm을 건다는 건... 정말 어렵지 않나요?  
복잡한 해킹을 하는 게 아니라 오디오를 가상 케이블로만 돌려도 뭔가 될 거 같은 느낌이에요

### Comment 52866

- Author: crawler
- Created: 2026-03-12T10:37:59+09:00
- Points: 1
- Parent comment: 52865
- Depth: 1

> JavaScript에서 네이티브 함수에 .toString()을 호출하면 "function appendBuffer() { [native code] }"를 반환하지만, 몽키패치된 함수는 실제 소스 코드를 반환하는 특성 활용  
  
와 근데 되게 재밌게 주고 받았네요 ㅋㅋㅋㅋ AI는 절대 생각하지 않았을 꼼수들을 생각한 모습이 보입니다.
