Tài liệu
/
Công thức
/

Ẩn tên hàm khỏi phân tích của LLM

Ẩn tên hàm khỏi phân tích của LLM

Pro

Vấn đề

Bạn đã bật vmObfuscation: true, chạy nó trên một tệp có chứa một hàm như validateLicense, và nhận thấy rằng đầu ra đã làm rối vẫn chứa nguyên văn validateLicense: phần thân đã biến mất, được thay bằng bytecode, nhưng chính cái tên vẫn nằm đó, ai cũng nhìn thấy.

JavaScript

Nhân điều đó lên trên một cơ sở mã thực tế, bạn sẽ có một danh sách tên hàm như validateLicense, decryptPayload, processPayment, checkSubscription. Một LLM không cần bẻ khóa bytecode để hiểu chương trình làm gì: chỉ riêng các cái tên đã đủ để nó đưa ra một bản tóm tắt tự tin và chính xác về hành vi của mô-đun. Bytecode thì mờ đục; còn mục lục thì không.

Vì sao làm rối VM giữ lại các tên này

Với vmTargetFunctionsMode: 'root' (mặc định), trình làm rối biến đổi phần thân của mọi hàm cấp gốc thành bytecode VM nhưng cố ý giữ nguyên tên. Về mặt ngữ nghĩa, một khai báo hàm cấp gốc là một ràng buộc trên phạm vi bao quanh: với một script, đó là đối tượng toàn cục; và với một mô-đun, đó là phạm vi mô-đun (một hàm cấp cao nhất không được export có phạm vi mô-đun, không phải toàn cục, nhưng vẫn là hàm cấp gốc trong tệp). Trình làm rối không thể đổi tên nó một cách an toàn vì không có cách nào biết còn ai tham chiếu đến nó: một bundle khác, một <script> nội tuyến, một thuộc tính HTML onclick="validateLicense(...)", một lần tra cứu động window['validateLicense'], v.v.

Vì vậy, sự đánh đổi mà mặc định chọn là: bảo vệ phần cài đặt, giữ nguyên bề mặt công khai. Điều đó giúp việc tích hợp không bị hỏng, nhưng cũng có nghĩa là một LLM được tặng miễn phí một bảng chỉ mục của mọi điểm vào. Khi điều này xảy ra, kết quả làm rối sẽ báo cảnh báo VMGlobalFunctionNamesNotRenamed, liệt kê những tên vẫn còn đọc được.

Vì sao điều này quan trọng với dịch ngược có LLM hỗ trợ

Một kẻ tấn công là con người, khi đối mặt với vài trăm dòng điều phối bytecode, thường sẽ bỏ cuộc. Một LLM nhận cùng tệp đó sẽ chẳng buồn tấn công bytecode: nó sẽ đọc các cái tên, đối chiếu với vài chuỗi ký tự mà nó thấy được, rồi đưa ra kết quả kiểu như:

"Mô-đun này kiểm soát quyền truy cập một tính năng trả phí. validateLicense xác minh một token đã ký, checkExpiry từ chối giấy phép hết hạn, và activateFeature mở khóa giao diện sau khi kiểm tra thành công. Hàm hỗ trợ giải mã nằm trong decryptPayload."

Bản tóm tắt đó đủ để kẻ tấn công lên kế hoạch vượt qua có chủ đích mà không cần động đến VM. Chính các cái tên là chỗ rò rỉ.

Cách khắc phục: bọc mã của bạn trong một IIFE

Cách đơn giản và chắc chắn nhất để loại bỏ rò rỉ này là đẩy các hàm nhạy cảm xuống sâu thêm một cấp trong cây phạm vi. Các hàm được khai báo bên trong một hàm khác không phải là hàm cấp gốc, nên trình làm rối có thể tự do đổi tên chúng và đưa khai báo của chúng vào bytecode như mọi câu lệnh khác.

IIFE (Immediately-Invoked Function Expression, biểu thức hàm được gọi ngay) là cách nhẹ nhất để làm điều đó: nó chỉ thêm một hàm bọc duy nhất, chạy một lần và không để lộ gì qua tên.

Trước: tên bị lộ

JavaScript

Sau khi làm rối VM, cả validateLicense lẫn checkExpiry vẫn giữ nguyên tên trong đầu ra.

Sau: tên được ẩn sau một IIFE

JavaScript

Giờ đây cả hai khai báo hàm đều nằm trong thân của IIFE. Bản thân IIFE là cấu trúc cấp gốc duy nhất, và một IIFE ẩn danh không có tên nào để rò rỉ. Sau khi làm rối VM, các tên hàm được đổi và phần thân được chuyển thành bytecode.

Điều gì đã thay đổi. Các hàm không còn truy cập được dưới dạng biến toàn cục (chính điều đó đã làm lộ tên của chúng). Chúng vẫn gọi được, chỉ là từ bên trong cùng IIFE đó. Nếu thật sự có thứ gì cần gọi validateLicense từ bên ngoài, thì giờ nó không tới được hàm đó nữa; xem mẫu trampoline bên dưới. Nếu không có gì như vậy, bạn chẳng mất gì cả.

Các tên được export, định danh dành riêng, chuỗi và hành vi quan sát được vẫn có thể lộ ra. Hãy kiểm thử các phần tích hợp sau khi bọc, đặc biệt là biến toàn cục, export của mô-đun và mã kiểm tra tên hàm.

