OpenSSL编程初探三 - 根据给定的域名自动伪造应用证书

OpenSSL编程初探3 --- 根据给定的域名自动伪造应用证书

SSL中间人相关技术---根据给定的域名自动伪造证书

本文由****-蚍蜉撼青松【主页:http://blog.****.net/howeverpf】原创,转载请注明出处!


一、基于OpenSSL命令的证书手工制作流程

       在实现证书的自动生成前,必须先弄清楚使用OpenSSL命令手工制作证书的方法与步骤。以生成一个二级证书链为例,将会用到以下命令:

// 生成*CA的公钥证书和私钥文件,有效期10年(RSA 1024bits,默认)        
openssl req -new -x509 -days 3650 -keyout CARoot.key -out CARoot.crt                   
// 为*CA的私钥文件去除保护口令                                           
openssl rsa -in CARoot.key -out CARoot.key                                          
                                                                             
// 为应用证书生成私钥文件                                                    
openssl genrsa -out app.key 2048                                                
// 根据私钥文件,为应用证书生成 csr 文件(证书请求文件)                     
openssl req -new -key app.key -out app.csr                                          
// 使用CA的公私钥文件给 csr 文件签名,生成应用证书,有效期5年              
openssl ca -in app.csr -out app.crt -cert CARoot.crt -keyfile CARoot.key -days 1826 -policy policy_anything        
       其中前两句命令生成了一个自签名根证书CARoot.crt及其对应的私钥文件CARoot.key;后两句命令生成了一个名为app.crt的应用证书及其对应的私钥文件app. key,并用前面生成的CARoot.crt、CARoot.key为应用证书app.crt签名。

       对于这些命令和参数的具体含义,我已在另一篇博文《使用OpenSSL工具制作X.509证书的方法及其注意事项总结》里进行了详细阐述,此处略过不提。


二、证书自动伪造待解决的问题

        因为所有伪造的应用证书都可以使用一个自签名根证书签发,没有必要每次签发前重新生成。所以,本文所讨论的自动伪造证书,只是特指应用证书,并非是全自动从头开始伪造一个证书链,这就只会用到上一小节中第二部分提到的三条命令。

        在使用上述三步生成应用证书的时候,有几个地方会要求人机交互,因此由手工制作转为自动生成,首先要做的就是想办法避免或代替这些人机交互。下面根据证书的制作过程依次介绍:

  • (1) 在第二步,生成csr文件的时候,OpenSSL会要求输入一些关于证书持有者身份的信息【国家代码、省份、城市、公司、部门,以及通用名】,也称为DN字段。如果不想在命令运行过程中逐个输入这些DN字段的值,作为代替,可以在命令中直接使用选项-subj(这也是上节中所说的博文中有详细说明的),如下所示(以网易126为例):
openssl req -new -subj/C=CN/ST=Zhejiang/L=Hangzhou/O=NetEase\ \(Hangzhou\)\ Network\ Co.,\ Ltd/OU=MAIL\Dept./CN=*.126.com -key app.key -out app.csr       
  • (2) 在第三步,使用CA给csr文件签名的时候,OpenSSL会要求在运行过程中手工完成两次确认输入。如果想要避免,可以在命令里加上-batch选项,如下所示:

openssl ca -in app.csr -out app.crt -cert CARoot.crt-keyfile CARoot.key -days 1826 –policy policy_anything –batch

        找到了以上这些避免或代替人机交互的方法,下一步需要解决的问题是命令的各个参数如何取值,同样根据证书的制作过程依次介绍:

  • (1) 在第一步,为应用证书生成私钥文件的时候,需要指定密钥长度,这个长度值当然要和真实证书一致。OpenSSL提供了以下函数,以便从真实证书中提取这一信息:

// 获取真实证书的公钥(假设已经提前获取了指向X509结构真实证书的指针pstCert)  
EVP_PKEY *pstPubKey =X509_get_pubkey(X509 *pstCert);                             
// 获取真实证书中公钥的密钥长度                                             
int nKeyBitsLen =EVP_PKEY_bits(pstPubKey);                                        

  • (2) 在第二步,生成csr文件的时候,需要指定选项-subj的具体参数取值。这个参数说明了证书持有者的身份,所以也需要和真实证书保持一致。命令要求此选项参数必须符合:/type0=value0/type1=value1/type2=...的行形式。OpenSSL提供了以下函数从真实证书中以上述行形式提取这些身份信息:

// 获取真实证书的持有者信息(同上,假设已经提前获取了指向X509结构真实证书的指针pstCert)
X509_NAME *pstSubjInfo =X509_get_subject_name(X509 *pstCert);                    
// 将结构体形式的持有者信息输出为一行的形式:/type0=value0/type1=value1/type2=... 
char* X509_NAME_oneline(X509_NAME*pstSubjInfo, char *buf, int size);     

       但是,通过X509_NAME_oneline()函数获取的持有者信息存在空格、括号等特殊字符,还不能直接用于指定选项-subj的参数。因为该参数还对某些特殊字符有转码要求,所以我们另外实现了转码函数ConvertSubjInfo,对X509_NAME_oneline函数的输出做一些处理,其原型为:

int ConvertSubjInfo(char *pOriginalData, int nOrginalSize);                           

       函数ConvertSubjInfo的工作原理,是从X509_NAME_oneline函数输出的信息中依次提取国家代码、省份、城市、公司、部门,以及通用名这六个字段,判断字段的取值中是否有需处理的特殊字符,若有则转义,保存转义后的各字段取值,最后再将所有字段拼接成一个可作为选项-subj的参数的完整字符串。

  • (3) 在第三步,使用CA给csr文件签名的时候,有三个地方需要指定:CA的私钥文件、CA的公钥证书、应用证书的有效期。这些信息统一由本模块在初始化的时候从配置文件cert_forge.conf中获取。
  • (4) 另外,三条命令中生成的不同文件【私钥文件、csr文件、公钥证书】都需要命名,我们统一指定以目标的完整域名来为文件命名。

       前面不少地方提到,需要从真实证书提取信息。既然是自动化运行,自然也需要实现自动获取真实证书。为此,我们需要先模拟实现一个简易的SSL客户端,和真实的服务器建立SSL连接。OpenSSL提供了以下函数从SSL连接中获取证书信息:

X509 *pstRealCert = SSL_get_peer_certificate(SSL *pstSSL);  

       至此,我们已经拿到了所有所需的信息,而后就可以实用Linux提供的系统调用System(),依次执行上一节提到的制作应用证书的三个命令,从而完成证书的自动伪造。


三、证书自动伪造程序的实现

        证书自动伪造程序一般是作为SSL中间人主程序的一个独立模块,模块的整个流程如下图所示:

OpenSSL编程初探三 - 根据给定的域名自动伪造应用证书

       我封装了一个名为 CAutoFake 的类来实现这个模块,模块几个关键函数编码实现如下:

3.1 函数GetRealCert() 

       此函数的功能是与真实服务器建立SSL连接并获取真实证书。

// 从服务器获取真实的证书
//返回 成功返回 true
bool CAutoFake::GetRealCert()
{
    int nSocket;         // TCP套接字句柄
    SSL_CTX *pstCtx;     // SSL会话环境句柄
    SSL *pstSSL;         // SSL套接字句柄
    X509 *pstRealCert;   // 服务器证书的句柄
    sockaddr_in addr_server;
    int err;

    // 创建一个与 真实服务器 通信的TCP套接字
    nSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (nSocket == -1)
    {
        DbgSysErrPrint("Create socket failed! ");
        return false;
    }
    // 填充服务器地址信息
    addr_server.sin_family      = AF_INET;
    addr_server.sin_addr.s_addr = m_nDstIp;
    addr_server.sin_port        = m_nDstPort;
    // 与服务器建立TCP连接
    err = connect(nSocket, (sockaddr *)&addr_server, sizeof(addr_server));
    if (err == -1)
    {
        DbgSysErrPrint("Connect to server failed! ");
        return false;
    }
    // 创建客户端SSL会话环境
    pstCtx = SSL_CTX_new(SSLv23_client_method());
    if (pstCtx == 0)
    {
        DbgErrPrint("SSL_CTX_new failed!");
        return false;
    }
    // 创建一个与服务器通信的SSL套接字
    pstSSL = SSL_new(pstCtx);
    if (pstSSL == 0)
    {
        DbgErrPrint("SSL_new failed!");
        return false;
    }
    // 将与服务器通信的 SSL套接字&&TCP套接字 进行可读写地绑定
    SSL_set_fd(pstSSL, nSocket);
    // 与服务器建立SSL连接
    err = SSL_connect(pstSSL);
    if (err == -1)
    {
        DbgErrPrint("SSL_connect to server failed!");
        return false;
    }
    DbgMsgPrint("SSL_connect to server success!");

    // 根据SSL套接字句柄获取真实的服务器证书
    pstRealCert = SSL_get_peer_certificate(pstSSL);
    if (pstRealCert==NULL)
    {
        DbgErrPrint("Get real cert failed!");
        return false;
    }

    // 从服务器证书中取出要用的信息
    if (GetInfoFromCert(pstRealCert)!=0)
    {
        DbgErrPrint("Get info from real cert failed!");
        return false;
    }

    X509_free(pstRealCert);
    SSL_free(pstSSL);
    close(nSocket);
    SSL_CTX_free(pstCtx);

    return true;
}


