Go 逆向研究

基礎信息

  go 語言默認採用靜態編譯的策略,這意味着各種標準庫和第三方庫包括 runtime 和 gc 都會被全靜態鏈接構建,這導致 go 二進制文件較大,同時 go 函數調用約定,數據結構和棧管理策略非常特殊,而且不同 go 版本之間的細節也存在很多差異,這一系列原因導致 go 逆向存在諸多難處。不過根據其特殊之處入手可以幫助進行符號恢復,字符串引用恢復等操作來幫助逆向工程師獲得更好的體驗。

calling convention

不同 go 版本之間存在一定差異。

stack

  對於go function來說,它的棧設計本身就非常獨特,因爲操作系統下的threads managed對用戶來說已經完全被 go runtime 抽象,這使得用戶只需要關注存在於用戶空間的新抽象 goroutines,這使得 go runtime 可以自己設置棧的行爲準則。

  當 go 函數啓動的時候,首先就會爲棧分配一個固定大小的空間,但是這個大小在不同版本之間存在差異,在 go1.12 的時候最小棧空間爲 2kb,但是 go1.2 變成了 4kb,然後在 go1.4 再次變回 2kb,我查看了目前的 go1.20.1 版本同樣是 2kb。然後在接下來的函數調用中會對棧空間是否合適進行檢查,如果需要的話通過runtime.morestack_noctxt函數對棧空間進行擴充,這也對應了彙編中經常看到的call prologue(this particular prologue is present only in routines with local variables):

在進行棧擴充的時候採用整體 Free 重新分配的策略,同時一般每次newsize := oldsize * 2,如下:

  根據不同的架構,棧具有不同的最大值,棧的分配超過該值就會引發錯誤。同時如果需要的話,棧空間可以被 gc 回收,在回收的時候棧空間變化爲newsize := oldsize/2,並且會複用之前的棧地址。另一個值得注意的點是自動 go1.3 之後開始,goroutine stacks 的實現方式從 segmented model 轉變爲 contiguous model,contiguous model 優化了 segmented model 的 hot split 問題,這裏就不細說,需要了解可以參考該鏈接。

call arguments and return value

  在 go 中參數和返回值都會存儲在 caller 的棧空間裏面,存儲返回值的空間會被預先分配,然後由被調用函數寫入對應的值,通過這種方式 go 實現了多返回值機制,但是最新版的 go 語言中參數和返回值也可以在寄存器中傳遞:

https://tip.golang.org/src/cmd/compile/abi-internal

Function calls pass arguments and results using a combination of the stack and machine registers. Each argument or result is passed either entirely in registers or entirely on the stack. Because access to registers is generally faster than access to the stack, arguments and results are preferentially passed in registers. However, any argument or result that contains a non-trivial array or does not fit entirely in the remaining available registers is passed on the stack.

  因此新版 go 語言中參數和返回值的傳遞優先採用寄存器,但是寄存器參數傳遞規則並不是類似 x86_64 的 rdi, rsi 等,而是有一套屬於自己的算法,具體可以參考上面的鏈接,比較值得注意的是在新版(當前爲 1.20.1)go 的調用約定中,參數和返回值可以共享寄存器但是不會共享棧空間,同時即使有些參數通過寄存機傳參,caller 依然會在棧空間中依然會爲它們預留一定的空間,同時 caller 也會爲寄存器傳參的參數在棧空間中預留spill area溢出區:

如圖 rax 和 rbx 用來保存返回值,同時也用到了棧上預留的空間。同時,如上面提到的,假如有 struct,array 和 string 類型的參數那麼調用約定就會變得更爲複雜:

f(a1 uint8, a2 [2]uintptr, a3 uint8) (r1 struct { x uintptr; y [2]uintptr }, r2 string)

  上面的官方例子很好的講述了這一點,假設存在寄存器 R0-R9,在函數起始階段 a1 會被賦予 R0,a3 會被賦予 R1,a2 則是在棧中初始化,棧中爲 a1 和 a3 預留的空間則不會初始化。在函數結束階段,r2.base 也就是字符串所在的地址被賦予到 R0,r2.len 也就是字符串長度被賦予 R1,r1.x 和 r1.y 則被初始化在棧上。總結就是如果參數或者返回值中包含類似 array 結構,那麼就會被放在棧上操作,其它則通過寄存器操作,字符串則因爲 go 自己獨特的存儲模式(後面會細說)需要分不同的部分進行傳遞。在棧空間排布上,參數要比返回值處於更低的地址,同時也比 spill area 更低。

