アイスソードルートキット検出器についてもっと知りたいのですが。
私はそれを使っただけで、自分で勉強したことはありません。私はいつもそれがどのように機能するのか知りたいと思っていました。私が理解している限り、メモリ内のさまざまなウィンドウのデータ構造を直接調べて、その結果をカーネルが返すものと比較しますか?これは本当ですか? (私の推測では、私は正しくない.....または部分的に正しい)。
誰かが私にそれがどのように/なぜ機能するのか説明できますか?
tl; dr-同じことを行う2つの関数の結果を比較し、違いを探します。
その単一のルートキットスキャナーに焦点を当てるのではなく、ルートキットが使用する一般的な手法と、それらを見つける方法について説明します。これにより、関連する課題の概要がわかりやすくなります。
ルートキットは、特定のシステムコールをインターセプトし、それらのパラメーターまたは結果を変更することによって機能します。フックの仕組みを説明せずに、ルートキットファインダーの仕組みを説明するのは困難です。
たとえば、Windowsでは、CreateToolhelp32Snapshot
を呼び出すと、現在実行中のプロセスのスナップショットが作成され、グローバルヒープに保存されます。 Process32First
関数とProcess32Next
関数を使用すると、アプリケーションはプロセスのリストを反復処理できます。
ここに簡単な例があります:
PROCESSENTRY32 proc;
procList = CreateToolhelp32Snapshot(flags, 0);
Process32First(procList, &proc); // seek to first item in list and store it in proc
do {
printf("Process ID: %d\n", proc.th32ProcessID);
printf("Thread count: %d\n", proc.th32Threads);
printf("Path: %s\n", proc.szExeFile);
printf("-----\n");
}
while (Process32Next(procList, &proc));
ここで、特定のプロセスをリストから非表示にしたいとします。これを行うには、主に2つの方法があります。ユーザーモードとカーネルモードです。 1つ目(ユーザーモード)は、スプーフィングするプロセスのインポートアドレステーブル(IAT)をフックすることです。つまり、非表示のプロセスを表示したくないプロセスです。他の方法もありますが、これが最も簡単に説明できます。
プログラムをコンパイルすると、実行可能ファイルには、DLLインポート元の名前、そのDLL内のAPIの名前、およびその相対仮想名で構成されるインポートのリストが含まれますアドレス(RVA)これはすべて PEInfo などのツールで表示できるインポートディレクトリに保存されます。
プログラムが実行されると、実行可能ファイルがメモリにロードされます。カーネルはインポートリストを調べ、メモリにロードする必要のあるDLLと、すでに共有メモリにあるDLLを識別します。これが完了すると、メモリ内のインポートされた関数のアドレスが(RVAまたは名前で)検出され、メモリ内のRVA値にそれらのアドレスが書き込まれます。その結果、IATはインポートされたすべてのAPIに対して大きなジャンプテーブルのように機能するため、プログラムはcall [addrOfIAT + n*4]
などの命令を実行して、テーブル内のn
th APIを呼び出すことができます。
メモリ内のIATのアドレスを置き換えると、プログラムでAPIの代わりに独自のコードを呼び出すことができます。この場合、プロセスを非表示にする最も簡単な方法は、Process32First
とProcess32Next
をフックすることです。実行可能ファイルのヘッダーを解析すると、IATが読み込まれるアドレスと、IAT内のAPIのオフセットを見つけることができます。それがわかったら、IATをフックできます。それを行うにはたくさんの方法があります-IATメモリを直接上書きする、それを行うためのコードを注入する、またはDLLを注入する。どのように行うかは重要ではなく、この回答の範囲外です。目標は、IATからアドレスをコピーし、そのアドレスを独自のコードのアドレスで上書きすることです。この場合、次の擬似コードを想像してみましょう:
void* Process32FirstOriginal;
void* Process32NextOriginal;
const int HiddenProcessId = 1234; // ID of the process we want to hide
void InstallHook()
{
int offsetP32First = 0x40; // offsets in the IAT
int offsetP32Next = 0x44;
// make a backup of the actual API addresses
// this isn't actually how we'd access the IAT, I'm just being simplistic
Process32FirstOriginal = IAT[offsetP32First];
Process32NextOriginal = IAT[offsetP32Next];
// patch the IAT with the address of our hooks
IAT[offsetP32First] = &Process32FirstHooked;
IAT[offsetP32Next] = &Process32NextHooked;
}
int Process32FirstHooked(void* snapshot, PROCESSENTRY32* proc)
{
// call the original function
int result = Process32FirstOriginal(snapshot, proc);
// did we just fetch the process we're trying to hide?
if (proc.th32ProcessId == HiddenProcessId)
{
// skip the process we're trying to hide and get the next one
result = Process32NextHooked(snapshot, proc);
}
return result;
}
int Process32NextHooked(void* snapshot, PROCESSENTRY32* proc)
{
int result = Process32NextOriginal(snapshot, proc);
if (proc.th32ProcessId == HiddenProcessId)
{
result = Process32NextHooked(snapshot, proc);
}
return result;
}
ここで何をしているのですか?
Process32First
とProcess32Next
のラッパー関数を実装して、現在実行しようとしているプロセスをスキップします。つまり、プログラムがフックされたAPIを呼び出そうとすると、実際にはフックが呼び出されます。次に、フックは元の関数を呼び出し、結果を操作します。
では、どのようにしてルートキット検出器がこれらのフックを見つけるのでしょうか?いくつかの方法があります:
ntdll
関数など)。これは、ユーザーモードルートキットがこれらにパッチを適用しない限り機能します。ルートキットの2番目のタイプ、カーネルモードが登場しました。この場合、ルートキットはほぼ同じことを行いますが、ユーザーモードからカーネルモードの呼び出しにサービスを提供するIATではなく、システムサービスディスパッチテーブル(SSDT)をフックします。 SSDTは基本的にIATと同じですが、すべてのカーネルモードAPIのアドレスが含まれています。ルートキットは、CreateToolhelp32Snapshot
呼び出しのサービスを担当するカーネルAPIをフックし、非表示にするプロセスを除外します。これにより、スキャナーが検出した結果もフックされるため、通常の不一致スキャンが機能しなくなります。
では、カーネルモードのルートキットをスキャンするにはどうすればよいでしょうか。答えは、難しいことです。ルートキットがSSDTをフックしている場合、それに頼ることはできません。したがって、カーネルオブジェクトの読み取りと操作を行うには、独自のバージョンのカーネルAPIを実装する必要があります。これは、カーネルオブジェクトの直接変更(DKOM)と呼ばれます。これらのオブジェクトは通常文書化されていないか、部分的にしか文書化されておらず、Windowsのバージョン間で変更される可能性があるため、これはトリッキーです。 Windowsカーネルのプロセスは、二重リンクリストのEPROCESS
構造体で表されます。
// get the EPROCESS struct for the current executing process
EPROCESS* eproc = PsGetCurrentProcess();
// get the LIST_ENTRY item for the EPROCESS, so we can iterate the linked list
LIST_ENTRY currentEntry = eproc->ActiveProcessLinks;
// store the first pID, so we know when we've looped the list
DWORD startPID = (DWORD) eproc->UniqueProcessId;
int count = 0;
while(1)
{
// find the EPROCESS structure from the LIST_ENTRY object
eproc = (EPROCESS*)((DWORD)currentEntry - OFFSET_LIST_FLINK);
// are we at the end of the list?
if (count > 0 && eproc->UniqueProcessId == startPID)
{
// we've gone through the whole list!
KdPrint("END\n");
break;
}
// print the process ID to the debugger
KdPrint("Process ID: %d\n", eproc->UniqueProcessId);
// go to the next entry
currentEntry = *currentEntry.FLink;
count++;
}
次に、このプロセスIDのリストを通常のカーネルモードおよびユーザーモードAPIによって生成されたリストと比較して、どのプロセスが非表示になっていて、どこでフックが行われたかを確認できます。
残念ながら、マルウェアにはこれを行う同じ機能があります。リストから非表示のプロセスのプロセスを削除できます。
void HideProcess(EPROCESS* proc)
{
LIST_ENTRY hideEntry = eproc->ActiveProcessLinks;
// get the previous and next list entries
LIST_ENTRY prevEntry = *hideEntry.BLink;
LIST_ENTRY nextEntry = *hideEntry.Flink;
// set their forward and backward links to skip over the hidden entry
prevEntry.FLink = &nextEntry;
nextEntry.BLink = &prevEntry;
// set the hidden entry's forward and backward links to itself
hideEntry.FLink = &hideEntry;
hideEntry.BLink = &hideEntry;
}
これにより、リストからプロセスが効果的に削除され、スキャンに対する以前のDKOMアプローチが不可能になります。
ここから、私たちができる唯一のアプローチは、アーティファクトスキャンの武装競争です。これには、隠しオブジェクトを潜在的に参照しているカーネルオブジェクトを識別して、それらを識別することが含まれます。あるいは、異常な値を特定するために、EPROCESS
エントリのようなオブジェクトまたは他のカーネルオブジェクトをメモリでスキャンすることもできます。 SSDTフックは、メモリ内の実際のAPI(シグネチャでスキャン)を検索し、それらの実際のアドレスをSSDTに格納されているアドレスと比較することで、この方法で識別できます。
うまくいけば、これによりルートキットがどのように機能し、どのようにルートキットを探すことができるかについて、より包括的な理解が得られます。
参考文献: