大家好,我是张晋涛。

咱们用 Go 构建的二进制文件中默认蕴含了很多有用的信息。例如,能够获取构建用的 Go 版本:

(这里我应用我始终参加的一个开源我的项目 KIND 为例)

➜  kind git:(master) ✗ go version ./bin/kind ./bin/kind: go1.16

或者也能够获取该二进制所依赖的模块信息:

➜  kind git:(master) ✗ go version -m ./bin/kind./bin/kind: go1.16        path    sigs.k8s.io/kind        mod     sigs.k8s.io/kind        (devel)        dep     github.com/BurntSushi/toml      v0.3.1        dep     github.com/alessio/shellescape  v1.4.1        dep     github.com/evanphx/json-patch/v5        v5.2.0        dep     github.com/mattn/go-isatty      v0.0.12        dep     github.com/pelletier/go-toml    v1.8.1  h1:1Nf83orprkJyknT6h7zbuEGUEjcyVlCxSUGTENmNCRM=        dep     github.com/pkg/errors   v0.9.1        dep     github.com/spf13/cobra  v1.1.1        dep     github.com/spf13/pflag  v1.0.5        dep     golang.org/x/sys        v0.0.0-20210124154548-22da62e12c0c      h1:VwygUrnw9jn88c4u8GD3rZQbqrP/tgas88tPUbBxQrk=        dep     gopkg.in/yaml.v2        v2.2.8        dep     gopkg.in/yaml.v3        v3.0.0-20210107192922-496545a6307b      h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=        dep     k8s.io/apimachinery     v0.20.2        dep     sigs.k8s.io/yaml        v1.2.0

查看 KIND 代码仓库中的 go.mod文件,都蕴含在内了。

其实 Linux 零碎中二进制文件蕴含额定的信息并非 Go 所特有的,上面我将具体介绍其外部原理和实现。当然,用 Go 构建的二进制文件仍是本文的配角。

Linux ELF 格局

ELF 是 Executable and Linkable Format 的缩写,是一种用于可执行文件、指标文件、共享库和外围转储(core dump)的规范文件格式。ELF 文件 通常 是编译器之类的输入,并且是二进制格局。以 Go 编译出的可执行文件为例,咱们应用 file 命令即可看到其具体类型 ELF 64-bit LSB executable

➜  kind git:(master) ✗ file ./bin/kind ./bin/kind: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, not stripped

本文中咱们来具体看看 64 位可执行文件应用的 ELF 文件格式的构造和 Linux 内核源码中对它的定义。

应用 ELF 文件格式的可执行文件是由 ELF 头(ELF Header) 开始,后跟 程序头(Program Header)节头(Section Header) 或两者均有组成的。

ELF 头

ELF 头始终位于文件的零偏移(zero offset)处(即:终点地位),同时在 ELF 头中还定义了程序头和节头的偏移量。

咱们能够通过 readelf 命令查看可执行文件的 ELF 头,如下:

➜  kind git:(master) ✗ readelf -h ./bin/kind ELF Header:  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00   Class:                             ELF64  Data:                              2's complement, little endian  Version:                           1 (current)  OS/ABI:                            UNIX - System V  ABI Version:                       0  Type:                              EXEC (Executable file)  Machine:                           Advanced Micro Devices X86-64  Version:                           0x1  Entry point address:               0x46c460  Start of program headers:          64 (bytes into file)  Start of section headers:          400 (bytes into file)  Flags:                             0x0  Size of this header:               64 (bytes)  Size of program headers:           56 (bytes)  Number of program headers:         6  Size of section headers:           64 (bytes)  Number of section headers:         15  Section header string table index: 3

从下面的输入咱们能够看到,ELF 头是以某个 Magic 开始的,此 Magic 标识了无关文件的信息,即:前四个 16 进制数,示意这是一个 ELF 文件。具体来说,将它们换算成其对应的 ASCII 码即可:

45 = E

4c = L

46 = F

7f 是其前缀,当然,也能够间接在 Linux 内核源码中拿到此处的具体定义:

// include/uapi/linux/elf.h#L340-L343#define    ELFMAG0        0x7f        /* EI_MAG */#define    ELFMAG1        'E'#define    ELFMAG2        'L'#define    ELFMAG3        'F'

