cvechecker调研分享

cvechecker

简述

cvechecker是一个工具,会根据你提供的软件列表自动确认装在本机的软件的相应版本,然后跟网上的在线数据库(database of potential vulnerable software)进行漏洞与软件版本的匹配,发现可疑漏洞会报告。


术语

  • CVE

Common Vulnerabilities and Exposures:通用漏洞披露(维基百科)

CVE:A list of entries — each containing an identification number, a description, and at least one public reference — for publicly known cybersecurity vulnerabilities.

格式:CVE-YYYY-NNNN;YYYY为西元纪念,NNNN为流水编号

例子:2014年发现的心脏出血漏洞(Heartbleed bug)编号为 CVE-2014-0160

  • CPE

Common Platform Enumeration

一种结构化的命名规范/标准。

例子:

规范的版本较多,对于一个这样的信息:
[part=”a”,vendor=”microsoft”,product=”internet_explorer”,version=”8.0.6001”,update=”beta”]

CPE格式可以如下:

cpe:2.3: a:microsoft:internet_explorer:8.0.6001:beta:*:*:*:*:*:*

  • CVSS

Common Vulnerability Scoring System:通用漏洞评分系统

它是工业标准,用于描述安全漏洞严重程度的统一评分方案。


cvechecker的安装

  1. 官方下载压缩包解压
  2. 进入cvechecker的根目录,运行 ./configure 开始配置
  3. 运行 make
  4. 运行 make install
  5. 完成安装

注意点:运行 configure 不成功的话,可能先需要解决一些依赖问题。cvechecker默认使用sqlite3来管理cve数据,支持sqlite、sqlite3、mysql,如果使用mysql需要做一些额外的配置,具体可以参考这个链接


cvechecker的使用

  1. cvechecker -i

    在本机初始化数据库(默认使用sqlite3),用来存储网上下载到本地的CVE,在我的ubuntu上试验的时候,初始化数据库会在目录 /usr/local/var/ 中创建名叫cvechecker文件夹,需要root权限来运行。当然可以直接修改cvechecker根目录下conf文件夹中的 cvechecker.conf 文件来改变数据库存放的位置

  2. pullcves pull

    该命令会自动从网络下载CVE数据,存到数据库中去

  3. 准备工作

    find / -path /mnt -prune -o -path /media -prune -o -type f -perm -o+x > scanlist.txt
    echo "/proc/version" >> scanlist.txt
    cvechecker -b scanlist.txt

    每一次系统软件有更改,都需要让cvechecker重新加载系统安装的二进制文件清单。cvechecker不会去自动重新加载文件清单,更加不会去感知系统软件版本的变动。所以需要手动使用 find 命令去感知所有executable commands和libraries

  4. cvechecker -r or cvechecker -r -C > output.csv

    启动检查,cvechecker会打印出一个漏洞列表,加一个 -C 选项会打印出csv格式。

    更加具体详细的命令选项,可以参考链接


cvechecker扫描结果解读

File "/home/zingphoy/Software/odp/lib/gcc-3.4.5/libsasl2.so.2" (CPE = cpe:/a:carnegie_mellon_university:cyrus-sasl:2.1.19:::) on host ubuntu (key ubuntu)
  Potential vulnerability found (CVE-2009-0688)
  CVSS Score is 7.5
  Full vulnerability match (incl. edition/language)
  • 第一段是具体的文件匹配到了一个具体的CPE信息
  • 第二段是相应的CVE编号
  • 第三段是CVSS分数
  • 第四段是匹配出一个漏洞的可能性

实现原理

cvechecker的核心功能就是拿到软件的版本号,然后根据规则去匹配cve漏洞库,cvechecker是怎么做的呢?Sven(开发者)没有依赖于包版本管理器,而是直接通过查找软件包的版本号,然后构造CPE(Common Platform Enumeration)信息,通过在cve数据库中查找给CPE信息来提取CVE信息。每个软件包都包含了众多的软件,如ls和cat都属于coreutils,那么如何从系统软件列表差找到软件包列表及版本号便是Sven方法的关键所在。通过查看代码发现,Sven自己维护了一个软件包和各个命令的映射关系表。内容类似于:

,perlivp,1,perlivp$,# perlivp v([0-9][0-9](.[0-9]+)+),a,perl,perl,\1,,,
,libvorbis.so,1,libvorbis.so.
,Xiph.Org libVorbis ([0-9][0-9]*(.[0-9]+)+),a,xiph,libvorbis,\1,,,