具體到逆向目標這裏給出對於 amd64 和 arm64 的相關內容,對於 amd64 架構來說,下列寄存器會被用於傳遞整數類型的參數和結果:

RAX, RBX, RCX, RDI, RSI, R8, R9, R10, R11

  使用 X0-X14 來傳遞浮點類型的參數和結果,而對於 arm64 架構來說,它使用 R0-R15 來保存整數類型的參數和結果,使用 F0-F15 來保存浮點類型的參數和結果。

Type System

內建基礎類型:

  在內建基礎類型上,go 的表現基本和 C 語言類似,在逆向分析過程中最重要的類型拆解其實就是結構體,在 Go 語言裏面 rtype 是很多類型的基礎實現,它會被嵌入到其它的 struct types:

https://go.dev/src/reflect/type.go

// rtype is the common implementation of most values.
// It is embedded in other struct types.
//
// rtype must be kept in sync with ../runtime/type.go:/^type._type.
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
        // function for comparing objects of this type
        // (ptr to object A, ptr to object B) -> ==?
        equal     func(unsafe.Pointer, unsafe.Pointer) bool
        gcdata    *byte   // garbage collection data
        str       nameOff // string form
        ptrToThis typeOff // type for pointer to this type, may be zero
}

  對於逆向工程來說,這些字段非常有幫助,kind:代表了目標結構體的基礎類型,nameOff可以知道目標結構體的名字,ptrToThis則代表了指向該結構體的指針類型,在 go 開發過程中很多情況下會針對指針實現結構體函數,這裏的ptrToThis幫助找到指針類型所在的位置從而可以找到對應實現的結構體函數來幫助逆向:

// ptrType represents a pointer type.
type ptrType struct {
        rtype
        elem *rtype // pointer element (pointed at) type
}

接下來是 struct 的實現:

// structType represents a struct type.
type structType struct {
        rtype
        pkgPath name
        fields  []structField // sorted by offset
}

所以在struct的實現中包含所在的包路徑,然後接下來跟一個structField數組:

// Struct field
type structField struct {
        name   name    // name is always non-empty
        typ    *rtype  // type of field
        offset uintptr // byte offset of field
}

其中 name 代表的是文件名,還有一個 rtype 指針用來該 Field 的類型,通過對這些結構體信息進行組織就可以對目標結構體進行還原。

爲了理解 go 二進制文件的數據分佈,必須理解 go 獨特的 moduledata,但是隨着版本的變化 moduledata 也一直在改變,這裏是針對 go1.20.1 的解析:

https://go.dev/src/runtime/symtab.go

//moduledata records information about the layout of the executable
// image. It is written by the linker. Any changes here must be
// matched changes to the code in cmd/link/internal/ld/symtab.go:symtab.
// moduledata is stored in statically allocated non-pointer memory;
// none of the pointers here are visible to the garbage collector.
type moduledata struct {
        pcHeader     *pcHeader
        funcnametab  []byte
        cutab        []uint32
        filetab      []byte
        pctab        []byte
        pclntable    []byte
        ftab         []functab
        findfunctab  uintptr
        minpc, maxpc uintptr

        text, etext           uintptr
        noptrdata, enoptrdata uintptr
        data, edata           uintptr
        bss, ebss             uintptr
        noptrbss, enoptrbss   uintptr
        covctrs, ecovctrs     uintptr
        end, gcdata, gcbss    uintptr
        types, etypes         uintptr
        rodata                uintptr
        gofunc                uintptr // go.func.*

        textsectmap []textsect
        typelinks   []int32 // offsets from types
        itablinks   []*itab

        ptab []ptabEntry

        pluginpath string
        pkghashes  []modulehash

        modulename   string
        modulehashes []modulehash

        hasmain uint8 // 1 if module contains the main function, 0 otherwise

        gcdatamask, gcbssmask bitvector

        typemap map[typeOff]*_type // offset to *_rtype in previous module

        bad bool // module failed to load and should be ignored

        next *moduledata
}

  詳細看該結構體就知道這個對於逆向來說非常重要,可以幫助定位不同段的位置和相關信息,對於類型系統來說最重要的就是 types, etypestypelinks,types 段中包含 type descriptions 而 typelinks 段中包含對於 types 的偏移:

