- DRM ที่อิง JavaScript ซึ่งรันอยู่ในเบราว์เซอร์ สามารถถูกเลี่ยงได้โดยพื้นฐาน เพราะท้ายที่สุดข้อมูลเสียงที่ถูกถอดรหัสแล้วต้องผ่านพื้นที่ที่ JavaScript เข้าถึงได้
- HotAudio เป็นแพลตฟอร์มโฮสต์เสียง ASMR แบบ NSFW ที่ใช้ MediaSource Extensions API สร้างระบบป้องกันการคัดลอกแบบเข้ารหัสและส่งเป็นชังก์ของตนเอง
- เป็นบันทึกการปะทะกัน 3 รอบที่นักพัฒนาแพตช์ซ้ำ ๆ (ลบตัวแปรโกลบอล, ตรวจสอบแฮช, ตรวจสอบความสมบูรณ์ด้วย
.toString(), แยกด้วย iframe/Shadow DOM) ขณะที่ฝั่งโจมตีก็ตอบโต้ทุกครั้งด้วย การ hook โปรโตไทป์และเทคนิคอำพราง
- DRM ที่ใช้งานได้จริงจำเป็นต้องมีการป้องกันฮาร์ดแวร์บนฐาน Trusted Execution Environment(TEE) (เช่น Widevine, FairPlay) แต่แพลตฟอร์มขนาดเล็กเข้าถึงได้ยากเพราะต้นทุนไลเซนส์และปัญหาโครงสร้างพื้นฐาน
- JavaScript DRM มีประโยชน์ในฐานะ แรงเสียดทาน (friction) สำหรับผู้ใช้ทั่วไป แต่ไม่อาจหยุดผู้โจมตีที่ชำนาญได้ จึงมีช่องว่างอย่างมากระหว่างความคาดหวังกับความเป็นจริงหากจะเรียกมันว่า “DRM”
พื้นหลัง: HotAudio และข้อจำกัดโดยกำเนิดของ JavaScript DRM
- HotAudio เป็นเว็บไซต์โฮสต์เสียง ASMR แบบ NSFW และอ้างว่าเป็นแพลตฟอร์มที่มี ฟีเจอร์ป้องกัน 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 ส่งเสียงในรูปแบบเข้ารหัส และใช้วิธีถอดรหัสแบบกำหนดเองบน JavaScript โดยถอดรหัสและเล่นเป็นชังก์ผ่าน MediaSource Extensions(MSE) API
- วิธีนี้มีประสิทธิภาพในการป้องกันการกดคลิกขวาเพื่อบันทึกหรือดาวน์โหลดตรงจากแท็บเครือข่ายของผู้ใช้ทั่วไป
- PCM(Pulse-Code Modulation) คือฟอร์แมตเสียงดิจิทัลแบบไม่บีบอัดขั้นสุดท้ายที่ถูกส่งไปยังลำโพง และเป็นปลายทางของทุก pipeline เสียง
- แต่ในการโจมตีจริง ไม่จำเป็นต้องไล่ไปถึง PCM เพราะจุดสุดท้ายที่ JavaScript ยังเข้าถึงได้คือเมธอด
SourceBuffer.appendBuffer() ซึ่งเป็นเป้าหมายหลัก
- ตอนที่
appendBuffer ถูกเรียก ข้อมูลอยู่ในสถานะที่ JavaScript ถอดรหัสแล้ว และตัวถอดรหัส AAC/Opus ของเบราว์เซอร์ไม่เข้าใจการเข้ารหัสเฉพาะของ HotAudio จึงรับได้เฉพาะ ข้อมูลที่ถูกถอดรหัสแล้วในรูปโค้ดกมาตรฐาน
- ช่วงเวลาระหว่างการถอดรหัสเสร็จสิ้นกับการส่งต่อให้เอนจินสื่อของเบราว์เซอร์ คือ “golden moment” ที่สามารถดักจับได้
Act 1: V1.0 — การเปิดเผยตัวแปรโกลบอลและการ hook โปรโตไทป์
- ตัวเล่นของ HotAudio เปิดเผยออบเจ็กต์แหล่งเสียงผ่าน ตัวแปรโกลบอล ชื่อ
window.as
- ส่วนขยาย V1 ดัก ไฟล์
nozzle.js ที่ HotAudio ส่งมาเสมอในขั้นตอนคำขอเครือข่าย แล้วฉีดโค้ดที่แก้ไขเข้าไป
- มีการ monkey patch
SourceBuffer.prototype.appendBuffer เพื่อเก็บชังก์ที่ถอดรหัสแล้วลงในอาร์เรย์ พร้อมเรียกฟังก์ชันเดิมตามปกติ
- จากนั้นปิดเสียง
window.as.el ตั้งความเร็วเล่นเป็น 16 เท่า (ค่าสูงสุดของเบราว์เซอร์) เพื่อบัฟเฟอร์เสียงทั้งหมดอย่างรวดเร็ว และเมื่อเกิดอีเวนต์ ended ก็รวมเป็น Blob เพื่อดาวน์โหลดเป็นไฟล์ .m4a
- นี่คือ การโจมตีแบบ MITM ฝั่งไคลเอนต์ โดยใช้ browser extension API ซึ่งทำให้เซิร์ฟเวอร์ของ HotAudio ไม่อาจรู้ได้ว่าถูกแก้ไข
-
การตอบโต้ครั้งแรกของ fermaw
- fermaw ออกแพตช์ประมาณ 2 สัปดาห์หลังปล่อยสู่สาธารณะ
- ลบ การเปิดเผยตัวแปรโกลบอล
window.as และห่อโค้ดเริ่มต้นไว้ใน closure เพื่อกันการเข้าถึงจากภายนอก
- เพิ่ม การตรวจสอบแฮช สำหรับ
nozzle.js (คาดว่าเป็น SRI, แฮชแบบกำหนดเอง หรือระบบ nonce ฝั่งเซิร์ฟเวอร์)
- ถ้าไฟล์ที่ถูกแก้ไขไม่ตรงกับแฮชมาตรฐาน ตัวเล่นจะไม่เริ่มทำงาน
Act 2: V2.0 — เทคนิคอำพรางและการ hook แบบทั่วไป
-
การป้องกันในหน่วยความจำของ fermaw
- ใน JavaScript เมื่อเรียก
.toString() กับฟังก์ชันเนทีฟ จะได้ "function appendBuffer() { [native code] }" แต่ฟังก์ชันที่ถูก monkey patch จะคืนค่าเป็น ซอร์สโค้ดจริง ซึ่งถูกนำมาใช้เป็นจุดตรวจจับ
- fermaw เพิ่มการตรวจสอบความสมบูรณ์ว่า ถ้า
SourceBuffer.prototype.appendBuffer.toString() ไม่มี '[native code]' ก็จะ ปฏิเสธการเล่น
- ขั้นตอนเริ่มต้นตัวเล่นก็ถูกทำให้สับสนมากขึ้น จนยากจะหา class
AudioSource ด้วย polling loop
-
mockToString — ฟังก์ชันอำพรางที่หลอกการตรวจสอบความสมบูรณ์
- override ให้
.toString() ของฟังก์ชันที่ถูก hook คืนค่าเป็น "function ชื่อ() { [native code] }"
- ทำให้การตรวจสอบของ fermaw ได้ false negative และไม่อาจตรวจพบว่าถูก hook อยู่
-
การ hook HTMLMediaElement.prototype.play
- แทนที่จะมองหา
window.as หรือชื่อคลาสเฉพาะ ก็เปลี่ยนมาใช้วิธีทั่วไปด้วยการ hook HTMLMediaElement.prototype.play
- ไม่ว่าชื่อออบเจ็กต์ตัวเล่นหรือความลึกของ closure จะเป็นอย่างไร เมื่อมีการเรียก
.play() ก็สามารถ จับออบเจ็กต์เสียงได้อัตโนมัติ
- บนอุปกรณ์พกพามักมีตัวเล่นเดียวที่ทำงานอยู่ จึงยากจะใช้
.play() หลายครั้งเพื่อขัดขวางการวิเคราะห์ย้อนกลับ
-
การตรึงถาวรผ่าน Object.defineProperty
- มีการแทนที่
window.Audio ด้วย constructor ที่ถูก hijack แล้วตั้ง writable: false, configurable: false
- ต่อให้โค้ดของ fermaw พยายาม กู้คืน constructor
Audio เดิม เบราว์เซอร์ก็จะโยน TypeError
- ทำให้ hook ถูก คงอยู่ถาวร ตลอดอายุของหน้า
Act 3: V3.0 — การ hook เต็มรูปแบบในระดับ property descriptor
-
ความพยายามของ fermaw ในการแยกด้วย iframe และ Shadow DOM
<iframe> มี window, document และ prototype chain ที่แยกอิสระ ของตัวเอง ดังนั้น hook ของ parent window จึงไม่ส่งผลภายใน iframe
- Shadow DOM เป็น DOM subtree แบบแยกที่ไม่สามารถสำรวจองค์ประกอบภายในได้ด้วย
querySelector ของเอกสารหลัก
- ยังมีความพยายามใช้
srcObject เพื่อกำหนดออบเจ็กต์ MediaStream/MediaSource โดยตรง และหลบเลี่ยงการดักจับแบบอิง URL
-
การตอบโต้ของ V3: hook ระดับ browser property descriptor
- ใช้
Object.getOwnPropertyDescriptor เพื่อ hook setter ของ src และ srcObject บน HTMLMediaElement.prototype โดยตรง
- ไม่ว่าออบเจ็กต์เสียงจะอยู่ในเอกสารหลัก, iframe หรือ web component เมื่อมีการกำหนด source ก็จะถูก hook ทันที
- ติดตั้ง hook ก่อน iframe เริ่มต้นทำงานด้วยการฉีดตั้งแต่
document_start
-
การ hook addSourceBuffer: แก้ race condition
- ในเวอร์ชันก่อน หาก hook
SourceBuffer.prototype.appendBuffer ในระดับโปรโตไทป์ แล้วโค้ดของ fermaw แคช reference ของ appendBuffer ไว้ก่อนติดตั้ง hook ก็สามารถหลบได้
- ใน V3 จึงเปลี่ยนไป hook
MediaSource.prototype.addSourceBuffer เพื่อดัก จังหวะสร้างอินสแตนซ์ SourceBuffer
- ทันทีที่อินสแตนซ์ถูกคืนค่า ก็จะติดตั้ง hook
appendBuffer ลงบนอินสแตนซ์นั้นโดยตรงเป็น own property
- เพราะ hook เสร็จก่อนที่โค้ดของหน้าจะเห็นอินสแตนซ์ จึง ไม่สามารถหลบด้วยการแคชได้โดยสิ้นเชิง
-
ตัวรับฟังอีเวนต์ใน capture phase — ตาข่ายนิรภัยชั้นสุดท้าย
- ใช้
document.addEventListener พร้อม useCapture: true เพื่อตรวจจับอีเวนต์ play, loadedmetadata ใน capture phase
- อีเวนต์ของเบราว์เซอร์จะแพร่จาก capture phase (root→target) ก่อนเสมอ จึงทำงาน ก่อน event listener ของโค้ด HotAudio ทุกครั้ง
- ด้วย 4 ชั้น ได้แก่ การ hook โปรโตไทป์
addSourceBuffer + การ hook property descriptor ของ src/srcObject + การ hook play() + ตัวรับฟังอีเวนต์ใน capture phase จึงครอบคลุมทุกเส้นทางการเล่นสื่อของเบราว์เซอร์
ระบบอัตโนมัติ: กระบวนการดาวน์โหลดความเร็วสูง
- ปิดเสียงออบเจ็กต์เสียงที่จับได้ ตั้ง
playbackRate เป็น 16 เท่า แล้วเล่นจากต้นจนจบ
- เบราว์เซอร์จะเร่งวนลูป fetch→ถอดรหัส→ส่งเข้า
SourceBuffer เพื่อเติมบัฟเฟอร์ด้านหน้าตำแหน่งเล่น และทุกชังก์ก็จะถูกเก็บผ่าน appendBuffer ที่ถูก hook
- Chrome จำกัดความเร็วเล่นไว้ที่ 16 เท่า (แม้ HTML spec จะไม่ได้กำหนดเพดานไว้ แต่เป็นข้อจำกัดของ Chromium)
- fermaw ใช้ throttling กับทราฟฟิกแบบ burst (จากหลายร้อย KB/s เหลือราว 50 KB/s) แต่ก็ยังเร็วกว่าการฟังแบบเรียลไทม์หลายเท่า
- การจำกัดที่แรงกว่านี้ทำให้สตรีมของผู้ใช้ปกติกระตุก จึงทำได้ยากในทางปฏิบัติ
-
การควบคุมความเร็วแบบปรับตัว
- เป็นฟีเจอร์ที่เพิ่มใน V3 โดยตรวจสอบช่วงเวลา
buffered และ ปรับความเร็วเล่นแบบไดนามิกตามสถานะบัฟเฟอร์
- ถ้ามีบัฟเฟอร์เผื่อเกิน 15 วินาทีจะเพิ่มความเร็ว และถ้าน้อยกว่า 3 วินาทีจะลดความเร็ว
- ป้องกันปัญหาเบราว์เซอร์ค้าง (stall) และไม่เกิดอีเวนต์
ended บนการเชื่อมต่อช้า
-
การสร้างไฟล์สุดท้าย
- เมื่อเล่นจบ (
ended หรือ currentTime เข้าใกล้ duration) จะรวมชังก์ที่เก็บไว้เป็น Blob แล้วดาวน์โหลดเป็น .m4a
- อาจเกิดอาร์ติแฟกต์เป็น การเติมความเงียบ จากชังก์ที่ไม่สมบูรณ์ตรงขอบบัฟเฟอร์ ซึ่งทำความสะอาดต่อได้ด้วย
ffmpeg
ฟังก์ชัน spoof() ของ V3: การอำพรางที่แนบเนียนยิ่งขึ้น
mockToString ใน V2 คืนสตริง native code แบบ ฮาร์ดโค้ด แต่มีจุดอ่อนตรงที่รูปแบบช่องว่างหรือการจัดวางของสตริง [native code] อาจต่างกันเล็กน้อยในแต่ละเบราว์เซอร์หรือแพลตฟอร์ม
spoof() ใน V3 จึงเปลี่ยนมาจับ สตริง native code จริงจากฟังก์ชันต้นฉบับก่อนถูก hook แล้วคืนค่านั้นตรง ๆ ทำให้ปลอมได้สมบูรณ์
- ใช้อ้างอิง
Function.prototype.call และ Function.prototype.toString ที่แคชไว้ตั้งแต่เริ่มสคริปต์ในรูป _call.call(_toString, original)
- ดังนั้นต่อให้
.toString ถูกโค้ดอื่นแก้ไขภายหลัง ก็ ไม่ได้รับผลกระทบ
ข้อจำกัดโดยเนื้อแท้ของ DRM และข้อพิจารณาด้านจริยธรรม
- ประวัติศาสตร์ทั้งหมดของ DRM คือการวนซ้ำของปัญหาแบบ “ให้กล่องที่ล็อกไว้พร้อมยื่นกุญแจให้ในเวลาเดียวกัน”
- นับตั้งแต่การแคร็ก DVD ที่เข้ารหัส CSS ครั้งแรกในปี 1999 อุตสาหกรรมภาพยนตร์และเพลงก็แพ้การต่อสู้นี้มาโดยตลอด
- แม้แต่ DRM เกมที่ซับซ้อนที่สุดอย่าง Denuvo ก็ยังถูกแคร็กในเกมใหญ่ส่วนมากภายในไม่กี่สัปดาห์หลังวางจำหน่าย
- ช่วงหนึ่งความเร็วในการแคร็กลดลงหลัง Empress แคร็กเกอร์ชื่อดังวางมือ แต่เมื่อมี exploit สไตล์ไฮเปอร์ไวเซอร์ ปรากฏขึ้น การแคร็กก็กลับมาคึกคักอีกครั้ง
- ตราบใดที่ทั้งคอนเทนต์และคีย์ถอดรหัสยังอยู่บนเครื่องไคลเอนต์ การดักจับโดยผู้ใช้ที่มีแรงจูงใจและเครื่องมือเพียงพอย่อม หลีกเลี่ยงไม่ได้
บทสรุป: JavaScript DRM เป็นเพียง “แรงเสียดทานที่ซับซ้อน” ไม่ใช่ DRM ที่แท้จริง
- DRM ของ HotAudio ไม่ได้สะท้อนว่า fermaw ไร้ความสามารถ แต่เป็น ขีดสูงสุดที่ JavaScript DRM ทำได้แล้ว
- มันมีทั้งการถอดรหัสฝั่งไคลเอนต์, การส่งแบบชังก์ และการตรวจจับการแก้ไขเชิงรุกครบถ้วน ซึ่งสำหรับผู้ใช้ส่วนใหญ่ที่ไม่รู้จัก browser extension ก็ให้ผล เหมือนปิดกั้นได้สมบูรณ์
- แต่การเรียกสิ่งนี้ว่า “DRM” ทำให้เกิด ความคาดหวังแบบเดียวกับ DRM แท้ที่อิงฮาร์ดแวร์ TEE ซึ่งเป็นปัญหา
- แฟนตัวยงของครีเอเตอร์ ASMR มักทุ่มเทมากพอที่จะอยากได้สำเนาออฟไลน์ และหากมีช่องทางเสียเงินอย่าง Patreon ก็เป็นกลุ่มที่ น่าจะยอมจ่ายอย่างเต็มใจ
- แม้จะเข้าใจได้ว่าผู้สร้างคอนเทนต์ต้องการรูปแบบการปกป้องบางอย่าง แต่การทำสิ่งนี้ด้วย JavaScript นั้น เป็นแนวทางที่ไม่เหมาะสมโดยพื้นฐาน
4 ความคิดเห็น
คงเป็นการต่อสู้เชิงเทคนิคที่สนุกกันทั้งสองฝ่ายจริง ๆ
เมื่อก่อนผมก็เคยเจอเหมือนกันว่าอยู่ ๆ API ส่งค่าที่ถูกเข้ารหัสมาให้ เลยคิดว่าถ้าฝั่งไคลเอนต์ได้รับค่าที่เข้ารหัส อย่างน้อยก็น่าจะต้องมีที่ไหนสักแห่งที่ถอดรหัสมันอยู่ ก็เลยก๊อบ JavaScript bundle ทั้งก้อนมาตรงนั้น เพิ่ม
console.logหนึ่งบรรทัดไว้หน้าส่วนโค้ดถอดรหัส แล้วแปะลงใน developer console ไปตรง ๆ เลย ปรากฏว่าดันทำงานได้เฉยเลย? ยังไงก็ตาม พอรู้คีย์เข้ารหัสแล้ว หลังจากนั้นก็ง่ายเลยครับ ปรากฏว่ามันไปรับคีย์จาก response อื่นของ API มาใช้อีกที 555ถ้าเป็น ASMR แบบ NSFW (Not Safe For Work) ล่ะก็..
นี่คือการเล่าเรื่องการแฮ็กเว็บผู้ใหญ่แบบลงลึกมากในเชิงเทคนิคเลยนะ -.-;
สุดท้ายแล้วความก้าวหน้าทางเทคโนโลยีก็มักเกิดขึ้นจากฝั่งผู้ใหญ่ทั้งนั้น...?
พอมาคิดดูแล้ว การใส่ DRM กับเสียงนี่... มันยากมากจริง ๆ ไม่ใช่เหรอ?
ไม่ใช่ว่าต้องแฮ็กอะไรซับซ้อน แค่ส่งเสียงผ่านสายสัญญาณเสมือนก็น่าจะทำอะไรได้แล้วเหมือนกัน
> ใน JavaScript เมื่อเรียก
.toString()กับฟังก์ชันเนทีฟ จะคืนค่าเป็นfunction appendBuffer() { [native code] }แต่ฟังก์ชันที่ถูก monkey patch จะคืนซอร์สโค้ดจริงออกมา ซึ่งเป็นการอาศัยคุณสมบัตินี้แต่ก็น่าสนุกดีนะที่โต้ตอบกันไปมาแบบนั้น 55555 ดูออกเลยว่ามีการคิดลูกเล่นแยบยลที่ AI คงไม่มีทางนึกถึงแน่ ๆ