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=1 bs=32
# 从/dev/random处获取32个字节的随机数,并放置于本地的rand.txt文件
dd if=/dev/random of=./rand.txt count=1 bs=32

/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进行逆向,但是难以避免未来的技术可以攻陷。此时,要保证从随机发生器返回的数据量小于熵池内部状态的熵值,这样输出数据是完全不可预知的。同时,当输出数据时,熵池的熵值也要进行相应地降低。

熵池结构


熵池的结构,如上图所示,图片来源于[1],并对其进行一定的修改,使其更加符合本次阅读的源码结构。

linux内核随机数生成器有三个熵池组成,分别是input_pool,blocking_poolnonblocking_pool。每个熵池都有自己的熵值计数器,用于保存熵池的随机程度。如果有环境噪音流入,会先直接进入input_pool中,同时会测量该环境噪音的熵值,并更新其计数器。blocking_poolnonblocking_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:1;__u8 last_data[EXTRACT_SIZE];
};

poolinfo指向熵池信息的结构体,里面有熵池大小以及基于GF(2)进行熵池混合的参数;pool指向具体存放熵池内部状态的数组;pull指向可以进行拉取熵的熵池,由blocking_poolnonblocking_pool使用;limit代表熵池的熵值是否需要限制在一定的范围内;lock为内核自旋锁,用于解决多线程抢占该熵池的问题;entropy_count是该熵池的计数器,用于保存熵值;剩下的都是为了用于方便操作熵池内部状态。

input_pool的定义如下:

static __u32 input_pool_data[INPUT_POOL_WORDS];static struct entropy_store input_pool = {.poolinfo = &poolinfo_table[0],.name = "input",.limit = 1,.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 0;
}

在初始化的时候,熵的加入依靠rand_initializeinit_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_randomnessadd_disk_randomness会最终调用add_timer_randomness完成熵池所有的更新,add_timer_randomness使用三阶时间差的方法进行熵值的估算。
add_interrupt_randomness使用自行实现的fast_mix完成熵池的更新。

随机数的生成

产生随机数的两个核心函数是 extract_entropyextract_entropy_userextract_entropy是在内核态中进行随机数的生成,并拷贝至内核态的内存中,所以不需要考虑与用户态交互的细节。而extract_entropy_user需要把生成的随机数拷贝到用户态的内存中,不仅需要使用copy_to_user进行内存拷贝,还需要在while循环中考虑进程调度和信号处理等问题,防止while循环耗时过长。extract_entropy_userextract_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_poolblocking_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 = 0; i < r->poolinfo->poolwords; i += 16)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 intstatic 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 */{ 128,  103,    76, 51, 25, 1 },/* x^32 + x^26 + x^20 + x^14 + x^7 + x + 1 -- 15 */{ 32,   26, 20, 14, 7,  1 },
};#define POOLBITS    poolwords*32
#define POOLBYTES   poolwords*4struct 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[1],.name = "nonblocking",.pool = nonblocking_pool_data
};static __u32 const twist_table[8] = {0x00000000, 0x3b6e20c8, 0x76dc4190, 0x4db26158,0xedb88320, 0xd6d6a3e8, 0x9b64c2b0, 0xa00ae278 };static inline uint32_t rol32(uint32_t word, unsigned int shift){return (word << shift) | (word >> (32 - shift));
}static inline uint32_t ror32(uint32_t word, unsigned int shift){return (word >> shift) | (word << (32 - 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[64]){unsigned long i, j, tap1, tap2, tap3, tap4, tap5;int input_rotate;int wordmask = r->poolinfo->poolwords - 1;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 & 31);i = (i - 1) & 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 >> 3) ^ twist_table[w & 7];input_rotate += i ? 7 : 14;}r->input_rotate = input_rotate;r->add_ptr = i;if (out)for (j = 0; j < 16; j++)((__u32 *)out)[j] = r->pool[(i - j) & wordmask];
}static void extract_buf(struct entropy_store *r, __u8 *out)
{int i;union {__u32 w[5];unsigned long l[LONGS(EXTRACT_SIZE)];} hash;__u32 workspace[SHA_WORKSPACE_WORDS];__u8 extract[64];/* Generate a hash across the pool, 16 words (512 bits) at a time */sha_init(hash.w);for (i = 0; i < r->poolinfo->poolwords; i += 16)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, 0, sizeof(extract));memset(workspace, 0, sizeof(workspace));hash.w[0] ^= hash.w[3]; hash.w[1] ^= hash.w[4];hash.w[2] ^= rol32(hash.w[2], 16);memcpy(out, &hash, EXTRACT_SIZE);memset(&hash, 0, sizeof(hash));
}static ssize_t extract_entropy(struct entropy_store *r, void *buf,size_t nbytes){ssize_t ret = 0, 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, 0, 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[32];time_t seconds = time(NULL);mix_pool_bytes(&nonblocking_pool, &seconds, sizeof(time_t), NULL);if(argc == 2) seconds = (time_t) atol(argv[1]);int i;get_random_bytes(buf, 32);for(i=0; i<32; ++i){printf("%d ", buf[i]);}printf("\n");return 0;
}

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