接下来的数 02 是与 Class 字段绝对应的,示意其体系结构,它能够是 32 位(=01) 或是 64 位(=02)的,此处显示 02 示意是 64 位的,再有 readelf 将其转换为 ELF64 进行展现。这里的取值同样能够在 Linux 内核源码中找到:

// include/uapi/linux/elf.h#L347-L349#define    ELFCLASSNONE    0        /* EI_CLASS */#define    ELFCLASS32    1#define    ELFCLASS64    2

再前面的两个 01 01 则是与 Data 字段和 Version 字段绝对应的,Data 有两个取值别离是 LSB(01)和 MSB(02),这里倒没什么必要开展。另外就是 Version 以后只有一个取值,即 01 。

// include/uapi/linux/elf.h#L352-L358#define ELFDATANONE    0        /* e_ident[EI_DATA] */#define ELFDATA2LSB    1#define ELFDATA2MSB    2#define EV_NONE        0        /* e_version, EI_VERSION */#define EV_CURRENT    1#define EV_NUM        2

接下来须要留神的就是我后面提到的对于偏移量的内容,即输入中的以下内容:

  Start of program headers:          64 (bytes into file)  Start of section headers:          400 (bytes into file)  Flags:                             0x0  Size of this header:               64 (bytes)  Size of program headers:           56 (bytes)  Number of program headers:         6  Size of section headers:           64 (bytes)  Number of section headers:         15

ELF 头总是在终点,在此例中接下来是程序头(Program Header),随后是节头(Section Header),这里的输入显示程序头是从 64 开始的,所以节头的地位就是:

64 + 56 * 6 = 400

与上述输入合乎,同理,节头的完结地位是:

400 + 15 * 64 = 1360

下一节内容中将用到这部分常识。

程序头

通过 readelf -l 能够看到其程序头,蕴含了若干段(Segment),内核看到这些段时,将调用 mmap syscall 来应用它们映射到虚拟地址空间。这部分不是本文的重点,咱们暂且跳过有个印象即可。

➜  kind git:(master) ✗ readelf -l ./bin/kind Elf file type is EXEC (Executable file)Entry point 0x46c460There are 6 program headers, starting at offset 64Program Headers:  Type           Offset             VirtAddr           PhysAddr                 FileSiz            MemSiz              Flags  Align  PHDR           0x0000000000000040 0x0000000000400040 0x0000000000400040                 0x0000000000000150 0x0000000000000150  R      0x1000  LOAD           0x0000000000000000 0x0000000000400000 0x0000000000400000                 0x0000000000333a75 0x0000000000333a75  R E    0x1000  LOAD           0x0000000000334000 0x0000000000734000 0x0000000000734000                 0x00000000002b3be8 0x00000000002b3be8  R      0x1000  LOAD           0x00000000005e8000 0x00000000009e8000 0x00000000009e8000                 0x0000000000020ac0 0x00000000000552d0  RW     0x1000  GNU_STACK      0x0000000000000000 0x0000000000000000 0x0000000000000000                 0x0000000000000000 0x0000000000000000  RW     0x8  LOOS+0x5041580 0x0000000000000000 0x0000000000000000 0x0000000000000000                 0x0000000000000000 0x0000000000000000         0x8 Section to Segment mapping:  Segment Sections...   00        01     .text    02     .rodata .typelink .itablink .gosymtab .gopclntab    03     .go.buildinfo .noptrdata .data .bss .noptrbss    04        05     

节头

应用 readelf -S 即可查看其节头,其构造如下:

// include/uapi/linux/elf.h#L317-L328typedef struct elf64_shdr {  Elf64_Word sh_name;        /* Section name, index in string tbl */  Elf64_Word sh_type;        /* Type of section */  Elf64_Xword sh_flags;        /* Miscellaneous section attributes */  Elf64_Addr sh_addr;        /* Section virtual addr at execution */  Elf64_Off sh_offset;        /* Section file offset */  Elf64_Xword sh_size;        /* Size of section in bytes */  Elf64_Word sh_link;        /* Index of another section */  Elf64_Word sh_info;        /* Additional section information */  Elf64_Xword sh_addralign;    /* Section alignment */  Elf64_Xword sh_entsize;    /* Entry size if section holds table */} Elf64_Shdr;

