天天看點

Linux随機數發生器Linux随機數發生器

Linux随機數發生器

日期:2017-11-29 01:42:10 星期三
  • Linux随機數發生器
    • 一、源代碼的基本情況
      • Linux核心版本
      • 涉及檔案
      • 功能概述
    • 二、外部通路接口
      • 核心層輸出接口
      • 使用者層輸出接口
      • 環境噪音輸入接口
    • 三、核心源碼分析
      • 随機數發生器理論
      • 熵池結構
      • 熵的加入
      • 随機數的生成
      • 啟動腳本
    • 四、回答主要問題
      • 核心代碼抽取
      • 初始化漏洞攻擊
    • 五、參考文獻

一、源代碼的基本情況

Linux核心版本

3.5.4

涉及檔案

  • /include/linux/random.h
  • /drivers/char/random.c

功能概述

Linux核心随機數生成器是Linux作業系統核心的重要組成部分,該生成器從裝置驅動等地方擷取環境的随機噪聲,從理論上傳回難于預測的真随機數。該生成器的應用場景涵蓋于具有較高安全性的協定棧,防止被黑客等進行爆破,如TCP序列号生成、DNS封包ID生成、TLS/SSL加密之類需要難以被預測的随機數場景。

二、外部通路接口

核心層輸出接口

void get_random_bytes(void *buf, int nbytes);
void get_random_bytes_arch(void *buf, int nbytes);
           

get_random_bytes

,是一個核心接口,僅在核心程式設計中使用,會将随機數按照nbytes大小填充到buf分區,可用于TCP序列号的生成等。該函數為非阻塞,不會因為熵池的熵值不夠而阻塞,是以會有一定的安全問題。

get_random_bytes_arch

,也是一個核心接口,使用的是硬體架構級别的随機數生成器,是以效率比上面軟體生成的随機數高出好多。但是難以保證該硬體級别的随機數生成器是否足夠安全。如果需要比較快速,而且相信該硬體随機數生成器的安全性話,是個不錯的選擇。如果硬體不支援,會調用

get_random_bytes

函數。

使用者層輸出接口

除了上面兩個核心接口,在使用者層面也有相應的接口,就是linux中常見的

/dev/urandom

/dev/random

這兩個檔案。在shell中,可以用dd和cat等指令直接擷取随機數,也可以直接寫個程式讀取這兩個檔案即可。

# 從/dev/urandom處擷取32個位元組的随機數,并放置于本地的urand.txt檔案
dd if=/dev/urandom of=./urand.txt count= bs=
# 從/dev/random處擷取32個位元組的随機數,并放置于本地的rand.txt檔案
dd if=/dev/random of=./rand.txt count= bs=
           

/dev/random

适用于對随機數安全性要求比較高的情形,但為了保證安全,該檔案的讀取偶爾會阻塞等待熵池的熵值達到某個門檻值。

/dev/urandom

沒有上面的限制,不會因為熵值不夠而導緻阻塞,可以源源不斷擷取所需的随機數。但随着越來越多的随機數被擷取,而且熵池的随機源沒有被進一步補充,新産生的随機數的安全性就會有一定的下降,有可能被人攻破,但大多數的應用場景已經夠用了。

環境噪音輸入接口

void add_device_randomness(const void *, unsigned int);
void add_input_randomness(unsigned int type, unsigned int code, \
                          unsigned int value);
void add_interrupt_randomness(int irq, int irq_flags);
void add_disk_randomness(struct gendisk *disk);
           

add_device_randomness

,是一個核心接口,可将裝置資訊或者系統啟動等資訊添加到input和nonblocking的熵池中去,不同的機器會有不同的裝置資訊,如MAC位址、機器序列号等,主要是用于初始化這些熵池。為了避免擁有相同裝置的機器帶來的安全問題,該接口不增加熵池的熵值。

add_input_randomness

,也是一個核心接口,用于添加輸入裝置的随機性,該随機性包括輸入事件本身的資訊和輸入的中斷計時。

add_interrupt_randomness

,也是一個核心接口,使用中斷計時作為随機源添加到熵池中。

add_disk_randomness

,也是一個核心接口,使用硬碟的尋道時間作為熵池的随機源,但對于高性能的固态硬碟來講,尋道時間非常短,而且相對穩定,是以此時的尋道時間不适合用來作為随機源。

三、核心源碼分析

随機數發生器理論

計算機作為一種可預測性較強的裝置,很難生成真正的随機數,然而可以使用僞随機數算法來緩解這個問題。但僞随機數有很緻命的一點,就是攻擊者可以通過各種攻擊手段猜到僞随機數發生器的序列,在一些應用場景下,使用這樣的僞随機數是十分危險的。

