SSL证书真伪检测工具实现方法

近期我们对SSL协议、X.509证书进行研究,并使用python语言与Bro工具编写了辨别SSL证书真伪性的工具,并在其基础上进行效率优化。内容并不复杂,所以我也就简单的将我们的思路写到此处,方便做类似项目的同学进行参考。

前言介绍

本课题系统地研究SSL协议、X.509证书体系、中间人攻击原理和网络协议分析工具Bro,深入地调研中间人攻击案例和伪造SSL证书案例,旨在设计一种通过日志分析来检测伪造SSL证书的方法,能够通过分析Bro产生的SSL日志与X.509日志,发现证书伪造行为。

我们的设计思路大致如下:①获取访问流量包。②使用Bro工具将流量包解析出X.509日志。③通过数据库、pycurl包进行检测。④分析检测结果。

此项目重点在于将日志中的不同访问网页的证书信息进行分析,抓取到该网页的Issuer (颁发者)、Subject(主体)、Validity (有效期)等。而网页中用来验证证书是否合法的方法如下:

  • 1、证书是否是信任的有效证书。所谓信任:浏览器内置了信任的根证书,就是看看web服务器的证书是不是这些信任根发的或者信任根的二级证书机构颁发的。所谓有效,就是看看web服务器证书是否在有效期,是否被吊销了。
  • 2、对方是不是上述证书的合法持有者。简单来说证明对方是否持有证书的对应私钥。验证方法两种,一种是对方签个名,我用证书验证签名;另外一种是用证书做个信封,看对方是否能解开。以上的所有验证,除了验证证书是否吊销需要和CA关联,其他都可以自己完成。验证正式是否吊销可以采用黑名单方式或者OCSP方式。黑名单就是定期从CA下载一个名单列表,里面有吊销的证书序列号,自己在本地比对一下就行。优点是效率高。缺点是不实时。OCSP是实时连接CA去验证,优点是实时,缺点是效率不高。

具体来说,现在存在的网站是如何验证证书是否是合法的呢?下面我引用下网友写的比较好的验证流程。

  1. 证书都是由CA组织下认可的根证书Root签发的(其中有两种形式,第一种是该组织有一个Root,每一家的Root Ca都需要其签名,该方案基于利益考量几乎没人采用,而是第二种方案,每家都有自己的Root CA 可以自签或者互相签名)。这个组织很难进,目前几乎完全由欧美控制,每年都有轮值主席负责该年CA组织工作,主要涉及到新的RFC审核和修改,新的CA申请和已有CA日志审核以及提出新的CA方案等。其他不通过该组织认证的证书签发者都是不安全的,此外该组织会对每年每个CA签发的证书进行审核。因此可以保证正常途径签发的证书根是绝对可信的。所有改组织通过的CA会强迫浏览器和系统安装(常见的厂商有VeriSign, Microsoft, Oracle和Molliza 这也是强制力的来源)
  2. 证书分为DV(Digital Verification),OV(Organization Verification)和EV(Extended Verification),其中EV证书最贵,可以在浏览器中看到绿色的就是EV证书。证书从申请到批准要走很久的流程,需要提供很多的公司认证信息和个人信息,否则是不会通过的。因此可以保证签发的证书内容是可信的。
  3. 证书是需要预装的,特别是根证书。IE和Chrome是通过内置在Windows系统中的TrustStore来管理根证书(当然自己也可以手动导入自签证书,浏览不会认可的因为有OCSP和CRL–之后细讲);而Firefox则是内置在自己的浏览中。
  4. 综上,通俗的来说一个CA如果要商业化,要做以下几步:申请加入CA组织,然后向Microsoft提申请加入TrustStore(通过Windows自我更新或者通过其他证书导入时加入)和Mozilla组织申请加入Firefox TrustStore。

而证书的工作原理是如何的呢?