3.2 函数GetInfoFromCert()

       在函数GetRealCert()的最后我们调用了自定义函数GetInfoFromCert(),它的功能是从证书中提取相关信息并作一定处理。

// 从证书中提取相关信息
//参数 pstCert【输入】,服务器的证书
//返回 成功返回 0
int CAutoFake::GetInfoFromCert(X509 *pstCert)
{
    EVP_PKEY *pstPubKey;       // 真实证书的公钥
    char *pszOriginalSubj;
    int nSize=0;

    // 获取真实证书的公钥
    pstPubKey = X509_get_pubkey(pstCert);
    // 获取真实证书中公钥的密钥长度
    m_nKeyBitsLen = EVP_PKEY_bits(pstPubKey);
    if (m_nKeyBitsLen==0)
    {
        DbgErrPrint("Get num bits from real cert failed!");
        return -1;
    }
    DbgMsgPrint("Bytes size: %d, Bits length: %d.", EVP_PKEY_size(pstPubKey), m_nKeyBitsLen);

    // 获取真实证书的持有者信息
    pszOriginalSubj = X509_NAME_oneline(X509_get_subject_name(pstCert),0,0);
    if (pszOriginalSubj==NULL)
    {
        DbgErrPrint("Get subject info from real cert failed!");
        return -1;
    }
    nSize = strlen(pszOriginalSubj);
    if (nSize<=0)
    {
        DbgErrPrint("Size of subject info is too short!");
        return -1;
    }
    DbgMsgPrint("Original subject: %s", pszOriginalSubj);

    // 按openssl ca 命令对-subj参数的要求做格式转换
    if (ConvertSubjInfo(pszOriginalSubj, nSize)!=0)
    {
        DbgErrPrint("Convert subj info of real cert failed!");
        OPENSSL_free(pszOriginalSubj);
        return -1;
    }

    OPENSSL_free(pszOriginalSubj);
    return 0;
}

3.3 函数ConvertSubjInfo()

        在函数GetInfoFromCert()中,除了几个OpenSSL的API以外,我们还调用了一个自定义函数ConvertSubjInfo(),它的功能和原型在前文第二节已经提到,此处不再赘述。如前所属,此函数的是从X509_NAME_oneline函数输出的信息中依次提取国家代码、省份、城市、公司、部门,以及通用名这六个字段,判断字段的取值中是否有需处理的特殊字符,若有则转义,保存转义后的各字段取值,最后再将所有字段拼接成一个可作为选项-subj的参数的完整字符串。

        为此,函数ConvertSubjInfo()首先需解决的问题是提取各Field的值,也就需要判定各Field在原始字符串中的起点和终点,这会用到下面这个函数:

// 获取 证书持有者信息 中某field值的长度
//参数 pData【输入】,证书持有者信息 中待查找数据
//参数 nSize【输入】,证书持有者信息 中待查找数据的长度
//返回 成功则返回该field值的长度
int CAutoFake::GetFieldLength(const char *pData, int nSize)
{
    int nOffset = 0;
    int i=0;

    m_nBackslashCount = 0;
    m_bFindNext = false;
    m_bFieldValid = true;
    for(i=0; i<nSize; i++)
    {
        if (*(pData+i) == '=')
        {
            m_bFindNext = true;
            break;
        }
        else if (*(pData+i) == '/')
        {
            m_nBackslashCount++;
            nOffset = i;
        }
        else if (*(pData+i) == '\\')
            m_bFieldValid = false;
    }

    if (m_nBackslashCount==0)
        nOffset = i;
    else if (m_nBackslashCount>1)
        m_bFieldValid = false;

    return nOffset;
}
        有了起点和终点,各Field的值就可以提取出来,接下来就需要对各Field的值按要求转码,由以下函数完成:

// 设定某field转换后的值
//参数 pDst 【输入】,该field转换后的值
//参数 pSrc 【输入】,该field转换前的值
//参数 nSize【输入】,该field转换前的值的长度
//返回 成功返回true
bool CAutoFake::SetFieldValue(char *pDst, const char *pSrc, int nSize)
{
    int i, j;

    for (i=0,j=0; i<nSize; i++)
    {
        if(*(pSrc+i)==' ' || *(pSrc+i)=='(' || *(pSrc+i)==')')
        {
            *(pDst+j++) = '\\';
        }
        *(pDst+j++) = *(pSrc+i);
    }

    *(pDst+j) = '\0';

    return true;
}
        最后,再把各Field转化后的值拼接起来,由函数CatFieldsToOneline()实现,如下:
// 将所有field拼合成一个完整的 -subj 参数
//参数 pstTransedFields 【输入】,含有所有field取值的结构
//返回 成功返回true
bool CAutoFake::CatFieldsToOneline(CERT_SUBJECT *pstTransedFields)
{
    int nOffset;

    nOffset = m_stParsePre.nCsize + pstTransedFields->nCsize
            + m_stParsePre.nSTsize + pstTransedFields->nSTsize
            + m_stParsePre.nLsize + pstTransedFields->nLsize
            + m_stParsePre.nOsize + pstTransedFields->nOsize
            + m_stParsePre.nOUsize + pstTransedFields->nOUsize
            + m_stParsePre.nCNsize + pstTransedFields->nCNsize;
    if (nOffset > sizeof(m_szTransedSubj)-1)
    {
        DbgErrPrint("Need more than %d bytes to store Transform subject info!", nOffset);
        return false;
    }

    if (!m_stFind.bFindCountry)
    {
        DbgErrPrint(" '/C=' can't be found in real cert!");
        return false;
    }
    strcpy(m_szTransedSubj, m_stParsePre.pCountry);
    strcat(m_szTransedSubj, pstTransedFields->pCountry);

    if (m_stFind.bFindState)
    {
        strcat(m_szTransedSubj, m_stParsePre.pState);
        strcat(m_szTransedSubj, pstTransedFields->pState);
    }
    if (m_stFind.bFindLocality)
    {
        strcat(m_szTransedSubj, m_stParsePre.pLocality);
        strcat(m_szTransedSubj, pstTransedFields->pLocality);
    }
    if (m_stFind.bFindOrganization)
    {
        strcat(m_szTransedSubj, m_stParsePre.pOrganization);
        strcat(m_szTransedSubj, pstTransedFields->pOrganization);
    }
    if (m_stFind.bFindOrgUnit)
    {
        strcat(m_szTransedSubj, m_stParsePre.pOrgUnit);
        strcat(m_szTransedSubj, pstTransedFields->pOrgUnit);
    }

    if (!m_stFind.bFindCommonName)
    {
        DbgErrPrint(" '/CN=' can't be found in real cert!");
        return false;
    }
    strcat(m_szTransedSubj, m_stParsePre.pCommonName);
    strcat(m_szTransedSubj, pstTransedFields->pCommonName);

    return true;
}

3.4 函数FakeCert()

         前面已经获取了所有的必要信息,最后调用函数FakeCert()完成证书的自动生成。其实现如下:

