Retpoline là gì và nó hoạt động như thế nào?


244

Để giảm thiểu việc tiết lộ bộ nhớ hoặc xử lý bộ nhớ chéo ( tấn công Spectre ), nhân Linux 1 sẽ được biên dịch với một tùy chọn mới , được -mindirect-branch=thunk-externgiới thiệu gccđể thực hiện các cuộc gọi gián tiếp thông qua cái gọi là retpoline .

Đây dường như là một thuật ngữ mới được phát minh khi một tìm kiếm Google chỉ xuất hiện lần sử dụng gần đây (nói chung là tất cả vào năm 2018).

Retpoline là gì và làm thế nào để ngăn chặn các cuộc tấn công tiết lộ thông tin hạt nhân gần đây?


1 Nó không phải Linux cụ thể, tuy nhiên - cấu trúc tương tự hoặc giống hệt nhau dường như được sử dụng như một phần của chiến lược giảm nhẹ trên hệ điều hành khác.


6
Một bài viết hỗ trợ thú vị từ Google.
sgbj

2
ồ, vậy là nó được phát âm là / træmpəˈlin / (người Mỹ) hoặc / træmpəˌliːn / (người Anh)
Walter Tross

2
Bạn có thể đề cập rằng đây là nhân Linux , mặc dù vậy gcc, theo cách đó! Tôi đã không nhận ra lkml.org/lkml/2018/1/3/780 như trên trang web Danh sách gửi thư hạt nhân Linux, thậm chí không một lần tôi nhìn vào đó (và được phục vụ một ảnh chụp nhanh khi nó ngoại tuyến).
PJTraill

@PJTraill - đã thêm thẻ nhân Linux
RichVel

@PJTraill - điểm tốt, tôi đã cập nhật văn bản câu hỏi. Lưu ý rằng tôi đã thấy nó đầu tiên trong nhân Linux vì quy trình phát triển tương đối mở, nhưng không nghi ngờ gì nữa, các kỹ thuật tương tự hoặc tương tự đang được sử dụng như là sự giảm thiểu trong toàn bộ các hệ điều hành nguồn mở và đóng. Vì vậy, tôi không thấy điều này là dành riêng cho Linux, nhưng liên kết chắc chắn là vậy.
BeeOnRope

Câu trả lời:


158

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_thunkcuộ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 RETlệ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 callcuố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_CALLmacro, 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.
  • leathê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.
  • retnhảy tới *ADDRvà đặ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 calllệnh, giả định rằng retcâ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 JMPdẫn lệnh bằng các hướng dẫn. Bằng cách sử dụng LFENCE, PAUSEhoặ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ơ.


Tôi không nghĩ rằng hướng dẫn LỚN là quan trọng, thay vào đó, việc triển khai của Google sử dụng hướng dẫn PAUSE. support.google.com/faqsfurt/7625886 Lưu ý rằng tài liệu bạn trích dẫn nói "sẽ không thực thi" sẽ không "sẽ không được thực hiện theo suy đoán".
Ross Ridge

1
Từ trang Câu hỏi thường gặp của Google: "Hướng dẫn tạm dừng trong các vòng lặp đầu cơ của chúng tôi ở trên không bắt buộc phải chính xác. Nhưng điều đó có nghĩa là việc thực thi đầu cơ phi sản xuất chiếm ít đơn vị chức năng hơn trên bộ xử lý." Vì vậy, nó không hỗ trợ kết luận của bạn rằng LỚN là họ quan trọng ở đây.
Ross Ridge

@RossRidge Tôi đồng ý một phần, với tôi điều này giống như hai triển khai có thể có của một vòng lặp vô hạn gợi ý CPU không thực thi mã theo cách suy đoán theo PAUSE / LFENCE. Tuy nhiên, nếu LẬP TỨC được thực hiện theo suy đoán và không được khôi phục vì suy đoán là chính xác, điều này sẽ mâu thuẫn với tuyên bố rằng nó sẽ chỉ được thực hiện khi quá trình tải bộ nhớ kết thúc. (Nếu không, toàn bộ tập lệnh đã được thực hiện theo suy đoán sẽ phải được khôi phục và thực hiện lại để hoàn thành các thông số kỹ thuật)
Tobias Ribizel

1
Điều này có lợi thế là push/ retkhông làm mất cân bằng ngăn xếp dự báo địa chỉ trả về. Có một dự đoán sai (đi đến lfencetrước khi địa chỉ trả lại thực tế được sử dụng), nhưng sử dụng call+ sửa đổi rspcân bằng ra điều đó ret.
Peter Cordes

