首页 > 其他分享 >Docker镜像扫描器的实现

Docker镜像扫描器的实现

时间:2022-09-07 11:03:58浏览次数:118  
标签:扫描器 err ctx 镜像 router Docker local clair

Docker镜像简介

这篇文章算抛砖引玉,给大家提供一些简单的思路。

首先要做Docker镜像扫描,我们必须要懂Docker镜像是怎么回事。

Docker镜像是由文件系统叠加而成。最底层是bootfs,之上的部分为rootfs。

bootfs是docker镜像最底层的引导文件系统,包含bootloader和操作系统内核。

rootfs通常包含一个操作系统运行所需的文件系统。这一层作为基础镜像。

在基础镜像之上,会加入各种镜像,如emacs、apache等。

如何分析镜像

对镜像进行分析,无外乎静态分析和动态分析两种方式。而开源的可参考的实现有

专注于静态分析的Clair和容器关联分析与监控的Weave Scope。但Weave Scope似乎跟安全关系不太大,下面笔者会给出一些动态分析的思路。

首先,我们看以下威名远扬的Clair。Clair目前支持appc和docker容器的静态分析。

Clair整体架构如下:

Clair包含以下核心模块。

获取器(Fetcher)-从公共源收集漏洞数据

检测器(Detector)-指出容器镜像中包含的Feature

容器格式器(Image Format)- Clair已知的容器镜像格式,包括Docker,ACI

通知钩子(Notification Hook)-当新的漏洞被发现时或者已经存在的漏洞发生改变时通知用户/机器

数据库(Databases)-存储容器中各个层以及漏洞

Worker-每个Post Layer都会启动一个worker进行Layer Detect

编译与使用

Clair目前共发布了21个release。我们这里使用第20个release版本,既V2.0.0进行源码剖析。

为了减少在编译过程中的错误,建议使用ubuntu进行编译。并在编译之前,确保git,bzr,rpm,xz等模块已经安装好。Golang版本使用1.8.3以上。并确保已经安装好postgresql,笔者使用的版本为9.5.建议你也与笔者保持一致。

使用go build github.com/coreos/clair/cmd/clair编译clair

使用gobuild github.com/coreos/analyze-local-images编译analyze-local-images

其中Clair作为server端analyze-local-images作为Client端

简单使用如下。通过analyze-local-images分析nginx:latest镜像。

两者交互的整个流程可以简化为:

Analyze-local-images源码分析

在使用analyze-local-images时,我们可以指定一些参数。

analyze-local-images -endpoint "http://10.28.182.152:6060"

-my-address "10.28.182.151" nginx:latest

其中,endpoint为clair主机的ip地址。my-address为运行analyze-local-images这个客户端的地址。

postLayerURI是向clair API V1发送数据库的路由,getLayerFeaturesURI是从clair API V1获取漏洞信息的路由。

analyze-local-images在主函数调用intMain()函数,而intMain会首先去解析用户的输入参数。例如刚才的endpoint。

Analyze-local-images是主要执行流程为:

main()->intMain()->AnalyzeLocalImage()—>analyzeLayer()->getLayer()

func intMain() int {

//解析命令行参数,并给刚才定义的一些全局变量赋值。

......

//创建一个临时目录

tmpPath, err := ioutil.TempDir("", "analyze-local-image-")

//在/tmp目录下创建以analyze-local-image-开头的文件夹。

//为了能够清楚的观察/tmp下目录的变化,我们将defer os.RemoveAll(tmpPath)这句注释掉,再重新编译。

......

//调用AnalyzeLocalImage方法分析镜像

go func() {

analyzeCh

}()

}

镜像被解压到tmp目录下的目录结构如下:

analyze-local-images与clair服务端进行交互的两个主要方法为analyzeLayer和getLayer。analyzeLayer向clair发送JSON格式的数据。而getLayer用来获取clair的请求。并将json格式数据解码后格式化输出。

