TL;DR
文章較長,代碼很多,可直接拖到文末看講解視頻。
背景
在云原生領(lǐng)域中,Cilium是容器管理上最著名的網(wǎng)絡(luò)編排、可觀察性、網(wǎng)絡(luò)安全的開源軟件。基于革命性技術(shù)eBPF實現(xiàn),并用XDP、TC等功能實現(xiàn)了L3、L4層的防火墻、負(fù)載均衡軟件,具備優(yōu)秀的網(wǎng)絡(luò)安全處理能力,但在運(yùn)行時安全上,Cilium一直是缺失的。
2022年5月,在歐洲舉行的KubeCon技術(shù)峰會期間,Cilium的母公司Isovalent發(fā)布了云原生運(yùn)行時防護(hù)系統(tǒng)Tetragon[1],填補(bǔ)這一空缺。
Tetragon的面世,意味著與falco、tracee、KubeArmor、datadog-agent等幾款產(chǎn)品正面競爭,eBPF運(yùn)行時防護(hù)領(lǐng)域愈加內(nèi)卷。
Tetragon介紹
摘自Tetragon官方倉庫[2]的產(chǎn)品介紹。
eBPF實時性
Tetragon 是一個運(yùn)行時安全實時和可觀察性工具。它直接在內(nèi)核中對事件做相應(yīng)動作,比如執(zhí)行過濾、阻止,無需再將事件發(fā)送到用戶空間處理。
對于可觀察性用例,直接在內(nèi)核中應(yīng)用過濾器會大大減少觀察開銷。避免昂貴的上下文切換,尤其是對于高頻事件,例如發(fā)送、讀取或?qū)懭氩僮鳎瑴p少了大量的內(nèi)存、CPU等資源。
同時,Tetragon提供了豐富的過濾器(文件、套接字、二進(jìn)制名稱、命名空間等),允許用戶篩選重要且相關(guān)的事件,并傳遞給用戶空間。
eBPF靈活性
Tetragon 可以掛載到Linux Kernel中的任何函數(shù),并過濾其參數(shù)、返回值、進(jìn)程元數(shù)據(jù)(例如,可執(zhí)行文件名稱)、文件和其他屬性。
跟蹤策略通過編寫跟蹤策略,用戶可以解決各種安全性和可觀察性用例。Tetragon社區(qū)中,提供了一些常見的跟蹤策略,可以解決大部分的可觀察性和安全用例。
用戶也可以創(chuàng)建新的規(guī)則策略部署,自定義配置文件,以滿足業(yè)務(wù)需求。
eBPF 內(nèi)核感知
Tetragon 通過eBPF鉤子感知Linux Kernel狀態(tài),并將狀態(tài)與Kubernetes用戶策略相結(jié)合,以創(chuàng)建由內(nèi)核實時執(zhí)行的規(guī)則,來增強(qiáng)云原生場景的安全防護(hù)功能。例如,當(dāng)容器內(nèi)惡意程序更改其權(quán)限時,我們可以創(chuàng)建一個策略來觸發(fā)警報,甚至在進(jìn)程有機(jī)會完成系統(tǒng)調(diào)用并可能運(yùn)行其他系統(tǒng)調(diào)用之前終止該進(jìn)程。
Tetragon阻斷實現(xiàn)原理
以上是Tetragon官方的介紹,提到具備阻斷
能力,并在技術(shù)峰會上,展示了相關(guān)阻斷的截圖,有必要了解一下其實現(xiàn)原理。
tetragon的運(yùn)行原理會在下篇詳細(xì)介紹,本篇主要講實時阻斷原理。本文分析的代碼版本為首次發(fā)布的tag v0.8.0[3] ,commit ID:75e49ab。
業(yè)界常見方式
LKM的內(nèi)核模塊
、LD_PRELOAD
的動態(tài)鏈接庫修改、基于LSM的selinux
、seccomp
技術(shù)等都是常見做內(nèi)核態(tài)/用戶態(tài)運(yùn)行時阻斷的技術(shù)方案,而缺點(diǎn)就比較明顯,系統(tǒng)穩(wěn)定性、規(guī)則靈活性、變更周期等問題比較突出。
當(dāng)然,也有使用內(nèi)核模塊方式。方法是把eBPF特性,封裝在內(nèi)核模塊里,再安裝到老版本的內(nèi)核上,這樣,就可以覆蓋更多內(nèi)核版本了。但backport新特性的做法在社區(qū)里很不推薦,維護(hù)成本特別高,需要比較大的內(nèi)核研發(fā)團(tuán)隊與深厚的技術(shù)功底。。
云原生生態(tài)中,CNCF的項目Falco具備內(nèi)核模塊與eBPF探針兩套驅(qū)動引擎,提供數(shù)據(jù)收集能力。同類產(chǎn)品Tracee也是,還基于LSM接口,實現(xiàn)了一定的防御阻斷能力,同時支持使用者自定義語法配置文件,進(jìn)行檢測、判斷、阻斷規(guī)則的修改快速更新,以達(dá)到更好的防御能力。
作為云原生領(lǐng)域的容器管理軟件領(lǐng)頭羊,Cilium也會落后,但Linux Security Module[4]鉤子(以下簡稱LSM)需要Linux Kernel 5.7以上版本,而業(yè)界多數(shù)內(nèi)核版本都不會這么新。Cilium有沒有使用LSM類HOOK進(jìn)行阻斷呢?我們一起來看一下。
配置文件
前面提到,Tetragon靈活性更高,可以讀取配置文件規(guī)則,應(yīng)用到內(nèi)核態(tài)。以代碼倉庫的crds/examples/open_kill.yaml
為例,語法規(guī)則分為如下幾部分
- kprobe函數(shù)名
- 函數(shù)原型參數(shù)
- 進(jìn)程過濾配置
- 參數(shù)過濾配置
- 執(zhí)行動作
其中,matchActions
字段為匹配后的執(zhí)行動作,比如這里的Sigkill
-call:"__x64_sys_write"
syscall:true
args:
-index:0
type:"fd"
-index:1
type:"char_buf"
sizeArgIndex:3
-index:2
type:"size_t"
selectors:
-matchPIDs:
-operator:NotIn
followForks:true
isNamespacePID:true
values:
-0
-1
matchArgs:
-index:0
operator:"Prefix"
values:
-"/etc/passwd"
matchActions:
-action:Sigkill
yaml配置文件的解析在pkg/k8s/apis/cilium.io/v1alpha1/types.go
中的TracingPolicySpec
結(jié)構(gòu)體中,包含KProbeSpec
和TracepointSpec
, 對應(yīng)json:"kprobes"
和json:"tracepoints"
兩個json的結(jié)構(gòu)。
typeTracingPolicySpecstruct{
//+kubebuilderOptional
//Alistofkprobespecs.
KProbes[]KProbeSpec`json:"kprobes"`
//+kubebuilderOptional
//Alistoftracepointspecs.
Tracepoints[]TracepointSpec`json:"tracepoints"`
}
同時,Tetragon還支持遠(yuǎn)程下發(fā)配置,配置結(jié)構(gòu)與yaml結(jié)構(gòu)是一樣的。這里相比傳統(tǒng)的內(nèi)核模塊等技術(shù)方案,靈活性更高。
用戶空間
詳細(xì)執(zhí)行流程會在下篇分享,直入主題。
數(shù)據(jù)結(jié)構(gòu)
在項目中,抽象出一些概念:
- Tetragon由多個Sensors傳感器構(gòu)成
- Sensor由多個Programs和Maps構(gòu)成
- 每個Program對應(yīng)eBPF代碼的HOOK函數(shù)
- 每個Map是相應(yīng)Program的bpf的數(shù)據(jù)交互map
//ProgramreprentsaBPFprogram.
typeProgramstruct{
//NameisthenameoftheBPFobjectfile.
Namestring
//Attachistheattachmentpoint,e.g.thekernelfunction.
Attachstring
//Labelistheprogramsectionnametoloadfromprogram.
Labelstring
//PinPathisthepinnedpathtothisprogram.Notethisisarelativepath
//basedontheBPFdirectoryFGSisrunningunder.
PinPathstring
//RetProbeindicateswhetherakprobeisakretprobe.
RetProbebool
//ErrorFatalindicateswhetheraprogrammustloadandfatalotherwise.
//Mostprogramwillsetthistotrue.Forexample,kernelfunctionshooks
//maychangeacrossverionssodifferentnamesareattempted,hence
//avoidingfatalingwhenthefirstattemptfails.
ErrorFatalbool
//Needsoverridebpfprogram
Overridebool
//TypeisthetypeofBPFprogram.Forexample,tc,skb,tracepoint,
//etc.
Typestring
LoadStateState
//TraceFDisneededbecausetracepointsareaddeddifferentthankprobes
//forexample.TheFDistokeepareferencetothetracepointprogramin
//ordertodeleteit.TODO:ThiscanbemovedintoloaderDatafor
//tracepoints.
TraceFDint
//LoaderDatarepresentsper-typespecificfields.
LoaderDatainterface{}
//unloaderfortheprogram.nilifnotloaded.
unloaderunloader.Unloader
}
ExecveV53=program.Builder(
"bpf_execve_event_v53.o",
"sched/sched_process_exec",
"tracepoint/sys_execve",
"event_execve",
"execve",
)
Sensors傳感器加載
默認(rèn)傳感器注冊
pkg/sensors/tracing
包下由兩個文件對傳感器進(jìn)行默認(rèn)注冊,分別是generictracepoint.go
與generickprobe.go
,寫入如下兩個Sensors到registeredTracingSensors
中。
- observerKprobeSensor ,kprobe類型HOOK
- observerTracepointSensor, tracepoint類型HOOK
同時,還會注冊自定義eBPF probe加載器到registeredProbeLoad
中。
//generickprobe.go#Line=43
funcinit(){
kprobe:=&observerKprobeSensor{
name:"kprobesensor",
}
sensors.RegisterProbeType("generic_kprobe",kprobe)
sensors.RegisterTracingSensorsAtInit(kprobe.name,kprobe)
observer.RegisterEventHandlerAtInit(ops.MSG_OP_GENERIC_KPROBE,handleGenericKprobe)
}
策略文件解析
GetSensorsFromParserPolicy
方法遍歷所有Sensors,調(diào)用它的SpecHandler
方法,解析yaml配置。解析配置后,生成新的傳感器對象,作為整個應(yīng)用啟動、注入、監(jiān)聽的所有傳感器。
//cmd/tetragon/main.go#line=209
startSensors,err=sensors.GetSensorsFromParserPolicy(&cnf.Spec)
//pkg/sensors/sensors.go#line=160
for_,s:=rangeregisteredTracingSensors{
sensor,err:=s.SpecHandler(spec)
iferr!=nil{
returnnil,err
}
ifsensor==nil{
continue
}
sensors=append(sensors,sensor)
}
新增傳感器
接上,yaml解析器讀取格式后,根據(jù)當(dāng)前傳感器的類型(kprobe還是tracepoint),處理yaml配置文件對應(yīng)的HOOK配置。
kprobe類型以kprobe
類型的hook配置為例,調(diào)用 addGenericKprobeSensors
詳細(xì)處理每一個配置內(nèi)容。
//pkg/sensors/tracing/generickprobe.go#line=242-516
funcaddGenericKprobeSensors(kprobes[]v1alpha1.KProbeSpec,btfBaseFilestring)(*sensors.Sensor,error){
//遍歷所有kprobes的元素
fori:=rangekprobes{
f:=&kprobes[i]
funcName:=f.Call
//1.驗證配置文件中配置依賴,比如Sigkill需要內(nèi)核大于5.3
// 2. 解析匹配參數(shù)(進(jìn)程名、namespace、路徑、五元組等)寫入BTF對象
//3.解析ReturnArg返回值參數(shù),寫入到BTF對象
//4.過濾保留參數(shù),寫入BTF對象
//5.解析Filters邏輯,寫入BTF對象
//6.解析Binary名字到內(nèi)核數(shù)據(jù)結(jié)構(gòu)體
//7.將屬性寫入BTF指針,以便加載
//8.判斷action是否為SIGKILL,并寫入BTF對象
kernelSelectors,err:=selectors.InitKernelSelectors(f)
kprobeEntry:=genericKprobe{
loadArgs:kprobeLoadArgs{
filters:kernelSelectors,
btf:uintptr(btfobj),
retprobe:setRetprobe,
syscall:is_syscall,
},
argSigPrinters:argSigPrinters,
argReturnPrinters:argReturnPrinters,
userReturnFilters:userReturnFilters,
funcName:funcName,
pendingEvents:map[uint64]pendingEvent{},
tableId:idtable.UninitializedEntryID,
}
genericKprobeTable.AddEntry(&kprobeEntry)
//9.設(shè)定這個kprobe所需的ebpf字節(jié)碼文件信息
loadProgName:="bpf_generic_kprobe_v53.o"
// 10. 利用如上信息,填充到prog的結(jié)構(gòu)體中。
//11.在prog結(jié)構(gòu)體中,label字段的值都是**kprobe/generic_kprobe**
load:=program.Builder(
path.Join(option.Config.HubbleLib,loadProgName),
funcName,
"kprobe/generic_kprobe",
"kprobe"+"_"+funcName,
"generic_kprobe").
SetLoaderData(kprobeEntry.tableId)
}
//創(chuàng)建生成新的sensor,并返回
return&sensors.Sensor{
Name:"__generic_kprobe_sensors__",
Progs:progs,
Maps:[]*program.Map{},
},nil
}
解析過程如下:
- 驗證配置文件中配置依賴,比如Sigkill需要內(nèi)核大于5.3
- 解析匹配參數(shù)(進(jìn)程名、namespace、路徑、五元組等)寫入BTF對象
- 解析ReturnArg 返回值參數(shù),寫入到BTF對象
- 過濾保留參數(shù),寫入BTF對象
- 解析Filters邏輯,寫入BTF對象
- 解析Binary名字到內(nèi)核數(shù)據(jù)結(jié)構(gòu)體
- 將屬性寫入BTF指針,以便加載
- 判斷action是否為SIGKILL,并寫入BTF對象
- 設(shè)定這個kprobe所需的ebpf字節(jié)碼文件信息
- 利用如上信息,填充到prog的結(jié)構(gòu)體中。
- 在prog結(jié)構(gòu)體中,label字段的值都是kprobe/generic_kprobe
解析完成后,返回一個新的sensor
,并添加到Sensors傳感器
數(shù)組中。
至此,完成了配置文件的解析。在這里,阻斷指令的配置,是保存在genericKprobe.loadArgs.filters這個byte數(shù)組中的。
動態(tài)更新
在tetragon項目中,還具備從遠(yuǎn)程下發(fā)新的規(guī)則,更新、添加Sensor傳感器功能,相應(yīng)代碼在pkg/observer/observer.go
中,本篇不做過多展開,會在下篇分享。
//InitSensorManagerstartsthesensorcontrollerandsttmanager.
func(k*Observer)InitSensorManager()error{
varerrerror
SensorManager,err=sensors.StartSensorManager(k.bpfDir,k.mapDir,k.ciliumDir)
returnerr
}
eBPF加載與掛載
Sensors啟動
傳感器啟動加載,執(zhí)行流程為obs.Start(ctx, startSensors)
-> config.LoadConfig
-> load.Load
。
在Load方法里,對每一個Program元素,調(diào)用observerLoadInstance
方法進(jìn)行加載。
// load.Load
//Loadloadsthesensor,byloadingalltheBPFprogramsandmaps.
func(s*Sensor)Load(stopCtxcontext.Context,bpfDir,mapDir,ciliumDirstring)error{
for_,p:=ranges.Progs{
//...
//加載每個prog
iferr:=observerLoadInstance(stopCtx,bpfDir,mapDir,ciliumDir,p);err!=nil{
returnerr
}
//...
}
}
eBPF Program加載、掛載
在對每個eBPF program進(jìn)行加載時,會判斷HOOK的類型,針對tracepoint
特殊判斷處理。這里還是以Kprobe
為例。代碼調(diào)用loadInstance
函數(shù),邏輯中判斷是否存在自定義的加載器
:
-
若有,則調(diào)用
s.LoadProbe
加載; -
若沒有,則調(diào)用
loader.LoadKprobeProgram
加載;
//pkg/sensors/load.go#Line=297
ifs,ok:=registeredProbeLoad[load.Type];ok{
logger.GetLogger().WithField("Program",load.Name).WithField("Type",load.Type).Infof("Loadprobe")
returns.LoadProbe(LoadProbeArgs{
BPFDir:bpfDir,
MapDir:mapDir,
CiliumDir:ciliumDir,
Load:load,
Version:version,
Verbose:verbose,
})
}
returnloader.LoadKprobeProgram(
version,verbose,
btfObj,
load.Name,
load.Attach,
load.Label,
filepath.Join(bpfDir,load.PinPath),
mapDir,
load.RetProbe)
同樣,以前面提到的observerKprobeSensor
類型傳感器,已經(jīng)注冊自己的Probe加載器,那么會走s.LoadProbe()
邏輯,之后,調(diào)用loadGenericKprobeSensor()
-> loadGenericKprobe()
進(jìn)行加載。
這里的load.Name、load.Attach、load.Label的值,來自前面的yaml配置文件讀取部分,值分別為bpf_generic_kprobe_v53.o、 __x64_sys_write 、 kprobe/generic_kprobe,也就是說,不管是哪個kprobe函數(shù),都會被掛載到kprobe/generic_kprobe上,都被generic_kprobe_event()
這個eBPF 函數(shù)處理,起到統(tǒng)一管理的網(wǎng)關(guān)作用。
kprobe/generic_kprobe對應(yīng)的eBPF代碼在bpf/process/bpf_generic_kprobe.c
文件里,我們在后面內(nèi)核空間代碼詳細(xì)分析。
__attribute__((section(("kprobe/generic_kprobe")),used))int
generic_kprobe_event(structpt_regs*ctx)
{
returngeneric_kprobe_start_process_filter(ctx);
}
filters過濾器
loadGenericKprobe()
函數(shù)最后一個參數(shù)filters
過濾器參數(shù),類型是[4096]byte
funcloadGenericKprobe(bpfDir,mapDirstring,versionint,p*program.Program,btfuintptr,genmapDirstring,filters[4096]byte)error{
}
其內(nèi)容為前面yaml格式解析中的第5步,
kernelSelectors,err:=selectors.InitKernelSelectors(f)
格式構(gòu)成為
filter := [length][matchPIDs][matchBinaries][matchArgs][matchNamespaces][matchCapabilities][matchNamespaceChanges][matchCapabilityChanges]
這些數(shù)據(jù),也是在內(nèi)核空間eBPF邏輯中,實現(xiàn)參數(shù)匹配,動作響應(yīng)的判斷依據(jù)。
Cgo函數(shù)調(diào)用
在調(diào)用BPF SYSCALL的實現(xiàn)上,Tetragon沒有使用母公司自己的Cilium/ebpf庫,而是使用CGo包裝了libbpf進(jìn)行加載。使用的版本還是0.2.0,當(dāng)前社區(qū)最新版為0.7.0,比較老。(下一篇再講原因)
loadGenericKprobe()
函數(shù)調(diào)用bpf.LoadGenericKprobeProgram()
,并把filters
傳遞給下一個CGO的函數(shù)C.generic_kprobe_loader()
,函數(shù)定義在pkg/bpf/loader.go
的476行。
intgeneric_kprobe_loader(constintversion,
constintverbosity,
booloverride,
void*btf,
constchar*prog,
constchar*attach,
constchar*label,
constchar*__prog,
constchar*mapdir,
constchar*genmapdir,
void*filters){
structbpf_object*obj;
interr;
obj=generic_loader_args(version,verbosity,override,btf,prog,attach,
label,__prog,mapdir,filters,BPF_PROG_TYPE_KPROBE);
if(!obj){
return-1;
}
//...
}
之后,再調(diào)用CGO的C函數(shù)generic_loader_args()
進(jìn)行BPF SYSCALL調(diào)用,加載eBPF程序,掛載到對應(yīng)kprobe
函數(shù)上。之后,再寫入eBPF Maps。
eBPF Maps創(chuàng)建寫入
Tetragon使用eBPF Maps進(jìn)行用戶空間與內(nèi)核空間的配置數(shù)據(jù)交互,比如yaml配置文件中,各種過濾條件,匹配后的處理動作等。
還是以open_kill.yaml
為例,涉及了兩類eBPF Map
:
- 特征匹配規(guī)則,也就是配置的內(nèi)容,比如需要保護(hù)的文件路徑、IP黑名單等,稱之為filters規(guī)則
- 路由分發(fā)規(guī)則,也就是tetragon程序內(nèi)部,用于eBPF HOOK的函數(shù)網(wǎng)關(guān)處理各類參數(shù)的自用規(guī)則,用尾調(diào)用Tail Call類型的map實現(xiàn)。
filters規(guī)則Map
在generic_loader_args()
里,創(chuàng)建了"filter_map"
eBPF Map,并將判斷規(guī)則、阻斷規(guī)則filter bytes數(shù)組寫入到這個map里,格式依舊是[4096]byte
的字節(jié)流。
//...
char*filter_map="filter_map";
//...
map_fd=bpf_object__find_map_fd_by_name(obj,filter_map);
if(map_fd>=0){
err=bpf_map_update_elem(map_fd,&zero,filter,BPF_ANY);
if(err){
printf("WARNING:mapupdateelem%serror%d
",filter_map,err);
}
}
Tail Call尾調(diào)用Map
在eBPF Program加載部分提到,eBPF Kprobe函數(shù)kprobe/generic_kprobe
(即generic_kprobe_event()
)作為統(tǒng)一的過濾處理網(wǎng)關(guān),針對不同的kprobe,其參數(shù)個數(shù)、參數(shù)類型一定是不一樣的,比如文件讀寫函數(shù)__x64_sys_write
有三個參數(shù),分別是
args:
-index:0
type:"fd"
-index:1
type:"char_buf"
sizeArgIndex:3
-index:2
type:"size_t"
而SOCKET連接函數(shù)__x64_connect
只有兩個參數(shù),分別是
args:
-index:0
type:"sockfd"
-index:1
type:"sockaddr"
并且,他們的參數(shù)類型也不一樣,作為統(tǒng)一網(wǎng)關(guān),且面對參數(shù)個數(shù)、參數(shù)類型都不一樣的問題,Tetragon在eBPF的解決方案是使用BPF_MAP_TYPE_PROG_ARRAY
類型的eBPF Map實現(xiàn),用于尾調(diào)用Tail Call
處理。,
kprobe_calls尾調(diào)用Map
structbpf_map_def__attribute__((section("maps"),used))kprobe_calls={
.type=BPF_MAP_TYPE_PROG_ARRAY,
.key_size=sizeof(__u32),
.value_size=sizeof(__u32),
.max_entries=11,
};
kprobe_calls Map寫入
map_bpf=bpf_object__find_map_by_name(obj,"kprobe_calls");
//...
err=bpf_map__pin(map_bpf,map_name);
//...
map_fd=bpf_map__fd(map_bpf);
for(i=0;i11;i++){
structbpf_program*prog;
charprog_name[20];
charpin_name[200];
intfd;
snprintf(prog_name,sizeof(prog_name),"kprobe/%i",i);
prog=bpf_object__find_program_by_title(obj,prog_name);
if(!prog)
gotoout;
fd=bpf_program__fd(prog);
if(fd0){
err=errno;
gotoerr;
}
snprintf(pin_name,sizeof(pin_name),"%s_%i",__prog,i);
bpf_program__unpin(prog,pin_name);
err=bpf_program__pin(prog,pin_name);
if(err){
printf("programpin%stailcallerr%d
",pin_name,err);
gotoerr;
}
err=bpf_map_update_elem(map_fd,&i,&fd,BPF_ANY);
if(err){
printf("mapupdateelemi%i%stailcallerr%d%d
",i,prog_name,err,errno);
gotoerr;
}
}
用戶空間程序,讀取eBPF二進(jìn)制文件,讀取kprobe開頭的的eBPF函數(shù),以ID作為Key,寫入到kprobe_calls尾調(diào)用表中。
一共11個函數(shù),都在bpf/process/bpf_generic_kprobe.c
文件里,分別是:
- kprobe/0 對應(yīng) generic_kprobe_process_event0
- kprobe/1 對應(yīng) generic_kprobe_process_event1
- kprobe/2 對應(yīng) generic_kprobe_process_event2
- kprobe/3 對應(yīng) generic_kprobe_process_event3
- kprobe/4 對應(yīng) generic_kprobe_process_event4
- kprobe/5 對應(yīng) generic_kprobe_process_filter
- kprobe/6 對應(yīng) generic_kprobe_filter_arg1
- kprobe/7 對應(yīng) generic_kprobe_filter_arg2
- kprobe/8 對應(yīng) generic_kprobe_filter_arg3
- kprobe/9 對應(yīng) generic_kprobe_filter_arg4
- kprobe/10 對應(yīng) generic_kprobe_filter_arg5
至此,涉及eBPF阻斷功能的用戶空間邏輯全部完成。
內(nèi)核空間
在內(nèi)核空間,入口函數(shù)為用戶空間HOOK的kprobe點(diǎn) "kprobe/generic_kprobe" ,對應(yīng) generic_kprobe_event() 函數(shù)。這函數(shù)內(nèi)部只有一個 **generic_kprobe_start_process_filter()**的調(diào)用。
統(tǒng)一網(wǎng)關(guān)觸發(fā)
前面提到,tetragon是把open_kill.yaml
中的所有kprobe syscall
都交給這個eBPF函數(shù)處理,所以,當(dāng)__x64_sys_write
這些HOOK點(diǎn)觸發(fā)后,邏輯都交給generic_kprobe_start_process_filter()
處理。
//bpf/process/bpf_generic_kprobe.c#line=48
staticinline__attribute__((always_inline))int
generic_kprobe_start_process_filter(void*ctx)
{
//...
/*Tailcallintofilters.*/
tail_call(ctx,&kprobe_calls,5);
return0;
}
程序內(nèi)部獲取當(dāng)前進(jìn)程信息(struct task_struct、namespace、caps等)后,使用Tail Call尾調(diào)用轉(zhuǎn)交kprobe_calls
Maps的第5
個函數(shù)處理,即generic_kprobe_process_filter()
。
提醒
因eBPF虛擬機(jī)寄存器限制,只能獲取前5個參數(shù)。
進(jìn)程過濾流程
獲取當(dāng)前進(jìn)程信息后,進(jìn)入下一個流程,獲取當(dāng)前kprobe的進(jìn)程參數(shù)。即進(jìn)入generic_kprobe_process_filter()
函數(shù)內(nèi)部
//bpf/process/bpf_generic_kprobe.c#Line=146
//...
ret=generic_process_filter(msg,&filter_map,&process_call_heap);
if(ret==PFILTER_CONTINUE)
tail_call(ctx,&kprobe_calls,5);
elseif(ret==PFILTER_ACCEPT)
tail_call(ctx,&kprobe_calls,0);
/*Iffilterdoesnotacceptdropit.Ideallywewould
*logerrorcodesforlaterreview,TBD.
*/
returnPFILTER_REJECT;
這里對kprobe
所在進(jìn)程信息,以及配置文件中信息匹配,判斷是否要走過濾、阻斷流程。流程邏輯如下
PFILTER_ACCEPT 逐個進(jìn)入5類進(jìn)程event事件判斷
- generic_process_event0()
- generic_process_event1()
- generic_process_event2()
- generic_process_event3()
- generic_process_event4()
- generic_kprobe_filter_arg1()
PFILTER_CONTINUE直接進(jìn)入?yún)?shù)判斷
- generic_kprobe_filter_arg1()
OTHER 其他情況
直接返回PFILTER_REJECT
eBPF HOOK流程。
參數(shù)判斷流程
當(dāng)前kprobe
函數(shù)是否需要過濾判斷完成后,流程轉(zhuǎn)入真正的判斷邏輯中,即tailcals
尾調(diào)用的第6
個函數(shù)處理,即generic_kprobe_filter_arg1()
__attribute__((section(("kprobe/6")),used))int
generic_kprobe_filter_arg1(void*ctx)
{
returnfilter_read_arg(ctx,0,&process_call_heap,&filter_map,
&kprobe_calls,&override_tasks);
}
其中,核心處理函數(shù)為filter_read_arg()
,第四個參數(shù)&filter_map
就是來自用戶空間解析yaml的filters bytes數(shù)組
。
filter_read_arg()
函數(shù)判斷觸發(fā)當(dāng)前kprobe的進(jìn)程filter
配置,若沒找到,則調(diào)用kprobe/7至kprobe/9的尾調(diào)用函數(shù),逐個查找filter
配置。
阻斷動作執(zhí)行
當(dāng)找到filter
配置后,則讀取配置中相應(yīng)的action
參數(shù)類型,開始進(jìn)行相應(yīng)動作分類判斷,執(zhí)行相關(guān)流程邏輯,這塊都是在__do_action()
函數(shù)中完成的。
actions=(structselector_action*)&f[actoff];
postit=do_actions(e,actions,override_tasks);
action
動作的類型有下面幾種
- ACTION_POST = 0,
- ACTION_FOLLOWFD = 1,
- ACTION_SIGKILL = 2,
- ACTION_UNFOLLOWFD = 3,
- ACTION_OVERRIDE = 4,
每個action
動作類型都有相應(yīng)的處理邏輯,本文重點(diǎn)是阻斷的實現(xiàn),那么只需要關(guān)注ACTION_SIGKILL
類型。
staticinline__attribute__((always_inline))long
__do_action(longi,structmsg_generic_kprobe*e,
structselector_action*actions,structbpf_map_def*override_tasks)
{
intaction=actions->act[i];
switch(action){
caseACTION_UNFOLLOWFD:
caseACTION_FOLLOWFD:
//...
break;
caseACTION_SIGKILL:
if(bpf_core_enum_value(tetragon_args,sigkill))
send_signal(FGS_SIGKILL);
break;
caseACTION_OVERRIDE:
default:
break;
}
if(!err){
e->action=action;
return++i;
}
return-1;
}
可以看到,針對類型,是調(diào)用了send_signal()
函數(shù)進(jìn)行下發(fā)FGS_SIGKILL
指令給當(dāng)前進(jìn)程,完整阻斷動作。send_signal()
函數(shù)是ebpf的內(nèi)置函數(shù),在Kernel 5.3版本[5]里增加。
阻斷演示視頻可以到CNCF (Cloud Native Computing Foundation)的油管觀看:Real Time Security - eBPF for Preventing attacks - Liz Rice, Isovalent[6]
LSM HOOK比較LSM類HOOK
是在Kernel 5.7以后才添加。阻斷功能的實現(xiàn),Tetragon選擇send_signal()
的方式,有著兼容更多內(nèi)核版本的優(yōu)勢。并且其kprobe的HOOK點(diǎn)上,可以實現(xiàn)網(wǎng)關(guān)式通用處理,通過配置方式,更靈活地變更HOOK點(diǎn),避免更新eBPF字節(jié)碼的方式。
- 更靈活
- 網(wǎng)關(guān)式
- 內(nèi)核版本覆蓋多
總結(jié)
Tetragon是一個實時識別阻斷的運(yùn)行時防護(hù)系統(tǒng)。具備網(wǎng)關(guān)式統(tǒng)一處理抓手,可以覆蓋更多內(nèi)核版本,通過配置文件方式靈活變更HOOK點(diǎn)。在eBPF技術(shù)支持下,還具備熱掛載,系統(tǒng)穩(wěn)定性高,程序可靠性高等特點(diǎn)。是主機(jī)運(yùn)行時防護(hù)系統(tǒng)HIDS的最佳學(xué)習(xí)項目。
筆者水平有限,若有錯誤,歡迎指出,謝謝。
Tetragon阻斷爭論
2022年5月,云原生安全公司Isovalent的CTO宣布開源了其內(nèi)部開發(fā)了多年的基于eBPF安全監(jiān)控和阻斷的方案:Tetragon。稱可以防御容器逃逸的Linux內(nèi)核漏洞。
安全研究人員Felix Wilhelm的質(zhì)疑,在Tetragone: A Lesson in Security Fundamentals[7]認(rèn)為可以輕易繞過,并用CVE-2021-22555[8]漏洞修改版演示。
這篇文章從標(biāo)題上都充滿了各種嘲諷,Tetragon
單詞加了e
,大概是gone
的諧音吧。grsecurity
是linux 內(nèi)核安全經(jīng)驗非常深厚的大廠,對這個領(lǐng)域比較精通。但Tetragon
的優(yōu)勢并不是內(nèi)核底層安全能力。
賽博堡壘(HardenedVault)也撰寫一篇文章,云原生安全Tetragon案例之安全產(chǎn)品自防護(hù)[9] 認(rèn)為該產(chǎn)品必定失敗。
隨著云原生的流行,Linux內(nèi)核安全成為了一個無法繞開的問題,某個容器被攻陷后可以向Linux內(nèi)核發(fā)起攻擊,一旦成功則整個主機(jī)都會被攻擊者控制,如果你不想你的產(chǎn)品耗資上百萬美金后攻擊者兩個小時就攻陷的話,那應(yīng)該認(rèn)真的考慮是否應(yīng)該從一開始就建立正確的威脅模型。另外,eBPF機(jī)制更適合實現(xiàn)審計監(jiān)控類的安全方案而非防護(hù)阻斷類,VED的eBPF版本也僅僅是為審計而設(shè)計,剩下的事情你應(yīng)該讓SIEM和SOC團(tuán)隊去做,在安全流程上我們也應(yīng)該遵循KISS(Keep it simple, stupid!)原則,不是嗎?
我的看法
針對漏洞利用的方法(注:不是漏洞)的防御機(jī)制通常會針對三個階段:
- Pre-exploitation(前漏洞利用階段)
- Exploitation(漏洞利用階段)
- Post-exploitation(后漏洞利用階段)
Tetragon的阻斷功能是在Exploitation漏洞利用階段
生效的,因為是可以直接阻斷,讓此次漏洞攻擊失敗。其次,認(rèn)可幾位安全人員的關(guān)于Tetragon
適用場景說法,更適合阻斷用戶空間的內(nèi)核逃逸漏洞。
否定的聲音來自底層防御的傳統(tǒng)廠商,其實他們沒明白,Tetragon
的優(yōu)勢是可以動態(tài)、輕量、無感知的提升防御能力,并不是完全防御所有攻擊方式。
業(yè)務(wù)優(yōu)先于安全
在云原生領(lǐng)域,業(yè)務(wù)類型多數(shù)是web服務(wù),安全級別不需要那么高,硬件宿主機(jī)重啟成本較高,性能要求大,業(yè)務(wù)優(yōu)先,安全其次。過于嚴(yán)格的安全檢測,占用過多資源,影響業(yè)務(wù)運(yùn)行速度,性價比低,成本高。這是云原生場景不能接受的。但偶爾的入侵行為是能容忍的。所以業(yè)務(wù)優(yōu)先級大于安全是第一守則。
安全優(yōu)先于業(yè)務(wù)
傳統(tǒng)安全廠商的產(chǎn)品對系統(tǒng)穩(wěn)定性、可用性、性能都有較大影響,且存在熱更新的難題,哪怕解決了,依舊是特別重的方案。在輕量、動態(tài)、熱更新的需求下,顯得特別笨重。
機(jī)密數(shù)據(jù)庫等保密程度較高的服務(wù)器,數(shù)據(jù)安全大于業(yè)務(wù)功能,愿意犧牲性能換取安全性,那么這種場景適合傳統(tǒng)安全廠商的產(chǎn)品。這種場景的規(guī)則是安全優(yōu)先級大于業(yè)務(wù),更適合傳統(tǒng)安全廠商發(fā)揮。
爭議總結(jié)
當(dāng)今互聯(lián)網(wǎng)的服務(wù)器市場中,云原生業(yè)務(wù)占比越來越高,這將會是Tetragon、Falco、Tracee、Datadog等運(yùn)行時安全產(chǎn)品愈加內(nèi)卷的動力。
對于用戶來說,根據(jù)自己的業(yè)務(wù)特性,選擇相應(yīng)的防御檢測產(chǎn)品,滿足自己業(yè)務(wù)需求。
原文標(biāo)題:Tetragon阻斷爭論
文章出處:【微信公眾號:一口Linux】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
-
內(nèi)核
+關(guān)注
關(guān)注
3文章
1372瀏覽量
40276 -
Linux
+關(guān)注
關(guān)注
87文章
11292瀏覽量
209326 -
云原生
+關(guān)注
關(guān)注
0文章
248瀏覽量
7947
原文標(biāo)題:Tetragon阻斷爭論
文章出處:【微信號:yikoulinux,微信公眾號:一口Linux】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
相關(guān)推薦
評論