1
Rất tiếc, lợi thế hơn push / ret(trong bình luận cuối cùng của tôi). re: chỉnh sửa của bạn: Không thể điền vào RSB vì retpoline bao gồm a call. Nếu chế độ tiền nhân đã thực hiện chuyển đổi ngữ cảnh ở đó, chúng tôi sẽ tiếp tục thực thi với RSB được khởi tạo từ bộ calllập lịch. Nhưng có lẽ một trình xử lý ngắt có thể kết thúc với đủ rets để làm trống RSB.
Peter Cordes

46

Một retpoline được thiết kế để bảo vệ chống lại việc khai thác mục tiêu nhánh ( CVE-2017-5715 ). Đây là một cuộc tấn công trong đó một lệnh nhánh gián tiếp trong kernel được sử dụng để buộc thực thi đầu cơ của một đoạn mã tùy ý. Mã được chọn là một "tiện ích" bằng cách nào đó hữu ích cho kẻ tấn công. Ví dụ mã có thể được chọn để sẽ rò rỉ dữ liệu kernel thông qua cách nó ảnh hưởng đến bộ đệm. Retpoline ngăn chặn việc khai thác này bằng cách thay thế tất cả các lệnh rẽ nhánh gián tiếp bằng lệnh quay lại.

Tôi nghĩ những gì quan trọng về retpoline chỉ là phần "ret", nó thay thế nhánh gián tiếp bằng một lệnh trả về để CPU sử dụng bộ dự báo ngăn xếp trở lại thay vì bộ dự báo nhánh có thể khai thác. Nếu thay vào đó, một lệnh đẩy và trả lại đơn giản được sử dụng thì mã được thực thi theo suy đoán sẽ là mã mà cuối cùng hàm sẽ quay trở lại, không phải là tiện ích nào đó hữu ích cho kẻ tấn công. Lợi ích chính của phần tấm bạt lò xo dường như là duy trì ngăn xếp trả về để khi chức năng thực sự trở lại với người gọi của nó, điều này được dự đoán chính xác.

Ý tưởng cơ bản đằng sau việc tiêm mục tiêu chi nhánh rất đơn giản. Nó lợi dụng thực tế là CPU không ghi lại địa chỉ đầy đủ của nguồn và đích của các nhánh trong bộ đệm đích của nhánh. Vì vậy, kẻ tấn công có thể lấp đầy bộ đệm bằng cách sử dụng các bước nhảy trong không gian địa chỉ của chính nó sẽ dẫn đến các lần truy cập dự đoán khi một bước nhảy gián tiếp cụ thể được thực hiện trong không gian địa chỉ kernel.

Lưu ý rằng retpoline không ngăn chặn việc tiết lộ thông tin hạt nhân trực tiếp, nó chỉ ngăn chặn các hướng dẫn chi nhánh gián tiếp được sử dụng để thực thi một cách cụ thể một tiện ích sẽ tiết lộ thông tin. Nếu kẻ tấn công có thể tìm thấy một số phương tiện khác để thực thi tiện ích thì retpoline sẽ không ngăn chặn cuộc tấn công.

Các cuộc tấn công Spectre trên giấy : Khai thác thực thi đầu cơ của Paul Kocher, Daniel Genkin, Daniel Gruss, Werner Haas, Mike Hamburg, Moritz Lipp, Stefan Mangard, Thomas Prescher, Michael Schwarz và Yuval Yarom đưa ra cái nhìn tổng quan sau đây về cách các nhánh gián tiếp có thể được khai thác :

Khai thác các chi nhánh gián tiếp. Vẽ từ lập trình hướng trở lại (ROP), trong phương thức này, kẻ tấn công chọn một tiện íchtừ không gian địa chỉ của nạn nhân và ảnh hưởng đến nạn nhân để thực hiện tiện ích theo suy đoán. Không giống như ROP, kẻ tấn công không dựa vào lỗ hổng trong mã nạn nhân. Thay vào đó, kẻ tấn công huấn luyện Bộ đệm mục tiêu chi nhánh (BTB) để dự đoán sai một nhánh từ một lệnh rẽ nhánh gián tiếp đến địa chỉ của tiện ích, dẫn đến việc thực hiện đầu cơ của tiện ích. Trong khi các hướng dẫn được thực hiện theo suy đoán bị bỏ qua, các hiệu ứng của chúng trên bộ đệm không được hoàn nguyên. Những hiệu ứng này có thể được sử dụng bởi các tiện ích để rò rỉ thông tin nhạy cảm. Chúng tôi chỉ ra cách thức, với một lựa chọn cẩn thận của một tiện ích, phương pháp này có thể được sử dụng để đọc bộ nhớ tùy ý từ nạn nhân.