func AnalyzeLocalImage(imageName string, minSeverity database.Severity, endpoint, myAddress, tmpPath string) error {

//保存镜像到tmp目录下

//调用save方法

//save方法的原理就是使用docker save镜像名先将镜像打包成tar文件

//然后使用tar命令将文件再解压到tmp文件中。

err := save(imageName, tmpPath)

.......

//调用historyFromManifest方法,读取manifest.json文件获取每一层的id名,保存在layerIDs中。

//如果从manifest.json文件中获取不到,则读取历史记录

layerIDs, err := historyFromManifest(tmpPath)

if err != nil {

layerIDs, err = historyFromCommand(imageName)

}

......

//如果clair不在本机,则在analyze-local-images上开启HTTP服务,默认端口为9279

......

//分析每一层,既将每一层下的layer.tar文件发送到clair服务端

err = analyzeLayer(endpoint, tmpPath+"/"+layerIDs[i]+"/layer.tar", layerIDs[i], layerIDs[i-1])

......

}

func AnalyzeLocalImage(imageName string, minSeverity database.Severity, endpoint, myAddress, tmpPath string) error {

......

//获取漏洞信息

layer, err := getLayer(endpoint, layerIDs[len(layerIDs)-1])

//打印漏洞报告

......

for _, feature := range layer.Features {

if len(feature.Vulnerabilities) > 0 {

for _, vulnerability := range feature.Vulnerabilities {

severity := database.Severity(vulnerability.Severity)

isSafe = false

if minSeverity.Compare(severity) > 0 {

continue

}

hasVisibleVulnerabilities = true

vulnerabilities = append(vulnerabilities, vulnerabilityInfo)

}

}

}

//排序输出报告美化

.....

}

至此,对analyze-local-images的源码已经分析完毕。从中可以可以看出。analyze-local-images做的事情很简单。

就是将layer.tar发送给clair。并将clair分析后的结果通过API接口获取到并在本地打印。

Clair源码剖析

analyze-local-images 发送layer.tar文件后主要是由/worker.go下的ProcessLayer方法进行处理的。

这里先简单讲下clair的目录结构,我们仅需要重点关注有注释的文件夹。

--api //api接口

-- cmd//服务端主程序

--contrib

--database //数据库相关

--Documentation

--ext //拓展功能

-- pkg//通用方法

-- testdata

`--vendor

为了能够深入理解Clair,我们还是要从其main函数开始分析。

/cmd/clair/main.go

funcmain() {

//解析命令行参数,默认从/etc/clair/config.yaml读取数据库配置信息

......

//加载配置文件

config, err :=LoadConfig(*flagConfigPath)

if err != nil {

log.WithError(err).Fatal("failedto load configuration")

}

//初始化日志系统

......

//启动clair

Boot(config)

}

/cmd/clair/main.go

funcBoot(config *Config) {

......

//打开数据库

db, err :=database.Open(config.Database)

if err != nil {

log.Fatal(err)

}

defer db.Close()

//启动notifier服务

st.Begin()

go clair.RunNotifier(config.Notifier,db, st)

//启动clair的Rest API服务

st.Begin()

go api.Run(config.API, db, st)

st.Begin()

//启动clair的健康检测服务

go api.RunHealth(config.API, db, st)

//启动updater服务

st.Begin()

go clair.RunUpdater(config.Updater,db, st)

// Wait for interruption and shutdowngracefully.

waitForSignals(syscall.SIGINT,syscall.SIGTERM)

log.Info("Received interruption,gracefully stopping ...")

st.Stop()

}

Go api.Run执行后,clair会开启Rest服务。

/api/api.go

func Run(cfg *Config, store database.Datastore, st *stopper.Stopper) {

defer st.End()

//如果配置为空就不启动服务

......

srv := &graceful.Server{

Timeout: 0, // Already handled by our TimeOut middleware

NoSignalHandling: true, // We want to use our own Stopper

Server: &http.Server{

Addr: ":" + strconv.Itoa(cfg.Port),

TLSConfig: tlsConfig,

Handler: http.TimeoutHandler(newAPIHandler(cfg, store), cfg.Timeout, timeoutResponse),

},

}

//启动HTTP服务

listenAndServeWithStopper(srv, st, cfg.CertFile, cfg.KeyFile)

log.Info("main API stopped")

}

Api.Run中调用api.newAPIHandler生成一个API Handler来处理所有的API请求。

/api/router.go

funcnewAPIHandler(cfg *Config, store database.Datastore) http.Handler {

router := make(router)

router["/v1"] =v1.NewRouter(store, cfg.PaginationKey)

return router

}

所有的router对应的Handler都在

/api/v1/router.go中:

funcNewRouter(store database.Datastore, paginationKey string) *httprouter.Router {

router := httprouter.New()

ctx := &context

// Layers

router.POST("/layers",httpHandler(postLayer, ctx))

router.GET("/layers/:layerName", httpHandler(getLayer, ctx))

router.DELETE("/layers/:layerName", httpHandler(deleteLayer,ctx))

// Namespaces

router.GET("/namespaces",httpHandler(getNamespaces, ctx))

// Vulnerabilities

router.GET("/namespaces/:namespaceName/vulnerabilities",httpHandler(getVulnerabilities, ctx))

router.POST("/namespaces/:namespaceName/vulnerabilities",httpHandler(postVulnerability, ctx))

router.GET("/namespaces/:namespaceName/vulnerabilities/:vulnerabilityName",httpHandler(getVulnerability, ctx))

router.PUT("/namespaces/:namespaceName/vulnerabilities/:vulnerabilityName",httpHandler(putVulnerability, ctx))

router.DELETE("/namespaces/:namespaceName/vulnerabilities/:vulnerabilityName",httpHandler(deleteVulnerability, ctx))

// Fixes

router.GET("/namespaces/:namespaceName/vulnerabilities/:vulnerabilityName/fixes",httpHandler(getFixes, ctx))

router.PUT("/namespaces/:namespaceName/vulnerabilities/:vulnerabilityName/fixes/:fixName",httpHandler(putFix, ctx))

router.DELETE("/namespaces/:namespaceName/vulnerabilities/:vulnerabilityName/fixes/:fixName",httpHandler(deleteFix, ctx))

// Notifications

router.GET("/notifications/:notificationName",httpHandler(getNotification, ctx))

router.DELETE("/notifications/:notificationName",httpHandler(deleteNotification, ctx))

// Metrics

router.GET("/metrics",httpHandler(getMetrics, ctx))

return router

}

而具体的Handler是在/api/v1/routers.go中

例如analyze-local-images发送的layer.tar文件,最终会交给postLayer方法处理。

funcpostLayer(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx*context) (string, int) {

......

err = clair.ProcessLayer(ctx.Store,request.Layer.Format, request.Layer.Name, request.Layer.ParentName,request.Layer.Path, request.Layer.Headers)

......

}

而ProcessLayer方法就是在/worker.go中定义的。

funcProcessLayer(datastore database.Datastore, imageFormat, name, parentName, pathstring, headers map[string]string) error {

//参数验证

......

//检测层是否已经入库

layer, err := datastore.FindLayer(name, false, false)

if err != nil && err !=commonerr.ErrNotFound {

return err

}

//如果存在并且该layer的Engine Version比DB中记录的大于等于3(目前最大的worker version),则表明已经detect过这个layer,则结束返回。否则detectContent对数据进行解析。

// Analyze the content.

layer.Namespace, layer.Features, err =detectContent(imageFormat, name, path, headers, layer.Parent)

if err != nil {

return err

}

return datastore.InsertLayer(layer)

}

在detectContent方法如下:

func detectContent(imageFormat,name, path string, headers map[string]string, parent *database.Layer)(namespace *database.Namespace, featureVersions []database.FeatureVersion, errerror) {

......

//解析namespace

namespace, err = detectNamespace(name,files, parent)

if err != nil {

return

}

//解析特征版本

featureVersions, err = detectFeatureVersions(name, files, namespace,parent)

if err != nil {

return

}

......

return

}

Docker镜像静态扫描器的简易实现

通过刚才的源码分析,结合analyze-local-images以及clair。我们可以先实现一个简易的Docker静态分析器。对docker镜像逐层分析,实现输出软件特征版本。以便于我们了解clair的工作原理。

这里直接给出github链接:

https://github.com/MXi4oyu/DockerXScan/releases/tag/0.1

感兴趣的朋友可以自行下载测试。

这里给出Docker镜像静态扫描器的简易架构。

Docker镜像深度分析

(1)Webshell检测

对于webshell检测,我们可以采用三种方式。

方式一:模糊hash

模糊hash算法使用的是:https://ssdeep-project.github.io

我们根据其API实现了Go语言的绑定:gossdeep

主要API函数有两个,一个是Fuzzy_hash_file,一个是Fuzzy_compare。

1.提取文件模糊hash

Fuzzy_hash_file("/var/www/shell.php")

2.比较模糊hash

Fuzzy_compare("3:YD6xL4fYvn:Y2xMwvn","3:YD6xL4fYvn:Y2xMwvk")

方式二:yara规则引擎

根据yara规则库进行检测

Yara("./libs/php.yar","/var/www/")

方式三:机器学习

机器学习,分类算法:CNN-Text-Classfication

https://github.com/dennybritz/cnn-text-classification-tf/

(2)木马病毒检测

我们知道开源杀毒引擎ClamAV的病毒库非常强大,主要有

1) 已知的恶意二进制文件的MD5哈希值

2) PE(Windows 中可执行文件格式)节的MD5哈希值

3) 十六进制特征码(shellcode)

4) 存档元数据特征码

5) 已知的合法文件的白名单数据库

我们可以

将clamav的病毒库转换为yara规则,进行恶意代码识别。也可以利用开源的yara规则,进行木马病毒的检测。

(3)镜像历史分析

(4)动态扫描

通过docker的配置文件,我们可以获取到其暴漏出来的端口。模拟运行后,可以用常规的黑客漏洞扫描进行扫描。

(5)调用监控

利用Docker API检测文件与系统调用

这里先给出一些深度分析的思路,限于篇幅,我们会在以后的文章中做详细介绍。

标签:扫描器,err,ctx,镜像,router,Docker,local,clair
From: https://www.cnblogs.com/dhcn/p/16664535.html

相关文章

  • Centos更改镜像源
     1)备份原有镜像源文件mv/etc/yum.repos.d/CentOS-Base.repo/etc/yum.repos.d/CentOS-Base.repo.bak 2)下载阿里云镜像源文件curl-o/etc/yum.repos......
  • Docker 入门指南
    Docker入门指南目录基础概念安装教程基本操作常用安装构建操作容器编排壹.基础概念什么是Docker?Docker是基于Go开发的应用容器引擎,属于Linux容器的一种封......
  • 让我们学习,如何使用 python 创建自己的端口扫描器
    让我们学习,如何使用python创建自己的端口扫描器PortScannerPythonPicture本教程仅包含用于创建端口扫描器的四个不同代码片段。这些端口扫描器将为Web服务和外部......
  • docker安装过程报错
    前面四个步骤为:(1)yum-yinstallgcc(安装GNU编译器套件) (2)yum-yinstallgcc-c++(安装GNU编译器套件) (3)yuminstall-yyum-utils(安装工具包) (4)yum-config-manage......
  • 如何优雅的对 Docker 容器进行健康检查
    公众号关注 「奇妙的Linux世界」设为「星标」,每天带你玩转Linux! 自1.12版本之后,Docker引入了原生的健康检查实现。对于容器而言,最简单的健康检查是进程级的健康......
  • 如何用 React 烹饪 ‍ Docker。第 1 部分 - 基本理论和命令。
    怎么做饭‍码头工人与反应。第1部分-基本理论和命令。如果你可以创建一个容器——那就去做吧!Photoby伊恩·泰勒on不飞溅基础码头工人。为了什么?Docker......
  • docker-compose 模板文件
    docker-compose模板文件模板文件是使用Compose的核心,涉及到的指令关键字也比较多。但大家不用担心,这里面大部分指令跟dockerrun相关参数的含义都是类似的。默认的......
  • docker常用镜像命令
    由于想把自己写的Python代码和所需的环境放到docker,所以最近又去学了下docker相关知识,这篇先整理一下docker镜像命令1、列出本地镜像dockerimages用法 dockerimages......
  • Docker
    容器查询正在运行的容器:dockerps查询所有容器:dockerps-a删除容器1.先停止容器:dockerstop 容器id2.删除:dockerrm 容器id停掉所有容器:docker stop$(dockerp......
  • docker-compose 命令说明
    docker-compose命令说明1.1命令选项-f,--fileFILE指定使用的Compose模板文件,默认为docker-compose.yml,可以多次指定。-p,--project-nameNAME指定项目名称,默......