1. 程式人生 > >WIN10 X64下通過TLS實現反除錯

WIN10 X64下通過TLS實現反除錯

1 TLS技術簡介

Thread Local Storage(TLS),是Windows為解決一個程序中多個執行緒同時訪問全域性變數而提供的機制。TLS可以簡單地由作業系統代為完成整個互斥過程,也可以由使用者自己編寫控制訊號量的函式。當程序中的執行緒訪問預先制定的記憶體空間時,作業系統會呼叫系統預設的或使用者自定義的訊號量函式,保證資料的完整性與正確性。

system segment descriptor

基於TLS的反除錯,原理實為在實際的入口點程式碼執行之前執行檢測偵錯程式程式碼,實現方式便是使用TLS回撥函式實現。通過TLS反除錯實現的效果,形如上圖,在OD動態偵錯程式載入程式到入口點之前便已經執行反除錯程式碼並退出程式。此外,利用TLS啟動時,某些病毒也得以能夠在偵錯程式啟動之前就開始執行,因為一些偵錯程式是在程式的主入口點處切入的。

1.1 TLS回撥函式

當用戶選擇使用自己編寫的回撥函式時,在應用程式初始化階段,系統將要呼叫一個由使用者編寫的回撥函式以完成相應的初始化以及其他的一些初始化工作。此呼叫必須在程式真正開始執行到入口點之前就完成,以保證程式執行的正確性。
TLS回撥函式具有如下的函式原型:

void NTAPI TlsCallBackFunction(PVOID Handle, DWORD Reason, PVOID Reserve);

1.2 TLS的資料結構

Windows的可執行檔案為PE格式,在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回撥函式地址以及其他一些資訊。

2 具體實現及原理

充分利用TLS回撥函式在程式入口點之前就能獲得程式控制權的特性,在TLS回撥函式中進行反除錯操作比傳統的反除錯技術有更好的效果。

2.1 VS2015 X64 release下的demo

Microsoft提供的VC編譯器都支援直接在程式中使用TLS,下文都將使用VS2015進行操作。

#include <Windows.h>
#include <tchar.h>

#pragma comment(lib,"ntdll.lib")

extern "C" NTSTATUS NTAPI NtQueryInformationProcess(HANDLE hProcess, ULONG InfoClass, PVOID Buffer, ULONG Length, PULONG ReturnLength);

#define NtCurrentProcess() (HANDLE)-1


void NTAPI __stdcall TLS_CALLBACK(PVOID DllHandle, DWORD Reason, PVOID Reserved)
{
    if (IsDebuggerPresent())
    {
        MessageBoxA(NULL, "TLS_CALLBACK: Debugger Detected!", "TLS Callback", MB_OK);
//      ExitProcess(1);
    }
    else
    {
        MessageBoxA(NULL, "TLS_CALLBACK: No Debugger Present!...", "TLS Callback", MB_OK);
    }
}

void NTAPI __stdcall TLS_CALLBACK_2(PVOID DllHandle, DWORD Reason, PVOID Reserved)
{
    HANDLE DebugPort = NULL;
    if (!NtQueryInformationProcess(
        NtCurrentProcess(),
        7,          // ProcessDebugPort
        &DebugPort, // If debugger is present, it will be set to -1 | Otherwise, it is set to NULL
        sizeof(HANDLE),
        NULL))
    {
        if (DebugPort)
        {
            MessageBoxA(NULL, "TLS_CALLBACK2: Debugger detected!", "TLS callback", MB_ICONSTOP);
        }

        else
        {
            MessageBoxA(NULL, "TLS_CALLBACK2: No debugger detected", "TLS callback", MB_ICONINFORMATION);
        }
    }
}

//linker spec通知連結器PE檔案要建立TLS目錄,注意X86和X64的區別
#ifdef _M_IX86
#pragma comment (linker, "/INCLUDE:__tls_used")
#pragma comment (linker, "/INCLUDE:__tls_callback")
#else
#pragma comment (linker, "/INCLUDE:_tls_used")
#pragma comment (linker, "/INCLUDE:_tls_callback")
#endif
//建立TLS段
EXTERN_C
#ifdef _M_X64
#pragma const_seg (".CRT$XLB")
const
#else
#pragma data_seg (".CRT$XLB")
#endif
//end linker

//tls import定義多個回撥函式
PIMAGE_TLS_CALLBACK _tls_callback[] = { TLS_CALLBACK, TLS_CALLBACK_2, 0 };
#pragma data_seg ()
#pragma const_seg ()
//end 

int APIENTRY _tWinMain(HINSTANCE hInstance,
    HINSTANCE hPrevInstance,
    LPTSTR    lpCmdLine,
    int       nCmdShow)
{

    MessageBoxA(NULL, "Hello Wolrd!...:)", "main()", MB_OK);
    return 0;

}

要在程式中使用TLS,必須為TLS資料單獨建一個數據段,用相關資料填充此段,並通知連結器為TLS資料在PE檔案頭中新增資料。為此,需要在程式原始檔中新增如下程式碼:

//linker spec通知連結器PE檔案要建立TLS目錄,注意X86和X64的區別
#ifdef _M_IX86
#pragma comment (linker, "/INCLUDE:__tls_used")
#pragma comment (linker, "/INCLUDE:__tls_callback")
#else
#pragma comment (linker, "/INCLUDE:_tls_used")
#pragma comment (linker, "/INCLUDE:_tls_callback")
#endif
//建立TLS段
EXTERN_C
#ifdef _M_X64
#pragma const_seg (".CRT$XLB")
const
#else
#pragma data_seg (".CRT$XLB")
#endif
//end linker
//tls import定義多個回撥函式
PIMAGE_TLS_CALLBACK _tls_callback[] = { TLS_CALLBACK, TLS_CALLBACK_2, 0 };
#pragma data_seg ()
#pragma const_seg ()
//end 

其中_tls_callback[]陣列中儲存了所有的TLS回撥函式指標。值得指出的是,陣列必須以NULL指標結束,且陣列中的每一個回撥函式在程式初始化時都會被呼叫,程式設計師可按需要新增。但程式設計師不應當假設作業系統已何種順序呼叫回撥函式。如此則要求在TLS回撥函式中進行反除錯操作需要一定的獨立性。

編譯器和連結器使用幾個特殊的變數來支援隱式TLS。具體來說,變數_tls_used(X86下變數型別為IMAGE_TLS_DIRECTORY,X64下變數型別為IMAGE_TLS_DIRECTORY64)由C執行時庫建立,靜態連結時該變量表示TLS目錄結構並被最終的映像檔案使用(由於名字修飾的原因,在C++中需要使用extern “C”連結,儲存型別為外部引入,因為CRT程式碼已經建立了該變數)。TLS目錄是PE檔案頭的一部分,用於告訴載入器如何管理執行緒區域性變數,連結時,連結器查詢變數_tls_used(注意:X86下使用雙下劃線__tls_used,在X64下使用單下劃線_tls_used),並確保其與最終PE檔案中的TLS目錄重疊。

C執行時庫中宣告變數_tls_used的原始碼位於tlssup.c檔案中(與Visual Studio一起釋出)。_tls_used標準的宣告方式如下所示:

#ifdef _WIN64
_CRTALLOC(".rdata$T")
extern const IMAGE_TLS_DIRECTORY64 _tls_used =
{
        (ULONGLONG) &_tls_start,        // start of tls data
        (ULONGLONG) &_tls_end,          // end of tls data
        (ULONGLONG) &_tls_index,        // address of tls_index
        (ULONGLONG) (&__xl_a+1),        // pointer to call back array
        (ULONG) 0,                      // size of tls zero fill
        (ULONG) 0                       // characteristics
};
#else  /* _WIN64 */
_CRTALLOC(".rdata$T")
extern const IMAGE_TLS_DIRECTORY _tls_used =
{
        (ULONG)(ULONG_PTR) &_tls_start, // start of tls data
        (ULONG)(ULONG_PTR) &_tls_end,   // end of tls data
        (ULONG)(ULONG_PTR) &_tls_index, // address of tls_index
        (ULONG)(ULONG_PTR) (&__xl_a+1), // pointer to call back array
        (ULONG) 0,                      // size of tls zero fill
        (ULONG) 0                       // characteristics
};
#endif  /* _WIN64 */

同樣,CRT程式碼提供了一種機制,該機制允許程式註冊一系列與DllMain具有類似簽名的TLS回撥函式。(這些回撥函式可以在主映像檔案中存在,而DllMain則不可以)。回撥函式型別為PIMAGE_TLS_CALLBACK,TLS目錄指向一個以NULL結尾的callbacks陣列(在實際測試中,回撥函式是依次被呼叫的。但程式設計師不應當假設作業系統已何種順序呼叫回撥函式)。

TLS回撥函式中第二個引數決定了函式在那種情況下(DllMain函式一樣)被呼叫:

  • DLL_PROCESS_ATTACH:是指新程序建立時,初始化主執行緒時執行。
  • DLL_PROCESS_DETACH:是指在程序終止時執行。
  • DLL_THREAD_ATTACH:是指建立新執行緒時,但是不包括主執行緒。
  • DLL_THREAD_DETACH:是指在所有執行緒終止時執行,但是同樣不包括主執行緒。

需要指出的是,在TLS回撥函式執行時,VC執行庫msvcrt.dll,mfc.dll等並未載入,不能使用C庫的函式(比如printf)。如果有需要使用,應該使用LoadLibrary()函式載入相應的庫並使用GetProcAddress()獲得函式地址。但此類操作可能會導致偵錯程式的相關事件觸發,不建議進行此類操作。

對於一般的PE檔案不會使用TLS回撥(實際中,大部分使用DllMain來完成獨立於執行緒的初始化工作)。但是TLS回撥支援卻是完全可以工作的。為了使用CRT提供的TLS回撥支援,需要我們宣告一個存放在以“.CRT$XLx“為名的節裡面,這裡x是一個位於A和Z之間的字母。例如,如下的程式碼片段:

#pragma section(“.CRT$XLY”,long,read)
extern “C” __declspec(allocate(“.CRT$XLY”))
PIMAGE_TLS_CALLBACK _xl_y = MyTlsCallback;