编译过程:

gcc rand.c -o rand

由于解决了所有对外的依赖,用最简单的方式进行编译即可。

初始化漏洞攻击

原理:
上面提取的程序,使用了常用的time(NULL)产生的时间作为随机源,然后不再添加新的随机熵源了。
由于time(NULL)返回是以“秒”为单位,同时该程序产出的随机数我们也得到了,所以我们只需要知道上次启动该程序的大致时间,对初始化的时间设置一定的范围进行爆破。当生成的随机数数列符合我们之前得到的随机数数列,此时进行初始化的时间种子便是之前运行该程序的初始条件,利用该初始条件我们可以预测该程序接下来产生的随机数。

实验方案:
1. 在特定时间点,运行上述提取程序,产生32个随机数,将前20个作为攻击程序的输入,将12个作为攻击程序的预测输出。
2. 提取的程序可以自定义参数作为随机数种子,方便攻击程序调用进行破解,默认不填使用系统时间。
3. 取一个时间范围,运行攻击程序进行爆破,攻击程序会以不同时间作为种子调用提取程序进行爆破猜解,如果发现前20个符合输出,随即进行后续预测,输出后续的12个生成的随机数。
4. 判断攻击程序预测的12个随机数,和之前随机数程序生成的是否一致,即可判断该攻击是否真实有效。

实验Python代码:

# encoding: utf-8import os
import time
import randomdef 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 intListif __name__ == '__main__':target = getRet()print "target List: ", targetwaitTime = random.random() * 30print "wait for %s s" % waitTimetime.sleep(waitTime)print "let's attack!"endTime = int(time.time())beginTime = endTime - 100for seed in range(beginTime, endTime):ret = getRet(seed)OK = Truefor i in range(12):if ret[i] != target[i]:OK = Falsebreakif OK:print "found the random seed %d" % seedprint "the predict list is ", ret[12:]print "the target list is ", target[12:]

实验结果:

进行三次实验,得到实验截图如下所示:

可见,进行三次猜解,均成功预测原程序接下来的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

Linux随机数发生器相关推荐

  1. c51汇编语言随机数函数,汇编语言随机数发生器

    我最近开始学习汇编语言.对于我正在研究的项目,我必须使用线性一致性来生成随机数生成器.我想拿三个数字.上限,下限和我想要的随机数的数量.如式为获得一个随机数,我想出了....汇编语言随机数发生器 ra ...

  2. OpenCV之core 模块. 核心功能(2)基本绘图 随机数发生器绘制文字 离散傅立叶变换 输入输出XML和YAML文件 与 OpenCV 1 同时使用

    基本绘图 目的 本节你将学到: 如何用 Point 在图像中定义 2D 点 如何以及为何使用 Scalar 用OpenCV的函数 line 绘 直线 用OpenCV的函数 ellipse 绘 椭圆 用 ...

  3. OpenSSL v0.9.8a随机数发生器分析(合集)

    OpenSSL随机数发生器 本文档对OpenSSL使用的随机数进行研究分析,主要涉及OpenSSL v0.9.8a的随机数发生器以及其在Windows系统下的熵源采集情况. 目录 1. 概况... 1 ...

  4. 量子计算机真随机数,量子真随机数发生器研究取得进展

    量子真随机数发生器研究取得进展 超高速真随机数发生器简化设计方案(上)与后处理方法(下) 信息科学技术学院郭弘教授课题组与物理学院张建玮副教授课题组开展合作,在用光学随机源设计研制量子真随机数发生器的 ...

  5. OpenCV 【十六】RNG随机数发生器putText绘制文字

    1 目的 使用 随机数发生器类 (RNG) 并得到均匀分布的随机数. 通过使用函数 putText 显示文字. 第一步是实例化一个 Random Number Generator(随机数发生器对象) ...

  6. 【STM32】随机数发生器相关函数和类型

    00. 目录 文章目录 00. 目录 01. RNG概述 02. 相关类型 03. 相关函数 04. 结构体封装 05. 预留 06. 附录 07. 声明 01. RNG概述 RNG 处理器是一个以连 ...

  7. 【STM32】随机数发生器详解

    00. 目录 文章目录 00. 目录 01. 随机数发生器简介 02. 随机数发生器主要特性 03. 随机数发生器功能说明 04. 随机数发生器操作 05. 随机数发生器寄存器 5.1 RNG 控制寄 ...

  8. 随机数发生器怎么用_用随机数发生器射击自己的脚

    随机数发生器怎么用 这将不是解释随机数生成器毕竟不是那么随机的文章之一. 因此,您中的那些人希望获得有关如何破解老虎机,继续前进的指南,在这里什么也看不到. 相反,它是有关一个不太常见的锁争用问题的帖 ...

  9. 基础学习笔记之opencv(14):随机数发生器绘制文字

    本文主要介绍下opencv中自带的一个随机数发生器的类RNG,这里我用这个类来画一些图形,和基础学习笔记之opencv(13):基本绘图 一文中类似,只是 这里画出来图像的坐标,大小,颜色,角度等所有 ...

最新文章

  1. 指尖上的电商---(4).net开发solr
  2. java 重写方法 访问权限_为何Java中子类重写方法的访问权限不能低于父类中权限(内含里氏替换原则)...
  3. 上验证cudnn是否安装成功_windows和linux上的tensorflow安装(极简安装方法)
  4. gulp+browserSync自动刷新页面
  5. linux fedora下实现锐捷认证
  6. mongodb [main] Automatically disabling TLS 1.0, to force-enable TLS 1.0 specify
  7. Omni Recover for Mac版 - 一站式iPhone数据恢复
  8. Last_Errno: 1755 Last_Error: Cannot execute the current event group in the parallel mode错误解决
  9. 参会指南丨3分钟带你玩转2019数据技术嘉年华!
  10. tomcat7.0支持什么版本的jdk_JDK/Java 16 可能带来什么新特性?
  11. MySQL----数据的显示位宽
  12. 网站代码有服务器系统限制吗,服务器内存最大大小限制(示例代码)
  13. Migrations有两个文件迁移数据的方法
  14. linux gnu主要精神,最符合GNU精神的Linux发行版gnewsense
  15. Angr安装与使用之使用篇(九)
  16. 计算机一级怎么发送邮件到别人邮箱,电脑怎样发邮件到指定的邮箱地址(小白必须电脑操作邮件收发)...
  17. vxe下拉框样式被覆盖
  18. 手机计算机文件夹加密文件,如何给手机以及电脑文件加密?
  19. melogin.cn主页登录_melogin.cn登陆界面
  20. 编译内核出错:Unable to find the ncurses package. recipe for target ‘scripts/kconfig/.mconf-cfg‘ failed

热门文章

  1. Ciena为电信运营商推出开放式D-NFV解决方案
  2. php怎样转花为ppt,玩转PPT之技巧47:用PPT制作花瓣图形
  3. HTTPS下强制重定向认证
  4. 互联网广告系统综述二业务背景
  5. 大专生当老师还是php,过来人忠告:研究生毕业后去大专当老师比去本科当辅导员更有前途...
  6. 一篇文章教你撰写产品开发信
  7. 最小费用最大流 HDU1533
  8. LeetCode题解(1533):找到最大整数的索引(Python)
  9. 学习Python编程基础学习笔记(5.模块和包)
  10. 全网最全面的Expect的方法等解析!!! 建议收藏!