看过cvechecker帮助文档的用户都知道,用户自己也可以定义添加这种映射表,而默认的映射表是由Sven自己维护的,默认只有650条,因此这里就是漏报的一处主要原因。

这个映射表解决了从命令到软件包的映射,但怎么获取软件包的版本号呢?继续看代码:

/**
 * Here is where the various version extraction methods are supported.
 * We currently still only support a single method (1, which is the
 * "strings -n 3 <file>" command execution) but now we can see if we can
 * support additional methods as well.
 */
if (filetype == 1) {
        char buffer[BUFFERSIZE];
        int ret;

        zero_string(buffer, BUFFERSIZE);
        ret = strings_extract_version(ws, &preg, pmatch, &cpe_data);

        if (ret == 0) {
                add_to_sqlite_database(ws, cpe_data);
                cpe_to_string(buffer, BUFFERSIZE, cpe_data);
                fprintf(stdout, " - Found match for %s/%s:\t%s\n", ws->currentdir, ws->currentfile, buffer);
        };
} else {
        fprintf(stderr, " ! %s/%s: The sqlite3 implementation currently doesn't support file type %d\n", ws->currentdir, ws->currentfile, filetype);
};
ws->rc = 0;
return 0;

原来是通过strings读取二进制程序中的标示,然后通过正则表达式来提取其中的版本信息。系统中的软件多种多样,尽管有默认的编程约定,但不同的开发者肯定会使用不同的声明方式,难免会无法找到或找错版本信息。此外,各个发行版本在发现CVE时往往采用backporting策略,因此即使CVE已经修复,软件包的大版本号也不会发生变化,这也是引起误报的主要原因。

更具体看函数调用(可上github自查)—— strings_extract_version( )的声明和实现:

cvechecker/src/output/stringscmd.h

#include <regex.h>
#include <stdio.h>
#include <string.h>
#include "../cvecheck_common.h"
#include "../swstring.h"

/*
 * Copyright 2010 Sven Vermeulen.
 * Subject to the GNU Public License, version 3.
 */

// strings_extract_version - Method for extracting the version from the file using the strings command
int strings_extract_version(struct workstate * ws, regex_t * preg, regmatch_t * pmatch, struct cpe_data * cpe); 

cvechecker/src/sqlite3/sqlite3_impl.c

#include "sqlite3_impl.h"
...

int strings_extract_version(struct workstate * ws, regex_t * preg, regmatch_t * pmatch, struct cpe_data * cpe) {
    const char * stringcmd = NULL;
    char file[FILENAMESIZE];
    char data[FILENAMESIZE];
    FILE * workfile = NULL;
    const config_setting_t * stringcmdcfg = NULL;
    char * buffer;
    int rc;
    int retv;

    zero_string(data, BUFFERSIZE);

    stringcmdcfg = config_lookup(ws->cfg, "stringcmd");
    if (stringcmdcfg == NULL) {
        fprintf(stderr, "Configuration file does not contain stringcmd directive.\n");
    return 1;
    };
    stringcmd    = config_setting_get_string(stringcmdcfg);
    if ((swstrlen(stringcmd) == 0) || (swstrlen(stringcmd) > FILENAMESIZE-1)) {
        fprintf(stderr, "Configuration files 'stringcmd' directive cannot be empty or exceed %d characters\n", FILENAMESIZE-1);
        return 1;
    };

    if (swstrlen(ws->currentdir)+swstrlen(ws->currentfile) > FILENAMESIZE-1) {
        fprintf(stderr, "File path cannot exceed %d characters\n", FILENAMESIZE-1);
        return 1;
    };

    sprintf(file, "%s/%s", ws->currentdir, ws->currentfile);
    buffer = substitute_variable(stringcmd, "@", "@", "file", file);

    retv = 1;

    workfile = popen(buffer, "r");
    while (fgets(data, FILENAMESIZE, workfile) != 0) {
        if (data[FILENAMESIZE-1] != 0x00) 
            data[FILENAMESIZE-1] = 0x00;
        if (data[swstrlen(data)-1] == '\n')
            data[swstrlen(data)-1] = 0x00;
    rc = regexec(preg, data, 16, pmatch, 0);
    if (!rc) {
        retv = 0;
        // Found a match, extracting version (but first, print it out)
        for (rc = preg->re_nsub; rc > 0; rc--) {
            retv += search_and_substitute_group(preg, pmatch, data, cpe, rc);
        };
        break;
    };
    zero_string(data, FILENAMESIZE);
    }
    pclose(workfile);
    free(buffer);

    return retv;
};