以访问https://www.google.com(https)举例。

  1. 浏览器发现此为HTTPS请求,握手拿到google的证书,先从系统(Windows)或者浏览器内置(Firefox)检查证书链是否正确。【补充】简略步骤如下a. 客户端发送信息,带上支持的SSL或者TLS版本(注意不同浏览器版本支持不同)b. 服务器返回确认使用的加密通信协议版本以及加密随机数和证书c. 浏览器验证证书 -> OCSP或者CRL 结合自带truststore注意此处验证分为双向验证和单向验证,单向验证客户浏览器即可完成,即客户端truststore存放服务器public证书;双向验证客户浏览器需要带客户端证书到服务器端由服务器端验证,客户端truststore存放服务器端public证书,keystore存放自身private证书,服务器端truststore存放客户端public证书,keystore存放自身private证书。
  2. 如果验证失败则会拦截
  3. 之后浏览器会尝试查CRL(证书吊销列表)和OCSP(在线证书检查),其中OCSP是前者的替代和新技术,这是由于CRL发布时间一般是7天(不过接到新通知要改为1天了)并且很大不方便。但是考虑到老浏览器只能用CRL,并且CRL可以缓存本地对于网速差情况还是有用的,此外Firefox虽然支持OCSP但是可以手动关闭也是CRL存在的原因。注意:CA不会直接暴露到外网的,如果需要访问CA服务器需要使用硬件Token并且多人在场录像,且只能远程访问。OCSP相当于证书数据库的备份而已是直接暴露在外网的可以通过HTTP或者HTTPS访问。
  4. 如果发现证书并没有被吊销或者过期则浏览器对EV证书会显示为绿色,对OV证书则是通过放行。否则弹出通知—该网站不可信(不同浏览器不同–Edge浏览器)
    而开始时我们也准备使用上述方法进行对工具的编写,可是我们发现我们并不能直接得到证书的公钥。而我们也得知非伪造的证书的subject是正常的,也就是说当我们拿到一个伪造证书时,可以curl一下此网页进行再访问,并获得subject。进行比对后我们就很清晰的得到其真伪性了。

下面我具体的交代一下此检验工具的编写思路。

系统编写思路

如上图所示,我们检测工具可由上述部分组成:

  • ①使用wireShark或者Bro工具获取流量包。
  • ②使用python脚本对所获取流量包进行日志解析以便我们能提取出所需要的ssl证书信息。
  • ③对证书进行详细分析(辨别真伪)并记录。
  • ④分析最终情况并得出所需结论。

下图展示ssl分析器的具体架构

对于日志解析器来说,它可以提取可用数据并进行解析。我们知道一个流量包中记录了访问记录的各种数据,而并不是所有的数据都可以为我所用。所以我只需提取出与项目相关的数据即可。

由于日志解析器的内容相对较难编写,我将python代码公布于此。

''' This module handles the mechanics around easily pulling in Bro Log data
    The read_log method is a generator (in the python sense) for rows in a Bro log, 
    because of this, it's memory efficient and does not read the entire file into memory.
'''

import csv
import datetime
import optparse
import itertools
import os
import sys
import test_mysql

class BroLogReader():
    ''' This class implements a python based Bro Log Reader. '''

    def __init__(self):
        ''' Init for BroLogReader. '''
        self._delimiter = 't'

    def read_log(self, logfile, max_rows=None):
        ''' The read_log method is a generator for rows in a Bro log. 
            Usage: rows = my_bro_reader.read_log(logfile) 
                   for row in rows:
                       do something with row
            Because this method returns a generator, it's memory
            efficient and does not read the entire file in at once.
        '''

        # First parse the header of the bro log
        bro_fptr, field_names, field_types = self._parse_bro_header(logfile)

        # Note: The parse_bro_header method has advanced us to the first
        #       real data row, so we can use the normal csv reader.
        reader = csv.DictReader(bro_fptr, fieldnames=field_names,
                                delimiter=self._delimiter, restval='BRO_STOP')
        for _row in itertools.islice(reader, 0, max_rows):
            values = self._cast_dict(_row)
            if (values):
                yield values

    def _parse_bro_header(self, logfile):
        ''' This method tries to parse the Bro log header section.
            Note: My googling is failing me on the documentation on the format,
                  so just making a lot of assumptions and skipping some shit.
            Assumption 1: The delimeter is a tab.
            Assumption 2: Types are either time, string, int or float
            Assumption 3: The header is always ends with #fields and #types as
                          the last two lines.

            Format example:
                #separator x09
                #set_separator  ,
                #empty_field    (empty)
                #unset_field    -
                #path   httpheader_recon
                #fields ts  origin  useragent   header_events_json
                #types  time    string  string  string
        '''

        # Open the logfile
        _file = open(logfile, 'rb')

        # Skip until you find the #fields line
        _line = next(_file)
        while (not _line.startswith('#fields')):
            _line = next(_file)

        # Read in the field names
        _field_names = _line.strip().split(self._delimiter)[
                       1:]  # [1:] represents record except #fields,self._delimiter is defined in __init__(self)

        # Read in the types
        _line = next(_file)
        _field_types = _line.strip().split(self._delimiter)[1:]

        # Return the header info
        return _file, _field_names, _field_types

    def _cast_dict(self, data_dict):
        ''' Internal method that makes sure any dictionary elements
            are properly cast into the correct types, instead of
            just treating everything like a string from the csv file
        '''
        for key, value in data_dict.iteritems():
            if (value == 'BRO_STOP'):
                return None
            data_dict[key] = self._cast_value(value)
        return data_dict

    def _cast_value(self, value):
        ''' Internal method that makes sure any dictionary elements
            are properly cast into the correct types, instead of
            just treating everything like a string from the csv file
        '''
        # First try time
        try:
            return datetime.datetime.fromtimestamp(float(value))

        # Next try a set of primitive types
        except ValueError:
            tests = (int, float, str)
            for test in tests:
                try:
                    return test(value)
                except ValueError:
                    continue
            return value

