ภาพลวงตาของ JavaScript DRM: กระบวนการทำลายการป้องกันการคัดลอกของ HotAudio ได้ภายใน 3 ยก
(therantydev.com)- 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
- ใน JavaScript เมื่อเรียก
-
mockToString — ฟังก์ชันอำพรางที่หลอกการตรวจสอบความสมบูรณ์
- override ให้
.toString()ของฟังก์ชันที่ถูก hook คืนค่าเป็น"function ชื่อ() { [native code] }" - ทำให้การตรวจสอบของ fermaw ได้ false negative และไม่อาจตรวจพบว่าถูก hook อยู่
- override ให้
-
การ hook
HTMLMediaElement.prototype.play- แทนที่จะมองหา
window.asหรือชื่อคลาสเฉพาะ ก็เปลี่ยนมาใช้วิธีทั่วไปด้วยการ hookHTMLMediaElement.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 เสร็จก่อนที่โค้ดของหน้าจะเห็นอินสแตนซ์ จึง ไม่สามารถหลบด้วยการแคชได้โดยสิ้นเชิง
- ทันทีที่อินสแตนซ์ถูกคืนค่า ก็จะติดตั้ง hook
- ในเวอร์ชันก่อน หาก 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+ การ hookplay()+ ตัวรับฟังอีเวนต์ใน 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บนการเชื่อมต่อช้า
- เป็นฟีเจอร์ที่เพิ่มใน V3 โดยตรวจสอบช่วงเวลา
-
การสร้างไฟล์สุดท้าย
- เมื่อเล่นจบ (
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 กับเสียงนี่... มันยากมากจริง ๆ ไม่ใช่เหรอ?
ไม่ใช่ว่าต้องแฮ็กอะไรซับซ้อน แค่ส่งเสียงผ่านสายสัญญาณเสมือนก็น่าจะทำอะไรได้แล้วเหมือนกัน
แต่ก็น่าสนุกดีนะที่โต้ตอบกันไปมาแบบนั้น 55555 ดูออกเลยว่ามีการคิดลูกเล่นแยบยลที่ AI คงไม่มีทางนึกถึงแน่ ๆ