Try   HackMD

Linux 核心專題: 判定常數時間

執行人: Booker-Chen
專題解說影片

任務簡介

回顧第一次作業提到的 dudect,詳述其原理,並重現相關實驗,從而歸納出判斷給定的程式碼是否屬於常數時間的實作。應彙整其他學員的成果,例如:

TODO: 探討常數時間的重要性

為什麼需要知道程式碼的執行是否為常數時間 ?
從資訊安全、密碼學,和處理器的行為來探討。

解讀計算機編碼 中提到若一段程式碼的運行不是常數時間的話,就可能會被發起時序攻擊 (timing attack)。

取絕對值 abs

有分支 (branch) 的絕對值函式

int abs(int value)
{
    return (value < 0) ? -value : value;
}

該函式當遇到負數時會比遇到正數時多執行 -value 這道敘述,

針對 32 位元沒有分支 (branchless) 的絕對值函式

int32_t abs(int32_t x)
{
    int32_t mask = (x >> 31);
    return (x ^ mask) - mask;
}

x > 0mask = 0x00000000(x ^ mask) = xx - mask = x
x < 0mask = 0xFFFFFFFF(x ^ mask) = ~x~x - mask = ~x + 1 = -x

memcmp

Linux manual page 提到關於 memcmp 的描述

Do not use memcmp() to compare confidential data, such as cryptographic secrets, because the CPU time required for the comparison depends on the contents of the addresses compared, this function is subject to timing-based side-channel attacks. In such cases, a function that performs comparisons indeterministic time, depending only on n (the quantity of bytes compared) is required.

從這段描述中可以得知 memcmp 的運行並不是常數時間,因為當它在比較的過程中一發現兩者不同,就會直接回傳值了。

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

常數時間的 memcmp

2019q1 第 3 週測驗題 (上) 測驗一

#include <stdint.h>
#include <stddef.h>
int cst_memcmp(const void *m1, const void *m2, size_t n) {
    const uint8_t *pm1 = (const uint8_t *) m1 + n;
    const uint8_t *pm2 = (const uint8_t *) m2 + n;
    int res = 0;
    if (n) {
        do {
            int diff = *--pm1 - *--pm2;
            res = (res & (((diff - 1) & ~diff) >> 8)) | diff;
        } while (pm1 != m1);
    }
    return ((res - 1) >> 8) + (res >> 8) + 1;
}
X = `(diff - 1)` Y = `diff`

專注於程式碼本身,不要列出填空的 X, Y

收到,已更改

解析 res = (res & (((diff - 1) & ~diff) >> 8)) | diff

diff 的範圍為 255 ~ -255,即 0x000000FF ~ 0x000000000xFFFFFFFF ~ 0xFFFFFF01

diff = 0
((diff - 1) & ~diff) = 0xFFFFFFFF & 0xFFFFFFFF = 0xFFFFFFFF
0xFFFFFFFF >> 8 = 0xFFFFFFFF
res & 0xFFFFFFFF = res
res | diff = res | 0 = res
res = res

diff != 0
(((diff - 1) & ~diff) >> 8) = 0
res & 0 = 0
0 | diff = diff
res = diff

解析 ((res - 1) >> 8) + (res >> 8) + 1

res > 0
(res - 1) >> 8 = 0
res >> 8 = 0
0 + 0 + 1 = 1

res < 0
(res - 1) >> 8 = 0xFFFFFFFF
res >> 8 = 0xFFFFFFFF
0xFFFFFFFF + 0xFFFFFFFF + 1 = -1

res = 0
(res - 1) >> 8 = 0xFFFFFFFF
res >> 8 = 0
0xFFFFFFFF + 0 + 1 = 0

其他常數時間的 memcmp 實作

TODO: dudect 背後的原理

研讀論文,應彙整其他學員的成果,詳述其原理,提及相關數學和統計學議題。

為什麼選擇 dudect ?

