Tài liệu
/

Khắc phục sự cố

/

Làm rối VM với eval và new Function

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à obfuscator phát ra, và 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 thành bytecode được điều phối qua một trình thông dịch nằm trong runtime. Các định danh trong phạm vi bao quanh cũng bị đổi tên. Cả hai phép biến đổi này đều tương tác kém với mã được dựng từ chuỗi lúc chạy: eval(s), new Function(...s) và Function(...s). Nếu mã dựng lúc chạy tham chiếu tới một định danh mà obfuscator đã đổi tên, bạn sẽ gặp Uncaught ReferenceError: <renamed-name> is not defined ngay lần đầu hàm được sinh ra chạy.

Obfuscator 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 giải thích từng dòng.

Obfuscator làm gì, nhìn 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 literal)Chạy đúng. Hàm chứa lệnh gọi này, cùng mọi hàm được định nghĩa bên trong nó, không còn được chuyển thành bytecode VM (direct eval đọc các biến cục bộ bao quanh, mà VM không giữ lại khi một hàm đã được biên dịch thành bytecode).
eval(dynamicExpression)Có thể gặp lỗi khi chạy với ReferenceError. Hàm chứa lệnh gọi này, cùng mọi hàm được định nghĩa bên trong nó, cũng không còn được chuyển thành bytecode VM.
(0, eval)(s) / window.eval(s) (gián tiếp)Hàm chứa lệnh gọi này được chuyển thành bytecode VM bình thường. Eval gián tiếp chạy trong phạm vi toàn cục và không thấy được các biến cục bộ bao quanh, nên các biến cục bộ bị đổi tên không thể làm hỏng nó. Nó vẫn có thể lỗi nếu mã được thực thi tham chiếu tới một biến toàn cục đã bị đổi tên hoặc bị xóa.
new Function('a', 'b', 'return a + b') (mọi đối số đều là chuỗi literal)Chạy đúng. Hàm chứa lệnh gọi này được chuyển thành bytecode VM bình thường.
new Function(dynamicBody) / Function(dynamicBody)Có thể gặp lỗi khi chạy với ReferenceError. Hàm chứa lệnh gọi này, cùng mọi hàm được định nghĩa bên trong nó, cũng không còn được chuyển thành bytecode VM.

Trình biên dịch cũng phát hiện eval?.(code) theo hướng thận trọng và xử lý nó như direct eval. JavaScript định nghĩa dạng gọi tùy chọn này là eval gián tiếp; việc phát hiện của trình biên dịch không thay đổi hành vi đó của ngôn ngữ.

Chỉ một số mẫu trong số này tạo ra cảnh báo không nghiêm trọng, và chỉ khi lệnh gọi nằm bên trong một hàm: một lệnh gọi eval, new Function hoặc Function động báo cả DynamicCodeRenameRisk lẫn VMDynamicCodeSkipped, một lệnh gọi eval('...') tĩnh chỉ báo VMDynamicCodeSkipped, còn eval gián tiếp và một new Function hoàn toàn tĩnh không báo gì. Một lệnh gọi động ở cấp cao nhất của tệp, bên ngoài mọi hàm, không bao giờ được báo. Xem Phát hiện vấn đề trước khi chạy bên dưới để biết các loại cảnh báo và một đoạn mã CI.

Việc bỏ qua lan truyền qua các IIFE. Nếu một IIFE cấp cao nhất bọc toàn bộ bundle của bạn và bất kỳ hàm nào bên trong nó dùng eval hoặc new Function động, toàn bộ IIFE sẽ bị bỏ qua khi chuyển thành bytecode VM. Xem Hành vi của direct eval để biết mẫu gỡ bọc IIFE giúp giới hạn phạm vi ảnh hưởng.

Vì sao mã tĩnh và mã độ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 nơi nó được gọi. Khi s là một chuỗi literal, obfuscator có thể phân tích thân mã lúc làm rối và đổi tên các định danh nhất quán với mã bao quanh. Khi s là một biểu thức động, việc phân tích chỉ diễn ra lúc chạy, khi đó các định danh đã bị đổi tên, nên mã nguồn dựng lúc chạy tham chiếu tới những tên cũ không còn tồn tại.

new Function(s) và eval gián tiếp hoạt động khác: thân mã luôn chạy trong phạm vi toàn cục, chỉ truy cập được các biến toàn cục và không bao giờ truy cập được các biến cục bộ quanh lệnh gọi. Chúng không thể phụ thuộc vào biến cục bộ bị đổi tên, nhưng vẫn có thể lỗi nếu chúng tham chiếu tới các biến toàn cục đã bị đổi tên hoặc bị xóa, hoặc nếu bạn dựng thân mã bằng cách nối một định danh đã bị đổi tên vào đó (ví dụ qua func.toString() của một hàm mà obfuscator đã viết lại phần bên trong).

Một thân mã tĩnh chỉ dùng tham số của chính nó, như new Function('a', 'b', 'return a + b'), không có sự phụ thuộc như vậy: thân mã là một chuỗi thuần mà bộ đổi tên không bao giờ kiểm tra. Obfuscator giữ nguyên lệnh gọi và hàm bao quanh vẫn đủ điều kiện để chuyển thành bytecode VM. Chỉ đổi cú pháp eval thôi thì không làm cho mã động bất kỳ trở nên an toàn; hãy xem lại các cảnh báo và kiểm thử bundle cuối cùng.

