TLSスレッドローカルストレージによる逆デバッグ
14567 ワード
[TOC]反海賊版技術の中で、最も大きな役割を果たすのは反デバッグ技術である.しかし、従来のデバッグバックテクノロジーには、プログラムが本当に実行されてからデバッグバック手段を採用する弱点があります.実際には、逆デバッグコードが実行される前に、デバッガはプログラムの実行に影響を与える時間が多く、プログラムの入り口にブレークポイントコマンドを挿入してプログラムをデバッグすることもできます.C/C++言語でコンパイルされたプログラムでは、main()関数を実行する前に、C/C++コンパイラが挿入した大きなコードが実行され、デバッガにプログラムの実行に影響を与える可能性があります.
TLS逆デバッグ
TLS概要
TLS設計の本意は,マルチスレッドプログラムにおける変数同期の問題を解決するためであり,Thread Local Storageの略であり,スレッドローカルストレージを意味する.スレッド自体には他のスレッドとは独立したスタック空間があるため、スレッド内のローカル変数は同期の問題を考慮する必要はありません.マルチスレッド同期の問題はグローバル変数へのアクセスであり、TLSはオペレーティングシステムのサポートの下で、グローバル変数を特殊な節にパッケージ化することによって、スレッドを作成するたびにこの節のデータをコピーとして、プロセスの空きアドレス空間にコピーする.後で、スレッドは、同期制御を追加することなく、ローカル変数にアクセスするように、他のスレッドとは異なるグローバル変数のコピーにアクセスできます.
スレッドにローカルに格納されているグローバル変数のテストコード
実行後、valueがコピーcopyとして3つの異なるスレッドに割り当てられているため、スレッドが値を変更すると、他のスレッドの変数が変化します.
このすべての変数の数値copyを異なるスレッドに変換するプロセスは、スレッド全体が実行される前に実行される[メインスレッドを含む]ので、このスレッドTLSコールバック関数でいくつかのデバッグ検出を行い、プロセスがデバッグされているかどうかを判定し、デバッグが発見された場合、退去または他の操作を行うことができる.
TLSは、相互反発プロセス全体を完了するためにオペレーティングシステムによって簡単に代えることもできるし、ユーザ自身が制御信号量の関数を記述することもできる.プロセス内のスレッドが予め定められたメモリ空間にアクセスすると、オペレーティングシステムはシステムのデフォルトまたはユーザー定義の信号量関数を呼び出し、データの完全性と正確性を保証します.一方、Coderが自分で作成した信号量関数を使用することを選択すると、アプリケーションの初期化フェーズでは、信号量の初期化および他の初期化作業を完了するために、ユーザによって作成された初期化関数が呼び出されます.この呼び出しは、プログラムの実行がエントリポイントに実行される前に完了し、プログラムの実行の正確性を保証する必要があります.
TLSの逆デバッグに基づいて、原理は実際のエントリポイントコードが実行される前に検出デバッガコードを実行し、実現方式はTLSコールバック関数を用いて実現する.
ODダイナミックデバッガは、エントリポイントにプログラムをロードする前に、逆デバッグコードを実行し、プログラムを終了します.さらに、TLSを使用して起動すると、一部のウイルスは、プログラムのメインエントリポイントで切り込まれているため、デバッガが起動する前に実行を開始することもできる.
TLSコールバック関数
TLSコールバック関数の原型は以下の通りである:void NTAPI TlsCallBackFunction(PVOID Handle,DWORD Reason,PVOID Reserve);TLS逆デバッグを実現することは、TLSコールバック関数がプログラムエントリポイントの前にプログラム制御権を得ることができる特性を十分に利用し、一般的な逆デバッグ技術がより効果的になるようにすることである.PE形式では、TLSデータに空間を開き、位置はIMAGE_である.NT_HEADERS.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_TLS]. DataDirectoryの要素には、typedef struct_というデータ構造があります.IMAGE_DATA_DIRECTORY { DWORD VirtualAddress; DWORD Size; } IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
TLSのDataDirectory要素、VirtualAddressメンバーは、相互反発が必要なメモリアドレス、TLSコールバック関数アドレス、およびその他の情報へのアクセスを定義する構造体を指します.
マイクロソフトが提供するVCコンパイラのデフォルトでは、プログラムでTLSを直接使用することがサポートされています.プログラムでTLSを使用するには、まずTLSデータのためにデータセグメントを個別に構築し、関連データでこのセグメントを埋め込み、リンクにTLSデータのPEファイルヘッダにデータを追加するように通知します.
サンプルコード
常用デバッガ検出スキーム
int 3[0 xcc]ブレークポイント検出
PE実行可能ファイルは初期化段階でPEファイルが完全にメモリにロードされます.PEファイルヘッダを解析してプログラムのエントリポイントを取得し、エントリポイントの前のバイトまたは複数のバイトを「そのコードがブレークポイントに設定されているかどうかを検出するために、悪意のあるソフトウェアは命令操作コード0 xccを検索することができる」と判断し、デバッガがプログラムエントリポイントでブレークすることを阻止する.次のコードでは、GetModuleHandle(NULL)を使用してアプリケーションのロード・ベース・アドレスを取得します.GetModuleHandle()手動読取プログラムのPEBをシミュレートして得ることもできる.しかし、プログラムの汎用性のために、ここではGetModuleHandle()が使用されている.プログラムのロード・ベースを取得した後、その強制クラスを関連ポインタに変換し、計算を行い、最終的にプログラムのエントリ・ポイントのメモリ内の具体的なアドレスを取得します.
次のコードは,プログラムエントリポイントの20バイトをスキャンすることにより,デバッグブレークポイントの有無を判断し,ある場合はデバッガが本プロセスをロードし,直接プロセスを終了することを示す.
なお、VCは、TLSコールバック関数の実行時にライブラリmsvcrtを実行する.dll,mfc.dllなどはロードされておらず、Cライブラリの関数は使用できません.使用する必要がある場合は、LoadLibrary()関数を使用して対応するライブラリにロードし、GetProcAddress()を使用して関数アドレスを取得します.しかし、このような操作はデバッガの関連イベントをトリガーする可能性があり、このような操作は推奨されない.
CRC検査と検証
独自のチェックサムを計算できます.チェックサムが変化した場合、デバッグ中であり、コード内部にブレークポイントが配置されています.
コード運転時間長チェック
コード実行時にシステム実行時間を動的にチェックしてデバッガが存在するか否かを判定できます
PEB特殊フラグビット検出
Windowsでは、プログラムが現在デバッグされている場合、システム呼び出しisDebuggerPresentは1を返し、そうでない場合は0を返します.このシステムAPIは、単純検査PEBプロセス環境ブロックのフラグビットを呼び出し、デバッガが実行されている間にフラグビットが1に設定される.このチェックは、PEBプロセス環境ブロックの2バイト目で直接完了し、コードを読み出すことができます.
上記のコードでは、eaxをPEB(プロセス環境ブロック)に設定し、PEBの2番目のバイトにアクセスし、そのバイトの内容をeaxに移動する.eaxがゼロであるかどうかを確認することで、この検出を完了します.ゼロの場合、デバッガは存在しません.そうでない場合は、デバッガが存在することを示します.
プロセスが事前に実行するデバッガによって作成された場合、ntdllが与えられます.dllのスタック操作ルーチンには、FLG_であるフラグが設定されています.HEAP_ENABLE_TAIL_CHECK FLG_HEAP_ENABLE_FREE_CHECK FLG_HEAP_VALIDATE_PARAMETERSこれらのフラグは、次のコードで確認できます.
上記のコードでは、PEBにアクセスし、PEBのアドレスにオフセット量68 hを加えてスタック操作ルーチンで使用されるこれらのフラグの開始位置に到達することで、これらのフラグをチェックすることでデバッガが存在するかどうかを知ることができます.
次のように、ForceFlagsなどのスタックヘッダ内のフラグを確認しても、デバッガが動作しているかどうかを検出できます.
上記のコードでは、PEBのオフセット量でプロセスのスタックおよびスタックフラグにアクセスする方法を示しています.これらの内容を確認することで、Forceフラグが現在稼働しているデバッガによって事前に1に設定されているかどうかを知ることができます.
システムAPI検査デバッグ
もう1つのデバッガを検出する方法は、NtQueryInformationProcessというシステムAPI呼び出しを使用することである.ProcessInformationClassを7に設定この関数を呼び出すことができ、ProcessDebugPortを参照することができる.プロセスがデバッグされている場合、関数は-1を返します.サンプルコードは次のとおりです.
本例では,まずNtQueryInformationProcessのパラメータをスタックに押し込む.これらのパラメータは、1つ目がハンドル(この例では0)で、2つ目がプロセス情報の長さ(この例では4バイト)で、3つ目はデバッガが存在するかどうかの情報を返す変数です.値がゼロ以外の場合、プロセスがデバッガの下で実行されていることを示します.それ以外の場合、すべての正常な4番目のパラメータは、プロセス情報カテゴリ(この例では7であり、ProcessDebugPortを表す)の最後のパラメータが戻り長であることを示します.これらのパラメータを使用してNtQueryInformationProcessを呼び出した後の戻り値はisdebuggedにあります.その後、戻り値が0であるかどうかをテストします.
その他
また、デバイスリストにデバッガの名前が含まれているかどうかを確認したり、デバッガ用のレジストリキーが存在しているかどうかを確認したり、メモリをスキャンしてデバッガのコードが含まれているかどうかを確認したりする方法もあります.もう1つのEPOと非常に類似した方法は、PEヘッダ内のスレッドローカルメモリ(TLS)テーブル項目を介してプログラムのエントリポイントを参照するようにPEローダに通知することである.これにより、プログラムのエントリポイントを先に読み出すのではなく、TLSのコードを最初に実行することができます.このため、TLSはプログラム起動時にデバッグに必要な検出を完了することができる.TLSから起動すると、一部のデバッガはプログラムのメインエントリポイントで切り込まれているため、ウイルスはデバッガが起動する前に実行できるようになります.
TLS逆デバッグ
TLS概要
TLS設計の本意は,マルチスレッドプログラムにおける変数同期の問題を解決するためであり,Thread Local Storageの略であり,スレッドローカルストレージを意味する.スレッド自体には他のスレッドとは独立したスタック空間があるため、スレッド内のローカル変数は同期の問題を考慮する必要はありません.マルチスレッド同期の問題はグローバル変数へのアクセスであり、TLSはオペレーティングシステムのサポートの下で、グローバル変数を特殊な節にパッケージ化することによって、スレッドを作成するたびにこの節のデータをコピーとして、プロセスの空きアドレス空間にコピーする.後で、スレッドは、同期制御を追加することなく、ローカル変数にアクセスするように、他のスレッドとは異なるグローバル変数のコピーにアクセスできます.
スレッドにローカルに格納されているグローバル変数のテストコード
#include "stdafx.h"
#include
#include
#include
using namespace std;
// TLS
__declspec(thread) int value =0xcccccccc;__
DWORD WINAPI NewThread ( LPVOID lParam )
{
// value,
value = *((int*)lParam);
while(1)
{
printf("%d
",value);
Sleep(1000);
}
return 0 ;
}
#define THREAD_NUM 3
int main(int argc, char* argv[])
{
int arry[3]={1,2,3};
// TLS value 5
value = 5 ;
//
HANDLE hThread[THREAD_NUM];
for (int loop = 0; loop < THREAD_NUM; loop++)
{
hThread[loop] = CreateThread ( NULL, 0, NewThread, &arry[loop], 0, NULL );
Sleep(1000);
}
//
WaitForMultipleObjects(THREAD_NUM, hThread, TRUE, INFINITE);
return 0;
}
実行後、valueがコピーcopyとして3つの異なるスレッドに割り当てられているため、スレッドが値を変更すると、他のスレッドの変数が変化します.
このすべての変数の数値copyを異なるスレッドに変換するプロセスは、スレッド全体が実行される前に実行される[メインスレッドを含む]ので、このスレッドTLSコールバック関数でいくつかのデバッグ検出を行い、プロセスがデバッグされているかどうかを判定し、デバッグが発見された場合、退去または他の操作を行うことができる.
TLSは、相互反発プロセス全体を完了するためにオペレーティングシステムによって簡単に代えることもできるし、ユーザ自身が制御信号量の関数を記述することもできる.プロセス内のスレッドが予め定められたメモリ空間にアクセスすると、オペレーティングシステムはシステムのデフォルトまたはユーザー定義の信号量関数を呼び出し、データの完全性と正確性を保証します.一方、Coderが自分で作成した信号量関数を使用することを選択すると、アプリケーションの初期化フェーズでは、信号量の初期化および他の初期化作業を完了するために、ユーザによって作成された初期化関数が呼び出されます.この呼び出しは、プログラムの実行がエントリポイントに実行される前に完了し、プログラムの実行の正確性を保証する必要があります.
TLSの逆デバッグに基づいて、原理は実際のエントリポイントコードが実行される前に検出デバッガコードを実行し、実現方式はTLSコールバック関数を用いて実現する.
ODダイナミックデバッガは、エントリポイントにプログラムをロードする前に、逆デバッグコードを実行し、プログラムを終了します.さらに、TLSを使用して起動すると、一部のウイルスは、プログラムのメインエントリポイントで切り込まれているため、デバッガが起動する前に実行を開始することもできる.
TLSコールバック関数
TLSコールバック関数の原型は以下の通りである:void NTAPI TlsCallBackFunction(PVOID Handle,DWORD Reason,PVOID Reserve);TLS逆デバッグを実現することは、TLSコールバック関数がプログラムエントリポイントの前にプログラム制御権を得ることができる特性を十分に利用し、一般的な逆デバッグ技術がより効果的になるようにすることである.PE形式では、TLSデータに空間を開き、位置はIMAGE_である.NT_HEADERS.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_TLS]. DataDirectoryの要素には、typedef struct_というデータ構造があります.IMAGE_DATA_DIRECTORY { DWORD VirtualAddress; DWORD Size; } IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
TLSのDataDirectory要素、VirtualAddressメンバーは、相互反発が必要なメモリアドレス、TLSコールバック関数アドレス、およびその他の情報へのアクセスを定義する構造体を指します.
マイクロソフトが提供するVCコンパイラのデフォルトでは、プログラムでTLSを直接使用することがサポートされています.プログラムでTLSを使用するには、まずTLSデータのためにデータセグメントを個別に構築し、関連データでこのセグメントを埋め込み、リンクにTLSデータのPEファイルヘッダにデータを追加するように通知します.
サンプルコード
#include “windows.h”
#include “iostream”
#include”tlhelp32.h”
// PE TLS
#pragma comment(linker, “/INCLUDE:__tls_used”)
void lookupprocess(void);
void Debugger(void);
void NTAPI tls_callback(PVOID h, DWORD reason, PVOID pv)
lookupprocess();
Debugger();
MessageBox(NULL,”Not Main!”,”Test1″,MB_OK);
return;
}
// TLS
#pragma data_seg(“.CRT$XLB”)
//
PIMAGE_TLS_CALLBACK p_thread_callback = tls_callback;
#pragma data_seg()
int main()
{
MessageBox(NULL,”Main!”,”Test1″,MB_OK);
return 0;
}
//anti-debug1
void lookupprocess()
{
PROCESSENTRY32 pe32;
pe32.dwSize = sizeof(pe32);
HANDLE hProcessSnap = ::CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS,0);
BOOL bMore = ::Process32First(hProcessSnap,&pe32);
while(bMore)
{
strlwr(pe32.szExeFile);
if (!strcmp(pe32.szExeFile,”ollyice.exe”))
{
exit(0);
}
if (!strcmp(pe32.szExeFile,”ollydbg.exe”))
{
exit(0);
}
if (!strcmp(pe32.szExeFile,”peid.exe”))
{
exit(0);
}
if (!strcmp(pe32.szExeFile,”idaq.exe”))
{
exit(0);
}
bMore = ::Process32Next(hProcessSnap,&pe32);
}
::CloseHandle(hProcessSnap);
}
//anti-debug2
void Debugger(void)
{
int result=0;
__asm{
mov eax, dword ptr fs:[30h]//TEB 30H
movzx eax, byte ptr ds:[eax+2h]// PEB BeingDebug, 1
mov
result,eax
}
if (result) exit(0);
}
常用デバッガ検出スキーム
int 3[0 xcc]ブレークポイント検出
PE実行可能ファイルは初期化段階でPEファイルが完全にメモリにロードされます.PEファイルヘッダを解析してプログラムのエントリポイントを取得し、エントリポイントの前のバイトまたは複数のバイトを「そのコードがブレークポイントに設定されているかどうかを検出するために、悪意のあるソフトウェアは命令操作コード0 xccを検索することができる」と判断し、デバッガがプログラムエントリポイントでブレークすることを阻止する.次のコードでは、GetModuleHandle(NULL)を使用してアプリケーションのロード・ベース・アドレスを取得します.GetModuleHandle()手動読取プログラムのPEBをシミュレートして得ることもできる.しかし、プログラムの汎用性のために、ここではGetModuleHandle()が使用されている.プログラムのロード・ベースを取得した後、その強制クラスを関連ポインタに変換し、計算を行い、最終的にプログラムのエントリ・ポイントのメモリ内の具体的なアドレスを取得します.
IMAGE_DOS_HEADER *dos_head=(IMAGE_DOS_HEADER *)GetModuleHandle(NULL);
PIMAGE_NT_HEADERS32 nt_head=(PIMAGE_NT_HEADERS32)((DWORD)dos_head+(DWORD)dos_head->e_lfanew);
BYTE*OEP=(BYTE*)(nt_head->OptionalHeader.AddressOfEntryPoint+(DWORD)dos_head);
次のコードは,プログラムエントリポイントの20バイトをスキャンすることにより,デバッグブレークポイントの有無を判断し,ある場合はデバッガが本プロセスをロードし,直接プロセスを終了することを示す.
for(unsigned long index=0;index<20;index++)
{
if(OEP[index]==0xcc)
{
ExitProcess(0);
}
}
なお、VCは、TLSコールバック関数の実行時にライブラリmsvcrtを実行する.dll,mfc.dllなどはロードされておらず、Cライブラリの関数は使用できません.使用する必要がある場合は、LoadLibrary()関数を使用して対応するライブラリにロードし、GetProcAddress()を使用して関数アドレスを取得します.しかし、このような操作はデバッガの関連イベントをトリガーする可能性があり、このような操作は推奨されない.
CRC検査と検証
独自のチェックサムを計算できます.チェックサムが変化した場合、デバッグ中であり、コード内部にブレークポイントが配置されています.
コード運転時間長チェック
コード実行時にシステム実行時間を動的にチェックしてデバッガが存在するか否かを判定できます
PEB特殊フラグビット検出
Windowsでは、プログラムが現在デバッグされている場合、システム呼び出しisDebuggerPresentは1を返し、そうでない場合は0を返します.このシステムAPIは、単純検査PEBプロセス環境ブロックのフラグビットを呼び出し、デバッガが実行されている間にフラグビットが1に設定される.このチェックは、PEBプロセス環境ブロックの2バイト目で直接完了し、コードを読み出すことができます.
mov eax,fs:[30h]
move eax,byte [eax+2]
test eax,eax
jne @DdebuggerDetected
上記のコードでは、eaxをPEB(プロセス環境ブロック)に設定し、PEBの2番目のバイトにアクセスし、そのバイトの内容をeaxに移動する.eaxがゼロであるかどうかを確認することで、この検出を完了します.ゼロの場合、デバッガは存在しません.そうでない場合は、デバッガが存在することを示します.
プロセスが事前に実行するデバッガによって作成された場合、ntdllが与えられます.dllのスタック操作ルーチンには、FLG_であるフラグが設定されています.HEAP_ENABLE_TAIL_CHECK FLG_HEAP_ENABLE_FREE_CHECK FLG_HEAP_VALIDATE_PARAMETERSこれらのフラグは、次のコードで確認できます.
mov eax,fs:[30h]
mov eax,[eax+68h]
and eax,0x70
test eax,eax
jne @DebuggerDetected
上記のコードでは、PEBにアクセスし、PEBのアドレスにオフセット量68 hを加えてスタック操作ルーチンで使用されるこれらのフラグの開始位置に到達することで、これらのフラグをチェックすることでデバッガが存在するかどうかを知ることができます.
次のように、ForceFlagsなどのスタックヘッダ内のフラグを確認しても、デバッガが動作しているかどうかを検出できます.
mov eax,fs:[30h]
mov eax,[eax+18h] ;process heap
mov eax,[eax+10h] ;heap flags
test eax,eax
jne @DebuggerDetected
上記のコードでは、PEBのオフセット量でプロセスのスタックおよびスタックフラグにアクセスする方法を示しています.これらの内容を確認することで、Forceフラグが現在稼働しているデバッガによって事前に1に設定されているかどうかを知ることができます.
システムAPI検査デバッグ
もう1つのデバッガを検出する方法は、NtQueryInformationProcessというシステムAPI呼び出しを使用することである.ProcessInformationClassを7に設定この関数を呼び出すことができ、ProcessDebugPortを参照することができる.プロセスがデバッグされている場合、関数は-1を返します.サンプルコードは次のとおりです.
push 0
push 4
push offset isdebugged
push 7 ;ProcessDebugPort
push -1
call NtQueryInformationProcess
test eax,eax
jne @ExitError
cmp isdebugged,0
jne @DebuggerDetected
本例では,まずNtQueryInformationProcessのパラメータをスタックに押し込む.これらのパラメータは、1つ目がハンドル(この例では0)で、2つ目がプロセス情報の長さ(この例では4バイト)で、3つ目はデバッガが存在するかどうかの情報を返す変数です.値がゼロ以外の場合、プロセスがデバッガの下で実行されていることを示します.それ以外の場合、すべての正常な4番目のパラメータは、プロセス情報カテゴリ(この例では7であり、ProcessDebugPortを表す)の最後のパラメータが戻り長であることを示します.これらのパラメータを使用してNtQueryInformationProcessを呼び出した後の戻り値はisdebuggedにあります.その後、戻り値が0であるかどうかをテストします.
その他
また、デバイスリストにデバッガの名前が含まれているかどうかを確認したり、デバッガ用のレジストリキーが存在しているかどうかを確認したり、メモリをスキャンしてデバッガのコードが含まれているかどうかを確認したりする方法もあります.もう1つのEPOと非常に類似した方法は、PEヘッダ内のスレッドローカルメモリ(TLS)テーブル項目を介してプログラムのエントリポイントを参照するようにPEローダに通知することである.これにより、プログラムのエントリポイントを先に読み出すのではなく、TLSのコードを最初に実行することができます.このため、TLSはプログラム起動時にデバッグに必要な検出を完了することができる.TLSから起動すると、一部のデバッガはプログラムのメインエントリポイントで切り込まれているため、ウイルスはデバッガが起動する前に実行できるようになります.