对照理论的命令输入,含意就很显著了。

➜  kind git:(master) ✗ readelf -S ./bin/kind There are 15 section headers, starting at offset 0x190:Section Headers:  [Nr] Name              Type             Address           Offset       Size              EntSize          Flags  Link  Info  Align  [ 0]                   NULL             0000000000000000  00000000       0000000000000000  0000000000000000           0     0     0  [ 1] .text             PROGBITS         0000000000401000  00001000       0000000000332a75  0000000000000000  AX       0     0     32  [ 2] .rodata           PROGBITS         0000000000734000  00334000       000000000011f157  0000000000000000   A       0     0     32  [ 3] .shstrtab         STRTAB           0000000000000000  00453160       00000000000000a4  0000000000000000           0     0     1  [ 4] .typelink         PROGBITS         0000000000853220  00453220       00000000000022a0  0000000000000000   A       0     0     32  [ 5] .itablink         PROGBITS         00000000008554c0  004554c0       0000000000000978  0000000000000000   A       0     0     32  [ 6] .gosymtab         PROGBITS         0000000000855e38  00455e38       0000000000000000  0000000000000000   A       0     0     1  [ 7] .gopclntab        PROGBITS         0000000000855e40  00455e40       0000000000191da8  0000000000000000   A       0     0     32  [ 8] .go.buildinfo     PROGBITS         00000000009e8000  005e8000       0000000000000020  0000000000000000  WA       0     0     16  [ 9] .noptrdata        PROGBITS         00000000009e8020  005e8020       0000000000017240  0000000000000000  WA       0     0     32  [10] .data             PROGBITS         00000000009ff260  005ff260       0000000000009850  0000000000000000  WA       0     0     32  [11] .bss              NOBITS           0000000000a08ac0  00608ac0       000000000002f170  0000000000000000  WA       0     0     32  [12] .noptrbss         NOBITS           0000000000a37c40  00637c40       0000000000005690  0000000000000000  WA       0     0     32  [13] .symtab           SYMTAB           0000000000000000  00609000       0000000000030a20  0000000000000018          14   208     8  [14] .strtab           STRTAB           0000000000000000  00639a20       000000000004178d  0000000000000000           0     0     1Key to Flags:  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),  L (link order), O (extra OS processing required), G (group), T (TLS),  C (compressed), x (unknown), o (OS specific), E (exclude),  l (large), p (processor specific)

Go 二进制文件探秘

本文中,咱们重点关注名为 .go.buildinfo 的局部。 应用 objdump 查看其具体内容:

➜  kind git:(master) ✗ objdump -s -j .go.buildinfo ./bin/kind./bin/kind:     file format elf64-x86-64Contents of section .go.buildinfo: 9e8000 ff20476f 20627569 6c64696e 663a0800  . Go buildinf:.. 9e8010 a0fc9f00 00000000 e0fc9f00 00000000  ................

这里咱们按程序来,先看到第一行的 16 个字节。

  • 前 14 个字节是魔术字节,必须为 \xff Go buildinf:
  • 第 15 字节示意其指针大小,这里的值为 0x08,示意 8 个字节;
  • 第 16 字节用于判断字节序是大端模式还是小端模式,非 0 为大端模式,0 为小端模式。

咱们持续看第 17 字节开始的内容。

Go 版本信息

后面咱们也看到了以后应用的字节序是小端模式,这里的地址应该是 0x009ffca0

咱们来取出 16 字节的内容:

➜  kind git:(master) ✗ objdump -s --start-address 0x009ffca0 --stop-address 0x009ffcb0 ./bin/kind   ./bin/kind:     file format elf64-x86-64Contents of section .data: 9ffca0 f5027d00 00000000 06000000 00000000  ..}.............

这里后面的 8 个字节是 Go 版本的信息,后 8 个字节是版本所占的大小(这里示意占 6 个字节)。