Lỗi bạn gặp khi chạy

Triệu chứng thường gặp là một ReferenceError ngay lần đầu hàm được dựng động chạy:

Text

TU ở đây là một định danh đã đổi tên mà obfuscator tạo ra bên trong phạm vi IIFE của bundle. Lệnh gọi eval / hàm khởi tạo Function động thực thi một thân mã tham chiếu tới nó, nhưng thân mã đó 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

Obfuscator phát ra các cảnh báo không nghiêm trọng qua API để bạn có thể bắt một số mẫu này trong CI trước khi phát hành. Có hai loại cảnh báo liên quan, và cả hai chỉ được báo cho các lệnh gọi nằm bên trong một hàm:

  • DynamicCodeRenameRisk - một hàm dựng mã từ một chuỗi lúc chạy: một eval trực tiếp hoặc một lệnh gọi new Function / Function có thân mã không tĩnh, hoặc fn.toString() được chèn vào một <script> hay Worker. Đây là cảnh báo dự báo một ReferenceError.
  • VMDynamicCodeSkipped - một hàm, cùng mọi hàm được định nghĩa bên trong nó, đã bị bỏ qua khi chuyển thành bytecode VM vì nó chứa một eval trực tiếp hoặc một lệnh gọi new Function / Function động. Cảnh báo này cũng được phát ra cho một eval('literal') tĩnh an toàn, nên nó báo hiệu việc mất lớp bảo vệ VM chứ không phải một lỗi khi chạy. Bao gồm tên hàm (nếu có) và cấu trúc đã gây ra việc bỏ qua.

Gói npm javascript-obfuscator không cung cấp cảnh báo trong kết quả của nó, nên một cổng kiểm tra CI đọc chúng từ phản hồi của API: các thông điệp result và chunk_end mang một mảng warnings. Ví dụ dưới đây dùng readObfuscationResponse(), bộ đọc luồng từ Tham khảo API, và chỉ làm hỏng bản dựng khi có DynamicCodeRenameRisk; hãy thêm VMDynamicCodeSkipped vào bộ lọc nếu việc một hàm mất lớp bảo vệ VM cũng nên chặn bản phát hành.

Code

Nếu một loại cảnh báo là điều được dự kiến trong bản dựng của bạn và bạn muốn tắt nó ngay từ nguồn thay vì lọc trong CI, tùy chọn warnings (v7.8.0+) kiểm soát những cảnh báo nào được phát ra: 'none' tắt tất cả, còn một ánh xạ theo từng loại như { VMDynamicCodeSkipped: false } chỉ tắt một loại và giữ lại các loại còn lại.

Cách xử lý tạm thời

  • 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 không thấy được các biến cục bộ bao quanh, và cũng vì thế mà không thể tham chiếu tới các biến cục bộ đã bị đổi tên. Hàm chứa lệnh gọi vẫn được chuyển thành bytecode VM. Mã được thực thi vẫn không được phụ thuộc vào các biến toàn cục mà obfuscator đổi tên.

  • Làm cho thân mã hoàn toàn tĩnh

    Với new Function, nếu bạn có thể biểu diễn thân mã bằng một chuỗi literal / template literal duy nhất không có phép nội suy, lệnh gọi sẽ không có rủi ro đổi tên và hàm bao quanh vẫn được chuyển thành bytecode. 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 cao nhất riêng, và gỡ bọc IIFE

    Mỗi hàm cấp cao nhất được kiểm tra độc lập. Đưa lệnh gọi mã động vào một hàm cấp cao nhất riêng nghĩa là chỉ hàm đó không được chuyển thành bytecode VM, thay vì việc bỏ qua lan truyền qua một IIFE cấp cao nhất bọc toàn bộ bundle của bạn.

  • Chuyển sang vmTargetFunctionsMode: 'comment'

    Chế độ chọn tham gia: chỉ các hàm được đánh dấu bằng /* javascript-obfuscator:vm */ mới được chuyển thành bytecode. Đừng đánh dấu hàm chứa lệnh gọi mã động, và đánh dấu các hàm còn lại. Giữ nguyên các chú thích này cho đến bước làm rối. Xem Nhắm mục tiêu hàm.

  • Ghi đè việc bỏ qua bằng vmForceCompileDynamicCode: true (v6.14.0+)

    Lối thoát cuối cùng. Khi được bật, obfuscator vẫn chuyển hàm bao quanh thành bytecode và tắt cảnh báo VMDynamicCodeSkipped. Nó không thể sửa các phụ thuộc về phạm vi hay định danh bị đổi tên: chỉ dùng khi bạn có thể đảm bảo rằng thân mã dựng lúc chạy không bao giờ tham chiếu tới một định danh mà obfuscator đổi tên, nếu không bạn sẽ đổi một lần làm rối suôn sẻ lấy một ReferenceError khi chạy. DynamicCodeRenameRisk vẫn được phát ra để CI tiếp tục chặn dựa trên nó. Trong bảng điều khiển, đây là công tắc "Force Compile Dynamic Code" trong nhóm Ghi đè của phần VM.

Trang liên quan