Dokumentasi
/

Pemecahan masalah

/

Obfuskasi VM dengan eval dan new Function

Obfuskasi VM dengan eval dan new Function

Cara obfuskasi VM menangani konstruksi kode dinamis (direct eval dan konstruktor Function), apa yang dikompilasi menjadi bytecode dan apa yang dilewati, peringatan yang dikeluarkan obfuscator, serta cara mendiagnosis ReferenceError saat runtime.

Mengapa ini penting

Obfuskasi VM mengompilasi isi fungsi menjadi bytecode yang dijalankan oleh interpreter di dalam runtime. Pengenal di cakupan sekitarnya juga diganti namanya. Kedua transformasi ini berinteraksi buruk dengan kode yang dibangun dari string saat runtime: eval(s), new Function(...s), dan Function(...s). Jika kode yang dibangun saat runtime mereferensikan pengenal yang sudah diganti namanya oleh obfuscator, Anda akan mendapatkan Uncaught ReferenceError: <renamed-name> is not defined saat fungsi yang dihasilkan pertama kali dijalankan.

Obfuscator menangani setiap pola secara berbeda. Tabel di bawah adalah versi singkatnya; bagian selanjutnya dari halaman ini menjelaskan setiap barisnya.

Apa yang dilakukan obfuscator, secara sekilas

Pola dalam kode sumber AndaApa yang terjadi
eval('literal string') (isinya berupa literal string)Berjalan dengan benar (kecuali vmForceCompileDynamicCode aktif, lihat Solusi alternatif). Fungsi yang memuat pemanggilan ini, beserta setiap fungsi yang didefinisikan di dalamnya, kehilangan kompilasi bytecode VM (direct eval membaca variabel lokal di sekitarnya, yang tidak dipertahankan oleh VM setelah sebuah fungsi dikompilasi menjadi bytecode).
eval(dynamicExpression)Dapat gagal saat runtime dengan ReferenceError. Fungsi yang memuat pemanggilan ini, beserta setiap fungsi yang didefinisikan di dalamnya, juga kehilangan kompilasi bytecode VM.
(0, eval)(s) / window.eval(s) (indirect)Fungsi yang memuat pemanggilan ini dikompilasi menjadi bytecode VM seperti biasa. Indirect eval berjalan di cakupan global dan tidak dapat melihat variabel lokal di sekitarnya, sehingga variabel lokal yang diganti namanya tidak dapat merusaknya. Ia tetap dapat gagal jika kode yang dievaluasi mereferensikan global yang diganti namanya atau dihapus.
new Function('a', 'b', 'return a + b') (semua argumen berupa literal string)Berjalan dengan benar. Fungsi yang memuat pemanggilan ini dikompilasi menjadi bytecode VM seperti biasa.
new Function(dynamicBody) / Function(dynamicBody)Dapat gagal saat runtime dengan ReferenceError. Fungsi yang memuat pemanggilan ini, beserta setiap fungsi yang didefinisikan di dalamnya, juga kehilangan kompilasi bytecode VM.

Compiler juga mendeteksi eval?.(code) secara konservatif dan menanganinya seperti direct eval. JavaScript mendefinisikan bentuk pemanggilan opsional ini sebagai indirect eval; deteksi oleh compiler tidak mengubah perilaku bahasa tersebut.

Hanya sebagian dari pola ini yang menghasilkan peringatan non-fatal, dan hanya ketika pemanggilannya berada di dalam sebuah fungsi: pemanggilan eval, new Function, atau Function dinamis melaporkan DynamicCodeRenameRisk sekaligus VMDynamicCodeSkipped (VMDynamicCodeSkipped hanya untuk fungsi yang seharusnya dikompilasi oleh VM: tidak dikecualikan, dan ditandai jika dalam mode komentar; untuk fungsi bersarang, peringatan ini menyebut leluhur tingkat atas yang dilewati), eval('...') statis hanya melaporkan VMDynamicCodeSkipped, sedangkan indirect eval dan new Function yang sepenuhnya statis tidak melaporkan apa pun. Pemanggilan dinamis di tingkat atas berkas, di luar fungsi mana pun, tidak pernah dilaporkan. Lihat Mendeteksi masalah sebelum runtime di bawah untuk jenis peringatan dan cuplikan CI.

