首页 > 代码库 > OpenSSL编程初探3 --- 根据给定的域名自动伪造应用证书
OpenSSL编程初探3 --- 根据给定的域名自动伪造应用证书
SSL中间人相关技术---根据给定的域名自动伪造证书
本文由CSDN-蚍蜉撼青松【主页:http://blog.csdn.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中间人主程序的一个独立模块,模块的整个流程如下图所示:
我封装了一个名为 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; };
------本文由CSDN-蚍蜉撼青松【主页:http://blog.csdn.net/howeverpf】原创,转载请注明出处!------