Để đánh lừa BTB, kẻ tấn công tìm địa chỉ ảo của tiện ích trong không gian địa chỉ của nạn nhân, sau đó thực hiện các nhánh gián tiếp đến địa chỉ này. Việc huấn luyện này được thực hiện từ không gian địa chỉ của kẻ tấn công và không có vấn đề gì ở địa chỉ tiện ích trong không gian địa chỉ của kẻ tấn công; tất cả những gì được yêu cầu là nhánh được sử dụng cho các nhánh đào tạo để sử dụng cùng một địa chỉ ảo đích. (Trên thực tế, miễn là kẻ tấn công xử lý các trường hợp ngoại lệ, cuộc tấn công có thể hoạt động ngay cả khi không có mã được ánh xạ tại địa chỉ ảo của tiện ích trong không gian địa chỉ của kẻ tấn công.) Cũng không cần phải khớp hoàn toàn địa chỉ nguồn. của chi nhánh được sử dụng cho đào tạo và địa chỉ của chi nhánh được nhắm mục tiêu. Do đó, kẻ tấn công có sự linh hoạt đáng kể trong việc thiết lập đào tạo.

Một mục blog có tiêu đề Đọc bộ nhớ đặc quyền với kênh phụ của nhóm Project Zero tại Google cung cấp một ví dụ khác về cách tiêm mục tiêu nhánh có thể được sử dụng để tạo khai thác hoạt động.


9

Câu hỏi này đã được hỏi cách đây một thời gian và xứng đáng có câu trả lời mới hơn.

Tóm tắt :

Các chuỗi Retpoline của Viking là một cấu trúc phần mềm cho phép tách các nhánh gián tiếp khỏi thực thi đầu cơ. Điều này có thể được áp dụng để bảo vệ các nhị phân nhạy cảm (chẳng hạn như triển khai hệ điều hành hoặc trình ảo hóa) khỏi các cuộc tấn công tiêm mục tiêu nhánh chống lại các nhánh gián tiếp của chúng.

Từ " ret poline " là một từ ghép của các từ "trở lại" và "tấm bạt lò xo", giống như sự cải thiện " rel poline " được đặt ra từ "cuộc gọi tương đối" và "tấm bạt lò xo". Đó là một cấu trúc tấm bạt lò xo được xây dựng bằng cách sử dụng các hoạt động hoàn trả, điều này cũng đảm bảo theo nghĩa bóng rằng bất kỳ thực thi đầu cơ liên quan nào cũng sẽ nảy ra không ngừng.

Để giảm thiểu việc tiết lộ bộ nhớ hoặc xử lý bộ nhớ chéo (tấn công Spectre), nhân Linux [1] sẽ được biên dịch với một tùy chọn mới, được -mindirect-branch=thunk-externgiới thiệu cho gcc để thực hiện các cuộc gọi gián tiếp thông qua cái gọi là retpoline.

[1] Tuy nhiên, nó không phải là đặc thù của Linux - cấu trúc tương tự hoặc giống hệt nhau dường như được sử dụng như một phần của chiến lược giảm thiểu trên các HĐH khác.

Việc sử dụng tùy chọn trình biên dịch này chỉ bảo vệ chống lại Spectre V2 trong các bộ xử lý bị ảnh hưởng có cập nhật vi mã cần thiết cho CVE-2017-5715. Nó sẽ ' hoạt động ' trên bất kỳ mã nào (không chỉ là kernel), mà chỉ mã chứa "bí mật" mới đáng để tấn công.

Đây dường như là một thuật ngữ mới được phát minh khi một tìm kiếm Google chỉ xuất hiện lần sử dụng gần đây (nói chung là tất cả vào năm 2018).

Các trình biên dịch LLVM đã có một -mretpolinecông tắc từ trước ngày 04 tháng 1 2018 . Ngày đó là khi lỗ hổng đầu tiên được báo cáo công khai . GCC đã cung cấp các bản vá của họ vào ngày 7 tháng 1 năm 2018.

Ngày CVE cho thấy lỗ hổng đã được ' phát hiện ' vào năm 2017, nhưng nó ảnh hưởng đến một số bộ xử lý được sản xuất trong hai thập kỷ qua (do đó có khả năng nó đã được phát hiện từ lâu).

Retpoline là gì và làm thế nào để ngăn chặn các cuộc tấn công tiết lộ thông tin hạt nhân gần đây?