Pelewatan ini merambat melalui IIFE. Jika IIFE tingkat atas membungkus seluruh bundel Anda dan ada fungsi di dalamnya yang menggunakan direct eval (statis maupun dinamis) atau pemanggilan new Function / Function dinamis, seluruh IIFE dikecualikan dari kompilasi bytecode VM. Lihat Direct eval() Menonaktifkan Obfuskasi VM untuk pola membongkar IIFE yang membatasi dampaknya.

Mengapa statis dan dinamis diperlakukan berbeda

eval(s) dapat membaca dan menulis variabel lokal dari fungsi tempat ia dipanggil. Ketika s berupa literal string, obfuscator dapat mengurai isinya pada saat obfuskasi dan mengganti nama pengenal secara konsisten dengan kode di sekitarnya. Ketika s berupa ekspresi dinamis, parsing baru terjadi saat runtime, dan pada saat itu pengenal sudah diganti namanya, sehingga kode sumber yang dibangun saat runtime mereferensikan nama lama yang sudah tidak ada.

new Function(s) dan indirect eval bekerja secara berbeda: isinya selalu berjalan di cakupan global, hanya dengan akses ke variabel global dan tidak pernah ke variabel lokal di sekitar pemanggilan. Keduanya tidak dapat bergantung pada variabel lokal yang diganti namanya, tetapi tetap dapat gagal jika mereferensikan global yang diganti namanya atau dihapus, atau jika Anda membangun isinya dengan menggabungkan pengenal yang diganti namanya ke dalamnya (misalnya melalui func.toString() dari fungsi yang bagian dalamnya sudah ditulis ulang oleh obfuscator).

Isi statis yang hanya menggunakan parameternya sendiri, seperti new Function('a', 'b', 'return a + b'), tidak memiliki ketergantungan semacam itu: isinya adalah string biasa yang tidak pernah diperiksa oleh proses penggantian nama. Obfuscator membiarkan pemanggilan tersebut apa adanya dan fungsi di sekitarnya tetap memenuhi syarat untuk kompilasi bytecode VM. Mengubah sintaks eval saja tidak membuat kode dinamis sembarang menjadi aman; tinjau peringatannya dan uji bundel akhirnya.

Error yang Anda lihat saat runtime

Gejala yang umum adalah ReferenceError saat fungsi yang dibangun secara dinamis pertama kali dijalankan:

Teks

TU di sini adalah pengenal yang diganti namanya dan diperkenalkan obfuscator di dalam cakupan IIFE bundel. Pemanggilan eval dinamis / konstruktor Function mengevaluasi isi yang mereferensikannya, tetapi isi tersebut berjalan di cakupan tempat TU tidak terdefinisi.

Mendeteksi masalah sebelum runtime

Obfuscator mengeluarkan peringatan non-fatal melalui API sehingga Anda dapat menangkap sebagian pola ini di CI sebelum dirilis. Dua jenis peringatan yang relevan. Keduanya hanya dilaporkan untuk pemanggilan di dalam sebuah fungsi, kecuali kasus .toString(), yang dilaporkan di mana pun pemanggilannya berada ketika fungsi yang diserialisasi bersarang di dalam fungsi lain atau tidak dapat ditentukan (hanya build VM):

  • DynamicCodeRenameRisk - sebuah fungsi membangun kode dari string saat runtime: direct eval atau pemanggilan new Function / Function yang isinya tidak statis, atau fn.toString() yang disisipkan ke dalam <script> atau Worker. Inilah peringatan yang memprediksi ReferenceError.
  • VMDynamicCodeSkipped - kompilasi bytecode VM dilewati untuk sebuah fungsi karena fungsi tersebut memuat direct eval atau pemanggilan new Function / Function dinamis. Dalam mode root (bawaan), setiap fungsi yang didefinisikan di dalamnya juga dilewati; dalam mode komentar, fungsi bersarang yang Anda tandai secara terpisah tetap divirtualisasi kecuali fungsi itu sendiri memuat pemanggilan tersebut. Peringatan ini juga muncul untuk eval('literal') statis yang aman, sehingga menandakan hilangnya proteksi VM, bukan kegagalan saat runtime. Mencakup nama fungsi (jika tersedia) dan konstruksi yang memicu pelewatan tersebut.

