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