到此,cvechecker已经获取了系统软件包列表以及软件包对应的版本号,将该信息处理成CPE信息,在CVE列表中查找即可。


优缺点

优点

  • 扫描速度快,扫描时资源占用少,程序不用需要后台运行,默认配置即可覆盖常用的软件
  • 当扫描规则支持得越丰富,cvechecker的威力就会越大,从某种程度上说它是一个成长性的扫描工具,通过人为配置(如同WAF添加规则一样),作用会越来越强大
  • cvechecker虽然无法准确的查询出当前系统的cve信息,但是很好的解决了不同Linux版本中包管理器差异的问题,底层依赖较少

缺点

  • cvechecker处理中涉及到众多的映射信息,如程序和软件包的映射表、CVE列表,默认会下载当前所有的CVE信息,使用数据库来组织这些映射信息,需要占用不少磁盘空间,截止至2018-08-02,nvdcve漏洞信息的文件就有820MB大小
  • 难以保证漏报率,需要进行大量人工干预

应用场景

作为主机软件安全的基准扫描,由于扫描时消耗的资源也不是特别大,可以作为例行扫描来使用;

除了在单台机器上都部署一个cvechecker之外,这里考虑一种集中式的cvechecker。本身cvechecker并不需要长期后台运行,只需要给它一个cpe信息它就可以开始匹配,所以可以试着做成一个后台服务器,上面部署cvechecker,其他机器上装一个agent例行收集机器上软件的cpe信息。agent把收集的信息发送到服务器让上面cvechecker完成匹配工作再返回结果。当然这里的额外工作量就是完成一个收集cpe信息的agent,肯定涉及到不少的兼容工作,而且部署在server上的cvechecker本身的软件版本侦查能力也被浪费了。这样做好处就是可以不用再每一台机器上都拉去nvdcve漏洞信息,可以省去不少磁盘空间,并且只需要维护一套匹配规则,更加方便人为配置匹配规则,而后台负责匹配的cvechecker也可以做成集群式,只要数据库和匹配规则适当地分发下去,就可以连续高负载地匹配漏洞。

对于特殊的、硬件环境较为低端苛刻的服务器,上面只部署了极少数的软件服务,而且存储空间十分小,可考虑使用watchlist功能,手动配置需要扫描的软件的CPE信息,而不需要从网上拉取一个巨大的数据库存在本地。


对比公司内部项目

内部某项目的资产、指纹收集功能也是收集软件和类库的版本号,主要的技术手段是调用软件的shell命令或find文件名,再去截取版本号。这种方案是针对性地获取版本号,覆盖类库&软件若干种。将这些版本号上报到后台,后台会进行CVE版本匹配,找出对应的软件版本是否存在已知CVE漏洞。

部分已脱敏的golang代码片段:

var g_ClassLibraryInfo = []ClassLibraryInfo{
    {"apr","libapr-1.so.0","ldconfig -p|grep libapr-|head -1|cut -d '>' -f 2","ldconfig -p|grep libapr-|head -1|cut -d '>' -f 2|cut -d '.' -f 3",""},
    {....}
    ......
}
......
case "apr":
    classlibpath, classlibversion = GetLibInfo("libapr-",3,4,5)
case "apr-util":
    classlibpath, classlibversion = GetLibInfo("libaprutil",3,4,5)
case ...

获取软件版本号似乎市面上还没有一个通用的方案,cvechecker也需要针对性地获取软件版本。cvechecker主要是通过编写匹配规则来针对性获取,可以看配置文件 cvechecker/versions.dat ,获取方法上面已经分析过了,就是利用strings命令+正则的方式来匹配版本号,其优点就是不同Linux系统都比较通用(内部的方案其实也是比较通用的,至少在公司需要兼容的几种系统上没有特别问题),但是没有明显数据支撑两者到底哪一个更好,所以这里拿不出高下之分的定论。

缺点也是很明显的,如果软件版本号格式一旦修改,那么规则都会对新的版本号失效,不过一般正规有名气的软件发行到市面上的版本号格式都比较固定,所以不需要担心这个问题。


参考资料

漏洞扫描基本概念

CPE官网

CVE官网

Natinal Vulnerability Database - CVE数据库

cpe格式详解

cvechecker官方使用文档

cvechecker的github文档

cvechecker实现原理分析

cvechecker 3.0

如何选择漏洞扫描工具(扫盲帖)

cvechecker的github仓库

打赏
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2017-2022 Zingphoy Han
  • 访问人数: | 浏览次数:

一块钱一个俯卧撑 O_O

微信