
Framework de Desenvolvimento de Shellcode em Modo de Usuário do Windows (WUMSDF)
v1.1O projeto SILVERPICK é um Windows User-Mode Shellcode Development Framework (WUMSDF) cujo único propósito é capacitar desenvolvedores de capacidades a construir blocos de Position Independent Code (PIC) para Windows x64 usando C/C++ de maneira fácil, de modo a reduzir os custos de desenvolvimento de tal empreendimento.
Ele deriva do projeto WILDBEAST e, como tal, utiliza:
Visual Studio Code como editor de códigoMinGW-w64 como toolchain de compilaçãoGNU Make como sistema de buildVocê pode encontrar as instruções de configuração aqui: GCC-Clang-Setup-Windows
Por favor, note que este projeto está usando MSYS2.
Escrever shellcode em linguagens de programação de alto nível não é novidade, e inúmeros posts de blog e artigos de pesquisa foram publicados sobre o assunto desde 2010. Então, o que há de novo no SILVERPICK?
Bem, fico feliz que você tenha perguntado.
SILVERPICK tem um belo repertório de truques escondidos na manga, mas, acima de tudo, esta é a minha visão sobre o assunto.
Então, sem mais delongas, apresento a vocês meu primeiro truque.
Desde que Matt Graeber popularizou escrever shellcode em C, a maioria das pessoas tem usado seu stub de alinhamento de pilha de 16 bytes escrito em linguagem Assembly.
Embora isso não seja um problema, já que não somos a IKEA, assembly não deveria ser necessário, e de fato não é.
Existe um GCC Atributo de Função que emitirá o stub de alinhamento de pilha para você.
Conheça o atributo de função force_align_arg_pointer na forma de uma útil macro ALIGN_STACK, que gera o seguinte assembly:
Disassembly of section .init:
<PicEntry>:
push rbp
mov rbp,rsp
and rsp,0xfffffffffffffff0
sub rsp,0x20
call <PicEntry+0x11> IMAGE_REL_AMD64_REL32 .text$payload
leave
ret
O que é a seção .init, você pergunta? Bem, isso serve como uma boa transição para o meu segundo truque.
Matt Graeber pode ter popularizado escrever shellcode em C em algum momento, mas, na verdade, foi Paul Ungur quem reviveu essa arte negra com Stardust.
Agora, Stardust usa um script de linker do Binutils para controlar o posicionamento de funções e dados na seção PE apropriada na ordem correta. Essa técnica em si é derivada do trabalho de Austin Hudson, e muitas pessoas usam uma variante de seus scripts de linker.
Embora scripts de linker sejam ótimos para ordenar seções do linker, se tudo o que você precisa é colocar uma determinada função no início da seção de código, eles são desnecessários.
Entre o atributo de função section com um nome de seção especial chamado .init, que indica ao linker que a função contém código de inicialização em tempo de execução pré-main() e deve estar primeiro na ordem de link.
Para esse efeito, a macro CODE_BEGIN foi criada.
Para o meu terceiro truque, apresento a vocês a macro STACK_STRING.
Em C, você pode criar uma stack string (uma string construída dinamicamente na pilha) declarando o literal de string como um array de caracteres ANSI:
char charrHelloKitty[] = { 'H', 'e', 'l', 'l', 'o', 'K', 'i', 't', 't', 'y', '\0' };
Em C++, você pode criar uma stack string simplesmente marcando um array char como constexpr:
constexpr char charrHelloKitty[]{ "HelloKitty" };
No entanto, ambas as técnicas se tornam inúteis diante de otimizações do compilador se os literais de string forem suficientemente grandes, ao contrário da nossa solução, que funcionará independentemente do comprimento da string e do nível de otimizações do compilador, graças a um engenhoso hack de metaprogramação com templates C++ cortesia de Can Bölük.
Usar essa macro é bastante simples:
STACK_STRING(sstrText, "an extra long hello world!");
STACK_STRING(sstrCaption, "Demo");
MessageBoxA(nullptr, sstrText.data(), sstrCaption.data(), MB_OK);
Isso gerará o seguinte assembly:
mov [rsp+58h+var_23], 61h ; 'a'
mov [rsp+58h+var_22], 6Eh ; 'n'
mov [rsp+58h+var_21], 20h ; ' '
mov [rsp+58h+var_20], 65h ; 'e'
mov [rsp+58h+var_1F], 78h ; 'x'
mov [rsp+58h+var_1E], 74h ; 't'
mov [rsp+58h+var_1D], 72h ; 'r'
mov [rsp+58h+var_1C], 61h ; 'a'
mov [rsp+58h+var_1B], 20h ; ' '
mov [rsp+58h+var_1A], 6Ch ; 'l'
mov [rsp+58h+var_19], 6Fh ; 'o'
mov [rsp+58h+var_18], 6Eh ; 'n'
mov [rsp+58h+var_17], 67h ; 'g'
mov [rsp+58h+var_16], 20h ; ' '
mov [rsp+58h+var_15], 68h ; 'h'
mov [rsp+58h+var_14], 65h ; 'e'
mov [rsp+58h+var_13], 6Ch ; 'l'
mov [rsp+58h+var_12], 6Ch ; 'l'
mov [rsp+58h+var_11], 6Fh ; 'o'
mov [rsp+58h+var_10], 20h ; ' '
mov [rsp+58h+var_2F], 0
mov [rsp+58h+var_F], 77h ; 'w'
mov [rsp+58h+var_E], 6Fh ; 'o'
mov [rsp+58h+var_D], 72h ; 'r'
mov [rsp+58h+var_C], 6Ch ; 'l'
mov [rsp+58h+var_B], 64h ; 'd'
mov [rsp+58h+var_A], 21h ; '!'
mov [rsp+58h+var_33], 44h ; 'D'
mov [rsp+58h+var_32], 65h ; 'e'
mov [rsp+58h+var_31], 6Dh ; 'm'
mov [rsp+58h+var_30], 6Fh ; 'o'
Falando em C++, apresento a vocês hashing de strings em tempo de compilação para o meu quarto truque.
Embora este não seja um conceito novo, SILVERPICK oferece algumas melhorias em relação às implementações públicas existentes.
Primeiramente, usamos a variante de 64 bits da popular função hash não criptográfica FNV-1a para reduzir a probabilidade de um ataque bem-sucedido de colisão de hash.
Em segundo lugar, usamos um parâmetro modificado para a função hash para nos defendermos contra consultas de tabelas hash pré-calculadas, como o HashDB. Crucialmente, isso não altera as propriedades da função hash.
Para fazer hash de uma string curta em tempo de execução, basta usar a macro HASH_STRING_RUN_TIME.
Para fazer hash de um literal de string curta em tempo de compilação, basta usar a macro HASH_STRING_COMPILE_TIME. A avaliação somente em tempo de compilação é garantida via consteval.
Acontece que você pode implementar um bom punhado de funções da C Runtime Library (CRT) usando instruções de string x86. Então, é claro, eu tive que implementá-las usando uma mistura de intrínsecos do compilador e assembly inline.
Você quer usar a função msvcrt!memset no seu código? Use a macro ZERO_MEMORY em vez disso, que usa a instrução rep stosb emitida por meio de um intrínseco do compilador.
E quanto à função msvcrt!memcpy ou à função msvcrt!memmove, você pergunta? Conheça a macro COPY_MEMORY como substituta, que usa a instrução rep movsb emitida por meio de um intrínseco do compilador.
Mas e uma alternativa para a função msvcrt!memcmp? Acontece que não existe exatamente um intrínseco do compilador disponível para emitir a instrução repe cmpsb. Então, escrevemos uma função compare_memory usando assembly inline.
Finalmente, se você procura um substituto para a função msvcrt!memchr, conheça a função scan_memory, que mais uma vez usa assembly inline, já que não há intrínseco do compilador disponível para emitir a instrução repne scasb.
Ah, e eu esqueci de mencionar que você pode escrever sua própria versão mais segura da função msvcrt!strlen usando a rotina scan_memory da seguinte forma:
DWORD_PTR dwptrExportNameLength = std::min(BIT_CAST(DWORD_PTR, scan_memory(strExportName, 0x00, MAX_EXPORTED_SYMBOL_NAME_LEN)) - BIT_CAST(DWORD_PTR, strExportName), MAX_EXPORTED_SYMBOL_NAME_LEN);
Por favor, observe que essas implementações podem não produzir o código com melhor desempenho, dependendo da microarquitetura do CPU alvo. No entanto, elas têm a garantia de fazer o trabalho.
Interessado em mais truques de salão?
Há muitas outras macros pequenas em Common.h que existem para abstrair algumas das complexidades de manipular o compilador.
Uma implementação sem dependências da função GetModuleHandle é fornecida em UserModuleBase.cpp. Para simplificar a facilidade de uso, uma macro útil denominada GET_USER_MODULE_BASE foi criada.
Da mesma forma, uma implementação sem dependências da função GetProcAddress é fornecida em PEParse.cpp, que é então envolvida em uma macro prática apropriadamente chamada de GET_EXPORTED_SYMBOL_ADDRESS. Além disso, duas outras macros foram fornecidas para auxiliar na vinculação dinâmica em tempo de execução - INITIALIZE_FUNCTION_POINTER para declarar e inicializar um ponteiro de função em 0, e RESOLVE_FUNCTION_POINTER para resolver o referido ponteiro de função.
A integração com Visual Studio Code está embutida no projeto para que os desenvolvedores possam usar o atalho de teclado Ctrl+Shift+B para um processo de build sem complicações.
A integração com GitHub Actions também está embutida no projeto para permitir builds de CI.
O projeto tem certo orgulho em sua estrutura bem organizada, bem como no código totalmente comentado e relativamente limpo.
Por fim, dê uma olhada no Makefile do projeto, que contém a melhor seleção de flags do compilador e do linker que irão gerar código pequeno, seguro e amigável ao OPSEC. Enquanto isso, o log detalhado e um arquivo de mapa do linker gerado fornecerão visibilidade sobre o processo de build para facilitar uma compreensão mais profunda da toolchain. Além disso, cada unidade de tradução também produz um arquivo de desmontagem que, após inspeção, muitas vezes fará você dizer coisas como "o que o compilador fez agora?", etc.
Se você está convencido sobre o framework, esta seção descreve como você pode utilizá-lo.
O seguinte é um trecho retirado de PicMain.cpp:
/// @brief PIC start function
/// @param None
/// @return None
EXTERN_C NO_INLINE VOID __stdcall payload(
VOID
) {
// Init local variables
PVOID pKernel32 = nullptr;
INITIALIZE_FUNCTION_POINTER(LoadLibraryA);
HMODULE hUser32 = nullptr;
STACK_STRING(sstrUser32, "user32.dll");
INITIALIZE_FUNCTION_POINTER(MessageBoxA);
STACK_STRING(sstrText, "an extra long hello world!");
STACK_STRING(sstrCaption, "Demo");
// Get the image base address of kernel32.dll
pKernel32 = GET_USER_MODULE_BASE("kernel32.dll");
if (pKernel32 == nullptr)
goto cleanup;
// Resolve kernel32!LoadLibraryA
RESOLVE_FUNCTION_POINTER(pKernel32, LoadLibraryA);
if (LoadLibraryA == nullptr)
goto cleanup;
// Load User32.dll into the process VAS
hUser32 = LoadLibraryA(sstrUser32.data());
if (hUser32 == nullptr)
goto cleanup;
// Resolve user32!MessageBoxA
RESOLVE_FUNCTION_POINTER(hUser32, MessageBoxA);
if (MessageBoxA == nullptr)
goto cleanup;
// Display a message box
MessageBoxA(nullptr, sstrText.data(), sstrCaption.data(), MB_OK);
// Cleanup
cleanup:
return;
}
Parece fácil o suficiente, não?
Ao escrever PIC em C/C++ usando o framework SILVERPICK, você precisa levar em consideração as seguintes regras:
payload como você trataria a função main de um programa tradicional, ou seja, como o (pseudo) ponto de entrada.Windows API ou Native API só podem ser usadas por meio de vinculação dinâmica em tempo de execução após garantir que o protótipo da função esteja disponível no arquivo de cabeçalho correspondente.Esta seção contém uma lista não exaustiva de melhorias planejadas que serão integradas em um projeto futuro.
Clang/LLVM.GS.Export Address Filtering (EAF).A seguir está uma lista de referências organizadas em ordem cronológica que se mostraram inestimáveis para mim durante minha pesquisa e foram amplamente usadas como inspiração para este projeto:
Escrevendo Shellcode com um Compilador C por Nick Harbour (2010)
Shellcode com um Compilador C por Didier Stevens (2010)
Escrevendo Shellcode Otimizado para Windows em C por Matt Graeber (2013)
Shellcode do melhor jeito, ou como simplesmente usar seu compilador por Justin Fisher (2016)
ShellcodeStdio por Jack Ullrich (2016)
Shellcode: Um PIC para Windows usando troca de chaves RSA-2048, AES-256, SHA-3 por Odzhan (2016)
Escrevendo Shellcode Otimizado para Windows por Dimitri Fourny (2017)
Escrevendo e Compilando Shellcode em C por Aleksandra Doniec e Mantvydas Baranauskas (2021)
Criando Shellcode a partir de qualquer Código Usando Visual Studio e C++ por Hamid Memar (2021)
Escrevendo Shellcode Otimizado para Windows em C por Philip Woldhek (2021)
De C, com assembly inline, a shellcode por Steve Salinas (2023)
Como Criar Seu Próprio Shellcode Windows x86/64 com Visual Studio por Yazid Benjamaa (2023)
Design moderno de implantes: desenvolvimento de malware independente de posição por Paul Ungur (2024)
Do C ao shellcode (maneira simples) por Print3M (2024)
relocatable por Tijme Gommers (2025)
Curso Intensivo de Desenvolvimento de PIC por Raphael Mudge (2025)
scfw por Petr Beneš (2026)
O que me pareceu estranho foi a classificação VirTool:Win64/Silepesz.A pelo Microsoft Defender.
Estes são os bytes que a assinatura cobre:
[+] Target file size: 2560 bytes
[+] Analyzing...
[!] Identified end of bad bytes at offset 0x4CB
000003CB 44 24 2F 32 C6 44 24 30 2E C6 44 24 31 64 C6 44 D$/2�D$0.�D$1d�D
000003DB 24 32 6C C6 44 24 33 6C C6 44 24 35 61 C6 44 24 $2l�D$3l�D$5a�D$
000003EB 36 6E C6 44 24 37 20 C6 44 24 38 65 C6 44 24 39 6n�D$7 �D$8e�D$9
000003FB 78 C6 44 24 3A 74 C6 44 24 3B 72 C6 44 24 3C 61 x�D$:t�D$;r�D$<a
0000040B C6 44 24 3D 20 C6 44 24 3E 6C C6 44 24 3F 6F C6 �D$= �D$>l�D$?o�
0000041B 44 24 40 6E C6 44 24 41 67 C6 44 24 42 20 C6 44 D$@n�D$Ag�D$B �D
0000042B 24 43 68 C6 44 24 44 65 C6 44 24 45 6C C6 44 24 $Ch�D$De�D$El�D$
0000043B 46 6C C6 44 24 47 6F C6 44 24 48 20 C6 44 24 29 Fl�D$Go�D$H �D$)
0000044B 00 C6 44 24 49 77 C6 44 24 4A 6F C6 44 24 4B 72 .�D$Iw�D$Jo�D$Kr
0000045B C6 44 24 4C 6C C6 44 24 4D 64 C6 44 24 4E 21 C6 �D$Ll�D$Md�D$N!�
0000046B 44 24 25 44 C6 44 24 26 65 C6 44 24 27 6D C6 44 D$%D�D$&e�D$'m�D
0000047B 24 28 6F E8 55 00 00 00 48 85 C0 74 4B 48 BA 58 $(o�U...H.AtKH�X
0000048B D0 CC C6 F8 E7 BF 0A 48 89 C1 E8 86 FD FF FF 48 DI�o��.H.A�.y��H
0000049B 85 C0 74 34 48 8D 4C 24 2A FF D0 48 85 C0 74 28 .At4H.L$*�DH.At(
000004AB 48 BA D9 92 FB 55 9A AC 70 E0 48 89 C1 E8 63 FD H�U.�U.�p�H.A�cy
000004BB FF FF 48 85 C0 74 11 48 8D 54 24 35 45 31 C9 4C ��H.At.H.T$5E1�L
Isso se traduz de volta para o seguinte código-fonte:
pKernel32 = GET_USER_MODULE_BASE("kernel32.dll");
if (pKernel32 == nullptr)
goto cleanup;
RESOLVE_FUNCTION_POINTER(pKernel32, LoadLibraryA); // mov rdx, 0x0ABFE7F8C6CCD058 (FNV-1a hash of "LoadLibraryA" with modified offset basis)
if (LoadLibraryA == nullptr)
goto cleanup;
hUser32 = LoadLibraryA(sstrUser32.data());
if (hUser32 == nullptr)
goto cleanup;
RESOLVE_FUNCTION_POINTER(hUser32, MessageBoxA); // mov rdx, 0xE070AC9A55FB92D9 (FNV-1a hash of "MessageBoxA" with modified offset basis)
if (MessageBoxA == nullptr)
goto cleanup;
MessageBoxA(nullptr, sstrText.data(), sstrCaption.data(), MB_OK); // arg setup only
Escusado será dizer que esta é uma detecção extremamente frágil e só detectará o código de exemplo exato mostrado no exemplo. No entanto, ela destaca a importância de usar hashes polimórficos de API.