From 8bce64fb7581af62b2590706b78c042eeeec989a Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Sun, 17 Dec 2023 16:50:10 -0500 Subject: [PATCH] add detection of ELF security features Signed-off-by: Alex Goodman --- .github/workflows/validations.yaml | 6 + Taskfile.yaml | 14 +- cmd/syft/cli/options/catalog.go | 5 + cmd/syft/cli/options/file.go | 12 +- internal/task/file_tasks.go | 22 ++ syft/cataloging/filecataloging/config.go | 15 +- syft/create_sbom_config.go | 3 + syft/file/cataloger/executable/cataloger.go | 233 ++++++++++++++++++ syft/file/cataloger/executable/elf.go | 219 ++++++++++++++++ syft/file/cataloger/executable/elf_test.go | 160 ++++++++++++ .../executable/test-fixtures/.gitignore | 3 + .../executable/test-fixtures/Dockerfile | 16 ++ .../executable/test-fixtures/Makefile | 34 +++ .../executable/test-fixtures/expected_verify | 15 ++ .../executable/test-fixtures/project/Makefile | 93 +++++++ .../executable/test-fixtures/project/lib.c | 6 + .../executable/test-fixtures/project/lib.h | 6 + .../executable/test-fixtures/project/main.c | 106 ++++++++ syft/file/executable.go | 51 ++++ syft/format/syftjson/model/file.go | 13 +- syft/format/syftjson/to_format_model.go | 18 +- syft/format/syftjson/to_syft_model.go | 7 + .../internal/unionreader/union_reader.go | 0 .../internal/unionreader/union_reader_test.go | 0 syft/pkg/cataloger/binary/classifier.go | 2 +- syft/pkg/cataloger/golang/parse_go_binary.go | 2 +- syft/pkg/cataloger/golang/scan_binary.go | 2 +- .../java/graalvm_native_image_cataloger.go | 2 +- .../graalvm_native_image_cataloger_test.go | 2 +- .../kernel/parse_linux_kernel_file.go | 2 +- .../kernel/parse_linux_kernel_module_file.go | 2 +- syft/pkg/cataloger/rust/parse_audit_binary.go | 2 +- syft/sbom/sbom.go | 1 + 33 files changed, 1041 insertions(+), 33 deletions(-) create mode 100644 syft/file/cataloger/executable/cataloger.go create mode 100644 syft/file/cataloger/executable/elf.go create mode 100644 syft/file/cataloger/executable/elf_test.go create mode 100644 syft/file/cataloger/executable/test-fixtures/.gitignore create mode 100644 syft/file/cataloger/executable/test-fixtures/Dockerfile create mode 100644 syft/file/cataloger/executable/test-fixtures/Makefile create mode 100644 syft/file/cataloger/executable/test-fixtures/expected_verify create mode 100644 syft/file/cataloger/executable/test-fixtures/project/Makefile create mode 100644 syft/file/cataloger/executable/test-fixtures/project/lib.c create mode 100644 syft/file/cataloger/executable/test-fixtures/project/lib.h create mode 100644 syft/file/cataloger/executable/test-fixtures/project/main.c create mode 100644 syft/file/executable.go rename syft/{pkg/cataloger => }/internal/unionreader/union_reader.go (100%) rename syft/{pkg/cataloger => }/internal/unionreader/union_reader_test.go (100%) diff --git a/.github/workflows/validations.yaml b/.github/workflows/validations.yaml index dcd38440cdb..0d349daba2b 100644 --- a/.github/workflows/validations.yaml +++ b/.github/workflows/validations.yaml @@ -36,6 +36,12 @@ jobs: - name: Bootstrap environment uses: ./.github/actions/bootstrap + - name: Restore file executable test-fixture cache + uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 #v3.3.2 + with: + path: syft/file/cataloger/executable/test-fixtures/bin + key: ${{ runner.os }}-unit-file-executable-cache-${{ hashFiles( 'syft/file/cataloger/executable/test-fixtures/cache.fingerprint' ) }} + - name: Restore Java test-fixture cache uses: actions/cache@e12d46a63a90f2fae62d114769bbf2a179198b5c #v3.3.3 with: diff --git a/Taskfile.yaml b/Taskfile.yaml index e6f9115c7be..e790c56b155 100644 --- a/Taskfile.yaml +++ b/Taskfile.yaml @@ -265,6 +265,7 @@ tasks: fingerprints: desc: Generate test fixture fingerprints generates: + - syft/file/cataloger/executable/test-fixtures/cache.fingerprint - test/integration/test-fixtures/cache.fingerprint - syft/pkg/cataloger/binary/test-fixtures/cache.fingerprint - syft/pkg/cataloger/java/test-fixtures/java-builds/cache.fingerprint @@ -274,17 +275,19 @@ tasks: - test/install/cache.fingerprint - test/cli/test-fixtures/cache.fingerprint cmds: + # for EXECUTABLE unit test fixtures + - "cd syft/file/cataloger/executable/test-fixtures && make cache.fingerprint" # for IMAGE integration test fixtures - "cd test/integration/test-fixtures && make cache.fingerprint" - # for BINARY test fixtures + # for BINARY unit test fixtures - "cd syft/pkg/cataloger/binary/test-fixtures && make cache.fingerprint" - # for JAVA BUILD test fixtures + # for JAVA BUILD unit test fixtures - "cd syft/pkg/cataloger/java/test-fixtures/java-builds && make cache.fingerprint" - # for GO BINARY test fixtures + # for GO BINARY unit test fixtures - "cd syft/pkg/cataloger/golang/test-fixtures/archs && make binaries.fingerprint" - # for RPM test fixtures + # for RPM unit test fixtures - "cd syft/pkg/cataloger/redhat/test-fixtures && make rpms.fingerprint" - # for Kernel test fixtures + # for Kernel unit test fixtures - "cd syft/pkg/cataloger/kernel/test-fixtures && make cache.fingerprint" # for INSTALL integration test fixtures - "cd test/install && make cache.fingerprint" @@ -294,6 +297,7 @@ tasks: fixtures: desc: Generate test fixtures cmds: + - "cd syft/file/cataloger/executable/test-fixtures && make" - "cd syft/pkg/cataloger/java/test-fixtures/java-builds && make" - "cd syft/pkg/cataloger/redhat/test-fixtures && make" - "cd syft/pkg/cataloger/binary/test-fixtures && make" diff --git a/cmd/syft/cli/options/catalog.go b/cmd/syft/cli/options/catalog.go index 3d8baafe27c..141d0b7ebaf 100644 --- a/cmd/syft/cli/options/catalog.go +++ b/cmd/syft/cli/options/catalog.go @@ -2,6 +2,7 @@ package options import ( "fmt" + "github.com/anchore/syft/syft/file/cataloger/executable" "sort" "strings" @@ -112,6 +113,10 @@ func (cfg Catalog) ToFilesConfig() filecataloging.Config { Globs: cfg.File.Content.Globs, SkipFilesAboveSize: cfg.File.Content.SkipFilesAboveSize, }, + Executable: executable.Config{ + MIMETypes: executable.DefaultConfig().MIMETypes, + Globs: cfg.File.Executable.Globs, + }, } } diff --git a/cmd/syft/cli/options/file.go b/cmd/syft/cli/options/file.go index b0eb8a1fc96..78c25029d3a 100644 --- a/cmd/syft/cli/options/file.go +++ b/cmd/syft/cli/options/file.go @@ -8,8 +8,9 @@ import ( ) type fileConfig struct { - Metadata fileMetadata `yaml:"metadata" json:"metadata" mapstructure:"metadata"` - Content fileContent `yaml:"content" json:"content" mapstructure:"content"` + Metadata fileMetadata `yaml:"metadata" json:"metadata" mapstructure:"metadata"` + Content fileContent `yaml:"content" json:"content" mapstructure:"content"` + Executable fileExecutable `yaml:"executable" json:"executable" mapstructure:"executable"` } type fileMetadata struct { @@ -22,6 +23,10 @@ type fileContent struct { Globs []string `yaml:"globs" json:"globs" mapstructure:"globs"` } +type fileExecutable struct { + Globs []string `yaml:"globs" json:"globs" mapstructure:"globs"` +} + func defaultFileConfig() fileConfig { return fileConfig{ Metadata: fileMetadata{ @@ -31,6 +36,9 @@ func defaultFileConfig() fileConfig { Content: fileContent{ SkipFilesAboveSize: 250 * intFile.KB, }, + Executable: fileExecutable{ + Globs: nil, + }, } } diff --git a/internal/task/file_tasks.go b/internal/task/file_tasks.go index ec0dd86f2d6..6cffc6c17f9 100644 --- a/internal/task/file_tasks.go +++ b/internal/task/file_tasks.go @@ -4,6 +4,7 @@ import ( "context" "crypto" "fmt" + "github.com/anchore/syft/syft/file/cataloger/executable" "github.com/anchore/syft/internal/sbomsync" "github.com/anchore/syft/syft/artifact" @@ -100,6 +101,27 @@ func NewFileContentCatalogerTask(cfg filecontent.Config) Task { return NewTask("file-content-cataloger", fn) } +func NewExecutableCatalogerTask(cfg executable.Config) Task { + cat := executable.NewCataloger(cfg) + + fn := func(ctx context.Context, resolver file.Resolver, builder sbomsync.Builder) error { + accessor := builder.(sbomsync.Accessor) + + result, err := cat.Catalog(resolver) + if err != nil { + return err + } + + accessor.WriteToSBOM(func(sbom *sbom.SBOM) { + sbom.Artifacts.Executables = result + }) + + return nil + } + + return NewTask("file-executable-cataloger", fn) +} + // TODO: this should be replaced with a fix that allows passing a coordinate or location iterator to the cataloger // Today internal to both cataloger this functions differently: a slice of coordinates vs a channel of locations func coordinatesForSelection(selection file.Selection, accessor sbomsync.Accessor) ([]file.Coordinates, bool) { diff --git a/syft/cataloging/filecataloging/config.go b/syft/cataloging/filecataloging/config.go index 80559fb00b2..74cea7a71db 100644 --- a/syft/cataloging/filecataloging/config.go +++ b/syft/cataloging/filecataloging/config.go @@ -4,6 +4,7 @@ import ( "crypto" "encoding/json" "fmt" + "github.com/anchore/syft/syft/file/cataloger/executable" "strings" intFile "github.com/anchore/syft/internal/file" @@ -13,9 +14,10 @@ import ( ) type Config struct { - Selection file.Selection `yaml:"selection" json:"selection" mapstructure:"selection"` - Hashers []crypto.Hash `yaml:"hashers" json:"hashers" mapstructure:"hashers"` - Content filecontent.Config `yaml:"content" json:"content" mapstructure:"content"` + Selection file.Selection `yaml:"selection" json:"selection" mapstructure:"selection"` + Hashers []crypto.Hash `yaml:"hashers" json:"hashers" mapstructure:"hashers"` + Content filecontent.Config `yaml:"content" json:"content" mapstructure:"content"` + Executable executable.Config `yaml:"executable" json:"executable" mapstructure:"executable"` } type configMarshaledForm struct { @@ -30,9 +32,10 @@ func DefaultConfig() Config { log.WithFields("error", err).Warn("unable to create file hashers") } return Config{ - Selection: file.FilesOwnedByPackageSelection, - Hashers: hashers, - Content: filecontent.DefaultConfig(), + Selection: file.FilesOwnedByPackageSelection, + Hashers: hashers, + Content: filecontent.DefaultConfig(), + Executable: executable.DefaultConfig(), } } diff --git a/syft/create_sbom_config.go b/syft/create_sbom_config.go index 14ef5a0fc40..f5aaf2a85dc 100644 --- a/syft/create_sbom_config.go +++ b/syft/create_sbom_config.go @@ -185,6 +185,9 @@ func (c *CreateSBOMConfig) fileTasks() []task.Task { if t := task.NewFileContentCatalogerTask(c.Files.Content); t != nil { tsks = append(tsks, t) } + if t := task.NewExecutableCatalogerTask(c.Files.Executable); t != nil { + tsks = append(tsks, t) + } return tsks } diff --git a/syft/file/cataloger/executable/cataloger.go b/syft/file/cataloger/executable/cataloger.go new file mode 100644 index 00000000000..6be41bc2998 --- /dev/null +++ b/syft/file/cataloger/executable/cataloger.go @@ -0,0 +1,233 @@ +package executable + +import ( + "bytes" + "debug/elf" + "debug/macho" + "encoding/binary" + "fmt" + "github.com/anchore/syft/internal" + "github.com/anchore/syft/internal/bus" + "github.com/anchore/syft/internal/log" + "github.com/anchore/syft/syft/event/monitor" + "github.com/anchore/syft/syft/file" + "github.com/anchore/syft/syft/internal/unionreader" + "github.com/bmatcuk/doublestar/v4" + "github.com/dustin/go-humanize" +) + +type Config struct { + MIMETypes []string `json:"mimeTypes" yaml:"mimeTypes" mapstructure:"mimeTypes"` + Globs []string `json:"globs" yaml:"globs" mapstructure:"globs"` +} + +type Cataloger struct { + config Config +} + +func DefaultConfig() Config { + return Config{ + MIMETypes: internal.ExecutableMIMETypeSet.List(), + Globs: nil, + } +} + +func NewCataloger(cfg Config) *Cataloger { + return &Cataloger{ + config: cfg, + } +} + +func (i *Cataloger) Catalog(resolver file.Resolver) (map[file.Coordinates]file.Executable, error) { + locs, err := resolver.FilesByMIMEType(i.config.MIMETypes...) + if err != nil { + return nil, fmt.Errorf("unable to get file locations for binaries: %w", err) + } + + locs, err = filterByGlobs(locs, i.config.Globs) + if err != nil { + return nil, err + } + + prog := catalogingProgress(int64(len(locs))) + + results := make(map[file.Coordinates]file.Executable) + for _, loc := range locs { + prog.AtomicStage.Set(loc.Path()) + + reader, err := resolver.FileContentsByLocation(loc) + if err != nil { + // TODO: known-unknowns + log.WithFields("error", err).Warnf("unable to get file contents for %q", loc.RealPath) + continue + } + exec, err := processExecutable(loc, reader.(unionreader.UnionReader)) + if err != nil { + log.WithFields("error", err).Warnf("unable to process executable %q", loc.RealPath) + } + if exec != nil { + prog.Increment() + results[loc.Coordinates] = *exec + } + } + + log.Debugf("executable cataloger processed %d files", len(results)) + + prog.AtomicStage.Set(fmt.Sprintf("%s executables", humanize.Comma(prog.Current()))) + prog.SetCompleted() + + return results, nil +} + +func catalogingProgress(locations int64) *monitor.CatalogerTaskProgress { + info := monitor.GenericTask{ + Title: monitor.Title{ + Default: "Executables", + }, + ParentID: monitor.TopLevelCatalogingTaskID, + } + + return bus.StartCatalogerTask(info, locations, "") +} + +func filterByGlobs(locs []file.Location, globs []string) ([]file.Location, error) { + if len(globs) == 0 { + return locs, nil + } + var filteredLocs []file.Location + for _, loc := range locs { + + matches, err := locationMatchesGlob(loc, globs) + if err != nil { + return nil, err + } + if matches { + filteredLocs = append(filteredLocs, loc) + } + + } + return filteredLocs, nil +} + +func locationMatchesGlob(loc file.Location, globs []string) (bool, error) { + for _, glob := range globs { + for _, path := range []string{loc.RealPath, loc.AccessPath} { + if path == "" { + continue + } + matches, err := doublestar.Match(glob, path) + if err != nil { + return false, fmt.Errorf("unable to match glob %q to path %q: %w", glob, path, err) + } + if matches { + return true, nil + } + } + } + return false, nil +} + +func processExecutable(loc file.Location, reader unionreader.UnionReader) (*file.Executable, error) { + data := file.Executable{} + + // determine the executable format + + format, err := findExecutableFormat(reader) + if err != nil { + return nil, fmt.Errorf("unable to determine executable kind: %w", err) + } + + if format == "" { + log.Debugf("unable to determine executable format for %q", loc.RealPath) + return nil, nil + } + + data.Format = format + + securityFeatures, err := findSecurityFeatures(format, reader) + if err != nil { + log.WithFields("error", err).Warnf("unable to determine security features for %q", loc.RealPath) + return nil, nil + } + + data.SecurityFeatures = securityFeatures + + return &data, nil +} + +func findExecutableFormat(reader unionreader.UnionReader) (file.ExecutableFormat, error) { + // read the first sector of the file + buf := make([]byte, 512) + n, err := reader.ReadAt(buf, 0) + if err != nil { + return "", fmt.Errorf("unable to read first sector of file: %w", err) + } + if n < 512 { + return "", fmt.Errorf("unable to read enough bytes to determine executable format") + } + + switch { + case isMacho(buf): + return file.MachO, nil + case isPE(buf): + return file.PE, nil + case isELF(buf): + return file.ELF, nil + } + + return "", nil +} + +func isMacho(by []byte) bool { + // sourced from https://github.com/gabriel-vasile/mimetype/blob/02af149c0dfd1444d9256fc33c2012bb3153e1d2/internal/magic/binary.go#L44 + + if classOrMachOFat(by) && by[7] < 20 { + return true + } + + if len(by) < 4 { + return false + } + + be := binary.BigEndian.Uint32(by) + le := binary.LittleEndian.Uint32(by) + + return be == macho.Magic32 || + le == macho.Magic32 || + be == macho.Magic64 || + le == macho.Magic64 +} + +// Java bytecode and Mach-O binaries share the same magic number. +// More info here https://github.com/threatstack/libmagic/blob/master/magic/Magdir/cafebabe +func classOrMachOFat(in []byte) bool { + // sourced from https://github.com/gabriel-vasile/mimetype/blob/02af149c0dfd1444d9256fc33c2012bb3153e1d2/internal/magic/binary.go#L44 + + // There should be at least 8 bytes for both of them because the only way to + // quickly distinguish them is by comparing byte at position 7 + if len(in) < 8 { + return false + } + + return bytes.HasPrefix(in, []byte{0xCA, 0xFE, 0xBA, 0xBE}) +} + +func isPE(by []byte) bool { + return bytes.HasPrefix(by, []byte("MZ")) +} + +func isELF(by []byte) bool { + return bytes.HasPrefix(by, []byte(elf.ELFMAG)) +} + +func findSecurityFeatures(format file.ExecutableFormat, reader unionreader.UnionReader) (*file.ELFSecurityFeatures, error) { + switch format { + case file.ELF: + return findELFSecurityFeatures(reader) + //case file.PE: + // return findPESecurityFeatures(reader) + //case file.MachO: + // return findMachOSecurityFeatures(reader) + } + return nil, fmt.Errorf("unsupported executable format: %q", format) +} diff --git a/syft/file/cataloger/executable/elf.go b/syft/file/cataloger/executable/elf.go new file mode 100644 index 00000000000..57eb1bb5eb3 --- /dev/null +++ b/syft/file/cataloger/executable/elf.go @@ -0,0 +1,219 @@ +package executable + +import ( + "debug/elf" + "github.com/anchore/syft/internal/log" + "github.com/anchore/syft/syft/file" + "github.com/anchore/syft/syft/internal/unionreader" + "github.com/scylladb/go-set/strset" + "regexp" + "strings" +) + +func findELFSecurityFeatures(reader unionreader.UnionReader) (*file.ELFSecurityFeatures, error) { + f, err := elf.NewFile(reader) + if err != nil { + return nil, nil + } + + features := 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), + } + + return &features, nil +} + +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 { + // TODO: known-unknowns + log.WithFields("error", err).Warn("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 { + // TODO: known-unknowns + log.WithFields("error", err).Warn("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 { + // TODO: known-unknowns + log.WithFields("error", err).Warn("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 { + // TODO: known-unknowns + 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 { + // TODO: known-unknowns + 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) +} diff --git a/syft/file/cataloger/executable/elf_test.go b/syft/file/cataloger/executable/elf_test.go new file mode 100644 index 00000000000..7f5df0e80d2 --- /dev/null +++ b/syft/file/cataloger/executable/elf_test.go @@ -0,0 +1,160 @@ +package executable + +import ( + "github.com/anchore/syft/syft/file" + "github.com/anchore/syft/syft/internal/unionreader" + "github.com/google/go-cmp/cmp" + "github.com/stretchr/testify/require" + "os" + "path/filepath" + "testing" +) + +func Test_findELFSecurityFeatures(t *testing.T) { + + readerForFixture := func(t *testing.T, fixture string) unionreader.UnionReader { + t.Helper() + f, err := os.Open(filepath.Join("test-fixtures", fixture)) + require.NoError(t, err) + return f + } + + tests := []struct { + name string + fixture string + want *file.ELFSecurityFeatures + wantStripped bool + wantErr require.ErrorAssertionFunc + }{ + { + name: "detect canary", + fixture: "bin/with_canary", + want: &file.ELFSecurityFeatures{ + StackCanary: boolRef(true), // ! important ! + RelocationReadOnly: file.RelocationReadOnlyNone, + LlvmSafeStack: boolRef(false), + LlvmControlFlowIntegrity: boolRef(false), + ClangFortifySource: boolRef(false), + }, + }, + { + name: "detect nx", + fixture: "bin/with_nx", + want: &file.ELFSecurityFeatures{ + StackCanary: boolRef(false), + NoExecutable: true, // ! important ! + RelocationReadOnly: file.RelocationReadOnlyNone, + LlvmSafeStack: boolRef(false), + LlvmControlFlowIntegrity: boolRef(false), + ClangFortifySource: boolRef(false), + }, + }, + { + name: "detect relro", + fixture: "bin/with_relro", + want: &file.ELFSecurityFeatures{ + StackCanary: boolRef(false), + RelocationReadOnly: file.RelocationReadOnlyFull, // ! important ! + LlvmSafeStack: boolRef(false), + LlvmControlFlowIntegrity: boolRef(false), + ClangFortifySource: boolRef(false), + }, + }, + { + name: "detect partial relro", + fixture: "bin/with_partial_relro", + want: &file.ELFSecurityFeatures{ + StackCanary: boolRef(false), + RelocationReadOnly: file.RelocationReadOnlyPartial, // ! important ! + LlvmSafeStack: boolRef(false), + LlvmControlFlowIntegrity: boolRef(false), + ClangFortifySource: boolRef(false), + }, + }, + { + name: "detect pie", + fixture: "bin/with_pie", + want: &file.ELFSecurityFeatures{ + StackCanary: boolRef(false), + RelocationReadOnly: file.RelocationReadOnlyNone, + PositionIndependentExecutable: true, // ! important ! + DynamicSharedObject: true, // ! important ! + LlvmSafeStack: boolRef(false), + LlvmControlFlowIntegrity: boolRef(false), + ClangFortifySource: boolRef(false), + }, + }, + { + name: "detect dso", + fixture: "bin/pie_false_positive.so", + want: &file.ELFSecurityFeatures{ + StackCanary: boolRef(false), + RelocationReadOnly: file.RelocationReadOnlyPartial, + NoExecutable: true, + PositionIndependentExecutable: false, // ! important ! + DynamicSharedObject: true, // ! important ! + LlvmSafeStack: boolRef(false), + LlvmControlFlowIntegrity: boolRef(false), + ClangFortifySource: boolRef(false), + }, + }, + { + name: "detect safestack", + fixture: "bin/with_safestack", + want: &file.ELFSecurityFeatures{ + NoExecutable: true, + StackCanary: boolRef(false), + RelocationReadOnly: file.RelocationReadOnlyPartial, + PositionIndependentExecutable: false, + DynamicSharedObject: false, + LlvmSafeStack: boolRef(true), // ! important ! + LlvmControlFlowIntegrity: boolRef(false), + ClangFortifySource: boolRef(false), + }, + }, + { + name: "detect cfi", + fixture: "bin/with_cfi", + want: &file.ELFSecurityFeatures{ + NoExecutable: true, + StackCanary: boolRef(false), + RelocationReadOnly: file.RelocationReadOnlyPartial, + PositionIndependentExecutable: false, + DynamicSharedObject: false, + LlvmSafeStack: boolRef(false), + LlvmControlFlowIntegrity: boolRef(true), // ! important ! + ClangFortifySource: boolRef(false), + }, + }, + { + name: "detect fortify", + fixture: "bin/with_fortify", + want: &file.ELFSecurityFeatures{ + NoExecutable: true, + StackCanary: boolRef(false), + RelocationReadOnly: file.RelocationReadOnlyPartial, + PositionIndependentExecutable: false, + DynamicSharedObject: false, + LlvmSafeStack: boolRef(false), + LlvmControlFlowIntegrity: boolRef(false), + ClangFortifySource: boolRef(true), // ! important ! + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.wantErr == nil { + tt.wantErr = require.NoError + } + got, err := findELFSecurityFeatures(readerForFixture(t, tt.fixture)) + tt.wantErr(t, err) + if err != nil { + return + } + + if d := cmp.Diff(tt.want, got); d != "" { + t.Errorf("findELFSecurityFeatures() mismatch (-want +got):\n%s", d) + } + }) + } +} diff --git a/syft/file/cataloger/executable/test-fixtures/.gitignore b/syft/file/cataloger/executable/test-fixtures/.gitignore new file mode 100644 index 00000000000..89845afaa3a --- /dev/null +++ b/syft/file/cataloger/executable/test-fixtures/.gitignore @@ -0,0 +1,3 @@ +bin +actual_verify +Dockerfile.sha256 \ No newline at end of file diff --git a/syft/file/cataloger/executable/test-fixtures/Dockerfile b/syft/file/cataloger/executable/test-fixtures/Dockerfile new file mode 100644 index 00000000000..46425e08fd7 --- /dev/null +++ b/syft/file/cataloger/executable/test-fixtures/Dockerfile @@ -0,0 +1,16 @@ +FROM gcc:9.5.0 + +RUN apt update -y && apt install -y clang cmake git make m4 pkg-config zlib1g-dev + +## from https://github.com/runsafesecurity/selfrando/blob/tb-v0.4.2/docs/linux-build-instructions.md +#RUN git clone https://github.com/runsafesecurity/selfrando.git && \ +# export SR_ARCH=`uname -m | sed s/i686/x86/` && \ +# cd selfrando && \ +# cmake . -DSR_DEBUG_LEVEL=env -DCMAKE_BUILD_TYPE=Release -DSR_BUILD_LIBELF=1 \ +# -DSR_ARCH=$SR_ARCH -DSR_LOG=console \ +# -DSR_FORCE_INPLACE=1 -G "Unix Makefiles" \ +# -DCMAKE_INSTALL_PREFIX:PATH=$PWD/out/$SR_ARCH +#RUN cd selfrando && make -j`nprocs --all` +#RUN cd selfrando && make install + +RUN curl -o /bin/checksec https://raw.githubusercontent.com/slimm609/checksec.sh/2.6.0/checksec && chmod +x /bin/checksec diff --git a/syft/file/cataloger/executable/test-fixtures/Makefile b/syft/file/cataloger/executable/test-fixtures/Makefile new file mode 100644 index 00000000000..08d0f88db9c --- /dev/null +++ b/syft/file/cataloger/executable/test-fixtures/Makefile @@ -0,0 +1,34 @@ +BIN=./bin +TOOL_IMAGE=localhost/syft-bin-build-tools:latest +VERIFY_FILE=actual_verify + +all: build verify + +dockerfile-check: + @sha256sum -c Dockerfile.sha256 || (echo "Dockerfile has changed" && exit 1) + +# for selfrando... +# docker buildx build --platform linux/amd64 -t $(TOOL_IMAGE) . + +tools: + @(docker inspect $(TOOL_IMAGE) > /dev/null && make dockerfile-check) || (docker build -t $(TOOL_IMAGE) . && sha256sum Dockerfile > Dockerfile.sha256) + +build: tools + mkdir -p $(BIN) + docker run -it -v $(shell pwd):/mount -w /mount/project $(TOOL_IMAGE) make + +verify: tools + @rm $(VERIFY_FILE) + docker run -it -v $(shell pwd):/mount -w /mount/project $(TOOL_IMAGE) make verify | tee $(VERIFY_FILE) + @diff -u expected_verify $(VERIFY_FILE) && (echo "PASS" || (echo "FAIL" && exit 1)) + +debug: + docker run -it --rm -v $(shell pwd):/mount -w /mount/project $(TOOL_IMAGE) bash + +cache.fingerprint: + @find project Dockerfile Makefile -type f -exec md5sum {} + | awk '{print $1}' | sort | tee cache.fingerprint + +clean: + rm -f $(BIN)/* + +.PHONY: build verify debug build-image build-bins clean dockerfile-check cache.fingerprint diff --git a/syft/file/cataloger/executable/test-fixtures/expected_verify b/syft/file/cataloger/executable/test-fixtures/expected_verify new file mode 100644 index 00000000000..a9587ef6667 --- /dev/null +++ b/syft/file/cataloger/executable/test-fixtures/expected_verify @@ -0,0 +1,15 @@ +/bin/checksec --dir=../bin --extended +RELRO STACK CANARY NX PIE SELFRANDO Clang CFI SafeStack RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable Filename +No RELRO  No canary found NX disabled No PIE  No Selfrando  No Clang CFI found No SafeStack found No RPATH  RUNPATH  119 Symbols  No 0 2 ../bin/no_protection +No RELRO  No canary found NX enabled  No PIE  No Selfrando  No Clang CFI found No SafeStack found No RPATH  No RUNPATH  119 Symbols  No 0 2 ../bin/with_nx +No RELRO  No canary found NX disabled No PIE  No Selfrando  No Clang CFI found No SafeStack found RPATH  No RUNPATH  119 Symbols  No 0 2 ../bin/with_rpath +No RELRO  Canary found  NX disabled No PIE  No Selfrando  No Clang CFI found No SafeStack found No RPATH  No RUNPATH  122 Symbols  No 0 2 ../bin/with_canary +Full RELRO  No canary found NX disabled No PIE  No Selfrando  No Clang CFI found No SafeStack found No RPATH  No RUNPATH  118 Symbols  No 0 2 ../bin/with_relro +No RELRO  No canary found NX disabled PIE enabled  No Selfrando  No Clang CFI found No SafeStack found No RPATH  No RUNPATH  117 Symbols  No 0 2 ../bin/with_pie +No RELRO  No canary found NX disabled No PIE  No Selfrando  No Clang CFI found No SafeStack found No RPATH  RUNPATH  119 Symbols  No 0 2 ../bin/with_runpath +Partial RELRO No canary found NX enabled  No PIE  No Selfrando  No Clang CFI found SafeStack found  No RPATH  No RUNPATH  175 Symbols  No 0 3 ../bin/with_safestack +Partial RELRO No canary found NX enabled  DSO  No Selfrando  No Clang CFI found No SafeStack found No RPATH  No RUNPATH  68 Symbols  No 0 0 ../bin/pie_false_positive.so +Partial RELRO No canary found NX enabled  No PIE  No Selfrando  Clang CFI found  No SafeStack found No RPATH  No RUNPATH  120 Symbols  No 0 2 ../bin/with_cfi +Partial RELRO No canary found NX disabled No PIE  No Selfrando  No Clang CFI found No SafeStack found No RPATH  No RUNPATH  119 Symbols  No 0 2 ../bin/with_partial_relro +Full RELRO  Canary found  NX enabled  PIE enabled  No Selfrando  No Clang CFI found No SafeStack found No RPATH  No RUNPATH  118 Symbols  No 0 2 ../bin/protected +Partial RELRO No canary found NX enabled  No PIE  No Selfrando  No Clang CFI found No SafeStack found No RPATH  No RUNPATH  109 Symbols  Yes 1 2 ../bin/with_fortify diff --git a/syft/file/cataloger/executable/test-fixtures/project/Makefile b/syft/file/cataloger/executable/test-fixtures/project/Makefile new file mode 100644 index 00000000000..b21a2e46dc7 --- /dev/null +++ b/syft/file/cataloger/executable/test-fixtures/project/Makefile @@ -0,0 +1,93 @@ +### GCC Options ############################################ +CANARY := -fstack-protector +NO_CANARY := -fno-stack-protector + +SHARED_OBJ := -shared + +RELRO := -z relro -z now +PARTIAL_RELRO := -z relro +NO_RELRO := -z norelro + +NX := -z noexecstack +NO_NX := -z execstack + +PIE := -fpic -pie +NO_PIE := -no-pie + +# deprecated +RPATH := -Wl,--disable-new-dtags,-rpath,./libs + +# replaces RPATH (thus us mutually exclusive with it) +RUNPATH := -Wl,-rpath,./libs + +GCCFLAGS := -g + +### Clang Options ############################################ + +SAFE_STACK := -fsanitize=safe-stack + +CFI := -flto -fvisibility=hidden -fsanitize=cfi + +FORTIFY := -O2 -D_FORTIFY_SOURCE=2 + +### Common Options ############################################ + +SRC := main.c +LIB_SRC := lib.c +BIN := ../bin + +BINS := $(BIN)/no_protection $(BIN)/with_nx $(BIN)/pie_false_positive.so $(BIN)/with_pie $(BIN)/with_canary $(BIN)/with_relro $(BIN)/with_partial_relro $(BIN)/with_rpath $(BIN)/with_runpath $(BIN)/with_safestack $(BIN)/with_cfi $(BIN)/with_fortify $(BIN)/protected +#.PHONY: verify $(BIN)/no_protection $(BIN)/with_nx $(BIN)/pie_false_positive.so $(BIN)/with_pie $(BIN)/with_canary $(BIN)/with_relro $(BIN)/with_partial_relro $(BIN)/with_rpath $(BIN)/with_runpath $(BIN)/with_safestack $(BIN)/with_cfi $(BIN)/with_fortify $(BIN)/protected +.PHONY: verify clean all + +all: $(BINS) + + +$(BIN)/no_protection : $(SRC) + gcc $< -o $@ $(GCCFLAGS) $(NO_CANARY) $(NO_NX) $(NO_RELRO) $(NO_PIE) $(RUNPATH) + +$(BIN)/with_nx : $(SRC) + gcc $< -o $@ $(GCCFLAGS) $(NO_CANARY) $(NX) $(NO_RELRO) $(NO_PIE) + +$(BIN)/pie_false_positive.so: $(LIB_SRC) + gcc $< -c -Wall -Werror -fpic $(LIB_SRC) + gcc -shared -o $@ lib.o ; rm lib.o + +$(BIN)/with_pie: $(SRC) + gcc $< -o $@ $(GCCFLAGS) $(NO_CANARY) $(NO_NX) $(NO_RELRO) $(PIE) + +$(BIN)/with_canary: $(SRC) + gcc $< -o $@ $(GCCFLAGS) $(CANARY) $(NO_NX) $(NO_RELRO) $(NO_PIE) + +$(BIN)/with_relro: $(SRC) + gcc $< -o $@ $(GCCFLAGS) $(NO_CANARY) $(NO_NX) $(RELRO) $(NO_PIE) + +$(BIN)/with_partial_relro: $(SRC) + gcc $< -o $@ $(GCCFLAGS) $(NO_CANARY) $(NO_NX) $(PARTIAL_RELRO) $(NO_PIE) + +$(BIN)/with_rpath: $(SRC) + gcc $< -o $@ $(GCCFLAGS) $(NO_CANARY) $(NO_NX) $(NO_RELRO) $(NO_PIE) $(RPATH) + +$(BIN)/with_runpath: $(SRC) + gcc $< -o $@ $(GCCFLAGS) $(NO_CANARY) $(NO_NX) $(NO_RELRO) $(NO_PIE) $(RUNPATH) + +$(BIN)/with_safestack: $(SRC) + clang $< -o $@ $(SAFE_STACK) + +$(BIN)/with_cfi: $(SRC) + clang $< -o $@ $(CFI) + +$(BIN)/with_fortify: $(SRC) + clang $< -o $@ $(FORTIFY) + +#$(BIN)/with_selfrando: $(SRC) +# srenv gcc $< -o $@ $(GCCFLAGS) $(NO_CANARY) $(NO_NX) $(NO_RELRO) $(NO_PIE) + +$(BIN)/protected: $(SRC) + gcc $< -o $@ $(GCCFLAGS) $(CANARY) $(NX) $(RELRO) $(PIE) + +verify: + /bin/checksec --dir=$(BIN) --extended + +clean: + rm -rf $(BINS) \ No newline at end of file diff --git a/syft/file/cataloger/executable/test-fixtures/project/lib.c b/syft/file/cataloger/executable/test-fixtures/project/lib.c new file mode 100644 index 00000000000..f665b71dc9e --- /dev/null +++ b/syft/file/cataloger/executable/test-fixtures/project/lib.c @@ -0,0 +1,6 @@ +#include + +void foo(void) +{ + puts("Share me!"); +} \ No newline at end of file diff --git a/syft/file/cataloger/executable/test-fixtures/project/lib.h b/syft/file/cataloger/executable/test-fixtures/project/lib.h new file mode 100644 index 00000000000..15f9be76b49 --- /dev/null +++ b/syft/file/cataloger/executable/test-fixtures/project/lib.h @@ -0,0 +1,6 @@ +#ifndef foo_h__ +#define foo_h__ + +extern void foo(void); + +#endif // foo_h__ \ No newline at end of file diff --git a/syft/file/cataloger/executable/test-fixtures/project/main.c b/syft/file/cataloger/executable/test-fixtures/project/main.c new file mode 100644 index 00000000000..6bb5257acf9 --- /dev/null +++ b/syft/file/cataloger/executable/test-fixtures/project/main.c @@ -0,0 +1,106 @@ +#include +#include +#include + +// source: https://github.com/trailofbits/clang-cfi-showcase/blob/master/cfi_icall.c + +typedef int (*int_arg_fn)(int); +typedef int (*float_arg_fn)(float); + +static int int_arg(int arg) { + printf("In %s: (%d)\n", __FUNCTION__, arg); + return 0; +} + +static int float_arg(float arg) { + printf("CFI should protect transfer to here\n"); + printf("In %s: (%f)\n", __FUNCTION__, (double)arg); + return 0; +} + +static int bad_int_arg(int arg) { + printf("CFI will not protect transfer to here\n"); + printf("In %s: (%d)\n", __FUNCTION__, arg); + return 0; +} + +static int not_entry_point(int arg) { + // nop sled for x86 / x86-64 + // these instructions act as a buffer + // for an indirect control flow transfer to skip + // a valid function entry point, but continue + // to execute normal code + __asm__ volatile ( + "nop\n" "nop\n" "nop\n" "nop\n" "nop\n" "nop\n" "nop\n" "nop\n" "nop\n" "nop\n" + "nop\n" "nop\n" "nop\n" "nop\n" "nop\n" "nop\n" "nop\n" "nop\n" "nop\n" "nop\n" + "nop\n" "nop\n" "nop\n" "nop\n" "nop\n" "nop\n" "nop\n" "nop\n" "nop\n" "nop\n" + "nop\n" "nop\n" "nop\n" "nop\n" "nop\n" "nop\n" "nop\n" "nop\n" "nop\n" "nop\n" + "nop\n" "nop\n" "nop\n" "nop\n" "nop\n" "nop\n" "nop\n" "nop\n" "nop\n" "nop\n" + "nop\n" "nop\n" "nop\n" "nop\n" "nop\n" "nop\n" "nop\n" "nop\n" "nop\n" "nop\n" + "nop\n" "nop\n" "nop\n" "nop\n" "nop\n" "nop\n" "nop\n" "nop\n" "nop\n" "nop\n" + "nop\n" "nop\n" "nop\n" "nop\n" "nop\n" "nop\n" "nop\n" "nop\n" "nop\n" "nop\n" + ); + printf("CFI ensures control flow only transfers to potentially valid destinations\n"); + printf("In %s: (%d)\n", __FUNCTION__, arg); + // need to exit or the program will segfault anyway, + // since the indirect call skipped the function preamble + exit(arg); +} + +struct foo { + int_arg_fn int_funcs[1]; + int_arg_fn bad_int_funcs[1]; + float_arg_fn float_funcs[1]; + int_arg_fn not_entries[1]; +}; + +// the struct aligns the function pointer arrays +// so indexing past the end will reliably +// call working function pointers +static struct foo f = { + .int_funcs = {int_arg}, + .bad_int_funcs = {bad_int_arg}, + .float_funcs = {float_arg}, + .not_entries = {(int_arg_fn)((uintptr_t)(not_entry_point)+0x20)} +}; + +void simple1() { + char buf[16]; + fgets(buf, sizeof(buf), stdin); + printf(buf); +} + +void simple2() { + char buf[16]; + scanf("%s", buf); +} + + +int main(int argc, char **argv) { + if(argc != 2) { + printf("Usage: %s