def fileRows(fileName):  #count the whole lines
    f = open(fileName,'r')
    i=0
    for ii in f.readlines():
        i=i+1
    return i

def get_allfilename(input_dir):
    for (path,dirname,filenames) in os.walk(input_dir):
        return filenames


if __name__ == '__main__':

    # Handle command-line arguments
    PARSER = optparse.OptionParser()
    PARSER.add_option('--logfiledir', default=None, help='Logfile to read from.  Default: %default')
    OPTIONS, ARGUMENTS = PARSER.parse_args()
    # print OPTIONS, ARGUMENTS

    for filename in get_allfilename(OPTIONS.logfiledir):
        filedir=OPTIONS.logfiledir+"/"+filename

    # Create a BRO log file reader and pull from the logfile
        BRO_LOG = BroLogReader()

        RECORDS = BRO_LOG.read_log(filedir, max_rows=fileRows(filedir))
        for row in RECORDS:
            test_mysql.mysql_con(row)

而证书分析器是我们工具的重点。

证书信息分析器是整个检测方法的核心部分,其主要功能是根据日志记录中证书相关的字段信息来分析并判断该记录中的证书是伪造证书,还是可疑证书或非伪造证书。
证书信息分析器的整体运行流程如下图所示。首先,证书信息分析器将提取经过日志解析器解析后的日志记录中的字段certificate.subject、certificate.issuer、certificate.not_valid_before、certificate.not_valid_after的值,分别代表了证书的主体、颁发者、有效期(包含起始时间和结束时间两个属性)。
如果某个证书的签发机构是其本身,那么称该证书为自签名证书。一般的自签名证书都不是由受信任的根证书颁发机构颁发,系统无法验证其安全性,因此一般来说自签名证书都属于伪造证书,且多用于SSL中间人攻击中。因此,如果certificate.subject值等于certificate.issuer值,即颁发机构就是主体本身,说明该证书属于自签名证书,证书信息分析器判定其是伪造证书,若证书信息数据集中已有该证书记录,则更新出现次数字段的值,若不存在该证书记录,就将证书字段相关信息与检测结果存入证书信息数据集中。
而如果certificate.subject值不等于certificate.issuer值,即颁发者不是主体,说明该证书不是自签名证书,且存在着一条证书链。由于本作品的流量采集环境Bro只具备记录证书的基本信息的功能,没有验证证书的能力,而且日志记录的数量庞大,若对每个证书都再进行一次证书链的验证过程,将耗费大量时间与内存。因此在这里采用这样的方法:对当前日志记录中的证书,查询证书信息数据集中同一证书不同记录(同一证书代表它们拥有相同的subject值)的证书信息,若不存在该证书的日志记录,那么判定该证书为非伪造证书,并将证书字段相关信息与检测结果存入证书信息数据集中。若存在该证书的日志记录,且与证书信息数据集中证书信息完全匹配,证书信息分析器判定该证书为非伪造证书,并更新出现次数字段的值。
若存在一条记录的证书信息与其他记录的证书信息不相同,判定该证书为可疑证书,并将证书字段相关信息与检测结果存入证书信息数据集中,并修改其他记录的检测结果。对于这些可疑证书的日志记录,系统将进一步通过可疑证书检测器来判定它们到底是伪造证书还是非伪造证书。

