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 Anda | Apa 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:
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: directevalatau pemanggilannew Function/Functionyang isinya tidak statis, ataufn.toString()yang disisipkan ke dalam<script>atau Worker. Inilah peringatan yang memprediksiReferenceError.VMDynamicCodeSkipped- kompilasi bytecode VM dilewati untuk sebuah fungsi karena fungsi tersebut memuat directevalatau pemanggilannew Function/Functiondinamis. 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 untukeval('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.
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, directevaltidak 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 melemparReferenceError, dan penulisan hilang tanpa pemberitahuan.eval('...')statis dalam situasi ini sama sekali tidak melaporkan peringatan, sehingga gerbang CI tidak akan menangkapnya;DynamicCodeRenameRisktetap 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
- Direct eval() Menonaktifkan Obfuskasi VM - pola membongkar IIFE untuk membatasi dampak pelewatan.
- Menargetkan Fungsi Tertentu - menggunakan
vmTargetFunctionsModeuntuk memilih ikut atau tidak pada tingkat fungsi.
