v3 candidate
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user