Điểm:11

If/else có dễ bị tấn công kênh phụ theo thời gian không?

lá cờ tf
Tom

Tôi có một nhánh trong C++:

nếu (x & 1)
{
    x = hàm_1(x);
}
khác
{
    x = hàm_2(x);
}

Nếu hàm_1chức năng_2 là thời gian không đổi và phải mất cùng thời gian để tính toán chúng, liệu việc phân nhánh như vậy có còn dễ bị tấn công kênh phụ không? Kẻ tấn công bằng cách nào đó có thể biết điều kiện nào đã được thực thi không?

Tom avatar
lá cờ tf
Tom
@kelalaka được rồi, cảm ơn. Còn ((x & 1) * funtcion_1(x)) ^ ((~x & 1) * funtcion_2(x)) thì sao? Là tốt hơn hay vẫn còn xấu? Tôi nghi ngờ đó cũng không phải là một giải pháp tốt, nhưng tôi không chắc.
kelalaka avatar
lá cờ in
Tôi không thấy sự khác biệt, bên cạnh đó, đảm bảo rằng không có gì được tối ưu hóa. Đây là lý do tại sao các khối ASM phổ biến khi triển khai kênh bên. Ở trên có lỗi, sửa ở đây ( bình luận khác bị xóa) $$x = (x \wedge 1)* f_1(x) + ((x \wedge 1)\oplus 0x1 ) * f_2(x)$$
Tom avatar
lá cờ tf
Tom
@kelalaka vâng, đã xảy ra nhầm lẫn, đó là lý do tại sao tôi nghĩ đó là điều gì đó khác biệt. Bây giờ kết quả giống như trong công thức của tôi. Nhưng tôi nghĩ của bạn vẫn tốt hơn, bởi vì trong công thức của tôi có "~x & 1" và đây có lẽ không phải là thời gian cố định (so với x & 1), đặc biệt nếu x là số lớn
kelalaka avatar
lá cờ in
Nó không phải là về tính bất biến của ~ hay x-or.Đó là về việc bạn luôn thực hiện cả hai phương thức bất kể giá trị của $x$ và ở bước cuối cùng, bạn thêm chúng bằng mặt nạ. Điều này thậm chí không yêu cầu phải dựa vào tính kịp thời liên tục của $f_1$ và $f_2$
kelalaka avatar
lá cờ in
Xem câu trả lời chính tắc của [Squeamish Ossifrage](https://crypto.stackexchange.com/a/96634/18298)...
Điểm:14
lá cờ ng

Có, nếu/khác dễ bị tấn công theo thời gian. Vì vậy, việc chọn hàm để gọi theo chỉ số mảng, như trong đó câu trả lời khác.

Giải pháp tiếp theo đáng tin cậy duy nhất để thực thi thời gian liên tục bằng ngôn ngữ cấp cao, thiếu dấu hiệu về môi trường đích, là: Đảm bảo đường dẫn thực thi mã và các mẫu truy cập bộ nhớ độc lập với dữ liệu.

Điều này ngăn cản việc gọi một trong số hàm_1 hoặc chức năng_2 (như mã của câu hỏi và câu trả lời khác đã nói) và mảng lập chỉ mục, dựa trên dữ liệu thay đổi dưới sự kiểm soát hoặc theo cách mà kẻ thù có thể quan sát được, thậm chí một phần và gián tiếp.

Nếu chi phí thêm cho việc gọi cả hai hàm có thể chấp nhận được và hành vi của các hàm được xác định rõ và chấp nhận được bất kể bit thứ tự thấp của đầu vào của chúng, thì điều này sẽ thực hiện được với hầu hết các trình biên dịch và phần cứng C hiện tại:

   m = -(x&1); // biến mặt nạ; giả sử m và x là int
   x = (hàm_1(x) & m) | (hàm_2(x) & ~m);

hoặc tương đương

   x = hàm_1(x) & -(x&1) | hàm_2(x) & ~ -(x&1);

Điều này gọi cả hai chức năng và chọn kết quả một cách thích hợp, sử dụng mặt nạ đó -(x&1) có tất cả các bit tại 1 cho số lẻ x, tại 0 khác.

Tôi không biết bất kỳ trình biên dịch CPU + C nào được giới thiệu từ năm 1985, nơi sẽ có sự phụ thuộc về thời gian vào x được giới thiệu bởi kỹ thuật đó. Tài liệu hiện đại về việc triển khai tiền điện tử trong thời gian liên tục cho rằng không có gì, không cần phải nói. Đó là mặc dù thời gian thực thi của các CPU hiệu suất cao hiện đại là không xác định và có thể thay đổi không thể đoán trước theo quan điểm của một lập trình viên ứng dụng (ví dụ: do trình biên dịch/trình liên kết và các tùy chọn của chúng, căn chỉnh mã và dữ liệu, ngắt, trạng thái chờ bộ nhớ và chu kỳ làm mới , bộ đệm, đường ống, thực thi suy đoán, bộ dự đoán nhánh, các luồng khác trên cùng lõi/CPU/CPU được liên kết, phân xử tài nguyên giữa các luồng trên cùng lõi, ảo hóa, cài đặt BIOS hoặc VM và luồng vi mã bị lỗi dường như vô tận thay đổi ảnh hưởng đến hiệu suất để giảm thiểu cuộc tấn công kênh bên được báo cáo mới nhấtâ¦)

Điều quan trọng là, giả thuyết phi thực tế đó trong câu hỏi

hàm_1chức năng_2 là thời gian không đổi và phải mất cùng thời gian để tính toán chúng

có thể được thay thế bằng hợp lý và sai:

Sự thay đổi về thời gian thực hiện của hàm_1 không tương quan với giá trị của x. chức năng_2 có cùng một tài sản.

Hãy cẩn thận 1: các ngôn ngữ C/C++ chỉ cung cấp bảo hiểm về kết quả thu được, không bảo hiểm về thời gian thực hiện. Tôi biết không có hướng dẫn sử dụng trình biên dịch nào quan tâm và một số CPU không có tài liệu hướng dẫn.

Hãy cẩn thận 2: -(x&1) để lấy mặt nạ theo thứ tự thấp x là một thành ngữ nổi tiếng và ngày nay hoạt động trên hầu hết các CPU. Đó là bởi vì họ sử dụng bù hai quy ước, với -1 đại diện bởi tất cả những người. Nhưng tôi đã mất dấu nếu C hứa sẽ che giấu bất kỳ hành vi khác nào mà phần cứng có thể có (tôi để lại điều đó cho các nhận xét hữu ích). Và điều đó cần phải được xác định chắc chắn không có vấn đề gì.

Cảnh báo trước 3: Có thể cần phải điều chỉnh tùy theo loại x do C quy định khuyến mại. tôi thường đúc 1 đến loại x, ví dụ. nếu x là một uint32_t, Tôi sử dụng -(x&(uint32_t)1)và sẽ hài lòng với nó nếu không có cảnh báo của trình biên dịch.

Một số trình biên dịch phàn nàn khi lấy âm của một biểu thức không dấu. Họ sai, theo tiêu chuẩn C. Con đường dễ dàng là cúi đầu và sử dụng (0-(x&1)), với việc truyền các hằng số như trên. Một cách khác là vô hiệu hóa khiếu nại đó bằng một số pragma hoặc tùy chọn. Tuy nhiên, theo kinh nghiệm của tôi, một cách khác là vô vọng, đó là gửi báo cáo sự cố. Các đối số được thực hiện cẩn thận chuyển đến NUL (tương đương cục bộ của /dev/null) bao gồm: cấu trúc hữu ích, phổ biến, được hỗ trợ rõ ràng bởi ISO C; và đưa ra cảnh báo về nó có thể khiến tất cả các cảnh báo bị tắt tiếng một cách bừa bãi.

Lưu ý: Tôi giả sử kiểu trả về của hàm_1chức năng_2 là cùng loại với x.


Việc thực hiện bảo mật/mã hóa trên các CPU tiêu chuẩn nơi mà các đối thủ cũng có thể chạy mã đã trở nên dễ vỡ kể từ những năm 1980. Có những tiến bộ hữu ích: cách ly quy trình với Đơn vị quản lý bộ nhớ, vòng bảo mật hoặc vùng kín. Có phần mềm phỏng đoán để giảm mức độ dễ bị tổn thương xuống mức có thể chấp nhận được. Nhưng, cho đến nay, không có viên đạn bạc nào.

Vì vậy, khi rủi ro cao, tốt nhất bạn nên giảm tải các chức năng bảo mật cho phần cứng đáng tin cậy (Thẻ thông minh, CPU bảo mật, HSM, TPMâ¦) được thiết kế để bảo mật trước chứ không phải hiệu suất trước; và nơi các đối thủ không thể chạy mã (đơn giản nhất là tốt nhất) hoặc được thiết kế sẵn.

SAI Peregrinus avatar
lá cờ si
C không (chưa) hứa hẹn hành vi bổ sung của hai. Trong chuẩn C23 sắp tới, điều đó sẽ được đảm bảo, vì vậy một số nền tảng sẽ cần mô phỏng phần bù hai để có trình biên dịch C23. Ngoài ra, biểu thức không phải là không dấu, ngay cả khi `x` không dấu. `1` là một số nguyên có dấu, vì vậy cả `x` và `1` đều được thăng cấp theo các quy tắc trong mục 6.3.1.8. Sau đó, `&` được áp dụng và kết quả bị phủ định. Kết quả là loại số nguyên (được thăng cấp) của `(x&1)`. Bạn có thể sử dụng chữ đã nhập (ví dụ: `1ULL`) để làm cho `1` có cùng thứ hạng với `x`.
lá cờ in
@SAIPeregrinus: POSIX thực hiện [hứa](https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/stdint.h.html) hai phần bổ sung ngay bây giờ, vì vậy nếu bạn đang phát triển cho một mục tiêu tuân thủ POSIX (mà là hầu hết trong số họ bao gồm cả Windows), thì bạn tốt.
Lorenzo Donati support Ukraine avatar
lá cờ ru
@SAIPeregrinus Nitpick: 1ULL là `unsigned long long` có kích thước *ít nhất* 64 bit, theo tiêu chuẩn C, do đó, nó có thứ hạng cao hơn `x` (`uint32_t`). Tham khảo: Tiêu chuẩn C (Dự thảo N1256 - C99) phần **5.2.4.2.1 Kích thước của các loại số nguyên**: *giá trị tối đa cho một đối tượng thuộc loại unsigned long long int* `ULLONG_MAX 18446744073709551615 // 2^64 â 1`
Tom avatar
lá cờ tf
Tom
Đó là câu trả lời tuyệt vời và chủ đề lớn, như tôi thấy. Nhân tiện, tôi đang làm việc với kiểu dữ liệu __m128i, vì vậy tôi làm một số thứ như: _mm_xor_si128(x,k) & -_mm_cvtsi128_si64(x & 1). Để có được bit có ý nghĩa thấp nhất, tôi cần sử dụng _mm_cvtsi128_si64. _mm_cvtsi128_si64 và __m128i là hai loại dữ liệu khác nhau, nhưng AND vẫn hoạt động bình thường nhờ thuộc tính __m128i. Vì vậy, không phải lúc nào 1 cũng phải cùng loại với x và thậm chí x&1 không nhất thiết phải cùng loại với đầu ra của hàm_1 hoặc hàm_2. Nhưng SSE2 là trường hợp đặc biệt.
Tom avatar
lá cờ tf
Tom
@fgrieu Nếu tôi hiểu mọi thứ một cách chính xác, sẽ không có vấn đề gì khi chúng ta thay thế công thức được trình bày bằng x = function_1(x) & -(x&1) | hàm_2(x) & -(x&1^1)? Vì vậy, chúng tôi sử dụng xor 1 thay vì ~.
fgrieu avatar
lá cờ ng
@Tom: Vâng, điều đó cũng hoạt động. Tuy nhiên, hầu hết các trình biên dịch sẽ nhận thấy rằng `~ -(x&1)` có thể được tính toán hiệu quả từ `-(x&1)` hoặc miễn phí nếu toán tử `&~` có hỗ trợ phần cứng, điều này phổ biến vì nó được sử dụng trong nhiều ứng dụng phổ biến và hữu ích thành ngữ, e.g. trong `y&m | z&~m`. Nhưng tôi nghi ngờ rằng nhiều trình biên dịch sẽ sử dụng lại `-(x&1)` trong đánh giá `& -(x&1^1)`.
fgrieu avatar
lá cờ ng
@Tom: Cảm ơn bạn đã làm rõ. Tôi đã xóa bình luận của bạn vì nó đã được đọc, do đó không còn cần thiết nữa. Nâng cao nhận xét là đủ cảm ơn (và vì lý do này, nhận xét hiện tại sẽ bị hủy RSN).
Điểm:4
lá cờ us

Điều này bắt đầu như một bản chỉnh sửa cho câu trả lời của fgrieu, nhưng nó rất dài, vì vậy tôi quyết định đăng nó dưới dạng một câu trả lời riêng.

Tôi đồng ý với tất cả những gì fgrieu đã viết trong câu trả lời của anh ấy

Tôi sẽ trích dẫn câu trả lời của fgrieu ở đây để nhấn mạnh hơn:

Giải pháp đáng tin cậy tiếp theo duy nhất để thực thi thời gian liên tục bằng ngôn ngữ cấp cao, thiếu dấu hiệu về môi trường đích, là: Đảm bảo đường dẫn thực thi mã và các mẫu truy cập bộ nhớ độc lập với dữ liệu.

Nếu chúng tôi không xem xét các mẫu truy cập bộ nhớ, vì nó không thuộc phạm vi của câu hỏi. các trích dẫn có thể chỉ có về mặt lý thuyết có thể đạt được với thời gian truy cập bộ nhớ không đổi và thời gian thực hiện liên tục của mọi lệnh. Tất nhiên những điều này đi kèm với chi phí của các chu kỳ xung nhịp bổ sung và truy cập bộ nhớ bổ sung.

Các mẫu truy cập bộ nhớ đề cập đến một kiểu tấn công kênh bên khác yêu cầu Bộ nhớ truy cập ngẫu nhiên không rõ ràng (ORAM) để được bảo mật trước các cuộc tấn công kênh bên.


Nếu hàm_1 và hàm_2 là thời gian không đổi và nó mất như nhau thời gian để tính toán chúng, liệu sự phân nhánh như vậy có còn dễ bị tổn thương đối với tấn công kênh phụ?

Thông thường nhất là có, nếu/khác dễ bị tấn công theo thời gian, bởi vì khi bạn sử dụng câu lệnh if/else, bạn có thể dễ dàng mong đợi từ trình biên dịch hoặc môi trường thời gian chạy để thực thi một nhánh. Trong mọi trường hợp, bạn thường cố gắng tránh nó.

Việc chọn hàm để gọi theo chỉ mục mảng, chẳng hạn như trong câu trả lời của psyllurg trong Python không phải là cách tiếp cận tồi, nó có thời gian truy cập bộ nhớ không đổi và mã được Python thực thi để thực hiện lệnh gọi hàm là thời gian liên tục.


Trước khi tôi bắt đầu.

  • Trong trường hợp truy cập bộ nhớ, lưu ý rằng vì chúng tôi quan tâm đến các cuộc tấn công theo thời gian, nên chúng tôi chỉ quan tâm đến quyền truy cập bộ nhớ thời gian và không hoa văn điều đó có thể làm rò rỉ thông tin vì điều này sẽ làm cho tình hình của chúng ta phức tạp hơn một chút.
  • Cuối cùng, tôi chỉ xem xét bộ đệm, hiện tại chúng tôi không xem xét bộ đệm, vì mọi người đều biết bộ đệm CPU có thể là cơn ác mộng tồi tệ nhất đối với người viết mật mã trong những tình huống như vậy.
  • Tôi giả sử rằng mỗi lệnh không bao gồm quyền truy cập bộ nhớ sẽ thực hiện cùng một chu kỳ đồng hồ để thực thi.

Phân tích câu trả lời của psychurg:

Hãy lấy mã Python đơn giản này làm ví dụ

xác định f1(x):
    trả lại x + 1

xác định f2(x):
    trả lại x + 2

chắc chắn chính():
    chức năng = [f1, f2]
    x = int(input("Nhập giá trị:"))
    i = x % 2
    kết quả = chức năng [i] (x)
    in (kết quả)

nếu __name__=='__main__':
    chủ yếu()

Điều này được biên dịch thành mã byte Python sau (đầu ra của pycdas cho .pyc được biên dịch bằng Python 3.10):

        [Mã số]
            Tên tệp: test.py
            Tên đối tượng: chính
            Số đối số: 0
            Số lượng đối số chỉ Pos: 0
            KW Chỉ Arg Đếm: 0
            Người dân địa phương: 4
            Kích thước ngăn xếp: 3
            Cờ: 0x00000043 (CO_OPTIMIZED | CO_NEWLOCALS | CO_NOFREE)
            [Tên]
                'f1'
                'f2'
                'int'
                'đầu vào'
                'in'
            [Tên biến]
                'chức năng'
                'x'
                'tôi'
                'kết quả'
            [Vac miễn phí]
            [Tế bào Vars]
            [Hằng số]
                Không có
                'Nhập giá trị:'
                2
            [Tháo gỡ]
                0 LOAD_GLOBAL 0: f1
                2 LOAD_GLOBAL 1: f2
                4 BUILD_LIST 2
                6 CỬA HÀNG_FAST 0: chức năng
                8 LOAD_GLOBAL 2: int
                10 LOAD_GLOBAL 3: đầu vào
                12 LOAD_CONST 1: 'Nhập giá trị:'
                14 GỌI_CHỨC NĂNG 1
                16 GỌI_FUNCTION 1
                18 CỬA HÀNG_FAST 1: x
                20 LOAD_FAST 1: x
                22 LOAD_CONST 2: 2
                24 BINARY_MODULO           
                26 CỬA HÀNG_FAST 2: tôi
                28 LOAD_FAST 0: chức năng
                30 LOAD_FAST 2: tôi
                32 BINARY_SUBSCR           
                34 LOAD_FAST 1: x
                36 GỌI_FUNCTION 1
                38 STORE_FAST 3: kết quả
                40 LOAD_GLOBAL 4: in
                42 LOAD_FAST 3: kết quả
                44 GỌI_FUNCTION 1
                46 POP_TOP                 
                48 LOAD_CONST 0: Không
                50 RETURN_VALUE

Việc tra cứu và thực hiện của dòng kết quả = chức năng [i] (x) được thực hiện từ byte dòng 26-36. Chúng ta hãy nhìn vào BINARY_SUBSCR nhà điều hành. Nó nhận hai đối số, một danh sách (nó có thể là một lệnh hoặc bộ nhưng chúng ta đừng tập trung vào điều này) làm đối số đầu tiên và một chỉ mục từ ngăn xếp (những đối số đã được tải trước đó bằng LOAD_FAST), trả về giá trị tại chỉ mục đó và giảm ngăn xếp đi 1.

Bây giờ, chúng ta hãy xem làm thế nào BINARY_SUBSCR được triển khai trong CPython. Việc thực hiện có thể được tìm thấy đây và là như sau:

        MỤC TIÊU(BINARY_SUBSCR) {
            PREDICTED(BINARY_SUBSCR);
            PyObject *sub = POP();
            PyObject * container = TOP();
            PyObject *res = PyObject_GetItem(container, sub);
            Py_DECREF(vùng chứa);
            Py_DECREF(phụ);
            SET_TOP(độ phân giải);
            nếu (độ phân giải == NULL)
                lỗi goto;
            JUMPBY(INLINE_CACHE_ENTRIES_BINARY_SUBSCR);
            GỬI ĐI();
        }

Bây giờ toàn bộ phân tích có thể được tập trung vào PyObject *res = PyObject_GetItem(container, sub);. Đây là một phương thức chung và cho đến khi mục được truy xuất, nhiều phương thức khác được gọi trong phương thức trung gian. Tất nhiên chúng ta có thể mong đợi $O(1)$ sự phức tạp. Nó vẫn còn để kiểm tra nó. Cuối cùng PyList_GetItem được gọi là như sau:

PyObject *
PyList_GetItem(PyObject *op, Py_ssize_t i)
{
    nếu (!PyList_Check(op)) {
        PyErr_BadInternalCall();
        trả về NULL;
    }
    if (!valid_index(i, Py_SIZE(op))) {
        _Py_DECLARE_STR(list_err, "danh sách chỉ mục nằm ngoài phạm vi");
        PyErr_SetObject(PyExc_IndexError, &_Py_STR(list_err));
        trả về NULL;
    }
    return ((PyListObject *)op) -> ob_item[i];
}

Như chúng ta có thể thấy ở dòng cuối cùng. Nó có $O(1)$ sự phức tạp.Tất nhiên, do sự phức tạp của các ngôn ngữ cấp cao, chúng không bao giờ được sử dụng trong thực tế cho các ứng dụng như vậy. Vì vậy, hãy thử mã này bằng ngôn ngữ cấp thấp hơn, chẳng hạn như C, để xem nó tạo ra kết quả gì.

#include <stdio.h>

int f1(int x) {
    trả về x + 1;
}

int f2(int x) {
    trả về x + 2;
}

int main() {
    intx;
    scanf("%d", &x);
    int (*(hàm[2]))(int) = {f1, f2};
    int tôi;
    tôi = x %2;
    kết quả int = hàm [i] (x);
}

Đây là x86_64 từ Godbolt với GCC mới nhất mà không cần tối ưu hóa:

F1:
        thêm vào   $sp,$sp,-8
        sw      $fp,4($sp)
        di chuyển    $fp,$sp
        sw      $4,8($fp)
        tôi      $2,8($fp)
        ngủ gật
        thêm vào   $2,$2,1
        di chuyển    $sp,$fp
        tôi      $fp,4($sp)
        thêm vào   $sp,$sp,8
        $31
        ngủ gật

f2:
        thêm vào   $sp,$sp,-8
        sw      $fp,4($sp)
        di chuyển    $fp,$sp
        sw      $4,8($fp)
        tôi      $2,8($fp)
        ngủ gật
        thêm vào   $2,$2,2
        di chuyển    $sp,$fp
        tôi      $fp,4($sp)
        thêm vào   $sp,$sp,8
        $31
        ngủ gật

$LC0:
    .ascii "%d\000"
chủ yếu:
    thêm $sp,$sp,-56
    sw $31,52($sp)
    sw $fp,48($sp)
    di chuyển $fp,$sp
    thêm $2,$fp,32
    di chuyển $5,$2
    lui $2,% hi($LC0)
    thêm $4,$2,%lo($LC0)
        jal __isoc99_scanf
        ngủ gật

        lui     $2,%hi(f1)
    thêm $2,$2,%lo(f1)
    sw $2,36($fp)
    lui $2,%hi(f2)
        thêm vào   $2,$2,%lo(f2)
        sw      $2,40($fp)
        tôi      $3,32($fp)
        li      $2,-2147483648 # 0xffffffff80000000
    giá trị $2,$2,0x1
    và $2,$3,$2
        bgez    $2,$L6
        ngủ gật

        thêm vào   $2,$2,-1
        li      $3,-2 # 0xffffffffffffffffe
    hoặc $2,$2,$3
        thêm vào   $2,$2,1
$L6:
    sw $2,24($fp)
    tôi $2,24($fp)
    ngủ gật
    sll $2,$2,2
    thêm $3,$fp,24
    thêm $2,$3,$2
        tôi      $2,12($2)
        tôi      $3,32($fp)
        ngủ gật
        di chuyển    $4,$3
        di chuyển    $25,$2
        jalr $25
        ngủ gật

        sw      $2,28($fp)
        di chuyển    $2,$0
        di chuyển    $sp,$fp
        tôi      $31,52($sp)
        tôi      $fp,48($sp)
        thêm vào   $sp,$sp,56
        $31
        ngủ gật

Cụ thể hơn, chúng tôi quan tâm đến các hướng dẫn này trong đó lệnh gọi được thực hiện cho chức năng thích hợp:

        tôi      $2,24($fp)
        ngủ gật
        sll     $2,$2,2
        thêm vào   $3,$fp,24
        thêm bạn    $2,$3,$2
    tôi $2,12($2)
    tôi $3,32($fp)
    ngủ gật
    di chuyển $4,$3
    di chuyển $25,$2
    jalr $25
        ngủ gật

        sw      $2,28($fp)
        di chuyển    $2,$0

Như chúng ta có thể thấy không có nhánh nào được tạo, ngoại trừ từ jalr gọi hàm thích hợp.


Phân tích câu trả lời của fgrieu

Tất nhiên, dễ dàng thấy rằng đây là thời gian không đổi:

#include <stdio.h>

int f1(int x) {
    trả về x + 1;
}

int f2(int x) {
    trả về x + 2;
}

int main() {
    intx;
    scanf("%d", &x);
    int (*(hàm[2]))(int) = {f1, f2};
    int m = -(x&1); // mặt nạ biến m phải cùng kiểu với x
    x = (hàm_1(x) & m) | (hàm_2(x) & ~m);
    printf(x);
}

Một lần nữa, Goldbolt với các tùy chọn tương tự:

f1:
        đẩy rbp
        mov rbp, rsp
        mov DWORD PTR [rbp-4], edi
        mov eax, DWORD PTR [rbp-4]
        thêm eax, 1
        nhạc pop
        rút lui
f2:
        đẩy rbp
        mov rbp, rsp
        mov DWORD PTR [rbp-4], edi
        mov eax, DWORD PTR [rbp-4]
        thêm eax, 2
        nhạc pop
        rút lui
.LC0:
        .chuỗi "%d"
chủ yếu:
        đẩy rbp
        mov rbp, rsp
        đẩy rbx
        rsp phụ, 40
        lea rax, [rbp-24]
        mov rsi, rax
        mov edi, BÙM PHẲNG:.LC0
        di chuyển eax, 0
        gọi __isoc99_scanf
        mov QWORD PTR [rbp-48], BÙM PHẲNG: f1
        mov QWORD PTR [rbp-40], BÙM PHẲNG: f2
        mov eax, DWORD PTR [rbp-24]
        và eax, 1
        tiêu cực
        mov DWORD PTR [rbp-20], eax
        mov eax, DWORD PTR [rbp-24]
        di chuyển edi, eax
        di chuyển eax, 0
        chức năng gọi_1
        và eax, DWORD PTR [rbp-20]
        di chuyển ebx, eax
        mov eax, DWORD PTR [rbp-24]
        di chuyển edi, eax
        di chuyển eax, 0
        chức năng gọi_2
        mov edx, DWORD PTR [rbp-20]
        không phải edx
        và eax, edx
        hoặc eax, ebx
        mov DWORD PTR [rbp-24], eax
        mov eax, DWORD PTR [rbp-24]
        cdqe
        mov rdi, rax
        di chuyển eax, 0
        gọi printf
        di chuyển eax, 0
        mov rbx, QWORD PTR [rbp-8]
        rời khỏi
        rút lui

Chúng tôi tập trung vào phần này khi việc gán cho m được thực hiện và phân nhánh nội tuyến:

        mov eax, DWORD PTR [rbp-24]
        và eax, 1
        tiêu cực
        mov DWORD PTR [rbp-20], ea
        mov eax, DWORD PTR [rbp-24]
        di chuyển edi, eax
        di chuyển eax, 0
        chức năng gọi_1
        và eax, DWORD PTR [rbp-20]
        di chuyển ebx, eax
        mov eax, DWORD PTR [rbp-24]
        di chuyển edi, eax
        di chuyển eax, 0
        chức năng gọi_2
        mov edx, DWORD PTR [rbp-20]
        không phải edx
        và eax, edx
        hoặc eax, ebx
        mov DWORD PTR [rbp-24], eax

Chúng ta thực sự có thể thấy việc thực hiện thời gian liên tục.

Vì vậy, sự khác biệt giữa các giải pháp này là gì:

Cả hai đều đáp ứng các biện pháp phân tích chống thời gian nhưng biện pháp thứ hai cũng ẩn các mẫu truy cập. Nó luôn gọi cả hai chức năng. Vì chúng tôi chưa đề cập đến bộ đệm cho đến nay, nên với sự hiện diện của bộ đệm, bộ đệm thứ hai có vẻ an toàn hơn trước các cuộc tấn công kênh phụ theo thời gian.Đơn giản vì nó cần mã của cả hai chức năng trong bộ đệm để thực thi lệnh. Trong trường hợp thứ hai, chỉ cái đang được gọi được lưu vào bộ đệm. Nếu chúng ta giả sử rằng trong một khoảng thời gian cụ thể f1 đã được gọi và f2 bị xóa khỏi bộ đệm, sẽ có sự khác biệt về thời gian khi f2 sẽ được gọi lại.

Đối với các giải pháp khác và đọc thêm về vấn đề này, bạn có thể xem xét [1][2].

Điểm:-1
lá cờ kr

Bạn có thể đặt các hàm vào một mảng và giới thiệu chúng theo chỉ mục.

Trong Python, nó sẽ giống như sau:

chắc chắn f1( x ):
    ...

xác định f2( x ):
    ...

chức năng = [ f1, f2 ]

i = x % 2

kết quả = chức năng [ i ]( x )
Tom avatar
lá cờ tf
Tom
Cảm ơn. Điều này cũng có vẻ là thông minh. Tất nhiên, tôi chỉ đang cố gắng làm cho mã của mình tốt hơn, hiện tại điều này không dành cho mục đích sử dụng chuyên nghiệp.
kelalaka avatar
lá cờ in
Bạn có chắc chắn điều này (là | sẽ) không được chuyển đổi dưới dạng if-else trong nền không? Dựa vào trình biên dịch là quá rủi ro miễn là bạn không làm việc với [Thomas Pornin T1](https://www.youtube.com/watch?v=IHbtK5Kwt6A)
lá cờ kr
@kelalaka: Tôi chắc chắn rằng nó hoạt động như mong đợi. Đây không phải là trường hợp có thể được quyết định bởi trình biên dịch hoặc bởi môi trường thời gian chạy trong các nền tảng phổ biến như C, C++, C#, Java, Python. Những gì thường được tối ưu hóa là các biểu thức **boolean**. Ví dụ. nếu có biểu thức `a & b` và bộ thực thi biết `a == false` (hoặc `a == 0`), thì trong nhiều nền tảng, nó thậm chí không được ủy quyền cho bộ thực thi mà được yêu cầu bởi đặc tả ngôn ngữ mà thời gian chạy *phải* bỏ qua đánh giá toán hạng thứ 2.
fgrieu avatar
lá cờ ng
Đây có thể là một cải tiến. Nhưng **điều đó không giải quyết triệt để vấn đề**. Tất cả những thứ bao gồm bộ nhớ cache, căn chỉnh... kết hợp với nhau để khiến mã di động cho máy tính hiện đại không thể hoạt động liên tục trong khi có đường dẫn mã hoặc mẫu truy cập dữ liệu phụ thuộc vào dữ liệu, như trong câu hỏi. Đạt được điều đó chỉ có thể với sự kiểm soát chặt chẽ môi trường thực thi (ví dụ: trong hợp ngữ trên CPU với mô hình rõ ràng về các chu kỳ thực thi) và các ngôn ngữ cấp cao không cho phép điều đó.
lá cờ kr
@fgrieu: Bắt đầu từ một số bạn **không thể** kiểm soát việc thực thi.Ngay cả khi bạn triển khai nó trong trình biên dịch chương trình hợp ngữ, bạn không thể kiểm soát cách HĐH tổ chức quyền truy cập vào bộ nhớ, cách phân chia bộ nhớ, cách HĐH sử dụng các tối ưu hóa khác nhau. Ở cấp độ xa hơn, ngay cả hệ điều hành cũng không thể kiểm soát cách máy tính sử dụng các loại bộ đệm khác nhau. Hơn nữa, hầu hết các CPU hiện đại trong PC sử dụng **dự đoán nhánh**, để một số mã có thể được thực thi **trước** (trước khi nó thực sự cần thiết) và kết quả của nó có thể được sử dụng sau này hoặc có thể không dùng rồi mới vứt. Và không có cách nào để kiểm soát nó.
lá cờ kr
@fgrieu: ... Đó là lý do tại sao chỉ nên nói về việc giảm rò rỉ thông tin kênh bên ở một số cấp độ vĩ mô. Một số bước giảm rò rỉ ở cấp độ thấp có thể bao gồm sử dụng các HĐH chuyên dụng, sử dụng phần cứng chuyên dụng, sử dụng các công cụ tạo ra nhiều "tiếng ồn" hơn trong quá trình thực thi. Tôi coi OP cụ thể là một câu hỏi về việc giảm rò rỉ kênh bên ở cấp độ * vĩ mô *.
fgrieu avatar
lá cờ ng
@mentallurg: Tôi hoàn toàn đồng ý về "bạn không thể kiểm soát việc thực thi" và phần còn lại của nhận xét đó.Tôi coi đó là đối số giải pháp mà câu trả lời phác thảo và bất kỳ giải pháp nào dẫn đến việc thực thi một chức năng duy nhất theo tính chẵn lẻ của `x`, chỉ nên được kỳ vọng _giảm_ các biến thể thời gian tương quan với tính chẵn lẻ của `x`, khi một trong câu trả lời của tôi, khi nào và nếu có thể, _removes_ chúng trên tất cả các kiến ​​trúc mà tôi biết, bất kể hệ điều hành, CPU, dự đoán nhánh và trạng thái của vi mã đối mặt với bất kỳ cuộc tấn công mới nhất nào.

Đă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.