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
+321
View File
@@ -0,0 +1,321 @@
package parsers
import (
"encoding/asn1"
"encoding/pem"
"errors"
"fmt"
"strings"
"github.com/andydixon/nexus/internal/leviathan/document"
)
// ParseASN1 produces a field tree for the DER TLV structure starting
// at `at`, spanning at most `length` bytes. Nested SEQUENCE / SET
// structures are decoded recursively; primitive content is rendered
// as best-effort.
func ParseASN1(d *document.Document, at, length int64) (*Field, error) {
size := d.Size()
if length < 0 || at+length > size {
length = size - at
}
if length <= 0 {
return nil, errors.New("asn1: empty range")
}
buf, err := d.Range(at, length)
if err != nil {
return nil, err
}
// If the buffer looks like PEM, decode first.
if pemBlock, _ := pem.Decode(buf); pemBlock != nil {
root := &Field{Name: "PEM " + pemBlock.Type, Type: "container", Offset: at, Length: length}
inner, err := decodeASN1(pemBlock.Bytes, at)
if err == nil {
root.Children = append(root.Children, inner)
} else {
root.Note = "pem decoded but asn1 failed: " + err.Error()
}
return root, nil
}
return decodeASN1(buf, at)
}
// decodeASN1 walks buf recursively, treating it as one or more DER TLVs.
func decodeASN1(buf []byte, base int64) (*Field, error) {
root := &Field{Name: "ASN.1", Type: "container", Offset: base, Length: int64(len(buf))}
off := 0
for off < len(buf) {
raw, consumed, err := readTLV(buf[off:])
if err != nil {
if off == 0 {
return nil, err
}
break
}
node := asn1Node(raw, base+int64(off))
root.Children = append(root.Children, node)
off += consumed
}
if len(root.Children) == 0 {
return nil, errors.New("asn1: no TLV decoded")
}
if len(root.Children) == 1 {
return root.Children[0], nil
}
return root, nil
}
// tlv is one DER TLV we've split out of the byte stream.
type tlv struct {
Class int // 0=universal, 1=application, 2=context, 3=private
Constructed bool
Tag int
Content []byte // the V of the TLV
HeaderLen int // length of T+L bytes
}
func readTLV(buf []byte) (tlv, int, error) {
if len(buf) < 2 {
return tlv{}, 0, errors.New("short TLV")
}
b0 := buf[0]
t := tlv{
Class: int(b0 >> 6),
Constructed: b0&0x20 != 0,
}
tagVal := int(b0 & 0x1f)
i := 1
if tagVal == 0x1f {
// multi-byte tag
tagVal = 0
for i < len(buf) {
c := buf[i]
i++
tagVal = (tagVal << 7) | int(c&0x7f)
if c&0x80 == 0 {
break
}
}
}
t.Tag = tagVal
if i >= len(buf) {
return tlv{}, 0, errors.New("truncated length")
}
ln := int(buf[i])
i++
if ln >= 0x80 {
n := ln & 0x7f
if n == 0 || n > 8 || i+n > len(buf) {
return tlv{}, 0, errors.New("bad length")
}
ln = 0
for k := 0; k < n; k++ {
ln = (ln << 8) | int(buf[i+k])
}
i += n
}
if i+ln > len(buf) {
return tlv{}, 0, errors.New("length exceeds buffer")
}
t.HeaderLen = i
t.Content = buf[i : i+ln]
return t, i + ln, nil
}
func asn1Node(t tlv, off int64) *Field {
name, kind := asn1TagName(t)
total := int64(t.HeaderLen + len(t.Content))
node := &Field{Name: name, Type: kind, Offset: off, Length: total}
if t.Class == 2 {
node.Name = fmt.Sprintf("[%d]", t.Tag)
}
if t.Constructed {
// Recurse into children.
inner := t.Content
childOff := off + int64(t.HeaderLen)
i := 0
for i < len(inner) {
child, consumed, err := readTLV(inner[i:])
if err != nil {
break
}
node.Children = append(node.Children, asn1Node(child, childOff+int64(i)))
i += consumed
}
} else {
node.Value = renderPrimitive(t)
}
return node
}
func asn1TagName(t tlv) (string, string) {
if t.Class != 0 {
prefix := []string{"", "APP ", "CTX ", "PRIV "}[t.Class]
return fmt.Sprintf("%s%d", prefix, t.Tag), "tag"
}
names := map[int]string{
1: "BOOLEAN",
2: "INTEGER",
3: "BIT STRING",
4: "OCTET STRING",
5: "NULL",
6: "OBJECT IDENTIFIER",
10: "ENUMERATED",
12: "UTF8String",
16: "SEQUENCE",
17: "SET",
19: "PrintableString",
20: "T61String",
22: "IA5String",
23: "UTCTime",
24: "GeneralizedTime",
}
if n, ok := names[t.Tag]; ok {
return n, strings.ToLower(n)
}
return fmt.Sprintf("tag %d", t.Tag), "tag"
}
func renderPrimitive(t tlv) string {
switch t.Tag {
case 1:
if len(t.Content) == 1 {
if t.Content[0] == 0 {
return "false"
}
return "true"
}
case 2:
return fmt.Sprintf("%s (%d bytes)", bigIntString(t.Content), len(t.Content))
case 3:
if len(t.Content) == 0 {
return "empty"
}
return fmt.Sprintf("unused=%d bits=%x", t.Content[0], t.Content[1:])
case 4:
if isPrintable(t.Content) {
return fmt.Sprintf("%q (%d bytes)", string(t.Content), len(t.Content))
}
return fmt.Sprintf("%x (%d bytes)", t.Content, len(t.Content))
case 5:
return "null"
case 6:
oid, err := parseOID(t.Content)
if err != nil {
return err.Error()
}
return oid + oidNote(oid)
case 12, 19, 20, 22:
return fmt.Sprintf("%q", string(t.Content))
case 23, 24:
return fmt.Sprintf("%q (time)", string(t.Content))
}
if isPrintable(t.Content) {
return fmt.Sprintf("%q", string(t.Content))
}
if len(t.Content) > 32 {
return fmt.Sprintf("%x… (%d bytes)", t.Content[:32], len(t.Content))
}
return fmt.Sprintf("%x", t.Content)
}
func bigIntString(b []byte) string {
if len(b) == 0 {
return "0"
}
if len(b) <= 8 {
var v int64
negative := b[0]&0x80 != 0
for _, c := range b {
v = (v << 8) | int64(c)
}
if negative {
// sign-extend
v -= 1 << uint(8*len(b))
}
return fmt.Sprintf("%d", v)
}
// large — hex is more useful than a decimal approximation.
return fmt.Sprintf("0x%x", b)
}
func parseOID(b []byte) (string, error) {
// Use Go's asn1 parser for correctness.
var oid asn1.ObjectIdentifier
rest, err := asn1.Unmarshal(append([]byte{0x06, byte(len(b))}, b...), &oid)
if err != nil || len(rest) != 0 {
// Fallback manual decode.
return manualOID(b), nil
}
return oid.String(), nil
}
func manualOID(b []byte) string {
if len(b) == 0 {
return ""
}
parts := []string{}
first := int(b[0])
parts = append(parts, fmt.Sprintf("%d.%d", first/40, first%40))
v := 0
for _, c := range b[1:] {
v = (v << 7) | int(c&0x7f)
if c&0x80 == 0 {
parts = append(parts, fmt.Sprintf("%d", v))
v = 0
}
}
return strings.Join(parts, ".")
}
// Known-OID table for human-readable hints in cert dumps.
var oidNames = map[string]string{
"1.2.840.113549.1.1.1": " rsaEncryption",
"1.2.840.113549.1.1.11": " sha256WithRSAEncryption",
"1.2.840.113549.1.1.5": " sha1WithRSAEncryption",
"1.2.840.113549.1.1.13": " sha512WithRSAEncryption",
"1.2.840.10045.2.1": " ecPublicKey",
"1.2.840.10045.4.3.2": " ecdsaWithSHA256",
"1.3.101.112": " Ed25519",
"2.5.4.3": " commonName",
"2.5.4.6": " countryName",
"2.5.4.7": " localityName",
"2.5.4.8": " stateOrProvinceName",
"2.5.4.10": " organizationName",
"2.5.4.11": " organizationalUnitName",
"2.5.4.5": " serialNumber",
"1.2.840.113549.1.9.1": " emailAddress",
"2.5.29.14": " subjectKeyIdentifier",
"2.5.29.15": " keyUsage",
"2.5.29.17": " subjectAltName",
"2.5.29.19": " basicConstraints",
"2.5.29.31": " cRLDistributionPoints",
"2.5.29.32": " certificatePolicies",
"2.5.29.35": " authorityKeyIdentifier",
"2.5.29.37": " extKeyUsage",
"1.3.6.1.5.5.7.3.1": " serverAuth",
"1.3.6.1.5.5.7.3.2": " clientAuth",
"1.3.6.1.5.5.7.1.1": " authorityInfoAccess",
}
func oidNote(oid string) string {
if n, ok := oidNames[oid]; ok {
return n
}
return ""
}
func isPrintable(b []byte) bool {
if len(b) == 0 {
return false
}
for _, c := range b {
if c == 0x09 || c == 0x0a || c == 0x0d {
continue
}
if c < 0x20 || c > 0x7e {
return false
}
}
return true
}
+196
View File
@@ -0,0 +1,196 @@
package parsers
import (
"crypto/sha256"
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"strings"
"github.com/andydixon/nexus/internal/leviathan/document"
)
// ParseCerts scans the whole document for PEM-encoded certificates
// and for DER-encoded X.509 certificates appearing at arbitrary
// offsets (heuristic: SEQUENCE 0x30 0x82 <len>, then tbsCertificate
// SEQUENCE, then version INTEGER). Returns a list of decoded certs.
func ParseCerts(d *document.Document) (*Field, error) {
size := d.Size()
root := &Field{Name: "Certificates", Type: "container", Offset: 0, Length: size}
// Read whole doc — bounded by caller's judgment. For huge firmware
// blobs this might be expensive but certs are small; most real
// cases are <few MB of text or firmware tables.
if size > 32*1024*1024 {
return nil, errors.New("certs scan skipped: file > 32 MiB")
}
all, err := d.Range(0, size)
if err != nil {
return nil, err
}
// PEM pass.
rest := all
pemCount := 0
var pemOff int64 = 0
for {
block, next := pem.Decode(rest)
if block == nil {
break
}
// Approximate offset: the header "-----BEGIN" will be somewhere in `rest`.
idx := indexOfHeader(rest)
if idx < 0 {
break
}
start := pemOff + int64(idx)
end := pemOff + int64(len(rest)-len(next))
pemCount++
child := &Field{
Name: fmt.Sprintf("PEM %s #%d", block.Type, pemCount),
Type: "pem",
Offset: start,
Length: end - start,
}
if block.Type == "CERTIFICATE" {
if cert, err := x509.ParseCertificate(block.Bytes); err == nil {
populateCert(child, cert)
} else {
child.Note = "x509 parse failed: " + err.Error()
}
}
root.Children = append(root.Children, child)
pemOff += int64(len(rest) - len(next))
rest = next
}
// DER pass — scan for SEQUENCE 0x30 0x82 xx yy (definite long-form, 2 length bytes).
//
// Each candidate runs a full x509.ParseCertificate over a blob up to 64 KiB.
// Without guards a buffer dense with "30 82" byte pairs (random/firmware
// data, or a hostile file) would attempt that parse at nearly every offset
// — O(n) expensive parses, enough to freeze the UI. Two guards keep it
// bounded: a cheap structural pre-filter (a DER certificate body begins
// with the tbsCertificate SEQUENCE, 0x30) rejects almost all false
// candidates without parsing, and a hard cap on parse attempts bounds even
// adversarial input that defeats the pre-filter.
const maxDERAttempts = 1 << 16
derCount := 0
derAttempts := 0
for i := 0; i+4 < len(all); i++ {
if all[i] != 0x30 || all[i+1] != 0x82 {
continue
}
if all[i+4] != 0x30 {
continue
}
totalLen := int(all[i+2])<<8 | int(all[i+3])
if totalLen < 100 || i+4+totalLen > len(all) {
continue
}
if derAttempts++; derAttempts > maxDERAttempts {
break
}
blob := all[i : i+4+totalLen]
cert, err := x509.ParseCertificate(blob)
if err != nil {
continue
}
derCount++
child := &Field{
Name: fmt.Sprintf("DER cert #%d", derCount),
Type: "der",
Offset: int64(i),
Length: int64(4 + totalLen),
}
populateCert(child, cert)
root.Children = append(root.Children, child)
i += 4 + totalLen - 1
}
if len(root.Children) == 0 {
return nil, errors.New("no certificates found")
}
root.Value = fmt.Sprintf("PEM=%d DER=%d", pemCount, derCount)
return root, nil
}
func populateCert(f *Field, c *x509.Certificate) {
f.Value = fmt.Sprintf("subject=%q issuer=%q serial=%x",
c.Subject.String(), c.Issuer.String(), c.SerialNumber)
add := func(name, value string) {
f.Children = append(f.Children, &Field{Name: name, Type: "field", Offset: f.Offset, Length: 0, Value: value})
}
add("subject", c.Subject.String())
add("issuer", c.Issuer.String())
add("serial", fmt.Sprintf("%x", c.SerialNumber))
add("valid_from", c.NotBefore.UTC().Format("2006-01-02 15:04:05 UTC"))
add("valid_until", c.NotAfter.UTC().Format("2006-01-02 15:04:05 UTC"))
add("pubkey_alg", c.PublicKeyAlgorithm.String())
add("sig_alg", c.SignatureAlgorithm.String())
if len(c.DNSNames) > 0 {
add("dns_names", strings.Join(c.DNSNames, ", "))
}
if len(c.IPAddresses) > 0 {
ips := make([]string, 0, len(c.IPAddresses))
for _, ip := range c.IPAddresses {
ips = append(ips, ip.String())
}
add("ip_addresses", strings.Join(ips, ", "))
}
if len(c.EmailAddresses) > 0 {
add("emails", strings.Join(c.EmailAddresses, ", "))
}
if c.IsCA {
add("ca", "true")
}
if c.KeyUsage != 0 {
add("key_usage", fmt.Sprintf("0x%x", int(c.KeyUsage)))
}
if len(c.ExtKeyUsage) > 0 {
parts := make([]string, 0, len(c.ExtKeyUsage))
for _, u := range c.ExtKeyUsage {
parts = append(parts, extKeyUsageName(u))
}
add("ext_key_usage", strings.Join(parts, ", "))
}
// SHA-256 fingerprint of DER
add("sha256_fingerprint", sha256Hex(c.Raw))
}
func extKeyUsageName(u x509.ExtKeyUsage) string {
switch u {
case x509.ExtKeyUsageAny:
return "any"
case x509.ExtKeyUsageServerAuth:
return "serverAuth"
case x509.ExtKeyUsageClientAuth:
return "clientAuth"
case x509.ExtKeyUsageCodeSigning:
return "codeSigning"
case x509.ExtKeyUsageEmailProtection:
return "emailProtection"
case x509.ExtKeyUsageTimeStamping:
return "timeStamping"
case x509.ExtKeyUsageOCSPSigning:
return "OCSPSigning"
}
return fmt.Sprintf("%d", int(u))
}
func sha256Hex(b []byte) string {
h := sha256.Sum256(b)
return fmt.Sprintf("%x", h[:])
}
// indexOfHeader returns the byte offset of the next PEM BEGIN line.
func indexOfHeader(b []byte) int {
const tag = "-----BEGIN"
for i := 0; i+len(tag) <= len(b); i++ {
if string(b[i:i+len(tag)]) == tag {
return i
}
}
return -1
}
+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
}
File diff suppressed because it is too large Load Diff
+662
View File
@@ -0,0 +1,662 @@
// Additional structure parsers that round out the structures panel into a
// forensic-grade set: gzip, tar, SQLite, TIFF, pcap/pcapng and the OLE/CFB
// compound-file container used by legacy Office documents and MSI installers.
//
// Each parser follows the same contract as the parsers in formats.go: it
// returns a *Field tree anchored at absolute document offsets, never panics on
// truncated input, and bounds every loop so a hostile file cannot wedge the UI.
package parsers
import (
"encoding/binary"
"errors"
"fmt"
"strconv"
"strings"
"time"
"github.com/andydixon/nexus/internal/leviathan/document"
)
// leaf builds a terminal Field with no children.
func leaf(name, typ string, off, length int64, value string) *Field {
return &Field{Name: name, Type: typ, Offset: off, Length: length, Value: value}
}
// ---------- gzip (RFC 1952) ----------
// ParseGZIP decodes the gzip member header, optional name/comment/extra fields
// and the trailing CRC-32 / ISIZE footer.
func ParseGZIP(d *document.Document) (*Field, error) {
size := d.Size()
hdr, err := d.Range(0, min64(10, size))
if err != nil || len(hdr) < 10 {
return nil, errors.New("gzip too short")
}
if hdr[0] != 0x1f || hdr[1] != 0x8b {
return nil, errors.New("not gzip")
}
root := &Field{Name: "GZIP", Type: "container", Offset: 0, Length: size}
method := hdr[2]
methodName := "deflate"
if method != 8 {
methodName = fmt.Sprintf("unknown(%d)", method)
}
flg := hdr[3]
mtime := binary.LittleEndian.Uint32(hdr[4:8])
root.Children = append(root.Children,
leaf("id", "bytes", 0, 2, "\\x1f\\x8b"),
leaf("method", "u8", 2, 1, methodName),
leaf("flags", "u8", 3, 1, gzipFlags(flg)),
leaf("mtime", "u32le", 4, 4, gzipTime(mtime)),
leaf("extra_flags", "u8", 8, 1, fmt.Sprintf("%d", hdr[8])),
leaf("os", "u8", 9, 1, gzipOS(hdr[9])),
)
off := int64(10)
// FEXTRA
if flg&0x04 != 0 && off+2 <= size {
xb, _ := d.Range(off, 2)
xlen := int64(binary.LittleEndian.Uint16(xb))
root.Children = append(root.Children, leaf("extra", "bytes", off, 2+xlen, fmt.Sprintf("%d bytes", xlen)))
off += 2 + xlen
}
// FNAME
if flg&0x08 != 0 {
s, n := readCStringField(d, off, size)
root.Children = append(root.Children, leaf("name", "asciiz", off, n, s))
off += n
}
// FCOMMENT
if flg&0x10 != 0 {
s, n := readCStringField(d, off, size)
root.Children = append(root.Children, leaf("comment", "asciiz", off, n, s))
off += n
}
// FHCRC
if flg&0x02 != 0 && off+2 <= size {
hb, _ := d.Range(off, 2)
root.Children = append(root.Children, leaf("header_crc16", "u16le", off, 2, fmt.Sprintf("0x%04x", binary.LittleEndian.Uint16(hb))))
off += 2
}
if size >= off+8 {
root.Children = append(root.Children, leaf("compressed_data", "bytes", off, size-off-8, fmt.Sprintf("%d bytes", size-off-8)))
ft, _ := d.Range(size-8, 8)
root.Children = append(root.Children,
leaf("crc32", "u32le", size-8, 4, fmt.Sprintf("0x%08x", binary.LittleEndian.Uint32(ft[0:4]))),
leaf("isize", "u32le", size-4, 4, fmt.Sprintf("%d bytes (mod 2^32)", binary.LittleEndian.Uint32(ft[4:8]))),
)
}
return root, nil
}
func gzipFlags(f byte) string {
var on []string
for _, b := range []struct {
mask byte
name string
}{{0x01, "TEXT"}, {0x02, "HCRC"}, {0x04, "EXTRA"}, {0x08, "NAME"}, {0x10, "COMMENT"}} {
if f&b.mask != 0 {
on = append(on, b.name)
}
}
if len(on) == 0 {
return fmt.Sprintf("0x%02x", f)
}
return fmt.Sprintf("0x%02x (%s)", f, strings.Join(on, "|"))
}
func gzipOS(b byte) string {
names := map[byte]string{0: "FAT", 1: "Amiga", 2: "VMS", 3: "Unix", 6: "HPFS", 7: "Macintosh", 11: "NTFS", 255: "unknown"}
if n, ok := names[b]; ok {
return fmt.Sprintf("%d (%s)", b, n)
}
return fmt.Sprintf("%d", b)
}
func gzipTime(t uint32) string {
if t == 0 {
return "0 (no timestamp)"
}
return fmt.Sprintf("%d (%s)", t, time.Unix(int64(t), 0).UTC().Format("2006-01-02 15:04:05 UTC"))
}
// ---------- tar (POSIX ustar) ----------
// ParseTAR walks the 512-byte record headers of a ustar/GNU tar archive.
func ParseTAR(d *document.Document) (*Field, error) {
size := d.Size()
root := &Field{Name: "TAR", Type: "container", Offset: 0}
off := int64(0)
entries := 0
for off+512 <= size && entries < 10000 {
blk, err := d.Range(off, 512)
if err != nil {
break
}
if allZero(blk) {
root.Children = append(root.Children, leaf("end_of_archive", "bytes", off, 512, "zero block"))
off += 512
break
}
name := trimZeroStr(blk[0:100])
prefix := trimZeroStr(blk[345:500])
full := name
if prefix != "" {
full = prefix + "/" + name
}
fsize := octal(blk[124:136])
mtime := octal(blk[136:148])
typ := blk[156]
e := &Field{
Name: full, Type: "entry", Offset: off,
Length: 512 + roundUp(fsize, 512),
Value: fmt.Sprintf("%s size=%d", tarType(typ), fsize),
Children: []*Field{
leaf("name", "asciiz", off, 100, name),
leaf("mode", "octal", off+100, 8, fmt.Sprintf("0%o", octal(blk[100:108]))),
leaf("uid", "octal", off+108, 8, fmt.Sprintf("%d", octal(blk[108:116]))),
leaf("gid", "octal", off+116, 8, fmt.Sprintf("%d", octal(blk[116:124]))),
leaf("size", "octal", off+124, 12, fmt.Sprintf("%d", fsize)),
leaf("mtime", "octal", off+136, 12, tarTime(mtime)),
leaf("typeflag", "u8", off+156, 1, tarType(typ)),
leaf("magic", "ascii", off+257, 8, trimZeroStr(blk[257:265])),
leaf("uname", "asciiz", off+265, 32, trimZeroStr(blk[265:297])),
leaf("gname", "asciiz", off+297, 32, trimZeroStr(blk[297:329])),
},
}
if prefix != "" {
e.Children = append(e.Children, leaf("prefix", "asciiz", off+345, 155, prefix))
}
root.Children = append(root.Children, e)
off += 512 + roundUp(fsize, 512)
entries++
}
root.Length = off
if len(root.Children) == 0 {
return nil, errors.New("no tar entries")
}
return root, nil
}
func tarType(t byte) string {
switch t {
case 0, '0':
return "file"
case '1':
return "hardlink"
case '2':
return "symlink"
case '3':
return "char-device"
case '4':
return "block-device"
case '5':
return "directory"
case '6':
return "fifo"
case '7':
return "contiguous"
case 'x':
return "pax-header"
case 'g':
return "pax-global"
case 'L':
return "gnu-longname"
case 'K':
return "gnu-longlink"
default:
return fmt.Sprintf("type-%q", t)
}
}
func tarTime(t int64) string {
if t == 0 {
return "0"
}
return fmt.Sprintf("%d (%s)", t, time.Unix(t, 0).UTC().Format("2006-01-02 15:04:05 UTC"))
}
// ---------- SQLite 3 ----------
// ParseSQLite decodes the 100-byte SQLite database header.
func ParseSQLite(d *document.Document) (*Field, error) {
hdr, err := d.Range(0, min64(100, d.Size()))
if err != nil || len(hdr) < 100 {
return nil, errors.New("sqlite header too short")
}
if string(hdr[0:16]) != "SQLite format 3\x00" {
return nil, errors.New("not a SQLite database")
}
be := binary.BigEndian
pageSize := int64(be.Uint16(hdr[16:18]))
if pageSize == 1 {
pageSize = 65536
}
enc := map[uint32]string{1: "UTF-8", 2: "UTF-16le", 3: "UTF-16be"}[be.Uint32(hdr[56:60])]
if enc == "" {
enc = "unknown"
}
root := &Field{
Name: "SQLite", Type: "container", Offset: 0, Length: d.Size(),
Value: fmt.Sprintf("page=%d pages=%d %s", pageSize, be.Uint32(hdr[28:32]), enc),
Children: []*Field{
leaf("magic", "ascii", 0, 16, "SQLite format 3"),
leaf("page_size", "u16be", 16, 2, fmt.Sprintf("%d", pageSize)),
leaf("write_version", "u8", 18, 1, sqliteFmtVer(hdr[18])),
leaf("read_version", "u8", 19, 1, sqliteFmtVer(hdr[19])),
leaf("reserved_space", "u8", 20, 1, fmt.Sprintf("%d", hdr[20])),
leaf("change_counter", "u32be", 24, 4, fmt.Sprintf("%d", be.Uint32(hdr[24:28]))),
leaf("page_count", "u32be", 28, 4, fmt.Sprintf("%d", be.Uint32(hdr[28:32]))),
leaf("freelist_head", "u32be", 32, 4, fmt.Sprintf("%d", be.Uint32(hdr[32:36]))),
leaf("freelist_pages", "u32be", 36, 4, fmt.Sprintf("%d", be.Uint32(hdr[36:40]))),
leaf("schema_cookie", "u32be", 40, 4, fmt.Sprintf("%d", be.Uint32(hdr[40:44]))),
leaf("schema_format", "u32be", 44, 4, fmt.Sprintf("%d", be.Uint32(hdr[44:48]))),
leaf("text_encoding", "u32be", 56, 4, enc),
leaf("user_version", "u32be", 60, 4, fmt.Sprintf("%d", be.Uint32(hdr[60:64]))),
leaf("vacuum_mode", "u32be", 64, 4, fmt.Sprintf("%d", be.Uint32(hdr[64:68]))),
leaf("application_id", "u32be", 68, 4, fmt.Sprintf("0x%08x", be.Uint32(hdr[68:72]))),
leaf("version_valid_for", "u32be", 92, 4, fmt.Sprintf("%d", be.Uint32(hdr[92:96]))),
leaf("sqlite_version", "u32be", 96, 4, sqliteVersion(be.Uint32(hdr[96:100]))),
},
}
return root, nil
}
func sqliteFmtVer(b byte) string {
switch b {
case 1:
return "1 (legacy/rollback)"
case 2:
return "2 (WAL)"
default:
return fmt.Sprintf("%d", b)
}
}
func sqliteVersion(v uint32) string {
// SQLITE_VERSION_NUMBER = major*1_000_000 + minor*1000 + patch.
return fmt.Sprintf("%d (%d.%d.%d)", v, v/1000000, (v/1000)%1000, v%1000)
}
// ---------- TIFF ----------
// ParseTIFF decodes the TIFF header and the entries of each image file
// directory (IFD), following the next-IFD chain.
func ParseTIFF(d *document.Document) (*Field, error) {
size := d.Size()
hdr, err := d.Range(0, min64(8, size))
if err != nil || len(hdr) < 8 {
return nil, errors.New("tiff too short")
}
var bo binary.ByteOrder
switch string(hdr[0:2]) {
case "II":
bo = binary.LittleEndian
case "MM":
bo = binary.BigEndian
default:
return nil, errors.New("not TIFF")
}
if bo.Uint16(hdr[2:4]) != 42 {
return nil, errors.New("bad TIFF magic")
}
order := "little-endian"
if string(hdr[0:2]) == "MM" {
order = "big-endian"
}
root := &Field{
Name: "TIFF", Type: "container", Offset: 0, Length: size, Value: order,
Children: []*Field{
leaf("byte_order", "ascii", 0, 2, order),
leaf("magic", "u16", 2, 2, "42"),
leaf("first_ifd", "u32", 4, 4, fmt.Sprintf("0x%x", bo.Uint32(hdr[4:8]))),
},
}
ifdOff := int64(bo.Uint32(hdr[4:8]))
for n := 0; ifdOff > 0 && ifdOff+2 <= size && n < 64; n++ {
cb, err := d.Range(ifdOff, 2)
if err != nil {
break
}
count := int64(bo.Uint16(cb))
ifd := &Field{
Name: fmt.Sprintf("IFD[%d]", n), Type: "directory", Offset: ifdOff,
Length: 2 + count*12 + 4, Value: fmt.Sprintf("%d entries", count),
}
for i := int64(0); i < count && i < 4096; i++ {
eoff := ifdOff + 2 + i*12
eb, err := d.Range(eoff, 12)
if err != nil || len(eb) < 12 {
break
}
tag := bo.Uint16(eb[0:2])
typ := bo.Uint16(eb[2:4])
cnt := bo.Uint32(eb[4:8])
val := bo.Uint32(eb[8:12])
ifd.Children = append(ifd.Children, leaf(
fmt.Sprintf("tag 0x%04x %s", tag, tiffTag(tag)), "ifd_entry", eoff, 12,
fmt.Sprintf("type=%s count=%d value=%d", tiffType(typ), cnt, val)))
}
root.Children = append(root.Children, ifd)
nb, err := d.Range(ifdOff+2+count*12, 4)
if err != nil || len(nb) < 4 {
break
}
ifdOff = int64(bo.Uint32(nb))
}
return root, nil
}
func tiffType(t uint16) string {
names := map[uint16]string{1: "BYTE", 2: "ASCII", 3: "SHORT", 4: "LONG", 5: "RATIONAL", 6: "SBYTE", 7: "UNDEFINED", 8: "SSHORT", 9: "SLONG", 10: "SRATIONAL", 11: "FLOAT", 12: "DOUBLE"}
if n, ok := names[t]; ok {
return n
}
return fmt.Sprintf("type%d", t)
}
func tiffTag(t uint16) string {
names := map[uint16]string{
256: "ImageWidth", 257: "ImageLength", 258: "BitsPerSample", 259: "Compression",
262: "PhotometricInterpretation", 273: "StripOffsets", 277: "SamplesPerPixel",
278: "RowsPerStrip", 279: "StripByteCounts", 282: "XResolution", 283: "YResolution",
296: "ResolutionUnit", 305: "Software", 306: "DateTime", 315: "Artist",
271: "Make", 272: "Model", 274: "Orientation", 34665: "ExifIFD", 34853: "GPSIFD",
}
if n, ok := names[t]; ok {
return n
}
return ""
}
// ---------- pcap / pcapng ----------
// ParsePCAP decodes a classic libpcap global header and walks packet records.
func ParsePCAP(d *document.Document) (*Field, error) {
size := d.Size()
hdr, err := d.Range(0, min64(24, size))
if err != nil || len(hdr) < 24 {
return nil, errors.New("pcap too short")
}
magic := binary.LittleEndian.Uint32(hdr[0:4])
var bo binary.ByteOrder
nano := false
switch magic {
case 0xa1b2c3d4:
bo = binary.LittleEndian
case 0xd4c3b2a1:
bo = binary.BigEndian
case 0xa1b23c4d:
bo, nano = binary.LittleEndian, true
case 0x4d3cb2a1:
bo, nano = binary.BigEndian, true
default:
return nil, errors.New("not pcap")
}
res := "microsecond"
if nano {
res = "nanosecond"
}
linktype := bo.Uint32(hdr[20:24])
root := &Field{
Name: "PCAP", Type: "container", Offset: 0, Length: size,
Value: fmt.Sprintf("%s, %s", res, pcapLinkType(linktype)),
Children: []*Field{
leaf("magic", "u32", 0, 4, fmt.Sprintf("0x%08x (%s)", magic, res)),
leaf("version", "u16", 4, 4, fmt.Sprintf("%d.%d", bo.Uint16(hdr[4:6]), bo.Uint16(hdr[6:8]))),
leaf("thiszone", "i32", 8, 4, fmt.Sprintf("%d", int32(bo.Uint32(hdr[8:12])))),
leaf("sigfigs", "u32", 12, 4, fmt.Sprintf("%d", bo.Uint32(hdr[12:16]))),
leaf("snaplen", "u32", 16, 4, fmt.Sprintf("%d", bo.Uint32(hdr[16:20]))),
leaf("linktype", "u32", 20, 4, fmt.Sprintf("%d (%s)", linktype, pcapLinkType(linktype))),
},
}
off := int64(24)
for n := 0; off+16 <= size && n < 100000; n++ {
rb, err := d.Range(off, 16)
if err != nil {
break
}
ts := bo.Uint32(rb[0:4])
us := bo.Uint32(rb[4:8])
inclLen := int64(bo.Uint32(rb[8:12]))
origLen := bo.Uint32(rb[12:16])
root.Children = append(root.Children, leaf(
fmt.Sprintf("packet[%d]", n), "record", off, 16+inclLen,
fmt.Sprintf("ts=%d.%06d incl=%d orig=%d", ts, us, inclLen, origLen)))
off += 16 + inclLen
}
return root, nil
}
// ParsePCAPNG decodes the pcapng block structure (section header + block walk).
func ParsePCAPNG(d *document.Document) (*Field, error) {
size := d.Size()
hb, err := d.Range(0, min64(12, size))
if err != nil || len(hb) < 12 {
return nil, errors.New("pcapng too short")
}
if binary.LittleEndian.Uint32(hb[0:4]) != 0x0a0d0d0a {
return nil, errors.New("not pcapng")
}
// Byte-order magic lives in the SHB body at offset 8.
var bo binary.ByteOrder
switch binary.LittleEndian.Uint32(hb[8:12]) {
case 0x1a2b3c4d:
bo = binary.LittleEndian
case 0x4d3c2b1a:
bo = binary.BigEndian
default:
return nil, errors.New("bad pcapng byte-order magic")
}
root := &Field{Name: "PCAPNG", Type: "container", Offset: 0, Length: size}
off := int64(0)
for n := 0; off+12 <= size && n < 100000; n++ {
bh, err := d.Range(off, 12)
if err != nil {
break
}
btype := bo.Uint32(bh[0:4])
blen := int64(bo.Uint32(bh[4:8]))
if blen < 12 || off+blen > size {
break
}
root.Children = append(root.Children, leaf(
fmt.Sprintf("block[%d] %s", n, pcapngBlock(btype)), "block", off, blen,
fmt.Sprintf("type=0x%08x length=%d", btype, blen)))
off += blen
}
return root, nil
}
func pcapngBlock(t uint32) string {
switch t {
case 0x0a0d0d0a:
return "SectionHeader"
case 0x00000001:
return "InterfaceDescription"
case 0x00000003:
return "SimplePacket"
case 0x00000004:
return "NameResolution"
case 0x00000005:
return "InterfaceStatistics"
case 0x00000006:
return "EnhancedPacket"
default:
return "block"
}
}
func pcapLinkType(t uint32) string {
names := map[uint32]string{0: "NULL", 1: "ETHERNET", 6: "TOKEN_RING", 9: "PPP", 101: "RAW", 105: "IEEE802_11", 113: "LINUX_SLL", 127: "IEEE802_11_RADIOTAP", 228: "IPV4", 229: "IPV6", 276: "LINUX_SLL2"}
if n, ok := names[t]; ok {
return n
}
return fmt.Sprintf("linktype%d", t)
}
// ---------- OLE / Compound File Binary (MS-CFB) ----------
var cfbMagic = []byte{0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1}
// ParseCFB decodes the compound-file header and lists the entries in the first
// directory sector (legacy .doc/.xls/.ppt/.msi container).
func ParseCFB(d *document.Document) (*Field, error) {
size := d.Size()
hdr, err := d.Range(0, min64(512, size))
if err != nil || len(hdr) < 76 {
return nil, errors.New("cfb too short")
}
if string(hdr[0:8]) != string(cfbMagic) {
return nil, errors.New("not a compound file")
}
le := binary.LittleEndian
major := le.Uint16(hdr[26:28])
sectorShift := le.Uint16(hdr[30:32])
sectorSize := int64(1) << sectorShift
firstDir := le.Uint32(hdr[48:52])
root := &Field{
Name: "CFB/OLE", Type: "container", Offset: 0, Length: size,
Value: fmt.Sprintf("v%d.%d sector=%d", major, le.Uint16(hdr[24:26]), sectorSize),
Children: []*Field{
leaf("signature", "bytes", 0, 8, "D0 CF 11 E0 A1 B1 1A E1"),
leaf("minor_version", "u16le", 24, 2, fmt.Sprintf("%d", le.Uint16(hdr[24:26]))),
leaf("major_version", "u16le", 26, 2, fmt.Sprintf("%d", major)),
leaf("byte_order", "u16le", 28, 2, fmt.Sprintf("0x%04x", le.Uint16(hdr[28:30]))),
leaf("sector_shift", "u16le", 30, 2, fmt.Sprintf("%d (%d bytes)", sectorShift, sectorSize)),
leaf("mini_sector_shift", "u16le", 32, 2, fmt.Sprintf("%d (%d bytes)", le.Uint16(hdr[32:34]), int64(1)<<le.Uint16(hdr[32:34]))),
leaf("num_dir_sectors", "u32le", 40, 4, fmt.Sprintf("%d", le.Uint32(hdr[40:44]))),
leaf("num_fat_sectors", "u32le", 44, 4, fmt.Sprintf("%d", le.Uint32(hdr[44:48]))),
leaf("first_dir_sector", "u32le", 48, 4, fmt.Sprintf("%d", firstDir)),
leaf("mini_stream_cutoff", "u32le", 56, 4, fmt.Sprintf("%d", le.Uint32(hdr[56:60]))),
leaf("first_minifat_sector", "u32le", 60, 4, fmt.Sprintf("%d", le.Uint32(hdr[60:64]))),
leaf("num_minifat_sectors", "u32le", 64, 4, fmt.Sprintf("%d", le.Uint32(hdr[64:68]))),
leaf("first_difat_sector", "u32le", 68, 4, fmt.Sprintf("%d", le.Uint32(hdr[68:72]))),
leaf("num_difat_sectors", "u32le", 72, 4, fmt.Sprintf("%d", le.Uint32(hdr[72:76]))),
},
}
// First directory sector holds up to sectorSize/128 directory entries.
dirOff := int64(firstDir+1) * sectorSize
if dir := cfbDirectory(d, dirOff, sectorSize, size); dir != nil {
root.Children = append(root.Children, dir)
}
return root, nil
}
func cfbDirectory(d *document.Document, off, sectorSize, size int64) *Field {
if off <= 0 || off+128 > size {
return nil
}
dir := &Field{Name: "directory", Type: "directory", Offset: off}
per := sectorSize / 128
count := int64(0)
for i := int64(0); i < per; i++ {
eoff := off + i*128
eb, err := d.Range(eoff, 128)
if err != nil || len(eb) < 128 {
break
}
nameLen := int64(binary.LittleEndian.Uint16(eb[64:66]))
objType := eb[66]
if objType == 0 { // unallocated entry
continue
}
name := ""
if nameLen >= 2 && nameLen <= 64 {
name = decodeUTF16LE(eb[0 : nameLen-2])
}
streamSize := binary.LittleEndian.Uint64(eb[120:128])
dir.Children = append(dir.Children, leaf(
cfbEntryLabel(name, count), "dir_entry", eoff, 128,
fmt.Sprintf("%s size=%d", cfbObjType(objType), streamSize)))
count++
}
dir.Length = sectorSize
dir.Value = fmt.Sprintf("%d entries", count)
if count == 0 {
return nil
}
return dir
}
func cfbEntryLabel(name string, i int64) string {
if name == "" {
return fmt.Sprintf("entry[%d]", i)
}
// Strip the control prefix Office uses on some stream names for display.
clean := strings.Map(func(r rune) rune {
if r < 0x20 {
return -1
}
return r
}, name)
if clean == "" {
clean = name
}
return fmt.Sprintf("entry[%d] %s", i, clean)
}
func cfbObjType(t byte) string {
switch t {
case 1:
return "storage"
case 2:
return "stream"
case 5:
return "root"
default:
return fmt.Sprintf("type%d", t)
}
}
// ---------- shared helpers ----------
func readCStringField(d *document.Document, off, size int64) (string, int64) {
var sb strings.Builder
n := int64(0)
for off+n < size && n < 4096 {
b, err := d.At(off + n)
if err != nil {
break
}
n++
if b == 0 {
break
}
sb.WriteByte(b)
}
return sb.String(), n
}
func allZero(b []byte) bool {
for _, x := range b {
if x != 0 {
return false
}
}
return true
}
func trimZeroStr(b []byte) string {
return strings.TrimRight(string(b), "\x00 ")
}
// octal parses a space/NUL-padded octal ASCII field (tar number encoding).
func octal(b []byte) int64 {
s := strings.TrimSpace(strings.Trim(string(b), "\x00"))
if s == "" {
return 0
}
v, err := strconv.ParseInt(s, 8, 64)
if err != nil {
return 0
}
return v
}
func roundUp(n, to int64) int64 {
if n <= 0 {
return 0
}
return ((n + to - 1) / to) * to
}
+408
View File
@@ -0,0 +1,408 @@
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
}
+150
View File
@@ -0,0 +1,150 @@
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
}