網路上有很多其他常數時間的檢測工具,像是 ctgrindctverif,那為什麼我們要使用 dudect 呢 ? 因為前面提到的這些檢測工具需要模擬硬體設備,然而要模擬出一個完整並且正確的硬體設備並不是一件容易的事。反觀 dudect 則是透過 leakage detection tests,在目標平台上對一段程式碼做執行時間的統計分析,如此一來就巧妙的規避需要模擬硬體的問題。

leakage detection test 實作流程

此部分的程式碼解釋採用的是最接近論文發表日期 2017 年 5 月 27 日的 commit 5237cc0 這個版本。

第一步 : Measure execution time

我們要重複地測量一個函式在兩個不同 class 的 input data 下的執行時間。

  1. Classes definition: 論文中採用的是 fix-vs-random,fixed class 是用來做 corner-case processing。

  2. Cycle counter: 現今的 CPU 提供 cycle counter 讓我們可以用來精準的測量出執行時間。
    在 cpucycles.c 中有 cpucycles 這個函式,其功能為回傳 processor 的 time-stamp counter 的值。
    rdtsc 這道指令會把 time-stamp counter 存進 EDX:EAX 這兩個暫存器中,透過宣告兩個無號整數 hilo,將 EDX、EAX 的值讀取出來,再透過將 hi 左移 32 位並與 lo 做 bitwise OR 之後,回傳 time-stamp counter 的值。

int64_t cpucycles(void) 
{
    unsigned int hi, lo;

    __asm__ volatile("rdtsc\n\t" : "=a"(lo), "=d"(hi));
    return ((int64_t)lo) | (((int64_t)hi) << 32);
}

  1. Environmental conditions: 為了減少環境的干擾,我們會在開始測量前,透過隨機指派 class 產生 input data。
    在要測試的檔案中完成函式 prepare_input。一開始使用 randombytes 這個函式使 input data 全部為 random class,接下來再透過 classes[i] = randombit(); 這行程式碼將該次量測的 input data 透過隨機的方式選擇使用 fix class 或是 random class。

注意書寫規範:

  • 使用 lab0 規範的程式碼書寫風格,務必用 clang-format 確認一致

lab0-c 採用的程式碼開發風格: CONTRIBUTING.md
已修正

void prepare_inputs(uint8_t *input_data, uint8_t *classes) 
{
    randombytes(input_data, number_measurements * chunk_size);
    for (size_t i = 0; i < number_measurements; i++) {
        classes[i] = randombit();
        if (classes[i] == 0) {
            memset(input_data + (size_t)i * chunk_size, 0x00, chunk_size);
        } else {
            /* leave random */
        }
    }
}

第二步 : Apply post-processing

在開始執行 statistical analysis 之前,我們可以採用一些 post-processing 的方法。

  1. Cropping
    通常時間分佈會朝較長的執行時間份佈,而呈現正偏差 (positively skewed) 。原因來自於 OS 的中斷或是很多行程的 Logical control flows 交錯…與測量目標無關的事件。為了避免這種情況,我們可以設定閾值,當執行時間超過這個閾值,就忽略掉該次的測量。
    可以透過 percentile.c 中的 percentile 函式和 fixture.c 中的 prepare_percentile 函式設定閾值。

注意書寫規範:

  • 使用 lab0 規範的程式碼書寫風格,務必用 clang-format 確認一致

已修正

prepare_percentiles 函式會將執行時間的陣列、一個 0 ~ 1 的值 (此處為函數

10.510×(i+1)DUDECT_NUMBER_PERCENTILES)、執行時間的陣列長度傳給 percentile 函式,並依據其回傳的值設為百分位數。

static void prepare_percentiles(int64_t *ticks) 
{
    for (size_t i = 0; i < number_percentiles; i++) {
        percentiles[i] = percentile(ticks, 
            1 - (pow(0.5, 10 * (double)(i + 1) / number_percentiles)),
            number_measurements);
    }
}

percentile 函式會依照 prepare_percentiles 傳過來的值做計算後把相應位置的值回傳回去。

int64_t percentile(int64_t *a, double which, size_t size) {
    qsort(a, size, sizeof(int64_t), (int (*)(const void *, const void *))cmp);
    size_t array_position = (size_t)((double)size * (double)which);
    assert(array_position >= 0);
    assert(array_position < size);
    /* printf("percentile %f is %lld\n", which, a[array_position]); */
    return a[array_position];
}