Một đánh đổi cần biết: nếu thân IIFE chứa một lệnh gọi eval trực tiếp, hoặc một lệnh gọi new Function(...) / Function(...) với thân hàm động, làm rối VM sẽ bỏ qua toàn bộ hàm đó cùng mọi thứ lồng bên trong (kèm cảnh báo VMDynamicCodeSkipped), vì mã nguồn được dựng lúc chạy có thể tham chiếu đến các định danh mà trình làm rối đã đổi tên. Khi đó phần mã bạn vừa chuyển vào IIFE sẽ quay về làm rối thông thường và mất lớp bảo vệ bytecode. Hãy dùng eval gián tiếp - (0, eval)(...) - hoặc xem Hành vi của direct eval để biết các lựa chọn.

Nếu một hàm thật sự cần là biến toàn cục thì sao?

Đôi khi một hàm thật sự là điểm vào công khai: một trình xử lý sự kiện nội tuyến, một callback JSONP, một hook của SDK bên thứ ba. Bạn có hai lựa chọn:

  • Để lộ một hàm trung chuyển mỏng, giữ logic bên trong IIFE. Khai báo một hàm bọc toàn cục nhỏ chỉ có nhiệm vụ gọi vào phần cài đặt nằm trong phạm vi IIFE. Tên hàm trung chuyển vẫn bị lộ, nhưng không mang thông tin ngữ nghĩa nào: hãy đặt tên là __entry1 hoặc tương tự, và toàn bộ logic có ý nghĩa vẫn được giấu kín.

    JavaScript

  • Viết lại nơi gọi. Nếu biến toàn cục chỉ tồn tại vì một onclick="validateLicense(...)" nội tuyến cần đến nó, hãy thay trình xử lý nội tuyến bằng addEventListener từ bên trong IIFE. HTML không còn nêu tên hàm, hàm không còn cần là biến toàn cục, và rò rỉ biến mất hoàn toàn.

Bộ khởi tạo biến cấp cao nhất: vmWrapTopLevelInitializers

Khai báo hàm không phải là thứ duy nhất nằm ở cấp gốc của một tệp. Bộ khởi tạo biến cấp cao nhất (hằng chuỗi, đối tượng cấu hình, bảng tra cứu) cũng đọc được y như vậy trong đầu ra khi chúng vẫn là JavaScript thuần. Một dòng như const API_BASE = '/api/v2/license' cho LLM biết nhiều không kém function validateLicense.

Tùy chọn vmWrapTopLevelInitializers (boolean, mặc định false; các preset VM hiện tại bật nó) bọc các bộ khởi tạo cấp cao nhất đủ điều kiện trong một IIFE, để chính giá trị được tính bằng bytecode VM lúc chạy thay vì nằm trong mã nguồn dưới dạng giá trị literal. Ở chế độ root, các bộ khởi tạo vẫn là JavaScript thuần được báo bằng cảnh báo VMTopLevelInitializerNotVirtualized.

Không có tùy chọn này

JavaScript

Với vmWrapTopLevelInitializers: true

JavaScript

Tên ràng buộc (MY_STRING) vẫn ở cấp gốc vì cùng lý do với tên hàm (có thể có thứ gì đó bên ngoài tệp tham chiếu đến nó), nhưng giá trị mà nó giữ giờ được VM tạo ra và không còn xuất hiện dưới dạng văn bản đọc được.

Chỉ có hiệu lực khi vmTargetFunctionsMode là 'root' (mặc định) và vmAsyncExecutor tắt. Ở chế độ comment, tùy chọn này không có tác dụng và không có cảnh báo nào được báo. Ở chế độ root khi dùng vmAsyncExecutor (chỉ ảo hóa các hàm async), nó cũng không có tác dụng, và bản dựng báo cáo VMTopLevelInitializerNotVirtualized cho các bộ khởi tạo còn lại ở dạng JavaScript thuần.

Nếu một tệp không có hàm nào để VM ảo hóa, kết quả sẽ báo VMNoFunctionsToVirtualize; hãy bọc phần mã bạn muốn bảo vệ trong một hàm, hoặc dùng chế độ comment để chọn tường minh các hàm nhạy cảm.

Khi như vậy vẫn chưa đủ

  • Tên được import từ các mô-đun khác. Nếu bạn gộp nhiều tệp và một mô-đun export validateLicense để mô-đun khác import, bundler sẽ giữ tên đó lộ ra trong đầu ra đã gộp, giống như cách các hàm cấp gốc bị lộ. Hãy bọc chính bundle trong một IIFE (hầu hết bundler đều làm được), hoặc chuyển export vào trong một IIFE và để lộ lại qua một hàm trung chuyển có tên vô nghĩa.
  • Môi trường chạy vẫn quan sát được. Làm rối làm tăng công sức cần thiết để hiểu và sửa mã; nó không thể bảo đảm rằng việc dịch ngược là bất khả thi. Hãy giữ bí mật và các quyết định bảo mật có thẩm quyền ở phía máy chủ.

Đừng vội dùng renameGlobals trước tiên. Tùy chọn này có tồn tại và sẽ đổi tên các định danh cấp gốc, nhưng nó không có cách nào biết định danh nào trong số đó được tham chiếu từ bên ngoài tệp (bundle khác, HTML nội tuyến, tra cứu động). Bật nó thường làm hỏng việc tích hợp theo những cách khó nhận ra. Bọc bằng IIFE an toàn hơn: nó không đổi tên bất cứ thứ gì toàn cục, nó chỉ ngừng tạo ra những biến toàn cục mà bạn không cần.