Bài viết được đề cập bởi sgbj trong các nhận xét được viết bởi Paul Turner của Google giải thích các chi tiết sau đây chi tiết hơn nhiều, nhưng tôi sẽ cho nó một shot:
Theo như tôi có thể ghép lại từ thông tin hạn chế vào lúc này, retpoline là một tấm bạt lò xo sử dụng một vòng lặp vô hạn không bao giờ được thực thi để ngăn CPU suy đoán mục tiêu của một bước nhảy gián tiếp.
Cách tiếp cận cơ bản có thể được nhìn thấy trong nhánh hạt nhân của Andi Kleen giải quyết vấn đề này:
Nó giới thiệu __x86.indirect_thunk
cuộc gọi mới tải mục tiêu cuộc gọi có địa chỉ bộ nhớ (mà tôi sẽ gọi ADDR
) được lưu trữ trên đỉnh ngăn xếp và thực hiện bước nhảy bằng cách sử dụng một RET
lệnh. Bản thân thunk sau đó được gọi bằng cách sử dụng macro NOSPEC_JMP / CALL , được sử dụng để thay thế nhiều cuộc gọi và nhảy gián tiếp (nếu không phải tất cả). Macro chỉ cần đặt mục tiêu cuộc gọi lên ngăn xếp và đặt địa chỉ trả về chính xác, nếu cần (lưu ý luồng điều khiển phi tuyến tính):
.macro NOSPEC_CALL target
jmp 1221f /* jumps to the end of the macro */
1222:
push \target /* pushes ADDR to the stack */
jmp __x86.indirect_thunk /* executes the indirect jump */
1221:
call 1222b /* pushes the return address to the stack */
.endm
Việc đặt call
cuối cùng là cần thiết để khi cuộc gọi gián tiếp kết thúc, luồng điều khiển tiếp tục đằng sau việc sử dụng NOSPEC_CALL
macro, do đó, nó có thể được sử dụng thay cho thông thườngcall
Bản thân thunk trông như sau:
call retpoline_call_target
2:
lfence /* stop speculation */
jmp 2b
retpoline_call_target:
lea 8(%rsp), %rsp
ret
Luồng điều khiển có thể hơi khó hiểu ở đây, vì vậy hãy để tôi làm rõ:
call
đẩy con trỏ lệnh hiện tại (nhãn 2) vào ngăn xếp.
lea
thêm 8 vào con trỏ ngăn xếp , loại bỏ hiệu quả tứ giác được đẩy gần đây nhất, là địa chỉ trả lại cuối cùng (vào nhãn 2). Sau này, đỉnh của các điểm xếp chồng tại địa chỉ trả lại thực ADDR một lần nữa.
ret
nhảy tới *ADDR
và đặt lại con trỏ ngăn xếp đến đầu ngăn xếp cuộc gọi.
Cuối cùng, toàn bộ hành vi này thực tế tương đương với việc nhảy trực tiếp vào *ADDR
. Lợi ích chúng ta nhận được là bộ dự báo nhánh được sử dụng cho các câu lệnh return (Return Stack Buffer, RSB), khi thực hiện call
lệnh, giả định rằng ret
câu lệnh tương ứng sẽ nhảy đến nhãn 2.
Phần sau nhãn 2 thực sự không bao giờ được thực thi, nó chỉ đơn giản là một vòng lặp vô hạn mà theo lý thuyết sẽ lấp đầy đường JMP
dẫn lệnh bằng các hướng dẫn. Bằng cách sử dụng LFENCE
, PAUSE
hoặc nói chung là một lệnh làm cho đường dẫn lệnh bị đình trệ sẽ ngăn CPU lãng phí bất kỳ sức mạnh và thời gian nào cho việc thực hiện đầu cơ này. Điều này là do trong trường hợp lệnh gọi retpoline_call_target sẽ trở lại bình thường, LFENCE
đó sẽ là hướng dẫn tiếp theo được thực hiện. Đây cũng là những gì người dự đoán chi nhánh sẽ dự đoán dựa trên địa chỉ trả lại ban đầu (nhãn 2)
Để trích dẫn từ hướng dẫn kiến trúc của Intel:
Các hướng dẫn tuân theo một LỚN có thể được lấy từ bộ nhớ trước khi có LẦN, nhưng chúng sẽ không được thực thi cho đến khi hoàn thành.
Tuy nhiên, xin lưu ý rằng thông số kỹ thuật không bao giờ đề cập đến việc LỪA ĐẢO và PAUSE khiến đường ống bị đình trệ, vì vậy tôi đang đọc một chút giữa các dòng ở đây.
Bây giờ trở lại câu hỏi ban đầu của bạn: Việc tiết lộ thông tin bộ nhớ kernel là có thể do sự kết hợp của hai ý tưởng:
Mặc dù thực thi đầu cơ không có tác dụng phụ khi đầu cơ sai, thực thi đầu cơ vẫn ảnh hưởng đến hệ thống phân cấp bộ đệm . Điều này có nghĩa là khi tải bộ nhớ được thực thi theo suy đoán, nó vẫn có thể khiến một dòng bộ đệm bị xóa. Sự thay đổi này trong hệ thống phân cấp bộ đệm có thể được xác định bằng cách cẩn thận đo thời gian truy cập vào bộ nhớ được ánh xạ vào cùng một bộ bộ đệm.
Bạn thậm chí có thể rò rỉ một số bit của bộ nhớ tùy ý khi địa chỉ nguồn của bộ nhớ đọc được đọc từ bộ nhớ kernel.
Bộ dự báo nhánh gián tiếp của CPU Intel chỉ sử dụng 12 bit thấp nhất của lệnh nguồn, do đó dễ dàng đầu độc tất cả 2 ^ 12 lịch sử dự đoán có thể có với các địa chỉ bộ nhớ do người dùng kiểm soát. Chúng có thể được dự đoán khi bước nhảy gián tiếp được dự đoán trong kernel, được thực hiện theo cách đặc biệt với các đặc quyền kernel. Sử dụng kênh bên thời gian bộ đệm, do đó bạn có thể rò rỉ bộ nhớ kernel tùy ý.
CẬP NHẬT: Trong danh sách gửi thư kernel , có một cuộc thảo luận đang diễn ra khiến tôi tin rằng các retpolines không giảm thiểu hoàn toàn các vấn đề dự đoán nhánh, vì khi Return Stack Buffer (RSB) chạy trống, các kiến trúc Intel gần đây (Skylake +) rơi trở lại đến Bộ đệm mục tiêu chi nhánh dễ bị tổn thương (BTB):
Retpoline như một chiến lược giảm thiểu hoán đổi các nhánh gián tiếp để trả lại, để tránh sử dụng các dự đoán đến từ BTB, vì chúng có thể bị kẻ tấn công đầu độc. Vấn đề với Skylake + là dòng chảy RSB quay trở lại sử dụng dự đoán BTB, cho phép kẻ tấn công kiểm soát đầu cơ.