- 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.js mà 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ện ended thì ghép thành Blob và 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
AudioSource bằng vòng lặp polling
-
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
-
Hook HTMLMediaElement.prototype.play
- Thay vì tìm
window.as hay tên lớp cụ thể, áp dụng cách tiếp cận tổng quát là hook HTMLMediaElement.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
-
Cố định vĩnh viễn bằng Object.defineProperty
- Thay
window.Audio bằng constructor đã bị chiếm quyền rồi đặt writable: false, configurable: false
- Nếu mã của fermaw cố khôi phục constructor
Audio gốc, trình duyệt sẽ ném ra TypeError
- Nhờ đó việc hook được duy trì vĩnh viễn trong suốt vòng đời trang
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, document và 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
querySelector từ tài liệu chính
- Cũng có thử cách dùng
srcObject để gán trực tiếp đối tượng MediaStream/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 setter src và srcObject trên HTMLMediaElement.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_start trước cả khi iframe khởi tạo
-
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ếu appendBuffer trướ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 instance SourceBuffer
- Ngay khi instance được trả về, cài hook
appendBuffer trự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
-
Event listener ở pha capture — lớp an toàn cuối cùng
- Theo dõi các sự kiện
play, loadedmetadata trên document.addEventListener với useCapture: 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 descriptor src/srcObject, hook play(), và event listener ở pha capture, mọi đường phát media của trình duyệt đều được bao phủ
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
playbackRate lê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 qua appendBuffer đã 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
ended trên kết nối chậm
-
Tạo tệp cuối cùng
- Khi phát xong (
ended hoặc currentTime tiệm cận duration), các chunk đã thu thập được ghép thành Blob để 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
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ảng
spoof() ở 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.call và Function.prototype.toString đã được cache từ đầu script dưới dạng _call.call(_toString, original)
- Vì thế dù
.toString bị mã khác sửa đổi về sau thì cấu trúc này vẫn không bị ảnh hưởng
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