diff --git a/Changelog.md b/Changelog.md index d12e7fd..f3d5e05 100644 --- a/Changelog.md +++ b/Changelog.md @@ -7,6 +7,8 @@ Always update Version in Makefile ### Added - NEW support for Capabilities +- NEW Capability support for ext2/3/4 and squashfs +- NEW Selinux support for SquashFS ### Changed - _check.py_ cleaned up a bit, avoiding using `shell=True` in subprocess invocations. diff --git a/Makefile b/Makefile index 5ea3751..364b0bf 100644 --- a/Makefile +++ b/Makefile @@ -26,6 +26,7 @@ release: build testsetup: gunzip -c test/test.img.gz >test/test.img gunzip -c test/ubifs.img.gz >test/ubifs.img + gunzip -c test/cap_ext2.img.gz >test/cap_ext2.img sudo setcap cap_net_admin+p test/test.cap.file getcap test/test.cap.file diff --git a/Readme.md b/Readme.md index b3d2bee..0eeb67c 100644 --- a/Readme.md +++ b/Readme.md @@ -10,7 +10,8 @@ FwAnalyzer relies on [e2tools](https://github.com/crmulliner/e2tools/) for ext f [squashfs-tools](https://github.com/plougher/squashfs-tools) for SquashFS filesystems, and [ubi_reader](https://github.com/crmulliner/ubi_reader) for UBIFS filesystems. [cpio](https://www.gnu.org/software/cpio/) for cpio archives. -SELinux/xattr support for ext2/3/4 images requires a patched version of [e2tools](https://github.com/crmulliner/e2tools/). +SELinux/Capability support for ext2/3/4 images requires a patched version of [e2tools](https://github.com/crmulliner/e2tools/). +SELinux/Capability support for SquashFS images requires a patched version of [squashfs-tools](https://github.com/crmulliner/squashfs-tools/). ![fwanalyzer](images/fwanalyzer.png) @@ -96,13 +97,15 @@ The global config is used to define some general parameters. The `FsType` (filesystem type) field selects the backend that is used to access the files in the image. The supported options for FsType are: - `dirfs`: to read files from a directory on the host running fwanalyzer, supports Capabilities (supported FsTypeOptions are: N/A) -- `extfs`: to read ext2/3/4 filesystem images (supported FsTypeOptions are: `selinux`) -- `squashfs`: to read SquashFS filesystem images (supported FsTypeOptions are: N/A) +- `extfs`: to read ext2/3/4 filesystem images (supported FsTypeOptions are: `selinux` and `capabilities`) +- `squashfs`: to read SquashFS filesystem images (supported FsTypeOptions are: `securityinfo`) - `ubifs`: to read UBIFS filesystem images (supported FsTypeOptions are: N/A) - `vfatfs`: to read VFat filesystem images (supported FsTypeOptions are: N/A) - `cpiofs`: to read cpio archives (supported FsTypeOptions are: `fixdirs`) The FsTypeOptions allow tuning of the FsType driver. +- `securityinfo`: will enable selinux and capability support for SquashFS images +- `capabilities`: will enable capability support when reading ext filesystem images - `selinux`: will enable selinux support when reading ext filesystem images - `fixdirs`: will attempt to work around a cpio issue where a file exists in a directory while there is no entry for the directory itself diff --git a/pkg/analyzer/analyzer.go b/pkg/analyzer/analyzer.go index 74f65cb..893ea75 100644 --- a/pkg/analyzer/analyzer.go +++ b/pkg/analyzer/analyzer.go @@ -117,17 +117,21 @@ func NewFromConfig(imagepath string, cfgdata string) *Analyzer { var fsp fsparser.FsParser // Set the parser based on the FSType in the config if strings.EqualFold(config.GlobalConfig.FSType, "extfs") { - fsp = extparser.New(imagepath, config.GlobalConfig.FSTypeOptions == "selinux") + fsp = extparser.New(imagepath, + strings.Contains(config.GlobalConfig.FSTypeOptions, "selinux"), + strings.Contains(config.GlobalConfig.FSTypeOptions, "capabilities")) } else if strings.EqualFold(config.GlobalConfig.FSType, "dirfs") { fsp = dirparser.New(imagepath) } else if strings.EqualFold(config.GlobalConfig.FSType, "vfatfs") { fsp = vfatparser.New(imagepath) } else if strings.EqualFold(config.GlobalConfig.FSType, "squashfs") { - fsp = squashfsparser.New(imagepath) + fsp = squashfsparser.New(imagepath, + strings.Contains(config.GlobalConfig.FSTypeOptions, "securityinfo")) } else if strings.EqualFold(config.GlobalConfig.FSType, "ubifs") { fsp = ubifsparser.New(imagepath) } else if strings.EqualFold(config.GlobalConfig.FSType, "cpiofs") { - fsp = cpioparser.New(imagepath, config.GlobalConfig.FSTypeOptions == "fixdirs") + fsp = cpioparser.New(imagepath, + strings.Contains(config.GlobalConfig.FSTypeOptions, "fixdirs")) } else { panic("Cannot find an appropriate parser: " + config.GlobalConfig.FSType) } diff --git a/pkg/extparser/extparser.go b/pkg/extparser/extparser.go index 96b9e54..9af0a11 100644 --- a/pkg/extparser/extparser.go +++ b/pkg/extparser/extparser.go @@ -25,13 +25,16 @@ import ( "strconv" "strings" + "github.com/cruise-automation/fwanalyzer/pkg/capability" "github.com/cruise-automation/fwanalyzer/pkg/fsparser" ) type Ext2Parser struct { - fileinfoReg *regexp.Regexp - selinux bool - imagepath string + fileinfoReg *regexp.Regexp + regexString string + selinux bool + capabilities bool + imagepath string } const ( @@ -39,17 +42,22 @@ const ( e2ToolsLs = "e2ls" ) -func New(imagepath string, selinux bool) *Ext2Parser { +func New(imagepath string, selinux, capabilities bool) *Ext2Parser { parser := &Ext2Parser{ // 365 120777 0 0 7 12-Jul-2018 10:15 true - fileinfoReg: regexp.MustCompile( - `^\s*(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+-\w+-\d+)\s+(\d+:\d+)\s+(\S+)`), - imagepath: imagepath, - selinux: false, + regexString: `^\s*(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+-\w+-\d+)\s+(\d+:\d+)\s+(\S+)`, + imagepath: imagepath, + selinux: false, + capabilities: false, } + if selinux && seLinuxSupported() { parser.enableSeLinux() } + if capabilities && capabilitiesSupported() { + parser.enableCapabilities() + } + parser.fileinfoReg = regexp.MustCompile(parser.regexString) return parser } @@ -60,11 +68,23 @@ func (e *Ext2Parser) ImageName() string { func (e *Ext2Parser) enableSeLinux() { // with selinux support (-Z) // 2600 100750 0 2000 1041 1-Jan-2009 03:00 init.environ.rc u:object_r:rootfs:s0 - e.fileinfoReg = regexp.MustCompile( - `^\s*(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+-\w+-\d+)\s+(\d+:\d+)\s+(\S+)\s+(\S+)`) + // `^\s*(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+-\w+-\d+)\s+(\d+:\d+)\s+(\S+)\s+(\S+)`) + + // append selinux part + e.regexString = e.regexString + `\s+(\S+)` e.selinux = true } +func (e *Ext2Parser) enableCapabilities() { + // with capabilites support (-C) + // 2600 100750 0 2000 1041 1-Jan-2009 03:00 init.environ.rc 0x2000001,0x0,0x0,0x0,0x0 + // `^\s*(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+-\w+-\d+)\s+(\d+:\d+)\s+(\S+)\s+(\S+)`) + + // append capability part + e.regexString = e.regexString + `\s+(\S+)` + e.capabilities = true +} + func (e *Ext2Parser) parseFileLine(line string) fsparser.FileInfo { res := e.fileinfoReg.FindAllStringSubmatch(line, -1) var fi fsparser.FileInfo @@ -81,6 +101,15 @@ func (e *Ext2Parser) parseFileLine(line string) fsparser.FileInfo { fi.SELinuxLabel = fsparser.SELinuxNoLabel } + if e.capabilities { + idx := 9 + if e.selinux { + idx = 10 + } + if res[0][idx] != "-" { + fi.Capabilities, _ = capability.New(res[0][idx]) + } + } return fi } @@ -91,6 +120,9 @@ func (e *Ext2Parser) getDirList(dirpath string, ignoreDot bool) ([]fsparser.File if e.selinux { params += "Z" } + if e.capabilities { + params += "C" + } out, err := exec.Command(e2ToolsLs, params, arg).CombinedOutput() if err != nil { // do NOT print file not found error @@ -162,3 +194,13 @@ func seLinuxSupported() bool { fmt.Fprintln(os.Stderr, "extparser: selinux not supported by your version of e2ls") return false } + +func capabilitiesSupported() bool { + out, _ := exec.Command(e2ToolsLs).CombinedOutput() + // look for C (capability support) in "Usage: e2ls [-acDfilrtZC][-d dir] file" + if strings.Contains(string(out), "C") { + return true + } + fmt.Fprintln(os.Stderr, "extparser: capabilities not supported by your version of e2ls") + return false +} diff --git a/pkg/extparser/extparser_test.go b/pkg/extparser/extparser_test.go index 302641e..f4ae45d 100644 --- a/pkg/extparser/extparser_test.go +++ b/pkg/extparser/extparser_test.go @@ -18,6 +18,7 @@ package extparser import ( "os" + "strings" "testing" ) @@ -26,7 +27,7 @@ var e *Ext2Parser func TestMain(t *testing.T) { testImage := "../../test/test.img" - e = New(testImage, false) + e = New(testImage, false, false) if e.ImageName() != testImage { t.Errorf("ImageName returned bad name") @@ -115,3 +116,21 @@ func TestGetFileInfo(t *testing.T) { } } } + +func TestCap(t *testing.T) { + testImage := "../../test/cap_ext2.img" + + e = New(testImage, false, true) + + if e.ImageName() != testImage { + t.Errorf("ImageName returned bad name") + } + + fi, err := e.GetFileInfo("/test") + if err != nil { + t.Error(err) + } + if !strings.EqualFold(fi.Capabilities[0], "cap_net_admin+p") { + t.Errorf("Capabilities %s don't match", fi.Capabilities) + } +} diff --git a/pkg/squashfsparser/squashfsparser.go b/pkg/squashfsparser/squashfsparser.go index fef7c91..12bbbc6 100644 --- a/pkg/squashfsparser/squashfsparser.go +++ b/pkg/squashfsparser/squashfsparser.go @@ -27,6 +27,7 @@ import ( "strconv" "strings" + "github.com/cruise-automation/fwanalyzer/pkg/capability" "github.com/cruise-automation/fwanalyzer/pkg/fsparser" ) @@ -35,15 +36,12 @@ const ( cpCmd = "cp" ) -var ( - // drwxr-xr-x administrator/administrator 66 2019-04-08 18:49 squashfs-root - fileLineRegex = regexp.MustCompile(`^([A-Za-z-]+)\s+([\-\.\w]+|\d+)/([\-\.\w]+|\d+)\s+(\d+)\s+(\d+-\d+-\d+)\s+(\d+:\d+)\s+(.*)$`) -) - // SquashFSParser parses SquashFS filesystem images. type SquashFSParser struct { - imagepath string - files map[string][]fsparser.FileInfo + fileLineRegex *regexp.Regexp + imagepath string + files map[string][]fsparser.FileInfo + securityInfo bool } func uidForUsername(username string) (int, error) { @@ -131,11 +129,25 @@ func getExtractFile(dirpath string) (string, error) { return extractFile.Name(), nil } +func (s *SquashFSParser) enableSecurityInfo() { + // drwxr-xr-x administrator/administrator 66 2019-04-08 18:49 squashfs-root - - + s.fileLineRegex = regexp.MustCompile(`^([A-Za-z-]+)\s+([\-\.\w]+|\d+)/([\-\.\w]+|\d+)\s+(\d+)\s+(\d+-\d+-\d+)\s+(\d+:\d+)\s+([\S ]+)\t(\S+)\s+(\S)`) + s.securityInfo = true +} + // New returns a new SquashFSParser instance for the given image file. -func New(imagepath string) *SquashFSParser { +func New(imagepath string, securityInfo bool) *SquashFSParser { parser := &SquashFSParser{ - imagepath: imagepath, + // drwxr-xr-x administrator/administrator 66 2019-04-08 18:49 squashfs-root + fileLineRegex: regexp.MustCompile(`^([A-Za-z-]+)\s+([\-\.\w]+|\d+)/([\-\.\w]+|\d+)\s+(\d+)\s+(\d+-\d+-\d+)\s+(\d+:\d+)\s+(.*)$`), + imagepath: imagepath, + securityInfo: false, } + + if securityInfo && securityInfoSupported() { + parser.enableSecurityInfo() + } + return parser } @@ -147,12 +159,12 @@ func normalizePath(filepath string) (dir string, name string) { return } -func parseFileLine(line string) (string, fsparser.FileInfo, error) { +func (s *SquashFSParser) parseFileLine(line string) (string, fsparser.FileInfo, error) { // TODO(jlarimer): add support for reading xattrs. unsquashfs can read // and write xattrs, but it doesn't display them when just listing files. var fi fsparser.FileInfo dirpath := "" - res := fileLineRegex.FindStringSubmatch(line) + res := s.fileLineRegex.FindStringSubmatch(line) if res == nil { return dirpath, fi, fmt.Errorf("Can't match line %s\n", line) } @@ -183,6 +195,14 @@ func parseFileLine(line string) (string, fsparser.FileInfo, error) { } else { dirpath, fi.Name = normalizePath(res[7]) } + + if s.securityInfo { + if res[8] != "-" { + fi.Capabilities, _ = capability.New(res[8]) + } + fi.SELinuxLabel = res[9] + } + return dirpath, fi, nil } @@ -192,14 +212,19 @@ func (s *SquashFSParser) loadFileList() error { } s.files = make(map[string][]fsparser.FileInfo) - out, err := exec.Command(unsquashfsCmd, "-d", "", "-ll", s.imagepath).CombinedOutput() + args := []string{"-d", "", "-lln", s.imagepath} + if s.securityInfo { + args = append([]string{"-llS"}, args...) + } + + out, err := exec.Command(unsquashfsCmd, args...).CombinedOutput() if err != nil { fmt.Fprintf(os.Stderr, "getDirList: %s", err) return err } lines := strings.Split(string(out), "\n") for _, line := range lines { - path, fi, err := parseFileLine(line) + path, fi, err := s.parseFileLine(line) if err == nil { dirfiles := s.files[path] dirfiles = append(dirfiles, fi) @@ -288,3 +313,13 @@ func (f *SquashFSParser) Supported() bool { _, err = exec.LookPath(cpCmd) return err == nil } + +func securityInfoSupported() bool { + out, _ := exec.Command(unsquashfsCmd).CombinedOutput() + // look for -ll[S] (securityInfo support) in output + if strings.Contains(string(out), "-ll[S]") { + return true + } + fmt.Fprintln(os.Stderr, "squashfsparser: security info (selinux + capabilities) not supported by your version of unsquashfs") + return false +} diff --git a/pkg/squashfsparser/squashfsparser_test.go b/pkg/squashfsparser/squashfsparser_test.go index 03cb6b6..e2b3e2f 100644 --- a/pkg/squashfsparser/squashfsparser_test.go +++ b/pkg/squashfsparser/squashfsparser_test.go @@ -19,6 +19,7 @@ package squashfsparser import ( "io/ioutil" "os" + "strings" "testing" "github.com/google/go-cmp/cmp" @@ -157,8 +158,10 @@ func TestParseFileLine(t *testing.T) { }, } + s := New("", false) + for _, test := range tests { - dirpath, fi, err := parseFileLine(test.line) + dirpath, fi, err := s.parseFileLine(test.line) if err != nil && !test.err { t.Errorf("parseFileLine(\"%s\") returned error but shouldn't have: %s", test.line, err) continue @@ -174,7 +177,7 @@ func TestParseFileLine(t *testing.T) { func TestImageName(t *testing.T) { testImage := "../../test/squashfs.img" - f := New(testImage) + f := New(testImage, false) imageName := f.ImageName() if imageName != testImage { @@ -184,7 +187,7 @@ func TestImageName(t *testing.T) { func TestDirInfoRoot(t *testing.T) { testImage := "../../test/squashfs.img" - f := New(testImage) + f := New(testImage, false) /* $ unsquashfs -d "" -ll test/squashfs.img @@ -295,7 +298,7 @@ func TestDirInfoRoot(t *testing.T) { func TestGetFileInfo(t *testing.T) { testImage := "../../test/squashfs.img" - f := New(testImage) + f := New(testImage, false) fi, err := f.GetFileInfo("/") if err != nil { @@ -335,7 +338,7 @@ func TestGetFileInfo(t *testing.T) { func TestCopyFile(t *testing.T) { testImage := "../../test/squashfs.img" - f := New(testImage) + f := New(testImage, false) if !f.CopyFile("/dir2/subdir2/file4", ".") { t.Errorf("CopyFile() returned false") @@ -354,3 +357,22 @@ func TestCopyFile(t *testing.T) { t.Errorf("file4 expected \"%s\" but got \"%s\"", expected, data) } } + +func TestSecurityInfo(t *testing.T) { + testImage := "../../test/squashfs_cap.img" + f := New(testImage, true) + + fi, err := f.GetFileInfo("/ifconfig") + if err != nil { + t.Error(err) + } + + if fi.SELinuxLabel != "-" { + t.Error("no selinux label should be present") + } + + if !strings.EqualFold(fi.Capabilities[0], "cap_net_admin+p") { + t.Errorf("bad capabilities: %s", fi.Capabilities) + } + +} diff --git a/test/cap_ext2.img.gz b/test/cap_ext2.img.gz new file mode 100644 index 0000000..b4621a1 Binary files /dev/null and b/test/cap_ext2.img.gz differ diff --git a/test/e2cp b/test/e2cp index 0a4a16b..133b867 100755 Binary files a/test/e2cp and b/test/e2cp differ diff --git a/test/squashfs_cap.img b/test/squashfs_cap.img new file mode 100644 index 0000000..9426a3e Binary files /dev/null and b/test/squashfs_cap.img differ diff --git a/test/unsquashfs b/test/unsquashfs new file mode 100755 index 0000000..4d41162 Binary files /dev/null and b/test/unsquashfs differ