go conn 读取byte数组后是否要_【技术推荐】正向角度看Go逆向
Go語言具有開發(fā)效率高,運(yùn)行速度快,跨平臺(tái)等優(yōu)點(diǎn),因此正越來越多的被攻擊者所使用,其生成的是可直接運(yùn)行的二進(jìn)制文件,因此對(duì)它的分析類似于普通C語言可執(zhí)行文件分析,但是又有所不同,本文將會(huì)使用正向與逆向結(jié)合的方式描述這些區(qū)別與特征。
語言特性
1?Compile與Runtime
Go語言類似于C語言,目標(biāo)是一個(gè)二進(jìn)制文件,逆向的也是native代碼,它有如下特性:
●?強(qiáng)類型檢查的編譯型語言,接近C但擁有原生的包管理,內(nèi)建的網(wǎng)絡(luò)包,協(xié)程等使其成為一款開發(fā)效率更高的工程級(jí)語言。
●?作為編譯型語言它有運(yùn)行速度快的優(yōu)點(diǎn),但是它又能通過內(nèi)置的運(yùn)行時(shí)符號(hào)信息實(shí)現(xiàn)反射這種動(dòng)態(tài)特性。
●?作為一種內(nèi)存安全的語言,它不僅有內(nèi)建的垃圾回收,還在編譯與運(yùn)行時(shí)提供了大量的安全檢查。
可見盡管它像C編譯的可執(zhí)行文件但是擁有更復(fù)雜的運(yùn)行時(shí)庫(kù),Go通常也是直接將這些庫(kù)統(tǒng)一打包成一個(gè)文件的,即使用靜態(tài)鏈接,因此其程序體積較大,且三方庫(kù)、標(biāo)準(zhǔn)庫(kù)與用戶代碼混在一起,需要區(qū)分,這可以用類似flirt方法做區(qū)分(特別是對(duì)于做了混淆的程序)。在分析Go語言編寫的二進(jìn)制程序前,需要弄清楚某一操作是發(fā)生在編譯期間還是運(yùn)行期間,能在編譯時(shí)做的事就在編譯時(shí)做,這能實(shí)現(xiàn)錯(cuò)誤前移并提高運(yùn)行效率等,而為了語言的靈活性引入的某些功能又必須在運(yùn)行時(shí)才能確定,在這時(shí)就需要想到運(yùn)行時(shí)它應(yīng)該怎么做,又需要為它提供哪些數(shù)據(jù),例如:
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)在第二行定義了一個(gè)字符串?dāng)?shù)組,第三行將其輸出,編譯階段就能確定元素訪問的指令以及下標(biāo)訪問是否越界,于是就可以去除s的類型信息。但是由于Printf的輸入是interface{}類型,因此在編譯時(shí)它無法得知傳入的數(shù)據(jù)實(shí)際為什么類型,但是作為一個(gè)輸出函數(shù),希望傳入數(shù)字時(shí)直接輸出,傳入數(shù)組時(shí)遍歷輸出每個(gè)元素,那么在傳入?yún)?shù)時(shí),就需要在編譯時(shí)把實(shí)際參數(shù)的類型與參數(shù)綁定后再傳入Printf,在運(yùn)行時(shí)它就能根據(jù)參數(shù)綁定的信息確定是什么類型了。其實(shí)在編譯時(shí),編譯器做的事還很多,從逆向看只需要注意它會(huì)將很多操作轉(zhuǎn)換為runtime的內(nèi)建函數(shù)調(diào)用,這些函數(shù)定義在cmd/compile/internal/gc/builtin/runtime.go,并且在src/runtime目錄下對(duì)應(yīng)文件中實(shí)現(xiàn),例如:
a := "123" + b + "321"將被轉(zhuǎn)換為concatstring3函數(shù)調(diào)用:
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)我們將在匯編中看到大量這類函數(shù)調(diào)用,本文將在對(duì)應(yīng)章節(jié)介紹最常見的一些函數(shù)。若需要觀察某語法最終編譯后的匯編代碼,除了使用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.go2 動(dòng)態(tài)與類型系統(tǒng)
盡管是編譯型語言,Go仍然提供了一定的動(dòng)態(tài)能力,這主要表現(xiàn)在接口與反射上,而這些能力離不開類型系統(tǒng),它需要保留必要的類型定義以及對(duì)象和類型之間的關(guān)聯(lián),這部分內(nèi)容無法在二進(jìn)制文件中被去除,否則會(huì)影響程序運(yùn)行,因此在Go逆向時(shí)能獲取到大量的符號(hào)信息,大大簡(jiǎn)化了逆向的難度,對(duì)此類信息已有大量文章介紹并有許多優(yōu)秀的的工具可供使用,例如go_parser與redress,因此本文不再贅述此內(nèi)容,此處推薦《Go二進(jìn)制文件逆向分析從基礎(chǔ)到進(jìn)階——綜述》。
本文將從語言特性上介紹Go語言編寫的二進(jìn)制文件在匯編下的各種結(jié)構(gòu),為了表述方便此處定義一些約定:
1. 盡管Go并非面向?qū)ο笳Z言,但是本文將Go的類型描述為類,將類型對(duì)應(yīng)的變量描述為類型的實(shí)例對(duì)象。
2. 本文分析的樣例是x64上的樣本,通篇會(huì)對(duì)應(yīng)該平臺(tái)敘述,一個(gè)機(jī)器字認(rèn)為是64bit。
3. 本文會(huì)涉及到Go的參數(shù)和匯編層面的參數(shù)描述,比如一個(gè)復(fù)數(shù)在Go層面是一個(gè)參數(shù),但是它占16字節(jié),在匯編上將會(huì)分成兩部分傳遞(不使用xmm時(shí)),就認(rèn)為匯編層面是兩個(gè)參數(shù)。
4. 一個(gè)復(fù)雜的實(shí)例對(duì)象可以分為索引頭和數(shù)據(jù)部分,它們?cè)趦?nèi)存中分散存儲(chǔ),下文提到一種數(shù)據(jù)所占內(nèi)存大小是指索引頭的大小,因?yàn)檫@部分是逆向關(guān)注的點(diǎn),詳見下文字符串結(jié)構(gòu)。
數(shù)據(jù)類型
1 數(shù)值類型
數(shù)值類型很簡(jiǎn)單只需要注意其大小即可:
2 字符串string
Go語言中字符串是二進(jìn)制安全的,它不以\0作為終止符,一個(gè)字符串對(duì)象在內(nèi)存中分為兩部分,一部分為如下結(jié)構(gòu),占兩個(gè)機(jī)器字用于索引數(shù)據(jù):
type StringHeader struct { Data uintptr // 字符串首地址 Len int // 字符串長(zhǎng)度}而它的另一部分才存放真正的數(shù)據(jù),它的大小由字符串長(zhǎng)度決定,在逆向中重點(diǎn)關(guān)注的是如上結(jié)構(gòu),因此說一個(gè)string占兩個(gè)機(jī)器字,后文其他結(jié)構(gòu)也按這種約定。例如下圖使用printf輸出一個(gè)字符串"hello world",它會(huì)將上述結(jié)構(gòu)入棧,由于沒有終止符ida無法正常識(shí)別字符串結(jié)束因此輸出了很多信息,我們需要依靠它的第二個(gè)域(此處的長(zhǎng)度0x0b)決定它的結(jié)束位置:
字符串常見的操作是字符串拼接,若拼接的個(gè)數(shù)不超過5個(gè)會(huì)調(diào)用concatstringN,否則會(huì)直接調(diào)用concatstrings,它們聲明如下,可見在多個(gè)字符串拼接時(shí)參數(shù)形式不同:
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時(shí)可以跳過第一個(gè)參數(shù),隨后入棧的參數(shù)即為字符串,而遇到concatstrings時(shí),跳過第一個(gè)參數(shù)后匯編層面還剩三個(gè)參數(shù),其中后兩個(gè)一般相同且指明字符串個(gè)數(shù),第一個(gè)參數(shù)則指明字符串?dāng)?shù)組的首地址,另外經(jīng)常出現(xiàn)的是string與[]byte之間的轉(zhuǎn)換,詳見下文slice部分。提醒一下,可能是優(yōu)化導(dǎo)致一般來說在棧內(nèi)一個(gè)純字符串的兩部分在物理上并沒有連續(xù)存放,例如下圖調(diào)用macaron的context.Query("username")獲取到的應(yīng)該是一個(gè)代表username的字符串,但是它們并沒有被連續(xù)存放:
因此ida中通過定義本地結(jié)構(gòu)體去解析string會(huì)遇到困難,其他結(jié)構(gòu)也存在這類情況,氣!
3 數(shù)組array
類似C把字符串看作char數(shù)組,Go類比如array和string的結(jié)構(gòu)類似,其真實(shí)數(shù)據(jù)也是在內(nèi)存里連續(xù)存放,而使用如下結(jié)構(gòu)索引數(shù)據(jù),對(duì)數(shù)組里的元素訪問其地址偏移在編譯時(shí)就能確定,總之逆向角度看它也是占兩個(gè)機(jī)器字:
type arrayHeader struct { Data uintptr Len int}數(shù)組有三種存儲(chǔ)位置,當(dāng)數(shù)組內(nèi)元素較少時(shí)可以直接存于棧上,較多時(shí)存于數(shù)據(jù)區(qū),而當(dāng)數(shù)據(jù)會(huì)被返回時(shí)會(huì)存于堆上。如下定義了三個(gè)局部變量,但是它們將在底層表現(xiàn)出不同的形態(tài):
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的匯編如下,它的初始值被定義在了數(shù)據(jù)段并進(jìn)行拷貝初始化:
事實(shí)上更常見的拷貝操作會(huì)被定義為如下這類函數(shù),因此若符號(hào)信息完整遇到無法識(shí)別出的函數(shù)一般也就是數(shù)據(jù)拷貝函數(shù):
變量c的匯編如下,盡管它和a的值一樣,但是它的地址會(huì)被返回,如果在C語言中這種寫法會(huì)造成嚴(yán)重的后果,不過Go作為內(nèi)存安全的語言在編譯時(shí)就識(shí)別出了該問題(指針逃逸)并將其放在了堆上,此處引出了runtime.newobject函數(shù),該函數(shù)傳入的是數(shù)據(jù)的類型指針,它將在堆上申請(qǐng)空間存放對(duì)象實(shí)例,返回的是新的對(duì)象指針:
經(jīng)常會(huì)遇到的情況是返回一個(gè)結(jié)構(gòu)體變量,然后將其賦值給newobject申請(qǐng)的新變量上。
4 切片slice
類似數(shù)組,切片的實(shí)例對(duì)象數(shù)據(jù)結(jié)構(gòu)如下,可知它占用了三個(gè)機(jī)器字,與它相關(guān)的函數(shù)是growslice表示擴(kuò)容,逆向時(shí)可忽略:
type SliceHeader struct { Data uintptr // 數(shù)據(jù)指針 Len int // 當(dāng)前長(zhǎng)度 Cap int // 可容納的長(zhǎng)度}更常見的函數(shù)是與字符串相關(guān)的轉(zhuǎn)換,它們?cè)诘讓诱{(diào)用的是如下函數(shù),此處我們依然不必關(guān)注第一個(gè)參數(shù):
func slicebytetostring(buf *[32]byte, ptr *byte, n int) stringfunc stringtoslicebyte(*[32]byte, string) []byte例如下圖:
可見傳入的是兩個(gè)參數(shù)代表一個(gè)string,返回了三個(gè)數(shù)據(jù)代表一個(gè)[]byte。
5 字典map
字典實(shí)現(xiàn)比較復(fù)雜,不過在逆向中會(huì)涉及到的內(nèi)容很簡(jiǎn)單,字典操作常見的會(huì)轉(zhuǎn)換為如下函數(shù),一般fastrand和makemap連用返回一個(gè)map,它為一個(gè)指針,讀字典時(shí)使用mapaccess1和mapaccess2,后者是使用,ok語法時(shí)生成的函數(shù),runtime里還有很多以2結(jié)尾的函數(shù)代表同樣的含義,后文不再贅述。寫字典時(shí)會(huì)使用mapassign函數(shù),它返回一個(gè)地址,將value寫入該地址,另外還比較常見的是對(duì)字典進(jìn)行遍歷,會(huì)使用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)事實(shí)上更常見的是上面這些函數(shù)的同類函數(shù),它們的后綴代表了對(duì)特定類型的優(yōu)化,例如如下代碼,它首先調(diào)用makemap_small創(chuàng)建了一個(gè)小字典并將其指針存于棧上,之后調(diào)用mapassign_faststr傳入一個(gè)字符串鍵并獲取一個(gè)槽,之后將數(shù)據(jù)寫入返回的槽地址里,這里就是一個(gè)創(chuàng)建字典并賦值的過程:
如下是訪問字典里數(shù)據(jù)的情況,調(diào)用mapaccess1_fast32傳入了一個(gè)32位的數(shù)字作為鍵:
可以看到mapaccess和mapassign的第一個(gè)參數(shù)代表字典的類型,因此能很容易知道字典操作參數(shù)和返回值的類型。
6 結(jié)構(gòu)體struct
類似于C語言,Go的結(jié)構(gòu)體也是由其他類型組成的復(fù)合結(jié)構(gòu),它里面域的順序也是定義的順序,里面的數(shù)據(jù)對(duì)齊規(guī)則和C一致不過我們可以直接從其類型信息獲得,不必自己算。在分析結(jié)構(gòu)體變量時(shí)必須要了解結(jié)構(gòu)體的類型結(jié)構(gòu)了,其定義如下:
type rtype struct { size uintptr // 該類型對(duì)象實(shí)例的大小 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時(shí),后者也是一種新的類型,它是指針但是所指對(duì)象屬于Person類,后者的類型位置存于此處}type structField struct { name name // 屬性名稱 typ *rtype // 該域的類型 offsetEmbed uintptr // 該屬性在對(duì)象中的偏移左移一位后與是否是嵌入類型的或,即offsetEmbed>>1得到該屬性在對(duì)象中的偏移}type structType struct { rtype pkgPath name // 包名 fields []structField // 域數(shù)組}type uncommonType struct { pkgPath nameOff // 包路徑 mcount uint16 // 方法數(shù) xcount uint16 // 導(dǎo)出的方法數(shù) moff uint32 // 方法數(shù)組的偏移,方法表也是有需的,先導(dǎo)出方法后私有方法,而其內(nèi)部按名稱字符串排序 _ uint32 // unused}type structTypeUncommon struct { structType u uncommonType}如下為macaron的Context結(jié)構(gòu)體的類型信息,可見它的實(shí)例對(duì)象占了0x90字節(jié),這實(shí)際上會(huì)和下面fields中對(duì)象所占空間對(duì)應(yīng):
通過macaron_Context_struct_fields可轉(zhuǎn)到每個(gè)域的定義,可見其域名稱域類型,偏移等:
結(jié)構(gòu)體類型作為自定義類型除了域之外,方法也很重要,這部分在后文會(huì)提到。
7 接口interface
接口和反射息息相關(guān),接口對(duì)象會(huì)包含實(shí)例對(duì)象類型信息與數(shù)據(jù)信息。這里需要分清幾個(gè)概念,一般我們是定義一種接口類型,再定義一種數(shù)據(jù)類型,并且在這種數(shù)據(jù)類型上實(shí)現(xiàn)一些方法,Go使用了類似鴨子類型,只要定義的數(shù)據(jù)類型實(shí)現(xiàn)了某個(gè)接口定義的全部方法則認(rèn)為實(shí)現(xiàn)了該接口。前面提到的兩個(gè)是類型,在程序運(yùn)行過程中對(duì)應(yīng)的是類型的實(shí)例對(duì)象,一般是將實(shí)例對(duì)象賦值給某接口,這可以發(fā)生在兩個(gè)階段,此處主要關(guān)注運(yùn)行時(shí)階段,這里在匯編上會(huì)看到如下函數(shù):
// 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)如上轉(zhuǎn)換后的結(jié)果就是接口類型的實(shí)例對(duì)象,此處先看第二個(gè)函數(shù),它生成的對(duì)象數(shù)據(jù)結(jié)構(gòu)如下,其中itab結(jié)構(gòu)體包含接口類型,轉(zhuǎn)換為接口前的實(shí)例對(duì)象的類型,以及接口的函數(shù)表等,而word是指向原對(duì)象數(shù)據(jù)的指針,逆向時(shí)主要關(guān)注word字段和itab的fun字段,fun字段是函數(shù)指針數(shù)組,它里元素的順序并非接口內(nèi)定義的順序,而是名稱字符串排序,因此對(duì)照源碼分析時(shí)需要先排序才能根據(jù)偏移確定實(shí)際調(diào)用的函數(shù):
type nonEmptyInterface struct { // see ../runtime/iface.c:/Itab itab *struct { ityp *rtype // 代表的接口的類型,靜態(tài)static interface type typ *rtype // 對(duì)象實(shí)例真實(shí)的類型,運(yùn)行時(shí)確定dynamic concrete type link unsafe.Pointer bad int32 unused int32 fun [100000]unsafe.Pointer // 方法表,具體大小由接口定義確定 } word unsafe.Pointer}這是舊版Go的實(shí)現(xiàn),在較新的版本中此結(jié)構(gòu)定義如下,在新版中它的起始位置偏移是0x18,因此我們可以直接通過調(diào)用偏移減0x18除以8獲取調(diào)用的是第幾個(gè)方法:
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}上面講的是第二個(gè)函數(shù)的作用,解釋第一個(gè)函數(shù)需要引入一種特殊的接口,即空接口,由于這種接口未定義任何方法,那么可以認(rèn)為所有對(duì)象都實(shí)現(xiàn)了該接口,因此它可以作為所有對(duì)象的容器,在底層它和其他接口也擁有不同的數(shù)據(jù)結(jié)構(gòu),空接口的對(duì)象數(shù)據(jù)結(jié)構(gòu)如下:
// emptyInterface is the header for an interface{} value.type emptyInterface struct { typ *rtype // 對(duì)象實(shí)例真實(shí)的類型指針 word unsafe.Pointer // 對(duì)象實(shí)例的數(shù)據(jù)指針}可見空接口兩個(gè)域剛好指明原始對(duì)象的類型和數(shù)據(jù)域,而且所有接口對(duì)象是占用兩個(gè)個(gè)機(jī)器字,另外常見的接口函數(shù)如下:
// 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的結(jié)果是第一行所指定接口類型對(duì)應(yīng)的接口對(duì)象,在最后一行它調(diào)用了itab+30h處的函數(shù),根據(jù)計(jì)算可知是字母序后的第4個(gè)函數(shù),這里可以直接查看接口的類型定義,獲知第四個(gè)函數(shù):
語法特征
1 創(chuàng)建對(duì)象
Go不是面向?qū)ο蟮?#xff0c;此處將Go的變量當(dāng)做對(duì)象來描述。函數(shù)調(diào)用棧作為一種結(jié)構(gòu)簡(jiǎn)單的數(shù)據(jù)結(jié)構(gòu)可以輕易高效的管理局部變量并實(shí)現(xiàn)垃圾回收,因此新建對(duì)象也優(yōu)先使用指令在棧上分配空間,當(dāng)指針需要逃逸或者動(dòng)態(tài)創(chuàng)建時(shí)會(huì)在堆區(qū)創(chuàng)建對(duì)象,這里涉及make和new兩個(gè)關(guān)鍵詞,不過在匯編層面它們分別對(duì)應(yīng)著makechan,makemap,makeslice與newobject,由于本文沒有介紹channel故不提它,剩下的makemap和newobject上文已經(jīng)提了,還剩makeslice,它的定義如下:
func?makeslice(et *_type, len, cap?int) unsafe.Pointer
如下,調(diào)用make([]uint8, 5,10)創(chuàng)建一個(gè)slice后,會(huì)生成此代碼:
2 函數(shù)與方法
2.1 棧空間
棧可以分為兩個(gè)區(qū)域,在棧底部存放局部變量,棧頂部做函數(shù)調(diào)用相關(guān)的參數(shù)與返回值傳遞,因此在分析時(shí)不能對(duì)頂部的var命名,因?yàn)樗惶刂改尘唧w變量而是隨時(shí)在變化的,錯(cuò)誤的命名容易造成混淆,如下圖,0xE60距0xEC0足夠遠(yuǎn),因此此處很大概率是局部變量可重命名,而0xEB8距棧頂很近,很大概率是用于傳參的,不要重命名:
2.2 變參
類似Python的一般變參實(shí)際被轉(zhuǎn)換為一個(gè)tuple,Go變參也被轉(zhuǎn)換為了一個(gè)slice,因此一個(gè)變參在匯編級(jí)別占3個(gè)參數(shù)位,如下代碼:
func VarArgDemo(args ...int) (sum int) {}func main() { VarArgDemo(1, 2, 3)}它會(huì)被編譯為如下形式:
這里先將1,2,3保存到rsp+80h+var_30開始的位置,然后將其首地址、長(zhǎng)度(3)、容量(3)放到棧上,之后調(diào)用VarArgDeme函數(shù)。
2.3 匿名函數(shù)
匿名函數(shù)通常會(huì)以外部函數(shù)名_funcX來命名,除此之外和普通函數(shù)沒什么不同,只是需要注意若使用了外部變量,即形成閉包時(shí),這些變量會(huì)以引用形式傳入,如在os/exec/exec.go中如下代碼:?
go func() { select { case c.ctx. c.Process.Kill() case c.waitDone: } }()其中c是外部變量,它在調(diào)用時(shí)會(huì)以參數(shù)形式傳入(newproc請(qǐng)見后文協(xié)程部分):
而在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是外部變量,它在調(diào)用時(shí)是將其存入外部寄存器(rdx)傳入的:
可見在使用到外部變量時(shí)它們會(huì)作為引用被傳入并使用。
2.4 方法
Go可以為任意自定義類型綁定方法,方法將會(huì)被轉(zhuǎn)換為普通函數(shù),并且將方法的接收者轉(zhuǎn)化為第一個(gè)參數(shù),再看看上文結(jié)構(gòu)體處的圖:
如上可見Context含44個(gè)導(dǎo)出方法,3個(gè)未導(dǎo)出方法,位置已經(jīng)被計(jì)算出在0xcdbaa8,因此可轉(zhuǎn)到方法定義數(shù)組:
如上可見,首先是可導(dǎo)出方法,它們按照名稱升序排序,之后是未導(dǎo)出方法,它們也是按名稱升序排序,另外導(dǎo)出方法有完整的函數(shù)簽名,而未導(dǎo)出方法只有函數(shù)名稱。在逆向時(shí)不必關(guān)心這一部分結(jié)構(gòu),解析工具會(huì)自動(dòng)將對(duì)應(yīng)的函數(shù)調(diào)用重命名,此處僅了解即可。
在逆向時(shí)工具會(huì)將其解析為類型名__方法名或類型名_方法名,因此遇到此類名稱時(shí)我們需要注意它的第一個(gè)參數(shù)是隱含參數(shù),類似C++的this指針,但Go的方法定義不僅支持傳引用,也支持傳值,因此第一個(gè)參數(shù)可能在匯編層面不只占一個(gè)機(jī)器字,如:
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()}編譯后如下所示:
根據(jù)定義兩個(gè)方法都沒有參數(shù),但是從匯編看它們都有參數(shù),如注釋,在逆向時(shí)是更常見的是像PPrint這種方法,即第一個(gè)參數(shù)是對(duì)象的指針。
2.5 函數(shù)反射
函數(shù)在普通使用和反射使用時(shí),被保存的信息不相同,普通使用不需要保存函數(shù)簽名,而反射會(huì)保存,更利于分析,如下代碼:
//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)}編譯后通過字符串搜索,可定位到被反射的函數(shù)簽名(當(dāng)然在逆向中并不知道應(yīng)該搜什么,而是在函數(shù)周圍尋找簽名):
而普通函數(shù)的簽名無法被搜到:
3 伸縮棧
由于go可以擁有大量的協(xié)程,若使用固定大小的棧將會(huì)造成內(nèi)存空間浪費(fèi),因此它使用伸縮棧,初始時(shí)一個(gè)普通協(xié)程只分配幾KB的棧,并在函數(shù)執(zhí)行前先判斷棧空間是否足夠,若不夠則通過一些方式擴(kuò)展棧,這在匯編上的表現(xiàn)形式如下:
在調(diào)用runtime·morestack*函數(shù)擴(kuò)展棧后會(huì)重新進(jìn)入函數(shù)并進(jìn)入左側(cè)分支,因此在分析時(shí)直接忽略右側(cè)分支即可。
4 調(diào)用約定
Go統(tǒng)一通過棧傳遞參數(shù)和返回值,這些空間由調(diào)用者維護(hù),返回值內(nèi)存會(huì)在調(diào)用前選擇性的被初始化,而參數(shù)傳遞是從左到右順序,在內(nèi)存中從下到上寫入棧,因此看到mov [rsp + 0xXX + var_XX], reg(棧頂)時(shí)就代表開始為函數(shù)調(diào)用準(zhǔn)備參數(shù)了,繼續(xù)向下就能確定函數(shù)的參數(shù)個(gè)數(shù)及內(nèi)容:
如圖,mov [rsp+108h+v_108], rdx即表示開始向棧上傳第一個(gè)參數(shù)了,從此處到call指令前都是傳參,此處可見在匯編層面?zhèn)髁?個(gè)參數(shù),其中第2個(gè)和第3個(gè)參數(shù)為Go語言里的第二個(gè)參數(shù),call指令之后為返回值,不過可能存在返回值未使用的情況,因此返回值的個(gè)數(shù)和含義需要從函數(shù)內(nèi)部分析,比如此處的Query我們已知arg_0/arg_8/arg_10為參數(shù),那么剩下的arg18/arg20即為返回值:
需要注意的是不能僅靠函數(shù)頭部就斷定參數(shù)個(gè)數(shù),例如當(dāng)參數(shù)為一個(gè)結(jié)構(gòu)體時(shí),可能頭部的argX只代表了其首位的地址,因此需要具體分析函數(shù)retn指令前的指令來確定返回值大小。
5 寫屏障
Go擁有垃圾回收,其三色標(biāo)記法使用了寫屏障的方法保證一致性,在垃圾收集過程中會(huì)將寫屏障標(biāo)志置位,此時(shí)會(huì)進(jìn)入另一條邏輯,但是我們?cè)谀嫦蚍治鲞^程中可以認(rèn)為該位未置位而直接分析無保護(hù)的情況:
如上圖,先判斷標(biāo)志,再?zèng)Q定是否進(jìn)入,在分析時(shí)可以直接認(rèn)為其永假并走左側(cè)分支。
6 協(xié)程go
使用go關(guān)鍵詞可以創(chuàng)建并運(yùn)行協(xié)程,它在匯編上會(huì)被表現(xiàn)為由runtime_newproc(fn,args?),它會(huì)封裝函數(shù)與參數(shù)并創(chuàng)建協(xié)程執(zhí)行信息,并在適當(dāng)時(shí)候被執(zhí)行,如:
這里執(zhí)行了go loop(),由于沒有參數(shù)此處newproc只被傳入了函數(shù)指針這一個(gè)參數(shù),否則會(huì)傳入繼續(xù)傳入函數(shù)所需的參數(shù),在分析時(shí)直接將函數(shù)作為在新的線程里執(zhí)行即可。
7 延遲執(zhí)行defer
延遲執(zhí)行一般用于資源釋放,它會(huì)先注冊(cè)到鏈表中并在當(dāng)前調(diào)用棧返回前執(zhí)行所有鏈表中注冊(cè)的函數(shù),在匯編層面會(huì)表現(xiàn)為runtime_deferproc,例如常見的鎖釋放操作:
這里它第一個(gè)參數(shù)代表延遲函數(shù)參數(shù)字節(jié)大小為8字節(jié),第二個(gè)參數(shù)為函數(shù)指針,第三個(gè)參數(shù)為延遲執(zhí)行函數(shù)的參數(shù),若創(chuàng)建失敗會(huì)直接返回,返回前會(huì)調(diào)用runtime_deferreturn去執(zhí)行其他創(chuàng)建的延遲執(zhí)行函數(shù),一般我們是不需要關(guān)注該語句的,因此可以直接跳過相關(guān)指令并向左側(cè)繼續(xù)分析。
8 調(diào)用c庫(kù)cgo
Go可以調(diào)用C代碼,但調(diào)用C會(huì)存在運(yùn)行時(shí)不一致,Go統(tǒng)一將C調(diào)用看作系統(tǒng)調(diào)用來處理調(diào)度等問題,另一方類型不一致才是我們需要關(guān)注的重點(diǎn),為了解決類型與命名空間等問題cgo會(huì)為C生成樁代碼來橋接Go,于是這類函數(shù)在Go語言側(cè)表現(xiàn)為XXX_CFunc__YYY,它封裝參數(shù)并調(diào)用runtime_cgocall轉(zhuǎn)換狀態(tài),在中間表示為NNN_cgo_abcdef123456_CFunc__ZZZ,這里它解包參數(shù)并調(diào)用實(shí)際c函數(shù),例如:
此處它調(diào)用了libc的void* realloc(void*, newsize),在Go側(cè)它封裝成了os_user__Cfunc_realloc,在該函數(shù)內(nèi)部參數(shù)被封裝成了結(jié)構(gòu)體并作為指針與函數(shù)指針一起被傳入了cgocall,而函數(shù)指針即_cgo_3298b262a8f6_Cfunc_realloc為中間層負(fù)責(zé)解包參數(shù)等并調(diào)用真正的C函數(shù):
9 其他
還有些內(nèi)容,如看到以panic開頭的分支不分析等不再演示,分析時(shí)遇到不認(rèn)識(shí)的三方庫(kù)函數(shù)和標(biāo)準(zhǔn)庫(kù)函數(shù)直接看源碼即可。
參考鏈接
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
推薦閱讀:
?【高級(jí)持續(xù)性威脅追蹤】Purple Fox 新變種講述從釣魚網(wǎng)站到Rootkit的故事
【漏洞通告】FasterXML Jackson-databind多個(gè)反序列化漏洞
【漏洞通告】Apache Flink文件寫入與任意文件讀取漏洞(CVE-2020-17518/CVE-2020-17519)
【漏洞通告】Zyxel多個(gè)設(shè)備密碼硬編碼漏洞(CVE-2020-29583)
【高級(jí)持續(xù)性威脅追蹤】來自Mustang Panda的攻擊? ?我兔又背鍋了!
深信服科技旗下安全實(shí)驗(yàn)室,致力于網(wǎng)絡(luò)安全攻防技術(shù)的研究和積累,深度洞察未知網(wǎng)絡(luò)安全威脅,解讀前沿安全技術(shù)。
●?掃碼關(guān)注我們
總結(jié)
以上是生活随笔為你收集整理的go conn 读取byte数组后是否要_【技术推荐】正向角度看Go逆向的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: php取key的value值,获取数组中
- 下一篇: 用vhdl实现4位加减法计数器_32位加