v1.1プロジェクト SILVERPICK は、Windows ユーザーモードシェルコード開発フレームワーク (WUMSDF) であり、その唯一の目的は、ケイパビリティ開発者が C/C++ を使用して Windows x64 向けの Position Independent Code (PIC) ブロブを容易に構築できるようにし、そのような取り組みの開発コストを削減することです。
これはプロジェクト WILDBEAST から派生しており、以下のものを活用します:
Visual Studio CodeMinGW-w64 ツールチェーンGNU Makeセットアップ手順はこちらから確認できます:GCC-Clang-Setup-Windows
このプロジェクトは MSYS2 を使用していることに注意してください。
高水準プログラミング言語でシェルコードを書くことは目新しいものではなく、2010年以降、数え切れないほどのブログ記事や研究論文が公開されてきました。では、SILVERPICK の新しさは何でしょうか?
それは良い質問ですね。
SILVERPICK には、袖に隠した素敵なトリックの袋がありますが、何よりも、これは私なりのこのテーマへの取り組みです。
それでは、前置きはこれくらいにして、最初のトリックを紹介します。
Matt Graeber が C でシェルコードを書くことを広めて以来、多くの人々が彼の Assembly 言語で書かれた16バイトのスタックアライメントスタブ を使用してきました。
これは問題ではありませんが、私たちは IKEA ではないので、アセンブリは必要ないはずであり、実際に必要ありません。
スタックアライメントスタブを生成してくれる GCC 関数属性 が存在します。
それが、便利な 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 セクションとは何かって? それは、私の2番目のトリックへの良い導入となります。
Matt Graeber がある時点で C によるシェルコード作成を広めたかもしれませんが、実際にこの黒魔術を復活させたのは、Stardust を生み出した Paul Ungur です。
さて、Stardust は Binutils リンカスクリプト を使用して、関数とデータを適切な PE セクションに正しい順序で配置します。この手法自体は Austin Hudson の作品に由来しており、多くの人々が彼のリンカスクリプトの変種を使用しています。
リンカスクリプトはリンカのセクション順序付けには最適ですが、特定の関数をコードセクションの先頭に配置するだけであれば、それらは不要です。
ここで、.init という特別なセクション名を持つ section 関数属性の出番です。これは、その関数に main() 実行前のランタイム初期化コードが含まれており、リンク順序で 最初 でなければならないことをリンカに示します。
このために、CODE_BEGIN マクロが作成されました。
3番目のトリックとして、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++ と言えば、4番目のトリックはコンパイル時文字列ハッシュです。
これは新しい概念ではありませんが、SILVERPICK は既存の公開実装に対していくつかの改良を提供します。
まず、ハッシュ衝突攻撃が成功する確率を減らすために、一般的な非暗号化ハッシュ関数 FNV-1a の64ビット版を使用します。
次に、HashDB などの事前計算されたハッシュテーブルルックアップ に対抗するために、ハッシュ関数に修正されたパラメータを使用します。重要なのは、これによってハッシュ関数の特性は 変わらない ことです。
実行時に短い文字列をハッシュするには、HASH_STRING_RUN_TIME マクロを使用するだけです。
コンパイル時に短い文字列リテラルをハッシュするには、HASH_STRING_COMPILE_TIME マクロを使用するだけです。コンパイル時のみの評価は consteval によって保証されます。
x86 文字列命令を使用して、かなりの数の C ランタイムライブラリ (CRT) 関数を実装できることがわかりました。そこで当然、私はコンパイラ組み込み関数とインラインアセンブリを組み合わせてそれらを実装することにしました。
コード内で msvcrt!memset 関数を使用したいですか? 代わりに ZERO_MEMORY マクロを使用してください。これはコンパイラ組み込み関数を介して生成された rep stosb 命令を使用します。
msvcrt!memcpy 関数や msvcrt!memmove 関数はどうかって? 代わりとなる COPY_MEMORY マクロを紹介します。これはコンパイラ組み込み関数を介して生成された rep movsb 命令を使用します。
では、msvcrt!memcmp 関数の代替はどうでしょうか? repe cmpsb 命令を生成するためのコンパイラ組み込み関数は正確には存在しないことがわかります。そこで、代わりにインラインアセンブリを使用した compare_memory 関数を記述します。
最後に、msvcrt!memchr 関数の置き換えをお探しなら、scan_memory 関数を紹介します。これもまた、repne scasb 命令を生成するコンパイラ組み込み関数がないため、インラインアセンブリを使用します。
ああ、それから、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 という適切な名前の便利なマクロにラップされています。さらに、実行時動的リンクを支援するための2つのマクロが用意されています - 関数ポインタを0に宣言して初期化する INITIALIZE_FUNCTION_POINTER と、その関数ポインタを解決する RESOLVE_FUNCTION_POINTER です。
Visual Studio Code 統合 がプロジェクトに組み込まれているため、開発者は Ctrl+Shift+B キーボードショートカットを使用して、手間のかからないビルドプロセスを実行できます。
GitHub Actions 統合 もプロジェクトに組み込まれており、CI ビルドを可能にします。
このプロジェクトは、よく整理された構造と、徹底的にコメントされ、比較的クリーンなコードにいくらかの誇りを持っています。
最後に、プロジェクトの Makefile をひと目見てください。そこには、小型で安全かつ OPSEC フレンドリーなコードを生成する、厳選されたコンパイラフラグとリンカフラグが含まれています。その一方で、冗長なログと生成されたリンカマップファイルは、ビルドプロセスへの可視性を提供し、ツールチェーンのより深い理解を促進します。さらに、各翻訳単位は逆アセンブルファイルも生成します。これを調べると、しばしば「コンパイラは今何をしたんだ?」といった感想を抱くことでしょう。
このフレームワークに魅力を感じたら、このセクションではその使用方法を説明します。
以下は 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) エクスプロイト緩和をバイパスする機能。以下は、私の調査中に非常に貴重であることが判明し、このプロジェクトのインスピレーションとして広く使用された、時系列順に並べた参考文献のリストです:
Cコンパイラでシェルコードを書く 著: Nick Harbour (2010)
Cコンパイラによるシェルコード 著: Didier Stevens (2010)
Cで最適化されたWindowsシェルコードを書く 著: Matt Graeber (2013)
より良いシェルコードの書き方、または単にコンパイラを使う方法 著: Justin Fisher (2016)
ShellcodeStdio 著: Jack Ullrich (2016)
シェルコード: RSA-2048鍵交換、AES-256、SHA-3を使用したWindows PIC 著: Odzhan (2016)
最適化されたWindowsシェルコードを書く 著: Dimitri Fourny (2017)
Cでのシェルコードの作成とコンパイル 著: Aleksandra Doniec と Mantvydas Baranauskas (2021)
Visual StudioとC++を使用して任意のコードからシェルコードを作成する 著: Hamid Memar (2021)
Cで最適化されたWindowsシェルコードを書く 著: Philip Woldhek (2021)
Cからインラインアセンブリを経てシェルコードへ 著: Steve Salinas (2023)
Visual Studioで独自のWindows x86/64シェルコードを作成する方法 著: Yazid Benjamaa (2023)
モダンインプラント設計:位置独立型マルウェア開発 著: Paul Ungur (2024)
Cからシェルコードへ(簡単な方法) 著: Print3M (2024)
著: Tijme Gommers (2025)
私が奇妙に思ったのは、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 ハッシュを使用することの重要性を浮き彫りにします。
PIC開発クラッシュコース 著: Raphael Mudge (2025)
scfw 著: Petr Beneš (2026)