➜  kind git:(master) ✗ objdump -s --start-address  0x007d02f5 --stop-address 0x007d02fb ./bin/kind./bin/kind:     file format elf64-x86-64Contents of section .rodata: 7d02f5 676f31 2e3136                        go1.16

所以,如上所示,咱们拿到了构建此二进制文件所用的 Go 版本的信息,是用 Go 1.16 进行构建的。

Go Module 信息

后面咱们应用了 17~24 字节的信息,这次咱们持续往后应用。

➜  kind git:(master) ✗ objdump -s --start-address  0x009ffce0 --stop-address 0x009ffcf0 ./bin/kind       ./bin/kind:     file format elf64-x86-64Contents of section .data: 9ffce0 5a567e00 00000000 e6020000 00000000  ZV~.............

与后面获取 Go 版本信息时雷同,前 8 个字节是指针,后 8 个字节是其大小。也就是说从 0x007e565a 开始,大小为 0x000002e6 ,所以咱们能够拿到以下内容:

➜  kind git:(master) ✗ objdump -s --start-address  0x007e565a --stop-address 0x7e5940 ./bin/kind./bin/kind:     file format elf64-x86-64Contents of section .rodata: 7e565a 3077 af0c9274 080241e1 c107e6d6 18e6 0w...t..A....... 7e566a 7061 74680973 6967732e 6b38732e 696f path.sigs.k8s.io 7e567a 2f6b 696e640a 6d6f6409 73696773 2e6b /kind.mod.sigs.k 7e568a 3873 2e696f2f 6b696e64 09286465 7665 8s.io/kind.(deve 7e569a 6c29 090a6465 70096769 74687562 2e63 l)..dep.github.c 7e56aa 6f6d 2f427572 6e745375 7368692f 746f om/BurntSushi/to 7e56ba 6d6c 0976302e 332e3109 0a646570 0967 ml.v0.3.1..dep.g 7e56ca 6974 6875622e 636f6d2f 616c6573 7369 ithub.com/alessi 7e56da 6f2f 7368656c 6c657363 61706509 7631 o/shellescape.v1 7e56ea 2e34 2e31090a 64657009 67697468 7562 .4.1..dep.github 7e56fa 2e63 6f6d2f65 76616e70 68782f6a 736f .com/evanphx/jso 7e570a 6e2d 70617463 682f7635 0976352e 322e n-patch/v5.v5.2. 7e571a 3009 0a646570 09676974 6875622e 636f 0..dep.github.co 7e572a 6d2f 6d617474 6e2f676f 2d697361 7474 m/mattn/go-isatt 7e573a 7909 76302e30 2e313209 0a646570 0967 y.v0.0.12..dep.g 7e574a 6974 6875622e 636f6d2f 70656c6c 6574 ithub.com/pellet 7e575a 6965 722f676f 2d746f6d 6c097631 2e38 ier/go-toml.v1.8 7e576a 2e31 0968313a 314e6638 336f7270 726b .1.h1:1Nf83orprk 7e577a 4a79 6b6e5436 68377a62 75454755 456a JyknT6h7zbuEGUEj 7e578a 6379 566c4378 53554754 454e6d4e 4352 cyVlCxSUGTENmNCR 7e579a 4d3d 0a646570 09676974 6875622e 636f M=.dep.github.co 7e57aa 6d2f 706b672f 6572726f 72730976 302e m/pkg/errors.v0. 7e57ba 392e 31090a64 65700967 69746875 622e 9.1..dep.github. 7e57ca 636f 6d2f7370 6631332f 636f6272 6109 com/spf13/cobra. 7e57da 7631 2e312e31 090a6465 70096769 7468 v1.1.1..dep.gith 7e57ea 7562 2e636f6d 2f737066 31332f70 666c ub.com/spf13/pfl 7e57fa 6167 0976312e 302e3509 0a646570 0967 ag.v1.0.5..dep.g 7e580a 6f6c 616e672e 6f72672f 782f7379 7309 olang.org/x/sys. 7e581a 7630 2e302e30 2d323032 31303132 3431 v0.0.0-202101241 7e582a 3534 3534382d 32326461 36326531 3263 54548-22da62e12c 7e583a 3063 0968313a 56777967 55726e77 396a 0c.h1:VwygUrnw9j 7e584a 6e38 38633475 38474433 725a5162 7172 n88c4u8GD3rZQbqr 7e585a 502f 74676173 38387450 55624278 5172 P/tgas88tPUbBxQr 7e586a 6b3d 0a646570 09676f70 6b672e69 6e2f k=.dep.gopkg.in/ 7e587a 7961 6d6c2e76 32097632 2e322e38 090a yaml.v2.v2.2.8.. 7e588a 6465 7009676f 706b672e 696e2f79 616d dep.gopkg.in/yam 7e589a 6c2e 76330976 332e302e 302d3230 3231 l.v3.v3.0.0-2021 7e58aa 3031 30373139 32393232 2d343936 3534 0107192922-49654 7e58ba 3561 36333037 62096831 3a683871 446f 5a6307b.h1:h8qDo 7e58ca 7461 4550754a 4154724d 6d573034 4e43 taEPuJATrMmW04NC 7e58da 7767 37763232 61484832 38777770 6175 wg7v22aHH28wwpau 7e58ea 5568 4b394f6f 3d0a6465 70096b38 732e UhK9Oo=.dep.k8s. 7e58fa 696f 2f617069 6d616368 696e6572 7909 io/apimachinery. 7e590a 7630 2e32302e 32090a64 65700973 6967 v0.20.2..dep.sig 7e591a 732e 6b38732e 696f2f79 616d6c09 7631 s.k8s.io/yaml.v1 7e592a 2e32 2e30090a f9324331 86182072 0082 .2.0...2C1.. r.. 7e593a 4210 4116d8f2                        B.A...          

