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檢測檔案與系統調用
這裡先給出一些深度分析的思路,限於篇幅,我們會在以後的文章中做詳細介紹。