安全圈 | 专注于最新网络信息安全讯息新闻

首页

docker鏡像掃描器的實現

作者 eppolito 时间 2020-02-27
all

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 <- AnalyzeLocalImage(imageName, minSeverity, *flagEndpoint, *flagMyAddress, tmpPath)    

    }()    

鏡像被解壓到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{vulnerability, feature, severity})  

            }  

        }  

    }  

    //排序輸出報告美化  

    …..  

至此,對analyze-local-images的源碼已經分析完畢。從中可以可以看出。analyze-local-images做的事情很簡單。

就是將layer.tar發送給clair。並將clair分析後的結果通過API介面獲取到並在本地列印。

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

  //

//

  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{store,paginationKey}

  // 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  }

    // 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檢測檔案與系統調用

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