v3 candidate
This commit is contained in:
@@ -0,0 +1,412 @@
|
||||
package parsers
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"github.com/andydixon/nexus/internal/leviathan/document"
|
||||
)
|
||||
|
||||
// ExecutableRegion is a file-backed executable code range.
|
||||
type ExecutableRegion struct {
|
||||
Name string `json:"name"`
|
||||
Offset int64 `json:"offset"`
|
||||
Size int64 `json:"size"`
|
||||
Address uint64 `json:"address"`
|
||||
}
|
||||
|
||||
// FunctionSeed is a named function entry mapped to a file offset.
|
||||
type FunctionSeed struct {
|
||||
Name string `json:"name"`
|
||||
Offset int64 `json:"offset"`
|
||||
Address uint64 `json:"address"`
|
||||
Size uint64 `json:"size"`
|
||||
Source string `json:"source"`
|
||||
}
|
||||
|
||||
// ExecutableInfo contains disassembly-oriented metadata for an ELF or PE.
|
||||
type ExecutableInfo struct {
|
||||
Format string `json:"format"`
|
||||
Arch string `json:"arch"`
|
||||
Entry int64 `json:"entry"`
|
||||
Regions []ExecutableRegion `json:"regions"`
|
||||
Seeds []FunctionSeed `json:"seeds"`
|
||||
}
|
||||
|
||||
// InspectExecutable extracts executable ranges, entry point and named function
|
||||
// seeds without scraping the display-oriented parser tree.
|
||||
func InspectExecutable(d *document.Document) (*ExecutableInfo, error) {
|
||||
head, err := d.Range(0, min64(64, d.Size()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(head) >= 4 && string(head[:4]) == "\x7fELF" {
|
||||
return inspectELFExecutable(d, head)
|
||||
}
|
||||
if len(head) >= 2 && string(head[:2]) == "MZ" {
|
||||
return inspectPEExecutable(d)
|
||||
}
|
||||
return nil, errors.New("not a supported ELF or PE executable")
|
||||
}
|
||||
|
||||
func inspectELFExecutable(d *document.Document, hdr []byte) (*ExecutableInfo, error) {
|
||||
if len(hdr) < 52 || (hdr[4] != 1 && hdr[4] != 2) || (hdr[5] != 1 && hdr[5] != 2) {
|
||||
return nil, errors.New("invalid ELF header")
|
||||
}
|
||||
class, data := hdr[4], hdr[5]
|
||||
bo := elfByteOrder(data)
|
||||
var machine uint16
|
||||
var entry, phoff, shoff uint64
|
||||
var phentsize, phnum, shentsize, shnum uint16
|
||||
if class == 2 {
|
||||
if len(hdr) < 64 {
|
||||
return nil, errors.New("ELF64 header too short")
|
||||
}
|
||||
machine = bo.Uint16(hdr[18:20])
|
||||
entry = bo.Uint64(hdr[24:32])
|
||||
phoff = bo.Uint64(hdr[32:40])
|
||||
shoff = bo.Uint64(hdr[40:48])
|
||||
phentsize, phnum = bo.Uint16(hdr[54:56]), bo.Uint16(hdr[56:58])
|
||||
shentsize, shnum = bo.Uint16(hdr[58:60]), bo.Uint16(hdr[60:62])
|
||||
} else {
|
||||
machine = bo.Uint16(hdr[18:20])
|
||||
entry = uint64(bo.Uint32(hdr[24:28]))
|
||||
phoff = uint64(bo.Uint32(hdr[28:32]))
|
||||
shoff = uint64(bo.Uint32(hdr[32:36]))
|
||||
phentsize, phnum = bo.Uint16(hdr[42:44]), bo.Uint16(hdr[44:46])
|
||||
shentsize, shnum = bo.Uint16(hdr[46:48]), bo.Uint16(hdr[48:50])
|
||||
}
|
||||
sections, _ := executableELFSections(d, bo, class, shoff, shentsize, shnum)
|
||||
info := &ExecutableInfo{Format: "elf", Arch: elfDisasmArch(machine, data), Entry: -1}
|
||||
if info.Arch == "" {
|
||||
return nil, fmt.Errorf("unsupported ELF machine 0x%x", machine)
|
||||
}
|
||||
for i, section := range sections {
|
||||
if section.flags&0x4 != 0 && validFileRange(d, section.fileOff, section.secSize) {
|
||||
info.Regions = append(info.Regions, ExecutableRegion{
|
||||
Name: fmt.Sprintf("section[%d]", i), Offset: int64(section.fileOff),
|
||||
Size: int64(section.secSize), Address: section.addr,
|
||||
})
|
||||
}
|
||||
}
|
||||
if len(info.Regions) == 0 {
|
||||
info.Regions = executableELFSegments(d, bo, class, phoff, phentsize, phnum)
|
||||
}
|
||||
if off, ok := elfAddressToOffset(sections, entry); ok {
|
||||
info.Entry = off
|
||||
info.Seeds = append(info.Seeds, FunctionSeed{Name: "entry", Offset: off, Address: entry, Source: "header"})
|
||||
} else if off, ok := regionAddressToOffset(info.Regions, entry); ok {
|
||||
info.Entry = off
|
||||
info.Seeds = append(info.Seeds, FunctionSeed{Name: "entry", Offset: off, Address: entry, Source: "header"})
|
||||
}
|
||||
for _, symtab := range sections {
|
||||
if symtab.typ == 2 || symtab.typ == 11 {
|
||||
info.Seeds = append(info.Seeds, executableELFFunctions(d, bo, class, sections, symtab)...)
|
||||
}
|
||||
}
|
||||
finishExecutableInfo(info)
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func executableELFSegments(d *document.Document, bo binary.ByteOrder, class byte, off uint64, entrySize, count uint16) []ExecutableRegion {
|
||||
if off == 0 || entrySize == 0 || count == 0 || count > 8192 {
|
||||
return nil
|
||||
}
|
||||
regions := make([]ExecutableRegion, 0)
|
||||
for i := uint16(0); i < count; i++ {
|
||||
buf, err := d.Range(int64(off)+int64(i)*int64(entrySize), int64(entrySize))
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
var typ, flags uint32
|
||||
var fileOff, address, fileSize uint64
|
||||
if class == 2 {
|
||||
if len(buf) < 56 {
|
||||
break
|
||||
}
|
||||
typ, flags = bo.Uint32(buf[0:4]), bo.Uint32(buf[4:8])
|
||||
fileOff, address, fileSize = bo.Uint64(buf[8:16]), bo.Uint64(buf[16:24]), bo.Uint64(buf[32:40])
|
||||
} else {
|
||||
if len(buf) < 32 {
|
||||
break
|
||||
}
|
||||
typ, flags = bo.Uint32(buf[0:4]), bo.Uint32(buf[24:28])
|
||||
fileOff, address, fileSize = uint64(bo.Uint32(buf[4:8])), uint64(bo.Uint32(buf[8:12])), uint64(bo.Uint32(buf[16:20]))
|
||||
}
|
||||
if typ == 1 && flags&0x1 != 0 && validFileRange(d, fileOff, fileSize) {
|
||||
regions = append(regions, ExecutableRegion{
|
||||
Name: fmt.Sprintf("PT_LOAD[%d]", i), Offset: int64(fileOff), Size: int64(fileSize), Address: address,
|
||||
})
|
||||
}
|
||||
}
|
||||
return regions
|
||||
}
|
||||
|
||||
func executableELFSections(d *document.Document, bo binary.ByteOrder, class byte, off uint64, entrySize, count uint16) ([]elfSectionInfo, error) {
|
||||
if off == 0 || entrySize == 0 || count == 0 || count > 8192 {
|
||||
return nil, errors.New("ELF has no usable section table")
|
||||
}
|
||||
sections := make([]elfSectionInfo, 0, count)
|
||||
for i := uint16(0); i < count; i++ {
|
||||
buf, err := d.Range(int64(off)+int64(i)*int64(entrySize), int64(entrySize))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var section elfSectionInfo
|
||||
if class == 2 {
|
||||
if len(buf) < 64 {
|
||||
return nil, errors.New("truncated ELF64 section")
|
||||
}
|
||||
section.typ, section.flags = bo.Uint32(buf[4:8]), bo.Uint64(buf[8:16])
|
||||
section.addr, section.fileOff, section.secSize = bo.Uint64(buf[16:24]), bo.Uint64(buf[24:32]), bo.Uint64(buf[32:40])
|
||||
section.link, section.entrySize = bo.Uint32(buf[40:44]), bo.Uint64(buf[56:64])
|
||||
} else {
|
||||
if len(buf) < 40 {
|
||||
return nil, errors.New("truncated ELF32 section")
|
||||
}
|
||||
section.typ, section.flags = bo.Uint32(buf[4:8]), uint64(bo.Uint32(buf[8:12]))
|
||||
section.addr = uint64(bo.Uint32(buf[12:16]))
|
||||
section.fileOff, section.secSize = uint64(bo.Uint32(buf[16:20])), uint64(bo.Uint32(buf[20:24]))
|
||||
section.link, section.entrySize = bo.Uint32(buf[24:28]), uint64(bo.Uint32(buf[36:40]))
|
||||
}
|
||||
sections = append(sections, section)
|
||||
}
|
||||
return sections, nil
|
||||
}
|
||||
|
||||
func executableELFFunctions(d *document.Document, bo binary.ByteOrder, class byte, sections []elfSectionInfo, symtab elfSectionInfo) []FunctionSeed {
|
||||
entrySize := symtab.entrySize
|
||||
if entrySize == 0 {
|
||||
entrySize = 16
|
||||
if class == 2 {
|
||||
entrySize = 24
|
||||
}
|
||||
}
|
||||
if !validFileRange(d, symtab.fileOff, symtab.secSize) || int(symtab.link) >= len(sections) {
|
||||
return nil
|
||||
}
|
||||
strsec := sections[symtab.link]
|
||||
if !validFileRange(d, strsec.fileOff, strsec.secSize) || strsec.secSize > 8<<20 {
|
||||
return nil
|
||||
}
|
||||
strtab, err := d.Range(int64(strsec.fileOff), int64(strsec.secSize))
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
count := int(symtab.secSize / entrySize)
|
||||
if count > 16384 {
|
||||
count = 16384
|
||||
}
|
||||
out := make([]FunctionSeed, 0)
|
||||
for i := 0; i < count; i++ {
|
||||
buf, err := d.Range(int64(symtab.fileOff)+int64(i)*int64(entrySize), int64(entrySize))
|
||||
if err != nil || len(buf) < int(entrySize) {
|
||||
break
|
||||
}
|
||||
var nameOff uint32
|
||||
var info byte
|
||||
var sectionIndex uint16
|
||||
var value, size uint64
|
||||
if class == 2 {
|
||||
nameOff, info, sectionIndex = bo.Uint32(buf[0:4]), buf[4], bo.Uint16(buf[6:8])
|
||||
value, size = bo.Uint64(buf[8:16]), bo.Uint64(buf[16:24])
|
||||
} else {
|
||||
nameOff, value, size = bo.Uint32(buf[0:4]), uint64(bo.Uint32(buf[4:8])), uint64(bo.Uint32(buf[8:12]))
|
||||
info, sectionIndex = buf[12], bo.Uint16(buf[14:16])
|
||||
}
|
||||
if info&0xf != 2 || sectionIndex == 0 || value == 0 {
|
||||
continue
|
||||
}
|
||||
name := elfStringAt(strtab, nameOff)
|
||||
offset, ok := elfAddressToOffset(sections, value)
|
||||
if name == "" || !ok {
|
||||
continue
|
||||
}
|
||||
source := "symtab"
|
||||
if symtab.typ == 11 {
|
||||
source = "dynsym"
|
||||
}
|
||||
out = append(out, FunctionSeed{Name: name, Offset: offset, Address: value, Size: size, Source: source})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func inspectPEExecutable(d *document.Document) (*ExecutableInfo, error) {
|
||||
sections, dirs, is64, err := peLayout(d)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
hdr, _ := d.Range(0, 64)
|
||||
lfanew := int64(binary.LittleEndian.Uint32(hdr[0x3c:0x40]))
|
||||
peHdr, _ := d.Range(lfanew, 24)
|
||||
machine := binary.LittleEndian.Uint16(peHdr[4:6])
|
||||
optSize := int64(binary.LittleEndian.Uint16(peHdr[20:22]))
|
||||
opt, err := d.Range(lfanew+24, optSize)
|
||||
if err != nil || len(opt) < 20 {
|
||||
return nil, errors.New("PE optional header too short")
|
||||
}
|
||||
entryRVA := binary.LittleEndian.Uint32(opt[16:20])
|
||||
info := &ExecutableInfo{Format: "pe", Arch: peDisasmArch(machine, is64), Entry: -1}
|
||||
if info.Arch == "" {
|
||||
return nil, fmt.Errorf("unsupported PE machine 0x%x", machine)
|
||||
}
|
||||
for _, section := range sections {
|
||||
if section.Characteristics&0x20000000 != 0 && validFileRange(d, uint64(section.RawOffset), uint64(section.RawSize)) {
|
||||
info.Regions = append(info.Regions, ExecutableRegion{
|
||||
Name: section.Name, Offset: int64(section.RawOffset), Size: int64(section.RawSize), Address: uint64(section.VirtualAddress),
|
||||
})
|
||||
}
|
||||
}
|
||||
if off, ok := peRVAToOffset(sections, entryRVA); ok {
|
||||
info.Entry = int64(off)
|
||||
info.Seeds = append(info.Seeds, FunctionSeed{Name: "entry", Offset: int64(off), Address: uint64(entryRVA), Source: "header"})
|
||||
}
|
||||
if len(dirs) > 0 && dirs[0].RVA != 0 {
|
||||
info.Seeds = append(info.Seeds, executablePEExports(d, sections, dirs[0])...)
|
||||
}
|
||||
finishExecutableInfo(info)
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func executablePEExports(d *document.Document, sections []peSectionInfo, dir peDataDirectory) []FunctionSeed {
|
||||
off, ok := peRVAToOffset(sections, dir.RVA)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
buf, err := d.Range(int64(off), 40)
|
||||
if err != nil || len(buf) < 40 {
|
||||
return nil
|
||||
}
|
||||
functionCount, nameCount := binary.LittleEndian.Uint32(buf[20:24]), binary.LittleEndian.Uint32(buf[24:28])
|
||||
functionsOff, funcsOK := peRVAToOffset(sections, binary.LittleEndian.Uint32(buf[28:32]))
|
||||
namesOff, namesOK := peRVAToOffset(sections, binary.LittleEndian.Uint32(buf[32:36]))
|
||||
ordinalsOff, ordOK := peRVAToOffset(sections, binary.LittleEndian.Uint32(buf[36:40]))
|
||||
if !funcsOK || !namesOK || !ordOK {
|
||||
return nil
|
||||
}
|
||||
if nameCount > 16384 {
|
||||
nameCount = 16384
|
||||
}
|
||||
out := make([]FunctionSeed, 0, nameCount)
|
||||
for i := uint32(0); i < nameCount; i++ {
|
||||
namePtr, nameErr := d.Range(int64(namesOff)+int64(i)*4, 4)
|
||||
ordBuf, ordErr := d.Range(int64(ordinalsOff)+int64(i)*2, 2)
|
||||
if nameErr != nil || ordErr != nil || len(namePtr) < 4 || len(ordBuf) < 2 {
|
||||
break
|
||||
}
|
||||
index := uint32(binary.LittleEndian.Uint16(ordBuf))
|
||||
if index >= functionCount {
|
||||
continue
|
||||
}
|
||||
funcBuf, err := d.Range(int64(functionsOff)+int64(index)*4, 4)
|
||||
if err != nil || len(funcBuf) < 4 {
|
||||
break
|
||||
}
|
||||
funcRVA := binary.LittleEndian.Uint32(funcBuf)
|
||||
if funcRVA >= dir.RVA && funcRVA < dir.RVA+dir.Size {
|
||||
continue
|
||||
}
|
||||
nameOff, nameOK := peRVAToOffset(sections, binary.LittleEndian.Uint32(namePtr))
|
||||
funcOff, funcOK := peRVAToOffset(sections, funcRVA)
|
||||
if !nameOK || !funcOK {
|
||||
continue
|
||||
}
|
||||
name := readCString(d, int64(nameOff), 512)
|
||||
if name != "" {
|
||||
out = append(out, FunctionSeed{Name: name, Offset: int64(funcOff), Address: uint64(funcRVA), Source: "export"})
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func validFileRange(d *document.Document, offset, size uint64) bool {
|
||||
return size > 0 && offset <= uint64(d.Size()) && size <= uint64(d.Size())-offset
|
||||
}
|
||||
|
||||
func elfAddressToOffset(sections []elfSectionInfo, address uint64) (int64, bool) {
|
||||
for _, section := range sections {
|
||||
if section.secSize > 0 && address >= section.addr && address < section.addr+section.secSize {
|
||||
return int64(section.fileOff + address - section.addr), true
|
||||
}
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func regionAddressToOffset(regions []ExecutableRegion, address uint64) (int64, bool) {
|
||||
for _, region := range regions {
|
||||
if address >= region.Address && address < region.Address+uint64(region.Size) {
|
||||
return region.Offset + int64(address-region.Address), true
|
||||
}
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func elfDisasmArch(machine uint16, data byte) string {
|
||||
switch machine {
|
||||
case 3:
|
||||
return "x86"
|
||||
case 8:
|
||||
if data == 1 {
|
||||
return "mipsle"
|
||||
}
|
||||
return "mips"
|
||||
case 40:
|
||||
return "arm32"
|
||||
case 62:
|
||||
return "x86_64"
|
||||
case 183:
|
||||
return "aarch64"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func peDisasmArch(machine uint16, _ bool) string {
|
||||
switch machine {
|
||||
case 0x14c:
|
||||
return "x86"
|
||||
case 0x8664:
|
||||
return "x86_64"
|
||||
case 0x1c0, 0x1c4:
|
||||
return "arm32"
|
||||
case 0xaa64:
|
||||
return "aarch64"
|
||||
case 0x166, 0x266, 0x366, 0x466:
|
||||
return "mipsle"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func finishExecutableInfo(info *ExecutableInfo) {
|
||||
sort.Slice(info.Regions, func(i, j int) bool { return info.Regions[i].Offset < info.Regions[j].Offset })
|
||||
sort.Slice(info.Seeds, func(i, j int) bool {
|
||||
if info.Seeds[i].Offset != info.Seeds[j].Offset {
|
||||
return info.Seeds[i].Offset < info.Seeds[j].Offset
|
||||
}
|
||||
return info.Seeds[i].Name < info.Seeds[j].Name
|
||||
})
|
||||
unique := info.Seeds[:0]
|
||||
for _, seed := range info.Seeds {
|
||||
if seed.Offset < 0 || !offsetInRegions(info.Regions, seed.Offset) {
|
||||
continue
|
||||
}
|
||||
if len(unique) > 0 && unique[len(unique)-1].Offset == seed.Offset {
|
||||
if unique[len(unique)-1].Name == "entry" && seed.Name != "entry" {
|
||||
unique[len(unique)-1] = seed
|
||||
}
|
||||
continue
|
||||
}
|
||||
unique = append(unique, seed)
|
||||
}
|
||||
info.Seeds = unique
|
||||
}
|
||||
|
||||
func offsetInRegions(regions []ExecutableRegion, offset int64) bool {
|
||||
for _, region := range regions {
|
||||
if offset >= region.Offset && offset < region.Offset+region.Size {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
Reference in New Issue
Block a user