之后我们则要对可以证书进行进一步的判断。

数据库部分

由于我们在检测ssl证书的过程中使用了数据库。所以我们在这里将数据库的搭建思路写出来。
为了存储证书的可用信息以及检测结果、统计证书的伪造情况等,本系统中设计了证书信息表、伪造证书主体统计表、伪造证书颁发者统计表。
其主要功能是统计证书的可用信息,包括证书的主体、证书的颁发者、证书的起始时间和结束时间,以便与后续获取到的真实证书的信息进行比对。对于证书信息完全相同的日志记录,将日志记录出现的次数存储在字段occur_time中,更便于存储和统计工作。而字段forged用于记录该日志记录的证书的检测结果。

下面放出mysql的数据库搭建sql文件内容。

drop database if exists Cert_data;
create database Cert_data;
use Cert_data;

CREATE TABLE certinfo_a(
cert_id int(11) not null auto_increment,
cert_subject varchar(500) not null,
cert_issuer varchar(500) not null,
startdate varchar(40) not null,
expiredate varchar(40) not null,
occur_time int(11) not null default'1',
forged int(11),
primary key(cert_id)
);

CREATE table certinfo_b like certinfo_a;
CREATE table certinfo_c like certinfo_a;
CREATE table certinfo_d like certinfo_a;
CREATE table certinfo_e like certinfo_a;
CREATE table certinfo_f like certinfo_a;
CREATE table certinfo_g like certinfo_a;
CREATE table certinfo_h like certinfo_a;
CREATE table certinfo_i like certinfo_a;
CREATE table certinfo_j like certinfo_a;
CREATE table certinfo_k like certinfo_a;
CREATE table certinfo_l like certinfo_a;
CREATE table certinfo_m like certinfo_a;
CREATE table certinfo_n like certinfo_a;
CREATE table certinfo_o like certinfo_a;
CREATE table certinfo_p like certinfo_a;

CREATE TABLE  subjectrecord(
cert_subject varchar(500) not null,
occur_time int(11) not null,
primary key(cert_subject)
);
CREATE TABLE  issuerrecord(
cert_issuer varchar(500) not null,
occur_time int(11) not null,
primary key(cert_issuer)
);

系统样例





当我们上传流量包后,我们进行测试得到上图的伪造证书与非伪造证书饼图。

当我们上传的流量包足够多时,我们的数据库中就会存在大量的数据样例。此时我们的系统工具则可以进行进一步的证书检测。当我们拿到新流量后,我们可以将其上传到我们的系统中并可以对其进行真伪检测。

测试结果

我们对我们手中的40w条流量数据进行了测试,我们得出如下内容。

通过对本次检测结果的分析,本文对SSL中间人攻击提出一些防范措施与建议。当用户采用HTTPS通信时,首先必须要进行证书链验证这一步骤。对于一些需要安装证书的网站,尤其是那些需要进行个人信息填写、网上即时交易的网站,务必要谨慎再谨慎,一旦发现安装的证书中存在任何可疑的地方,应立刻中断与该网站的连接。或者当用户已经安装了该网站的证书,但在与该网站通信时,网站仍然要求用户安装证书,那么这个证书很有可能就是伪造的,用户应当立即中断与网站的连接并通过其它可信赖的方式验证该网站是否更改过证书。
而对于那些浏览器出现诸如“该网站证书存在问题”或“您的连接不是私密连接”等警告字样时,也务必多加留意,检查该网站证书的主体与颁发者、该网站的域名是否与网站证书域名相一致,同时不应该在该网站上填写隐私信息或进行网上交易,因为此时用户很有可能就受到了中间人攻击。如果发现访问网站的速度比平时慢很多,有可能遭到了ARP攻击,此时应通过ARP查询来检验自己是否遭到了中间人攻击。 即便用户对网站证书有一定了解,也未必能察觉到自己正遭受SSL中间人攻击。

由于篇幅有限,部分内容无法进行详细的交代。对于ssl证书的检测工具的编写我们提出了一种思路,也希望能帮助到对此进行研究的同行。如有需要大家可以在下方留言,我会进行答复。

本文转载自:先知社区