關於閾值的選取是否合理 ?

對函數

10.510×(i+1)DUDECT_NUMBER_PERCENTILES 做分析,DUDECT_NUMBER_PERCENTILES 作者設為 100

i
10.510×(i+1)100
0 0.06696700846
1 0.1294494367
2 0.1877476036
3 0.2421417167
49 0.96875
50 0.970842719
51 0.9727952949
97 0.9988782243
98 0.9989533462
99 0.9990234375

可以看到在閾值的選取上,後 50 筆資料會集中選取後 3% 的執行時間,但是越長的執行時間就代表中間可能遇到與測量目標無關的事件。

  1. Higher-order preprocessing
    這裡不理解,只能從引用的論文 Leakage Assessment Methodology 中找到描述 :

For a second-order univariate test the mean of the mean-free squared traces

Y=(Xμ)2 is actually the variance (
CM2
) of the original traces.

對應到 fixture.c 中的 update_statistics 函式

/* 
 * do a second-order test (only if we have more than 10000 measurements).
 * Centered product pre-processing.
 */
if (t[0]->n[0] > 10000) {
    double centered = (double)difference - t[0]->mean[classes[i]];
    t_push(t[1 + number_percentiles], centered * centered, classes[i]);
}

第三步 : Apply statistical test

dudect 採用的 statistical test

我們要使用 statistical test 嘗試推翻虛無假說

H0 "the two timing distributions are equal"。

這篇論文在 III. Result -> A. Implement -> (b) Statistical test 中說明使用的 statistical test 是 Welch's t-test 搭配 Welford's online algorithm 做使用。

Welch's t-test 是 Student's t-test 的一種,適用於兩個獨立樣本具有相同或不相同的樣本數以及異質變異數的情況下。

Student's t-test 是一種虛無假說測試,當測試樣本在虛無假說下遵從 Student's t-distribution 就可以使用。

解釋 student's t-distribution

Student's t-distribution,

tv,是一個連續的機率分佈函數 (如下圖)。
image

tv 的分佈型態由參數
v
決定,
v=1
tv
就是 standard Cauchy distribution ,當
v
時,
tv
就是 standard Normal distribution

v,即為自由度 (Degrees of freedom),關於自由度的描述 :

In general, the degrees of freedom of an estimate of a parameter are equal to the number of independent scores that go into the estimate minus the number of parameters used as intermediate steps in the estimation of the parameter itself.

舉例來說,量測

N 個獨立點的變異數,獨立量測數值的個數為
N
,而 intermediate step 使用的參數 (在這裡就是平均數) 只有一個,所以自由度為
N1

Student's t-distribution 的機率分佈函數可以寫成 :

f(t)=Γ(v+12)πvΓ(v2)(1+t2v)(v+1)/2

v>1 且為偶數時,
f(t)=(v1)(v3)532v(v2)(v4)42(1+t2v)(v+1)/2

v>1 且為奇數時,
f(t)=(v1)(v3)422v(v2)(v4)53(1+t2v)(v+1)/2

這個機率分佈函數是對稱的,而且長的很像平均數為 0,變異數為 1 的常態分佈,只是頂端矮了一點,兩邊高了一點。

v 為 30 時就已經很接近常態分佈了(如下圖,紅線為 t-distribution,藍線為 normal distribution,綠線從中間依序往上為
v
= 1, 2, 3, 5, 10)。

image

程式碼實做 Welch's t-test

在 ttest.c 中有 t_pusht_compute 這兩個函式可以用做計算 Welch's t-test 的 t 值。

透過 Welford's online algorithm 可以求得樣本平均

xn 、有偏樣本變異數 (biased sample variance)
σn2
、無偏樣本變異數 (unbiased sample variance)
sn2

這裡使用 Welford's online algorithm 有兩個好處

  1. 節省記憶體空間
  2. 避免 Catastrophic cancellation

t_push 函式計算出平均

xn 以及
M2,n

計算

xn

xn = xn1 + xnxn1n

計算

M2,n

M2,n = M2,n1 + (xnxn1) × (xnxn)

注意書寫規範:

  • 使用 lab0 規範的程式碼書寫風格,務必用 clang-format 確認一致

已修正

void t_push(t_ctx *ctx, double x, uint8_t class) 
{
    assert(class == 0 || class == 1);
    ctx->n[class]++;
    /* 
     * Welford method for computing online variance
     * in a numerically stable way.
     * see Knuth Vol 2
     */
    double delta = x - ctx->mean[class];
    ctx->mean[class] = ctx->mean[class] + delta / ctx->n[class];
    ctx->m2[class] = ctx->m2[class] + delta * (x - ctx->mean[class]);
}

t_compute 函式透過 t_push 函式得出的

M2,n 計算出無偏樣本變異數
sn2
,進而求出 t 值。

sn2 = M2,nn1

t = x1x2sx12 + sx22

注意書寫規範:

  • 使用 lab0 規範的程式碼書寫風格,務必用 clang-format 確認一致

已修正

static double t_compute(ttest_ctx_t *ctx) 
{
    double var[2] = {0.0, 0.0};
    var[0] = ctx->m2[0] / (ctx->n[0] - 1);
    var[1] = ctx->m2[1] / (ctx->n[1] - 1);
    double num = (ctx->mean[0] - ctx->mean[1]);
    double den = sqrt(var[0] / ctx->n[0] + var[1] / ctx->n[1]);
    double t_value = num / den;
    return t_value;
}

選擇使用 Welch's t-test 的原因 :

dudect 判斷一段程式碼是否為常數時間的依據為比較兩個不同 class 的 timing distribution。所以我們需要知道兩個不同 class 執行時間的平均值和變異數。然而在 prepare_input 這個函式採用了 randomly chosen class,這樣會使得兩個獨立樣本具有相同或者不相同的樣本數,再加上我們無從得知母體的變異數以及兩個樣本的變異數可能相同或不同,所以選擇使用 Welch's t-test。

問題與討論

How many measurements to take?

作者在論文的 IV. DISCUSSION -> (e) 提到我們到底要做多少次的量測 ?

當一個 leakage detection test 在

N 個樣本的量測下無法推翻虛無假說,那我們不能進一步的推斷在
N+1
個樣本下也會有一樣的結果。

設定一個確切的量測數量是一個評估者的職責。當有證明足以推翻這個量測數量,比方說量測數量可以更多,那評估的結果也會跟著變動。

leakage detection test 並不會直接告訴你這段程式碼一定會或一定不會有 timing leakage,而是用統計的結果告訴你這段程式碼 “可能” 會有 timing leakage。

關於作者設定的一些數值

在閱讀論文以及重現實驗的過程中,發現作者對於閾值以及執行次數的設定上沒有到很嚴謹以及有些地方沒有很清楚。

  1. 關於 random class 和 fix class 的 timing distribution 累積分布函數圖的繪製。
  2. 論文中的 t 值設定為超過 4.5 即兩個 class 的 timing distribution 有很大的機率不一樣,但實驗中卻設為 10。
  3. 為什麼在 10000 筆量測前不能做 statistic test。
  4. 為什麼設定 t 值超過 500 (t_threshold_bananas) 就絕對不是常數時間。

關於這些提問我在 dudect 的 GitHub 發了一個 discussion

TODO: 重現實驗

選定原有程式和新增測試案例,重現常數時間判定的實驗。

這篇論文是在 2017 年 5 月 27 日發表的,所以該論文的實驗應該是使用最接近發表日期的 5237cc0 這個 commit 版本。

執行環境

避免在虛擬機器上重現實驗。

$ gcc --version
gcc (Ubuntu 13.2.0-23ubuntu4) 13.2.0

$ lscpu
Architecture:            x86_64
  CPU op-mode(s):        32-bit, 64-bit
  Address sizes:         39 bits physical, 48 bits virtual
  Byte Order:            Little Endian
