From d48e6b44f9d98b7a7ce20851d5d1993aceb02dfd Mon Sep 17 00:00:00 2001 From: Liam Galvin Date: Thu, 25 Nov 2021 22:15:24 +0000 Subject: [PATCH] Add mach-o support (#1) * Add mach-o support * Update readme --- README.md | 26 ++++++- pkg/format/definitions.go | 7 +- pkg/format/formats.go | 15 ++-- pkg/format/sniff_test.go | 8 +-- pkg/parser/macho/analyse.go | 19 +++++ pkg/parser/macho/hardening/arc.go | 18 +++++ pkg/parser/macho/hardening/encryption.go | 12 ++++ pkg/parser/macho/hardening/hardening.go | 42 +++++++++++ pkg/parser/macho/hardening/stack.go | 18 +++++ pkg/parser/macho/metadata.go | 25 +++++++ pkg/parser/macho/parser.go | 46 ++++++++++++ pkg/parser/macho/report.go | 90 ++++++++++++++++++++++++ pkg/parser/parsers.go | 2 + 13 files changed, 306 insertions(+), 22 deletions(-) create mode 100644 pkg/parser/macho/analyse.go create mode 100644 pkg/parser/macho/hardening/arc.go create mode 100644 pkg/parser/macho/hardening/encryption.go create mode 100644 pkg/parser/macho/hardening/hardening.go create mode 100644 pkg/parser/macho/hardening/stack.go create mode 100644 pkg/parser/macho/metadata.go create mode 100644 pkg/parser/macho/parser.go create mode 100644 pkg/parser/macho/report.go diff --git a/README.md b/README.md index 31598fe..574bfbe 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Analyse binaries for missing security features, information disclosure and more. -:construction: Extrude is in the early stages of development, and currently only supports ELF binaries. +:construction: Extrude is in the early stages of development, and currently only supports ELF and MachO binaries. PE (Windows) binaries will be supported soon. ![Screenshot](screenshot.png) @@ -27,9 +27,31 @@ You can optionally run extrude with docker via: docker run -v `pwd`:/blah -it ghcr.io/liamg/extrude /blah/targetfile ``` +## Supported Checks + +### ELF + +- PIE +- RELRO +- BIND NOW +- Fortified Source +- Stack Canary +- NX Stack + +### MachO + +- PIE +- Stack Canary +- NX Stack +- NX Heap +- ARC + +### Windows + +_Coming soon..._ + ## TODO -- Add support for Mach-o - Add support for PE - Add secret scanning - Detect packers diff --git a/pkg/format/definitions.go b/pkg/format/definitions.go index bb84372..e908416 100644 --- a/pkg/format/definitions.go +++ b/pkg/format/definitions.go @@ -20,15 +20,10 @@ var definitions = []definition{ }, }, { - format: MachO32, + format: MachO, signatures: [][]byte{ {0xfe, 0xed, 0xfa, 0xce}, {0xce, 0xfa, 0xed, 0xfe}, - }, - }, - { - format: MachO64, - signatures: [][]byte{ {0xfe, 0xed, 0xfa, 0xcf}, {0xcf, 0xfa, 0xed, 0xfe}, }, diff --git a/pkg/format/formats.go b/pkg/format/formats.go index b8b7ba5..120c1e2 100644 --- a/pkg/format/formats.go +++ b/pkg/format/formats.go @@ -7,8 +7,7 @@ type Format uint8 const ( Unknown Format = iota ELF - MachO32 - MachO64 + MachO PE ) @@ -16,10 +15,8 @@ func (f Format) Short() string { switch f { case ELF: return "ELF" - case MachO32: - return "Mach-O 32" - case MachO64: - return "Mach-O 64" + case MachO: + return "Mach-O" case PE: return "PE" } @@ -30,10 +27,8 @@ func (f Format) Long() string { switch f { case ELF: return "Executable and Linkable Format" - case MachO32: - return "32-bit Mach Object File" - case MachO64: - return "64-bit Mach Object File" + case MachO: + return "Mach Object File" case PE: return "Portable Executable" } diff --git a/pkg/format/sniff_test.go b/pkg/format/sniff_test.go index fef29d6..f430c4c 100644 --- a/pkg/format/sniff_test.go +++ b/pkg/format/sniff_test.go @@ -16,19 +16,19 @@ func TestFormatSniffing(t *testing.T) { }{ { content: []byte{0xfe, 0xed, 0xfa, 0xce}, - expected: MachO32, + expected: MachO, }, { content: []byte{0xce, 0xfa, 0xed, 0xfe}, - expected: MachO32, + expected: MachO, }, { content: []byte{0xfe, 0xed, 0xfa, 0xcf}, - expected: MachO64, + expected: MachO, }, { content: []byte{0xcf, 0xfa, 0xed, 0xfe}, - expected: MachO64, + expected: MachO, }, { content: []byte{0x7f, 'E', 'L', 'F'}, diff --git a/pkg/parser/macho/analyse.go b/pkg/parser/macho/analyse.go new file mode 100644 index 0000000..be18aa2 --- /dev/null +++ b/pkg/parser/macho/analyse.go @@ -0,0 +1,19 @@ +package macho + +import "github.com/liamg/extrude/pkg/parser/macho/hardening" + +func (m *Metadata) analyse() error { + + if m.fat != nil { + var coreAttr hardening.Attributes + for _, arch := range m.fat.Arches { + attr := hardening.IdentifyAttributes(arch.File) + coreAttr = coreAttr.Merge(attr) + } + m.Hardening = coreAttr + } else { + m.Hardening = hardening.IdentifyAttributes(m.thin) + } + + return nil +} diff --git a/pkg/parser/macho/hardening/arc.go b/pkg/parser/macho/hardening/arc.go new file mode 100644 index 0000000..2ba027f --- /dev/null +++ b/pkg/parser/macho/hardening/arc.go @@ -0,0 +1,18 @@ +package hardening + +import ( + "debug/macho" +) + +func checkAutomaticReferenceCounting(f *macho.File) bool { + symbols, err := f.ImportedSymbols() + if err != nil { + return false + } + for _, imp := range symbols { + if imp == "_objc_release" { + return true + } + } + return false +} diff --git a/pkg/parser/macho/hardening/encryption.go b/pkg/parser/macho/hardening/encryption.go new file mode 100644 index 0000000..598f63f --- /dev/null +++ b/pkg/parser/macho/hardening/encryption.go @@ -0,0 +1,12 @@ +package hardening + +import "debug/macho" + +const ( + EncInfo32 = 0x21 + EncInfo64 = 0x2c +) + +func checkEncrypted(f *macho.File) bool { + return f.Symtab.Cmd&EncInfo32 > 0 || f.Symtab.Cmd&EncInfo64 > 0 +} diff --git a/pkg/parser/macho/hardening/hardening.go b/pkg/parser/macho/hardening/hardening.go new file mode 100644 index 0000000..8829b1d --- /dev/null +++ b/pkg/parser/macho/hardening/hardening.go @@ -0,0 +1,42 @@ +package hardening + +import "debug/macho" + +type Attributes struct { + init bool + PositionIndependentExecutable bool + StackExecutionNotAllowed bool + HeapExecutionNotAllowed bool + StackProtected bool + AutomaticReferenceCounting bool + Encrypted bool +} + +func (a Attributes) Merge(b Attributes) Attributes { + if !a.init { + return b + } + if !b.init { + return a + } + return Attributes{ + PositionIndependentExecutable: a.PositionIndependentExecutable && b.PositionIndependentExecutable, + StackExecutionNotAllowed: a.StackExecutionNotAllowed && b.StackExecutionNotAllowed, + HeapExecutionNotAllowed: a.HeapExecutionNotAllowed && b.HeapExecutionNotAllowed, + StackProtected: a.StackProtected && b.StackProtected, + AutomaticReferenceCounting: a.AutomaticReferenceCounting && b.AutomaticReferenceCounting, + Encrypted: a.Encrypted && b.Encrypted, + } +} + +func IdentifyAttributes(f *macho.File) Attributes { + return Attributes{ + init: true, + PositionIndependentExecutable: f.Flags&macho.FlagPIE > 0, + StackExecutionNotAllowed: f.Flags&macho.FlagAllowStackExecution == 0, + HeapExecutionNotAllowed: f.Flags&macho.FlagNoHeapExecution > 0, + StackProtected: checkStackProtected(f), + AutomaticReferenceCounting: checkAutomaticReferenceCounting(f), + Encrypted: checkEncrypted(f), + } +} diff --git a/pkg/parser/macho/hardening/stack.go b/pkg/parser/macho/hardening/stack.go new file mode 100644 index 0000000..1fafe9f --- /dev/null +++ b/pkg/parser/macho/hardening/stack.go @@ -0,0 +1,18 @@ +package hardening + +import ( + "debug/macho" +) + +func checkStackProtected(f *macho.File) bool { + symbols, err := f.ImportedSymbols() + if err != nil { + return false + } + for _, imp := range symbols { + if imp == "___stack_chk_fail" || imp == "___stack_chk_guard" { + return true + } + } + return false +} diff --git a/pkg/parser/macho/metadata.go b/pkg/parser/macho/metadata.go new file mode 100644 index 0000000..2e113b7 --- /dev/null +++ b/pkg/parser/macho/metadata.go @@ -0,0 +1,25 @@ +package macho + +import ( + "debug/macho" + + "github.com/liamg/extrude/pkg/format" + "github.com/liamg/extrude/pkg/parser/macho/hardening" +) + +type Metadata struct { + File struct { + Path string + Name string + Format format.Format + } + Hardening hardening.Attributes + thin *macho.File + fat *macho.FatFile + Notes []Note +} + +type Note struct { + Heading string + Content string +} diff --git a/pkg/parser/macho/parser.go b/pkg/parser/macho/parser.go new file mode 100644 index 0000000..af935db --- /dev/null +++ b/pkg/parser/macho/parser.go @@ -0,0 +1,46 @@ +package macho + +import ( + "debug/macho" + "io" + "path/filepath" + + "github.com/liamg/extrude/pkg/format" + "github.com/liamg/extrude/pkg/report" +) + +type parser struct{} + +func New() *parser { + return &parser{} +} + +func (*parser) Parse(r io.ReaderAt, path string, format format.Format) (report.Reporter, error) { + + var metadata Metadata + + metadata.File.Path = path + metadata.File.Name = filepath.Base(path) + metadata.File.Format = format + + fat, err := macho.NewFatFile(r) + if err != nil { + if err != macho.ErrNotFat { + return nil, err + } + thin, err := macho.NewFile(r) + if err != nil { + return nil, err + } + defer func() { _ = thin.Close() }() + metadata.thin = thin + } else { + defer func() { _ = fat.Close() }() + metadata.fat = fat + } + + if err := metadata.analyse(); err != nil { + return nil, err + } + return &metadata, nil +} diff --git a/pkg/parser/macho/report.go b/pkg/parser/macho/report.go new file mode 100644 index 0000000..b34be70 --- /dev/null +++ b/pkg/parser/macho/report.go @@ -0,0 +1,90 @@ +package macho + +import ( + "strings" + + "github.com/liamg/extrude/pkg/report" +) + +func (m *Metadata) CreateReport() (report.Report, error) { + rep := report.New() + + overview := report.NewSection("Overview") + + overview.AddKeyValue("File", m.File.Path) + overview.AddKeyValue("Format", m.File.Format.String()) + + if m.fat != nil { + overview.AddKeyValue("Universal", "Yes (Fat)") + var arches []string + for _, arch := range m.fat.Arches { + arches = append(arches, arch.Cpu.String()) + } + overview.AddKeyValue("Architectures", strings.Join(arches, ", ")) + } else { + overview.AddKeyValue("Univeral", "No (Thin)") + overview.AddKeyValue("Type", m.thin.Type.String()) + overview.AddKeyValue("Architecture", m.thin.Cpu.String()) + } + + rep.AddSection(overview) + + security := report.NewSection("Security Features") + + security.AddTest( + "Position Independent Executable", + boolToResult(m.Hardening.PositionIndependentExecutable), + `A PIE binary and all of its dependencies are loaded into random locations within virtual memory each time the application is executed. This makes Return Oriented Programming (ROP) attacks much more difficult to execute reliably.`, + ) + + security.AddTest( + "Stack Canary", + boolToResult(m.Hardening.StackProtected), + `A "canary" value is pushed onto the stack immediately after the function return pointer. The canary value is then checked before the function returns; if it has changed, the program will abort. This makes buffer overflow attacks much more difficult to carry out.`, + ) + + security.AddTest( + "Non-Executable Stack", + boolToResult(m.Hardening.StackExecutionNotAllowed), + `Preventing the stack from being executable means that malicious code injected onto the stack cannot be run.`, + ) + + security.AddTest( + "Non-Executable Heap", + boolToResult(m.Hardening.HeapExecutionNotAllowed), + `Preventing the heap from being executable means that malicious code written to the heap cannot be run.`, + ) + + security.AddTest( + "Automatic Reference Counting", + boolToResult(m.Hardening.AutomaticReferenceCounting), + `ARC is a runtime memory safety mechanism which keeps track of objects and frees them once they are no longer referenced.`, + ) + + /* In progress... + security.AddTest( + "Encryption", + boolToResult(m.Hardening.Encrypted), + ``, + ) + */ + + rep.AddSection(security) + + if len(m.Notes) > 0 { + notes := report.NewSection("Other Findings") + for _, note := range m.Notes { + notes.AddTest(note.Heading, report.Warning, note.Content) + } + rep.AddSection(notes) + } + + return rep, nil +} + +func boolToResult(in bool) report.Result { + if in { + return report.Pass + } + return report.Fail +} diff --git a/pkg/parser/parsers.go b/pkg/parser/parsers.go index dad4905..7b63c65 100644 --- a/pkg/parser/parsers.go +++ b/pkg/parser/parsers.go @@ -5,6 +5,7 @@ import ( "github.com/liamg/extrude/pkg/format" "github.com/liamg/extrude/pkg/parser/elf" + "github.com/liamg/extrude/pkg/parser/macho" "github.com/liamg/extrude/pkg/report" ) @@ -16,4 +17,5 @@ var parsers = make(map[format.Format]Parser) func init() { parsers[format.ELF] = elf.New() + parsers[format.MachO] = macho.New() }