需要如此奇怪的節名是因為TLS回撥指標需要進行記憶體排序的原因。為了理解這種特殊宣告的作用,需要首先明白編譯器和連結器是如何組織PE檔案中的資料的。

PE檔案中,除了頭部資料,其它均是分不同節儲存的,節就是具有相同屬性(也保護屬性)集合的記憶體區域。關鍵字__declspec(allocate(“section-name”))告訴連結器在最終PE檔案中其作用域內的內容放在指定的節內。連結器額外支援將相似名字的節合併為一個大節的功能。該功能通過使用 節名字首+$+任意字串 的形式來啟用。連結器將合併具有相同節名字首的節為一個大節。
連結器對於相似節採用字典順序進行合併(對$後的字串進行排序)。這意味著在記憶體中,位於節“.CRT$XLB”中的變數將在位於節“.CRT$XLA”中變數位置的後面,但是在位於節“.CRT$XLZ”中的變數的前面。C執行時庫利用連結器的這一特性來建立一個以NULL結尾的TLS回撥陣列(將節“.CRT$XLZ”中放置一個NULL指標)。因此為了保證宣告的函式指標位於TLS回撥陣列內部,必須將它放在節“.CRT$XLx”中。

2.2 回撥函式的具體實現

2.2.1 使用IsDebuggerPresent檢測偵錯程式

微軟給我們提供了一個API函式用來檢測當前程式是否正在被除錯,這就是IsDebuggerPresent() ,這個函式的實現很簡單:

語法
    BOOL WINAPI IsDebuggerPresent(void);
引數
    該函式沒有引數
返回值
    如果當前程序執行在偵錯程式的上下文,返回值為非零值。
    如果當前程序沒有執行在偵錯程式的上下文,返回值是零。

2.2.2 使調DebugPort檢測偵錯程式

每個程序都有一個數據結構,EPROCESS,這個結構是在核心裡面的,系統用來標識和管理每一個win程序的基本資料結構。這個結構中包含了一個重要的欄位,DebugPort,如果一個程序不在被除錯的時候那麼就是NULL,否則他是一個指標,win程序中這個成員儲存的是用於接收除錯事件的LPC埠物件指標.發生除錯時,系統會向這個埠傳送除錯資訊。

Winxp下因為使用了專門用於除錯的核心物件DebugObject,所以debugport指向的是一個DebugObject物件,不管是指向LPC埠還是指向除錯物件,他們的作用都是用來傳遞除錯事件的,所以debugport中指向的物件,我們就叫做除錯埠。這個埠是連結偵錯程式程序和被除錯程序的紐帶,被除錯程式的事件由這個埠傳送到偵錯程式程序的。

HANDLE DebugPort = NULL;
    if (!NtQueryInformationProcess(
        NtCurrentProcess(),
        7,          // ProcessDebugPort
        &DebugPort, // If debugger is present, it will be set to -1 | Otherwise, it is set to NULL
        sizeof(HANDLE),
        NULL))
    {
        if (DebugPort)
        {
            MessageBoxA(NULL, "TLS_CALLBACK2: Debugger detected!", "TLS callback", MB_ICONSTOP);
        }

        else
        {
            MessageBoxA(NULL, "TLS_CALLBACK2: No debugger detected", "TLS callback", MB_ICONINFORMATION);
        }
    }

3 實際測試

3.1 測試直接執行

測試方法為直接在資源管理器中雙擊執行。 使用VS2015編譯後直接執行,程式正常執行。程式按照預期的順序呼叫TLS_CALLBACK, TLS_CALLBACK_2兩個回撥函式,最後呼叫_tWinMain函式,程式執行時一次彈出相應的視窗,並提示沒有檢測到偵錯程式。

這裡寫圖片描述

3.2 測試用偵錯程式載入

測試方法為分別使用VS2015自帶的偵錯程式載入除錯和使用OllyDebug載入生成的程式進行除錯。測試結果:兩者程式的呼叫順序和沒有使用偵錯程式的一樣,都是先呼叫TLS回撥函式,同時兩種偵錯程式中都提示檢測到偵錯程式。

system segment descriptor
system segment descriptor

用ollydbg(安裝了帶反除錯外掛的od會免疫,最好把外掛先清除)測試結果如上圖,程式也會檢測出偵錯程式。

4 總 結

傳統的反除錯技術都存在一個弱點:他們都在程式真正開始執行之後才採取反除錯手段。實際上在反除錯程式碼被執行前,偵錯程式有大量的時間來影響程式的執行,甚至可以在程式入口處插入斷點命令來除錯程式。對於使用C/C++語言編譯的程式來說,問題通常會更嚴重,在執行到main()函式之前,會執行C/C++編譯器插入的很大一段程式碼,這也給偵錯程式帶來影響程式執行的機會。

通過使用TLS技術作為反除錯技術的載體,來實現一種在程式入口之前就執行反除錯程式碼的技術。技術本身不會影響程式的執行,但能有效地防止偵錯程式的除錯。可以大大增強程式軟體的反盜版能力。如果能結合傳統技術,將使反除錯技術發展至一新高度。