409 lines
11 KiB
Go
409 lines
11 KiB
Go
package parsers
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"github.com/andydixon/nexus/internal/leviathan/document"
|
|
)
|
|
|
|
// Mach-O magic values (32/64, LE/BE) plus fat (universal) magic.
|
|
const (
|
|
machoMagic32 = 0xfeedface
|
|
machoCigam32 = 0xcefaedfe
|
|
machoMagic64 = 0xfeedfacf
|
|
machoCigam64 = 0xcffaedfe
|
|
machoFatMagic = 0xcafebabe // also Java .class; disambiguate by position + nfat_arch
|
|
machoFatCigam = 0xbebafeca
|
|
machoFatMagic64 = 0xcafebabf
|
|
machoFatCigam64 = 0xbfbafeca
|
|
)
|
|
|
|
// IsMachO reports whether the given prefix looks like a Mach-O header.
|
|
// Fat magic 0xcafebabe is shared with Java class files; we accept it
|
|
// only when nfat_arch is plausible and the first arch's cputype points
|
|
// somewhere in-document.
|
|
func IsMachO(head []byte, size int64) bool {
|
|
if len(head) < 8 {
|
|
return false
|
|
}
|
|
m := binary.LittleEndian.Uint32(head[:4])
|
|
switch m {
|
|
case machoMagic32, machoMagic64, machoCigam32, machoCigam64:
|
|
return true
|
|
}
|
|
mBE := binary.BigEndian.Uint32(head[:4])
|
|
switch mBE {
|
|
case machoFatMagic, machoFatMagic64:
|
|
// nfat_arch is next BE u32; plausibly small.
|
|
n := binary.BigEndian.Uint32(head[4:8])
|
|
return n > 0 && n < 64 && size >= 8+int64(n)*20
|
|
}
|
|
_ = mBE
|
|
return false
|
|
}
|
|
|
|
// ParseMachO returns a field tree for a Mach-O (fat or thin) file.
|
|
func ParseMachO(d *document.Document) (*Field, error) {
|
|
head, err := d.Range(0, min64(8, d.Size()))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(head) < 8 {
|
|
return nil, errors.New("file too small for Mach-O")
|
|
}
|
|
mLE := binary.LittleEndian.Uint32(head[:4])
|
|
mBE := binary.BigEndian.Uint32(head[:4])
|
|
|
|
switch mBE {
|
|
case machoFatMagic, machoFatMagic64:
|
|
return parseMachOFat(d, mBE == machoFatMagic64)
|
|
}
|
|
|
|
// Thin header.
|
|
switch mLE {
|
|
case machoMagic32:
|
|
return parseMachOThin(d, 0, binary.LittleEndian, false)
|
|
case machoMagic64:
|
|
return parseMachOThin(d, 0, binary.LittleEndian, true)
|
|
case machoCigam32:
|
|
return parseMachOThin(d, 0, binary.BigEndian, false)
|
|
case machoCigam64:
|
|
return parseMachOThin(d, 0, binary.BigEndian, true)
|
|
}
|
|
return nil, errors.New("not a Mach-O file")
|
|
}
|
|
|
|
func parseMachOFat(d *document.Document, is64 bool) (*Field, error) {
|
|
root := &Field{Name: "Mach-O fat", Type: "container", Offset: 0}
|
|
hdr, err := d.Range(0, 8)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
n := int64(binary.BigEndian.Uint32(hdr[4:8]))
|
|
root.Children = append(root.Children, &Field{
|
|
Name: "magic", Type: "u32", Offset: 0, Length: 4,
|
|
Value: fmt.Sprintf("0x%08x", binary.BigEndian.Uint32(hdr[:4])),
|
|
})
|
|
root.Children = append(root.Children, &Field{
|
|
Name: "nfat_arch", Type: "u32", Offset: 4, Length: 4,
|
|
Value: fmt.Sprintf("%d", n),
|
|
})
|
|
|
|
archSize := int64(20)
|
|
if is64 {
|
|
archSize = 32
|
|
}
|
|
off := int64(8)
|
|
for i := int64(0); i < n && off+archSize <= d.Size(); i++ {
|
|
arch, err := d.Range(off, archSize)
|
|
if err != nil {
|
|
break
|
|
}
|
|
cpuType := binary.BigEndian.Uint32(arch[0:4])
|
|
cpuSub := binary.BigEndian.Uint32(arch[4:8])
|
|
var archOff, archLen int64
|
|
if is64 {
|
|
archOff = int64(binary.BigEndian.Uint64(arch[8:16]))
|
|
archLen = int64(binary.BigEndian.Uint64(arch[16:24]))
|
|
} else {
|
|
archOff = int64(binary.BigEndian.Uint32(arch[8:12]))
|
|
archLen = int64(binary.BigEndian.Uint32(arch[12:16]))
|
|
}
|
|
node := &Field{
|
|
Name: fmt.Sprintf("arch[%d]", i),
|
|
Type: "fat_arch",
|
|
Offset: off,
|
|
Length: archSize,
|
|
Value: fmt.Sprintf("cpu=%s sub=0x%x offset=%d size=%d", cpuTypeName(cpuType), cpuSub, archOff, archLen),
|
|
}
|
|
// Parse the nested Mach-O at archOff if it fits.
|
|
if archOff > 0 && archOff+8 <= d.Size() {
|
|
if nested, err := parseMachOInline(d, archOff); err == nil {
|
|
node.Children = append(node.Children, nested)
|
|
}
|
|
}
|
|
root.Children = append(root.Children, node)
|
|
off += archSize
|
|
}
|
|
return root, nil
|
|
}
|
|
|
|
// parseMachOInline is like ParseMachO but anchored at a given offset
|
|
// (used for each arch inside a fat binary).
|
|
func parseMachOInline(d *document.Document, at int64) (*Field, error) {
|
|
head, err := d.Range(at, 8)
|
|
if err != nil || len(head) < 8 {
|
|
return nil, errors.New("short arch payload")
|
|
}
|
|
mLE := binary.LittleEndian.Uint32(head[:4])
|
|
switch mLE {
|
|
case machoMagic32:
|
|
return parseMachOThin(d, at, binary.LittleEndian, false)
|
|
case machoMagic64:
|
|
return parseMachOThin(d, at, binary.LittleEndian, true)
|
|
case machoCigam32:
|
|
return parseMachOThin(d, at, binary.BigEndian, false)
|
|
case machoCigam64:
|
|
return parseMachOThin(d, at, binary.BigEndian, true)
|
|
}
|
|
return nil, errors.New("arch payload is not Mach-O")
|
|
}
|
|
|
|
func parseMachOThin(d *document.Document, base int64, bo binary.ByteOrder, is64 bool) (*Field, error) {
|
|
headerLen := int64(28)
|
|
if is64 {
|
|
headerLen = 32
|
|
}
|
|
if base+headerLen > d.Size() {
|
|
return nil, errors.New("truncated Mach-O header")
|
|
}
|
|
hdr, err := d.Range(base, headerLen)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
magic := bo.Uint32(hdr[0:4])
|
|
cpuType := bo.Uint32(hdr[4:8])
|
|
cpuSub := bo.Uint32(hdr[8:12])
|
|
fileType := bo.Uint32(hdr[12:16])
|
|
ncmds := bo.Uint32(hdr[16:20])
|
|
sizeOfCmds := bo.Uint32(hdr[20:24])
|
|
flags := bo.Uint32(hdr[24:28])
|
|
|
|
name := "Mach-O (32)"
|
|
if is64 {
|
|
name = "Mach-O (64)"
|
|
}
|
|
root := &Field{Name: name, Type: "container", Offset: base, Length: headerLen + int64(sizeOfCmds)}
|
|
root.Children = append(root.Children,
|
|
&Field{Name: "magic", Type: "u32", Offset: base, Length: 4, Value: fmt.Sprintf("0x%08x", magic)},
|
|
&Field{Name: "cputype", Type: "u32", Offset: base + 4, Length: 4, Value: cpuTypeName(cpuType)},
|
|
&Field{Name: "cpusubtype", Type: "u32", Offset: base + 8, Length: 4, Value: fmt.Sprintf("0x%x", cpuSub)},
|
|
&Field{Name: "filetype", Type: "u32", Offset: base + 12, Length: 4, Value: machoFileType(fileType)},
|
|
&Field{Name: "ncmds", Type: "u32", Offset: base + 16, Length: 4, Value: fmt.Sprintf("%d", ncmds)},
|
|
&Field{Name: "sizeofcmds", Type: "u32", Offset: base + 20, Length: 4, Value: fmt.Sprintf("%d", sizeOfCmds)},
|
|
&Field{Name: "flags", Type: "u32", Offset: base + 24, Length: 4, Value: fmt.Sprintf("0x%08x", flags)},
|
|
)
|
|
|
|
// Load commands.
|
|
cmdOff := base + headerLen
|
|
cmdEnd := cmdOff + int64(sizeOfCmds)
|
|
if cmdEnd > d.Size() {
|
|
cmdEnd = d.Size()
|
|
}
|
|
cmds := &Field{Name: "load_commands", Type: "list", Offset: cmdOff, Length: int64(sizeOfCmds)}
|
|
for i := uint32(0); i < ncmds && cmdOff+8 <= cmdEnd; i++ {
|
|
h, err := d.Range(cmdOff, 8)
|
|
if err != nil {
|
|
break
|
|
}
|
|
cmd := bo.Uint32(h[0:4])
|
|
sz := int64(bo.Uint32(h[4:8]))
|
|
if sz < 8 || cmdOff+sz > cmdEnd {
|
|
break
|
|
}
|
|
buf, _ := d.Range(cmdOff, sz)
|
|
node := decodeLoadCommand(base, cmdOff, cmd, sz, buf, bo, is64)
|
|
cmds.Children = append(cmds.Children, node)
|
|
cmdOff += sz
|
|
}
|
|
root.Children = append(root.Children, cmds)
|
|
return root, nil
|
|
}
|
|
|
|
func decodeLoadCommand(base, off int64, cmd uint32, sz int64, buf []byte, bo binary.ByteOrder, is64 bool) *Field {
|
|
name, mask := loadCommandName(cmd)
|
|
node := &Field{
|
|
Name: name, Type: "load_command", Offset: off, Length: sz,
|
|
Value: fmt.Sprintf("cmd=0x%08x size=%d", cmd, sz),
|
|
}
|
|
if mask != 0 {
|
|
node.Note = "required"
|
|
}
|
|
|
|
switch {
|
|
case (cmd&^0x80000000) == 0x19 && is64 && sz >= 72:
|
|
// LC_SEGMENT_64
|
|
segName := cstring(buf[8:24])
|
|
vmAddr := bo.Uint64(buf[24:32])
|
|
vmSize := bo.Uint64(buf[32:40])
|
|
fileOff := bo.Uint64(buf[40:48])
|
|
fileSize := bo.Uint64(buf[48:56])
|
|
nsects := bo.Uint32(buf[64:68])
|
|
node.Value = fmt.Sprintf("name=%q vm=%#x+%#x file=%#x+%#x nsects=%d",
|
|
segName, vmAddr, vmSize, fileOff, fileSize, nsects)
|
|
sectOff := int64(72)
|
|
for i := uint32(0); i < nsects && sectOff+80 <= sz; i++ {
|
|
s := buf[sectOff:]
|
|
sn := cstring(s[0:16])
|
|
seg := cstring(s[16:32])
|
|
addr := bo.Uint64(s[32:40])
|
|
size := bo.Uint64(s[40:48])
|
|
foff := bo.Uint32(s[48:52])
|
|
node.Children = append(node.Children, &Field{
|
|
Name: sn, Type: "section64", Offset: off + sectOff, Length: 80,
|
|
Value: fmt.Sprintf("seg=%q addr=%#x size=%#x file=%#x", seg, addr, size, foff),
|
|
})
|
|
sectOff += 80
|
|
}
|
|
case cmd == 0x01 && sz >= 56:
|
|
// LC_SEGMENT (32)
|
|
segName := cstring(buf[8:24])
|
|
vmAddr := bo.Uint32(buf[24:28])
|
|
vmSize := bo.Uint32(buf[28:32])
|
|
fileOff := bo.Uint32(buf[32:36])
|
|
fileSize := bo.Uint32(buf[36:40])
|
|
nsects := bo.Uint32(buf[48:52])
|
|
node.Value = fmt.Sprintf("name=%q vm=%#x+%#x file=%#x+%#x nsects=%d",
|
|
segName, vmAddr, vmSize, fileOff, fileSize, nsects)
|
|
case cmd == 0x1b && sz >= 24:
|
|
// LC_UUID
|
|
u := buf[8:24]
|
|
node.Value = fmt.Sprintf("UUID=%x-%x-%x-%x-%x", u[0:4], u[4:6], u[6:8], u[8:10], u[10:16])
|
|
case (cmd&^0x80000000) == 0x0c && sz > 24:
|
|
// LC_LOAD_DYLIB / similar
|
|
strOff := bo.Uint32(buf[8:12])
|
|
if int(strOff) < len(buf) {
|
|
node.Value = fmt.Sprintf("lib=%q", cstring(buf[strOff:]))
|
|
}
|
|
case cmd == 0x0e && sz > 12:
|
|
// LC_LOAD_DYLINKER
|
|
strOff := bo.Uint32(buf[8:12])
|
|
if int(strOff) < len(buf) {
|
|
node.Value = fmt.Sprintf("dylinker=%q", cstring(buf[strOff:]))
|
|
}
|
|
case cmd == 0x21 && sz >= 16:
|
|
// LC_ENCRYPTION_INFO / _64
|
|
node.Value = "encryption info"
|
|
case (cmd &^ 0x80000000) == 0x22:
|
|
node.Value = "LC_DYLD_INFO"
|
|
case cmd == 0x25:
|
|
node.Value = "LC_VERSION_MIN_MACOSX"
|
|
case cmd == 0x28 || cmd == 0x29:
|
|
node.Value = "LC_MAIN / entry point"
|
|
}
|
|
return node
|
|
}
|
|
|
|
func cstring(b []byte) string {
|
|
for i, c := range b {
|
|
if c == 0 {
|
|
return string(b[:i])
|
|
}
|
|
}
|
|
return string(b)
|
|
}
|
|
|
|
func cpuTypeName(c uint32) string {
|
|
switch c {
|
|
case 0x7:
|
|
return "x86"
|
|
case 0x1000007:
|
|
return "x86_64"
|
|
case 0xc:
|
|
return "arm"
|
|
case 0x100000c:
|
|
return "arm64"
|
|
case 0x12:
|
|
return "ppc"
|
|
case 0x1000012:
|
|
return "ppc64"
|
|
}
|
|
return fmt.Sprintf("0x%x", c)
|
|
}
|
|
|
|
func machoFileType(t uint32) string {
|
|
switch t {
|
|
case 1:
|
|
return "OBJECT"
|
|
case 2:
|
|
return "EXECUTE"
|
|
case 3:
|
|
return "FVMLIB"
|
|
case 4:
|
|
return "CORE"
|
|
case 5:
|
|
return "PRELOAD"
|
|
case 6:
|
|
return "DYLIB"
|
|
case 7:
|
|
return "DYLINKER"
|
|
case 8:
|
|
return "BUNDLE"
|
|
case 9:
|
|
return "DYLIB_STUB"
|
|
case 10:
|
|
return "DSYM"
|
|
case 11:
|
|
return "KEXT_BUNDLE"
|
|
}
|
|
return fmt.Sprintf("%d", t)
|
|
}
|
|
|
|
// loadCommandName returns a human name and whether the required bit is set.
|
|
func loadCommandName(cmd uint32) (string, uint32) {
|
|
req := cmd & 0x80000000
|
|
v := cmd &^ 0x80000000
|
|
names := map[uint32]string{
|
|
0x01: "LC_SEGMENT",
|
|
0x02: "LC_SYMTAB",
|
|
0x03: "LC_SYMSEG",
|
|
0x04: "LC_THREAD",
|
|
0x05: "LC_UNIXTHREAD",
|
|
0x06: "LC_LOADFVMLIB",
|
|
0x07: "LC_IDFVMLIB",
|
|
0x08: "LC_IDENT",
|
|
0x09: "LC_FVMFILE",
|
|
0x0a: "LC_PREPAGE",
|
|
0x0b: "LC_DYSYMTAB",
|
|
0x0c: "LC_LOAD_DYLIB",
|
|
0x0d: "LC_ID_DYLIB",
|
|
0x0e: "LC_LOAD_DYLINKER",
|
|
0x0f: "LC_ID_DYLINKER",
|
|
0x10: "LC_PREBOUND_DYLIB",
|
|
0x11: "LC_ROUTINES",
|
|
0x12: "LC_SUB_FRAMEWORK",
|
|
0x13: "LC_SUB_UMBRELLA",
|
|
0x14: "LC_SUB_CLIENT",
|
|
0x15: "LC_SUB_LIBRARY",
|
|
0x16: "LC_TWOLEVEL_HINTS",
|
|
0x17: "LC_PREBIND_CKSUM",
|
|
0x18: "LC_LOAD_WEAK_DYLIB",
|
|
0x19: "LC_SEGMENT_64",
|
|
0x1a: "LC_ROUTINES_64",
|
|
0x1b: "LC_UUID",
|
|
0x1c: "LC_RPATH",
|
|
0x1d: "LC_CODE_SIGNATURE",
|
|
0x1e: "LC_SEGMENT_SPLIT_INFO",
|
|
0x1f: "LC_REEXPORT_DYLIB",
|
|
0x20: "LC_LAZY_LOAD_DYLIB",
|
|
0x21: "LC_ENCRYPTION_INFO",
|
|
0x22: "LC_DYLD_INFO",
|
|
0x23: "LC_LOAD_UPWARD_DYLIB",
|
|
0x24: "LC_VERSION_MIN_MACOSX",
|
|
0x25: "LC_VERSION_MIN_IPHONEOS",
|
|
0x26: "LC_FUNCTION_STARTS",
|
|
0x27: "LC_DYLD_ENVIRONMENT",
|
|
0x28: "LC_MAIN",
|
|
0x29: "LC_DATA_IN_CODE",
|
|
0x2a: "LC_SOURCE_VERSION",
|
|
0x2b: "LC_DYLIB_CODE_SIGN_DRS",
|
|
0x2c: "LC_ENCRYPTION_INFO_64",
|
|
0x2d: "LC_LINKER_OPTION",
|
|
0x2e: "LC_LINKER_OPTIMIZATION_HINT",
|
|
0x2f: "LC_VERSION_MIN_TVOS",
|
|
0x30: "LC_VERSION_MIN_WATCHOS",
|
|
0x31: "LC_NOTE",
|
|
0x32: "LC_BUILD_VERSION",
|
|
0x33: "LC_DYLD_EXPORTS_TRIE",
|
|
0x34: "LC_DYLD_CHAINED_FIXUPS",
|
|
0x35: "LC_FILESET_ENTRY",
|
|
}
|
|
if n, ok := names[v]; ok {
|
|
return n, req
|
|
}
|
|
return fmt.Sprintf("LC_0x%x", v), req
|
|
}
|