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 di-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 di-dispatch melalui interpreter di dalam runtime. Pengenal di scope 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 source AndaApa yang terjadi
eval('literal string') (isinya berupa literal string)Berjalan dengan benar. Fungsi yang memuat pemanggilan ini, beserta setiap fungsi yang didefinisikan di dalamnya, kehilangan bytecoding VM (direct eval membaca variabel lokal di sekitarnya, yang tidak dipertahankan oleh VM setelah sebuah fungsi dikompilasi menjadi bytecode).
eval(dynamicExpression)Dapat crash saat runtime dengan ReferenceError. Fungsi yang memuat pemanggilan ini, beserta setiap fungsi yang didefinisikan di dalamnya, juga kehilangan bytecoding VM.
(0, eval)(s) / window.eval(s) (indirect)Fungsi yang memuat pemanggilan ini di-bytecode VM seperti biasa. Indirect eval berjalan di scope 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 di-bytecode VM seperti biasa.
new Function(dynamicBody) / Function(dynamicBody)Dapat crash saat runtime dengan ReferenceError. Fungsi yang memuat pemanggilan ini, beserta setiap fungsi yang didefinisikan di dalamnya, juga kehilangan bytecoding VM.

Compiler juga mendeteksi eval?.(code) secara konservatif dan menanganinya seperti direct eval. JavaScript mendefinisikan bentuk optional-call 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, eval('...') statis hanya melaporkan VMDynamicCodeSkipped, sedangkan indirect eval dan new Function yang sepenuhnya statis tidak melaporkan apa pun. Pemanggilan dinamis di tingkat atas file, 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 eval dinamis atau new Function, seluruh IIFE dilewati dari bytecoding VM. Lihat Perilaku Direct eval 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 mem-parse 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 source yang dibangun saat runtime mereferensikan nama lama yang sudah tidak ada.

new Function(s) dan indirect eval bekerja secara berbeda: isinya selalu berjalan di scope 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 bytecoding 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:

Text

TU di sini adalah pengenal yang diganti namanya dan diperkenalkan obfuscator di dalam scope IIFE bundel. Pemanggilan eval dinamis / konstruktor Function mengevaluasi isi yang mereferensikannya, tetapi isi tersebut berjalan di scope 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, dan keduanya hanya dilaporkan untuk pemanggilan di dalam sebuah fungsi:

  • DynamicCodeRenameRisk - sebuah fungsi membangun kode dari string saat runtime: eval langsung 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 - bytecoding VM dilewati untuk sebuah fungsi, beserta setiap fungsi yang didefinisikan di dalamnya, karena fungsi tersebut memuat eval langsung atau pemanggilan new Function / Function dinamis. Peringatan ini juga muncul untuk eval('literal') statis yang aman, sehingga menandakan hilangnya proteksi VM, bukan crash 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.

Code

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 scope 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 di-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 di-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 bytecoding 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 di-bytecode. Jangan beri anotasi pada fungsi yang memuat pemanggilan kode dinamis, dan tandai sisanya. Pertahankan komentar ini sampai tahap obfuskasi. Lihat Menargetkan Fungsi.

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

    Jalan keluar terakhir. Ketika diaktifkan, obfuscator tetap mem-bytecode fungsi di sekitarnya dan menekan peringatan VMDynamicCodeSkipped. Opsi ini tidak dapat memperbaiki ketergantungan pada scope atau pada pengenal yang diganti namanya: gunakan hanya jika Anda dapat menjamin bahwa isi yang dibangun saat runtime tidak pernah mereferensikan pengenal yang diganti namanya oleh obfuscator; jika tidak, Anda menukar obfuskasi yang bersih dengan ReferenceError saat runtime. DynamicCodeRenameRisk tetap muncul sehingga CI tetap dapat memblokir berdasarkan peringatan itu. Di dasbor, ini adalah sakelar "Force Compile Dynamic Code" di bawah grup Overrides pada bagian VM.

Halaman terkait