mirror of
https://github.com/anchore/syft.git
synced 2026-08-19 08:38:25 +02:00
* fix(elf): bound compressed ELF section reads `debug/elf` takes a section's decompressed size from that section's own compression header, and a highly compressible stream really does deliver the bytes that header promises, so `internal/saferio` does not help: it faithfully allocates every one of them. A 2MB input file drives `elf.NewFile` to allocate over 10GB and return no error, which is a fatal OOM rather than a recoverable panic. Which sections get read is not up to the caller. `elf.NewFile` always reads the section-name string table, and `File.Symbols` reads `.symtab` plus whatever section its `Link` field points at, so being selective about sections is not enough to avoid it. New `elfutil.NewFile` is a drop-in for `elf.NewFile` that rejects a declared decompressed size over 128MB. Every production call site goes through it, and a ruleguard rule keeps the next one from going direct. The check runs in two parts, since `debug/elf` expands sections at two different times. The section-name string table is the only one `elf.NewFile` expands itself, so it is checked against the raw bytes before the call; everything else is expanded lazily by `(*Section).Open` and is checked after the parse, where names, types and decompressed sizes are already resolved. Only the sections syft can actually reach are bounded, which keeps the guard from costing real binaries. DWARF is excluded since nothing calls `File.DWARF`, so a large compressed `.debug_info` no longer skips the whole file, and sections `debug/elf` will not decompress anyway (`SHF_ALLOC`, `SHT_NOBITS`) are left alone. The legacy `.zdebug` form is matched on the section name the way `debug/elf` gates it rather than on the `ZLIB` magic, so an ordinary section starting with those four bytes is not mistaken for a compressed one. Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * fix(elf): gate debug/buildinfo behind the compressed-section check `debug/buildinfo.Read` opens ELF files with `debug/elf` itself, and `elf.NewFile` expands the section-name string table as it parses, so the golang cataloger was still reachable by the same bomb `elfutil` exists to stop. A 261KB fixture drove 1.4GB of allocation through `buildinfo.Read` and returned no error. `elfutil.CheckSectionNameTable` is now exported for that case: callers that cannot use `NewFile` because the `debug/elf` call is made for them inside another package. Both `buildinfo.Read` call sites go through it, including the UPX-decompressed one. Also corrects claims that did not hold up: - the package doc's 2MB-to-10GB figure is not reachable with zlib (~1000:1), so it now carries the measured 510KB-to-2.6GB, and names zstd's 32767:1 since that is what makes the small inputs possible - `.go.buildinfo` was listed as a hot-path section elfutil covers, but it is read through `debug/buildinfo` and never touches `Section.Data` - the graalvm comment claimed routing size rejections away from `*elf.FormatError` improved reporting; both branches are skipped by the caller and only the FormatError branch logs, so it did the opposite - `sharedLibraries` logged short and truncated files as real ELF failures, since `debug/elf` returns a bare `io.EOF` rather than an `*elf.FormatError` for those Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * added test comments around the negative cases Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * additional tests Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * better decomposition and comments Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * use a less brittle constant for error detection Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> --------- Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
268 lines
6.8 KiB
Go
268 lines
6.8 KiB
Go
package executable
|
|
|
|
import (
|
|
"debug/elf"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/scylladb/go-set/strset"
|
|
|
|
"github.com/anchore/syft/internal/log"
|
|
"github.com/anchore/syft/internal/unknown"
|
|
"github.com/anchore/syft/syft/file"
|
|
"github.com/anchore/syft/syft/internal/elfutil"
|
|
"github.com/anchore/syft/syft/internal/unionreader"
|
|
)
|
|
|
|
func findELFFeatures(data *file.Executable, reader unionreader.UnionReader) error {
|
|
f, err := elfutil.NewFile(reader)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
libs, err := f.ImportedLibraries()
|
|
if err != nil {
|
|
log.WithFields("error", err).Trace("unable to read imported libraries from elf file")
|
|
err = unknown.Joinf(err, "unable to read imported libraries from elf file: %w", err)
|
|
libs = nil
|
|
}
|
|
|
|
if libs == nil {
|
|
libs = []string{}
|
|
}
|
|
|
|
data.ImportedLibraries = libs
|
|
data.ELFSecurityFeatures = findELFSecurityFeatures(f)
|
|
data.HasEntrypoint = elfHasEntrypoint(f)
|
|
data.HasExports = elfHasExports(f)
|
|
|
|
return err
|
|
}
|
|
|
|
func findELFSecurityFeatures(f *elf.File) *file.ELFSecurityFeatures {
|
|
return &file.ELFSecurityFeatures{
|
|
SymbolTableStripped: isElfSymbolTableStripped(f),
|
|
StackCanary: checkElfStackCanary(f),
|
|
NoExecutable: checkElfNXProtection(f),
|
|
RelocationReadOnly: checkElfRelROProtection(f),
|
|
PositionIndependentExecutable: isELFPIE(f),
|
|
DynamicSharedObject: isELFDSO(f),
|
|
LlvmSafeStack: checkLLVMSafeStack(f),
|
|
LlvmControlFlowIntegrity: checkLLVMControlFlowIntegrity(f),
|
|
ClangFortifySource: checkClangFortifySource(f),
|
|
}
|
|
}
|
|
|
|
func isElfSymbolTableStripped(file *elf.File) bool {
|
|
return file.Section(".symtab") == nil
|
|
}
|
|
|
|
func checkElfStackCanary(file *elf.File) *bool {
|
|
return hasAnyDynamicSymbols(file, "__stack_chk_fail", "__stack_chk_guard")
|
|
}
|
|
|
|
func hasAnyDynamicSymbols(file *elf.File, symbolNames ...string) *bool {
|
|
dynSyms, err := file.DynamicSymbols()
|
|
if err != nil {
|
|
log.WithFields("error", err).Trace("unable to read dynamic symbols from elf file")
|
|
return nil
|
|
}
|
|
|
|
nameSet := strset.New(symbolNames...)
|
|
|
|
for _, sym := range dynSyms {
|
|
if nameSet.Has(sym.Name) {
|
|
return boolRef(true)
|
|
}
|
|
}
|
|
return boolRef(false)
|
|
}
|
|
|
|
func boolRef(b bool) *bool {
|
|
return &b
|
|
}
|
|
|
|
func checkElfNXProtection(file *elf.File) bool {
|
|
// find the program headers until you find the GNU_STACK segment
|
|
for _, prog := range file.Progs {
|
|
if prog.Type == elf.PT_GNU_STACK {
|
|
// check if the GNU_STACK segment is executable
|
|
return prog.Flags&elf.PF_X == 0
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func checkElfRelROProtection(f *elf.File) file.RelocationReadOnly {
|
|
// background on relro https://www.redhat.com/en/blog/hardening-elf-binaries-using-relocation-read-only-relro
|
|
hasRelro := false
|
|
hasBindNow := hasBindNowDynTagOrFlag(f)
|
|
|
|
for _, prog := range f.Progs {
|
|
if prog.Type == elf.PT_GNU_RELRO {
|
|
hasRelro = true
|
|
break
|
|
}
|
|
}
|
|
|
|
switch {
|
|
case hasRelro && hasBindNow:
|
|
return file.RelocationReadOnlyFull
|
|
case hasRelro:
|
|
return file.RelocationReadOnlyPartial
|
|
default:
|
|
return file.RelocationReadOnlyNone
|
|
}
|
|
}
|
|
|
|
func hasBindNowDynTagOrFlag(f *elf.File) bool {
|
|
if hasElfDynTag(f, elf.DT_BIND_NOW) {
|
|
// support older binaries...
|
|
return true
|
|
}
|
|
|
|
// "DT_BIND_NOW ... use has been superseded by the DF_BIND_NOW flag"
|
|
// source: https://refspecs.linuxbase.org/elf/gabi4+/ch5.dynamic.html
|
|
return hasElfDynFlag(f, elf.DF_BIND_NOW)
|
|
}
|
|
|
|
func hasElfDynFlag(f *elf.File, flag elf.DynFlag) bool {
|
|
vals, err := f.DynValue(elf.DT_FLAGS)
|
|
if err != nil {
|
|
log.WithFields("error", err).Trace("unable to read DT_FLAGS from elf file")
|
|
return false
|
|
}
|
|
for _, val := range vals {
|
|
if val&uint64(flag) != 0 {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func hasElfDynFlag1(f *elf.File, flag elf.DynFlag1) bool {
|
|
vals, err := f.DynValue(elf.DT_FLAGS_1)
|
|
if err != nil {
|
|
log.WithFields("error", err).Trace("unable to read DT_FLAGS_1 from elf file")
|
|
return false
|
|
}
|
|
for _, val := range vals {
|
|
if val&uint64(flag) != 0 {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func hasElfDynTag(f *elf.File, tag elf.DynTag) bool {
|
|
// source https://github.com/golang/go/blob/9b4b3e5acca2dabe107fa2c3ed963097d78a4562/src/cmd/cgo/internal/testshared/shared_test.go#L280
|
|
|
|
ds := f.SectionByType(elf.SHT_DYNAMIC)
|
|
if ds == nil {
|
|
return false
|
|
}
|
|
d, err := ds.Data()
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
for len(d) > 0 {
|
|
var t elf.DynTag
|
|
switch f.Class {
|
|
case elf.ELFCLASS32:
|
|
t = elf.DynTag(f.ByteOrder.Uint32(d[0:4]))
|
|
d = d[8:]
|
|
case elf.ELFCLASS64:
|
|
t = elf.DynTag(f.ByteOrder.Uint64(d[0:8]))
|
|
d = d[16:]
|
|
}
|
|
if t == tag {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func isELFPIE(f *elf.File) bool {
|
|
// being a shared object is not sufficient to be a PIE, the explicit flag must be set also
|
|
return isELFDSO(f) && hasElfDynFlag1(f, elf.DF_1_PIE)
|
|
}
|
|
|
|
func isELFDSO(f *elf.File) bool {
|
|
return f.Type == elf.ET_DYN
|
|
}
|
|
|
|
func checkLLVMSafeStack(file *elf.File) *bool {
|
|
// looking for the presence of https://github.com/microsoft/compiler-rt/blob/30b3b8cb5c9a0854f2f40f187c6f6773561a35f2/lib/safestack/safestack.cc#L207
|
|
return hasAnyDynamicSymbols(file, "__safestack_init")
|
|
}
|
|
|
|
func checkLLVMControlFlowIntegrity(file *elf.File) *bool {
|
|
// look for any symbols that are functions and end with ".cfi"
|
|
dynSyms, err := file.Symbols()
|
|
if err != nil {
|
|
log.WithFields("error", err).Trace("unable to read symbols from elf file")
|
|
return nil
|
|
}
|
|
|
|
for _, sym := range dynSyms {
|
|
if isFunction(sym) && strings.HasSuffix(sym.Name, ".cfi") {
|
|
return boolRef(true)
|
|
}
|
|
}
|
|
return boolRef(false)
|
|
}
|
|
|
|
func isFunction(sym elf.Symbol) bool {
|
|
return elf.ST_TYPE(sym.Info) == elf.STT_FUNC
|
|
}
|
|
|
|
var fortifyPattern = regexp.MustCompile(`__\w+_chk@.+`)
|
|
|
|
func checkClangFortifySource(file *elf.File) *bool {
|
|
dynSyms, err := file.Symbols()
|
|
if err != nil {
|
|
log.WithFields("error", err).Trace("unable to read symbols from elf file")
|
|
return nil
|
|
}
|
|
|
|
for _, sym := range dynSyms {
|
|
if isFunction(sym) && fortifyPattern.MatchString(sym.Name) {
|
|
return boolRef(true)
|
|
}
|
|
}
|
|
return boolRef(false)
|
|
}
|
|
|
|
func elfHasEntrypoint(f *elf.File) bool {
|
|
// this is akin to
|
|
// readelf -h ./path/to/bin | grep "Entry point address"
|
|
return f.Entry > 0
|
|
}
|
|
|
|
func elfHasExports(f *elf.File) bool {
|
|
// this is akin to:
|
|
// nm -D --defined-only ./path/to/bin | grep ' T \| W \| B '
|
|
// where:
|
|
// T - symbol in the text section
|
|
// W - weak symbol that might be overwritten
|
|
// B - variable located in the uninitialized data section
|
|
// really anything that is not marked with 'U' (undefined) is considered an export.
|
|
symbols, err := f.DynamicSymbols()
|
|
if err != nil {
|
|
log.WithFields("error", err).Trace("unable to get ELF dynamic symbols")
|
|
return false
|
|
}
|
|
|
|
for _, s := range symbols {
|
|
// check if the section is SHN_UNDEF, which is the "U" output type from "nm -D" meaning that the symbol
|
|
// is undefined, meaning it is not an export. Any entry that is not undefined is considered an export.
|
|
if s.Section != elf.SHN_UNDEF {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|