文章目录

  • 安全存储是什么?
  • 二、安全存储相关技术点
    • 1.加载dirf.db文件
    • 2. dirf.db文件和安全存储文件的格式
      • 2.1 安全文件的三个区域
      • 2.2 重要结构体的关系框图
      • 2.3 FEK和IV介绍
    • 3. 安全的基本操作
      • 3.1 安全文件的创建
      • 3.2 安全文件的读取
      • 3.3 安全文件的写入
    • 4. 安全存储中的加密解密
      • 4.1 数据结构的组成与作用
      • 4.2 元数据的加密
      • 4.3 数据块数据的加密
    • 总结

安全存储是什么?

OP-TEE的安全存储功能是OP-TEE为用户提供的安全存储机制。
用户可使用安全存储功能来保存敏感数据、密钥等信息。
使用OP-TEE安全存储功能保存数据时,OP-TEE会对需要被保存的数据进行加密,且每次更新安全文件时所用的加密密钥都会使用随机数重新生成,用户只要调用GP标准中定义的安全存储相关接口就能使用OP-TEE的安全存储功能对私有数据进行保护。

需要被保护的数据被OP-TEE加密后会被保存到REE侧的文件系统、EMMC的RPMB分区或数据库中,至于具体需要将加密后的数据保存到哪里则由芯片提供商决定。

安全存储功能可提供一个安全的存储环境,安全文件中数据的加解密过程都在OP-TEE中完成,且加解密密钥的生成也是在OP-TEE中进行的,这样就能保证数据的安全性。

二、安全存储相关技术点

1.加载dirf.db文件

我们以打开安全文件为例,跟踪学习一下OPTEE安全存储相关的调用流程。

@tee_ree_fs.c
提供了基本的文件操作接口

const struct tee_file_operations ree_fs_ops = {.open = ree_fs_open,.create = ree_fs_create,.close = ree_fs_close,.read = ree_fs_read,.write = ree_fs_write,.truncate = ree_fs_truncate,.rename = ree_fs_rename,.remove = ree_fs_remove,.opendir = ree_fs_opendir_rpc,.closedir = ree_fs_closedir_rpc,.readdir = ree_fs_readdir_rpc,
};

我们看一下其中的open函数ree_fs_open

static TEE_Result ree_fs_open(struct tee_pobj *po, size_t *size,struct tee_file_handle **fh)
{...res = get_dirh(&dirh);res = ree_fs_open_primitive(false, dfh.hash, &po->uuid, &dfh, fh);...
}

通过ree_fs_dirh_refcount全局变量 来控制是创建dirf.db文件,还是打开dirf.db文件

static TEE_Result get_dirh(struct tee_fs_dirfile_dirh **dirh)
{if (!ree_fs_dirh) {TEE_Result res = open_dirh(&ree_fs_dirh);if (res) {*dirh = NULL;return res;}}ree_fs_dirh_refcount++;assert(ree_fs_dirh);assert(ree_fs_dirh_refcount);*dirh = ree_fs_dirh;return TEE_SUCCESS;
}

@fs_dirfile.c

TEE_Result tee_fs_dirfile_open(bool create, uint8_t *hash,const struct tee_fs_dirfile_operations *fops,struct tee_fs_dirfile_dirh **dirh_ret)
{...dirh->fops = fops;res = fops->open(create, hash, NULL, NULL, &dirh->fh);
...
}static const struct tee_fs_dirfile_operations ree_dirf_ops = {.open = ree_fs_open_primitive,.close = ree_fs_close_primitive,.read = ree_fs_read_primitive,.write = ree_fs_write_primitive,.commit_writes = ree_dirf_commit_writes,
};

ree_fs_open_primitive函数

static TEE_Result ree_fs_open_primitive(bool create, uint8_t *hash,const TEE_UUID *uuid,struct tee_fs_dirfile_fileh *dfh,struct tee_file_handle **fh)
{struct tee_fs_fd *fdp;fdp = calloc(1, sizeof(struct tee_fs_fd));fdp->fd = -1;fdp->uuid = uuid;if (create)res = tee_fs_rpc_create_dfh(OPTEE_RPC_CMD_FS,dfh, &fdp->fd);elseres = tee_fs_rpc_open_dfh(OPTEE_RPC_CMD_FS, dfh, &fdp->fd);res = tee_fs_htree_open(create, hash, uuid, &ree_fs_storage_ops,fdp, &fdp->ht);
}

@fs_htree.c
读文件时,主要是通过如下代码实现的,其中有校验的部分,请查看如下代码注释部分。

TEE_Result tee_fs_htree_open(bool create, uint8_t *hash, const TEE_UUID *uuid,const struct tee_fs_htree_storage *stor,void *stor_aux, struct tee_fs_htree **ht_ret)
{TEE_Result res;struct tee_fs_htree *ht = calloc(1, sizeof(*ht));ht->uuid = uuid;ht->stor = stor;ht->stor_aux = stor_aux;if (create) {const struct tee_fs_htree_image dummy_head = { .counter = 0 };res = crypto_rng_read(ht->fek, sizeof(ht->fek));res = tee_fs_fek_crypt(ht->uuid, TEE_MODE_ENCRYPT, ht->fek,sizeof(ht->fek), ht->head.enc_fek);res = init_root_node(ht);ht->dirty = true;res = tee_fs_htree_sync_to_storage(&ht, hash);res = rpc_write_head(ht, 0, &dummy_head);} else {#读取dirf.db文件,调用rpc_read_node rpc_read读取dirf.db中的root node信息#其中函数调用过程:init_head_from_data -> rpc_read_node -> rpc_readres = init_head_from_data(ht, hash);#解密出root node内容并校验res = verify_root(ht);#读取dirf.db中所有node信息并创建文件的节点树res = init_tree_from_data(ht);#计算各节点内容的hash值,并与保存的hash进行比对,来校验整个hashtree是否合法res = verify_tree(ht);}
...
}

其中verify_root函数内容如下:

static TEE_Result verify_root(struct tee_fs_htree *ht)
{TEE_Result res;void *ctx;res = tee_fs_fek_crypt(ht->uuid, TEE_MODE_DECRYPT, ht->head.enc_fek,sizeof(ht->fek), ht->fek);if (res != TEE_SUCCESS)return res;res = authenc_init(&ctx, TEE_MODE_DECRYPT, ht, NULL, sizeof(ht->imeta));if (res != TEE_SUCCESS)return res;return authenc_decrypt_final(ctx, ht->head.tag, ht->head.imeta,sizeof(ht->imeta), &ht->imeta);
}

=============================================================================================

2. dirf.db文件和安全存储文件的格式

在了解了基本的流程后,我们接下来来扣一下细节,查看一下文件的格式以及结构体的定义等。

2.1 安全文件的三个区域

使用安全存储功能生成的文件都会使用相同的格式被保存,而且dirf.db文件与安全文件的格式也相同。
安全文件中的内容分为三个区域,分别用于保存文件头、结点、数据,文件的内容。
描述如下:

有三个重要的结构体:
tee_fs_htree_node_image:用于保存文件的节点node信息,通过节点可找到对应文件的头部或数据块信息;tee_fs_htree_image:用于保存安全文件的头部数据,从头部数据中可获取安全文件的加密密钥和加密头部时使用的IV值; tee_fs_fd:安全存储操作时使用的重要结构体,存放对文件操作时使用的fd、dir、TA的UUID等信息。

结构体字段的定义如下:
@core/include/tee/fs_htree.h


struct tee_fs_htree_meta {uint64_t length;
};struct tee_fs_htree_imeta {struct tee_fs_htree_meta meta;uint32_t max_node_id;
};struct tee_fs_htree_image {uint8_t iv[TEE_FS_HTREE_IV_SIZE];uint8_t tag[TEE_FS_HTREE_TAG_SIZE];uint8_t enc_fek[TEE_FS_HTREE_FEK_SIZE];uint8_t imeta[sizeof(struct tee_fs_htree_imeta)];uint32_t counter;
};struct tee_fs_htree_node_image {uint8_t hash[TEE_FS_HTREE_HASH_SIZE];uint8_t iv[TEE_FS_HTREE_IV_SIZE];uint8_t tag[TEE_FS_HTREE_TAG_SIZE];uint16_t flags;
};

2.2 重要结构体的关系框图

这些重要的结构体的关系框图如下:

其中tee_fs_htree_storage 结构体定义如下,包含了读写RPC调用的函数指针。

struct tee_fs_htree_storage {size_t block_size;TEE_Result (*rpc_read_init)(void *aux, struct tee_fs_rpc_operation *op,enum tee_fs_htree_type type, size_t idx,uint8_t vers, void **data);TEE_Result (*rpc_read_final)(struct tee_fs_rpc_operation *op,size_t *bytes);TEE_Result (*rpc_write_init)(void *aux, struct tee_fs_rpc_operation *op,enum tee_fs_htree_type type, size_t idx,uint8_t vers, void **data);TEE_Result (*rpc_write_final)(struct tee_fs_rpc_operation *op);
};

2.3 FEK和IV介绍

有几个重要的概念 要先介绍一下:

 1. SSKSecure Storage Key 安全存储密钥,在每台设备中都不一样(一般和chipID进行绑定)。2. TSKTrustedApplication Storage Key 可信应用的存储密钥,TSK是使用SSK作为密钥对TA的UUID经HMAC运算得到的。3. FEKFile Encryption Key 是安全存储功能用于对数据进行加密时使用的AES密钥。

使用场景:
FEK + IV值 ==》加密密文数据

这几个KEY的的使用关系:

ChipID + HUK    ==》 HMAC运算 ==》 SSK
SSK    + UUID   ==》 HMAC运算 ==》 TSK
TSK    + RANDOM ==》 AES_CBC ==》 Encryption_FEK

小结:总的来说OPTEE这么设计,主要是想利用两个文件来完成校验功能,一个是加密的安全文件,一个是全局的dirf.db文件。
每次想操作安全文件时,会先从dirf.db中找关键信息,同时做加解密和校验工作,来保证数据的安全性。

=============================================================================================

3. 安全的基本操作

了解了以上基本概述后,我们接下来看一下详细的安全文件的操作,看下是如何处理安全文件的数据的。
因为OPTEE中没有文件系统的功能,需要借助tee_supplicant守护进程来完成访问文件系统的工作。
读写安全文件也是类似的过程,TA发送RPC请求给tee_supplicant,tee_supplicant完成参数的解析和数据操作。

3.1 安全文件的创建

dirf.db文件的创建
在创建dirf.db文件过程中会产生一个随机数作为FEK,且在调用update_root函数时会产生另外一个随机数作为加密FEK的IV值并保存到head.iv中。每次文件的更新时,该IV值都会被新的随机数替代。

安全文件的创建
在TA中调用TEE_CreatePersistentObject接口时会创建安全文件。在创建安全文件时会初始化安全文件的数据区域。
安全文件创建完成之后,会将初始化数据加密后写入到安全文件中,然后更新整个安全文件的tee_fs_htree_node_image区域以及保存在文件头的tee_fs_htree_image区域,到此安全文件创建就已完毕。
为后续能够通过dirf.db文件找到该安全文件,则还需要更新dirf. db文件的内容,主要是更新dirf.db文件数据区域中的dirfile_entry数据。

3.2 安全文件的读取

TA对安全文件进行读写操作是通过调用TEE_ReadObjectData和TEE_WriteObjectData函数来实现的。这两个函数的执行最终会进入OP-TEE的内核空间中。
在OP-TEE内核空间调用对应的读写接口syscall_storage_obj_read和syscall_storage_obj_write函数来完成对安全文件中数据的读写操作。

看一下相关的代码:
@tee/tee_svc_storage.c
检查访问权限,调用

TEE_Result syscall_storage_obj_read(unsigned long obj, void *data, size_t len,uint64_t *count)
{...
res = vm_check_access_rights(&utc->uctx, TEE_MEMORY_ACCESS_WRITE,(uaddr_t)data, len);
res = o->pobj->fops->read(o->fh, pos_tmp, data, &bytes);
res = copy_to_user_private(count, &u_count, sizeof(*count));
}

@tee_ree_fs.c

static TEE_Result ree_fs_read(struct tee_file_handle *fh, size_t pos,void *buf, size_t *len)
{TEE_Result res;mutex_lock(&ree_fs_mutex);res = ree_fs_read_primitive(fh, pos, buf, len);mutex_unlock(&ree_fs_mutex);return res;
}static TEE_Result ree_fs_read_primitive(struct tee_file_handle *fh, size_t pos,void *buf, size_t *len)
{...struct tee_fs_htree_meta *meta = tee_fs_htree_get_meta(fdp->ht);#计算安全文件数据区域中的blockstart_block_num = pos_to_block_num(pos);end_block_num = pos_to_block_num(pos + remain_bytes - 1);block = get_tmp_block();#循环读取数据while (start_block_num <= end_block_num) {size_t offset = pos % BLOCK_SIZE;size_t size_to_read = MIN(remain_bytes, (size_t)BLOCK_SIZE);if (size_to_read + offset > BLOCK_SIZE)size_to_read = BLOCK_SIZE - offset;#使用IV和FEK解密数据res = tee_fs_htree_read_block(&fdp->ht, start_block_num, block);if (res != TEE_SUCCESS)goto exit;memcpy(data_ptr, block + offset, size_to_read);data_ptr += size_to_read;remain_bytes -= size_to_read;pos += size_to_read;start_block_num++;}res = TEE_SUCCESS;

3.3 安全文件的写入

写数据和读数据流程是类似的
@tee_fs_fs.c

static TEE_Result ree_fs_write(struct tee_file_handle *fh, size_t pos,const void *buf, size_t len)
{...res = get_dirh(&dirh);res = ree_fs_write_primitive(fh, pos, buf, len);
...
}static TEE_Result ree_fs_write_primitive(struct tee_file_handle *fh, size_t pos,const void *buf, size_t len)
{...file_size = tee_fs_htree_get_meta(fdp->ht)->length;if ((pos + len) < len)return TEE_ERROR_BAD_PARAMETERS;if (file_size < pos) {res = ree_fs_ftruncate_internal(fdp, pos);if (res != TEE_SUCCESS)return res;}return out_of_place_write(fdp, pos, buf, len);
}static TEE_Result out_of_place_write(struct tee_fs_fd *fdp, size_t pos,const void *buf, size_t len)
{size_t start_block_num = pos_to_block_num(pos);size_t end_block_num = pos_to_block_num(pos + len - 1);struct tee_fs_htree_meta *meta = tee_fs_htree_get_meta(fdp->ht);block = get_tmp_block();while (start_block_num <= end_block_num) {size_t offset = pos % BLOCK_SIZE;size_t size_to_write = MIN(remain_bytes, (size_t)BLOCK_SIZE);if (size_to_write + offset > BLOCK_SIZE)size_to_write = BLOCK_SIZE - offset;if (start_block_num * BLOCK_SIZE <ROUNDUP(meta->length, BLOCK_SIZE)) {res = tee_fs_htree_read_block(&fdp->ht,start_block_num, block);if (res != TEE_SUCCESS)goto exit;} else {memset(block, 0, BLOCK_SIZE);}...memset(block + offset, 0, size_to_write);res = tee_fs_htree_write_block(&fdp->ht, start_block_num,block);
...}if (pos > meta->length) {meta->length = pos;tee_fs_htree_meta_set_dirty(fdp->ht);}...
}

@fs_htree.c

TEE_Result tee_fs_htree_write_block(struct tee_fs_htree **ht_arg,size_t block_num, const void *block)
{...res = get_block_node(ht, true, block_num, &node);res = ht->stor->rpc_write_init(ht->stor_aux, &op,TEE_FS_HTREE_TYPE_BLOCK, block_num,block_vers, &enc_block);res = authenc_init(&ctx, TEE_MODE_ENCRYPT, ht, &node->node,ht->stor->block_size);res = authenc_encrypt_final(ctx, node->node.tag, block,ht->stor->block_size, enc_block);res = ht->stor->rpc_write_final(&op);
...
}

=============================================================================================

4. 安全存储中的加密解密

安全存储中的安全文件和dirf.db文件中的数据内容都是按照一定的格式保存的,
主要由三部分组成:tee_fs_htree_image、tee_fs_htree_node_image和数据区域块。

tee_fs_htree_image和tee_fs_htree_node_image结构体中保存的是安全文件操作时使用到的重要数据的密文数据;tee_fs_htree_image区域中的数据是对元数据经加密重要数据后生成的,而数据区域块和tee_fs_htree_node_image中的数据则是对数据块数据经加密后获得的。

4.1 数据结构的组成与作用

tee_fs_htree_image主要保存加密头部的IV值、加密安全文件的FEK使用的enc_fek以及加密之后生成的tag、imeta及标记两个tee_fs_htree_image哪个为最新的counter值。

tee_fs_htree_node_image保存节点的哈希值、加密数据块区域使用的IV值、标记使用哪个data block的ver的flag值以及加密需要被保存的数据时生成的tag数据。数据块区域保存的是需要被保存的数据的密文数据。

tee_fs_htree_image中的imeta是按照元数据的方式经加密对应的数据获得,tee_fs_htree_node_imaget中的tag跟数据块中的数据则是按照数据块加密策略经加密后获得。

4.2 元数据的加密

tee_fs_htree_image区域中的数据是按照元数据方式经加密生成的,该加密过程如图:

FEK:安全文件和dirf.db文件在执行加密操作时使用的密钥,该值在文件创建时使用随机数的方式生成。对已经创建好的文件进行操作时,该值会从tee_fs_htree_image的enc_fek成员中使用TSK解密获得;
TSK:使用SSK和UUID执行HMAC计算得到;
AES_ECB:将FEK使用TSK经AES的ECB模式加密操作后生成enc_fek;
Encrypted FEK:使用TSK加密FEK得到,保存在tee_fs_htree_image的enc_fek中,最终会被写入安全文件或者dirf.db文件头的头部中;
Meta IV:使用安全存储创建文件或将tee_fs_htree_image写入文件中都会被随机生成,最终会被写入安全文件或dirf.db文件头的头部中;
Meta Data:/data/tee目录下每个文件中存放的tee_fs_htree_node_image的个数相关的数据;
AES_GCM:将enc_fek+meta iv+meta data使用FEK和meta IV进行AES的GCM模式加密操作生成tag和Encryption Meta Data数据;
Tag:加密enc_fek+meta iv+meta data时生成的tag值,数据会被保存在tee_fs_htree_image中的tag成员中;Encryptoed Meta Data:加密enc_fek+meta iv+meta data时生成的imeta值,数据会被保存在tee_fs_htree_image中的imeta成员中。

4.3 数据块数据的加密

数据块区域和tee_fs_htree_node_image中的数据是按照数据块区域的加密策略经加密明文数据生成的,数据块区域加密策略的加密过程如图:

Encrypted FEK:使用TSK加密FEK得到,保存在tee_fs_htree_image的enc_fek中,最终会被写入安全文件或者dirf.db文件头的头部中;
TSK:使用SSK和UUID执行HMAC计算得到;AES_ECB:将Encrypted FEK使用TSK进行ECB模式的AES解密操作生成FEK;FEK:解密Encrypted FEK之后生成的FEK,用于加密需要被保存的数据块;
Block IV:每次加密数据区域中每个数据块是都会随机生成,然后被保存到tee_fs_htree_node_image变量的IV成员中;
Block Data:将需要被保存的数据更新到对应的数据块区域,然后重新加密后生成新的数据块的密文数据;
AES_GCM:将Block IV+Block data使用FEK和块IV进行GCM模式的AES加密操作生成tag和Encryption Block Data数据;
Tag:加密Block IV+Block data时生成的tag值,数据会被保存在tee_fs_htree_node_image中的tag成员中;
Encryption Block Data:加密Block IV+Block data时生成的Encryption BlockData值,数据会被保存在文件中数据区域对应的block中。

总结

安全存储功能是OP-TEE中的一个重要功能,为用户提供一个安全存取数据的方式。由于每次在对安全文件进行写入操作时都会使用随机数重新生成加密时使用的IV值,且加密时使用的密钥也在创建安全文件时使用随机数生成,并被加密保存到安全文件的头部中,所以很难非法获取到安全存储中保存的明文数据。
内部实现的数据操作比较复杂,原理和RPMB的存储有点类似,读写数据采用动态变化的参数如IV值或者Counter计数器值,防止重放或者暴力尝试破解。

参考资料
《手机安全和可信应用开发指南》

OPTEE之安全存储详解相关推荐

  1. 云原生存储详解:容器存储与 K8s 存储卷

    作者 | 阚俊宝 阿里云技术专家 导读:云原生存储详解系列文章将从云原生存储服务的概念.特点.需求.原理.使用及案例等方面,和大家一起探讨云原生存储技术新的机遇与挑战.本文为该系列文章的第二篇,会对容 ...

  2. k8s挂载目录_云原生存储详解:容器存储与 K8s 存储卷

    作者 | 阚俊宝 阿里云技术专家 导读:云原生存储详解系列文章将从云原生存储服务的概念.特点.需求.原理.使用及案例等方面,和大家一起探讨云原生存储技术新的机遇与挑战.本文为该系列文章的第二篇,会对容 ...

  3. docker修改镜像的存储位置_云原生存储详解:容器存储与 K8s 存储卷(内含赠书福利)...

    作者 | 阚俊宝  阿里巴巴技术专家 参与文末留言互动,即有机会获得赠书福利! 导读:云原生存储详解系列文章将从云原生存储服务的概念.特点.需求.原理.使用及案例等方面,和大家一起探讨云原生存储技术新 ...

  4. 云原生存储详解:容器存储与K8s存储卷

    作者 | 阚俊宝 阿里云技术专家 导读:云原生存储详解系列文章将从云原生存储服务的概念.特点.需求.原理.使用及案例等方面,和大家一起探讨云原生存储技术新的机遇与挑战.本文为该系列文章的第二篇,会对容 ...

  5. 虚拟化磁盘模式、数据存储详解

    虚拟化磁盘模式.数据存储详解 1. 配置模式 1.1. 普通 1.2. 普通延迟置零 1.3. 精简 2. 磁盘模式 2.1. 从属 2.2. 独立-持久 2.3. 独立-非持久 3. 数据存储 3. ...

  6. SAP PO 消息报文存储详解(永久保存SAP PO中间件消息之三)

    C#连接SAP HANA数据库(永久保存SAP PO中间件消息之一) SAP PO 消息监控里消息状态预览的设置(永久保存SAP PO中间件消息之二) SAP PO 消息报文存储详解(永久保存SAP ...

  7. 【数据的存储】浮点数在内存中的存储详解【超详细的保姆级别教程,让面试官心服口服】手撕浮点数存储使用方式

    [数据的存储]浮点数在内存中的存储详解[超详细的保姆级别教程,让面试官对你心服口服]手撕浮点数存储使用方式 作者: @小小Programmer 这是我的主页:@小小Programmer 在食用这篇博客 ...

  8. web存储详解(cookie、sessionStorage、localStorage、indexedDB)

    目录 一.web存储概念简介 1. 什么是web存储? 2. 为什么需要web存储? 二.web存储详解 1. cookie 2. sessionStorage和localStorage (1). 相 ...

  9. html5的web存储详解

    以前我们在本地存储数据都是用document.cookie来存储的,但是由于其的存储大小只有4K左右,解析也很复杂,给开发带来了诸多的不便.不过现在html5出了web的存储,弥补了cookie的不足 ...

最新文章

  1. hdu1074 状态压缩dp+记录方案
  2. centos下添加管理员组和添加管理员用户及相关操作
  3. 成功解决 raise RuntimeError(“The JPMML-SkLearn conversion application has failed. The Java executable
  4. 模拟网页行为之实践篇
  5. 【操作系统】进程通信-思维导图
  6. 对自定义SharePoint WebService的总结
  7. 总结MySQL建表、查询优化实用小技巧
  8. JavaScript综述
  9. oracle 数据备份视频,赵强老师:Oracle数据库(之四):备份与恢复视频课程
  10. mysql查看数据库表容量大小_详解MySQL查看数据库表容量大小的方法总结
  11. Python关于节假日的一些处理
  12. 天马行空脚踏实地,阿里巴巴有群百里挑一的天才应届生...
  13. 计算机应用二进制原因,计算机中采用二进制的主要原因是什么
  14. 计算机上e盘拒绝访问,E盘拒绝访问怎么办?Win7系统E盘拒绝访问的方法
  15. 《孤独的根号三》 中英文对照
  16. matlab qua2d,matlab 几个关于GPS/INS和GPS/AHRS的程序 - 下载 - 搜珍网
  17. raid5换硬盘显示ready_[原创]戴尔服务器raid5更换硬盘状态foreign怎么改成ready
  18. Jquery识别银行卡号码是否正确
  19. 黑群晖vmm专业版_折腾群晖笔记:利用VMM虚拟机 安装LEDE旁路由 实现软路由超强功能...
  20. 2018年8月24日英语学习

热门文章

  1. 玩转keybd_event
  2. 菜鸟也能搞定C++内存泄漏
  3. 在VC中动态加载ODBC的方法
  4. 程序员熬夜写代码,用C/C++打造一个安全的即时聊天系统
  5. java 完全匹配,Java 正则表达式匹配模式(贪婪型、勉强型、占有型)
  6. code换取微信openid_JSamp;微信_微信授权
  7. 这21个不太好搜索其含义的特殊符号你都知道吗?
  8. 益生菌拯救“社恐”?肿瘤攘外安内?胎盘似癌?这个世界怎么了。。。
  9. 1.7 编程基础之字符串 32 行程长度编码 python
  10. 第十一届蓝桥杯省赛C++组试题 第5题