正向角度看 Go 逆向

作者:漏洞研究團隊@深信服千里目安全實驗室
原文鏈接:https://mp.weixin.qq.com/s/CrmgqLwXUaR7Uccj_72f3g

Go語言具有開發效率高,運行速度快,跨平臺等優點,因此正越來越多的被攻擊者所使用,其生成的是可直接運行的二進制文件,因此對它的分析類似於普通C語言可執行文件分析,但是又有所不同,本文將會使用正向與逆向結合的方式描述這些區別與特徵。

一、語言特性

Ⅰ. Compile與Runtime

Go語言類似於C語言,目標是一個二進制文件,逆向的也是native代碼,它有如下特性:

1.強類型檢查的編譯型語言,接近C但擁有原生的包管理,內建的網絡包,協程等使其成爲一款開發效率更高的工程級語言。

2.作爲編譯型語言它有運行速度快的優點,但是它又能通過內置的運行時符號信息實現反射這種動態特性。

3.作爲一種內存安全的語言,它不僅有內建的垃圾回收,還在編譯與運行時提供了大量的安全檢查。

可見儘管它像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), AX

0x003f 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), AX

0x0052 00082 (str.go:4) MOVQ   "".b+112(SP), CX

0x0057 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), AX

0x0068 00104 (str.go:4) MOVQ   AX, 40(SP)

0x006d 00109 (str.go:4) MOVQ   $3, 48(SP)

0x0076 00118 (str.go:4) PCDATA  $1, $1

0x0076 00118 (str.go:4) CALL   runtime.concatstring3(SB)

我們將在彙編中看到大量這類函數調用,本文將在對應章節介紹最常見的一些函數。若需要觀察某語法最終編譯後的彙編代碼,除了使用ida等也可以直接使用如下三種方式:

go tool compile -N -l -S once.go

go tool compile -N -l once.go ; go tool objdump -gnu -s Do once.o

go build -gcflags -S once.go

Ⅱ. 動態與類型系統

儘管是編譯型語言,Go仍然提供了一定的動態能力,這主要表現在接口與反射上,而這些能力離不開類型系統,它需要保留必要的類型定義以及對象和類型之間的關聯,這部分內容無法在二進制文件中被去除,否則會影響程序運行,因此在Go逆向時能獲取到大量的符號信息,大大簡化了逆向的難度,對此類信息已有大量文章介紹並有許多優秀的的工具可供使用,例如go_parserredress,因此本文不再贅述此內容,此處推薦《Go二進制文件逆向分析從基礎到進階——綜述》。

本文將從語言特性上介紹Go語言編寫的二進制文件在彙編下的各種結構,爲了表述方便此處定義一些約定:

  1. 儘管Go並非面嚮對象語言,但是本文將Go的類型描述爲類,將類型對應的變量描述爲類型的實例對象。

  2. 本文分析的樣例是x64上的樣本,通篇會對應該平臺敘述,一個機器字認爲是64bit。

  3. 本文會涉及到Go的參數和彙編層面的參數描述,比如一個複數在Go層面是一個參數,但是它佔16字節,在彙編上將會分成兩部分傳遞(不使用xmm時),就認爲彙編層面是兩個參數。

  4. 一個複雜的實例對象可以分爲索引頭和數據部分,它們在內存中分散存儲,下文提到一種數據所佔內存大小是指索引頭的大小,因爲這部分是逆向關注的點,詳見下文字符串結構。

二、數據結構

Ⅰ. 數值類型

數值類型很簡單隻需要注意其大小即可:

類型 32位平臺 64位平臺
bool、int8、uint8 8bit 8bit
int16、uint16 16bit 16bit
int32、uint32、float32 32bit 32bit
int64、uint64、float64、complex64 64bit 64bit
int、uint、uintptr 32bit 64bit
complex128 128bit 1238bit

Ⅱ. 字符串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) string

func concatstring3(*[32]byte, string, string, string) string

func concatstring4(*[32]byte, string, string, string, string) string

func concatstring5(*[32]byte, string, string, string, string, string) string

func concatstrings(*[32]byte, []string) string

因此在遇到concatstringN時可以跳過第一個參數,隨後入棧的參數即爲字符串,而遇到concatstrings時,跳過第一個參數後彙編層面還剩三個參數,其中後兩個一般相同且指明字符串個數,第一個參數則指明字符串數組的首地址,另外經常出現的是string與[]byte之間的轉換,詳見下文slice部分。提醒一下,可能是優化導致一般來說在棧內一個純字符串的兩部分在物理上並沒有連續存放,例如下圖調用macaron的context.Query("username")獲取到的應該是一個代表username的字符串,但是它們並沒有被連續存放:

因此ida中通過定義本地結構體去解析string會遇到困難,其他結構也存在這類情況,氣。

Ⅲ. 數組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申請的新變量上。

Ⅳ. 切片slice

類似數組,切片的實例對象數據結構如下,可知它佔用了三個機器字,與它相關的函數是growslice表示擴容,逆向時可忽略:

type SliceHeader struct {

  Data uintptr            // 數據指針

  Len  int              //  當前長度

  Cap  int              // 可容納的長度

}

更常見的函數是與字符串相關的轉換,它們在底層調用的是如下函數,此處我們依然不必關注第一個參數:

func slicebytetostring(buf *[32]byte, ptr *byte, n int) string

func stringtoslicebyte(*[32]byte, string) []byte

例如下圖:

可見傳入的是兩個參數代表一個string,返回了三個數據代表一個[]byte。

Ⅴ. 字典map

字典實現比較複雜,不過在逆向中會涉及到的內容很簡單,字典操作常見的會轉換爲如下函數,一般fastrand和makemap連用返回一個map,它爲一個指針,讀字典時使用mapaccess1和mapaccess2,後者是使用,ok語法時生成的函數,runtime裏還有很多以2結尾的函數代表同樣的含義,後文不再贅述。寫字典時會使用mapassign函數,它返回一個地址,將value寫入該地址,另外還比較常見的是對字典進行遍歷,會使用mapiterinit和mapiternext配合:

func fastrand() uint32

func 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的第一個參數代表字典的類型,因此能很容易知道字典操作參數和返回值的類型。

Ⅵ. 結構體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可轉到每個域的定義,可見其域名稱域類型,偏移等:

結構體類型作爲自定義類型除了域之外,方法也很重要,這部分在後文會提到。

Ⅶ. 接口 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*個函數,這裏可以直接查看接口的類型定義,獲知第四個函數:

三、語法特性

Ⅰ. 新建對象

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後,會生成此代碼:

Ⅱ. 函數與方法

1.棧空間

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

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函數。

3.匿名函數

匿名函數通常會以外部函數名_funcX來命名,除此之外和普通函數沒什麼不同,只是需要注意若使用了外部變量,即形成閉包時,這些變量會以引用形式傳入,如在os/exec/exec.go中如下代碼:

go func() {
            select {
            case <-c.ctx.Done():
                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)傳入的:

可見在使用到外部變量時它們會作爲引用被傳入並使用。

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這種方法,即第一個參數是對象的指針。

5.函數反射

函數在普通使用和反射使用時,被保存的信息不相同,普通使用不需要保存函數簽名,而反射會保存,更利於分析,如下代碼:

//go:noinline

func Func1(b string, a int) bool {

  return a < len(b)

}

//go:noinline

func 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)

}

編譯後通過字符串搜索,可定位到被反射的函數簽名(當然在逆向中並不知道應該搜什麼,而是在函數週圍尋找簽名):

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

Ⅲ. 伸縮棧

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

在調用runtime·morestack*函數擴展棧後會重新進入函數並進入左側分支,因此在分析時直接忽略右側分支即可。

Ⅳ. 調用約定

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指令前的指令來確定返回值大小。

Ⅴ. 寫屏障

Go擁有垃圾回收,其三色標記法使用了寫屏障的方法保證一致性,在垃圾收集過程中會將寫屏障標誌置位,此時會進入另一條邏輯,但是我們在逆向分析過程中可以認爲該位未置位而直接分析無保護的情況:

如上圖,先判斷標誌,再決定是否進入,在分析時可以直接認爲其永假並走左側分支。

Ⅵ. 協程 go

使用go關鍵詞可以創建並運行協程,它在彙編上會被表現爲由runtime_newproc(fn,args?),它會封裝函數與參數並創建協程執行信息,並在適當時候被執行,如:

這裏執行了go loop(),由於沒有參數此處newproc只被傳入了函數指針這一個參數,否則會傳入繼續傳入函數所需的參數,在分析時直接將函數作爲在新的線程裏執行即可。

Ⅶ. 延遲執行 defer

延遲執行一般用於資源釋放,它會先註冊到鏈表中並在當前調用棧返回前執行所有鏈表中註冊的函數,在彙編層面會表現爲runtime_deferproc,例如常見的鎖釋放操作:

這裏它第一個參數代表延遲函數參數字節大小爲8字節,第二個參數爲函數指針,第三個參數爲延遲執行函數的參數,若創建失敗會直接返回,返回前會調用runtime_deferreturn去執行其他創建的延遲執行函數,一般我們是不需要關注該語句的,因此可以直接跳過相關指令並向左側繼續分析。

Ⅷ. 調用c庫 cgo

Go可以調用C代碼,但調用C會存在運行時不一致,Go統一將C調用看作系統調用來處理調度等問題,另一方類型不一致纔是我們需要關注的重點,爲了解決類型與命名空間等問題cgo會爲C生成樁代碼來橋接Go,於是這類函數在Go語言側表現爲XXX_CFuncYYY,它封裝參數並調用runtime_cgocall轉換狀態,在中間表示爲NNN_cgo_abcdef123456_CFuncZZZ,這裏它解包參數並調用實際c函數,例如:

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

Ⅸ. 其他

還有些內容,如看到以panic開頭的分支不分析等不再演示,分析時遇到不認識的三方庫函數和標準庫函數直接看源碼即可。

四、參考鏈接

  1. https://draveness.me/golang/

  2. https://tiancaiamao.gitbooks.io/

  3. https://tiancaiamao.gitbooks.io/go-internals/content/zh/02.3.html

  4. https://www.pnfsoftware.com/blog/analyzing-golang-executables/

  5. https://rednaga.io/2016/09/21/reversing_go_binaries_like_a_pro/

  6. https://research.swtch.com/interfaces


Paper 本文由 Seebug Paper 發佈,如需轉載請註明來源。本文地址:https://paper.seebug.org/1452/

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章