Paket npm javascript-obfuscator tidak mengekspos peringatan pada hasilnya, sehingga gerbang CI membacanya dari respons API: pesan result dan chunk_end membawa array warnings. Contoh di bawah memakai readObfuscationResponse(), pembaca stream dari Referensi API, dan menggagalkan build hanya pada DynamicCodeRenameRisk; tambahkan VMDynamicCodeSkipped ke filter jika hilangnya proteksi VM pada sebuah fungsi juga harus memblokir rilis.

Kode

Jika sebuah jenis peringatan memang sudah diperkirakan dalam build Anda dan Anda lebih suka membisukannya di sumbernya daripada menyaringnya di CI, opsi warnings (v7.8.0+) mengatur peringatan mana yang dikeluarkan: 'none' menekan semuanya, dan peta per jenis seperti { VMDynamicCodeSkipped: false } hanya membisukan satu jenis sambil mempertahankan sisanya.

Solusi alternatif

  • Beralih ke indirect eval ((0, eval)(s))

    Hanya berguna untuk kasus eval. Indirect eval berjalan di cakupan global, sehingga tidak dapat melihat variabel lokal di sekitarnya, dan karena itu juga tidak dapat mereferensikan variabel lokal yang diganti namanya. Fungsi yang memuat pemanggilan tersebut tetap dikompilasi menjadi bytecode VM. Kode yang dievaluasi tetap tidak boleh bergantung pada global yang diganti namanya oleh obfuscator.

  • Buat isinya sepenuhnya statis

    Untuk new Function, jika Anda dapat menyatakan isinya sebagai satu literal string / template literal tanpa interpolasi, pemanggilan tersebut tidak membawa risiko penggantian nama dan fungsi di sekitarnya tetap dikompilasi menjadi bytecode. new Function('a', 'b', 'return a + b') aman; new Function('return ' + expr) tidak.

  • Pindahkan pemanggilan ke fungsi tingkat atasnya sendiri, dan bongkar IIFE-nya

    Setiap fungsi tingkat atas diperiksa secara independen. Memindahkan pemanggilan kode dinamis ke fungsi tingkat atasnya sendiri berarti hanya fungsi itu yang kehilangan kompilasi bytecode VM, alih-alih pelewatan merambat melalui IIFE tingkat atas yang membungkus seluruh bundel Anda.

  • Beralih ke vmTargetFunctionsMode: 'comment'

    Mode opt-in: hanya fungsi yang ditandai dengan /* javascript-obfuscator:vm */ yang dikompilasi menjadi bytecode. Jangan beri anotasi pada fungsi yang memuat pemanggilan kode dinamis, dan tandai sisanya. Pertahankan komentar ini sampai tahap obfuskasi. Lihat Menargetkan Fungsi Tertentu.

  • Timpa pelewatan dengan vmForceCompileDynamicCode: true (v6.14.0+)

    Jalan keluar terakhir. Ketika diaktifkan, obfuscator tetap mengompilasi fungsi di sekitarnya menjadi bytecode dan menekan peringatan VMDynamicCodeSkipped. Opsi ini tidak dapat memperbaiki cakupan: di dalam fungsi yang dikompilasi paksa, direct eval tidak dapat membaca atau menulis variabel lokal atau parameter fungsi tersebut, maupun variabel dari fungsi pembungkus yang juga divirtualisasi, bahkan ketika kodenya berupa literal string. Pembacaan melempar ReferenceError, dan penulisan hilang tanpa pemberitahuan. eval('...') statis dalam situasi ini sama sekali tidak melaporkan peringatan, sehingga gerbang CI tidak akan menangkapnya; DynamicCodeRenameRisk tetap hanya muncul untuk isi yang dinamis. Gunakan opsi ini hanya jika kode yang dievaluasi tidak mereferensikan apa pun selain global. Di dasbor, ini adalah sakelar Force Compile Dynamic Code di bawah grup Penimpaan pada bagian VM.

Halaman terkait