Làm rối VM với eval và new Function
Cách làm rối VM xử lý việc dựng mã động (direct eval và hàm khởi tạo Function), phần nào được chuyển thành bytecode và phần nào bị bỏ qua, các cảnh báo mà trình làm rối phát ra, cùng cách chẩn đoán ReferenceError khi chạy.
Vì sao điều này quan trọng
Làm rối VM biên dịch thân hàm xuống thành bytecode được điều phối qua một bộ thông dịch nằm trong runtime. Các định danh trong
phạm vi bao quanh cũng được đổi tên. Cả hai phép biến đổi này đều tương tác không tốt với mã được dựng từ một chuỗi khi
chạy: eval(s), new Function(...s), và Function(...s). Nếu mã được dựng khi chạy tham chiếu đến một định danh mà
trình làm rối đã đổi tên, bạn sẽ nhận Uncaught ReferenceError: <renamed-name> is not defined ngay lần đầu tiên hàm được sinh ra
chạy.
Trình làm rối xử lý mỗi mẫu một cách khác nhau. Bảng dưới đây là bản tóm tắt; phần còn lại của trang này giải thích từng ô.
Trình làm rối làm gì, nhìn thoáng qua
| Mẫu trong mã nguồn của bạn | Điều gì xảy ra |
|---|---|
eval('literal string') (thân là một chuỗi ký tự) | Chạy đúng. Hàm chứa lệnh gọi này mất khả năng bytecode hóa VM (direct eval đọc các biến cục bộ xung quanh, vốn không được VM giữ lại một khi một hàm đã được biên dịch thành bytecode). |
eval(dynamicExpression) | Có thể sập khi chạy với ReferenceError. Hàm chứa lệnh gọi này — và mọi hàm được định nghĩa bên trong nó — cũng mất khả năng bytecode hóa VM. |
(0, eval)(s) / window.eval(s) (gián tiếp) | Chạy đúng. Hàm chứa lệnh gọi này được bytecode hóa VM bình thường. Eval gián tiếp không thể thấy các biến cục bộ xung quanh, nên việc đổi tên không thể phá vỡ nó. |
new Function('a', 'b', 'return a + b') (mọi đối số đều là chuỗi ký tự) | Chạy đúng. Hàm chứa lệnh gọi này được bytecode hóa VM bình thường. |
new Function(dynamicBody) / Function(dynamicBody) | Có thể sập khi chạy với ReferenceError. Hàm chứa lệnh gọi này — và mọi hàm được định nghĩa bên trong nó — cũng mất khả năng bytecode hóa VM. |
Tất cả các mẫu này cũng được đưa ra dưới dạng cảnh báo không nghiêm trọng trên kết quả làm rối — xem Phát hiện vấn đề trước khi chạy bên dưới để biết dạng của cảnh báo và một đoạn mã cho CI.
Vì sao tĩnh và động được xử lý khác nhau
eval(s) có thể đọc và ghi các biến cục bộ của hàm mà nó được gọi bên trong. Khi s là một chuỗi ký tự, trình
làm rối có thể phân tích thân đó tại thời điểm làm rối và đổi tên các định danh sao cho nhất quán với mã xung quanh. Khi
s là một biểu thức động, việc phân tích chỉ diễn ra khi chạy — mà tới lúc đó các định danh đã được
đổi tên rồi, nên mã dựng khi chạy tham chiếu đến các tên cũ không còn tồn tại nữa.
new Function(s) hoạt động khác: thân hàm luôn chạy như thể nó được định nghĩa ở đầu tệp của bạn, chỉ có quyền truy cập
tới các biến toàn cục và không bao giờ tới các biến cục bộ xung quanh lệnh gọi. Bản thân điều đó là an toàn — nhưng nếu bạn dựng thân
bằng cách nối một định danh đã đổi tên vào trong nó (ví dụ qua func.toString() của một hàm mà trình làm rối đã
viết lại phần bên trong), hàm được biên dịch khi chạy vẫn sẽ gặp đúng loại ReferenceError.
new Function('return 42') tĩnh không bao giờ mang rủi ro này: thân hàm là một chuỗi thuần mà bộ đổi tên không bao giờ xem xét, và
khi chạy nó chỉ cần thấy các biến toàn cục. Trình làm rối giữ nguyên lệnh gọi tại chỗ và hàm xung quanh vẫn
đủ điều kiện để bytecode hóa VM.
Lỗi bạn thấy khi chạy
Triệu chứng thường gặp là một ReferenceError ngay lần đầu tiên hàm được dựng động chạy:
TU ở đây là một định danh đã đổi tên mà trình làm rối đưa vào bên trong phạm vi IIFE của bundle. Lệnh gọi eval động /
hàm khởi tạo Function đánh giá một thân tham chiếu đến nó, nhưng thân đó chạy trong một phạm vi mà TU không được định nghĩa.
Phát hiện vấn đề trước khi chạy
Trình làm rối phát ra các cảnh báo không nghiêm trọng qua API để bạn có thể bắt các mẫu này trong CI trước khi phát hành. Hai loại cảnh báo có liên quan:
DynamicCodeRenameRisk— một hàm chứa lệnh gọieval/new Function/Functionđộng mà thân của nó được dựng khi chạy.VMDynamicCodeSkipped— việc bytecode hóa VM đã bị bỏ qua cho một hàm do một trong các mẫu ở trên. Bao gồm tên hàm (nếu có) và loại cấu trúc đã kích hoạt việc bỏ qua.
Nếu một loại cảnh báo là dự kiến trong bản build của bạn và bạn muốn tắt nó ngay tại nguồn thay vì lọc trong CI, tùy chọn
warnings (v7.8.0+) kiểm soát những gì getWarnings() phát ra: 'none' chặn mọi thứ, và một map theo từng loại
như { VMDynamicCodeSkipped: false } chỉ tắt tiếng một loại trong khi vẫn giữ những loại còn lại.
Cách xử lý
Chuyển sang eval gián tiếp (
(0, eval)(s))Chỉ hữu ích cho trường hợp
eval. Eval gián tiếp chạy trong phạm vi toàn cục, nên nó không thể thấy các biến cục bộ xung quanh — nhưng cũng vì lý do đó, nó không thể tham chiếu đến các định danh đã đổi tên. Hàm chứa lệnh gọi vẫn được bytecode hóa VM.Làm cho thân hàm hoàn toàn tĩnh
Với
new Function, nếu bạn có thể diễn đạt thân dưới dạng một chuỗi ký tự / template literal duy nhất không có nội suy nào, lệnh gọi sẽ không mang rủi ro đổi tên và hàm xung quanh nó vẫn được bytecode hóa.new Function('a', 'b', 'return a + b')thì ổn;new Function('return ' + expr)thì không.Chuyển lệnh gọi vào một hàm cấp gốc riêng, và tháo IIFE
Mỗi hàm cấp gốc được kiểm tra độc lập. Nâng lệnh gọi dựng mã động vào trong một hàm cấp gốc riêng nghĩa là chỉ mỗi hàm đó mất khả năng bytecode hóa VM, thay vì để việc bỏ qua lan truyền qua một IIFE cấp gốc vốn bao trọn cả bundle của bạn.
Chuyển sang
vmTargetFunctionsMode: 'comment'Chế độ chọn tham gia: chỉ những hàm được đánh dấu bằng
/* javascript-obfuscator:vm */mới được bytecode hóa. Hãy bỏ chú thích cho hàm chứa lệnh gọi mã động, và bytecode hóa phần còn lại. Xem Nhắm mục tiêu hàm.Ghi đè việc bỏ qua với
vmForceCompileDynamicCode: true(v6.14.0+)Lối thoát cuối cùng. Khi được bật, trình làm rối vẫn bytecode hóa hàm xung quanh và chặn cảnh báo
VMDynamicCodeSkipped. Chỉ dùng khi bạn có thể đảm bảo rằng thân được dựng khi chạy không bao giờ tham chiếu đến một định danh mà trình làm rối đổi tên — nếu không bạn đánh đổi một lần làm rối sạch để lấy mộtReferenceErrorkhi chạy.DynamicCodeRenameRiskvẫn được kích hoạt để CI có thể tiếp tục kiểm soát dựa vào nó. Trong bảng điều khiển, đây là công tắc "Force Compile Dynamic Code" nằm dưới nhóm Overrides của phần VM.
Trang liên quan
- Làm rối VM — Hành vi của direct eval — mẫu tháo IIFE để giới hạn phạm vi ảnh hưởng của việc bỏ qua.
- Làm rối VM — Nhắm mục tiêu hàm — dùng
vmTargetFunctionsModeđể chọn tham gia hoặc loại trừ ở mức độ từng hàm.
