Files
praetorian-inc-reduce-golan…/references/pe-structural-features.md
T
Michael Weber 3a6638a965 Initial open-source release
Skill, PE structural analyzer, and reference docs for systematically
reducing VirusTotal/EDR detection rates on compiled Go binaries via
structural analysis and disciplined A/B testing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 14:18:00 -04:00

5.4 KiB

PE Structural Features: ML Classifier Taxonomy

What AV ML engines measure, organized by feature category. Use this to understand which features your changes affect and what the classifier sees.

How ML Classifiers Work

Engines like Symantec ML.Attribute.HighConfidence and AhnLab MalwareX-gen extract a ~200-2000 dimensional feature vector from each PE binary, then run a trained classifier. No single feature triggers detection — the full vector is scored together.

This means:

  • Individual "anomalies" may not matter if everything else looks clean
  • Multiple small anomalies that point in the same direction are detected with high confidence
  • Fighting the toolchain identity adds inconsistency signals that raise the score

Feature Category Reference

1. Header Fields (High ML Weight)

Feature What ML Sees Notes
MajorLinkerVersion 3 = Go, 14 = MSVC, 2 = MinGW Strong toolchain fingerprint
TimeDateStamp 0 = Go/reproducible builds Epoch-zero is an anomaly but consistent with Go
SizeOfStackReserve Go default: 0x200000 Changing from default adds inconsistency
DllCharacteristics 0x8160 = Go standard Fine
CheckSum 0 = no checksum, nonzero = computed We compute it — slightly inconsistent with vanilla Go
Rich header Absent in Go/Rust/MinGW Absence correlates with non-MSVC toolchains

Key principle: Do NOT patch header fields to look like a different toolchain. The section layout, import style, and string patterns still scream Go. Inconsistency = higher score.

2. Section Table (Highest ML Weight)

Feature Vanilla Go Modified Build ML Impact
Section count 16 18 (+.rsrc, .edata) Medium
.data BSS ratio (virt/raw) 6.4x 208x Very High
Debug-like sections % 27% 50% High
Max section entropy 7.998 7.999 Low (both near max)
High-entropy section count 6 7 Low
Numeric section names (/4, /19…) 8 8 (same) Low
.symtab present Yes Yes (4x larger) Medium
Phantom .edata (section + no export dir) No Yes Medium

The 208x BSS ratio is the single largest structural anomaly. It comes from Sliver's global variable allocation. Vanilla Go hello-world has 6.4x. This is load-bearing — cannot be easily changed without modifying the linker.

3. Import Table (High ML Weight)

Feature Vanilla Go Modified Build
DLL count 1 4
Total imports 47 53
GetProcAddress present Yes Yes
LoadLibraryExW present Yes Yes (duplicated)

Unused import heuristic (AVG/Avast Win64:Evo-gen): DLLs with only 1-2 functions that are never called in the code trigger detection. Cap at 3 enriched DLLs max.

Import hash (imphash): Each randomized build has a unique imphash — no hash-based blocking.

4. Byte-Level Features (Used by Symantec, AhnLab)

Feature Vanilla Go Modified Build
File entropy 6.85 7.44
Byte histogram std 0.0114 0.0068 (more uniform)
Null byte ratio 17.6% 10.6%
Printable ratio 33.0% 35.5%

Higher file entropy (7.44 vs 6.85) comes from the WASM payload in debug sections. The byte distribution is more uniform (lower std) — consistent with encrypted/compressed content.

5. String Features (Used by Most Engines)

Feature Vanilla Go Modified Build
String count 11,638 61,919
Path strings 1,759 14,468
URL strings 6 33
MZ header strings 2 5

High string count and path count come from Sliver's large codebase. Hard to change.

6. Data Directory Completeness

Both vanilla Go and the modified build are missing:

  • DEBUG directory — legitimate software often has a PDB path here
  • LOAD_CONFIG — security cookies, guard flags, etc.
  • TLS — thread-local storage

These absences are shared with vanilla Go, so they don't add additional detection signal relative to baseline. Do not add fake directories — they create inconsistencies.

7. Go-Specific Anomaly Composite

The analyzer's anomaly score tracks deviations from standard PE norms. The modified build in the example below adds 3 anomalies beyond vanilla Go:

  • extreme_bss_ratio_208 — .data 34MB virtual / 163KB raw
  • phantom_edata — .edata section exists but export directory is empty
  • debug_sections_over_40pct — 50% of file is high-entropy compressed data

Vanilla Go already has: timestamp_zero, no_rich_header, linker_v3_go, numeric_section_names, has_coff_symtab, no_debug_directory, no_load_config, dynamic_api_resolution, many_high_entropy_sections.

Fixing the 3 extras is the highest-leverage structural target.

What Makes a Binary "Normal" to ML

ML classifiers are trained on millions of real Windows binaries. "Normal" means:

  • Consistent toolchain signals (same linker version, section layout, import style throughout)
  • Code+data sections dominate file size (not 50% compressed data)
  • BSS/virtual size ratio typical for the toolchain
  • Import DLLs actually used (no phantom imports)
  • No phantom sections (section exists but directory entry is missing/empty)

The goal is not to look like a generic Windows binary. The goal is to look like a large, legitimate Go program. Large Go programs exist and are clean on VT.