為了解決上面的問題,我們可以從計算機的環境中收集“環境噪音”等外部攻擊者難以擷取的資訊,然後把它們用于随機數的生成,就可以確定産生的随機數難以被預測。來自環境的随機性來源包括鍵盤、滑鼠和某些中斷的中斷計時,以及其他非确定性特别強和難以被預測的事件。把這些來源的随機性使用類似CRC機制的方法加入到“熵池”中,這種CRC機制在密碼學角度可能不是特别安全,但速度足夠快,也可以在一定程度上避免被惡意加入熵。在加入熵的過程中,還會根據實際情況計算随機數産生器的内部狀态中的熵值,即該熵值代表該系統的随機程度,側面反映該系統此刻的安全性。

在擷取随機數時,随機數是通過對熵池進行SHA哈希計算得到的。使用SHA哈希,是為了避免熵池的内部狀态直接被外部擷取,進而直接對後面的随機數進行預測。雖然目前沒有人可以對SHA進行逆向,但是難以避免未來的技術可以攻陷。此時,要保證從随機發生器傳回的資料量小于熵池内部狀态的熵值,這樣輸出資料是完全不可預知的。同時,當輸出資料時,熵池的熵值也要進行相應地降低。

熵池結構

Linux随機數發生器Linux随機數發生器

熵池的結構,如上圖所示,圖檔來源于[1],并對其進行一定的修改,使其更加符合本次閱讀的源碼結構。

linux核心随機數生成器有三個熵池組成,分别是

input_pool

,

blocking_pool

nonblocking_pool

。每個熵池都有自己的熵值計數器,用于儲存熵池的随機程度。如果有環境噪音流入,會先直接進入

input_pool

中,同時會測量該環境噪音的熵值,并更新其計數器。

blocking_pool

nonblocking_pool

會根據具體需求,向

input_pool

拉取熵,此時它們的熵值計數器也要有相應的增減。

熵池的資料結構如下:

struct entropy_store;
struct entropy_store {
    /* read-only data: */
    struct poolinfo *poolinfo;
    __u32 *pool;
    const char *name;
    struct entropy_store *pull;
    int limit;

    /* read-write data: */
    spinlock_t lock;
    unsigned add_ptr;
    unsigned input_rotate;
    int entropy_count;
    int entropy_total;
    unsigned int initialized:;
    __u8 last_data[EXTRACT_SIZE];
};
           

poolinfo

指向熵池資訊的結構體,裡面有熵池大小以及基于GF(2)進行熵池混合的參數;

pool

指向具體存放熵池内部狀态的數組;

pull

指向可以進行拉取熵的熵池,由

blocking_pool

nonblocking_pool

使用;

limit

代表熵池的熵值是否需要限制在一定的範圍内;

lock

為核心自旋鎖,用于解決多線程搶占該熵池的問題;

entropy_count

是該熵池的計數器,用于儲存熵值;剩下的都是為了用于友善操作熵池内部狀态。

input_pool

的定義如下:

static __u32 input_pool_data[INPUT_POOL_WORDS];

static struct entropy_store input_pool = {
    .poolinfo = &poolinfo_table[],
    .name = "input",
    .limit = ,
    .lock = __SPIN_LOCK_UNLOCKED(&input_pool.lock),
    .pool = input_pool_data
};
           

使用

static

關鍵詞對熵池内部狀态和熵池結構進行定義,保證其全局唯一。其他兩個熵池也是用類似的方法進行定義。

熵的加入

在熵池中加入熵的核心算法在

_mix_pool_bytes

函數中實作,使用了大量的異或、位移等運算将噪音資料打亂分散,最後存儲至熵池的數組中。

mix_pool_bytes

是對其進行自旋鎖的封裝,用于保證線程安全的。

credit_entropy_bits

用于更新熵池的熵值,裡面使用了一些原子操作,也可以保證線程安全。

static int rand_initialize(void)
{
    init_std_data(&input_pool);
    init_std_data(&blocking_pool);
    init_std_data(&nonblocking_pool);
    return ;
}
           

在初始化的時候,熵的加入依靠

rand_initialize

init_std_data

完成。他們使用時間和硬體随機數,調用

mix_pool_bytes

對三個熵池進行狀态初始化。

在系統運作過程中,使用上一章所講的四個導入接口進行熵池的更新:

void add_device_randomness(const void *, unsigned int);
void add_input_randomness(unsigned int type, unsigned int code, \
                          unsigned int value);
void add_interrupt_randomness(int irq, int irq_flags);
void add_disk_randomness(struct gendisk *disk);
           

add_device_randomese

根據裝置資訊直接更新熵池資料,但不對熵池的熵值進行更新。

add_input_randomness

add_disk_randomness

會最終調用

add_timer_randomness

完成熵池所有的更新,

add_timer_randomness

使用三階時間差的方法進行熵值的估算。

add_interrupt_randomness

使用自行實作的

fast_mix

完成熵池的更新。

随機數的生成

産生随機數的兩個核心函數是

extract_entropy

extract_entropy_user

extract_entropy

是在核心态中進行随機數的生成,并拷貝至核心态的記憶體中,是以不需要考慮與使用者态互動的細節。而

extract_entropy_user

需要把生成的随機數拷貝到使用者态的記憶體中,不僅需要使用

copy_to_user

進行記憶體拷貝,還需要在while循環中考慮程序排程和信号處理等問題,防止while循環耗時過長。

extract_entropy_user

extract_entropy

大體類似,不再展開對其讨論。

extract_entropy

的源碼大體如下,省去了一些無用部分:

static ssize_t extract_entropy(struct entropy_store *r, void *buf,
                 size_t nbytes, int min, int reserved)
{
...
    xfer_secondary_pool(r, nbytes);
    nbytes = account(r, nbytes, min, reserved);

    while (nbytes) {
        extract_buf(r, tmp);
        ...
    }
...
}
           

extract_entropy

先調用

xfer_secondary_pool

函數根據實際條件進行熵從

input_pool

blocking_pool

或者

nonblocking_pool

遷移。然後調用

account

函數根據目前熵池的特性計算目前熵池能導出的随機數的數量,比如

blocking_pool

需要保證熵值在一定範圍内,超出其範圍會縮減nbytes的大小。最後調用最重要也是最核心的

extract_buf

函數來對熵池裡的資料進行導出。

extract_buf

函數進行簡化後,代碼如下:

static void extract_buf(struct entropy_store *r, __u8 *out)
{
    ...
    sha_init(hash.w);
    spin_lock_irqsave(&r->lock, flags);
    for (i = ; i < r->poolinfo->poolwords; i += )
        sha_transform(hash.w, (__u8 *)(r->pool + i), workspace);
    __mix_pool_bytes(r, hash.w, sizeof(hash.w), extract);
    spin_unlock_irqrestore(&r->lock, flags);
...
}
           

extract_buf

函數使用SHA對熵池中部分資料進行hash,然後調用

__mix_pool_bytes

将hash值重新混入熵池中,重新打亂熵池,使得熵池更加不可預測,防止黑客多次取值後直接逆向出随機數生成器的内部狀态。

在核心态,可以直接調用

get_random_bytes

通過

extract_entropy

擷取

nonblocking_pool

産生的不太安全但速度足夠快的随機數。

在使用者态,可通過讀取

/dev/urandom

檔案,最終調用核心中的

urandom_read

函數使用

extract_entropy_user

完成

nonblocking_pool

産生的随機數的讀取;也可以通過讀取

/dev/random

檔案,最終調用核心中的

random_read

函數使用

extract_entropy_user

完成

blocking_pool

産生的較為安全随機數的讀取。

啟動腳本

在系統啟動初期,由于沒有足夠的熵源保證随機數生成的熵值,此時對其進行攻擊較為容易。一個可行的解決方法是,自行在Linux系統内添加啟動腳本和關機前執行腳本,在關機前先把系統原有熵池的資料存到某個檔案中,然後開機的時候再将這些資料恢複到熵池中,保證熵池持續具有較大的熵值。同時為了避免突然斷電或者系統崩潰導緻關機腳本未執行的情況,啟動時在恢複熵池後,又把熵池的狀态存下來,或者使用定時任務來儲存熵池狀态。

在ubuntu 14.04系統中,啟動腳本在

/etc/init.d/urandom

四、回答主要問題

核心代碼抽取

為了進一步簡化整體的代碼,在抽取代碼來實作獨立随機數模拟器的時候,做了以下修改:

1. 去除多熵池的設計,隻保留nonblocking pool。

2. 去除 熵值估計 和 熵值門檻值管理 的函數

3. 去除産生和管理random和urandom檔案的部分操作

4. 補充了一些缺失的函數,如 sha_init, sha_transform, rol32, min_t 等

5. 添加main函數,用時間作為種子mix_pool_bytes到nonblocking pool中做初始化,并列印出随機數來測試。

