web-dev-qa-db-ja.com

Iceswordルートキット検出器、それはどのように機能しますか?

アイスソードルートキット検出器についてもっと知りたいのですが。

私はそれを使っただけで、自分で勉強したことはありません。私はいつもそれがどのように機能するのか知りたいと思っていました。私が理解している限り、メモリ内のさまざまなウィンドウのデータ構造を直接調べて、その結果をカーネルが返すものと比較しますか?これは本当ですか? (私の推測では、私は正しくない.....または部分的に正しい)。

誰かが私にそれがどのように/なぜ機能するのか説明できますか?

6
user11869

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]などの命令を実行して、テーブル内のnth APIを呼び出すことができます。

メモリ内のIATのアドレスを置き換えると、プログラムでAPIの代わりに独自のコードを呼び出すことができます。この場合、プロセスを非表示にする最も簡単な方法は、Process32FirstProcess32Nextをフックすることです。実行可能ファイルのヘッダーを解析すると、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;
}

ここで何をしているのですか?

  1. Process32FirstProcess32Nextのラッパー関数を実装して、現在実行しようとしているプロセスをスキップします。
  2. IATから元の関数のアドレスを取得します。
  3. IATのアドレスをフックのアドレスで上書きします。

つまり、プログラムがフックされたAPIを呼び出そうとすると、実際にはフックが呼び出されます。次に、フックは元の関数を呼び出し、結果を操作します。

では、どのようにしてルートキット検出器がこれらのフックを見つけるのでしょうか?いくつかの方法があります:

  • 実行可能ファイルのインポートテーブルに従って、メモリ内の各プロセスのIATを反復処理し、アドレスをあるべきアドレス(= /// =)と比較します。これの欠点は、ルートキット検出器自体にメモリ内のIATパッチが適用されている場合、ルートキットはメモリとファイル読み取り関数の結果を単に操作できることです。
  • 非標準の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に格納されているアドレスと比較することで、この方法で識別できます。

うまくいけば、これによりルートキットがどのように機能し、どのようにルートキットを探すことができるかについて、より包括的な理解が得られます。

参考文献:

9
Polynomial