安全的伪随机数生成

安全的伪随机数生成
Photo by Privecstasy / Unsplash

实验概述和内容

在安全软件中,生成随机数是一项非常常见的任务。在许多情况下,加密密钥
不是由用户提供的,而是在软件内部生成的。它们的随机性非常大重要;否则,攻击者可以预测加密密钥,从而挫败加密的目的。许多开发人员知道如何从他们的数据库中生成随机数(例如用于蒙特卡罗模拟)所以为了安全起见,他们使用类似的方法来产生随机数。不幸的是,一个随机数序列可能有利于蒙特卡罗模拟,但他们可能是对加密密钥不利。开发人员需要知道如何生成安全的随机数,否则他们会犯错误。类似的错误也出现在一些知名产品上,包括网景和Kerberos。
学习为什么典型的随机数产生方法是不适当的用于生成秘密,如加密密钥。他们将进一步学习生成伪有利于安全的随机数。本文涵盖以下主题:

  • 伪随机数生成
  • 随机数生成中的错误
  • 生成加密密钥/dev/random和/dev/urandom设备文件

实验室环境

  • 本实验室已经在SEED Ubuntu 20.04虚拟机上进行了测试。你可以下载一个预建的
    映像,并在您自己的计算机上运行SEED虚拟机。然而,大多数种子实验可以在云上进行,您可以按照我们的说明在云上创建一个种子虚拟机

1、以错误的方式生成加密密钥

如果在Linux上运行的话,可以使用gcc命令编译time_random.c 并运行,这里我从课程中python实现换成C语言重新实现了一遍,当然随机数生成过程是一样的。

代码实现:

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#define KEYSIZE 16

void main()
{
    int i;
    char key[KEYSIZE];
    printf("%lld\n", (long long)time(NULL));
    srand(time(NULL));
    for (i = 0; i < KEYSIZE; i++)
    {
        key[i] = rand() % 256;
        printf("%.2x", (unsigned char)key[i]);
    }
    printf("\n");
}

执行运行命令,输出到time_random

gcc time_random.c -o time_random
./time_random

多次执行 ./time_random 文件几次,就会随机生成不同的结果:

image-20221031185428801
image-20221031185446871

因为他使用的是当前的时间来当作随机数的种子,所以生成不同的随机数,肯定会保证得到不同的随机,因为每一次运行的时间都不一样。

如果我们尝试注释这一行代码srand(time(NULL));,然后选择重新编译和运行他,那么现在生成的随机数就变成一样的了。

image-20221031185607182
image-20221031185624485

2、密钥预测和猜解

通过下面的命令获取时间的数量表示2022-10-20 02:08:49,离1970-1-1的时间总数。

date -d "2022-10-20 02:08:49" +%s

它返回166720231
然后,我们在其中的第12行前添加一个循环,将time_random.c在两小时内生成的所有可能的随机数列为time_guess.c

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#define KEYSIZE 16

void main()
{
    int i;
    char key[KEYSIZE];
    for (time_t t = 1524020929 - 60 * 60 * 2; t < 1524020929; t++) // within 2h window
    {
        srand(t);
        for (i = 0; i < KEYSIZE; i++)
        {
            key[i] = rand() % 256;
            printf("%.2x", (unsigned char)key[i]);
        }
        printf("\n");
    }
}

获取所有的输出到文本文件中保存:

gcc time_guess.c -o time_guess
time_guess > key_dict.txt

key_dict.txt中使用暴力破解编写一个guess_key.py程序来生成所有的密文与实际的密文依次比对出一模一样的那一个。

源代码如下:

#!/usr/bin/python3
from Crypto.Cipher import AES

data = bytearray.fromhex('255044462d312e350a25d0d4c5d80a34')
ciphertext = bytearray.fromhex('d06bf9d0dab8e8ef880660d2af65aa82')
iv = bytearray.fromhex('09080706050403020100A2B2C2D2E2F2')

with open('key_dict.txt') as f:
    keys = f.readlines()

for k in keys:
    k = k.rstrip('\n')
    key = bytearray.fromhex(k)
    cipher = AES.new(key=key, mode=AES.MODE_CBC, iv=iv)
    guess = cipher.encrypt(data)
    if guess == ciphertext:
        print("find the key:", k)
        exit(0)

print("cannot find the key!")

成功找出key:

$ chmod u+X guess_key.py
$ guess_key.py
find the key: 95fa2030e73ed3f8da761b4eb805dfd7

3与4、测量核的熵以及获取random

cat /dev/random | hexdump

这是设备中熵与随机数的生成但是这样的输出很慢,如果保持不动时几乎不会生成。使用命令可以在另一个终端中监控熵值的生成:

watch -n .1 cat /proc/sys/kernel/random/entropy_avail

当我移动鼠标或输入某些内容时,该值会快速增加。
每减少一次,就会出现一行新的随机数。
所以我们可以说,/dev/random使用了用户行为产生的可用熵来生成新的随机数。

image-20221031191357898

如果服务器与客户端使用/dev/random生成随机会话密钥。
在攻击者不断拒绝服务(DOS)攻击的过程中请求建立连接,导致服务器用完了/dev/androm的可用信息量,则随机数生成器被阻止。

5、学习使用urandom

cat /dev/urandom | hexdump

它一直在打印随机数字,所以我打算将前1MB数据量的输出截断保存为output.bin的文件:

head -c 1M /dev/urandom > output.bin

然后使用ent评估其信息密度,即熵的大小:

ent output.bin
image-20221031193324161

在大多数情况下,他已经足够随机了,满足大多数随机性要求。

使用系统文件 /dev/urandom生成一个大小为 256比特的随机数文件,并把他作为回话session密钥,使用在程序 read_random_key.c

#include <stdio.h>
#include <stdlib.h>
#define LEN 32 // 256 bits

int main()
{
    unsigned char *key = (unsigned char *)malloc(sizeof(unsigned char) * LEN);
    FILE *random = fopen("/dev/urandom", "r");
    fread(key, sizeof(unsigned char) * LEN, 1, random);
    fclose(random);
    printf("k = ");
    for (int i = 0; i < LEN; i++)
        printf("%.2x", key[i]);
    printf("\n");
    return 0;
}

编译文件:

gcc read_random_key.c -o read_random_key

执行输出: