在TOB业务中部署在服务器中的程序可能会被窃取.对此设计一套安全模块,通过设备信息, 有效期,业务信息的确认来实现业务安全, 主要使用openssl进行加密, upx进行加壳。

精简服务, 使用模块化方式设计.

  • 优点: 体量较小, 易于内嵌和扩展
  • 缺点: 暂未提供对外生成私钥的接口

基本思路

  1. RSA2048加密授权信息(依据NIAT SP800-57要求, 2011年-2030年业务至少使用RSA2048): 硬件信息(MAC/CPU), 有效期, 服务版本号, 业务信息
  2. 公钥代码写死,随版本更新, 私钥不对外发布暂时放到编译机上, 使用脚本生成授权信息.
  3. 主要流程: 生成公钥私钥->生成licence->服务启动时校验

RSA简介

  • 由于介绍RSA算法的文章实在很多,涉及到一些较复杂的数学, 而且openssl里面实现的方式与传统算法又有一些差异.于是就只用一句话介绍一下使用到的核心算法:

  • RSA是一种公私钥加密解密算法, 使用公钥a和私钥b, 能实现:

    • 原文^a mod N = 密文
    • 密文^b mod N = 原文
  • 2048指作为两个大素数乘积N的比特位数, 有一个RSA-challenge可以知道当前全世界被破解的最大比特位数

  • 由于RSA的秘钥生成过程是N->L=lcm(p-1,q-1)->E->D = E mod L - 1,

  • 加密的核心是通过公钥e和N找到私钥d的难度超出计算力.因此,谁知道私钥d,谁就能分解整数n。所以我们不能对不同的用户使用相同的n,否则这两个用户可以分别互相算出对方的私钥。

  • 工程上对于私钥的破解难度要高于公钥, 所以是用管理私钥, 公开公钥.一般接收信息加密,任何人都可以使用公钥进行加密,解密时,用户使用对应的私钥解密。本业务而言, 私钥存放理论安全的开发机(公钥写死业务代码, 所以版本更新时候复杂度并没有增加), 公钥二进制向外发布, 也就是重要的是私钥禁止发布而不是谁来加密谁来解密.

  • 值得注意的是, 使用RSA加密算法, 明文长度小于N/8, 除8的原因是bit/byte的转换

  • 在openssl.pem文件中, 公钥.pem包含公钥指数e和模数N, 私钥.pem包含版本号,模数N,公钥指数e,私钥指数d,素数p,q和中间数,所以公钥可以发布,私钥要求随源代码存放, 不进行发布

环境部署

  1. 安装openssl1.1, 注意版本可以不同, 但是由于openssl之前版本有重大安全风险,虽然这里只是用了加密部分,但是其他业务模块可能用到对应涉及风险的包,所以建议保持版本更新:

    1
    ./config shared zlib  --prefix=/usr/local/openssl && make && make install
  2. gcc ../main.cpp -I ../include/openssl/include -L../include/openssl/libs/libcrypto.a -lssl -lcrypto -ldl -lpthread链接库, 不然编译的时候会有一系列的undefine问题, 在网上没有找到CMakeLists.txt, 故将文件粘贴在这里:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    cmake_minimum_required(VERSION 3.10)
    project(rsa)

    set(CMAKE_CXX_STANDARD 11)
    set(OPENSSL_USE_STATIC_LIBS TRUE)
    set(CMAKE_CXX_FLAGS "-O4 -msse2 -msse3 -msse4 -std=c++11")

    include_directories(./include)

    add_executable(rsa main.cpp)
    target_link_libraries(rsa crypto)
  3. 可以使用@calvinshao分享的RSA C++加解密 或者这篇测试, 注意.pem文件需要自己生成一下(这里也可以进入openssl里面再生成, 不过进入后退格符号用不了很麻烦..):

    1
    2
    openssl genrsa -out priv_key.pem 2048 # 先生成私钥
    openssl rsa -in priv_key.pem -pubout -out pub_key.pem #从私钥提取公钥
  4. (*)命令行利用秘钥加密/解密文件

    1
    2
    3
    4
    # 加密, 使用公钥/私钥加密均可(由openssl.pem数据结构, 私钥文件包含公钥)
    openssl rsautl -encrypt -in file.txt -inkey pub_key.pem -pubin -out fileEncrypd.txt
    # 解密, 命令行只能使用私钥解密, 所以命令行格式在本业务不适用
    openssl rsautl -decrypt -in fileEncrypd.txt -inkey priv_key.pem -out fileDecrypd.txt

openssl RSA

