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 Anda | Apa 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:
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:evallangsung atau pemanggilannew Function/Functionyang isinya tidak statis, ataufn.toString()yang disisipkan ke dalam<script>atau Worker. Inilah peringatan yang memprediksiReferenceError.VMDynamicCodeSkipped- bytecoding VM dilewati untuk sebuah fungsi, beserta setiap fungsi yang didefinisikan di dalamnya, karena fungsi tersebut memuatevallangsung atau pemanggilannew Function/Functiondinamis. Peringatan ini juga muncul untukeval('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.
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 denganReferenceErrorsaat runtime.DynamicCodeRenameRisktetap 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
- Perilaku Direct eval - pola membongkar IIFE untuk membatasi dampak pelewatan.
- Menargetkan Fungsi - menggunakan
vmTargetFunctionsModeuntuk memilih ikut atau tidak pada tingkat fungsi.
