Ảo tưởng về DRM JavaScript: Quá trình vô hiệu hóa bảo vệ sao chép của HotAudio chỉ sau 3 vòng
(therantydev.com)- DRM dựa trên JavaScript chạy trong trình duyệt về bản chất luôn có thể bị vượt qua, vì dữ liệu âm thanh sau khi giải mã cuối cùng vẫn phải đi qua vùng mà JavaScript có thể truy cập
- HotAudio là nền tảng lưu trữ âm thanh ASMR NSFW, triển khai cơ chế bảo vệ sao chép riêng bằng cách dùng MediaSource Extensions API với phương thức mã hóa và truyền theo từng chunk
- Ghi lại cuộc đối đầu 3 giai đoạn khi nhà phát triển liên tục vá lỗi (loại bỏ biến toàn cục, kiểm tra hash, kiểm tra toàn vẹn
.toString(), cô lập bằng iframe/Shadow DOM) còn phía tấn công lần nào cũng đáp trả bằng hook prototype và kỹ thuật ngụy trang - DRM thực chất cần tới bảo vệ phần cứng dựa trên Trusted Execution Environment (TEE) như Widevine, FairPlay..., nhưng các nền tảng nhỏ khó tiếp cận vì chi phí giấy phép và hạ tầng
- DRM JavaScript có thể đóng vai trò ma sát (friction) hữu hiệu với người dùng phổ thông, nhưng không thể ngăn được kẻ tấn công có kinh nghiệm, nên có khoảng cách rất lớn giữa kỳ vọng và thực tế nếu gọi nó là “DRM”
Bối cảnh: HotAudio và giới hạn bẩm sinh của DRM JavaScript
- HotAudio là một trang lưu trữ âm thanh ASMR NSFW, tự nhận là nền tảng cung cấp tính năng bảo vệ DRM cho nhà sáng tạo
- Xuất hiện như một nền tảng thay thế khi các dịch vụ lưu trữ như Soundgasm, Mega... bị siết chặt bởi ToS
- Phân tích bắt đầu khi nhà phát triển fermaw nhắc trên Reddit rằng việc triển khai DRM này “rất thú vị”
- Về bản chất, mã JavaScript tồn tại trong vùng “userland”, tức là mã được phân phối cho người dùng và họ có thể truy cập, chỉnh sửa
- Dù có dùng key, nonce hay định dạng tệp mã hóa tinh vi đến đâu, dữ liệu sau khi đi qua logic giải mã JavaScript cuối cùng vẫn phải được đưa tới engine âm thanh của trình duyệt ở dạng plaintext
Vai trò của Trusted Execution Environment (TEE)
- Theo định nghĩa của Microsoft, TEE là “một vùng cách ly của CPU và bộ nhớ được bảo vệ bằng mật mã”, nơi mã bên ngoài không thể đọc hay sửa đổi dữ liệu bên trong
- TEE là vùng bảo mật dựa trên phần cứng như ARM TrustZone, Intel SGX..., và là nơi các Content Decryption Module (CDM) như Widevine, FairPlay, PlayReady hoạt động
- Các CDM này đảm bảo khóa giải mã và buffer media đã giải mã không bị lộ ra ngoài hệ điều hành chủ
- Để có giấy phép Widevine cần ký thỏa thuận cấp phép với Google, tích hợp binary native, triển khai hạ tầng, hoàn tất thủ tục pháp lý và chịu chi phí đáng kể
- Việc một nền tảng âm thanh NSFW quy mô nhỏ có được giấy phép Widevine gần như là không khả thi trong thực tế
Cách HotAudio triển khai và “ranh giới PCM”
- HotAudio gửi âm thanh ở dạng đã mã hóa, rồi dùng cách giải mã tùy biến bằng JavaScript để giải mã và phát theo từng chunk thông qua MediaSource Extensions (MSE) API
- Cách này có hiệu quả trong việc chặn thao tác lưu bằng chuột phải hoặc tải trực tiếp từ tab mạng đối với người dùng phổ thông
- PCM (Pulse-Code Modulation) là định dạng âm thanh số không nén cuối cùng được gửi tới loa, tức điểm cuối của mọi pipeline âm thanh
- Trong tấn công thực tế, không cần lần theo tới PCM; điểm cuối cùng mà JavaScript còn truy cập được, phương thức
SourceBuffer.appendBuffer(), mới là mục tiêu chính - Ở thời điểm
appendBufferđược gọi, dữ liệu đã được JavaScript giải mã xong; bộ giải mã AAC/Opus của trình duyệt không hiểu cơ chế mã hóa riêng của HotAudio nên chỉ chấp nhận dữ liệu đã giải mã ở dạng codec chuẩn - Khoảnh khắc giữa lúc giải mã xong và trước khi chuyển sang media engine của trình duyệt chính là “thời điểm vàng” có thể chặn bắt
Act 1: V1.0 — Lộ biến toàn cục và hook prototype
- Trình phát HotAudio làm lộ đối tượng nguồn âm thanh qua biến toàn cục
window.as - Extension V1 chặn ở giai đoạn request mạng đối với tệp
nozzle.jsmà HotAudio luôn tải, rồi tiêm mã đã chỉnh sửa - Thực hiện monkey patch với
SourceBuffer.prototype.appendBufferđể lưu các chunk đã giải mã vào mảng trong khi vẫn gọi hàm gốc bình thường - Tắt tiếng
window.as.el, đặt tốc độ phát lên 16x (mức tối đa của trình duyệt) để buffer nhanh toàn bộ audio, sau đó khi phát sinh sự kiệnendedthì ghép thànhBlobvà tải xuống dưới dạng tệp.m4a - Đây là kiểu tấn công MITM phía client dùng API của extension trình duyệt, nên máy chủ HotAudio không thể nhận biết việc bị can thiệp
-
Phản ứng đầu tiên của fermaw
- Khoảng 2 tuần sau bản phát hành công khai, fermaw tung bản vá
- Loại bỏ việc lộ biến toàn cục
window.as, đồng thời bọc mã khởi tạo trong closure để chặn truy cập từ bên ngoài - Thêm kiểm tra hash cho
nozzle.js(được cho là SRI, cơ chế tự hash tùy biến, hoặc hệ thống nonce phía server)- Nếu tệp bị chỉnh sửa và không khớp hash chuẩn, trình phát sẽ không khởi tạo
Act 2: V2.0 — Kỹ thuật ngụy trang và hook tổng quát
-
Cơ chế phòng thủ trong bộ nhớ của fermaw
- Trong JavaScript, khi gọi
.toString()trên hàm native sẽ nhận về"function appendBuffer() { [native code] }", còn hàm đã bị monkey patch sẽ trả về mã nguồn thực; fermaw đã tận dụng đặc điểm này - fermaw thêm kiểm tra toàn vẹn: nếu
SourceBuffer.prototype.appendBuffer.toString()không chứa'[native code]'thì từ chối phát - Quy trình khởi tạo trình phát cũng được làm rối để khó tìm lớp
AudioSourcebằng vòng lặp polling
- Trong JavaScript, khi gọi
-
mockToString — Hàm ngụy trang để đánh lừa kiểm tra toàn vẹn
- Override
.toString()của hàm đã hook để nó trả về"function tên() { [native code] }" - Khiến cơ chế kiểm tra toàn vẹn của fermaw cho ra false negative, tức không thể phát hiện việc đã bị hook
- Override
-
Hook
HTMLMediaElement.prototype.play- Thay vì tìm
window.ashay tên lớp cụ thể, áp dụng cách tiếp cận tổng quát là hookHTMLMediaElement.prototype.play - Bất kể tên đối tượng trình phát hay độ sâu closure là bao nhiêu, chỉ cần
.play()được gọi thì phần tử audio sẽ tự động bị bắt - Thiết bị di động thường chỉ có một trình phát hoạt động nên khó dùng nhiều lời gọi
.play()để cản trở việc đảo ngược
- Thay vì tìm
-
Cố định vĩnh viễn bằng
Object.defineProperty- Thay
window.Audiobằng constructor đã bị chiếm quyền rồi đặtwritable: false,configurable: false - Nếu mã của fermaw cố khôi phục constructor
Audiogốc, trình duyệt sẽ ném raTypeError - Nhờ đó việc hook được duy trì vĩnh viễn trong suốt vòng đời trang
- Thay
Act 3: V3.0 — Hook toàn diện ở cấp property descriptor
-
Nỗ lực cô lập bằng iframe và Shadow DOM của fermaw
<iframe>cówindow,documentvà chuỗi prototype độc lập riêng, nên hook trên window cha không áp dụng vào bên trong iframe- Shadow DOM là cây DOM con tách biệt, không thể dò phần tử bên trong bằng
querySelectortừ tài liệu chính - Cũng có thử cách dùng
srcObjectđể gán trực tiếp đối tượngMediaStream/MediaSource, qua đó né việc chặn bắt dựa trên URL
-
Cách đối phó của V3: hook ở cấp property descriptor của trình duyệt
- Dùng
Object.getOwnPropertyDescriptorđể hook trực tiếp settersrcvàsrcObjecttrênHTMLMediaElement.prototype- Dù phần tử audio nằm ở tài liệu chính, iframe hay web component, việc gán nguồn sẽ đều kích hoạt hook
- Thiết lập hook bằng tiêm ở
document_starttrước cả khi iframe khởi tạo
- Dùng
-
Hook
addSourceBuffer: giải quyết race condition- Ở bản trước, nếu hook
SourceBuffer.prototype.appendBufferở cấp prototype, mã của fermaw có thể cache tham chiếuappendBuffertrước khi hook được cài, từ đó vượt qua được - V3 hook
MediaSource.prototype.addSourceBufferđể chặn đúng thời điểm tạo instanceSourceBuffer- Ngay khi instance được trả về, cài hook
appendBuffertrực tiếp lên chính instance đó dưới dạng own property - Vì hook hoàn tất trước khi mã của trang nhìn thấy instance, nên không còn khả năng vượt qua bằng cache
- Ngay khi instance được trả về, cài hook
- Ở bản trước, nếu hook
-
Event listener ở pha capture — lớp an toàn cuối cùng
- Theo dõi các sự kiện
play,loadedmetadatatrêndocument.addEventListenervớiuseCapture: true(pha capture) - Sự kiện của trình duyệt lan truyền qua pha capture (gốc → đích) trước, nên sẽ luôn chạy trước listener của mã HotAudio
- Với 4 lớp gồm hook prototype
addSourceBuffer, hook property descriptorsrc/srcObject, hookplay(), và event listener ở pha capture, mọi đường phát media của trình duyệt đều được bao phủ
- Theo dõi các sự kiện
Tự động hóa: quy trình tải xuống tốc độ cao
- Phần tử audio bị bắt sẽ được tắt tiếng, đặt
playbackRatelên 16x rồi phát lại từ đầu - Trình duyệt liên tục lặp chu trình fetch → giải mã → chuyển vào
SourceBufferđể lấp đầy buffer phía trước vị trí phát, và mọi chunk đều được thu thập quaappendBufferđã hook - Chrome giới hạn tốc độ phát ở 16x (HTML spec không quy định trần cụ thể, nhưng đây là giới hạn trong triển khai của Chromium)
- fermaw áp dụng throttling cho burst traffic (từ vài trăm KB/s xuống khoảng 50 KB/s), nhưng vẫn nhanh hơn nhiều lần so với nghe thời gian thực
- Nếu siết mạnh hơn nữa sẽ gây gián đoạn ngay cả với người dùng phát trực tuyến bình thường, nên khó khả thi trong thực tế
-
Điều khiển tốc độ thích ứng
- Tính năng bổ sung ở V3: theo dõi dải thời gian
bufferedđể điều chỉnh động tốc độ phát theo trạng thái buffer- Nếu dư buffer trên 15 giây thì tăng tốc, dưới 3 giây thì giảm tốc
- Giúp tránh tình trạng trình duyệt bị khựng (stall) và không phát sinh sự kiện
endedtrên kết nối chậm
- Tính năng bổ sung ở V3: theo dõi dải thời gian
-
Tạo tệp cuối cùng
- Khi phát xong (
endedhoặccurrentTimetiệm cậnduration), các chunk đã thu thập được ghép thànhBlobđể tải xuống thành.m4a - Có thể xuất hiện artifact đệm im lặng do chunk ở ranh giới buffer không hoàn chỉnh; có thể xử lý hậu kỳ bằng
ffmpeg
- Khi phát xong (
Hàm spoof() trong V3: ngụy trang tinh vi hơn
mockToStringở V2 trả về chuỗi mã native bằng cách hardcode, nhưng tồn tại điểm yếu là khoảng trắng và định dạng của chuỗi[native code]có thể hơi khác nhau giữa trình duyệt hoặc nền tảngspoof()ở V3 đạt được việc giả mạo hoàn hảo bằng cách chụp lại đúng chuỗi mã native thật từ hàm gốc trước khi hook rồi trả về nguyên xi- Nó dùng các tham chiếu
Function.prototype.callvàFunction.prototype.toStringđã được cache từ đầu script dưới dạng_call.call(_toString, original)- Vì thế dù
.toStringbị mã khác sửa đổi về sau thì cấu trúc này vẫn không bị ảnh hưởng
- Vì thế dù
Giới hạn bản chất của DRM và góc nhìn đạo đức
- Toàn bộ lịch sử DRM là sự lặp lại của vấn đề “đưa cho người dùng một chiếc hộp có khóa nhưng đồng thời cũng phải đưa luôn chìa khóa”
- Kể từ khi đĩa DVD dùng mã hóa CSS lần đầu bị bẻ khóa vào năm 1999, ngành phim ảnh và âm nhạc đã liên tục thua trong cuộc chiến này
- Ngay cả DRM game tinh vi nhất là Denuvo cũng bị crack chỉ sau vài tuần phát hành với phần lớn game lớn
- Tốc độ crack từng chậm lại sau khi cracker nổi tiếng Empress rút lui, nhưng hiện đã sôi động trở lại nhờ sự xuất hiện của các exploit kiểu hypervisor
- Chừng nào nội dung và khóa giải mã còn cùng tồn tại trên máy khách, việc chặn bắt bởi người dùng có đủ động lực và công cụ vẫn là điều không thể tránh khỏi
Kết luận: DRM JavaScript chỉ là “ma sát tinh vi”, không phải DRM thực thụ
- DRM của HotAudio không phải thất bại vì fermaw kém năng lực, mà vì đó vốn là giới hạn tốt nhất mà DRM dựa trên JavaScript có thể đạt tới
- Họ đã triển khai đủ giải mã phía client, truyền theo chunk, và các kiểm tra chống can thiệp chủ động; với đa số người dùng không biết tới extension trình duyệt thì hiệu quả chặn hoàn toàn là có thật
- Nhưng nếu gọi nó là “DRM” thì sẽ vô tình tạo ra mức kỳ vọng giống với DRM thật dựa trên TEE phần cứng, và đó mới là vấn đề
- Người hâm mộ cuồng nhiệt của các nhà sáng tạo ASMR thường đủ gắn bó để muốn có bản sao ngoại tuyến, và nếu có kênh trả phí như Patreon thì đây cũng là nhóm sẵn sàng bỏ tiền mua
- Có thể hiểu vì sao nhà sáng tạo nội dung cần một hình thức bảo vệ nào đó, nhưng triển khai bằng JavaScript về bản chất vẫn là một cách tiếp cận không phù hợp
2 bình luận
Chắc hẳn đó đã là một màn đấu trí qua lại rất thú vị.
Trước đây tôi cũng từng gặp trường hợp phản hồi API đột nhiên trả về ở dạng đã mã hóa, nên tôi nghĩ rằng nếu phía client đã nhận được giá trị mã hóa thì hẳn ở đâu đó trên client sẽ có đoạn giải mã. Thế là tôi sao chép nguyên khối mã JavaScript đã được bundle, thêm một dòng
console.logngay trước phần mã giải mã, rồi dán nguyên vào console của trình duyệt. Bất ngờ là nó cứ thế chạy luôn. Dù sao thì sau khi tìm ra khóa mã hóa theo cách đó, bước tiếp theo lại khá dễ. Hóa ra nó đang lấy khóa từ một phản hồi khác của API để dùng thôi hahaNghĩ kỹ thì việc áp DRM lên âm thanh... chẳng phải là rất khó sao?
Có cảm giác là không cần hack phức tạp, chỉ cần cho âm thanh chạy qua cáp ảo là cũng làm được gì đó rồi