
Windows 사용자 모드 셸코드 개발 프레임워크 (WUMSDF)
v1.1SILVERPICK 프로젝트는 Windows User-Mode Shellcode Development Framework (WUMSDF)이며, 그 유일한 목적은 기능 개발자(capability developers)가 C/C++를 사용하여 Windows x64용 Position Independent Code (PIC) blob을 쉽게 빌드할 수 있게 하여 그러한 작업의 개발 비용을 줄이는 것입니다.
이 프로젝트는 WILDBEAST 프로젝트에서 파생되었으며, 따라서 다음을 활용합니다:
Visual Studio CodeMinGW-w64GNU Make설치 지침은 여기에서 확인할 수 있습니다: GCC-Clang-Setup-Windows
이 프로젝트는 MSYS2를 사용한다는 점에 유의하세요.
고수준 프로그래밍 언어로 셸코드를 작성하는 것은 새로운 일이 아니며, 2010년부터 이와 관련된 수많은 블로그 게시물과 연구 논문이 발표되었습니다. 그렇다면 SILVERPICK에는 무엇이 새로운 것일까요?
글쎄요, 그렇게 물어봐 주셔서 기쁩니다.
SILVERPICK은 비장의 멋진 작은 트릭들을 숨겨두고 있습니다. 하지만 무엇보다도, 이것은 이 주제에 대한 저만의 해석입니다.
그럼, 더 지체하지 않고 첫 번째 트릭을 소개하겠습니다.
Matt Graeber가 C로 셸코드를 작성하는 것을 대중화한 이후로, 대부분의 사람들은 그가 작성한 Assembly 언어로 쓰인 16바이트 스택 정렬 스텁을 사용해 왔습니다.
이것이 문제가 되는 것은 아니지만, 우리가 IKEA가 아니라는 점을 고려하면 어셈블리는 필요하지 않아야 하며, 실제로도 필요하지 않습니다.
여러분을 위해 스택 정렬 스텁을 생성해 주는 GCC 함수 속성(Function Attribute)이 존재합니다.
도움이 되는 ALIGN_STACK 매크로 형태의 force_align_arg_pointer 함수 속성을 만나보세요. 이 매크로는 다음 어셈블리를 생성합니다:
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
.init 섹션이 무엇이냐고 물으신다면? 글쎄요, 그것은 제 두 번째 트릭으로 이어지는 좋은 다리가 되어 줍니다.
Matt Graeber가 한때 C로 셸코드를 작성하는 것을 대중화했을지 모르지만, 실제로 이 흑마법을 Stardust로 부활시킨 사람은 Paul Ungur였습니다.
이제, Stardust는 Binutils 링커 스크립트를 사용하여 함수와 데이터가 올바른 순서로 적절한 PE 섹션에 배치되도록 제어합니다. 이 기법 자체는 Austin Hudson의 작업에서 파생된 것이며, 많은 사람들이 그의 링커 스크립트 변형을 사용합니다.
링커 스크립트는 링커 섹션 순서를 지정하는 데 훌륭하지만, 특정 함수를 코드 섹션의 시작 부분에 배치하기만 하면 되는 경우에는 불필요합니다.
여기서 등장하는 것이 .init이라는 특수 섹션 이름을 가진 section 함수 속성입니다. 이 속성은 링커에게 해당 함수가 main() 이전의 런타임 초기화 코드를 포함하고 있으며 링크 순서에서 _첫 번째_여야 한다는 것을 알려줍니다.
이를 위해 CODE_BEGIN 매크로가 만들어졌습니다.
세 번째 트릭으로 STACK_STRING 매크로를 소개합니다.
C에서는 문자열 리터럴을 ANSI 문자의 배열로 선언하여 스택 문자열(스택에 동적으로 구축되는 문자열)을 만들 수 있습니다:
char charrHelloKitty[] = { 'H', 'e', 'l', 'l', 'o', 'K', 'i', 't', 't', 'y', '\0' };
C++에서는 char 배열을 constexpr로 표시하기만 하면 스택 문자열을 만들 수 있습니다:
constexpr char charrHelloKitty[]{ "HelloKitty" };
그러나 컴파일러 최적화 앞에서는 이 두 기법 모두 문자열 리터럴이 충분히 큰 경우 무용지물이 됩니다. 반면 우리의 솔루션은 Can Bölük이 제공한 영리한 C++ 템플릿 메타프로그래밍 해킹 덕분에 문자열 길이와 컴파일러 최적화 수준에 관계없이 작동합니다.
이 매크로 사용법은 매우 간단합니다:
STACK_STRING(sstrText, "an extra long hello world!");
STACK_STRING(sstrCaption, "Demo");
MessageBoxA(nullptr, sstrText.data(), sstrCaption.data(), MB_OK);
이것은 다음 어셈블리를 생성합니다:
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'
C++에 관해 말하자면, 네 번째 트릭으로 컴파일 타임 문자열 해싱을 소개합니다.
이것이 새로운 개념은 아니지만, SILVERPICK은 기존 공개 구현에 비해 몇 가지 개선점을 제공합니다.
첫째, 해시 충돌 공격의 성공 확률을 줄이기 위해 널리 사용되는 비암호화 해시 함수인 FNV-1a의 64비트 변형을 사용합니다.
둘째, 사전 계산된 해시 테이블 조회 공격(예: HashDB)을 방어하기 위해 해시 함수에 수정된 매개변수를 사용합니다. 중요하게도, 이것은 해시 함수의 속성을 변경하지 않습니다.
런타임에 짧은 문자열을 해싱하려면 HASH_STRING_RUN_TIME 매크로를 사용하면 됩니다.
컴파일 타임에 짧은 문자열 리터럴을 해싱하려면 HASH_STRING_COMPILE_TIME 매크로를 사용하면 됩니다. 컴파일 타임 전용 평가는 consteval을 통해 보장됩니다.
x86 문자열 명령어로 상당히 많은 C Runtime Library (CRT) 함수를 구현할 수 있다는 것이 밝혀졌습니다. 그래서 당연하게도 저는 컴파일러 내장 함수(intrinsics)와 인라인 어셈블리를 혼합하여 이를 구현해야 했습니다.
코드에서 msvcrt!memset 함수를 사용하고 싶으신가요? 대신 컴파일러 내장 함수를 통해 생성되는 rep stosb 명령어를 사용하는 ZERO_MEMORY 매크로를 사용하세요.
msvcrt!memcpy 함수나 msvcrt!memmove 함수는 어떨까요? 컴파일러 내장 함수를 통해 생성되는 rep movsb 명령어를 사용하는 COPY_MEMORY 매크로를 대체재로 만나보세요.
그렇다면 msvcrt!memcmp 함수의 대안은 어떨까요? repe cmpsb 명령어를 생성할 수 있는 컴파일러 내장 함수가 정확히 존재하지 않는다는 것이 밝혀졌습니다. 그래서 대신 인라인 어셈블리를 사용하여 compare_memory 함수를 작성합니다.
마지막으로, msvcrt!memchr 함수의 대체재를 찾고 있다면, repne scasb 명령어를 생성할 수 있는 컴파일러 내장 함수가 없으므로 다시 인라인 어셈블리를 사용하는 scan_memory 함수를 만나보세요.
아, 그리고 scan_memory 루틴을 사용하여 msvcrt!strlen 함수의 더 안전한 자체 버전을 다음과 같이 작성할 수 있다는 점도 언급하는 것을 잊었나요:
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);
이 구현들은 대상 CPU 마이크로아키텍처에 따라 가장 성능이 좋은 코드를 생성하지 못할 수 있다는 점에 유의하세요. 그러나 작업을 완료하는 것은 보장됩니다.
좀 더 재미있는 트릭에 관심이 있으신가요?
Common.h에는 컴파일러를 다루는 복잡성을 추상화하기 위한 다른 작은 매크로들이 많이 있습니다.
의존성이 없는 GetModuleHandle 함수 구현이 UserModuleBase.cpp에 제공됩니다. 사용 편의성을 높이기 위해 GET_USER_MODULE_BASE라는 유용한 매크로가 만들어졌습니다.
마찬가지로, 의존성이 없는 GetProcAddress 함수 구현이 PEParse.cpp에 제공되며, 이는 GET_EXPORTED_SYMBOL_ADDRESS라는 적절한 이름의 편리한 매크로로 래핑됩니다. 또한 런타임 동적 링크를 지원하기 위해 두 개의 매크로가 더 제공됩니다 - 함수 포인터를 0으로 선언하고 초기화하는 INITIALIZE_FUNCTION_POINTER와 해당 함수 포인터를 해석하는 RESOLVE_FUNCTION_POINTER입니다.
개발자들이 번거로움 없는 빌드 프로세스를 위해 Ctrl+Shift+B 키보드 단축키를 사용할 수 있도록 Visual Studio Code 통합이 프로젝트에 내장되어 있습니다.
CI 빌드를 활성화하기 위해 GitHub Actions 통합도 프로젝트에 내장되어 있습니다.
이 프로젝트는 체계적으로 정리된 구조와 철저히 주석 처리되고 비교적 깨끗한 코드에 대해 어느 정도 자부심을 가지고 있습니다.
마지막으로, 프로젝트 Makefile을 한번 살펴보세요. 작고 안전하며 OPSEC 친화적인 코드를 생성할 최상의 컴파일러 및 링커 플래그 선택이 포함되어 있습니다. 한편, 상세 로깅과 생성된 링커 맵 파일은 빌드 프로세스에 대한 가시성을 제공하여 툴체인에 대한 더 깊은 이해를 돕습니다. 또한 각 번역 단위(translation unit)는 디스어셈블리 파일도 생성하는데, 이를 살펴보면 "컴파일러가 방금 뭘 한 거지?" 같은 말을 자주 하게 될 것입니다.
이 프레임워크가 마음에 드셨다면, 이 섹션에서는 이를 어떻게 사용하는지 설명합니다.
다음은 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;
}
충분히 쉬워 보이지 않나요?
SILVERPICK 프레임워크를 사용하여 C/C++로 PIC를 작성할 때 다음 규칙을 고려해야 합니다:
payload 함수를 기존 프로그램의 main 함수처럼, 즉 _(의사) 진입점_으로 취급하십시오.Windows API 또는 Native API 함수는 해당 헤더 파일에서 함수 프로토타입을 사용할 수 있는지 확인한 후 _런타임 동적 링크_를 통해서만 사용할 수 있습니다.이 섹션에는 향후 프로젝트에 통합될 예정인 계획된 개선 사항의 전체 목록이 아닌 일부가 포함되어 있습니다.
Clang/LLVM 툴체인으로 전환.GS 세그먼트 기준 주소를 검색하는 대체 방법.Export Address Filtering (EAF) 익스플로잇 완화를 우회하는 기능.다음은 제 연구 과정에서 매우 귀중하게 사용되었고 이 프로젝트의 영감으로 광범위하게 활용된 참고 자료를 시간순으로 정렬한 목록입니다:
Writing Shellcode with a C Compiler Nick Harbour (2010) 저
Shellcode with a C-compiler Didier Stevens (2010) 저
Writing Optimized Windows Shellcode in C Matt Graeber (2013) 저
Shellcode the better way, or how to just use your compiler Justin Fisher (2016) 저
ShellcodeStdio Jack Ullrich (2016) 저
Shellcode: A Windows PIC using RSA-2048 key exchange, AES-256, SHA-3 Odzhan (2016) 저
Writing Optimized Windows Shellcode Dimitri Fourny (2017) 저
Writing and Compiling Shellcode in C Aleksandra Doniec and Mantvydas Baranauskas (2021) 저
Creating Shellcode from any Code Using Visual Studio and C++ Hamid Memar (2021) 저
Writing Optimized Windows Shellcode in C Philip Woldhek (2021) 저
From C, with inline assembly, to shellcode Steve Salinas (2023) 저
How To Craft Your Own Windows x86/64 Shellcode with Visual Studio Yazid Benjamaa (2023) 저
Modern implant design: position independent malware development Paul Ungur (2024) 저
제가 이상하게 여긴 것은 Microsoft Defender의 VirTool:Win64/Silepesz.A 분류였습니다.
다음은 해당 시그니처가 포함하는 바이트입니다:
[+] 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
이것은 다시 다음과 같은 소스 코드로 해석됩니다:
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
말할 필요도 없이, 이것은 매우 취약한 탐지이며 예제에 표시된 정확한 샘플 코드만 탐지할 것입니다. 그러나 다형성 API 해시를 사용하는 것의 중요성을 강조하기는 합니다.
From C to shellcode (simple way) Print3M (2024) 저
relocatable Tijme Gommers (2025) 저
PIC Development Crash Course Raphael Mudge (2025) 저
scfw Petr Beneš (2026) 저