CPU(s):                  16
  On-line CPU(s) list:   0-15
Vendor ID:               GenuineIntel
  Model name:            Intel(R) Core(TM) i7-10870H CPU @ 2.20GHz
    CPU family:          6
    Model:               165
    Thread(s) per core:  2
    Core(s) per socket:  8
    Socket(s):           1
    Stepping:            2
    BogoMIPS:            4416.01
Virtualization features:
  Virtualization:        VT-x
  Hypervisor vendor:     Microsoft
  Virtualization type:   full
Caches (sum of all):
  L1d:                   256 KiB (8 instances)
  L1i:                   256 KiB (8 instances)
  L2:                    2 MiB (8 instances)
  L3:                    16 MiB (1 instance)
Vulnerabilities:
  Gather data sampling:  Unknown: Dependent on hypervisor status
  Itlb multihit:         KVM: Mitigation: VMX disabled
  L1tf:                  Not affected
  Mds:                   Not affected
  Meltdown:              Not affected
  Mmio stale data:       Vulnerable: Clear CPU buffers attempted, no microcode; SMT Host state unknown
  Retbleed:              Mitigation; Enhanced IBRS
  Spec rstack overflow:  Not affected
  Spec store bypass:     Mitigation; Speculative Store Bypass disabled via prctl and seccomp
  Spectre v1:            Mitigation; usercopy/swapgs barriers and __user pointer sanitization
  Spectre v2:            Mitigation; Enhanced IBRS, IBPB conditional, RSB filling, PBRSB-eIBRS SW sequence
  Srbds:                 Unknown: Dependent on hypervisor status
  Tsx async abort:       Not affected

測試一

設定一個長度為 16-bytes 的字串 s,該字串用來當作正確密碼。
我們要檢測函式 memcmp 在比較兩個字串的操作上是否為常數時間。

注意書寫規範:

  • 使用 lab0 規範的程式碼書寫風格,務必用 clang-format 確認一致

已修正

uint8_t secret[16] = {0x2b, 0x7e, 0x15, 0x16, 0x28, 0xae, 0xd2, 0xa6,
                      0xab, 0xf7, 0x15, 0x88, 0x09, 0xcf, 0x4f, 0x3c};

uint8_t do_one_computation(uint8_t *data)
{
    return (uint8_t)memcmp(secret, data, 16);
}

fix class 設定為 s,即正確密碼 (白箱測試)。

用 fix class 和 random class 各執行 do_one_computation 10000 遍,將範圍介在 percentiles[0]percentiles[99] 之間的執行時間繪製成累積分布函數圖。

image

t 值設定為 4.5,超過 4.5 表示 fix class 和 random class 兩者的 timing distribution 有極大的可能不同。

number_measurements 設置為 1e6,這個值為每一次測試需要執行 do_one_computation 的次數。

下圖為執行 120 秒後,t 值的變化趨勢,每條線代表不同的百分位數。

image

測試二

同測試一,但 fix class 設定為 16 個位元皆為 0 的陣列 (黑箱測試)。

image

下圖為執行 120 秒後,t 值的變化趨勢,每條線代表不同的百分位數。

image

下圖為執行 600 秒後,t 值的變化趨勢,每條線代表不同的百分位數。

image

測試三

fix class 設定為 16 個位元皆為 0 的陣列,memcmp 在位元組比較的過程中,不會一發現兩者相異馬上回傳結果,而是會逐位元組比較完指定長度之後再回傳結果。

注意書寫規範:

  • 使用 lab0 規範的程式碼書寫風格,務必用 clang-format 確認一致

已修正

static int util_cmp_const(const void *a, const void *b, const size_t size) 
{
    const unsigned char *_a = (const unsigned char *)a;
    const unsigned char *_b = (const unsigned char *)b;
    unsigned char result = 0;
    size_t i;

    for (i = 0; i < size; i++) {
        result |= _a[i] ^ _b[i];
    }

    return result;  /* returns 0 if equal, nonzero otherwise */
}

image

下圖為執行 120 秒後,t 值的變化趨勢,每條線代表不同的百分位數。

image