Đầu tiên, một vài định nghĩa:

  • Trampoline - Đôi khi được gọi là vectơ nhảy gián tiếp là các vị trí bộ nhớ giữ các địa chỉ trỏ đến các thói quen dịch vụ bị gián đoạn, các thói quen I / O, v.v. Theo truyền thống, GCC đã hỗ trợ các chức năng lồng nhau bằng cách tạo ra một tấm bạt lò xo thực thi trong thời gian chạy khi địa chỉ của chức năng lồng nhau được thực hiện. Đây là một đoạn mã nhỏ thường nằm trên ngăn xếp, trong khung ngăn xếp của hàm chứa. Tấm bạt lò xo tải thanh ghi chuỗi tĩnh và sau đó nhảy đến địa chỉ thực của hàm lồng nhau.

  • Thunk - Thunk là một chương trình con được sử dụng để tiêm một phép tính bổ sung vào một chương trình con khác. Thunk chủ yếu được sử dụng để trì hoãn một phép tính cho đến khi cần kết quả của nó hoặc để chèn các hoạt động ở đầu hoặc cuối của chương trình con khác

  • Ghi nhớ - Một chức năng ghi nhớ "ghi nhớ" các kết quả tương ứng với một số bộ đầu vào cụ thể. Các cuộc gọi tiếp theo với các đầu vào đã nhớ trả về kết quả đã nhớ thay vì tính toán lại, do đó loại bỏ chi phí chính của một cuộc gọi với các tham số đã cho từ tất cả trừ cuộc gọi đầu tiên được thực hiện cho hàm với các tham số đó.

Rất gần, một retpoline là một tấm bạt lò xo với một sự trở lại như một thunk , để ' cướp ' memoization trong ngành dự đoán gián tiếp.

Nguồn : Retpoline bao gồm một lệnh PAUSE cho Intel, nhưng một lệnh LFENCE là cần thiết cho AMD vì trên bộ xử lý đó, lệnh PAUSE không phải là một lệnh tuần tự hóa, vì vậy vòng lặp tạm dừng / jmp sẽ sử dụng năng lượng dư thừa vì nó được suy đoán khi chờ trả về để dự đoán sai mục tiêu chính xác.

Arstechnica có một lời giải thích đơn giản về vấn đề:

"Mỗi bộ xử lý có một hành vi kiến ​​trúc (hành vi được ghi lại mô tả cách thức hoạt động của các hướng dẫn và các lập trình viên phụ thuộc vào việc viết chương trình của họ) và hành vi vi kiến ​​trúc (cách thực hiện kiến ​​trúc thực tế). Chúng có thể phân kỳ theo những cách tinh tế. Ví dụ, về mặt kiến ​​trúc, một chương trình tải một giá trị từ một địa chỉ cụ thể trong bộ nhớ sẽ đợi cho đến khi biết địa chỉ đó trước khi thử thực hiện tải. Tuy nhiên, về mặt kiến ​​trúc, bộ xử lý có thể cố gắng đoán theo địa chỉ để nó có thể bắt đầu tải giá trị từ bộ nhớ (chậm) ngay cả trước khi hoàn toàn chắc chắn nên sử dụng địa chỉ nào.

Nếu bộ xử lý đoán sai, nó sẽ bỏ qua giá trị đoán và thực hiện tải lại, lần này với địa chỉ chính xác. Các hành vi được xác định kiến ​​trúc được bảo tồn. Nhưng dự đoán sai đó sẽ làm xáo trộn các phần khác của bộ xử lý, đặc biệt là nội dung của bộ đệm. Những nhiễu loạn vi kiến ​​trúc này có thể được phát hiện và đo lường bằng cách xác định thời gian cần thiết để truy cập dữ liệu cần (hoặc không nên) trong bộ đệm, cho phép một chương trình độc hại suy luận về các giá trị được lưu trong bộ nhớ. ".

Từ bài viết của Intel: " Retpoline: Giảm thiểu mục tiêu tiêm chích " ( .PDF ):

"Chuỗi retpoline ngăn không cho thực thi đầu cơ của bộ xử lý sử dụng" bộ dự báo nhánh gián tiếp "(một cách dự đoán luồng chương trình) để suy đoán địa chỉ được kiểm soát bởi một khai thác (đáp ứng yếu tố 4 trong năm yếu tố của tiêm mục tiêu nhánh (Biến thể Spectre 2 ) khai thác thành phần được liệt kê ở trên). ".

Lưu ý, yếu tố 4 là: "Việc khai thác phải ảnh hưởng thành công đến nhánh gián tiếp này đến dự đoán sai và thực thi một tiện ích. Tiện ích này, được khai thác chọn, rò rỉ dữ liệu bí mật qua kênh bên, thường là theo thời gian bộ đệm.".

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.