咱们胜利的拿到了其所依赖的 Modules 相干的信息,
这与咱们在文章结尾执行 go version -m ./bin/kind 是能够匹配上的,只不过这里的内容相当于是做了序列化。

具体实现

在后面的内容中,对于如何应用 readelf 和 objdump 命令获取二进制文件的的 Go 版本和 Module 信息就曾经波及到了其具体的原理。这里我来介绍下 Go 代码的实现。

节头的名称是硬编码在代码中的

//src/cmd/go/internal/version/exe.go#L106-L110    for _, s := range x.f.Sections {        if s.Name == ".go.buildinfo" {            return s.Addr        }    }

同时,魔术字节也是通过如下定义:

var buildInfoMagic = []byte("\xff Go buildinf:")

获取 Version 和 Module 相干信息的逻辑如下,在后面的内容中也曾经根本介绍过了,这里须要留神的也就是字节序相干的局部了。

    ptrSize := int(data[14])    bigEndian := data[15] != 0    var bo binary.ByteOrder    if bigEndian {        bo = binary.BigEndian    } else {        bo = binary.LittleEndian    }    var readPtr func([]byte) uint64    if ptrSize == 4 {        readPtr = func(b []byte) uint64 { return uint64(bo.Uint32(b)) }    } else {        readPtr = bo.Uint64    }    vers = readString(x, ptrSize, readPtr, readPtr(data[16:]))    if vers == "" {        return    }    mod = readString(x, ptrSize, readPtr, readPtr(data[16+ptrSize:]))    if len(mod) >= 33 && mod[len(mod)-17] == '\n' {        // Strip module framing.        mod = mod[16 : len(mod)-16]    } else {        mod = ""    }

总结

我在这篇文章中分享了如何从 Go 的二进制文件中获取构建它时所用的 Go 版本及它依赖的模块信息。如果对原理不感兴趣的话,间接通过 go version -m 二进制文件 即可获取相干的信息。

具体实现还是依赖于 ELF 文件格式中的相干信息,同时也介绍了 readelf 和 objdump 工具的根本应用,ELF 格局除了本文介绍的这种场景外,还有很多乏味的场景可用,比方为了平安进行逆向之类的。

另外,你可能会好奇从 Go 的二进制文件获取这些信息有什么作用。最间接的来说,能够用于安全漏洞扫描,比方查看其依赖项是否有安全漏洞;或是能够对依赖进行剖析(次要指:接触不到源代码的场景下)会比拟有用。


欢送订阅我的文章公众号【MoeLove】