改造後的代碼:

#include <string.h>
#include <stdint.h>
#include <stdio.h>
#include <time.h>

#define OUTPUT_POOL_WORDS 32
#define EXTRACT_SIZE 10
#define SHA_WORKSPACE_WORDS 80

#define LONGS(x) (((x) + sizeof(unsigned long) - 1)/sizeof(unsigned long))
#define min_t(type,x,y) \
           ({type __x=(x); type __y=(y); __x<__y?__x:__y;})
#define BigLong(x) (x)

#define __u32 unsigned int
#define __u8 unsigned char
#define ssize_t int

static struct poolinfo {
    int poolwords;
    int tap1, tap2, tap3, tap4, tap5;
} poolinfo_table[] = {
    /* x^128 + x^103 + x^76 + x^51 +x^25 + x + 1 -- 105 */
    { ,  ,    , , ,  },
    /* x^32 + x^26 + x^20 + x^14 + x^7 + x + 1 -- 15 */
    { ,   , , , ,   },
};

#define POOLBITS    poolwords*32
#define POOLBYTES   poolwords*4

struct entropy_store;
struct entropy_store {
    /* read-only data: */
    struct poolinfo *poolinfo;
    __u32 *pool;
    const char *name;
    /* read-write data: */
    unsigned add_ptr;
    unsigned input_rotate;
};

static __u32 nonblocking_pool_data[OUTPUT_POOL_WORDS];

static struct entropy_store nonblocking_pool = {
    .poolinfo = &poolinfo_table[],
    .name = "nonblocking",
    .pool = nonblocking_pool_data
};

static __u32 const twist_table[] = {
    , , , ,
    , , ,  };

static inline uint32_t rol32(uint32_t word, unsigned int shift){
    return (word << shift) | (word >> ( - shift));
}

static inline uint32_t ror32(uint32_t word, unsigned int shift){
    return (word >> shift) | (word << ( - shift));
}

/* SHA Begin */
void sha_transform(uint32_t *digest, const char *in, uint32_t *W);
void sha_init(uint32_t *buf);
/* SHA End */

static void mix_pool_bytes(struct entropy_store *r, const void *in,
                int nbytes, __u8 out[]){
    unsigned long i, j, tap1, tap2, tap3, tap4, tap5;
    int input_rotate;
    int wordmask = r->poolinfo->poolwords - ;
    const char *bytes = in;
    __u32 w;

    tap1 = r->poolinfo->tap1; tap2 = r->poolinfo->tap2;
    tap3 = r->poolinfo->tap3; tap4 = r->poolinfo->tap4;
    tap5 = r->poolinfo->tap5;

    input_rotate = r->input_rotate; i = r->add_ptr;

    /* mix one byte at a time to simplify size handling and churn faster */
    while (nbytes--) {
        w = rol32(*bytes++, input_rotate & );
        i = (i - ) & wordmask;

        /* XOR in the various taps */
        w ^= r->pool[i]; w ^= r->pool[(i + tap1) & wordmask]; 
        w ^= r->pool[(i + tap2) & wordmask]; w ^= r->pool[(i + tap3) & wordmask]; 
        w ^= r->pool[(i + tap4) & wordmask]; w ^= r->pool[(i + tap5) & wordmask];

        /* Mix the result back in with a twist */
        r->pool[i] = (w >> ) ^ twist_table[w & ];
        input_rotate += i ?  : ;
    }

    r->input_rotate = input_rotate;
    r->add_ptr = i;

    if (out)
        for (j = ; j < ; j++)
            ((__u32 *)out)[j] = r->pool[(i - j) & wordmask];
}

static void extract_buf(struct entropy_store *r, __u8 *out)
{
    int i;
    union {
        __u32 w[];
        unsigned long l[LONGS(EXTRACT_SIZE)];
    } hash;
    __u32 workspace[SHA_WORKSPACE_WORDS];
    __u8 extract[];

    /* Generate a hash across the pool, 16 words (512 bits) at a time */
    sha_init(hash.w);
    for (i = ; i < r->poolinfo->poolwords; i += )
        sha_transform(hash.w, (__u8 *)(r->pool + i), workspace);

    mix_pool_bytes(r, hash.w, sizeof(hash.w), extract);

    sha_transform(hash.w, extract, workspace);
    memset(extract, , sizeof(extract));
    memset(workspace, , sizeof(workspace));

    hash.w[] ^= hash.w[]; hash.w[] ^= hash.w[];
    hash.w[] ^= rol32(hash.w[], );

    memcpy(out, &hash, EXTRACT_SIZE);
    memset(&hash, , sizeof(hash));
}

