v3 candidate

This commit is contained in:
Andy Dixon
2026-06-16 11:51:07 +01:00
parent fcb19c96c8
commit 493bfed599
91 changed files with 28824 additions and 80 deletions
+412
View File
@@ -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
}