可以看到雖然沒有 types 段,但是存在 typelinks 段來很方便的定位到目標。因此通過遞歸搜索拆解類型信息並交叉引用就可以很好的完成對於類型的恢復。

對於 go 逆向來說,另一個重要的類型就是接口,在 go 源碼裏可以看到其實現爲:

type iface struct {
        tab  *itab
        data unsafe.Pointer
}

type itab struct {
         inter *interfacetype
         _type *_type
         hash  uint32 // copy of _type.hash. Used for type switches.
         _     [4]byte
         fun   [1]uintptr // variable sized. fun[0]==0 means _type does not implement inter.
}
type interfacetype struct {
         typ     _type
         pkgpath name
         mhdr    []imethod
}

上面就是 go 源碼內部的相關數據結構,其中_type 其實就是上述的 rtype,所以其實對於 iface 的結構來說就很清晰了,在 itab 中,fun 是一個對象的 virtual dispatch table 用來索引一些函數。

  無論是 interface 還是 struct 其實在逆向中都更關心它們包含的一些函數,在 go 的類型系統中還存在 funcType:

type funcType struct {
        rtype
        inCount  uint16
        outCount uint16 // top bit is set if last input parameter is ...
}

inCount 和 outCount 代表參數和返回值。

pclntab

  pclntab全名是Program Counter Line Table。2013 年由Russ Cox(Go 語言創始團隊成員,核心開發者) 從 Plan9 移植到 Go 1.2上,至今沒有太大變化。引入*pcnlntab*這個結構的最初動機,是爲 *Stack Trace* 服務的。當程序運行出錯要 *panic* 的時候,runtime 需要知道當前的位置,層級關係如 *pkg*->*src file*->*function or method*->*line number*,每一層的信息 runtime 都要知道。Go 就把這些信息結構化地打包到了編譯出的二進制文件中。除此之外,pcnlntab 中還包含了棧的動態管理用到的棧幀信息、垃圾回收用到的棧變量的生命週期信息以及二進制文件涉及的所有源碼文件路徑信息。這個結構體反過來就是我們逆向的重要參考。定義如下 (go 1.16 版本之後在源碼中才存在定義):

其中 magic 的值在 Go 1.16 之前是 0xFFFFFFFB,Go 1.16 到 17 是 0xFFFFFFFA,Go 1.18 是 0xFFFFFFF0。具體的解析邏輯在源碼中 pclntab.go。各字段含義如下:

  裏面比較重要的信息有函數表和源碼錶。其中函數表 (func table) 的起始地址,爲 (pclntab_addr + 8),第一個元素( uintptr N) 代表函數的個數。每兩個 uintptr 元素爲一組,即 (func_addr, func_struct_offset),每組第一個元素爲函數的地址,第二個元素爲函數結構體定義 (Function Struct) 相對於 pclntab 起始地址的偏移。Function Struct 定義如下:

struct Func
{
    uintptr      entry;     // start pc
    int32        name;      // name (offset to C string)
    int32        args;      // size of arguments passed to function
    int32        frame;     // size of function frame, including saved caller PC
    int32        pcsp;      // pcsp table (offset to pcvalue table)
    int32        pcfile;    // pcfile table (offset to pcvalue table)
    int32        pcln;      // pcln table (offset to pcvalue table)
    int32        nfuncdata; // number of entries in funcdata list
    int32        npcdata;   // number of entries in pcdata list
};

  對於逆向分析來說,這裏最有用的信息,就是函數名了。如上圖所示,函數名是一個以 0x00 結尾的 C-String。在 Function Struct 中,第二個元素只是 int32 類型的偏移值 (仍然相對於 pclntab 地址)。而 Function Struct 中第 3 個元素 args 在 Go 標準庫源碼 src/debug/gosym/symtab.go 中

解析這個 Function Struct 的一個類型定義中,有兩條註釋,說 Go 1.3 之後就沒這種信息了:

  另外,還有一些函數用上面的方式無法解析,是編譯器做循環展開時自動生成的匿名函數,也叫 Duff’s Device。這樣的函數知道它是用來連續操作內存 (拷貝、清零等等) 的就可以。

字符串引用

  對於字符串引用問題解決方法很粗暴沿用了 golang_loader_assist.py。核心就是觀察彙編中字符串引用格式,golang 中const char *字符串幾乎是堆在一起的,然後以str_addr + str_len的方式引用。

以 386 架構來說,觀察其字符串引用匯編如下:

