Files
nexus/internal/leviathan/parsers/peimports.go
T
2026-06-16 11:51:07 +01:00

151 lines
4.3 KiB
Go

package parsers
import (
"encoding/binary"
"errors"
"github.com/andydixon/nexus/internal/leviathan/document"
)
// ImportSym is one resolved PE import (a function name, or an ordinal).
type ImportSym struct {
DLL string `json:"dll"`
Func string `json:"func"`
Ordinal int `json:"ordinal"`
ByOrdinal bool `json:"byOrdinal"`
}
// PEImports walks the import directory of a PE file and returns the ordered
// list of imported symbols. It reuses the same RVA mapping and header parsing
// as ParsePE but yields a clean typed result (for imphash, capability
// analysis, etc.) rather than a display tree.
func PEImports(d *document.Document) ([]ImportSym, error) {
sections, dataDirs, is64, err := peLayout(d)
if err != nil {
return nil, err
}
if len(dataDirs) < 2 || dataDirs[1].RVA == 0 {
return nil, errors.New("no import directory")
}
off, ok := peRVAToOffset(sections, dataDirs[1].RVA)
if !ok {
return nil, errors.New("import RVA unmapped")
}
var out []ImportSym
for i := 0; i < 1024; i++ {
at := int64(off) + int64(i*20)
buf, err := d.Range(at, 20)
if err != nil || len(buf) < 20 {
break
}
oft := binary.LittleEndian.Uint32(buf[0:4])
nameRVA := binary.LittleEndian.Uint32(buf[12:16])
firstThunk := binary.LittleEndian.Uint32(buf[16:20])
if oft == 0 && nameRVA == 0 && firstThunk == 0 {
break
}
dll := ""
if nameOff, ok := peRVAToOffset(sections, nameRVA); ok {
dll = readCString(d, int64(nameOff), 256)
}
if dll == "" {
continue
}
out = append(out, peThunkSyms(d, sections, dll, oft, firstThunk, is64)...)
if len(out) > 100000 {
break
}
}
return out, nil
}
func peThunkSyms(d *document.Document, sections []peSectionInfo, dll string, oft, firstThunk uint32, is64 bool) []ImportSym {
thunkRVA := oft
if thunkRVA == 0 {
thunkRVA = firstThunk
}
thunkOff, ok := peRVAToOffset(sections, thunkRVA)
if !ok {
return nil
}
width := int64(4)
ordinalMask := uint64(0x80000000)
if is64 {
width = 8
ordinalMask = 0x8000000000000000
}
var out []ImportSym
for i := 0; i < 8192; i++ {
at := int64(thunkOff) + int64(i)*width
buf, err := d.Range(at, width)
if err != nil || int64(len(buf)) < width {
break
}
var thunk uint64
if is64 {
thunk = binary.LittleEndian.Uint64(buf)
} else {
thunk = uint64(binary.LittleEndian.Uint32(buf))
}
if thunk == 0 {
break
}
if thunk&ordinalMask != 0 {
out = append(out, ImportSym{DLL: dll, Ordinal: int(thunk & 0xffff), ByOrdinal: true})
} else if nameOff, ok := peRVAToOffset(sections, uint32(thunk)); ok {
name := readImportByName(d, int64(nameOff))
if name != "" {
out = append(out, ImportSym{DLL: dll, Func: name})
}
}
}
return out
}
// peLayout extracts the section table, data directories and 32/64-bitness from
// a PE without building the display field tree.
func peLayout(d *document.Document) ([]peSectionInfo, []peDataDirectory, bool, error) {
hdr, err := d.Range(0, 64)
if err != nil || len(hdr) < 64 {
return nil, nil, false, errors.New("too short")
}
lfanew := int64(binary.LittleEndian.Uint32(hdr[0x3c:0x40]))
peHdr, err := d.Range(lfanew, 24)
if err != nil || len(peHdr) < 24 || string(peHdr[:4]) != "PE\x00\x00" {
return nil, nil, false, errors.New("no PE signature")
}
numSections := int(binary.LittleEndian.Uint16(peHdr[6:8]))
sizeOptHdr := int64(binary.LittleEndian.Uint16(peHdr[20:22]))
optOff := lfanew + 24
var dataDirs []peDataDirectory
is64 := false
if sizeOptHdr > 0 {
opt, err := d.Range(optOff, sizeOptHdr)
if err == nil && len(opt) >= 2 {
_, dirs := parsePEOptionalHeader(opt, optOff)
dataDirs = dirs
is64 = binary.LittleEndian.Uint16(opt[0:2]) == 0x20b
}
}
sectOff := lfanew + 24 + sizeOptHdr
sections := make([]peSectionInfo, 0, numSections)
for i := 0; i < numSections; i++ {
sh, err := d.Range(sectOff, 40)
if err != nil || len(sh) < 40 {
break
}
sections = append(sections, peSectionInfo{
Name: string(trimZero(sh[0:8])),
VirtualSize: binary.LittleEndian.Uint32(sh[8:12]),
VirtualAddress: binary.LittleEndian.Uint32(sh[12:16]),
RawSize: binary.LittleEndian.Uint32(sh[16:20]),
RawOffset: binary.LittleEndian.Uint32(sh[20:24]),
Characteristics: binary.LittleEndian.Uint32(sh[36:40]),
})
sectOff += 40
}
return sections, dataDirs, is64, nil
}