Sản phẩm
Nhìn lại các cuộc tấn công Spectre từ xa trên hạ tầng Cloudflare Workers
(giờ Việt Nam)
Tóm tắt AI
Cloudflare đánh giá lại các lỗ hổng Spectre trên nền tảng Workers, phân tích các kỹ thuật tấn công mới và cách họ củng cố hệ thống để bảo vệ người dùng trước các mối đe dọa tiềm ẩn.
Bản dịch AI

Năm 2021, chúng tôi đã đánh giá các cuộc tấn công Spectre từ xa nhắm vào Cloudflare Workers. Dựa trên kết quả thu được, chúng tôi đã triển khai một cơ chế phòng thủ trong môi trường thực tế có tên là Dynamic Process Isolation (DyPrIs), giúp nhận diện các tập lệnh có dấu hiệu độc hại và cô lập chúng vào các tiến trình riêng biệt. Kể từ đó, nhiều kỹ thuật mới hơn trong lĩnh vực ổn định hóa các cuộc tấn công Spectre đã được phát hiện. Để hiểu liệu các kỹ thuật này có gây ra mối đe dọa cho môi trường thực tế của Workers hay không, chúng tôi đã quyết định đánh giá lại cuộc tấn công Spectre từ xa trong nội bộ. Việc xây dựng một bản chứng minh khái niệm (proof-of-concept) cập nhật trên môi trường thực tế đã cho phép chúng tôi đánh giá rủi ro của các cuộc tấn công Spectre dưới khối lượng công việc thực tế một cách thực nghiệm.
Để thực hiện thành công một cuộc tấn công kênh kề (side-channel attack) trong môi trường thực tế, kẻ tấn công bên ngoài phải vượt qua các trở ngại bổ sung như hoạt động trên các tài nguyên phần cứng dùng chung, ngắt (interrupts), chuyển đổi ngữ cảnh (context switches) và các bộ đếm thời gian có độ phân giải thấp (coarse-grained timers). Nghiên cứu của chúng tôi đã phát hiện ra một hạn chế trong quá trình triển khai DyPrIs và chúng tôi đã chứng minh được một cuộc tấn công Spectre từ xa có thể rò rỉ dữ liệu ổn định với tốc độ lên tới 12 bit/giây với độ chính xác 99% trong môi trường thực tế của Cloudflare Workers. Kết quả của nghiên cứu này là chúng tôi đã cải tiến DyPrIs, tích hợp V8 Sandbox và một cơ chế cô lập trong tiến trình (in-process isolation) để giảm thiểu hơn nữa rủi ro của các cuộc tấn công tiết lộ bộ nhớ.
Hôm nay, chúng tôi công bố một bài báo mô tả các phát hiện của mình, với sự đồng tác giả của Albert Pedersen, Haocheng Xiao, Sam Ainsworth, Nigel Topham và Martin Schwarzl. Bài báo này bao gồm các nghiên cứu được thực hiện trong năm 2024 và đầu năm 2025.
Lưu ý rằng cuộc tấn công được trình bày đã được giảm thiểu trong hệ thống thực tế nhờ các biện pháp đối phó do đội ngũ Cloudflare Workers Runtime áp dụng. Chúng tôi không tìm thấy bất kỳ dấu hiệu nào về việc khai thác thực tế trong ba năm qua.
Mô hình bảo mật của Cloudflare Workers
Cloudflare Workers chạy các mã JavaScript không đáng tin cậy tại biên (edge). Bằng cách tận dụng khả năng cô lập ở cấp độ ngôn ngữ dưới dạng V8 isolates, hàng chục nghìn người thuê có thể chia sẻ cùng một tiến trình hệ điều hành. Mỗi Worker có heap JavaScript riêng biệt. Thiết kế này giúp duy trì độ trễ khởi động thấp và cho phép chúng tôi chạy nhiều người thuê một cách hiệu quả so với việc cô lập toàn bộ tiến trình. Xung quanh môi trường runtime, chúng tôi có nhiều lớp phòng thủ như các quy trình vá lỗi V8 tự động, một sandbox hai lớp bao gồm Linux namespaces và seccomp filters, Cap’n Proto RPC, và khả năng lập lịch cho các tập lệnh nhất định trong các sandbox tiến trình riêng biệt. Tuy nhiên, một lỗ hổng đọc tùy ý (arbitrary read vulnerability) duy nhất trong một tiến trình Worker vẫn có thể dẫn đến rò rỉ dữ liệu giữa các người thuê. Một lỗ hổng rất khó giảm thiểu là lỗ hổng khai thác bản chất của thực thi suy đoán (speculative execution), cụ thể là Spectre trong tiến trình (in-process Spectre).
Spectre
Bạn có thể hình dung thực thi suy đoán giống như việc đi bộ đường dài. Tại một thời điểm nào đó, bạn đến một ngã rẽ và phải dự đoán nên đi hướng nào. Nếu dự đoán đúng, bạn tiết kiệm được thời gian và có thể tận hưởng ánh nắng cùng một món đồ uống giải khát tại một túp lều trên núi. Tuy nhiên, nếu bạn suy đoán sai hướng, bạn phải quay lại. Con đường trông có vẻ chưa có dấu chân, nhưng dấu chân của bạn đã để lại trong bùn.
Thực thi suy đoán trong CPU cũng hoạt động tương tự. Bộ dự đoán nhánh (branch prediction) thực hiện một dự đoán có cơ sở về kết quả của một nhánh trước thời hạn và CPU thực thi nó một cách suy đoán. Nếu dự đoán đúng, thực thi suy đoán giúp tiết kiệm thời gian. Tuy nhiên, nếu dự đoán sai, CPU phải loại bỏ các kết quả, quay lại và thực thi nhánh kia. Vì các lệnh được thực thi suy đoán này chỉ tồn tại tạm thời trong pipeline của CPU và không bao giờ được hoàn tất (retired) hoặc xác nhận (committed) vĩnh viễn, các tài liệu chuyên môn gọi chúng là các lệnh tạm thời (transient instructions) và khái quát hóa khái niệm này là thực thi tạm thời (transient execution).
Tuy nhiên, do thực thi tạm thời, vẫn còn một số dấu vết để lại trong trạng thái vi kiến trúc, ví dụ như trong các bộ nhớ đệm (cache) của CPU. Do đó, kẻ tấn công có thể sử dụng Spectre để truy cập bộ nhớ ngoài phạm vi một cách tạm thời, mã hóa một bit thông tin vào trạng thái bộ nhớ đệm và khai thác độ trễ khi truy cập lại dữ liệu để suy luận xem bit đó được thiết lập là 0 hay 1.
Để giảm thiểu các cuộc tấn công Spectre trong tiến trình, Cloudflare Workers đóng băng các bộ đếm thời gian cục bộ, không cho phép đa luồng và bộ nhớ dùng chung, đồng thời chủ động phát hiện, xáo trộn bộ nhớ định kỳ và cô lập các tập lệnh có dấu hiệu độc hại vào các tiến trình riêng biệt.
Các nguyên hàm tấn công (Attack primitives)
Nền tảng Cloudflare Workers hạn chế các bộ đếm thời gian một cách có chủ đích. Trong quá trình thực thi chỉ sử dụng CPU, thời gian thực tế bị đóng băng. Date.now và performance.now không cung cấp đồng hồ có độ phân giải cao liên tục tiến triển. Không có bộ nhớ dùng chung và không có đa luồng, vì vậy bộ đếm thời gian luồng đối nghịch cổ điển thông qua SharedArrayBuffer không khả dụng.
Để thực hiện thành công một cuộc tấn công, cần phải giải quyết một số thách thức. Thứ nhất, môi trường runtime của Workers bị hạn chế và việc đặt cùng vị trí (co-location) giữa kẻ tấn công và nạn nhân phải được đảm bảo. Thứ hai, cần tìm ra một bộ đếm thời gian từ xa đáng tin cậy, lý tưởng nhất là cùng vị trí, cho phép đo thời gian ổn định. Thứ ba, cuộc tấn công chạy trong điều kiện thực tế, nghĩa là nó đòi hỏi các biện pháp ổn định bổ sung như một Spectre gadget đáng tin cậy cho phép truy cập ngoài phạm vi 64-bit tạm thời, khuếch đại tín hiệu mạnh mẽ để xử lý nhiễu hệ thống và mạng, cùng một nguyên hàm để loại bỏ dữ liệu khỏi bộ nhớ đệm một cách đáng tin cậy.
Spectre gadget
Spectre gadget nhầm lẫn kiểu suy đoán (Speculative type confusion)
Với Spectre gadget phù hợp (đoạn mã trên), kẻ tấn công có thể truy cập tạm thời vào bộ nhớ ngoài phạm vi và mã hóa một bit vào bộ nhớ đệm (probeArray). Sau đó, kẻ tấn công đo độ trễ truy cập bộ nhớ để xác nhận xem dữ liệu đã được lưu vào bộ nhớ đệm hay chưa. Truy cập nhanh hơn có nghĩa là dòng dữ liệu đó đã được lưu vào bộ nhớ đệm và bit đó là 1. Ngược lại, truy cập chậm hơn có nghĩa là nó không được lưu vào bộ nhớ đệm và bit đó là 0. Trong cuộc tấn công của mình, chúng tôi sử dụng hai loại Spectre gadget khác nhau. Loại đầu tiên rò rỉ các con trỏ heap đã nén, ví dụ: địa chỉ cơ sở heap của isolate (root), và loại thứ hai tận dụng sự nhầm lẫn kiểu suy đoán để rò rỉ từ một con trỏ 64-bit không gian người dùng tùy ý do kẻ tấn công tạo ra. Tại thời điểm thực hiện nghiên cứu, V8 Sandbox vẫn chưa được triển khai trên Cloudflare Workers. Dưới cơ chế nén con trỏ, hầu hết các đối tượng sử dụng con trỏ nén 32-bit. TypedArray là một trong số ít ngoại lệ vẫn lưu trữ con trỏ 64-bit thô đến bộ nhớ lưu trữ của nó, đây chính xác là thứ mà gadget của chúng tôi lạm dụng.
Nhánh obj instanceof ObjP thực hiện kiểm tra kiểu, tức là một nhánh. Để huấn luyện sai bộ dự đoán nhánh, chúng tôi gọi gadget nhiều lần trên các thực thể ObjP thực, sau đó gọi nó trên một đối tượng khác với cấu trúc bộ nhớ do kẻ tấn công kiểm soát là ObjI. CPU suy đoán trên các nhánh được thực hiện và đi theo obj.ptr[0], mặc dù đối tượng có kiểu khác. Để rò rỉ một bit, chúng tôi che (mask) một bit và sử dụng nó để chọn một trong hai dòng probeArray. Việc dòng đó có được lưu vào bộ nhớ đệm hay không sẽ mã hóa bit đó.
Khai thác gadget rò rỉ heap, chúng tôi ánh xạ các đối tượng lân cận và định vị một mảng do kẻ tấn công kiểm soát. Gadget thứ hai của chúng tôi gây nhầm lẫn cho hai đối tượng lớn trải dài trên nhiều dòng bộ nhớ đệm, do đó trường kiểu (type field) nằm trên một dòng bộ nhớ đệm khác với trường mà chúng tôi đọc. Việc loại bỏ trường kiểu sẽ mở ra cửa sổ suy đoán trong khi trường mục tiêu vẫn được lưu trong bộ nhớ đệm, và việc đọc tạm thời sẽ đi theo một giá trị 64-bit do kẻ tấn công kiểm soát. Điều đó biến việc rò rỉ thành việc đọc địa chỉ tùy ý. Mô tả kỹ lưỡng hơn về kỹ thuật này có thể được tìm thấy trong bài báo.
Bản demo cục bộ về việc rò rỉ một địa chỉ 64-bit tùy ý.
Khuếch đại tín hiệu
Một lần truy cập bộ nhớ đệm thành công (cache hit) và một lần trượt (cache miss) khác nhau vài nano giây. Hơn nữa, một bộ đếm thời gian từ xa rất nhiễu ở quy mô vài micro giây đến vài mili giây. Do đó, cần một hình thức khuếch đại tín hiệu để phân biệt cache hit với cache miss. Stephen Röttger và Artur Janc đã phát hiện ra một cách để
, bằng cách khai thác chính sách thay thế bộ nhớ đệm dựa trên cây pseudo least recently used (PLRU) trong bộ nhớ đệm L1. PLRU dựa trên cây tổ chức mỗi tập hợp bộ nhớ đệm thành một cây nhị phân mà các nút của nó trỏ đến phía được sử dụng ít gần đây nhất, vì vậy CPU loại bỏ bằng cách đi theo các con trỏ đó. Với mẫu truy cập phù hợp, kẻ tấn công có thể giữ một dòng mục tiêu trong bộ nhớ đệm vô thời hạn bằng cách chạm vào nút lân cận trên cây của nó bất cứ khi nào các con trỏ hướng về phía mục tiêu. Khá thanh lịch, phải không? Tận dụng hành vi đó, thời gian của một sự kiện bộ nhớ đệm đơn lẻ có thể được khuếch đại tùy ý sao cho nó dẫn đến rất nhiều L1 hits (nhanh hơn) so với rất nhiều L1 misses trong trường hợp ngược lại.
Hình dưới đây minh họa liệu một địa chỉ bộ nhớ X có được lưu trong bộ nhớ đệm hay không. Nếu không, mẫu truy cập sẽ dẫn đến rất nhiều cache hits. Nếu nó có mặt, nó chiếm một nút trong cây, và sau đó bốn dòng bộ nhớ đệm cố gắng khớp vào ba nút, dẫn đến rất nhiều L1 misses.
Bộ đếm thời gian từ xa
Miễn là tín hiệu có thể được khuếch đại, một bộ đếm thời gian từ xa bị nhiễu là đủ để phân biệt một bit được mã hóa. Ví dụ, một kết nối WebSocket đến một máy chủ bên ngoài cung cấp các dấu thời gian độ phân giải cao là đủ. Bộ đếm thời gian có thể được lưu trữ tại Cloudflare hoặc tại một trung tâm dữ liệu cùng vị trí với trung tâm dữ liệu mục tiêu đang chạy Worker. Worker yêu cầu bộ đếm thời gian từ xa đánh dấu một dấu thời gian cho một sự kiện nhất định và tính toán delta cho một yêu cầu khác sau khi sự kiện đã dừng.
Trong bài báo, chúng tôi đã đánh giá một số thiết lập bộ đếm thời gian khác nhau và có thể đạt được độ phân giải dưới mili giây một cách đáng tin cậy trên giá trị trung vị (Median) chỉ với một vài mẫu ngay cả ở khoảng cách topo lớn hơn. Hình dưới đây cho thấy một sự kiện bộ nhớ đệm được khuếch đại bằng cách sử dụng khuếch đại PLRU dựa trên cây.
Các phép đo có thể lặp lại
Một phép đo đơn lẻ là không đủ để phân biệt dữ liệu được mã hóa theo thời gian một cách đáng tin cậy. Các máy chủ thực tế rất nhiễu, do đó kẻ tấn công phải lặp lại mỗi phép đo ít nhất vài lần và sử dụng một số bộ phân biệt thống kê. Lặp lại phép đo trong trường hợp của chúng tôi có nghĩa là đặt lại trạng thái bộ nhớ đệm. Hai thứ phải được xóa khỏi bộ nhớ đệm trước mỗi vòng. Giá trị mà nhánh suy đoán phụ thuộc vào phải bị loại bỏ, để quá trình phân giải nhánh bị đình trệ đủ lâu nhằm mở ra một cửa sổ suy đoán. Dòng thăm dò mã hóa bit bị rò rỉ phải bị loại bỏ, để lần truy cập tạm thời tiếp theo có thể lưu lại nó vào bộ nhớ đệm.
Vì không có lệnh trực tiếp nào khả dụng trong JavaScript, cách cổ điển để thực hiện việc này là xây dựng một tập hợp loại bỏ (eviction set). Một tập hợp loại bỏ là một nhóm các địa chỉ ánh xạ tới cùng một tập hợp bộ nhớ đệm với mục tiêu. Truy cập chúng theo mẫu phù hợp sẽ đẩy mục tiêu ra khỏi bộ nhớ đệm. Trong cuộc tấn công của họ, Stephen Röttger và Artur Janc đã sử dụng một danh sách loại bỏ để loại bỏ đáng tin cậy ít nhất vào bộ nhớ đệm L2. Cách này hiệu quả nhưng tốn kém. Việc xây dựng một tập hợp loại bỏ chính xác đòi hỏi nhiều phép đo thời gian, và bộ đếm thời gian của chúng tôi là một bộ đếm thời gian từ xa bị nhiễu. Cuộc tấn công từ xa trước đó nhắm vào Workers đã bỏ qua việc tìm kiếm bằng cách duyệt qua một mảng lớn hơn bộ nhớ đệm L1 và L2 trong mỗi vòng. Đó là một lựa chọn, nhưng thậm chí còn chậm hơn.
Dougall Johnson đã mô tả một cách thanh lịch hơn trong bài đăng blog rất thú vị của mình về khai thác Spectre trên JavaScript di động. Ý tưởng này tuân theo trực tiếp nguyên lý chuồng bồ câu. Nếu bạn cấp phát nhiều dữ liệu hơn mức bộ nhớ đệm có thể chứa, một dòng bộ nhớ đệm được chọn ngẫu nhiên gần như chắc chắn không được lưu trong bộ nhớ đệm. Đối với bộ nhớ đệm L2 256 KB, việc cấp phát 64 MB để lại xác suất tối đa 1/256 rằng một dòng bộ nhớ đệm ngẫu nhiên vẫn còn trong L2. Vì vậy, thay vì loại bỏ một dòng cụ thể, bạn không bao giờ loại bỏ cả. Bạn chọn một vị trí ngẫu nhiên mới đã bị loại bỏ với xác suất áp đảo. Tác dụng phụ thú vị của việc lặp lại thường xuyên trên mảng các đối tượng đó là nó sẽ dẫn đến hiệu ứng tự loại bỏ.
Để tận dụng điều này trong JavaScript, chúng tôi cấp phát một nhóm lớn các cặp đối tượng kẻ tấn công và nạn nhân vượt quá bộ nhớ đệm cấp cuối (last-level cache). Mỗi vòng đo chọn một cặp ngẫu nhiên mới. Con trỏ bản đồ (map pointer) của đối tượng, bộ mô tả lớp ẩn mà kiểm tra kiểu suy đoán đọc, do đó gần như chắc chắn đã bị loại bỏ.
Đặt cùng vị trí (co-locating) isolate của kẻ tấn công và nạn nhân
Để cuộc tấn công hoạt động, cả isolate của kẻ tấn công và nạn nhân phải được lập lịch trong cùng một tiến trình trên cùng một máy chủ biên. Người ta có thể nghĩ một cách trực quan rằng điều này sẽ khó khăn, xét đến việc Cloudflare vận hành hàng chục nghìn máy chủ biên, nhưng thực tế điều này khá tầm thường trên Cloudflare Workers. Vì Cloudflare Workers được thiết kế để thực thi trên bất kỳ máy chủ biên nào của Cloudflare, việc gọi tập lệnh nạn nhân từ tập lệnh kẻ tấn công bằng fetch(“https://victim.example”) trong hầu hết các trường hợp sẽ khiến bộ lập lịch khởi tạo một instance của worker nạn nhân trong cùng một tiến trình. Isolate của nạn nhân có thể được giữ sống bằng cách liên tục gửi các yêu cầu phụ đến nó theo một khoảng thời gian nhất định.
Hơn nữa, vì độ ổn định của cuộc tấn công phụ thuộc rất nhiều vào tải CPU của máy chủ biên đang chạy tập lệnh worker, điều này cho phép kẻ tấn công chạy cuộc tấn công một cách chiến lược tại một colo ngoài giờ cao điểm (ví dụ: ở một colo tại Úc trong giờ làm việc của châu Âu) nơi mức lưu lượng truy cập tương đối thấp.
Vượt qua các giới hạn tài nguyên của isolate
Môi trường runtime của Cloudflare Workers thực thi một tập hợp các giới hạn trên tất cả các isolate để bảo vệ nền tảng và ngăn chặn lạm dụng. Đối với mục đích thực hiện cuộc tấn công này, các giới hạn liên quan là 30 giây thời gian CPU và 1.000 yêu cầu phụ cho mỗi lần gọi. Các giới hạn này đã được tăng lên kể từ đó, nhưng các nguyên tắc sau đây vẫn còn phù hợp.
Đối với một Worker thông thường, mỗi yêu cầu HTTP, một sự kiện fetch, là một lần gọi mới đặt lại các giới hạn này. Vấn đề là làm sao để các yêu cầu tuần tự rơi vào cùng một máy chủ biên. Cân bằng tải và điều kiện mạng thay đổi khiến điều đó không đáng tin cậy. Durable Objects giải quyết vấn đề đó cho chúng tôi.
Durable Objects được xây dựng để phối hợp thời gian thực giữa các máy khách, vì vậy môi trường runtime coi mỗi tin nhắn WebSocket đến là một lần gọi giúp đặt lại thời gian CPU và giới hạn yêu cầu. Kẻ tấn công mở một WebSocket bền vững đến một worker Durable Object và gửi các tin nhắn keep-alive định kỳ. Điều này giữ cho một isolate duy nhất luôn hoạt động và cung cấp cho chúng tôi một kênh hai chiều, bền vững để thực hiện cuộc tấn công.
Bài viết được AI dịch và tổng hợp tự động từ Cloudflare Blog. Liên kết bài gốc ở phía trên. AIHOT.vn luôn dẫn nguồn đầy đủ — nếu bạn thấy điểm cần chỉnh sửa, hãy gửi ý kiến tại trang phản hồi.