mirror of
https://github.com/anchore/syft.git
synced 2026-11-05 08:56:41 +01:00
feat(executable): capture GNU ELF build IDs (#5426)
* feat(executable): capture GNU ELF build IDs Capture GNU ELF build IDs in Syft JSON so consumers can correlate otherwise unidentified binaries with known artifacts or build records. Read GNU build-ID notes from .note.gnu.build-id, with a 64 KiB read limit. Validate the note owner and type before recording the identifier. Add focused extraction and JSON round-trip tests and update the schema. Signed-off-by: Mario Corchero <mcorcherojim@bloomberg.net> * chore: bump JSON schema to 16.1.13 for executable identifiers 16.1.12 landed on main for coordinate fullPath, so the identifiers addition moves to the next version. Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * feat(executable): capture go build IDs from .note.go.buildid The go linker only writes a GNU build ID with -B or when linking externally, so most pure-Go binaries had no identifier. Every go ELF carries .note.go.buildid, captured as kind `go-build-id` in the form `go tool buildid` prints. Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * docs(executable): describe identifiers generically Identifier field docs (and the published schema) now give GNU hex and Go build IDs as examples instead of stating every value is a GNU build ID in lowercase hex, so the contract holds as more kinds are added. Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * fix(executable): keep identifier notes inside elfutil bounds Register `.note.gnu.build-id` and `.note.go.buildid` in elfutil's sectionsReadByName as that list asks, and never open a compressed identifier note. Real ones are SHF_ALLOC and cannot be compressed, so this drops the only decompression path into these reads. Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * test(executable): check identifiers from real linker output Cross-compiles a tiny go program with `-B` so the test sees a GNU build ID and a go build ID written by an actual linker and read through the cataloger entrypoint. Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> --------- Signed-off-by: Mario Corchero <mcorcherojim@bloomberg.net> Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> Co-authored-by: Mario Corchero <mcorcherojim@bloomberg.net> Co-authored-by: Alex Goodman <wagoodman@users.noreply.github.com>
This commit is contained in:
co-authored by
Mario Corchero
Alex Goodman
parent
fd712ee71a
commit
fa02ff2d26
@@ -3,7 +3,7 @@ package internal
|
|||||||
const (
|
const (
|
||||||
// JSONSchemaVersion is the current schema version output by the JSON encoder
|
// JSONSchemaVersion is the current schema version output by the JSON encoder
|
||||||
// This is roughly following the "SchemaVer" guidelines for versioning the JSON schema. Please see schema/json/README.md for details on how to increment.
|
// This is roughly following the "SchemaVer" guidelines for versioning the JSON schema. Please see schema/json/README.md for details on how to increment.
|
||||||
JSONSchemaVersion = "16.1.12"
|
JSONSchemaVersion = "16.1.13"
|
||||||
|
|
||||||
// Changelog
|
// Changelog
|
||||||
// 16.1.0 - reformulated the python pdm fields (added "URL" and removed the unused "path" field).
|
// 16.1.0 - reformulated the python pdm fields (added "URL" and removed the unused "path" field).
|
||||||
@@ -19,4 +19,5 @@ const (
|
|||||||
// 16.1.10 - add SafeTensorsModelInfo metadata type for the safetensors AI model cataloger
|
// 16.1.10 - add SafeTensorsModelInfo metadata type for the safetensors AI model cataloger
|
||||||
// 16.1.11 - add CpanDistribution and CpanUnpackedRelease metadata types for the perl catalogers
|
// 16.1.11 - add CpanDistribution and CpanUnpackedRelease metadata types for the perl catalogers
|
||||||
// 16.1.12 - add fullPath to file coordinates, naming the nested archive chain a file was found in (blank for files not within an archive)
|
// 16.1.12 - add fullPath to file coordinates, naming the nested archive chain a file was found in (blank for files not within an archive)
|
||||||
|
// 16.1.13 - add executable identifiers (ELF GNU and Go build IDs)
|
||||||
)
|
)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
"$id": "anchore.io/schema/syft/json/16.1.12/document",
|
"$id": "anchore.io/schema/syft/json/16.1.13/document",
|
||||||
"$ref": "#/$defs/Document",
|
"$ref": "#/$defs/Document",
|
||||||
"$defs": {
|
"$defs": {
|
||||||
"AlpmDbEntry": {
|
"AlpmDbEntry": {
|
||||||
@@ -1468,6 +1468,13 @@
|
|||||||
"elfSecurityFeatures": {
|
"elfSecurityFeatures": {
|
||||||
"$ref": "#/$defs/ELFSecurityFeatures",
|
"$ref": "#/$defs/ELFSecurityFeatures",
|
||||||
"description": "ELFSecurityFeatures contains ELF-specific security hardening information when Format is ELF."
|
"description": "ELFSecurityFeatures contains ELF-specific security hardening information when Format is ELF."
|
||||||
|
},
|
||||||
|
"identifiers": {
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/$defs/ExecutableIdentifier"
|
||||||
|
},
|
||||||
|
"type": "array",
|
||||||
|
"description": "Identifiers lists self-declared build identifiers found in the binary (e.g. ELF GNU build IDs or Go build IDs)."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"type": "object",
|
"type": "object",
|
||||||
@@ -1479,6 +1486,24 @@
|
|||||||
],
|
],
|
||||||
"description": "Executable contains metadata about binary files and their security features."
|
"description": "Executable contains metadata about binary files and their security features."
|
||||||
},
|
},
|
||||||
|
"ExecutableIdentifier": {
|
||||||
|
"properties": {
|
||||||
|
"kind": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Kind identifies the type and source of the identifier."
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Value is the identifier in the conventional text form for its Kind (e.g. lowercase hex for an ELF GNU build ID)."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"kind",
|
||||||
|
"value"
|
||||||
|
],
|
||||||
|
"description": "ExecutableIdentifier is a self-declared build identifier used to correlate a binary with its build or debug information."
|
||||||
|
},
|
||||||
"File": {
|
"File": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"id": {
|
"id": {
|
||||||
|
|||||||
@@ -2,11 +2,16 @@ package executable
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"debug/elf"
|
"debug/elf"
|
||||||
|
"encoding/binary"
|
||||||
|
"encoding/hex"
|
||||||
|
"io"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
"github.com/scylladb/go-set/strset"
|
"github.com/scylladb/go-set/strset"
|
||||||
|
|
||||||
|
intFile "github.com/anchore/syft/internal/file"
|
||||||
"github.com/anchore/syft/internal/log"
|
"github.com/anchore/syft/internal/log"
|
||||||
"github.com/anchore/syft/internal/unknown"
|
"github.com/anchore/syft/internal/unknown"
|
||||||
"github.com/anchore/syft/syft/file"
|
"github.com/anchore/syft/syft/file"
|
||||||
@@ -35,10 +40,132 @@ func findELFFeatures(data *file.Executable, reader unionreader.UnionReader) erro
|
|||||||
data.ELFSecurityFeatures = findELFSecurityFeatures(f)
|
data.ELFSecurityFeatures = findELFSecurityFeatures(f)
|
||||||
data.HasEntrypoint = elfHasEntrypoint(f)
|
data.HasEntrypoint = elfHasEntrypoint(f)
|
||||||
data.HasExports = elfHasExports(f)
|
data.HasExports = elfHasExports(f)
|
||||||
|
data.Identifiers = findELFIdentifiers(f)
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
// NT_GNU_BUILD_ID is not defined by debug/elf.
|
||||||
|
ntGNUBuildID = 3
|
||||||
|
// ELF_NOTE_GOBUILDID_TAG from cmd/link, the note type the go linker uses for the go build ID.
|
||||||
|
ntGoBuildID = 4
|
||||||
|
|
||||||
|
elfNoteOwnerGNU = "GNU"
|
||||||
|
elfNoteOwnerGo = "Go"
|
||||||
|
|
||||||
|
// bound decoded bytes read from any one identifier note section.
|
||||||
|
maxELFNoteBytes = 64 * intFile.KB
|
||||||
|
|
||||||
|
elfNoteHeaderSize = 12
|
||||||
|
)
|
||||||
|
|
||||||
|
type elfNote struct {
|
||||||
|
owner string
|
||||||
|
typ uint32
|
||||||
|
desc []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// elfIdentifierNote describes where one kind of identifier lives and how its descriptor becomes a value.
|
||||||
|
type elfIdentifierNote struct {
|
||||||
|
section string
|
||||||
|
owner string
|
||||||
|
typ uint32
|
||||||
|
kind file.ExecutableIdentifierKind
|
||||||
|
// encode returns the value for a descriptor, or "" to skip it.
|
||||||
|
encode func([]byte) string
|
||||||
|
}
|
||||||
|
|
||||||
|
var elfIdentifierNotes = []elfIdentifierNote{
|
||||||
|
{section: ".note.gnu.build-id", owner: elfNoteOwnerGNU, typ: ntGNUBuildID, kind: file.ELFGNUBuildID, encode: hex.EncodeToString},
|
||||||
|
// the go linker always writes this note, while it only writes a GNU build ID when asked (-B) or when linking externally
|
||||||
|
{section: ".note.go.buildid", owner: elfNoteOwnerGo, typ: ntGoBuildID, kind: file.GoBuildID, encode: goBuildIDValue},
|
||||||
|
}
|
||||||
|
|
||||||
|
func findELFIdentifiers(f *elf.File) []file.ExecutableIdentifier {
|
||||||
|
var ids []file.ExecutableIdentifier
|
||||||
|
for _, in := range elfIdentifierNotes {
|
||||||
|
ids = append(ids, findELFNoteIdentifiers(f, in)...)
|
||||||
|
}
|
||||||
|
return ids
|
||||||
|
}
|
||||||
|
|
||||||
|
func findELFNoteIdentifiers(f *elf.File, in elfIdentifierNote) []file.ExecutableIdentifier {
|
||||||
|
s := f.Section(in.section)
|
||||||
|
// real identifier notes are SHF_ALLOC, which the gABI forbids compressing, so a compressed one is
|
||||||
|
// hostile or broken and is never decompressed
|
||||||
|
if s == nil || s.Type != elf.SHT_NOTE || s.Flags&elf.SHF_COMPRESSED != 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
data, err := io.ReadAll(io.LimitReader(s.Open(), maxELFNoteBytes))
|
||||||
|
if err != nil {
|
||||||
|
log.WithFields("error", err, "section", in.section).Trace("unable to read ELF identifier note")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var ids []file.ExecutableIdentifier
|
||||||
|
seen := strset.New()
|
||||||
|
for _, n := range parseELFNotes(data, s.Addralign, f.ByteOrder) {
|
||||||
|
if n.owner != in.owner || n.typ != in.typ || len(n.desc) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
value := in.encode(n.desc)
|
||||||
|
if value == "" || seen.Has(value) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen.Add(value)
|
||||||
|
ids = append(ids, file.ExecutableIdentifier{Kind: in.kind, Value: value})
|
||||||
|
}
|
||||||
|
return ids
|
||||||
|
}
|
||||||
|
|
||||||
|
// goBuildIDValue keeps the go build ID as the text the go toolchain prints for it (e.g. "go tool buildid").
|
||||||
|
func goBuildIDValue(desc []byte) string {
|
||||||
|
if !utf8.Valid(desc) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return string(desc)
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseELFNotes decodes the Elf_Nhdr records in a note region, stopping at the first truncated record.
|
||||||
|
func parseELFNotes(data []byte, align uint64, order binary.ByteOrder) []elfNote {
|
||||||
|
// Some GNU property note regions use 8-byte alignment instead of 4.
|
||||||
|
if align != 8 {
|
||||||
|
align = 4
|
||||||
|
}
|
||||||
|
|
||||||
|
var notes []elfNote
|
||||||
|
for len(data) >= elfNoteHeaderSize {
|
||||||
|
nameSize := uint64(order.Uint32(data[0:4]))
|
||||||
|
descSize := uint64(order.Uint32(data[4:8]))
|
||||||
|
typ := order.Uint32(data[8:12])
|
||||||
|
|
||||||
|
nameEnd := elfNoteHeaderSize + nameSize
|
||||||
|
descStart := alignUp(nameEnd, align)
|
||||||
|
descEnd := descStart + descSize
|
||||||
|
if descEnd > uint64(len(data)) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
notes = append(notes, elfNote{
|
||||||
|
owner: strings.TrimRight(string(data[elfNoteHeaderSize:nameEnd]), "\x00"),
|
||||||
|
typ: typ,
|
||||||
|
desc: data[descStart:descEnd],
|
||||||
|
})
|
||||||
|
|
||||||
|
next := alignUp(descEnd, align)
|
||||||
|
if next >= uint64(len(data)) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
data = data[next:]
|
||||||
|
}
|
||||||
|
return notes
|
||||||
|
}
|
||||||
|
|
||||||
|
func alignUp(n, align uint64) uint64 {
|
||||||
|
return (n + align - 1) &^ (align - 1)
|
||||||
|
}
|
||||||
|
|
||||||
func findELFSecurityFeatures(f *elf.File) *file.ELFSecurityFeatures {
|
func findELFSecurityFeatures(f *elf.File) *file.ELFSecurityFeatures {
|
||||||
return &file.ELFSecurityFeatures{
|
return &file.ELFSecurityFeatures{
|
||||||
SymbolTableStripped: isElfSymbolTableStripped(f),
|
SymbolTableStripped: isElfSymbolTableStripped(f),
|
||||||
|
|||||||
@@ -4,8 +4,12 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"debug/elf"
|
"debug/elf"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
|
"encoding/hex"
|
||||||
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/google/go-cmp/cmp"
|
"github.com/google/go-cmp/cmp"
|
||||||
@@ -13,6 +17,7 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/anchore/syft/syft/file"
|
"github.com/anchore/syft/syft/file"
|
||||||
|
"github.com/anchore/syft/syft/internal/elfutil"
|
||||||
"github.com/anchore/syft/syft/internal/unionreader"
|
"github.com/anchore/syft/syft/internal/unionreader"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -285,6 +290,12 @@ func Test_hasElfDynTag(t *testing.T) {
|
|||||||
|
|
||||||
// elfWithDynamicSection builds a little-endian ELF holding only a null section and a SHT_DYNAMIC section.
|
// elfWithDynamicSection builds a little-endian ELF holding only a null section and a SHT_DYNAMIC section.
|
||||||
func elfWithDynamicSection(t *testing.T, class elf.Class, dynamic []byte) []byte {
|
func elfWithDynamicSection(t *testing.T, class elf.Class, dynamic []byte) []byte {
|
||||||
|
t.Helper()
|
||||||
|
return elfWithSection(t, class, elf.SHT_DYNAMIC, 1, dynamic)
|
||||||
|
}
|
||||||
|
|
||||||
|
// elfWithSection builds a little-endian ELF holding only a null section and one section of the given type.
|
||||||
|
func elfWithSection(t *testing.T, class elf.Class, typ elf.SectionType, align uint64, data []byte) []byte {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
ident := [elf.EI_NIDENT]byte{0x7f, 'E', 'L', 'F', byte(class), byte(elf.ELFDATA2LSB), byte(elf.EV_CURRENT)}
|
ident := [elf.EI_NIDENT]byte{0x7f, 'E', 'L', 'F', byte(class), byte(elf.ELFDATA2LSB), byte(elf.EV_CURRENT)}
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
@@ -300,7 +311,7 @@ func elfWithDynamicSection(t *testing.T, class elf.Class, dynamic []byte) []byte
|
|||||||
Shoff: uint32(ehsize), Ehsize: uint16(ehsize), Shentsize: uint16(shentsize), Shnum: 2,
|
Shoff: uint32(ehsize), Ehsize: uint16(ehsize), Shentsize: uint16(shentsize), Shnum: 2,
|
||||||
})
|
})
|
||||||
write(elf.Section32{})
|
write(elf.Section32{})
|
||||||
write(elf.Section32{Type: uint32(elf.SHT_DYNAMIC), Off: uint32(dataOff), Size: uint32(len(dynamic)), Addralign: 1})
|
write(elf.Section32{Type: uint32(typ), Off: uint32(dataOff), Size: uint32(len(data)), Addralign: uint32(align)})
|
||||||
} else {
|
} else {
|
||||||
ehsize, shentsize := binary.Size(elf.Header64{}), binary.Size(elf.Section64{})
|
ehsize, shentsize := binary.Size(elf.Header64{}), binary.Size(elf.Section64{})
|
||||||
dataOff := ehsize + 2*shentsize
|
dataOff := ehsize + 2*shentsize
|
||||||
@@ -309,8 +320,208 @@ func elfWithDynamicSection(t *testing.T, class elf.Class, dynamic []byte) []byte
|
|||||||
Shoff: uint64(ehsize), Ehsize: uint16(ehsize), Shentsize: uint16(shentsize), Shnum: 2,
|
Shoff: uint64(ehsize), Ehsize: uint16(ehsize), Shentsize: uint16(shentsize), Shnum: 2,
|
||||||
})
|
})
|
||||||
write(elf.Section64{})
|
write(elf.Section64{})
|
||||||
write(elf.Section64{Type: uint32(elf.SHT_DYNAMIC), Off: uint64(dataOff), Size: uint64(len(dynamic)), Addralign: 1})
|
write(elf.Section64{Type: uint32(typ), Off: uint64(dataOff), Size: uint64(len(data)), Addralign: align})
|
||||||
}
|
}
|
||||||
buf.Write(dynamic)
|
buf.Write(data)
|
||||||
return buf.Bytes()
|
return buf.Bytes()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// encodeELFNote encodes a single little-endian Elf_Nhdr record, padding name and descriptor to align.
|
||||||
|
func encodeELFNote(owner string, typ uint32, desc []byte, align int) []byte {
|
||||||
|
pad := func(b []byte) []byte {
|
||||||
|
for len(b)%align != 0 {
|
||||||
|
b = append(b, 0)
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
name := append([]byte(owner), 0)
|
||||||
|
out := binary.LittleEndian.AppendUint32(nil, uint32(len(name)))
|
||||||
|
out = binary.LittleEndian.AppendUint32(out, uint32(len(desc)))
|
||||||
|
out = binary.LittleEndian.AppendUint32(out, typ)
|
||||||
|
out = pad(append(out, name...))
|
||||||
|
return pad(append(out, desc...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_findELFIdentifiers(t *testing.T) {
|
||||||
|
sha1ID := bytes.Repeat([]byte{0xab}, 20)
|
||||||
|
customID := []byte{0xde, 0xad, 0xbe, 0xef}
|
||||||
|
longID := bytes.Repeat([]byte{0x01}, 32)
|
||||||
|
|
||||||
|
buildID := func(v []byte) file.ExecutableIdentifier {
|
||||||
|
return file.ExecutableIdentifier{Kind: file.ELFGNUBuildID, Value: hex.EncodeToString(v)}
|
||||||
|
}
|
||||||
|
const ntGNUABITag, ntGNUProperty = 1, 5
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
elf []byte
|
||||||
|
sectionName string
|
||||||
|
want []file.ExecutableIdentifier
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "sha1-length build id",
|
||||||
|
elf: elfWithSection(t, elf.ELFCLASS64, elf.SHT_NOTE, 4, encodeELFNote("GNU", ntGNUBuildID, sha1ID, 4)),
|
||||||
|
sectionName: ".note.gnu.build-id",
|
||||||
|
want: []file.ExecutableIdentifier{buildID(sha1ID)},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "caller-chosen short build id",
|
||||||
|
elf: elfWithSection(t, elf.ELFCLASS32, elf.SHT_NOTE, 4, encodeELFNote("GNU", ntGNUBuildID, customID, 4)),
|
||||||
|
sectionName: ".note.gnu.build-id",
|
||||||
|
want: []file.ExecutableIdentifier{buildID(customID)},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "build id after other notes in the same section",
|
||||||
|
elf: elfWithSection(t, elf.ELFCLASS64, elf.SHT_NOTE, 4, append(
|
||||||
|
encodeELFNote("GNU", ntGNUABITag, make([]byte, 16), 4),
|
||||||
|
encodeELFNote("GNU", ntGNUBuildID, longID, 4)...,
|
||||||
|
)),
|
||||||
|
sectionName: ".note.gnu.build-id",
|
||||||
|
want: []file.ExecutableIdentifier{buildID(longID)},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "8-byte aligned note section",
|
||||||
|
elf: elfWithSection(t, elf.ELFCLASS64, elf.SHT_NOTE, 8, append(
|
||||||
|
encodeELFNote("GNU", ntGNUProperty, make([]byte, 12), 8),
|
||||||
|
encodeELFNote("GNU", ntGNUBuildID, customID, 8)...,
|
||||||
|
)),
|
||||||
|
sectionName: ".note.gnu.build-id",
|
||||||
|
want: []file.ExecutableIdentifier{buildID(customID)},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "duplicate build ids",
|
||||||
|
elf: elfWithSection(t, elf.ELFCLASS64, elf.SHT_NOTE, 4, bytes.Repeat(
|
||||||
|
encodeELFNote("GNU", ntGNUBuildID, customID, 4), 2,
|
||||||
|
)),
|
||||||
|
sectionName: ".note.gnu.build-id",
|
||||||
|
want: []file.ExecutableIdentifier{buildID(customID)},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "build id in another section is ignored",
|
||||||
|
elf: elfWithSection(t, elf.ELFCLASS64, elf.SHT_NOTE, 4, encodeELFNote("GNU", ntGNUBuildID, sha1ID, 4)),
|
||||||
|
sectionName: ".note.other",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "build-id section with another section type is ignored",
|
||||||
|
elf: elfWithSection(t, elf.ELFCLASS64, elf.SHT_PROGBITS, 4, encodeELFNote("GNU", ntGNUBuildID, sha1ID, 4)),
|
||||||
|
sectionName: ".note.gnu.build-id",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "build-id section with another owner is ignored",
|
||||||
|
elf: elfWithSection(t, elf.ELFCLASS64, elf.SHT_NOTE, 4, encodeELFNote("FDO", ntGNUBuildID, sha1ID, 4)),
|
||||||
|
sectionName: ".note.gnu.build-id",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "build-id section with another note type is ignored",
|
||||||
|
elf: elfWithSection(t, elf.ELFCLASS64, elf.SHT_NOTE, 4, encodeELFNote("GNU", ntGNUABITag, sha1ID, 4)),
|
||||||
|
sectionName: ".note.gnu.build-id",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty descriptor is ignored",
|
||||||
|
elf: elfWithSection(t, elf.ELFCLASS64, elf.SHT_NOTE, 4, encodeELFNote("GNU", ntGNUBuildID, nil, 4)),
|
||||||
|
sectionName: ".note.gnu.build-id",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "truncated descriptor is ignored",
|
||||||
|
elf: elfWithSection(t, elf.ELFCLASS64, elf.SHT_NOTE, 4, encodeELFNote("GNU", ntGNUBuildID, sha1ID, 4)[:20]),
|
||||||
|
sectionName: ".note.gnu.build-id",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "go build id",
|
||||||
|
elf: elfWithSection(t, elf.ELFCLASS64, elf.SHT_NOTE, 4, encodeELFNote("Go\x00", ntGoBuildID, []byte("action/content"), 4)),
|
||||||
|
sectionName: ".note.go.buildid",
|
||||||
|
want: []file.ExecutableIdentifier{{Kind: file.GoBuildID, Value: "action/content"}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "go build id that is not utf-8 is ignored",
|
||||||
|
elf: elfWithSection(t, elf.ELFCLASS64, elf.SHT_NOTE, 4, encodeELFNote("Go\x00", ntGoBuildID, []byte{0xff, 0xfe}, 4)),
|
||||||
|
sectionName: ".note.go.buildid",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "gnu build id note in the go section is ignored",
|
||||||
|
elf: elfWithSection(t, elf.ELFCLASS64, elf.SHT_NOTE, 4, encodeELFNote("GNU", ntGNUBuildID, sha1ID, 4)),
|
||||||
|
sectionName: ".note.go.buildid",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no build-id section",
|
||||||
|
elf: elfWithDynamicSection(t, elf.ELFCLASS64, nil),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
f, err := elfutil.NewFile(bytes.NewReader(tt.elf))
|
||||||
|
require.NoError(t, err)
|
||||||
|
f.Sections[1].Name = tt.sectionName
|
||||||
|
assert.Equal(t, tt.want, findELFIdentifiers(f))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_findELFIdentifiers_compressedSectionIgnored(t *testing.T) {
|
||||||
|
f, err := elfutil.NewFile(bytes.NewReader(elfWithSection(t, elf.ELFCLASS64, elf.SHT_NOTE, 4, encodeELFNote("GNU", ntGNUBuildID, []byte{0xde, 0xad}, 4))))
|
||||||
|
require.NoError(t, err)
|
||||||
|
f.Sections[1].Name = ".note.gnu.build-id"
|
||||||
|
f.Sections[1].Flags |= elf.SHF_COMPRESSED
|
||||||
|
assert.Nil(t, findELFIdentifiers(f))
|
||||||
|
}
|
||||||
|
|
||||||
|
type countingNoteReader struct {
|
||||||
|
io.ReaderAt
|
||||||
|
bytesRead int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *countingNoteReader) ReadAt(p []byte, off int64) (int, error) {
|
||||||
|
n, err := r.ReaderAt.ReadAt(p, off)
|
||||||
|
r.bytesRead += n
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_findELFIdentifiers_readLimit(t *testing.T) {
|
||||||
|
id := encodeELFNote("GNU", ntGNUBuildID, []byte{0xde, 0xad, 0xbe, 0xef}, 4)
|
||||||
|
data := append(id, make([]byte, maxELFNoteBytes)...)
|
||||||
|
r := &countingNoteReader{ReaderAt: bytes.NewReader(elfWithSection(t, elf.ELFCLASS64, elf.SHT_NOTE, 4, data))}
|
||||||
|
f, err := elfutil.NewFile(r)
|
||||||
|
require.NoError(t, err)
|
||||||
|
f.Sections[1].Name = ".note.gnu.build-id"
|
||||||
|
r.bytesRead = 0
|
||||||
|
|
||||||
|
assert.Equal(t, []file.ExecutableIdentifier{{Kind: file.ELFGNUBuildID, Value: "deadbeef"}}, findELFIdentifiers(f))
|
||||||
|
assert.Equal(t, maxELFNoteBytes, r.bytesRead)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_parseELFNotes_bigEndian(t *testing.T) {
|
||||||
|
data := []byte{0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 3, 'G', 'N', 'U', 0, 0xde, 0xad, 0xbe, 0xef}
|
||||||
|
assert.Equal(t, []elfNote{{owner: "GNU", typ: ntGNUBuildID, desc: data[16:]}}, parseELFNotes(data, 4, binary.BigEndian))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test_findELFFeatures_realLinkerIdentifiers proves identifiers are picked up from real linker output, not just
|
||||||
|
// hand-built notes. The go toolchain is the one linker every test environment has, and -B makes it emit a GNU
|
||||||
|
// build ID next to the go build ID it always writes.
|
||||||
|
func Test_findELFFeatures_realLinkerIdentifiers(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("builds a go binary")
|
||||||
|
}
|
||||||
|
dir := t.TempDir()
|
||||||
|
src := filepath.Join(dir, "main.go")
|
||||||
|
bin := filepath.Join(dir, "main")
|
||||||
|
require.NoError(t, os.WriteFile(src, []byte("package main\n\nfunc main() {}\n"), 0o600))
|
||||||
|
|
||||||
|
build := exec.Command("go", "build", "-trimpath", "-ldflags=-B 0x0123456789abcdef", "-o", bin, src)
|
||||||
|
build.Env = append(os.Environ(), "GOOS=linux", "GOARCH=amd64", "CGO_ENABLED=0", "GOFLAGS=", "GOWORK=off")
|
||||||
|
out, err := build.CombinedOutput()
|
||||||
|
require.NoError(t, err, string(out))
|
||||||
|
|
||||||
|
goBuildID, err := exec.Command("go", "tool", "buildid", bin).Output()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
f, err := os.Open(bin)
|
||||||
|
require.NoError(t, err)
|
||||||
|
t.Cleanup(func() { _ = f.Close() })
|
||||||
|
|
||||||
|
var data file.Executable
|
||||||
|
require.NoError(t, findELFFeatures(&data, f))
|
||||||
|
assert.Equal(t, []file.ExecutableIdentifier{
|
||||||
|
{Kind: file.ELFGNUBuildID, Value: "0123456789abcdef"},
|
||||||
|
{Kind: file.GoBuildID, Value: strings.TrimSpace(string(goBuildID))},
|
||||||
|
}, data.Identifiers)
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ type (
|
|||||||
|
|
||||||
// RelocationReadOnly indicates the RELRO security protection level applied to an ELF binary.
|
// RelocationReadOnly indicates the RELRO security protection level applied to an ELF binary.
|
||||||
RelocationReadOnly string
|
RelocationReadOnly string
|
||||||
|
|
||||||
|
// ExecutableIdentifierKind identifies the type and source of an executable identifier.
|
||||||
|
ExecutableIdentifierKind string
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -16,6 +19,9 @@ const (
|
|||||||
RelocationReadOnlyNone RelocationReadOnly = "none" // no RELRO protection
|
RelocationReadOnlyNone RelocationReadOnly = "none" // no RELRO protection
|
||||||
RelocationReadOnlyPartial RelocationReadOnly = "partial" // partial RELRO protection
|
RelocationReadOnlyPartial RelocationReadOnly = "partial" // partial RELRO protection
|
||||||
RelocationReadOnlyFull RelocationReadOnly = "full" // full RELRO protection
|
RelocationReadOnlyFull RelocationReadOnly = "full" // full RELRO protection
|
||||||
|
|
||||||
|
ELFGNUBuildID ExecutableIdentifierKind = "elf-gnu-build-id" // GNU build ID from an ELF .note.gnu.build-id note
|
||||||
|
GoBuildID ExecutableIdentifierKind = "go-build-id" // build ID written by the go linker
|
||||||
)
|
)
|
||||||
|
|
||||||
// Executable contains metadata about binary files and their security features.
|
// Executable contains metadata about binary files and their security features.
|
||||||
@@ -34,6 +40,19 @@ type Executable struct {
|
|||||||
|
|
||||||
// ELFSecurityFeatures contains ELF-specific security hardening information when Format is ELF.
|
// ELFSecurityFeatures contains ELF-specific security hardening information when Format is ELF.
|
||||||
ELFSecurityFeatures *ELFSecurityFeatures `json:"elfSecurityFeatures,omitempty" yaml:"elfSecurityFeatures" mapstructure:"elfSecurityFeatures"`
|
ELFSecurityFeatures *ELFSecurityFeatures `json:"elfSecurityFeatures,omitempty" yaml:"elfSecurityFeatures" mapstructure:"elfSecurityFeatures"`
|
||||||
|
|
||||||
|
// Identifiers lists self-declared build identifiers found in the binary (e.g. ELF GNU build IDs or Go build IDs).
|
||||||
|
Identifiers []ExecutableIdentifier `json:"identifiers,omitempty" yaml:"identifiers" mapstructure:"identifiers"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExecutableIdentifier is a self-declared build identifier used to correlate a binary with its build or debug information.
|
||||||
|
// It can survive stripping; use file digests to verify content.
|
||||||
|
type ExecutableIdentifier struct {
|
||||||
|
// Kind identifies the type and source of the identifier.
|
||||||
|
Kind ExecutableIdentifierKind `json:"kind" yaml:"kind" mapstructure:"kind"`
|
||||||
|
|
||||||
|
// Value is the identifier in the conventional text form for its Kind (e.g. lowercase hex for an ELF GNU build ID).
|
||||||
|
Value string `json:"value" yaml:"value" mapstructure:"value"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ELFSecurityFeatures captures security hardening and protection mechanisms in ELF binaries.
|
// ELFSecurityFeatures captures security hardening and protection mechanisms in ELF binaries.
|
||||||
|
|||||||
@@ -219,6 +219,9 @@ func Test_encodeDecodeFileMetadata(t *testing.T) {
|
|||||||
Executables: map[file.Coordinates]file.Executable{
|
Executables: map[file.Coordinates]file.Executable{
|
||||||
c: {
|
c: {
|
||||||
Format: file.ELF,
|
Format: file.ELF,
|
||||||
|
Identifiers: []file.ExecutableIdentifier{
|
||||||
|
{Kind: file.ELFGNUBuildID, Value: "0123456789abcdef"},
|
||||||
|
},
|
||||||
ELFSecurityFeatures: &file.ELFSecurityFeatures{
|
ELFSecurityFeatures: &file.ELFSecurityFeatures{
|
||||||
SymbolTableStripped: false,
|
SymbolTableStripped: false,
|
||||||
StackCanary: boolRef(true),
|
StackCanary: boolRef(true),
|
||||||
@@ -288,4 +291,11 @@ func Test_encodeDecodeFileMetadata(t *testing.T) {
|
|||||||
assert.Equal(t, internal.JSONSchemaVersion, decodedVersion)
|
assert.Equal(t, internal.JSONSchemaVersion, decodedVersion)
|
||||||
|
|
||||||
require.Equal(t, s, *got)
|
require.Equal(t, s, *got)
|
||||||
|
|
||||||
|
executable := s.Artifacts.Executables[c]
|
||||||
|
executable.Identifiers = nil
|
||||||
|
s.Artifacts.Executables[c] = executable
|
||||||
|
buf.Reset()
|
||||||
|
require.NoError(t, enc.Encode(buf, s))
|
||||||
|
assert.NotContains(t, buf.String(), `"identifiers"`)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,6 +82,9 @@ var sectionsReadByName = map[string]struct{}{
|
|||||||
".gopclntab": {},
|
".gopclntab": {},
|
||||||
".note.package": {},
|
".note.package": {},
|
||||||
".modinfo": {},
|
".modinfo": {},
|
||||||
|
// identifier notes read by the executable cataloger
|
||||||
|
".note.gnu.build-id": {},
|
||||||
|
".note.go.buildid": {},
|
||||||
}
|
}
|
||||||
|
|
||||||
// wire sizes of the structures we decode, which differ between the two ELF classes
|
// wire sizes of the structures we decode, which differ between the two ELF classes
|
||||||
|
|||||||
Reference in New Issue
Block a user