diff --git a/cmd/audit/exe.go b/cmd/audit/exe.go new file mode 100644 index 0000000..f08435f --- /dev/null +++ b/cmd/audit/exe.go @@ -0,0 +1,209 @@ +package main + +import ( + "bytes" + "debug/elf" + "debug/macho" + "debug/pe" + "fmt" + "io" + "os" +) + +// An exe is a generic interface to an OS executable (ELF, Mach-O, PE). +type exe interface { + // Close closes the underlying file. + Close() error + + // ReadData reads and returns up to size byte starting at virtual address addr. + ReadData(addr, size uint64) ([]byte, error) + + // DataStart returns the writable data segment start address. + DataStart() uint64 +} + +// openExe opens file and returns it as an exe. + +// openExe opens file and returns it as an exe. +func openExe(file string) (exe, error) { + f, err := os.Open(file) + if err != nil { + return nil, err + } + data := make([]byte, 16) + if _, err := io.ReadFull(f, data); err != nil { + return nil, err + } + f.Seek(0, 0) + if bytes.HasPrefix(data, []byte("\x7FELF")) { + e, err := elf.NewFile(f) + if err != nil { + f.Close() + return nil, err + } + return &elfExe{f, e}, nil + } + if bytes.HasPrefix(data, []byte("MZ")) { + e, err := pe.NewFile(f) + if err != nil { + f.Close() + return nil, err + } + return &peExe{f, e}, nil + } + if bytes.HasPrefix(data, []byte("\xFE\xED\xFA")) || bytes.HasPrefix(data[1:], []byte("\xFA\xED\xFE")) { + e, err := macho.NewFile(f) + if err != nil { + f.Close() + return nil, err + } + return &machoExe{f, e}, nil + } + return nil, fmt.Errorf("unrecognized executable format") +} + +// elfExe is the ELF implementation of the exe interface. +type elfExe struct { + os *os.File + f *elf.File +} + +func (x *elfExe) Close() error { + return x.os.Close() +} + +func (x *elfExe) ReadData(addr, size uint64) ([]byte, error) { + for _, prog := range x.f.Progs { + fmt.Printf("%#x %#x %#x\n", addr, prog.Vaddr, prog.Vaddr+prog.Filesz) + if prog.Vaddr <= addr && addr <= prog.Vaddr+prog.Filesz-1 { + n := prog.Vaddr + prog.Filesz - addr + if n > size { + n = size + } + data := make([]byte, n) + _, err := prog.ReadAt(data, int64(addr-prog.Vaddr)) + if err != nil { + return nil, err + } + return data, nil + } + } + return nil, fmt.Errorf("address not mapped") +} + +func (x *elfExe) DataStart() uint64 { + for _, p := range x.f.Progs { + if p.Type == elf.PT_LOAD && p.Flags&(elf.PF_X|elf.PF_W) == elf.PF_W { + return p.Vaddr + } + } + return 0 +} + +// peExe is the PE (Windows Portable Executable) implementation of the exe interface. +type peExe struct { + os *os.File + f *pe.File +} + +func (x *peExe) Close() error { + return x.os.Close() +} + +func (x *peExe) imageBase() uint64 { + switch oh := x.f.OptionalHeader.(type) { + case *pe.OptionalHeader32: + return uint64(oh.ImageBase) + case *pe.OptionalHeader64: + return oh.ImageBase + } + return 0 +} + +func (x *peExe) ReadData(addr, size uint64) ([]byte, error) { + addr -= x.imageBase() + for _, sect := range x.f.Sections { + if uint64(sect.VirtualAddress) <= addr && addr <= uint64(sect.VirtualAddress+sect.Size-1) { + n := uint64(sect.VirtualAddress+sect.Size) - addr + if n > size { + n = size + } + data := make([]byte, n) + _, err := sect.ReadAt(data, int64(addr-uint64(sect.VirtualAddress))) + if err != nil { + return nil, err + } + return data, nil + } + } + return nil, fmt.Errorf("address not mapped") +} + +func (x *peExe) DataStart() uint64 { + // Assume data is first writable section. + const ( + IMAGE_SCN_CNT_CODE = 0x00000020 + IMAGE_SCN_CNT_INITIALIZED_DATA = 0x00000040 + IMAGE_SCN_CNT_UNINITIALIZED_DATA = 0x00000080 + IMAGE_SCN_MEM_EXECUTE = 0x20000000 + IMAGE_SCN_MEM_READ = 0x40000000 + IMAGE_SCN_MEM_WRITE = 0x80000000 + IMAGE_SCN_MEM_DISCARDABLE = 0x2000000 + IMAGE_SCN_LNK_NRELOC_OVFL = 0x1000000 + IMAGE_SCN_ALIGN_32BYTES = 0x600000 + ) + for _, sect := range x.f.Sections { + if sect.VirtualAddress != 0 && sect.Size != 0 && + sect.Characteristics&^IMAGE_SCN_ALIGN_32BYTES == IMAGE_SCN_CNT_INITIALIZED_DATA|IMAGE_SCN_MEM_READ|IMAGE_SCN_MEM_WRITE { + return uint64(sect.VirtualAddress) + x.imageBase() + } + } + return 0 +} + +// machoExe is the Mach-O (Apple macOS/iOS) implementation of the exe interface. +type machoExe struct { + os *os.File + f *macho.File +} + +func (x *machoExe) Close() error { + return x.os.Close() +} + +func (x *machoExe) ReadData(addr, size uint64) ([]byte, error) { + for _, load := range x.f.Loads { + seg, ok := load.(*macho.Segment) + if !ok { + continue + } + if seg.Addr <= addr && addr <= seg.Addr+seg.Filesz-1 { + if seg.Name == "__PAGEZERO" { + continue + } + n := seg.Addr + seg.Filesz - addr + if n > size { + n = size + } + data := make([]byte, n) + _, err := seg.ReadAt(data, int64(addr-seg.Addr)) + if err != nil { + return nil, err + } + return data, nil + } + } + return nil, fmt.Errorf("address not mapped") +} + +func (x *machoExe) DataStart() uint64 { + // Assume data is first non-empty writable segment. + const RW = 3 + for _, load := range x.f.Loads { + seg, ok := load.(*macho.Segment) + if ok && seg.Addr != 0 && seg.Filesz != 0 && seg.Prot == RW && seg.Maxprot == RW { + return seg.Addr + } + } + return 0 +} diff --git a/cmd/audit/main.go b/cmd/audit/main.go new file mode 100644 index 0000000..5dbf3c1 --- /dev/null +++ b/cmd/audit/main.go @@ -0,0 +1,140 @@ +package main + +import ( + "bytes" + "encoding/binary" + "fmt" + "os" + "runtime" + "strings" +) + +func main() { + args := os.Args + + if len(args) > 1 && args[1] == "server" { + fmt.Fprintf(os.Stderr, "Not implemented\n") + os.Exit(1) + } else if len(args) > 1 { + file := args[len(args)-1] + info, err := os.Stat(file) + if err != nil { + fmt.Fprintf(os.Stderr, "Error trying to process file :%v\n", err) + os.Exit(1) + } + scanFile(file, info) + } else { + //s : = server.New() + //s.start() + fmt.Fprintf(os.Stderr, "Usage: LALAL\n") + } + +} + +// scanFile scans file to try to report the Go and module versions. +func scanFile(file string, info os.FileInfo) { + if info.Mode()&os.ModeSymlink != 0 { + // Accept file symlinks only. + i, err := os.Stat(file) + if err != nil || !i.Mode().IsRegular() { + fmt.Fprintf(os.Stderr, "%s: symlink\n", file) + return + } + info = i + } + if !isExe(file, info) { + fmt.Fprintf(os.Stderr, "%s: not executable file\n", file) + return + } + + x, err := openExe(file) + if err != nil { + fmt.Fprintf(os.Stderr, "%s: %v\n", file, err) + return + } + defer x.Close() + + vers, mod := findVers(x) + if vers == "" { + fmt.Fprintf(os.Stderr, "%s: go version not found\n", file) + return + } + + fmt.Printf("%s: %s\n", file, vers) + if mod != "" { + fmt.Printf("\t%s\n", strings.Replace(mod[:len(mod)-1], "\n", "\n\t", -1)) + } +} + +// isExe reports whether the file should be considered executable. +func isExe(file string, info os.FileInfo) bool { + if runtime.GOOS == "windows" { + return strings.HasSuffix(strings.ToLower(file), ".exe") + } + return info.Mode().IsRegular() && info.Mode()&0111 != 0 +} + +// The build info blob left by the linker is identified by +// a 16-byte header, consisting of buildInfoMagic (14 bytes), +// the binary's pointer size (1 byte), +// and whether the binary is big endian (1 byte). +var buildInfoMagic = []byte("\xff Go buildinf:") + +// findVers finds and returns the Go version and module version information +// in the executable x. +func findVers(x exe) (vers, mod string) { + // Read the first 64kB of text to find the build info blob. + text := x.DataStart() + data, err := x.ReadData(text, 64*1024) + if err != nil { + return + } + for ; !bytes.HasPrefix(data, buildInfoMagic); data = data[32:] { + if len(data) < 32 { + return + } + } + + // Decode the blob. + ptrSize := int(data[14]) + bigEndian := data[15] != 0 + var bo binary.ByteOrder + if bigEndian { + bo = binary.BigEndian + } else { + bo = binary.LittleEndian + } + var readPtr func([]byte) uint64 + if ptrSize == 4 { + readPtr = func(b []byte) uint64 { return uint64(bo.Uint32(b)) } + } else { + readPtr = bo.Uint64 + } + vers = readString(x, ptrSize, readPtr, readPtr(data[16:])) + if vers == "" { + return + } + mod = readString(x, ptrSize, readPtr, readPtr(data[16+ptrSize:])) + if len(mod) >= 33 && mod[len(mod)-17] == '\n' { + // Strip module framing. + mod = mod[16 : len(mod)-16] + } else { + mod = "" + } + return +} + +// readString returns the string at address addr in the executable x. +func readString(x exe, ptrSize int, readPtr func([]byte) uint64, addr uint64) string { + hdr, err := x.ReadData(addr, uint64(2*ptrSize)) + if err != nil || len(hdr) < 2*ptrSize { + return "" + } + dataAddr := readPtr(hdr) + dataLen := readPtr(hdr[ptrSize:]) + data, err := x.ReadData(dataAddr, dataLen) + if err != nil || uint64(len(data)) < dataLen { + return "" + } + return string(data) +}