Go語言具有開發效率高,運行速度快,跨平臺等優點,因此正越來越多的被攻擊者所使用,其生成的是可直接運行的二進制文件,因此對它的分析類似于普通C語言可執行文件分析,但是又有所不同,本文將會使用正向與逆向結合的方式描述這些區別與特征。
語言特性
1?Compile與Runtime
Go語言類似于C語言,目標是一個二進制文件,逆向的也是native代碼,它有如下特性:
●?強類型檢查的編譯型語言,接近C但擁有原生的包管理,內建的網絡包,協程等使其成為一款開發效率更高的工程級語言。
●?作為編譯型語言它有運行速度快的優點,但是它又能通過內置的運行時符號信息實現反射這種動態特性。
●?作為一種內存安全的語言,它不僅有內建的垃圾回收,還在編譯與運行時提供了大量的安全檢查。
可見盡管它像C編譯的可執行文件但是擁有更復雜的運行時庫,Go通常也是直接將這些庫統一打包成一個文件的,即使用靜態鏈接,因此其程序體積較大,且三方庫、標準庫與用戶代碼混在一起,需要區分,這可以用類似flirt方法做區分(特別是對于做了混淆的程序)。在分析Go語言編寫的二進制程序前,需要弄清楚某一操作是發生在編譯期間還是運行期間,能在編譯時做的事就在編譯時做,這能實現錯誤前移并提高運行效率等,而為了語言的靈活性引入的某些功能又必須在運行時才能確定,在這時就需要想到運行時它應該怎么做,又需要為它提供哪些數據,例如:
func main() { s := [...]string{"hello", "world"} fmt.Printf("%s %s\n", s[0], s[1]) // func Printf(format string, a ...interface{}) (n int, err error)
在第二行定義了一個字符串數組,第三行將其輸出,編譯階段就能確定元素訪問的指令以及下標訪問是否越界,于是就可以去除s的類型信息。但是由于Printf的輸入是interface{}類型,因此在編譯時它無法得知傳入的數據實際為什么類型,但是作為一個輸出函數,希望傳入數字時直接輸出,傳入數組時遍歷輸出每個元素,那么在傳入參數時,就需要在編譯時把實際參數的類型與參數綁定后再傳入Printf,在運行時它就能根據參數綁定的信息確定是什么類型了。其實在編譯時,編譯器做的事還很多,從逆向看只需要注意它會將很多操作轉換為runtime的內建函數調用,這些函數定義在cmd/compile/internal/gc/builtin/runtime.go,并且在src/runtime目錄下對應文件中實現,例如:
a := "123" + b + "321"
將被轉換為concatstring3函數調用:
0x0038 00056 (str.go:4) LEAQ go.string."123"(SB), AX0x003f 00063 (str.go:4) MOVQ AX, 8(SP)0x0044 00068 (str.go:4) MOVQ $3, 16(SP)0x004d 00077 (str.go:4) MOVQ "".b+104(SP), AX0x0052 00082 (str.go:4) MOVQ "".b+112(SP), CX0x0057 00087 (str.go:4) MOVQ AX, 24(SP)0x005c 00092 (str.go:4) MOVQ CX, 32(SP)0x0061 00097 (str.go:4) LEAQ go.string."321"(SB), AX0x0068 00104 (str.go:4) MOVQ AX, 40(SP)0x006d 00109 (str.go:4) MOVQ $3, 48(SP)0x0076 00118 (str.go:4) PCDATA $1, $10x0076 00118 (str.go:4) CALL runtime.concatstring3(SB)
我們將在匯編中看到大量這類函數調用,本文將在對應章節介紹最常見的一些函數。若需要觀察某語法最終編譯后的匯編代碼,除了使用ida等也可以直接使用如下三種方式:
go tool compile -N -l -S once.gogo tool compile -N -l once.go ; go tool objdump -gnu -s Do once.ogo build -gcflags -S once.go
2 動態與類型系統
盡管是編譯型語言,Go仍然提供了一定的動態能力,這主要表現在接口與反射上,而這些能力離不開類型系統,它需要保留必要的類型定義以及對象和類型之間的關聯,這部分內容無法在二進制文件中被去除,否則會影響程序運行,因此在Go逆向時能獲取到大量的符號信息,大大簡化了逆向的難度,對此類信息已有大量文章介紹并有許多優秀的的工具可供使用,例如go_parser與redress,因此本文不再贅述此內容,此處推薦《Go二進制文件逆向分析從基礎到進階——綜述》。
本文將從語言特性上介紹Go語言編寫的二進制文件在匯編下的各種結構,為了表述方便此處定義一些約定:
1. 盡管Go并非面向對象語言,但是本文將Go的類型描述為類,將類型對應的變量描述為類型的實例對象。
2. 本文分析的樣例是x64上的樣本,通篇會對應該平臺敘述,一個機器字認為是64bit。
3. 本文會涉及到Go的參數和匯編層面的參數描述,比如一個復數在Go層面是一個參數,但是它占16字節,在匯編上將會分成兩部分傳遞(不使用xmm時),就認為匯編層面是兩個參數。
4. 一個復雜的實例對象可以分為索引頭和數據部分,它們在內存中分散存儲,下文提到一種數據所占內存大小是指索引頭的大小,因為這部分是逆向關注的點,詳見下文字符串結構。
數據類型
1 數值類型
數值類型很簡單只需要注意其大小即可:

2 字符串string
Go語言中字符串是二進制安全的,它不以\0作為終止符,一個字符串對象在內存中分為兩部分,一部分為如下結構,占兩個機器字用于索引數據:
type StringHeader struct { Data uintptr // 字符串首地址 Len int // 字符串長度}
而它的另一部分才存放真正的數據,它的大小由字符串長度決定,在逆向中重點關注的是如上結構,因此說一個string占兩個機器字,后文其他結構也按這種約定。例如下圖使用printf輸出一個字符串"hello world",它會將上述結構入棧,由于沒有終止符ida無法正常識別字符串結束因此輸出了很多信息,我們需要依靠它的第二個域(此處的長度0x0b)決定它的結束位置:

字符串常見的操作是字符串拼接,若拼接的個數不超過5個會調用concatstringN,否則會直接調用concatstrings,它們聲明如下,可見在多個字符串拼接時參數形式不同:
func concatstring2(*[32]byte, string, string) stringfunc concatstring3(*[32]byte, string, string, string) stringfunc concatstring4(*[32]byte, string, string, string, string) stringfunc concatstring5(*[32]byte, string, string, string, string, string) stringfunc concatstrings(*[32]byte, []string) string
因此在遇到concatstringN時可以跳過第一個參數,隨后入棧的參數即為字符串,而遇到concatstrings時,跳過第一個參數后匯編層面還剩三個參數,其中后兩個一般相同且指明字符串個數,第一個參數則指明字符串數組的首地址,另外經常出現的是string與[]byte之間的轉換,詳見下文slice部分。提醒一下,可能是優化導致一般來說在棧內一個純字符串的兩部分在物理上并沒有連續存放,例如下圖調用macaron的context.Query("username")獲取到的應該是一個代表username的字符串,但是它們并沒有被連續存放:

因此ida中通過定義本地結構體去解析string會遇到困難,其他結構也存在這類情況,氣!
3 數組array
類似C把字符串看作char數組,Go類比如array和string的結構類似,其真實數據也是在內存里連續存放,而使用如下結構索引數據,對數組里的元素訪問其地址偏移在編譯時就能確定,總之逆向角度看它也是占兩個機器字:
type arrayHeader struct { Data uintptr Len int}
數組有三種存儲位置,當數組內元素較少時可以直接存于棧上,較多時存于數據區,而當數據會被返回時會存于堆上。如下定義了三個局部變量,但是它們將在底層表現出不同的形態:
func ArrDemo() *[3]int { a := [...]int{1, 2, 3} b := [...]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 2, 3, 4, 5, 6, 7} c := [...]int{1, 2, 3} if len(a) < len(b) {return &c} return nil}
變量a的匯編如下,它直接在棧上定義并初始化:

變量b的匯編如下,它的初始值被定義在了數據段并進行拷貝初始化:

事實上更常見的拷貝操作會被定義為如下這類函數,因此若符號信息完整遇到無法識別出的函數一般也就是數據拷貝函數:

變量c的匯編如下,盡管它和a的值一樣,但是它的地址會被返回,如果在C語言中這種寫法會造成嚴重的后果,不過Go作為內存安全的語言在編譯時就識別出了該問題(指針逃逸)并將其放在了堆上,此處引出了runtime.newobject函數,該函數傳入的是數據的類型指針,它將在堆上申請空間存放對象實例,返回的是新的對象指針:

經常會遇到的情況是返回一個結構體變量,然后將其賦值給newobject申請的新變量上。
4 切片slice
類似數組,切片的實例對象數據結構如下,可知它占用了三個機器字,與它相關的函數是growslice表示擴容,逆向時可忽略:
type SliceHeader struct { Data uintptr // 數據指針 Len int // 當前長度 Cap int // 可容納的長度}
更常見的函數是與字符串相關的轉換,它們在底層調用的是如下函數,此處我們依然不必關注第一個參數:
func slicebytetostring(buf *[32]byte, ptr *byte, n int) stringfunc stringtoslicebyte(*[32]byte, string) []byte
例如下圖:

可見傳入的是兩個參數代表一個string,返回了三個數據代表一個[]byte。
5 字典map
字典實現比較復雜,不過在逆向中會涉及到的內容很簡單,字典操作常見的會轉換為如下函數,一般fastrand和makemap連用返回一個map,它為一個指針,讀字典時使用mapaccess1和mapaccess2,后者是使用,ok語法時生成的函數,runtime里還有很多以2結尾的函數代表同樣的含義,后文不再贅述。寫字典時會使用mapassign函數,它返回一個地址,將value寫入該地址,另外還比較常見的是對字典進行遍歷,會使用mapiterinit和mapiternext配合:
func fastrand() uint32func makemap(mapType *byte, hint int, mapbuf *any) (hmap map[any]any)func mapaccess1(mapType *byte, hmap map[any]any, key *any) (val *any)func mapaccess2(mapType *byte, hmap map[any]any, key *any) (val *any, pres bool)func mapassign(mapType *byte, hmap map[any]any, key *any) (val *any)func mapiterinit(mapType *byte, hmap map[any]any, hiter *any)func mapiternext(hiter *any)
事實上更常見的是上面這些函數的同類函數,它們的后綴代表了對特定類型的優化,例如如下代碼,它首先調用makemap_small創建了一個小字典并將其指針存于棧上,之后調用mapassign_faststr傳入一個字符串鍵并獲取一個槽,之后將數據寫入返回的槽地址里,這里就是一個創建字典并賦值的過程:

如下是訪問字典里數據的情況,調用mapaccess1_fast32傳入了一個32位的數字作為鍵:

可以看到mapaccess和mapassign的第一個參數代表字典的類型,因此能很容易知道字典操作參數和返回值的類型。
6 結構體struct
類似于C語言,Go的結構體也是由其他類型組成的復合結構,它里面域的順序也是定義的順序,里面的數據對齊規則和C一致不過我們可以直接從其類型信息獲得,不必自己算。在分析結構體變量時必須要了解結構體的類型結構了,其定義如下:
type rtype struct { size uintptr // 該類型對象實例的大小 ptrdata uintptr // number of bytes in the type that can contain pointers hash uint32 // hash of type; avoids computation in hash tables tflag tflag // extra type information flags align uint8 // alignment of variable with this type fieldAlign uint8 // alignment of struct field with this type kind uint8 // enumeration for C alg *typeAlg // algorithm table gcdata *byte // garbage collection data str nameOff // 名稱 ptrToThis typeOff // 指向該類型的指針,如該類為Person,代碼中使用到*Person時,后者也是一種新的類型,它是指針但是所指對象屬于Person類,后者的類型位置存于此處}type structField struct { name name // 屬性名稱 typ *rtype // 該域的類型 offsetEmbed uintptr // 該屬性在對象中的偏移左移一位后與是否是嵌入類型的或,即offsetEmbed>>1得到該屬性在對象中的偏移}type structType struct { rtype pkgPath name // 包名 fields []structField // 域數組}type uncommonType struct { pkgPath nameOff // 包路徑 mcount uint16 // 方法數 xcount uint16 // 導出的方法數 moff uint32 // 方法數組的偏移,方法表也是有需的,先導出方法后私有方法,而其內部按名稱字符串排序 _ uint32 // unused}type structTypeUncommon struct { structType u uncommonType}
如下為macaron的Context結構體的類型信息,可見它的實例對象占了0x90字節,這實際上會和下面fields中對象所占空間對應:

通過macaron_Context_struct_fields可轉到每個域的定義,可見其域名稱域類型,偏移等:

結構體類型作為自定義類型除了域之外,方法也很重要,這部分在后文會提到。
7 接口interface
接口和反射息息相關,接口對象會包含實例對象類型信息與數據信息。這里需要分清幾個概念,一般我們是定義一種接口類型,再定義一種數據類型,并且在這種數據類型上實現一些方法,Go使用了類似鴨子類型,只要定義的數據類型實現了某個接口定義的全部方法則認為實現了該接口。前面提到的兩個是類型,在程序運行過程中對應的是類型的實例對象,一般是將實例對象賦值給某接口,這可以發生在兩個階段,此處主要關注運行時階段,這里在匯編上會看到如下函數:
// Type to empty-interface conversion.func convT2E(typ *byte, elem *any) (ret any)// Type to non-empty-interface conversion.func convT2I(tab *byte, elem *any) (ret any)
如上轉換后的結果就是接口類型的實例對象,此處先看第二個函數,它生成的對象數據結構如下,其中itab結構體包含接口類型,轉換為接口前的實例對象的類型,以及接口的函數表等,而word是指向原對象數據的指針,逆向時主要關注word字段和itab的fun字段,fun字段是函數指針數組,它里元素的順序并非接口內定義的順序,而是名稱字符串排序,因此對照源碼分析時需要先排序才能根據偏移確定實際調用的函數:
type nonEmptyInterface struct { // see ../runtime/iface.c:/Itab itab *struct { ityp *rtype // 代表的接口的類型,靜態static interface type typ *rtype // 對象實例真實的類型,運行時確定dynamic concrete type link unsafe.Pointer bad int32 unused int32 fun [100000]unsafe.Pointer // 方法表,具體大小由接口定義確定 } word unsafe.Pointer}
這是舊版Go的實現,在較新的版本中此結構定義如下,在新版中它的起始位置偏移是0x18,因此我們可以直接通過調用偏移減0x18除以8獲取調用的是第幾個方法:
type nonEmptyInterface struct { // see ../runtime/iface.go:/Itab itab *struct { ityp *rtype // static interface type typ *rtype // dynamic concrete type hash uint32 // copy of typ.hash _ [4]byte fun [100000]unsafe.Pointer // method table } word unsafe.Pointer}
上面講的是第二個函數的作用,解釋第一個函數需要引入一種特殊的接口,即空接口,由于這種接口未定義任何方法,那么可以認為所有對象都實現了該接口,因此它可以作為所有對象的容器,在底層它和其他接口也擁有不同的數據結構,空接口的對象數據結構如下:
// emptyInterface is the header for an interface{} value.type emptyInterface struct { typ *rtype // 對象實例真實的類型指針 word unsafe.Pointer // 對象實例的數據指針}
可見空接口兩個域剛好指明原始對象的類型和數據域,而且所有接口對象是占用兩個個機器字,另外常見的接口函數如下:
// Non-empty-interface to non-empty-interface conversion.func convI2I(typ *byte, elem any) (ret any)// interface type assertions x.(T)func assertE2I(typ *byte, iface any) (ret any)func assertI2I(typ *byte, iface any) (ret any)
例如存在如下匯編代碼:

可以知道convI2I的結果是第一行所指定接口類型對應的接口對象,在最后一行它調用了itab+30h處的函數,根據計算可知是字母序后的第4個函數,這里可以直接查看接口的類型定義,獲知第四個函數:

語法特征
1 創建對象
Go不是面向對象的,此處將Go的變量當做對象來描述。函數調用棧作為一種結構簡單的數據結構可以輕易高效的管理局部變量并實現垃圾回收,因此新建對象也優先使用指令在棧上分配空間,當指針需要逃逸或者動態創建時會在堆區創建對象,這里涉及make和new兩個關鍵詞,不過在匯編層面它們分別對應著makechan,makemap,makeslice與newobject,由于本文沒有介紹channel故不提它,剩下的makemap和newobject上文已經提了,還剩makeslice,它的定義如下:
func?makeslice(et *_type, len, cap?int) unsafe.Pointer
如下,調用make([]uint8, 5,10)創建一個slice后,會生成此代碼:

2 函數與方法
2.1 棧空間
棧可以分為兩個區域,在棧底部存放局部變量,棧頂部做函數調用相關的參數與返回值傳遞,因此在分析時不能對頂部的var命名,因為它不特指某具體變量而是隨時在變化的,錯誤的命名容易造成混淆,如下圖,0xE60距0xEC0足夠遠,因此此處很大概率是局部變量可重命名,而0xEB8距棧頂很近,很大概率是用于傳參的,不要重命名:

2.2 變參
類似Python的一般變參實際被轉換為一個tuple,Go變參也被轉換為了一個slice,因此一個變參在匯編級別占3個參數位,如下代碼:
func VarArgDemo(args ...int) (sum int) {}func main() { VarArgDemo(1, 2, 3)}
它會被編譯為如下形式:

這里先將1,2,3保存到rsp+80h+var_30開始的位置,然后將其首地址、長度(3)、容量(3)放到棧上,之后調用VarArgDeme函數。
2.3 匿名函數
匿名函數通常會以外部函數名_funcX來命名,除此之外和普通函數沒什么不同,只是需要注意若使用了外部變量,即形成閉包時,這些變量會以引用形式傳入,如在os/exec/exec.go中如下代碼:?
go func() { select { case c.ctx. c.Process.Kill() case c.waitDone: } }()
其中c是外部變量,它在調用時會以參數形式傳入(newproc請見后文協程部分):

而在io/pipe.go中的如下代碼:
func (p *pipe) CloseRead(err error) error { if err == nil { err = ErrClosedPipe } p.rerr.Store(err) p.once.Do(func() { close(p.done) }) return nil}
其中p是外部變量,它在調用時是將其存入外部寄存器(rdx)傳入的:

可見在使用到外部變量時它們會作為引用被傳入并使用。
2.4 方法
Go可以為任意自定義類型綁定方法,方法將會被轉換為普通函數,并且將方法的接收者轉化為第一個參數,再看看上文結構體處的圖:

如上可見Context含44個導出方法,3個未導出方法,位置已經被計算出在0xcdbaa8,因此可轉到方法定義數組:


如上可見,首先是可導出方法,它們按照名稱升序排序,之后是未導出方法,它們也是按名稱升序排序,另外導出方法有完整的函數簽名,而未導出方法只有函數名稱。在逆向時不必關心這一部分結構,解析工具會自動將對應的函數調用重命名,此處僅了解即可。
在逆向時工具會將其解析為類型名__方法名或類型名_方法名,因此遇到此類名稱時我們需要注意它的第一個參數是隱含參數,類似C++的this指針,但Go的方法定義不僅支持傳引用,也支持傳值,因此第一個參數可能在匯編層面不只占一個機器字,如:
type Person struct { name string age int weight uint16 height uint16}func (p Person) Print() { fmt.Printf("%t\n", p)}func (p *Person) PPrint() { fmt.Printf("%t\n", p)}func main(){ lihua := Person{ name: "lihua", age: 18, weight: 60, height: 160, } lihua.Print() lihua.PPrint()}
編譯后如下所示:

根據定義兩個方法都沒有參數,但是從匯編看它們都有參數,如注釋,在逆向時是更常見的是像PPrint這種方法,即第一個參數是對象的指針。
2.5 函數反射
函數在普通使用和反射使用時,被保存的信息不相同,普通使用不需要保存函數簽名,而反射會保存,更利于分析,如下代碼:
//go:noinlinefunc Func1(b string, a int) bool { return a < len(b)}//go:noinlinefunc Func2(a int, b string) bool { return a < len(b)}func main(){ fmt.Println(Func1("233", 2)) v := reflect.ValueOf(Func2) fmt.Println(v.Kind()==reflect.Func)}
編譯后通過字符串搜索,可定位到被反射的函數簽名(當然在逆向中并不知道應該搜什么,而是在函數周圍尋找簽名):


而普通函數的簽名無法被搜到:

3 伸縮棧
由于go可以擁有大量的協程,若使用固定大小的棧將會造成內存空間浪費,因此它使用伸縮棧,初始時一個普通協程只分配幾KB的棧,并在函數執行前先判斷棧空間是否足夠,若不夠則通過一些方式擴展棧,這在匯編上的表現形式如下:

在調用runtime·morestack*函數擴展棧后會重新進入函數并進入左側分支,因此在分析時直接忽略右側分支即可。
4 調用約定
Go統一通過棧傳遞參數和返回值,這些空間由調用者維護,返回值內存會在調用前選擇性的被初始化,而參數傳遞是從左到右順序,在內存中從下到上寫入棧,因此看到mov [rsp + 0xXX + var_XX], reg(棧頂)時就代表開始為函數調用準備參數了,繼續向下就能確定函數的參數個數及內容:

如圖,mov [rsp+108h+v_108], rdx即表示開始向棧上傳第一個參數了,從此處到call指令前都是傳參,此處可見在匯編層面傳了3個參數,其中第2個和第3個參數為Go語言里的第二個參數,call指令之后為返回值,不過可能存在返回值未使用的情況,因此返回值的個數和含義需要從函數內部分析,比如此處的Query我們已知arg_0/arg_8/arg_10為參數,那么剩下的arg18/arg20即為返回值:

需要注意的是不能僅靠函數頭部就斷定參數個數,例如當參數為一個結構體時,可能頭部的argX只代表了其首位的地址,因此需要具體分析函數retn指令前的指令來確定返回值大小。
5 寫屏障
Go擁有垃圾回收,其三色標記法使用了寫屏障的方法保證一致性,在垃圾收集過程中會將寫屏障標志置位,此時會進入另一條邏輯,但是我們在逆向分析過程中可以認為該位未置位而直接分析無保護的情況:

如上圖,先判斷標志,再決定是否進入,在分析時可以直接認為其永假并走左側分支。
6 協程go
使用go關鍵詞可以創建并運行協程,它在匯編上會被表現為由runtime_newproc(fn,args?),它會封裝函數與參數并創建協程執行信息,并在適當時候被執行,如:

這里執行了go loop(),由于沒有參數此處newproc只被傳入了函數指針這一個參數,否則會傳入繼續傳入函數所需的參數,在分析時直接將函數作為在新的線程里執行即可。
7 延遲執行defer
延遲執行一般用于資源釋放,它會先注冊到鏈表中并在當前調用棧返回前執行所有鏈表中注冊的函數,在匯編層面會表現為runtime_deferproc,例如常見的鎖釋放操作:

這里它第一個參數代表延遲函數參數字節大小為8字節,第二個參數為函數指針,第三個參數為延遲執行函數的參數,若創建失敗會直接返回,返回前會調用runtime_deferreturn去執行其他創建的延遲執行函數,一般我們是不需要關注該語句的,因此可以直接跳過相關指令并向左側繼續分析。
8 調用c庫cgo
Go可以調用C代碼,但調用C會存在運行時不一致,Go統一將C調用看作系統調用來處理調度等問題,另一方類型不一致才是我們需要關注的重點,為了解決類型與命名空間等問題cgo會為C生成樁代碼來橋接Go,于是這類函數在Go語言側表現為XXX_CFunc__YYY,它封裝參數并調用runtime_cgocall轉換狀態,在中間表示為NNN_cgo_abcdef123456_CFunc__ZZZ,這里它解包參數并調用實際c函數,例如:

此處它調用了libc的void* realloc(void*, newsize),在Go側它封裝成了os_user__Cfunc_realloc,在該函數內部參數被封裝成了結構體并作為指針與函數指針一起被傳入了cgocall,而函數指針即_cgo_3298b262a8f6_Cfunc_realloc為中間層負責解包參數等并調用真正的C函數:

9 其他
還有些內容,如看到以panic開頭的分支不分析等不再演示,分析時遇到不認識的三方庫函數和標準庫函數直接看源碼即可。
參考鏈接
https://draveness.me/golang/
https://tiancaiamao.gitbooks.io/go-internals/content/zh/02.3.html
https://www.pnfsoftware.com/blog/analyzing-golang-executables/
https://rednaga.io/2016/09/21/reversing_go_binaries_like_a_pro/
https://research.swtch.com/interfaces
推薦閱讀:
?【高級持續性威脅追蹤】Purple Fox 新變種講述從釣魚網站到Rootkit的故事
【漏洞通告】FasterXML Jackson-databind多個反序列化漏洞
【漏洞通告】Apache Flink文件寫入與任意文件讀取漏洞(CVE-2020-17518/CVE-2020-17519)
【漏洞通告】Zyxel多個設備密碼硬編碼漏洞(CVE-2020-29583)
【高級持續性威脅追蹤】來自Mustang Panda的攻擊? ?我兔又背鍋了!

深信服科技旗下安全實驗室,致力于網絡安全攻防技術的研究和積累,深度洞察未知網絡安全威脅,解讀前沿安全技術。
●?掃碼關注我們