株式会社Ninjastars 技術系ブログ

「本質的安全を提供し、デジタル社会を進化させる!!!」

Windows int3アンチデバッグの原理

株式会社Ninjastars
セキュリティエンジニア:一瀬健二郎

今回はWindowsでのアンチデバッグとして入門的な手法であるint3によるデバッガ検出についてお話しさせていただきます。
実際に本質的な部分だけ取り出した原始的なデバッガを作成し、何故int3でデバッガを検出できるのか解説いたします。
アンチデバッグとしての実効性はほぼありませんが、デバッガの原理の理解の手助けとなれば幸いです。

f:id:Ninjastars:20200122060926p:plain
int3アンチデバッグを理解しよう!

環境

Windows10
Visual Studio C++開発環境

今回やること

int3アンチデバッグを実装したプログラムを簡易デバッガでアタッチして何故検出されるのか考えます。

導入

まずint3.cppとdebugger.cppをVisualStudioでビルドしてください。

プログラムの説明
int3.exe
1.起動すると自身のプロセスIDを出力。
2.入力待機
3.デバッグされていれば「detect!」、されていなければ「not detect!」を出力する。

debugger.exe
1.入力したプロセスIDのプロセスにアタッチ。
2.デバッグイベントが発生するまで待機。以下ループ。
3a例外が発生したら例外番号とプロセスID、スレッドIDを出力して実行を再開。この際例外は対象プロセスに渡さない。
3b.発生したデバッグイベントが例外でなかったら、対象プロセスにそのまま渡す。

実験

(1)検知される場合
1.int3.exeとdebugger.exeを起動して出力されたint3.exeに出力されたプロセスIDをdebugger.exeに入力してください。
2.int3.exeでEnterを押すと「detect!」と出力されデバッガが検知されます。

(2)検知されない場合

42行目を以下のように書き換え。(1から2へ)
1.ContinueDebugEvent(debug_event.dwProcessId, debug_event.dwThreadId, DBG_CONTINUE);
2.ContinueDebugEvent(debug_event.dwProcessId, debug_event.dwThreadId, DBG_EXCEPTION_NOT_HANDLED);

debugger.cppを上記のように再度書き換えビルドし、再度1と同じことをしてください。
今度は「not detect!」と出力されると思います。

原理

デバッガはソフトウェアブレークポイントを設置するとき、x86・x64アーキテクチャの場合int3(0xCC)をメモリの該当アドレスに書き込んで設置します。
当該プログラムでint3に処理が移ったとき、例外が発生し処理がデバッガに移ります。
デバッガはレジスタやメモリの値を取得するなどした後、ContinueDebugEventの第三引数をDBG_CONTINUEにして実行を再開します。
これは捕捉した例外を対象プロセスに渡さずに実行を再開するというものです。
これを逆手にとってint3アンチデバッグでは例外が発生する前提でプログラムを実装しておき、本来発生するはずの例外が発生しなかったら検知するというものです。

ただしデバッガ自身が設置したソフトウェアブレークポイントのアドレスを保存しておき、自身が設置したものか否かを判断して実験のようにContinueDebugEventに渡す引数を変えれば簡単に回避可能です。

まとめ

デバッガは例外を捕捉してそのまま渡したり無かったことにしたりするなど、イメージ的には通信をインターセプトするプロキシツールのようなことが出来ます。
またブレークポイントを設置してのレジスタの取得・操作やプロセスメモリの取得・書き換えなど大変便利なことが可能です。
勿論これはそのままアプリケーションやゲームに対する悪意ある攻撃にも利用可能です。
弊社ではデバッガの原理を理解し解析や診断に役立てるとともに、悪意ある攻撃に対する対策を施すためにもこういった勉強は継続的に行っています。
読者の皆様に置かれましても、今回の記事がそうしたことに少しでも役立てば幸いです。

ソースコード

int3.cpp

#include <windows.h> 
#include <iostream>

using namespace std;

int main()
{
    //自身のプロセスIDを出力
    cout << "PID:" << GetCurrentProcessId() << endl;
    getchar();

    int isDebug = 1;
    __try
    {
        __asm int 3
    }
    __except(EXCEPTION_EXECUTE_HANDLER)
    {
        isDebug = 0;
    }
    
    if (isDebug)
    {
        cout << "detect!" << endl;
    }
    else
    {
        cout << "not detect!" << endl;
    }
    getchar();
    return 0;
}

debugger.cpp

#include <iostream>
#include <windows.h>

using namespace std;

int main()
{
    DWORD pid;
    cout << "Please input PID." << endl;
    cin >> pid;

    //対象プロセスにアタッチする。
    HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, false, pid);
    if (hProcess == NULL) {
        cout << "OpenProcess Error" << endl;
        ExitProcess(-1);
        return -1;
    }

    if (!DebugActiveProcess(pid))
    {
        cout << "DebugActiveProcess Error" << endl;
        ExitProcess(-1);
        return -1;
    }

    cout << "PID:" << pid << endl;
    cout << "HANDLE:" << hProcess << endl;

    DEBUG_EVENT debug_event;
    while (true)
    {
        //デバッグイベントが発生するまで待機。
        if (WaitForDebugEvent(&debug_event, INFINITE))
        {
            if (debug_event.dwDebugEventCode == EXCEPTION_DEBUG_EVENT)
            {
                cout << "DebugEventCode:" << debug_event.dwDebugEventCode << endl;
                cout << "pid:" << debug_event.dwProcessId << endl;
                cout << "tid:" << debug_event.dwThreadId << endl;
                //例外をなかったことにして実行を再開。
                ContinueDebugEvent(debug_event.dwProcessId, debug_event.dwThreadId, DBG_CONTINUE);
            }
            else
            {
                //デバッグイベントをそのまま渡して実行を再開。
                ContinueDebugEvent(debug_event.dwProcessId, debug_event.dwThreadId, DBG_EXCEPTION_NOT_HANDLED);
            }
        }

    }
    return 0;
}

注意事項

本レポートに記載されている内容を許可されていないソフトウェアで行うと、場合によっては犯罪行為となる可能性があります。そのため、記事の内容を試す際には許可されたソフトウェアに対してのみ実施するようにしてください。

本レポートについて
お問い合せ
E-mail:ichise@ninjastars-net.com

株式会社Ninjastarsエンジニア
一瀬健二郎