Điểm:20

Làm cách nào tôi có thể hiểu liệu việc triển khai C của mình có phải là thời gian không đổi hay không (tức là có khả năng chống lại các cuộc tấn công theo thời gian)

lá cờ jp

Tôi có một mã cho phép nhân đa thức và nó được viết bằng C. Tôi nghe nói rằng liệu một lệnh cụ thể là "thời gian không đổi" có thể khác nhau tùy theo kiến ​​trúc và kiểu bộ xử lý hay không và không có bất kỳ tài liệu chính thức nào cho hành vi này.Làm cách nào để biết mã của tôi có phải là thời gian cố định hay không?

Lưu ý: Theo "Thời gian không đổi", ý tôi là một phần mềm hoặc đoạn mã có khả năng chống lại các cuộc tấn công theo thời gian. Tôi sử dụng UBUNTU trên PC i7 thế hệ thứ 10

kelalaka avatar
lá cờ in
Biên dịch và xem. Các trình biên dịch khác nhau hoạt động khác nhau và chúng cũng khác nhau về kiến ​​trúc đích. Bạn có thể sử dụng [Trình khám phá trình biên dịch](https://godbolt.org/) để xem ASM. Thông thường, nó được viết bằng ASM. [Xem T1 của Thomas Pornin](https://www.youtube.com/watch?v=IHbtK5Kwt6A).
kelalaka avatar
lá cờ in
Chỉ cần tập trung vào các phần phụ thuộc quan trọng. Lưu ý rằng mã của bạn có thể được xem lại trong codereview.se
esra avatar
lá cờ jp
@kelalaka điều đó có nghĩa là tôi phải xem mã lắp ráp để đảm bảo mã có phải là thời gian không đổi hay không? Có cần thiết phải phân tích nó với lắp ráp? Tôi có thể làm điều đó trên mã C trên PC của mình không?
esra avatar
lá cờ jp
@kelalaka mã của tôi không phải là một giao thức mã hóa đầy đủ, nó chỉ là mã nhân đa thức. Không có khóa ex. Tôi có thể đo xem mã nhân của tôi có phải là hằng số thời gian hay không?
kelalaka avatar
lá cờ in
Vâng, và vấn đề là bạn phải đảm bảo điều đó cho mọi lần biên dịch. Trình biên dịch Explorer là công cụ tuyệt vời cho việc này.
kelalaka avatar
lá cờ in
Câu hỏi trong thời gian tấn công là những gì bạn rò rỉ, nếu thông tin về các khóa không bị rò rỉ tại sao phải có thời gian liên tục?
esra avatar
lá cờ jp
@kelalaka xin lỗi vì lỗi của tôi. Tôi sử dụng một mẩu chìa khóa trong phép nhân. bạn đúng rồi
kelalaka avatar
lá cờ in
[1](https://crypto.stackexchange.com/q/30958/18298), [2](https://crypto.stackexchange.com/q/82095/18298). [3](https://crypto.stackexchange.com/q/33252/18298), [4](https://crypto.stackexchange.com/q/80492/18298), [5](https:// crypto.stackexchange.com/q/75408/18298) và nhiều trang khác... xem thẻ [tag:timing-attack], [tag:side-channel-attack]
kelalaka avatar
lá cờ in
Một bản sao trên nhiều trang web từ infosec : [Làm cách nào để ngăn chặn các cuộc tấn công kênh phụ chống lại xác thực?](https://security.stackexchange.com/q/220446/86735)
lá cờ nr
Tôi không hiểu tại sao mọi người lại muốn cố gắng đạt được mã thời gian không đổi. Như những người khác đã đề cập, không thể đảm bảo, ngay cả khi bạn sửa mã lắp ráp, bởi vì những gì một CPU làm ngày hôm nay có thể không phải là những gì thế hệ tiếp theo của CPU đó làm và hoàn toàn không có cách nào để dự đoán. Thay vì cố gắng làm điều gì đó bất khả thi, chỉ cần thêm một độ trễ ngẫu nhiên có độ lớn lớn hơn thời gian mà mã của bạn cần. Vâng, nó không thể ngăn chặn hoàn toàn các cuộc tấn công theo thời gian, nhưng nó tốt hơn nhiều so với việc lãng phí thời gian và năng lượng để cố gắng làm cho thời gian thực thi của CPU bằng nhau...
kelalaka avatar
lá cờ in
@user21820 trong khi các nhà nghiên cứu đưa ra khả năng xảy ra [các cuộc tấn công theo thời gian từ xa](http://crypto.stanford.edu/~dabo/papers/ssl-timing.pdf), có một mối lo ngại lớn là cuộc tấn công theo thời gian có thể làm lộ bí mật phím. Một lời giải thích đơn giản là [trong câu trả lời này](https://crypto.stackexchange.com/q/75408/18298). Chúng tôi không nói về việc bảo mật mọi thứ mà chúng tôi đang nói về việc bảo mật các bộ phận phụ thuộc chính. Đây là điểm tấn công nghiêm trọng từ thẻ thông minh đến máy tính đám mây dùng chung. Đây là mật mã mà ma quỷ trong các chi tiết và $$cryptgraphy \neq cryptocurrency$$
lá cờ nr
@kelalaka: Đợi đã, tôi không trả lời nhận xét của bạn, nhưng ý tưởng chung là người ta có thể xem mã lắp ráp để cố gắng đạt được hiệu suất thực thi liên tục. Và tôi không hiểu tại sao bạn dường như cho rằng tôi không biết các cuộc tấn công thời gian là gì. Hơn nữa, ngay cả những nhận xét về câu trả lời được liên kết của riêng bạn cũng lặp lại quan điểm tôi đã đưa ra ở đây.
kelalaka avatar
lá cờ in
@ user21820 toàn bộ quan điểm từ nhận xét đầu tiên của tôi, ASM. Hãy nhớ rằng, tất nhiên, Thomas Pornin đã xây dựng một trình biên dịch an toàn theo một số giả định và hiện tại họ đang cố gắng mở rộng nó thành Turing hoàn chỉnh, mặc dù không cần thiết.
lá cờ nr
@kelalaka: Ơ?? Chẳng phải tôi đã nói rõ ràng rằng việc lắp ráp là **không đủ** vì thiết kế CPU vật lý **không** đảm bảo bất cứ điều gì về thời gian thực hiện sao? Thật vô nghĩa khi tiếp tục nói về các mô hình trừu tượng đơn thuần khi bảo mật là về **thế giới thực**.
kelalaka avatar
lá cờ in
@ user21820 nó không được ghi một lần biên dịch cho mọi mục tiêu. Tôi chưa bao giờ nói điều đó. Người ta phải xem xét tất cả các thay đổi trong CPU để chắc chắn rằng ngay cả cùng một sê-ri cũng an toàn.... Hầu hết các cuộc tấn công xảy ra trong thế giới thực. Tất nhiên, có những cuộc tấn công học thuật hình dung ra khả năng, tuy nhiên, ứng dụng thực tế có thể còn lâu hơn như cuộc tấn công tiên tri đệm; 2003 đến 2013. Trong khoa học này đôi khi tấn công xảy ra trước, sau đó là mô hình và trường hợp khác. Chúng ta thậm chí không mô hình hóa thế giới thực để phân tích?
lá cờ nr
@kelalaka: Nếu bạn đang nói rằng bạn liên tục tiến hành phân tích từ đầu đến cuối cho mọi kiến ​​trúc CPU mục tiêu duy nhất mà bạn chạy mã của mình, thì vâng, tôi đồng ý rằng điều đó hiệu quả. Nhưng tất cả những nỗ lực đó có thực sự xứng đáng? Có nhiều cách tiết kiệm chi phí hơn để ngăn chặn các cuộc tấn công theo thời gian hơn là bận tâm về mã hợp ngữ.
marshal craft avatar
lá cờ de
thời gian không đổi có nghĩa là nó chạy trong một khoảng thời gian bị ràng buộc bởi một hằng số. Vì vậy, lượng thời gian để chạy chức năng với bất kỳ đầu vào có thể nào luôn nhỏ hơn một hằng số. Vì vậy, chẳng hạn như hàm luôn trả về sau 20 giây cho dù đầu vào là gì, nó chạy với thời gian không đổi.
marshal craft avatar
lá cờ de
Ngoài ra, đối với thuật toán trừu tượng của phép nhân đa thức, nó không phải là thời gian không đổi. Thời gian tăng lên khi thêm thừa số hoặc nhập đa thức bậc cao hơn. Bạn thực sự chưa hỏi một câu hỏi rõ ràng vì có 2 trường hợp. Hàm chấp nhận 2 đầu vào là các đa thức bậc khác nhau, về cơ bản là danh sách các hệ số có kích thước bằng bậc của đầu vào đa thức.
marshal craft avatar
lá cờ de
Trong trường hợp đơn giản này, chỉ cần chấp nhận 2 đầu vào, số phép nhân và phép cộng tăng lên như một hàm của bậc hoặc số hệ số. Vì vậy, nó không bị ràng buộc bởi một hằng số nói chung. Tuy nhiên, trên thực tế, trong quá trình triển khai của bạn, nếu có giới hạn tối đa đối với đầu vào, thì rất đơn giản là có, việc triển khai là thời gian không đổi. Đó sẽ là trường hợp xấu nhất đối với đầu vào tối đa. Vì vậy, giống như 2 bậc đa thức tối đa, với các hệ số có giá trị tối đa sẽ biểu thị thời gian không đổi. Chỉ vì việc triển khai của bạn sử dụng tài nguyên hữu hạn, được bảo vệ bởi kiến ​​trúc máy tính và phần mềm.
Điểm:30
lá cờ ph

Thật không may, trong trường hợp không có tài liệu từ nhà cung cấp CPU, bạn không thể chắc chắn 100% thuật toán nào sẽ hoặc sẽ không phải là thời gian không đổi. Điều đó nói rằng, chắc chắn có những quy tắc ngón tay cái có thể được sử dụng để giảm nguy cơ xảy ra sự cố.

  1. Bất cứ điều gì liên quan đến luồng điều khiển phân nhánh hoặc thực thi mã có điều kiện rõ ràng là một vấn đề.
  2. Các hoạt động so sánh có thể là một vấn đề ngay cả khi chúng không được sử dụng trực tiếp để tác động đến luồng điều khiển. Vấn đề là các toán tử so sánh thường lưu trữ kết quả của chúng trong một bộ đăng ký cờ và cách dễ nhất để lấy một kết quả riêng lẻ ra khỏi một thanh ghi cờ thường là thực thi phân nhánh hoặc có điều kiện.
  3. Booleans có thể là một vấn đề bởi vì trình biên dịch có thể chọn thay thế một hoạt động liên quan đến một boolean bằng thực thi có điều kiện hoặc phân nhánh.
  4. Thời gian truy cập bộ nhớ có thể khác nhau tùy thuộc vào địa chỉ được truy cập và trạng thái của bộ nhớ đệm. Điều này có nghĩa là mã liên quan đến bảng tra cứu có nguy cơ cao không phải là thời gian cố định.
  5. Phép nhân và phép chia có thể được thực hiện bằng cách sử dụng các thuật toán cần một số chu kỳ khác nhau để hoàn thành, phép nhân thường an toàn trên các bộ xử lý "ứng dụng" hiện đại nhưng có thể không có trên bộ điều khiển vi mô. Tôi sẽ không coi việc phân chia là an toàn trên bất kỳ bộ xử lý nào.
  6. Sự thay đổi và xoay bit theo một số vị trí khác nhau có thể là một vấn đề trên một số CPU vì chúng có thể chuyển thành các vòng lặp (trong phần mềm hoặc phần cứng).
  7. Phép toán cộng, trừ, thao tác theo bit và dịch chuyển bit theo hằng số nói chung là ổn.

Bạn có thể muốn xem qua https://www.bearssl.org/constanttime.html trong đó thảo luận về những vấn đề này chi tiết hơn.

kelalaka avatar
lá cờ in
Lưu ý rằng có một câu trả lời mở rộng tuyệt vời của Squamish Ossifrage trên Infosec [Làm cách nào tôi có thể ngăn chặn các cuộc tấn công kênh phụ chống lại xác thực?](https://security.stackexchange.com/q/220446/86735). Điều đó bao gồm tất cả chi tiết hơn nhiều.
lá cờ cn
Trên nền tảng 8 bit (thẻ thông minh), tôi đã thấy rằng trình biên dịch C đã triển khai tăng số 16 bit (tức là thêm 1) bằng ba lệnh (1) tăng byte thấp hơn, (2) nhảy qua có điều kiện lệnh tiếp theo nếu kết quả bằng 0 và (3) tăng byte cao hơn.
Điểm:12
lá cờ in

Lưu ý rằng câu trả lời xuất sắc này thuộc về Squamish Ossifrage rằng họ đã ngừng đóng góp! Tôi đã sao chép và dán sau đó tạo cộng đồng. Bỏ phiếu cho câu trả lời này không tạo ra bất cứ điều gì cho tôi. Nếu bạn có thể, hãy bỏ phiếu cho họ Infosec.


Từ ví dụ về mã được cung cấp, có thể xác định mật khẩu chính xác bằng cách định thời gian cho mã khi được cung cấp nhiều đầu vào khác nhau.

Đầu tiên, bạn không nên trực tiếp kiểm tra mật khẩu! tại ít nhất, trước tiên bạn nên băm mật khẩu bằng hàm băm mật khẩu như Argon2id và so sánh hàm băm mật khẩu của đầu vào với hàm băm mật khẩu bạn đã lưu trữ trong quá trình đăng ký người dùng (hoặc khi người dùng thay đổi mật khẩu lần cuối).

Tốt hơn nữa, bạn nên sử dụng giao thức thỏa thuận khóa được xác thực bằng mật khẩu như OPAQUE, nhưng những giao thức này có thể vượt quá mức lương của bạn vào lúc này cho đến khi chúng được áp dụng và triển khai rộng rãi hơn.

Tôi có thể làm gì để đảm bảo rằng mã của tôi không dễ bị tấn công theo thời gian như vậy?

Cách tốt nhất để bắt đầu là sử dụng một thói quen thư viện hoặc nguyên thủy mà một người nào khác đã viết rồi và có lý do để duy trì. Ví dụ: trong NaCl/libsodium, bạn có thể sử dụng crypto_verify_32 để so sánh hai chuỗi 32 byte, chẳng hạn như hai hàm băm Argon2id hoặc hai mã xác thực thông báo HMAC-SHA256. Sau đó, nỗ lực trả lời câu hỏi này có thể được tập trung vào một nơi duy nhất sẽ thu hút được nhiều sự chú ý và xem xét kỹ lưỡng và sẽ theo kịp những tiến bộ.

Nhưng hãy nói rằng bạn không có crypto_verify_32, hoặc bạn muốn tự thực hiện nó. Bạn có thể làm gì?

Để bắt đầu, bạn cần hiểu những hoạt động nào có các kênh phụ. Của nó hấp dẫn để nói rằng—như các câu trả lời khác đã làm—rằng kênh phụ phát sinh chỉ vì một sớm Huỷ bỏ. Nhưng đó không phải là toàn bộ câu chuyện. Nhìn chung, có nhiều hoạt động (ở đây viết bằng C để minh họa) có thể mất một khoảng thời gian tùy thuộc vào giá trị của các yếu tố đầu vào—chúng tôi gọi các thao tác này là biến thời gian hoạt động, trái ngược với hằng số thời gian*:

  • for (i = 0; i < n; i++) if (x[i] == y[i]) return EFAIL; rõ ràng mất ít vòng lặp hơn do đó, nó thực tế được đảm bảo chạy trong thời gian thay đổi tùy thuộc vào các giá trị bí mật của x[i]y[i].

  • Một điều kiện phụ thuộc bí mật đơn thuần for (i = 0; i < n; i++) if (x[i]) bad++;, nếu x[i] là bí mật, cũng có thể chạy trong thời gian thay đổi ngay cả khi vòng lặp không bị hủy bỏ sớm. Tại sao?

    • Đây là một xấp xỉ thô. Các hướng dẫn máy mà CPU có thể thực thi trông giống như sau:

      0: tmp := x[i]
              rẽ nhánh thành 1 nếu tmp bằng 0
              tệ := tệ + 1
      1: tôi := tôi + 1
              rẽ nhánh về 0 nếu i < n
      

      Các số hướng dẫn được thực thi phụ thuộc vào giá trị của x[i] ở mỗi lần lặp lại: chúng tôi bỏ qua tệ := tệ + 1 trên một số lần lặp. Đây là một mô hình tốt cho cách tấn công theo thời gian sớm, ví dụ., RSA hoạt động như trong Bài báo chuyên đề của Kocher về các cuộc tấn công thời gian: vòng lặp lũy thừa mô-đun chính tính toán bình phương mô-đun 2048 bit (giả sử) một cách vô điều kiện, nhưng tính toán phép nhân mô-đun 2048 bit có điều kiện tùy thuộc vào giá trị của số mũ bí mật. Việc bỏ qua phép nhân sẽ thay đổi đáng kể thời gian thực hiện của toàn bộ thao tác.

    • Tuy nhiên, có một lý do khác, và nó liên quan đến dự đoán chi nhánh, một yếu tố thiết kế quan trọng giúp cho các CPU hiện đại chạy rất nhanh trên nhiều khối lượng công việc—ngay cả khi bạn viết cùng một lượng mã (ví dụ: cùng một số lệnh máy và bằng cách nào đó bạn đảm bảo rằng chúng thực hiện cùng một số chu kỳ để tính toán ) trong mỗi nhánh của một điều kiện, thời gian cần thiết để thực thi có thể phụ thuộc vào cách thực hiện của điều kiện.

    • Nói chung, CPU rất tệ trong việc giữ hướng dẫn nào đã được thực hiện bí mật, vì vậy đừng làm cho lựa chọn hướng dẫn phụ thuộc vào bí mật.

  • Việc tra cứu bảng/mảng có thể mất một khoảng thời gian khác nhau tùy thuộc vào bộ nhớ nào đã được lưu trong bộ nhớ cache của CPU. Theo đó, nếu các vị trí trong mảng mà bạn đang đọc phụ thuộc vào một bí mật, thời gian cần thiết có thể phụ thuộc vào bí mật đã được khai thác để khôi phục khóa AES theo thời gian bộ đệm.

    (Điều này làm cho AES trở thành một thiết kế khá đáng nghi ngờ khi nhìn lại, với việc cố ý sử dụng các tra cứu bảng phụ thuộc vào khóa! NIST's cơ sở lý luận được công bố (§3.6.2, Tấn công vào triển khai: Vai trò của hoạt động) tuyên bố một cách tò mò rằng tra cứu bảng là không dễ bị tấn công theo thời gian mặc dù có rất nhiều cuộc tấn công như vậy đã được báo cáo kể từ đó.)

  • Thay đổi khoảng cách thay đổi như x = y << z có thể mất nhiều thời gian hơn trên một số CPU nếu z lớn hơn và ít thời gian hơn nếu nó nhỏ hơn.

    (Điều này làm cho RC5 và người vào chung kết AES RC6 thiết kế khá đáng ngờ khi nhìn lại, với việc họ cố ý sử dụng khoảng cách xoay phụ thuộc vào phím!)

  • Trên một số CPU, phép nhân có thể chạy nhanh hơn hoặc chậm hơn tùy thuộc vào việc nửa trên của đầu vào có bằng 0 hay không.

  • Về nguyên tắc, việc bổ sung số nguyên 64 bit trên CPU 32 bit có thể mất nhiều thời gian hơn tùy thuộc vào việc có mang hay không. Điều này là bởi vì, khi x, y, và z, là số nguyên 64 bit, logic x = y + z có thể trông giống như:

    int mang = 0;
    x[0] = y[0] + z[0];
    nếu (phần bổ sung trước bị tràn)
      mang = 1;
    x[1] = y[1] + z[1] + mang;
    

    Do đó, thời gian cần thiết có thể phụ thuộc vào việc liệu có sự chuyển đổi từ tổng của các nửa 32 bit thấp sang tổng của các nửa 32 bit cao hay không. (Trong thực tế, điều này thường chỉ là mối quan tâm đối với các CPU kỳ lạ hoặc đối với các loại kênh phụ khác như phân tích năng lượng có liên quan nhiều đến thẻ thông minh hơn là máy tính xách tay và điện thoại.)

Điều này nghe có vẻ hơi áp đảo. Chúng ta có thể làm gì?

Có một số thao tác thường làm chạy trong thời gian liên tục trên hầu hết các CPU. Họ đang:

  • hoạt động bitwise: x & y, x | y, x^y, ~xvà những cái khác không xuất hiện trong C như AND-with-complement.
  • khoảng cách không đổi thay đổi và luân chuyển như sự thay đổi x << 3 hoặc phép quayx <<< 3 (không phải tiêu chuẩn C nhưng phổ biến trong mật mã; nó có nghĩa là (x<<3) | (x >> (32 - 3)), nếu x là 32-bit).
  • Thường cộng và trừ số nguyên: x + y, x - y, khi nào xy là (giả sử) các số nguyên 32 bit không dấu trên CPU 32 bit và thường là các số nguyên 64 bit trên CPU 32 bit với sự trợ giúp của các lệnh ADD-with-carry.
  • Thỉnh thoảng phép nhân số nguyên, nhưng câu chuyện về phép nhân là phức tap, điều này không may cho mật mã vì phép nhân kết hợp các bit xung quanh khá độc đáo và có các thuộc tính đại số hữu ích.

Để được rõ ràng: tôi không có nghĩa là một trình biên dịch C đảm bảo các hoạt động này chạy trong thời gian không đổi nếu bạn sử dụng chúng trong chương trình C; Tôi chỉ đơn thuần sử dụng ký hiệu C cho các hoạt động mà CPU thường thực hiện trong thời gian không đổi. (Thông tin thêm về cảnh báo này trong giây lát.)

“Nhưng chờ đã,” bạn phản đối, “Làm sao tôi có thể viết một chương trình hữu ích từ những thao tác này? Không có điều kiện? Không có vòng lặp? Không có mảng?â

Đầu tiên, bạn không cần phải tránh các điều kiện, vòng lặp hoặc mảng hoàn toàn. Họ chỉ không thể phụ thuộc vào bí mật. Ví dụ, for (i = 0; i < 32; i++) ... x[i] ... Ổn. Nhưng mà for (i = 0; i < m[0]; i++) ... không ổn nếu m[0] được cho là bí mật, và for (i = 0; i < m[0]; i++) ... tab[x[i]] ... không ổn nếu x[i] được cho là bí mật.

Thứ hai, bạn vẫn có thể xây dựng những thứ này! Nó chỉ phức tạp hơn một chút. Ví dụ, giả sử b là một uint32_t hoặc là 0 hoặc 1. Sau đó b - 1 tương ứng là -1 = 0xffffffff hoặc 0, vì vậy

x = ((b - 1) & z) | (~(b - 1) & y);

nguyên nhân x = y nếu b là 1, hoặc x = z nếu b là 0âgiống như x = (b? y : z), nhưng không có nhánh. Rõ ràng, điều này đòi hỏi tính toán cả hai yz, vì vậy có một số tác động đến hiệu suất! Tương tự, bạn có thể tra cứu một phần tử của bảng bằng cách tra cứu tất cả các các phần tử của bảng và chọn phần tử bạn muốn bằng các thao tác bitwise như thế này. Không nhanh bằng x[i], nhưng cũng không bị rò rỉ.

Nói chung, bạn có thể chuyển đổi một chương trình có điều kiện thành một mạch logic không có điều kiện, ngay cả khi bạn không muốn để vì lý do hiệu suất. Có nhiều thủ thuật tương tự khác mà bạn có thể làm. Bạn có thể phác thảo một thói quen bình đẳng bộ nhớ thời gian không đổi, chẳng hạn như crypto_verify_32 như thế này, giả sử x và y là các mảng uint8_t:

kết quả uint32_t = 0;
cho (i = 0; i < 32; i++)
  kết quả |= x[i] ^ y[i];
return ((kết quả - 1) >> 8) & 1;

Bài tập: Điều này có trả về 0 cho bằng và 1 cho bất đẳng thức hay 0 cho bất đẳng thức và 1 cho bằng không?

Viết các chương trình như thế này—và áp dụng các hệ thống mật mã như X25519 khuyến khích triển khai giống như thế này, thay vì các hệ thống mật mã như RSA hoặc AES khuyến khích triển khai liên quan đến các nhánh phụ thuộc bí mật hoặc tra cứu bảng phụ thuộc bí mật—là một khởi đầu tốt để cắm các kênh phụ thời gian.

Nhưng, có một nhược điểm! Hãy nhớ khi tôi nói rằng trình biên dịch C không đảm bảo thời gian không đổi? Trình biên dịch C thông minh như Clang/LLVM có thể nhận ra thông minh đó crypto_verify_32 vòng lặp trên có thể được thực hiện hiệu quả hơn bằng cách hủy bỏ nó sớm và có thể hoàn tác công việc khó khăn mà bạn đã làm để viết lại nó dưới dạng một mạch logic chạy trong thời gian không đổi. (Trong những trường hợp khác, nó có thể giúp ích cho bạn, chẳng hạn bằng cách chuyển đổi x = (b?y:z); thành lệnh di chuyển có điều kiện, CMOV, không có nhánh, nhưng thông thường bạn không thể dựa vào thiện chí của trình biên dịch C.)

Có một số thủ thuật bạn có thể thực hiện để ngăn chặn điều này, chẳng hạn như một đoạn lắp ráp nội tuyến khiến trình biên dịch loại bỏ gần như tất cả các giả định để tối ưu hóa:

kết quả uint32_t = 0;
cho (i = 0; i < 32; i++)
  kết quả |= x[i] ^ y[i];
asm dễ bay hơi ("" ::: "bộ nhớ");
return ((kết quả - 1) >> 8) & 1;

Điều này có thể hoặc không thể làm việc với trình biên dịch của bạn. Để tự tin, bạn thực sự phải kiểm tra mã máy do trình biên dịch tạo ra—và thậm chí sau đó, trình biên dịch có thể thực hiện các tối ưu hóa đúng lúc mà viết lại mã máy theo phân tích lược tả, đặc biệt là trong các ngôn ngữ cấp cao hơn như Java. Vì vậy, bạn có thể thực sự muốn viết logic trong hợp ngữ (hoặc trong ngôn ngữ lập trình như qhasm có thể tạo ra hợp ngữ được tinh chỉnh đáng tin cậy hơn trình biên dịch C) và chỉ cần gọi nó từ C.

Có thể một ngày nào đó trình biên dịch C sẽ áp dụng một bí mật vòng loại, như hăng sô hoặc bay hơi, buộc trình biên dịch chỉ tạo các lệnh máy đã biết—trong một số kiểu CPU!—để chạy trong thời gian không đổi khi hoạt động trên đối tượng và ngăn trình biên dịch sử dụng các lối tắt như hủy bỏ sớm phụ thuộc vào bí mật từ một vòng lặp. Nhưng ngày đó vẫn chưa đến.

Ngoài ra còn có vấn đề về việc hướng dẫn máy nào thực sự chạy trong thời gian liên tục trên CPU, điều này đôi khi được ghi lại và đôi khi đáng tin cậy. Vì vậy ngoài việc làm kỹ thuật để xây dựng các chương trình của bạn từ các mạch logic, bạn cũng cần phải làm khoa học để tìm ra hoạt động nào thực sự an toàn để sử dụng trên CPU.

Điều này đưa chúng ta trở lại điểm ban đầu: Bạn thực sự muốn tập trung nỗ lực duy trì điều này thành thói quen của thư viện, để mỗi lập trình viên không phải theo dõi sự thất thường của trình biên dịch (và thiết kế CPU!) trong mã được tạo và thời gian của riêng họ, và thay vào đó có thể để nó cho gấu hàng xóm thân thiện của chúng tôi.


Có biện pháp đối phó nào khác ngoài logic thời gian không đổi không? Thỉnh thoảng đúng.

  • Bạn có thể đưa nhiễu ngẫu nhiên vào logic của mình, với hy vọng rằng nó sẽ gây nhầm lẫn cho các phép đo của kẻ tấn công. Nhưng đã có tiếng ồn trong các phép đo của họ, chẳng hạn như lập lịch trình trong hệ điều hành, vì vậy họ chỉ cần lấy thêm mẫu—và hóa ra tiếng ồn là không phải là một biện pháp đối phó kênh phụ rất hiệu quả.

    Cụ thể, nhiễu nhân tạo làm tăng chi phí của kẻ tấn công nhiều nhất bằng bình phương tỷ lệ nhiễu nhân tạo so với nhiễu thực, thấp hơn nhiều so với mức thường được coi là khoảng cách chấp nhận được đối với bảo mật trong mật mã. Vì vậy, nó chủ yếu khiến bạn mất rất nhiều thời gian mà không làm gì cả.

  • Bạn có thể sử dụng các thuộc tính đại số của hệ thống mật mã để ngẫu nhiên hóa nó, đôi khi được gọi là âblindingâ. Ví dụ, thay vì tính toán y^d mod n ở đâu đ là một số mũ bí mật trong RSA, bạn có thể chọn r một cách ngẫu nhiên, tính toán s := r^e mod n ở đâu e*d â¡ 1 (mod (n)), nhân y qua S để có được (y * r^e) mod n, tính toán (y * r^e)^d mod n = (r * y^d) mod n, rồi chia hết r.

    Nhiều triển khai, chẳng hạn như OpenSSL, sử dụng phương pháp này vì đây là cách dễ dàng để trang bị thêm cho triển khai hệ thống mật mã hiện có như RSA có cấu trúc đại số cần thiết. Đó không phải là một ý tưởng tồi giống như tiếng ồn ngẫu nhiên, nhưng nó có cái giá của nó: bạn phải thực hiện thêm công việc để ngẫu nhiên hóa, bạn phải có logic phân chia mô-đun hoặc đảo ngược—và các kênh bên vẫn có thể rò rỉ thông tin về rđ. Ví dụ, ngay cả phép lũy thừa mô-đun mù cũng sẽ làm rò rỉ trọng số Hamming của đ trừ khi bạn thực hiện các biện pháp đối phó bổ sung như thêm bội số ngẫu nhiên của (N) đến đ đầu tiên—có thể hiển thị các kênh phụ bổ sung, vân vân.

  • Đối với trường hợp cụ thể so sánh hai chuỗi byte bằng nhau (ví dụ: hai mã xác thực thông báo), một tùy chọn hợp lý là băm chúng với họ hàm giả ngẫu nhiên như HMAC-SHA256 dưới khóa bí mật một lần k, và kiểm tra xem HMAC-SHA256_k(x) == HMAC-SHA256_k(y).

    Xác suất dương tính giả là 1/2256, đó là một xác suất nhỏ hơn bạn từng phải lo lắng. Bạn có thể sử dụng đẳng thức thời gian thay đổi cho HMAC một cách an toàn vì nếu xkhông phải tương đương với y, thì lượng thời gian ngay cả trong ngây thơ nhất thói quen bình đẳng chuỗi byte (giả sử nó không thoát ra ở byte 0 đầu tiên hoặc thứ gì đó ngu ngốc như thế!) sẽ độc lập với các giá trị của xy: có xác suất 255/256 nó sẽ dừng sau một lần lặp, xác suất 65535/65536 sau hai lần lặp, vân vân.

    Tất nhiên, điều này chỉ thực sự hữu ích nếu bạn có thể triển khai HMAC-SHA256 trong thời gian liên tục! May mắn thay, SHA-256 được thiết kế để dễ dàng triển khai dưới dạng mạch logic thời gian không đổi, do đó, việc triển khai C có xu hướng để có khả năng chống lại các kênh bên một cách hợp lý—nhưng, giả sử, Python sẽ khiến bạn gặp rắc rối vì bộ đệm số nguyên nhỏ nếu không có gì khác.


* Thật không may, thuật ngữ này hơi khó hiểu. Đây hằng số thời gian có nghĩa là lượng thời gian không thay đổi tùy thuộc vào đầu vào, và không giống như tiệm cận khái niệm constant-timeâ trong khoa học máy tính, thường được viết là O(1), có nghĩa là lượng thời gian có thể thay đổi tùy thuộc vào đầu vào nhưng được giới hạn bởi một hằng số. Tôi xin lỗi. Tôi đã không phát minh ra thuật ngữ. Tôi có thể đã chọn thời gian cố địnhâ so với âthời gian thay đổiâ nhưng bây giờ đã quá muộn—ââhằng thời gianâ đã ăn sâu vào văn học.

Điểm:7
lá cờ id

Đây là hai xu của tôi:

Một cuộc tấn công theo thời gian sử dụng thời gian cần thiết để thực hiện một thuật toán dựa trên các đầu vào khác nhau.

Hãy xem một vấn đề đơn giản hơn, chẳng hạn như tìm xem một ký tự có tồn tại trong một chuỗi bí mật hay không. Trong một thuật toán truyền thống, bạn sẽ lặp qua chuỗi và trả về một giá trị boolean sau khi bạn tìm thấy ký tự. Điều này sẽ mất nhiều thời gian hơn khi một ký tự ở xa hơn, do đó làm rò rỉ một số thông tin của chuỗi bí mật.

Nếu chúng tôi muốn làm cho thời gian không đổi này, chúng tôi sẽ làm cho việc tìm ký tự mất cùng thời gian cho dù nó ở đầu hay cuối, ví dụ: lặp lại toàn bộ chuỗi và quay lại sau khi bạn kết thúc quá trình lặp lại.

Một thứ khác mất nhiều thời gian hơn là các chi nhánh. Một nhánh là một đoạn mã có thể được thực thi hay không, tức là một nếu bản tường trình.

Ví dụ lấy từ hàm nhân GF(2^8):

với chi nhánh:

nếu ((a >> 7) & 1)
    a = (a << 1)^ đa thức;
khác
    một <<= 1

không nhánh:

const uint8_t mask = -((a >> 7) & 1); // 0xff nếu bit cao nhất được đặt, ngược lại 0x00
a = (a << 1) ^ (đa thức & mặt nạ); // chỉ áp dụng đa thức nếu mặt nạ là 0xff

Như bạn có thể thấy, mã có nhánh có thể thực hiện thêm 1 thao tác trong một trong các trường hợp, trong khi mã không có nhánh luôn thực hiện các thao tác giống nhau.

Điểm:6
lá cờ de

Những gì bạn đã nghe là chính xác.

Đó là một cuộc đấu tranh khó khăn. Trình biên dịch và phần cứng không được thiết kế để giúp bạn và chúng được thiết kế có chủ ý để thực hiện những việc gây nhầm lẫn cho những gì bạn đang cố gắng thực hiện.

  • Tiêu chuẩn C không nói gì về thời gian. Trình biên dịch không thử để đảm bảo rằng, nếu mã C của bạn trông giống như một thuật toán thời gian không đổi, thì nó luôn biên dịch thành mã máy thời gian không đổi.

    Một cách tiếp cận là tìm hiểu rất chi tiết những gì trình biên dịch có thể làm và cách tránh tối ưu hóa gây nhiễu. Đánh dấu chức năng như vô tuyến, ví dụ, có thể quan trọng.

    Một cách tiếp cận khác là loại bỏ hoàn toàn trình biên dịch khỏi bức tranh. Bạn có thể biên dịch mã C nhạy cảm với thời gian của mình thành hợp ngữ một lần, kiểm tra hợp ngữ để đảm bảo rằng đó vẫn là thuật toán thời gian không đổi, kiểm tra mã hợp ngữ và sử dụng mã đó, chứ không phải C gốc, làm nguồn khi xây dựng chương trình của bạn.

  • Ở cấp độ mã máy, có một vấn đề tương tự. CPU không thử để đảm bảo rằng một chuỗi hướng dẫn liên tục luôn chạy trong thời gian không đổi. Nhiều cơ chế trong phần cứng (đặc biệt là bộ đệm) cố gắng tăng tốc mọi thứ, nhưng không phải lúc nào cũng thành công. Mã có thể chạy chậm hơn trong những trường hợp đặc biệt rất cụ thể khi việc tăng tốc không hiệu quả. Điều này có thể rò rỉ thông tin.

    Tôi không biết làm thế nào để giải quyết vấn đề này ngoại trừ việc hiểu rất rõ về phần cứng và đo lường.

user7761803 avatar
lá cờ vn
"CPU không cố gắng đảm bảo rằng một chuỗi liên tục các lệnh luôn chạy trong thời gian không đổi" không, nhưng một số kiến ​​trúc CPU cho phép định thời gian độc lập với dữ liệu của các lệnh riêng lẻ, ví dụ: trong Armv8.4A - https://developer.arm.com/documentation/ddi0595/2021-06/AArch64-Registers/DIT--Data-Independent-Timing
Điểm:4
lá cờ gd

2 xu khiêm tốn của tôi: các kỹ thuật cắt bit nếu phù hợp với trường hợp của bạn: https://timtaubert.de/blog/2018/08/bitslicing-an-introduction/

CHỈNH SỬA

Tôi không muốn sao chép (làm xấu chúng đi) những từ mà bạn có thể đọc trong trang được liên kết, nhưng tôi đã được yêu cầu một thứ gì đó hơn là một liên kết, vì vậy, giả sử rằng về cơ bản, việc cắt bit mô phỏng việc triển khai phần cứng trong phần mềm: toàn bộ thuật toán được trình bày như một chuỗi các hoạt động Boolean nguyên tử, sau đó được thực hiện trong thời gian không đổi. Tôi xin lỗi tôi không thể cung cấp thêm thông tin chi tiết, tôi chỉ mới biết về kỹ thuật này gần đây trong một cuộc hội thảo về các phương pháp hay nhất về mã hóa ứng dụng: Tôi chưa đi sâu hơn, nhưng có vẻ như đó là thứ bạn cần, vì bạn chỉ muốn tạo ra khả năng chống thời gian một phần rất cụ thể trong mã của bạn, do đó, chi phí triển khai có thể phải chăng trong trường hợp của bạn (tất nhiên đó sẽ là cơn ác mộng đau đớn đối với mã phạm vi rộng)

Điểm:3
lá cờ kz

Đây là tin tốt: Thuật toán của bạn không cần phải là "thời gian không đổi". Nó chỉ cần độc lập với dữ liệu đầu vào. Sẽ tốt nếu có các biến thể ngẫu nhiên; chúng thực sự hữu ích nếu các biến thể ngẫu nhiên lớn hơn các biến thể phụ thuộc vào dữ liệu.

Truy cập bộ nhớ nổi tiếng là chạy vào thời gian thay đổi. Vì vậy, nếu bạn có các mẫu truy cập phụ thuộc vào dữ liệu, thì điều đó thật tệ. Một ví dụ đã được đưa ra, bảng [x[i]].Truy cập x[i] trong một vòng lặp không phải là thời gian cố định, nhưng thời gian không phụ thuộc vào dữ liệu. Việc truy cập bảng [x[i]] phụ thuộc vào dữ liệu, vì vậy có thể lấy thông tin về dữ liệu.

Các thao tác nhân và chia có thể phụ thuộc vào dữ liệu, vì vậy đó là điều bạn cần kiểm tra.

poncho avatar
lá cờ my
“Chỉ cần độc lập với dữ liệu đầu vào”; trên thực tế, nó cần độc lập với dữ liệu bí mật, tức là bất kỳ thứ gì mà chúng ta cần cho rằng kẻ thù không có quyền truy cập. Sẽ không có vấn đề gì nếu thời gian thao tác chữ ký khóa công khai của bạn phụ thuộc vào thông báo, vì chúng tôi cho rằng dù sao thì thông báo cũng là công khai.
Điểm:2
lá cờ nc

Nếu bạn có thể chấp nhận câu trả lời "có thể là thời gian không đổi" (thay vì "thời gian chắc chắn là không đổi"), thì bạn có thể đánh giá bằng thực nghiệm xem mã của bạn có phải là thời gian không đổi hay không. Làm như vậy, tôi khuyên bạn nên công cụ thằng ngu. Để trích dẫn GitHub Readme:

Cái này hoạt động ra sao? Tóm lại, mã này có hai khác nhau đầu vào, chạy đoạn mã nhiều lần cho mỗi đầu vào và đo lường mất bao nhiêu thời gian. Nếu có sự khác biệt thống kê về (trung bình) thời gian cần thiết để chạy với các đầu vào khác nhau, thực hiện được coi là không phải là hằng số thời gian. Để biết chi tiết, đọc của chúng tôi paper hoặc xem nguồn.

Và:

Mã của tôi vượt qua bài kiểm tra này. Nó có nghĩa là nó thực sự là hằng số thời gian? Tuyệt đối không. […]

Một công cụ tương tự là ct-fuzz, sử dụng thử nghiệm mờ hộp xám dựa trên vùng phủ sóng để phát hiện rò rỉ thời gian. Thông tin thêm về phương pháp luận trong bài báo ct-fuzz: Fuzzing cho rò rỉ thời gian.

Với cả hai công cụ và ct-fuzz, bạn không thể chắc chắn 100% rằng mã C của bạn là thời gian không đổi, nhưng nếu có rò rỉ thời gian lớn, bạn sẽ tìm thấy chúng. Và nếu công cụ không thể tìm thấy bất kỳ rò rỉ thời gian nào, thì bạn có thể "tự tin một cách hợp lý" rằng không có rò rỉ.


Một vài công cụ khác có thể được sử dụng để xác định tĩnh xem một đoạn mã C có phải là thời gian không đổi hay không:

  • ctgrind, được xây dựng dựa trên valgrind, theo dõi mọi bit của bộ nhớ bí mật và kiểm tra xem các điều kiện có không phụ thuộc vào các bit bí mật hay không.

  • Kiểm tra bộ đệm, mà theo nó GitHub Readme:

CacheAudit là một công cụ phân tích tĩnh các kênh phụ của bộ đệm. Kiểm tra bộ đệm lấy đầu vào là nhị phân chương trình x86 32 bit và cấu hình bộ đệm, và nó tạo ra các đảm bảo an ninh định lượng, chính thức cho một tập hợp toàn diện các đối thủ bộ đệm, cụ thể là những đối thủ dựa trên quan sát trạng thái bộ đệm, dấu vết của các lần truy cập và bỏ lỡ và thực thi lần.

Bởi vì những công cụ đó sử dụng phân tích tĩnh, bạn có thể tự tin hơn rằng không có rò rỉ thời gian nếu công cụ không thể tìm thấy. Tuy nhiên, không có công cụ nào trong số đó có mô hình phần cứng chính xác về những gì rò rỉ và những gì không. Điều này có nghĩa là họ giả định rằng, chẳng hạn, phép nhân là hằng số thời gian. Nếu phép nhân thực sự không phải là thời gian cố định, thì những công cụ đó sẽ không phát hiện ra các rò rỉ thời gian tiềm ẩn. Xem của Peter Green câu trả lời để biết thêm về những hoạt động nào có thể bị rò rỉ, nhưng có thể được CacheAudit và ctgrind cho là liên tục.

Điểm:1
lá cờ br

Bạn có thể thực hiện các phép đo thời gian chính xác bằng hàm nội tại __rdtscp(), sau đó thực hiện thống kê về kết quả

Hàm này hiển thị thanh ghi 64 bit bên trong tăng dần ở mỗi chu kỳ xung nhịp, tần số xung nhịp là tần số bộ xử lý chính thức như được thấy trong /proc/cpuinfo sau ký hiệu @

Bộ xử lý thế hệ thứ 10 phải có cờ hằng_tsc như đã thấy trong/proc/cpuinfo

Đăng câu trả lời

Hầu hết mọi người không hiểu rằng việc đặt nhiều câu hỏi sẽ mở ra cơ hội học hỏi và cải thiện mối quan hệ giữa các cá nhân. Ví dụ, trong các nghiên cứu của Alison, mặc dù mọi người có thể nhớ chính xác có bao nhiêu câu hỏi đã được đặt ra trong các cuộc trò chuyện của họ, nhưng họ không trực giác nhận ra mối liên hệ giữa câu hỏi và sự yêu thích. Qua bốn nghiên cứu, trong đó những người tham gia tự tham gia vào các cuộc trò chuyện hoặc đọc bản ghi lại các cuộc trò chuyện của người khác, mọi người có xu hướng không nhận ra rằng việc đặt câu hỏi sẽ ảnh hưởng—hoặc đã ảnh hưởng—mức độ thân thiện giữa những người đối thoại.