mov     rcx, cs:qword_BC2908 ; str len
mov     rdx, cs:off_BC2900 ; str pointer
mov     [rsp+0A8h+var_90], rdx
mov     [rsp+0A8h+var_88], rcx
call    github_com_rs_zerolog_internal_json_Encoder_AppendKey

然後通過指針和長度來識別某一個字符串,而不是所有字符串。對於 arm 架構有如下兩種方式引用:

pattern0:   properly for local string variable invoking

LDR             R0, =aGodebugUnknown;   strptr
STR             R0, [SP,#0x50+var_4C]   
MOV             R1, #0x1E           ;   len
STR             R1, [SP,#0x50+var_48]
BL              runtime_printstring

pattern1:   properly for global string variable 
LDR             R3, =off_888AE8 ; ".SH NAME/dev/mem/dev/mtd/gid_map/static" strptr-len_ptr
...
...
BL              fmt_Fprintln

.data:0014C3F8 off_14C3F8      DCD aGlobalHelloMbA     ; DATA XREF: main_main+2C↑o
.data:0014C3F8                                         ; main_main+30↑r ...
.data:0014C3F8                                         ; "Global Hello !MB; allocated Other_ID_St"...
.data:0014C3FC dword_14C3FC    DCD 0xE

第一種:LDR,STR,MOV,STR 獲取字符串地址和長度並壓棧。

第二種:獲得一個指針該指針指向一個結構體包含字符串地址和長度。上面不管是 386 架構還是 arm 的兩種情況在引用字符串時,字符串地址和長度都是地址相鄰的 (這個可能對後續詳細識別引用有一定幫助)。

moduledata

  在 Go 語言的體系中,Module 是比 Package 更高層次的概念,具體表現在一個 Module 中可以包含多個不同的 Package,而每個 Package 中可以包含多個目錄和很多的源碼文件。相應地,Moduledata 在 Go 二進制文件中也是一個更高層次的數據結構,它包含很多其他結構的索引信息,可以看作是 Go 二進制文件中 RTSI(Runtime Symbol Information) 和 RTTI(Runtime Type Information) 的地圖:

// moduledata records information about the layout of the executable
// image. It is written by the linker. Any changes here must be
// matched changes to the code in cmd/internal/ld/symtab.go:symtab.
// moduledata is stored in statically allocated non-pointer memory;
// none of the pointers here are visible to the garbage collector.
type moduledata struct {
    pclntable    []byte
    ftab         []functab
    filetab      []uint32
    findfunctab  uintptr
    minpc, maxpc uintptr

    text, etext           uintptr
    noptrdata, enoptrdata uintptr
    data, edata           uintptr
    bss, ebss             uintptr
    noptrbss, enoptrbss   uintptr
    end, gcdata, gcbss    uintptr
    types, etypes         uintptr

    textsectmap []textsect
    typelinks   []int32 // offsets from types
    itablinks   []*itab

    ptab []ptabEntry

    pluginpath string
    pkghashes  []modulehash

    modulename   string
    modulehashes []modulehash

    hasmain uint8 // 1 if module contains the main function, 0 otherwise

    gcdatamask, gcbssmask bitvector

    typemap map[typeOff]*_type // offset to *_rtype in previous module

    bad bool // module failed to load and should be ignored

    next *moduledata
}//https://github.com/golang/go/blob/dev.boringcrypto.go1.13/src/runtime/symtab.go

根據 Moduledata 的定義,Moduledata 是可以串成鏈表的形式的,而一個完整的可執行 Go 二進制文件中,只有一個 firstmoduledata 包含如上完整的字段。簡單介紹一下關鍵字段:

firstmoduledata其第一個 uintptr 元素指向的位置,前 4 字節爲 pclntab 的Magic Number。所以以 uintptr 爲單位遍歷整個二進制文件找到符合這一點的地址作爲可能的,firstmoduledata起始地址。如果是真實的 firstmoduledata,它內部是有幾個字段可以跟 pclntab 中的數據進行交叉驗證的,比如:

  當然,不一定要驗證上面所有條件,驗證其中一部分甚至一個關鍵條件,就可以確認當前地址是否爲真正的 firstmoduledata**。**go_parser 中就是通過這種方法定位 firstmoduledata 然後根據不同版本從 pclntable 恢復函數名 (以及函數地址範圍) 和源碼路徑。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/qPqXUVj7K4WQxpUiiAqwiw