TLS(Thread Local Storage 線程局部存儲)html
一個進程中的每一個線程在訪問同一個線程局部存儲時,訪問到的都是獨立的綁定於該線程的數據塊。在PEB(進程環境塊)中TLS存儲槽共64個(位於PEB的TlsBitmapBits字段中,共64位(bit)微軟保證每一個進程最少擁有64個索引可用,若是須要,系統還會爲線程提供更多的索引)。每一個TEB(線程環境塊)偏移0x2C處是一個PVOID THreadLocalStoragePointer指針,存放着當前進程TLS副本地址。TLS對每一個線程均可見,而且這個值對於全部的線程都是相同的,操做這個索引就表明操做了全部線程的相同索引。程序員
typedef struct _IMAGE_TLS_DIRECTORY32 {
ULONG StartAddressOfRawData; // TLS模板的起始位置
ULONG EndAddressOfRawData; // TLS模板的結束地址
ULONG AddressOfIndex; // TLS索引的位置
ULONG AddressOfCallBacks; // TLS回調函數數組指針
ULONG SizeOfZeroFill; // 填充0
ULONG Characteristics; // 保留
} IMAGE_TLS_DIRECTORY32;
typedef IMAGE_TLS_DIRECTORY32 * PIMAGE_TLS_DIRECTORY32;windows
IMAGE_TLS_DIRECTORY32.AddressOfIndex 用於保存TLS索引的位置,索引的具體值由loader決定。數組
IMAGE_TLS_DIRECTORY32.AddressOfCallBacks 這是一個指針,它指向TLS回調函數組成的數組。這個數組是以NULL結尾的,所以,若是沒有回調函數的話,這個字段指向的位置應該是4字節的0。數據結構
TLS回調函數:是線程創建和退出時的回調函數,包括主線程和其它線程。程序能夠經過PE文件的方式提供一個或多個TLS回調函數,用以支持對TLS數組進行附加的初始化和終止操做,這種操做相似於面向對象程序設計中的構造函數和析構函數。回調函數的原型與DLL入口函數參數相同:typedef VOID (NTAPI*PIMAGE_TLS_CALLBACK) (PVOID DllHandle, DWORD Reason, PVOID Reserved);框架
TLS數據初始化和TLS回調函數調用都在入口點以前執行,也就是說TLS是程序最開始運行的地方。函數
下面附上Anti-Attach代碼DEMO工具
#include <stdio.h>
#include <windows.h>
#include <tchar.h>
// TLS被掛接次數
DWORD g_numAttachCount = 0;
#define MAX_NUM_OF_THREADS 1
// TLS回調函數
void __stdcall TlsCallBack(PVOID DllHandle,ULONG Reason,PVOID Reserved)
{
if ( Reason == DLL_PROCESS_ATTACH )
{
g_numAttachCount = 1;
}
else if ( Reason == DLL_THREAD_ATTACH )
{
// 進程有多個線程建立
g_numAttachCount++;
if ( g_numAttachCount > MAX_NUM_OF_THREADS )
{
MessageBox(NULL, _T("有人調戲?"), NULL, MB_OK);
ExitProcess(-1);
}
}
}
// TLS索引的位置
DWORD TlsIndex = 0;
// 回調函數數組
PIMAGE_TLS_CALLBACK TlsCallBackArr[] = { (PIMAGE_TLS_CALLBACK)TlsCallBack, NULL };
// 定義_tls_used
// 參考VC安裝的目錄C定義源碼: X:\Program Files\Microsoft Visual Studio 9.0\VC\crt\src\tlssup.c
extern "C"
{
IMAGE_TLS_DIRECTORY _tls_used={
0, // start of tls data
0, // end of tls data
(ULONG)(PULONG)&TlsIndex, // address of tls_index
(ULONG)TlsCallBackArr, // pointer to call back array
0, // size of tls zero fill
0 // characteristics
};
}
int main()
{
DWORD numCount = 0;
while ( true )
{
Sleep( 1000 );
printf( "等待OD Attach: %d\n", numCount++ );
}
return 0;
}
OllyDbg使用Attack的方式載入這個例子將會暫停在入口點。因爲TLS回調函數是在實際的入口點執行以前被調用的,OllyDbg應該配置一下使其在TLS回調被調用以前中斷在實際的loader。
能夠經過選擇選項->調試選項->事件->第一次中斷於->系統斷點來設置中斷於ntdll.dll內的實際loader代碼。這樣設置之後,OllyDbg將會中斷在位於執行TLS回調的ntdll!LdrpRunInitializeRoutines()以前的ntdll!_LdrpInitializeProcess(),這時就能夠在回調例程中下斷並跟蹤了。如,在內存映像的.text代碼段上設置內存訪問斷點,能夠斷在TLS回調函數。
參考文獻: http://www.pediy.com/kssd/pediy07/pediy7-658.htm
<<Windows PE 權威指南>> 戚利
<<加密與解密第三版>> 段鋼測試
看雪論壇加密
轉載於:http://hi.baidu.com/cppcoffee/item/ac6f0c173119e105b88a1a38 感謝原做者爲咱們帶來美妙絕倫的文章
盜版行爲日益猖獗,嚴重影響到軟件開發者和開發商的知識產權及利益,反盜版技術的重要性也愈來愈引發人們的重視。在反盜版技術中,起最大做用的當屬反調試技術。然而傳統的反調試技術都存在一個弱點:他們都在程序真正開始執行以後才採起反調試手段。實際上在反調試代碼被執行前,調試器有大量的時間來影響程序的執行,甚至能夠在程序入口處插入斷點命令來調試程序。對於使用C/C++語言編譯的程序來講,問題一般會更嚴重,在執行到main()函數以前,會執行C/C++編譯器插入的很大一段代碼,這也給調試器帶來影響程序執行的機會。
本文討論一種利用Windows提供的線程訪問互斥機制,來實現一種在程序入口以前就執行反調試代碼的技術。技術自己不會影響程序的執行,但能有效地防止調試器的調試。
1 TLS技術簡介
TLS全稱爲Thread Local Storage,是Windows爲解決一個進程中多個線程同時訪問全局變量而提供的機制。TLS能夠簡單地由操做系統代爲完成整個互斥過程,也能夠由用戶本身編寫控制信號量的函數。當進程中的線程訪問預先制定的內存空間時,操做系統會調用系統默認的或用戶自定義的信號量函數,保證數據的完整性與正確性[1]。
1.1 TLS回調函數
當用戶選擇使用本身編寫的信號量函數時,在應用程序初始化階段,系統將要調用一個由用戶編寫的初始化函數以完成信號量的初始化以及其餘的一些初始化工做。此調用必須在程序真正開始執行到入口點以前就完成,以保證程序執行的正確性。
TLS回調函數具備以下的函數原型:
void NTAPI TlsCallBackFunction(PVOID Handle, DWORD Reason, PVOID Reserve);[1]
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]。
2 具體實現及原理
充分利用TLS回調函數在程序入口點以前就能得到程序控制權的特性,在TLS回調函數中進行反調試操做比傳統的反調試技術有更好的效果。
2.1 在程序中使用TLS
Microsoft提供的VC編譯器都支持直接在程序中使用TLS,下文都將使用VC進行操做。
要在程序中使用TLS,必須爲TLS數據單獨建一個數據段,用相關數據填充此段,並通知連接器爲TLS數據在PE文件頭中添加數據。爲此,須要在程序源文件中添加以下代碼[3]:
#pragma comment(linker, "/INCLUDE:__tls_used")
#pragma data_seg(".CRT$XLB")
PIMAGE_TLS_CALLBACK TlsCallBackArray[] = {
TlsCallBackFunction1,
TlsCallBackFunction2,
......
NULL
};
#pragma data_seg()
其中TlsCallBackArray數組中保存了全部的TLS回調函數指針。值得指出的是,數組必須以NULL指針結束,且數組中的每個回調函數在程序初始化時都會被調用,程序員可按須要添加。但程序員不該當假設操做系統已何種順序調用回調函數。如此則要求在TLS回調函數中進行反調試操做須要必定的獨立性。
2.2 回調函數的具體實現 2.2.1 檢測調試器在程序入口插入的斷點
PE可執行文件在初始化階段,PE文件都會被完整地加載進入內存。經過分析PE文件頭來獲取程序的入口點,並對入口點的前一個或多個字節進行判斷,以阻止調試器在程序入口點下斷。在下面代碼中,使用GetModuleHandle(NULL)來得到應用程序的加載基址;也能夠模擬GetModuleHandle()手動讀取程序的PEB來獲得。但爲了程序的通用性,這裏仍然使用GetModuleHandle()。得到程序加載基址後,將其強制類轉換爲相關指針,並進行計算,最終獲得程序的入口點在內存中的具體地址[3]:
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);
}
}
須要指出的是,在TLS回調函數執行時,VC運行庫msvcrt.dll,mfc.dll等並未載入,不能使用C庫的函數。若是有須要使用,應該使用LoadLibrary()函數載入相應的庫並使用GetProcAddress()得到函數地址。但此類操做可能會致使調試器的相關事件觸發,不建議進行此類操做。
2.2.2 使調試器窗口無效
此處使用FindWindow()查找指定的窗口,並使用SetWindowsLong()將其超類化爲不可用,具體代碼見下:
HWND hd_od=FindWindow("ollydbg",NULL);
SetWindowLong(hd_od,GWL_STYLE,WS_DISABLED);
.........處理其餘類型的調試器
這裏須要說明的是,此類方法僅僅對ring3調試器才起做用。如SoftICE,WinDebug之類的內核調試器,由於根本不存在窗口,此種方法對其無效。
2.2.3 爲程序執行所必需的元素進行初始化或分配空間
這個操做的目的是爲了防止盜版者經過直接將TLS數據清除的方法來避開反調試。當程序正常運行所須要的內存空間或數據必需通過TLS回調函數初始化時,盜版者不能夠將程序的TLS數據清除。由於那樣作帶來的後果是程序運行根本不正常。
另外,若是這種初始化或分配空間的操做分散在各個TLS回調函數中完成,效果會更好。
2.2.4 堵塞輸入
此功能主要是爲了應對一些未知調試器和一些不在程序入口點下斷的調試器。
函數將首先檢查user32.dll導出的BlockInput()函數,若是函數代碼是被調試器修改過的,那麼將直接退出,代碼以下[4]:
BYTE *address=(BYTE *)GetProcAddress(LoadLibrary("user32.dll"),"BlockInput");;
bool modify=true;
for(int x=0;x<20;x++){
if(address[x]==0xff&&address[x+1]!=0xff){
modify=false;
break;
}
if(modify) ExitProcess(0);
檢查過BlockInput()函數正確性以後,函數將調用BlockInput(TRUE),阻塞用戶的鼠標和鍵盤輸入。但函數接下來並不當即取消阻塞,而在main()函數中發起一個異常後再取消阻塞。
這麼處理的理由很充分,在main()函數中發起異常將致使調試器捕獲異常,並暫停等待用戶輸入。而此時用戶輸入是被鎖定的,那麼程序就至關於被變相鎖死了,沒有辦法繼續調試。而當調試器不存在時,代碼中的__except()部分將直接得到執行權,並取消阻塞,程序正常運行。main函數中的具體代碼以下:
__try{
__asm{
xor eax,eax
div eax,eax
xor eax,eax
}
ExitProcess(0);
}
__except(1,1){
BlockInput(FALSE);
}
值得說明的是,這次在main()函數中調用BlockInput()而不檢查也是有緣由的。若是此時的BlockInput()函數被調試器修改過,那麼取消輸入鎖定將徹底不能工做,那麼以後的整個過程也是沒法調試的。
若是進一步深刻,還能夠測試異常返回所用的時間,不論過長或者太短,都可以說明調試器的存在。此處不繼續展開了。
2.2.5 建立監視線程
此步目的是爲了防止OllyDbg等調試器以進程附加的形式對程序進行調試。程序此步將建立一個子線程,監視調試器窗口的出現,若是發現調試器窗口,將其超類化爲不可用。子線程的代碼以下:
DWORD WINAPI Monitor(LPVOID s){
while(sign==TRUE){
HWND hd=FindWindow("ollydbg",NULL);
SetWindowLong(hd,GWL_STYLE,WS_DISABLED);
.......處理其它調試器窗口
Sleep(50);
}
return 0;
}
3 實際測試 3.1 測試直接執行
測試方法爲直接在資源管理器中雙擊執行。
3.2 測試用調試器加載
測試方法爲分別使用VC自帶的調試器加載調試和使用OllyDebug加載生成的程序進行調試。其中OllyDbg使用從看雪論壇下載的OllyDebug 1.10 CHS,並打開了全部的隱藏插件。
測試結果:二者調試的進程都直接退出,且沒有輸出任何信息。
圖3 使用OllyDebug 1.10,打開所有反反調試插件調試,程序直接退出
3.3 測試用調試器附加進程
測試方法爲先在資源管理器中雙擊程序運行,而後打開OllyDebug試圖附加進程。
測試結果:OllyDebug窗口直接失效。
4 總 結
經過使用TLS技術做爲反調試技術的載體,能夠大大加強程序軟件的反盜版能力。若是能結合傳統技術,將使反調試技術發展至一新高度。
===============================================
在上篇中,咱們會爲讀者介紹惡意軟件經常使用來的反仿真技術。本文中,咱們將向讀者介紹惡意軟件用以阻礙對其進行逆向工程的各類反調試技術,以幫助讀者很好的理解這些技術,從而可以更有效地對惡意軟件進行動態檢測和分析。
1、反調試技術
反調試技術是一種常見的反檢測技術,由於惡意軟件老是企圖監視本身的代碼以檢測是否本身正在被調試。爲作到這一點,惡意軟件能夠檢查本身代碼是否被設置了斷點,或者直接經過系統調用來檢測調試器。
1.斷點
爲了檢測其代碼是否被設置斷點,惡意軟件能夠查找指令操做碼0xcc(調試器會使用該指令在斷點處取得惡意軟件的控制權),它會引發一個SIGTRAP。若是惡意軟件代碼自己創建了一個單獨的處理程序的話,惡意軟件也能夠設置僞斷點。用這種方法惡意軟件能夠在被設置斷點的狀況下繼續執行其指令。
惡意軟件也能夠設法覆蓋斷點,例若有的病毒採用了反向解密循環來覆蓋病毒中的斷點。相反,還有的病毒則使用漢明碼自我糾正自身的代碼。漢明碼使得程序能夠檢測並修改錯誤,可是在這裏卻使病毒可以檢測並清除在它的代碼中的斷點。
2.計算校驗和
惡意軟件也能夠計算自身的校驗和,若是校驗和發生變化,那麼病毒會假定它正在被調試,而且其代碼內部已被放置斷點。VAMPiRE是一款抗反調試工具,可用來逃避斷點的檢測。VaMPiRE經過在內存中維護一張斷點表來達到目的,該表記錄已被設置的全部斷點。該程序由一個頁故障處理程序(PFH),一個通用保護故障處理程序(GPFH),一個單步處理程序和一個框架API組成。當一個斷點被觸發的時候,控制權要麼傳給PFH(處理設置在代碼、數據或者內存映射I/O中的斷點),要麼傳給GPFH(處理遺留的I/O斷點)。單步處理程序用於存放斷點,使斷點能夠屢次使用。
3.檢測調試器
在Linux系統上檢測調試器有一個簡單的方法,只要調用Ptrace便可,由於對於一個特定的進程而言沒法連續地調用Ptrace兩次以上。在Windows中,若是程序目前處於被調試狀態的話,系統調用isDebuggerPresent將返回1,不然返回0。這個系統調用簡單檢查一個標誌位,當調試器正在運行時該標誌位被置1。直接經過進程環境塊的第二個字節就能夠完成這項檢查,如下代碼爲你們展現的就是這種技術:
mov eax, fs:[30h]
move eax, byte [eax+2]
test eax, eax
jne @DdebuggerDetected
在上面的代碼中,eax被設置爲PEB(進程環境塊),而後訪問PEB的第二個字節,並將該字節的內容移入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的地址加上偏移量68h到達堆操做例程所使用的這些標誌的起始位置,經過檢查這些標誌就能知道是否存在調試器。
檢查堆頭部內諸如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了。
另外一種檢測調試器的方法是,使用NtQueryInformationProcess這個系統調用。咱們能夠將ProcessInformationClass設爲7來調用該函數,這樣會引用ProcessDebugPort,若是該進程正在被調試的話,該函數將返回-1。示例代碼以下所示。
push 0push 4push offset isdebuggedpush 7 ;ProcessDebugPortpush -1call NtQueryInformationProcesstest eax, eaxjne @ExitErrorcmp isdebugged, 0jne @DebuggerDetected
在本例中,首先把NtQueryInformationProcess的參數壓入堆棧。這些參數介紹以下:第一個是句柄(在本例中是0),第二個是進程信息的長度(在本例中爲4字節),接下來是進程信息類別(在本例中是7,表示ProcessDebugPort),下一個是一個變量,用於返回是否存在調試器的信息。若是該值爲非零值,那麼說明該進程正運行在一個調試器下;不然,說明一切正常。最後一個參數是返回長度。使用這些參數調用NtQueryInformationProcess後的返回值位於isdebugged中。隨後測試該返回值是否爲0便可。
另外,還有其餘一些檢測調試器的方法,如檢查設備列表是否含有調試器的名稱,檢查是否存在用於調試器的註冊表鍵,以及經過掃描內存以檢查其中是否含有調試器的代碼等。
另外一種很是相似於EPO的方法是,通知PE加載器經過PE頭部中的線程局部存儲器(TLS)表項來引用程序的入口點。這會致使首先執行TLS中的代碼,而不是先去讀取程序的入口點。所以,TLS在程序啓動就能夠完成反調試所需檢測。從TLS啓動時,使得病毒得以可以在調試器啓動以前就開始運行,由於一些調試器是在程序的主入口點處切入的。