Docker鏡像掃描器的實現

https://cloud.tencent.com/developer/article/1039768

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檢測文件與系統調用

這裏先給出一些深度分析的思路,限於篇幅,我們會在以後的文章中做詳細介紹。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章