github.com/anchore/syft@v1.4.2-0.20240516191711-1bec1fc5d397/syft/file/cataloger/executable/elf.go (about)

     1  package executable
     2  
     3  import (
     4  	"debug/elf"
     5  	"regexp"
     6  	"strings"
     7  
     8  	"github.com/scylladb/go-set/strset"
     9  
    10  	"github.com/anchore/syft/internal/log"
    11  	"github.com/anchore/syft/syft/file"
    12  	"github.com/anchore/syft/syft/internal/unionreader"
    13  )
    14  
    15  func findELFFeatures(data *file.Executable, reader unionreader.UnionReader) error {
    16  	f, err := elf.NewFile(reader)
    17  	if err != nil {
    18  		return err
    19  	}
    20  
    21  	libs, err := f.ImportedLibraries()
    22  	if err != nil {
    23  		// TODO: known-unknowns
    24  		log.WithFields("error", err).Trace("unable to read imported libraries from elf file")
    25  		libs = nil
    26  	}
    27  
    28  	if libs == nil {
    29  		libs = []string{}
    30  	}
    31  
    32  	data.ImportedLibraries = libs
    33  	data.ELFSecurityFeatures = findELFSecurityFeatures(f)
    34  	data.HasEntrypoint = elfHasEntrypoint(f)
    35  	data.HasExports = elfHasExports(f)
    36  
    37  	return nil
    38  }
    39  
    40  func findELFSecurityFeatures(f *elf.File) *file.ELFSecurityFeatures {
    41  	return &file.ELFSecurityFeatures{
    42  		SymbolTableStripped:           isElfSymbolTableStripped(f),
    43  		StackCanary:                   checkElfStackCanary(f),
    44  		NoExecutable:                  checkElfNXProtection(f),
    45  		RelocationReadOnly:            checkElfRelROProtection(f),
    46  		PositionIndependentExecutable: isELFPIE(f),
    47  		DynamicSharedObject:           isELFDSO(f),
    48  		LlvmSafeStack:                 checkLLVMSafeStack(f),
    49  		LlvmControlFlowIntegrity:      checkLLVMControlFlowIntegrity(f),
    50  		ClangFortifySource:            checkClangFortifySource(f),
    51  	}
    52  }
    53  
    54  func isElfSymbolTableStripped(file *elf.File) bool {
    55  	return file.Section(".symtab") == nil
    56  }
    57  
    58  func checkElfStackCanary(file *elf.File) *bool {
    59  	return hasAnyDynamicSymbols(file, "__stack_chk_fail", "__stack_chk_guard")
    60  }
    61  
    62  func hasAnyDynamicSymbols(file *elf.File, symbolNames ...string) *bool {
    63  	dynSyms, err := file.DynamicSymbols()
    64  	if err != nil {
    65  		// TODO: known-unknowns
    66  		log.WithFields("error", err).Trace("unable to read dynamic symbols from elf file")
    67  		return nil
    68  	}
    69  
    70  	nameSet := strset.New(symbolNames...)
    71  
    72  	for _, sym := range dynSyms {
    73  		if nameSet.Has(sym.Name) {
    74  			return boolRef(true)
    75  		}
    76  	}
    77  	return boolRef(false)
    78  }
    79  
    80  func boolRef(b bool) *bool {
    81  	return &b
    82  }
    83  
    84  func checkElfNXProtection(file *elf.File) bool {
    85  	// find the program headers until you find the GNU_STACK segment
    86  	for _, prog := range file.Progs {
    87  		if prog.Type == elf.PT_GNU_STACK {
    88  			// check if the GNU_STACK segment is executable
    89  			return prog.Flags&elf.PF_X == 0
    90  		}
    91  	}
    92  
    93  	return false
    94  }
    95  
    96  func checkElfRelROProtection(f *elf.File) file.RelocationReadOnly {
    97  	// background on relro https://www.redhat.com/en/blog/hardening-elf-binaries-using-relocation-read-only-relro
    98  	hasRelro := false
    99  	hasBindNow := hasBindNowDynTagOrFlag(f)
   100  
   101  	for _, prog := range f.Progs {
   102  		if prog.Type == elf.PT_GNU_RELRO {
   103  			hasRelro = true
   104  			break
   105  		}
   106  	}
   107  
   108  	switch {
   109  	case hasRelro && hasBindNow:
   110  		return file.RelocationReadOnlyFull
   111  	case hasRelro:
   112  		return file.RelocationReadOnlyPartial
   113  	default:
   114  		return file.RelocationReadOnlyNone
   115  	}
   116  }
   117  
   118  func hasBindNowDynTagOrFlag(f *elf.File) bool {
   119  	if hasElfDynTag(f, elf.DT_BIND_NOW) {
   120  		// support older binaries...
   121  		return true
   122  	}
   123  
   124  	// "DT_BIND_NOW ... use has been superseded by the DF_BIND_NOW flag"
   125  	// source: https://refspecs.linuxbase.org/elf/gabi4+/ch5.dynamic.html
   126  	return hasElfDynFlag(f, elf.DF_BIND_NOW)
   127  }
   128  
   129  func hasElfDynFlag(f *elf.File, flag elf.DynFlag) bool {
   130  	vals, err := f.DynValue(elf.DT_FLAGS)
   131  	if err != nil {
   132  		// TODO: known-unknowns
   133  		log.WithFields("error", err).Trace("unable to read DT_FLAGS from elf file")
   134  		return false
   135  	}
   136  	for _, val := range vals {
   137  		if val&uint64(flag) != 0 {
   138  			return true
   139  		}
   140  	}
   141  	return false
   142  }
   143  
   144  func hasElfDynFlag1(f *elf.File, flag elf.DynFlag1) bool {
   145  	vals, err := f.DynValue(elf.DT_FLAGS_1)
   146  	if err != nil {
   147  		// TODO: known-unknowns
   148  		log.WithFields("error", err).Trace("unable to read DT_FLAGS_1 from elf file")
   149  		return false
   150  	}
   151  	for _, val := range vals {
   152  		if val&uint64(flag) != 0 {
   153  			return true
   154  		}
   155  	}
   156  	return false
   157  }
   158  
   159  func hasElfDynTag(f *elf.File, tag elf.DynTag) bool {
   160  	// source https://github.com/golang/go/blob/9b4b3e5acca2dabe107fa2c3ed963097d78a4562/src/cmd/cgo/internal/testshared/shared_test.go#L280
   161  
   162  	ds := f.SectionByType(elf.SHT_DYNAMIC)
   163  	if ds == nil {
   164  		return false
   165  	}
   166  	d, err := ds.Data()
   167  	if err != nil {
   168  		return false
   169  	}
   170  
   171  	for len(d) > 0 {
   172  		var t elf.DynTag
   173  		switch f.Class {
   174  		case elf.ELFCLASS32:
   175  			t = elf.DynTag(f.ByteOrder.Uint32(d[0:4]))
   176  			d = d[8:]
   177  		case elf.ELFCLASS64:
   178  			t = elf.DynTag(f.ByteOrder.Uint64(d[0:8]))
   179  			d = d[16:]
   180  		}
   181  		if t == tag {
   182  			return true
   183  		}
   184  	}
   185  	return false
   186  }
   187  
   188  func isELFPIE(f *elf.File) bool {
   189  	// being a shared object is not sufficient to be a PIE, the explicit flag must be set also
   190  	return isELFDSO(f) && hasElfDynFlag1(f, elf.DF_1_PIE)
   191  }
   192  
   193  func isELFDSO(f *elf.File) bool {
   194  	return f.Type == elf.ET_DYN
   195  }
   196  
   197  func checkLLVMSafeStack(file *elf.File) *bool {
   198  	// looking for the presence of https://github.com/microsoft/compiler-rt/blob/30b3b8cb5c9a0854f2f40f187c6f6773561a35f2/lib/safestack/safestack.cc#L207
   199  	return hasAnyDynamicSymbols(file, "__safestack_init")
   200  }
   201  
   202  func checkLLVMControlFlowIntegrity(file *elf.File) *bool {
   203  	// look for any symbols that are functions and end with ".cfi"
   204  	dynSyms, err := file.Symbols()
   205  	if err != nil {
   206  		// TODO: known-unknowns
   207  		log.WithFields("error", err).Trace("unable to read symbols from elf file")
   208  		return nil
   209  	}
   210  
   211  	for _, sym := range dynSyms {
   212  		if isFunction(sym) && strings.HasSuffix(sym.Name, ".cfi") {
   213  			return boolRef(true)
   214  		}
   215  	}
   216  	return boolRef(false)
   217  }
   218  
   219  func isFunction(sym elf.Symbol) bool {
   220  	return elf.ST_TYPE(sym.Info) == elf.STT_FUNC
   221  }
   222  
   223  var fortifyPattern = regexp.MustCompile(`__\w+_chk@.+`)
   224  
   225  func checkClangFortifySource(file *elf.File) *bool {
   226  	dynSyms, err := file.Symbols()
   227  	if err != nil {
   228  		// TODO: known-unknowns
   229  		log.WithFields("error", err).Trace("unable to read symbols from elf file")
   230  		return nil
   231  	}
   232  
   233  	for _, sym := range dynSyms {
   234  		if isFunction(sym) && fortifyPattern.MatchString(sym.Name) {
   235  			return boolRef(true)
   236  		}
   237  	}
   238  	return boolRef(false)
   239  }
   240  
   241  func elfHasEntrypoint(f *elf.File) bool {
   242  	// this is akin to
   243  	//    readelf -h ./path/to/bin | grep "Entry point address"
   244  	return f.Entry > 0
   245  }
   246  
   247  func elfHasExports(f *elf.File) bool {
   248  	// this is akin to:
   249  	//    nm -D --defined-only ./path/to/bin | grep ' T \| W \| B '
   250  	// where:
   251  	//   T - symbol in the text section
   252  	//   W - weak symbol that might be overwritten
   253  	//   B - variable located in the uninitialized data section
   254  	// really anything that is not marked with 'U' (undefined) is considered an export.
   255  	symbols, err := f.DynamicSymbols()
   256  	if err != nil {
   257  		// TODO: known-unknowns?
   258  		return false
   259  	}
   260  
   261  	for _, s := range symbols {
   262  		// check if the section is SHN_UNDEF, which is the "U" output type from "nm -D" meaning that the symbol
   263  		// is undefined, meaning it is not an export. Any entry that is not undefined is considered an export.
   264  		if s.Section != elf.SHN_UNDEF {
   265  			return true
   266  		}
   267  	}
   268  
   269  	return false
   270  }