您的位置:首页 > 编程语言

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

2014-05-31 19:02 447 查看

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;
};

By The Way

        本文只为技术分享,作者本人一向认为,技术本身是没有好坏的,关键在于人心!

        :)希望看到本文的读者也能认同我这个观点~

------本文由CSDN-蚍蜉撼青松【主页:http://blog.csdn.net/howeverpf】原创,转载请注明出处!------
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  X.509 openssl 伪造 自动