RSA秘钥生成

  • 秘钥生成网上流传的RSA_generate_key版本不建议使用,调用RSA_generate_key_ex即可:

    1
    2
    3
    4
    5
    6
    7
    RSA * rsa = RSA_new();
    BIGNUM* bne = BN_new();
    if(bne == NULL){
    printf("bne null!");
    }
    BN_set_word(bne, RSA_F4); //65537, 标准会推荐素数(公钥)
    int ret = RSA_generate_key_ex(rsa, kBits, bne, NULL);
  • 在于输出函数使用, 笔者尝试多种方法, 最终使用这种可以在命令行中通过命令检测通过(另外openssl支持直接二次加密私钥):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    // 公钥输出
    BIO *pub_key = BIO_new(BIO_s_file());
    if(PEM_write_bio_RSA_PUBKEY(pub_key, rsa) != 1){
    printf("Public Key File Write Error\n");
    return;
    }
    BIO_free_all(pub_key);
    // 私钥输出
    BIO *priv_key = BIO_new_file(fout_priv_key, "w");
    if(PEM_write_bio_RSAPrivateKey(priv_key, rsa, NULL, NULL, NULL, NULL, NULL) != 1){
    printf("Private Key File Write Error\n");
    return;
    }
    // 二次加密私钥输出
    std::string password = "TTS_ANS";
    if(PEM_write_bio_RSAPrivateKey(priv_key, rsa, EVP_des_ede3_cbc(),
    (unsigned char*)password.c_str(),
    password.size(), NULL, NULL) != 1){

RSA使用

具体数据结构

发现openssl的结果体定义有个规律, 就是小写_st.
这样的好处是全局搜索的时候可以很快找到
这里列举一下最主要用到的两个结构体RSABIO

  • struct RSA

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    struct rsa_st {
    /*
    * The first parameter is used to pickup errors where this is passed
    * instead of aEVP_PKEY, it is set to 0
    */
    int pad; // padding, 保证明文c的位数不大于N
    long version; //
    const RSA_METHOD *meth;
    /* functional reference if 'meth' is ENGINE-provided */
    ENGINE *engine; // crypto/engine/eng_int.h
    //BIGNUM: bit chunks 数组实现的大数
    BIGNUM *n; // 模数
    BIGNUM *e; // public exponent 公钥
    BIGNUM *d; // private exponent 私钥
    BIGNUM *p; // 生成RSA的大素数p
    BIGNUM *q; // 生成RSA的大素数q
    BIGNUM *dmp1; // e^dmp1 = 1 mod (p-1)
    BIGNUM *dmq1; // e^dmq1 = 1 mod (q-1)
    BIGNUM *iqmp; // q^iqmq = 1 mod p
    /* be careful using this if the RSA structure is shared */
    CRYPTO_EX_DATA ex_data;
    int references;
    int flags;
    /* Used to cache montgomery values */
    BN_MONT_CTX *_method_mod_n;
    BN_MONT_CTX *_method_mod_p;
    BN_MONT_CTX *_method_mod_q;
    /*
    * all BIGNUM values are actually in the following data, if it is not
    * NULL
    */
    char *bignum_data;
    BN_BLINDING *blinding;
    BN_BLINDING *mt_blinding;
    CRYPTO_RWLOCK *lock;
    };

    typedef struct rsa_st RSA;
  • struct BIO

    openssl用于进行内定结果体与外界抽象的类, 封装文件,内存,日志,stdin/stdout,socket,加解密,摘要

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    struct bio_st {
    const BIO_METHOD *method;
    /* bio, mode, argp, argi, argl, ret */
    long (*callback) (struct bio_st *, int, const char *, int, long, long);
    char *cb_arg; /* first argument for the callback */
    int init; // 初始化标记, 初始化后为1
    int shutdown; // 关闭标记, 不为0释放资源
    int flags; /* extra storage: 控制函数行为 */
    int retry_reason; // socket/ ssl异步阻塞
    int num;
    void *ptr; // 文件句柄/内存地址
    struct bio_st *next_bio; /* used by filter BIOs */
    struct bio_st *prev_bio; /* used by filter BIOs */
    int references; // 被引用数量
    uint64_t num_read; // 已读取字节
    uint64_t num_write; // 已写入字节
    CRYPTO_EX_DATA ex_data;
    CRYPTO_RWLOCK *lock;
    };

配置加密(linux upx加壳)

项目配置文件独立程序体发布,
对于配置文件, 我们使用RSA2048加密由于明文长度需要小于(kBits/8-11)有以下两个问题

  1. 加密速度慢, 破解要求位数高, 256位AES破解强度相当于15360位RSA
  2. RSA对外发布的是公钥, 即使写死程序, 也面临潜在攻击(前文讲了公钥破解难度相当于程序破解的原因)

于是业界现有解决方案是混合加密, 也就是RSA2048加密AES秘钥, AES秘钥加密配置文件

TOB业务配置文件加密的权衡

  1. 可行性: RSA秘钥发布一定是只能发布公钥, 公钥实现过程中往往使用常用素数{3, 5, 7, 65535}. 指数和N一旦发布便可以被业务部署方得到.进一步, 被部署方得到的公钥可以解密得到AES, 从而加密配置文件可以在程序中得到.
  2. 安全性: 由于公钥的原因, 通过gdb调试可以破解得到AES秘钥.从而在本地得到配置文件明文.所以一方面需要内存进行解密操作, 另一方面需要尽量禁止程序gdb调试.
  3. 对于公钥和AES加密后的信息, 通过二进制破解可以检索到. 一方面需要进行代码明文混淆, 程序加壳处理, 另一方面可以考虑会话形式发布有有效期的AES秘钥.
  4. 最后, 需要依赖licence进行关键执行点检测宿主机是否被扩散.

AES

  1. 一般对于原文有两种加密模式

    • ECB模式:并发对多个分组进行加密
    • CBC模式:串行加密, 下一个加密块与上一个密文相关
  2. 对于AES加密解密, km上面文章很多, 这里就不复制粘贴咯~参考这篇即可,api很多,先用简单的api调通,比如cfb需要整除这些坑以后再解决就好~

AES加密

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/* data structure that contains the key itself */
AES_KEY key;

/* set the encryption key */
AES_set_encrypt_key(ckey, 128, &key);

/* set where on the 128 bit encrypted block to begin encryption*/
int num = 0;

while (1) {
bytes_read = fread(indata, 1, AES_BLOCK_SIZE, ifp);
AES_encrypt(indata, outdata, &key);
// AES_encrypt(indata, outdata, bytes_read, &key, ivec, &num,
// AES_ENCRYPT);
printf("encode------\n%s\n-----\n%s\n\n", indata, outdata);
bytes_written = fwrite(outdata, 1, bytes_read, ofp);
if (bytes_read < AES_BLOCK_SIZE)
break;
}

AES解密

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/* data structure that contains the key itself */
AES_KEY key;

/* set the encryption key */

// void AES_encrypt(const unsigned char *in, unsigned char *out, const AES_KEY *key);
AES_set_decrypt_key(ckey, 128, &key);

/* set where on the 128 bit encrypted block to begin encryption*/
int num = 0;

while (1) {
bytes_read = fread(indata, 1, AES_BLOCK_SIZE, ifp);
// void AES_decrypt(const unsigned char *in, unsigned char *out, const AES_KEY *key);
// AES_decrypt(indata, outdata, bytes_read, &key, ivec, &num,
// AES_ENCRYPT);
AES_decrypt(indata, outdata, &key);
printf("decode------\n%s\n-----\n%s\n\n", indata, outdata);
bytes_written = fwrite(outdata, 1, bytes_read, ofp);
if (bytes_read < AES_BLOCK_SIZE)
break;
}
// 是不是觉得这段代码和上面的很像..没错我真的是copy过来的....

upx加壳

  • 对于写死在代码里面的秘钥, 在编译出来的程序是长成这样的:
    没有加壳的文件可以轻易找到写死秘钥

是不是看完之后就只想说一句woc…
也就是发布出去秘钥无论如何都是不安全的!!!
如果是直接发布AES秘钥可以直接找到
如果发布被RSA2048私钥加密的AES秘钥, 公钥暴露之后也就直接找到AES了..
甚至可以直接替换公钥伪造license

  • 对于这种问题可通过联机验证来解决, 但是本题要求单机验证..所以只能发包过程中混淆+校验秘钥MD5处理.
  • 本工程使用基础的加壳软件upx进行实践

下载upx

  • 官网下载最新版就好了..平台支持多

加壳

upx加壳真的方便, upx ./exename 就可以了..

加壳出来文件可以看到是找不到原始字符串的啦!!

而且和源程序跑的结果一样呐

而且包体也变小啦!!

但是加壳容易, 解壳也分分钟哇!!!

破坏壳使得无法直接解密

这时候我们需要修改加壳程序, 只要改了一个字节, upx就不能顺畅的解密

  • 胆子小的可以修改(增删改)末尾的含有upx的一段, 和源程序肯定无关
  • windows据说copy个目录就能直接改变了一个字节
  • 胆子肥的可以在别的地方改一点东西, 这样破解难度高, 但是代码有潜在风险.

tricks

  1. 对于本文实现的加密程序, 没有必要base64存储.直接二进制存储,读取即可

项目遇到的坑

  1. include\openssl\rsa.h:13:34: fatal error: openssl/opensslconf.h: No such file or directory # include <openssl/opensslconf.h>: <openssl/opensslconf.h> 是由OpenSSL的Configure命令创建的, 源代码中只有.in文件

  2. 一开始我make之后链接编译文件夹里面的libcrypto和libssl, 发现仍然有undefined报错, 然后选择在gcc里面静态链接set(OPENSSL_USE_STATIC_LIBS TRUE)即可.

reference

Last But Not Least

  1. RSA算法好, 但是长度限制十分严格, 可以作为licence加密, 但是对于配置文件加密建议使用RSA对AES秘钥加密从而混合加密.
  2. 欢迎批评指正:P