// 根据真实证书的信息生成伪造的证书和私钥文件
//返回 成功返回true
bool CAutoFake::FakeCert()
{
    char szCmd[1024]="";

    /*// 生成采用DES算法加密保护的私钥
    sprintf(szCmd, "openssl genrsa -des -out %s.key -passout pass:%s %d", m_pszDomain, m_pszPassword, m_nKeyBitsLen);
    DbgMsgPrint("Commond: %s", szCmd);
    system(szCmd);

    // 取掉私钥文件的保护口令
    sprintf(szCmd, "openssl rsa -in %s.key -out %s.key -passin pass:%s", m_pszDomain, m_pszDomain, m_pszPassword);
    DbgMsgPrint("Commond: %s", szCmd);
    system(szCmd);*/

    // 生成明文无保护的私钥
    sprintf(szCmd, "openssl genrsa -out %s.key %d", m_pszDomain, m_nKeyBitsLen);
    DbgMsgPrint("Commond: %s", szCmd);
    system(szCmd);

    // 根据私钥、持有者信息生成 证书请求文件
    sprintf(szCmd, "openssl req -new -subj %s -key %s.key -out %s.csr", m_szTransedSubj, m_pszDomain, m_pszDomain);
    DbgMsgPrint("Commond: %s", szCmd);
    system(szCmd);

    // 签名,生成证书
    sprintf(szCmd, "openssl ca -in %s.csr -out %s.crt -cert SSLCA.crt -keyfile SSLCA.key -days %d -policy policy_anything -batch", m_pszDomain, m_pszDomain, m_nValidDays);
    DbgMsgPrint("Commond: %s", szCmd);
    system(szCmd);

    return true;
}
         最早,傻不拉几的先生成了一个经加密保护的私钥,再解密。后来发现可以直接生成无加密保护的私钥,赶紧改了…………

3.5 程序中用到的一些结构以及类CAutoFake的原型

// 证书持有者信息
typedef struct CertSubject {
    char *pCountry;      //所在国家
    char *pState;        //所在州/省份
    char *pLocality;     //所在城市
    char *pOrganization; //所属组织/公司/单位
    char *pOrgUnit;      //所属部门
    char *pCommonName;   //通用名(对于应用证书,即域名)

    int nCsize;
    int nSTsize;
    int nLsize;
    int nOsize;
    int nOUsize;
    int nCNsize;
}CERT_SUBJECT;

// 证书持有者信息解析状态
typedef struct ParseState {
    bool bFindCountry;
    bool bFindState;
    bool bFindLocality;
    bool bFindOrganization;
    bool bFindOrgUnit;
    bool bFindCommonName;
}PARSE_STATE;

// 构造参数
typedef struct FakeParam{
    char *pszDomain;
    unsigned long nDstIp;
    unsigned short nDstPort;
}FAKE_PARAM;

class CAutoFake
{
public:
    CAutoFake(FAKE_PARAM *pstFakeParam);
    ~CAutoFake();
    static bool InitStatic();
    static bool LoadConf();
    static bool Release();
    bool Start();

protected:
    static bool ParseConfLine(char *ppDst, char *pLineInfo, int nPreSize, int nMaxSize);
    bool GetRealCert();
    int  GetInfoFromCert(X509 *pstCert);
    int  ConvertSubjInfo(char *pOriginalData, int nOrginalSize);
    int  GetFieldLength(const char *pData, int nSize);
    bool SetFieldValue(char *pDst, const char *pSrc, int nSize);
    bool CatFieldsToOneline(CERT_SUBJECT *pstTransedFields);
    bool FakeCert();
    bool WriteDB();

protected:
    // 静态成员
    static int m_nValidDays;      // 证书有效期(天数)
    static char *m_pszPassword;   // 密钥文件保护口令
    static CERT_SUBJECT m_stParsePre;      // 证书持有者信息各项的前缀特征
    static CERT_SUBJECT m_stDefaultValue;  // 证书持有者信息各项的默认值
    // 目标域名相关信息
    char *m_pszDomain;         // 目标域名
    unsigned long  m_nDstIp;   // 目标域名对应的真实IP【网络字节序】
    unsigned short m_nDstPort; // 目标域名开放的SSL端口【网络字节序】
    // 真实证书中提取的几个有用信息
    char m_szTransedSubj[512]; // 经过转义的证书持有者信息
    int  m_nKeyBitsLen;        // 密钥长度(比特数)
    // 解析状态
    PARSE_STATE m_stFind;      // 证书持有者信息各项是否存在
    bool m_bFindNext;
    bool m_bFieldValid;
    int m_nBackslashCount;
};



------本文由****-蚍蜉撼青松【主页:http://blog.****.net/howeverpf】原创,转载请注明出处!------