Go-Embed

Starting from Go 1.16, the language introduced the go:embed directive, providing native support for embedding static files.
Embedded files can be accessed via an embed.FS pseudo-filesystem. They are read-only and packaged directly within the compiled binary.

Understanding How Go-Embed Works

The embed.FS file container structure:

type FS struct {
    files *[]file
}
type file struct {
    name string   // file name
    data string  // file content
    hash [16]byte // truncated SHA256 hash
}

tests
├── embedemo.go
└── misc
    ├── bdir
    │   └── sample.txt
    ├── sample.txt
    └── sample2.txt
package main
import (
    "embed"
    "fmt"
    "log"
)

//go:embed misc
var embedFiles embed.FS
func main() {
    content, err := embedFiles.ReadFile("misc/sample.txt")
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(string(content))
}

Use debugging to inspect what contents are stored in the files after compilation:

//go:embed <filename> is a compiler directive. During go build, it triggers the WriteEmbed function to process the directive:

Analyzing with Decompiler Tools

In the main function, calling embed.FS.ReadFile

Locate .rodata and display it using a hex viewer:

The files studied above are Mach-O for the ARM64 architecture. Later, I also compiled ELF and PE binaries, and their storage structures are the same. You can use the debug/* packages to parse files for each architecture, convert virtual addresses to file offsets, and thus extract the embedded files.

Building an Automated Tool

It has been open-sourced and can extract embedded files from PE, ELF, and Mach-O binaries: BreakOnCrash/go-embed-extractor.

The downside is that you have to manually locate the FS pointer 😅

@leonjza shared a method using the radare tool to find the FS structure pointer, which can then be combined with go-embed-extractor to extract embedded files.

Reference Links