【技術推薦】正向角度看Go逆向

深信服千里目發表於2021-01-12



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







2 動態與型別系統







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






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



1. 儘管Go並非面嚮物件語言,但是本文將Go的型別描述為類,將型別對應的變數描述為型別的例項物件。

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

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

4. 一個複雜的例項物件可以分為索引頭和資料部分,它們在記憶體中分散儲存,下文提到一種資料所佔記憶體大小是指索引頭的大小,因為這部分是逆向關注的點,詳見下文字串結構。








資料型別




1 數值型別










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



【技術推薦】正向角度看Go逆向







2 字串string







Go語言中字串是二進位制安全的,它不以\0作為終止符,一個字串物件在記憶體中分為兩部分,一部分為如下結構,佔兩個機器字用於索引資料:



type StringHeader struct {
    Data uintptr            // 字串首地址
    Len  int                // 字串長度
}



而它的另一部分才存放真正的資料,它的大小由字串長度決定,在逆向中重點關注的是如上結構,因此說一個string佔兩個機器字,後文其他結構也按這種約定。例如下圖使用printf輸出一個字串"hello world",它會將上述結構入棧,由於沒有終止符ida無法正常識別字串結束因此輸出了很多資訊,我們需要依靠它的第二個域(此處的長度0x0b)決定它的結束位置:



【技術推薦】正向角度看Go逆向



字串常見的操作是字串拼接,若拼接的個數不超過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的字串,但是它們並沒有被連續存放:



【技術推薦】正向角度看Go逆向



因此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的彙編如下,它直接在棧上定義並初始化:



【技術推薦】正向角度看Go逆向



變數b的彙編如下,它的初始值被定義在了資料段並進行拷貝初始化:



【技術推薦】正向角度看Go逆向



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



【技術推薦】正向角度看Go逆向



變數c的彙編如下,儘管它和a的值一樣,但是它的地址會被返回,如果在C語言中這種寫法會造成嚴重的後果,不過Go作為記憶體安全的語言在編譯時就識別出了該問題(指標逃逸)並將其放在了堆上,此處引出了runtime.newobject函式,該函式傳入的是資料的型別指標,它將在堆上申請空間存放物件例項,返回的是新的物件指標:



【技術推薦】正向角度看Go逆向



經常會遇到的情況是返回一個結構體變數,然後將其賦值給newobject申請的新變數上。







4 切片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



例如下圖:



【技術推薦】正向角度看Go逆向



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







5 字典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傳入一個字串鍵並獲取一個槽,之後將資料寫入返回的槽地址裡,這裡就是一個建立字典並賦值的過程:



【技術推薦】正向角度看Go逆向



如下是訪問字典裡資料的情況,呼叫mapaccess1_fast32傳入了一個32位的數字作為鍵:



【技術推薦】正向角度看Go逆向



可以看到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中物件所佔空間對應:



【技術推薦】正向角度看Go逆向



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



【技術推薦】正向角度看Go逆向



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







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)



例如存在如下彙編程式碼:



【技術推薦】正向角度看Go逆向



可以知道convI2I的結果是第一行所指定介面型別對應的介面物件,在最後一行它呼叫了itab+30h處的函式,根據計算可知是字母序後的第4個函式,這裡可以直接檢視介面的型別定義,獲知第四個函式:



【技術推薦】正向角度看Go逆向








語法特徵







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



【技術推薦】正向角度看Go逆向







2 函式與方法







2.1 棧空間



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



【技術推薦】正向角度看Go逆向






2.2 變參



類似Python的一般變參實際被轉換為一個tuple,Go變參也被轉換為了一個slice,因此一個變參在彙編級別佔3個引數位,如下程式碼:



func VarArgDemo(args ...int) (sum int) {}
func main() {
    VarArgDemo(1, 2, 3)
}



它會被編譯為如下形式:



【技術推薦】正向角度看Go逆向



這裡先將1,2,3儲存到rsp+80h+var_30開始的位置,然後將其首地址,長度(3),容量(3)放到棧上,之後呼叫VarArgDeme函式。






2.3 匿名函式



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

os/exec/exec.go中如下程式碼:
  go func() {
            select {
            case <-c.ctx.Done():
                c.Process.Kill()
            case <-c.waitDone:
            }
        }()



其中c是外部變數,它在呼叫時會以引數形式傳入(newproc請見後文協程部分):



【技術推薦】正向角度看Go逆向



而在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)傳入的:



【技術推薦】正向角度看Go逆向



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






2.4 方法



Go可以為任意自定義型別繫結方法,方法將會被轉換為普通函式,並且將方法的接收者轉化為第一個引數,再看看上文結構體處的圖:



【技術推薦】正向角度看Go逆向



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



【技術推薦】正向角度看Go逆向



【技術推薦】正向角度看Go逆向



如上可見,首先是可匯出方法,它們按照名稱升序排序,之後是未匯出方法,它們也是按名稱升序排序,另外匯出方法有完整的函式簽名,而未匯出方法只有函式名稱。在逆向時不必關心這一部分結構,解析工具會自動將對應的函式呼叫重新命名,此處僅瞭解即可。



在逆向時工具會將其解析為型別名__方法名或型別名_方法名,因此遇到此類名稱時我們需要注意它的第一個引數是隱含引數,類似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()
}



編譯後如下所示:



【技術推薦】正向角度看Go逆向



根據定義兩個方法都沒有引數,但是從彙編看它們都有引數,如註釋,在逆向時是更常見的是像PPrint這種方法,即第一個引數是物件的指標。






2.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逆向



【技術推薦】正向角度看Go逆向



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



【技術推薦】正向角度看Go逆向







3 伸縮棧







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



【技術推薦】正向角度看Go逆向



在呼叫runtime·morestack*函式擴充套件棧後會重新進入函式並進入左側分支,因此在分析時直接忽略右側分支即可。







4 呼叫約定







Go統一通過棧傳遞引數和返回值,這些空間由呼叫者維護,返回值記憶體會在呼叫前選擇性的被初始化,而引數傳遞是從左到右順序,在記憶體中從下到上寫入棧,因此看到mov [rsp + 0xXX + var_XX], reg(棧頂)時就代表開始為函式呼叫準備引數了,繼續向下就能確定函式的引數個數及內容:



【技術推薦】正向角度看Go逆向



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



【技術推薦】正向角度看Go逆向



需要注意的是不能僅靠函式頭部就斷定引數個數,例如當引數為一個結構體時,可能頭部的argX只代表了其首位的地址,因此需要具體分析函式retn指令前的指令來確定返回值大小。







5 寫屏障







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



【技術推薦】正向角度看Go逆向



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







6 協程go







使用go關鍵詞可以建立並執行協程,它在彙編上會被表現為由runtime_newproc(fn,args?),它會封裝函式與引數並建立協程執行資訊,並在適當時候被執行,如:



【技術推薦】正向角度看Go逆向



這裡執行了go loop(),由於沒有引數此處newproc只被傳入了函式指標這一個引數,否則會傳入繼續傳入函式所需的引數,在分析時直接將函式作為在新的執行緒裡執行即可。







7 延遲執行defer










延遲執行一般用於資源釋放,它會先註冊到連結串列中並在當前呼叫棧返回前執行所有連結串列中註冊的函式,在彙編層面會表現為runtime_deferproc,例如常見的鎖釋放操作:



【技術推薦】正向角度看Go逆向



這裡它第一個引數代表延遲函式引數位元組大小為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函式,例如:



【技術推薦】正向角度看Go逆向



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



【技術推薦】正向角度看Go逆向







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/interface




相關文章