static ssize_t extract_entropy(struct entropy_store *r, void *buf,
                 size_t nbytes){
    ssize_t ret = , i;
    __u8 tmp[EXTRACT_SIZE];

    while (nbytes) {
        extract_buf(r, tmp);
        i = min_t(int, nbytes, EXTRACT_SIZE);
        memcpy(buf, tmp, i);
        nbytes -= i; buf += i; ret += i;
    }

    /* Wipe data just returned from memory */
    memset(tmp, , sizeof(tmp));
    return ret;
}

void get_random_bytes(void *buf, int nbytes){
    extract_entropy(&nonblocking_pool, buf, nbytes);
}


int main(int argc,char* argv[]){
    char buf[];
    time_t seconds = time(NULL);
    mix_pool_bytes(&nonblocking_pool, &seconds, sizeof(time_t), NULL);
    if(argc == ) seconds = (time_t) atol(argv[]);

    int i;
    get_random_bytes(buf, );
    for(i=; i<; ++i){
        printf("%d ", buf[i]);
    }
    printf("\n");
    return ;
}
           

由于篇幅有限,上面的 SHA 部分被省去,具體源碼可看附件。

編譯過程:

由于解決了所有對外的依賴,用最簡單的方式進行編譯即可。

初始化漏洞攻擊

原理:

上面提取的程式,使用了常用的time(NULL)産生的時間作為随機源,然後不再添加新的随機熵源了。

由于time(NULL)傳回是以“秒”為機關,同時該程式産出的随機數我們也得到了,是以我們隻需要知道上次啟動該程式的大緻時間,對初始化的時間設定一定的範圍進行爆破。當生成的随機數數列符合我們之前得到的随機數數列,此時進行初始化的時間種子便是之前運作該程式的初始條件,利用該初始條件我們可以預測該程式接下來産生的随機數。

實驗方案:

1. 在特定時間點,運作上述提取程式,産生32個随機數,将前20個作為攻擊程式的輸入,将12個作為攻擊程式的預測輸出。

2. 提取的程式可以自定義參數作為随機數種子,友善攻擊程式調用進行破解,預設不填使用系統時間。

3. 取一個時間範圍,運作攻擊程式進行爆破,攻擊程式會以不同時間作為種子調用提取程式進行爆破猜解,如果發現前20個符合輸出,随即進行後續預測,輸出後續的12個生成的随機數。

4. 判斷攻擊程式預測的12個随機數,和之前随機數程式生成的是否一緻,即可判斷該攻擊是否真實有效。

實驗Python代碼:

# encoding: utf-8

import os
import time
import random

def getRet(seed=None):
    if seed is None:
        argList = ['./rand']
    else:
        argList = ['./rand', str(seed)]
    wf, rf = os.popen2(argList)
    strList = rf.read().strip().split(' ')
    intList = map(lambda x: int(x), strList)
    wf.close()
    rf.close()
    return intList


if __name__ == '__main__':
    target = getRet()
    print "target List: ", target

    waitTime = random.random() * 
    print "wait for %s s" % waitTime
    time.sleep(waitTime)
    print "let's attack!"

    endTime = int(time.time())
    beginTime = endTime - 

    for seed in range(beginTime, endTime):
        ret = getRet(seed)
        OK = True
        for i in range():
            if ret[i] != target[i]:
                OK = False
                break
        if OK:
            print "found the random seed %d" % seed
            print "the predict list is ", ret[:]
            print "the target list is ", target[:]
           

實驗結果:

進行三次實驗,得到實驗截圖如下所示:

Linux随機數發生器Linux随機數發生器

可見,進行三次猜解,均成功預測原程式接下來的12個随機數數列。是以,一個随機數生成系統,如果沒有持續添加新的熵,而且初始狀态的搜尋空間比較小的情況下,很容易對整個随機數生成系統進行攻破。

五、參考文獻

  1. 曹潤聰, 曹立明. Linux 随機數生成器的原理及缺陷[J]. 計算機技術與發展, 2007, 17(10): 109-112.
  2. Gutterman Z, Pinkas B, Reinman T. Analysis of the linux random number generator[C]//Security and Privacy, 2006 IEEE Symposium on. IEEE, 2006: 15 pp.-385.
  3. Patrick Lacharme, Andrea Röck, Vincent Stubel, Marion Videau. Analysis of the Linux Random Number Generator. http://users.ics.aalto.fi/arock/slides/slides_devrand.pdf

繼續閱讀