TLS的特别之處在于使得程式的入口點EP不是第一條執行的指令,是以常常用于反調試檢測之中。
用一個已經開啟的TLS的程式來做說明。
資料結構
TLS存在于PE檔案格式之中。
IMAGE_DATA_DIRECTORY DataDirectory[9]
存放了TLS目錄的位址。
winNT.h [F12 可得到定義位置]
#define IMAGE_DIRECTORY_ENTRY_TLS 9 // TLS Directory
同其他目錄表數組一樣,也是位元組結構 (VA+Size)
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress;
DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
從TLS的VA處,可以找到該目錄的詳細資訊。
位下的TLS目錄詳情
typedef struct _IMAGE_TLS_DIRECTORY32 { //SIZE:0x18h
DWORD StartAddressOfRawData;
DWORD EndAddressOfRawData;
DWORD AddressOfIndex; // PDWORD
DWORD AddressOfCallBacks; // PIMAGE_TLS_CALLBACK *
DWORD SizeOfZeroFill;
DWORD Characteristics;
} IMAGE_TLS_DIRECTORY32;
typedef IMAGE_TLS_DIRECTORY32 * PIMAGE_TLS_DIRECTORY32;
AddressOfcallBacks是一個指向指針數組的指針,指向的指針數組是TLS注冊的回調函數位址。回調函數以數組形式連續分布,并以一個全為0的DWORD值來表示結束。
一個TLS可以有多個回調函數,這些回調函數都會被調用。
是以
00401000
處就是TLS注冊的回調函數,也可以看到,本程式隻注冊了一個回調函數。
觸發機制
當建立/終止線程時會自動調用TLS的回調函數。
EP是在系統建立了該程式的主線程之後進入的,是以TLS回調函數先于EP運作,這也正是很多反調試技術用TLS的原因。
TLS callback函數的定義
typedef VOID
(NTAPI *PIMAGE_TLS_CALLBACK) (
PVOID DllHandle, //子產品句柄,即加載位址
DWORD Reason,
PVOID Reserved
);
其中reason有以下幾種:(winNT.h)
#define DLL_PROCESS_ATTACH 1 程序啟動
#define DLL_THREAD_ATTACH 2 線程啟動
#define DLL_THREAD_DETACH 3 線程退出
#define DLL_PROCESS_DETACH 0 程序退出
程式設計執行個體
// tls_test.cpp : 定義控制台應用程式的入口點。
//
#include "stdafx.h"
#include <windows.h>
#include <stdio.h>
#pragma comment(linker,"/INCLUDE:__tls_used")
void NTAPI tls_callback1(LPVOID dllhhanle,DWORD reason,PVOID Reserved)
{
printf("Tls_callback1 :dllhandle=%x,reason=%d\n",dllhhanle,reason);
}
void NTAPI tls_callback2(LPVOID dllhhanle,DWORD reason,PVOID Reserved)
{
printf("Tls_callback2 :dllhandle=%x,reason=%d\n",dllhhanle,reason);
}
#pragma data_seg(".CRT$XLX")
PIMAGE_TLS_CALLBACK pTLS_CALLBACKs[]={tls_callback1,tls_callback2,}; //end with 0
#pragma data_seg()
DWORD WINAPI ThreadProc(LPVOID lpParam)
{
printf("ThreadProc() Start\n");
printf("ThreadProc() end\n");
return ;
}
int _tmain(int argc, _TCHAR* argv[])
{
HANDLE hThread = NULL;
printf("Main Start\n");
hThread = CreateThread(NULL,,ThreadProc,NULL,,NULL);
WaitForSingleObject(hThread,*);
CloseHandle(hThread);
printf("Main end\n");
return ;
}
關于代碼的說明,在《黑客免殺攻防》上說的非常清楚。
表明要使用TLS表
#pragma data_seg(".CRT$XLX")
PIMAGE_TLS_CALLBACK pTLS_CALLBACKs[]={tls_callback1,tls_callback2,};
#pragma data_seg()
注冊回調函數,
.CRT$XLX
CRT
表示
C RunTime` 機制
x 表示辨別名随機,但不代表這裡可随機,隻能用X
L 表示tls callback section
X (B-Y)任意一個字母都可以
WaitForSingleObject(hThread,*1000);
保證主線程執行時,建立的線程已經退出。(使主線程阻塞60s)
有意思的是,上述代碼是在VS2008下編譯的。用VC6.0編譯,TLS無效。當時就不知道怎麼辦了,在網上搜到了解決方案。先說點題外話,這就是和大牛的差距,他們可能是這麼想的,如果TLS調用除了問題,那麼一定是TLS在PE中的結構有問題。然後他們去檢視TLS結構,果然發現問題在了這裡。是以,想法很重要,沒出來結果就要好好琢磨,想出來就去試試。
我來對這個問題進行一下總結,并給出新的解決方法。
TLS回調問題,網上已有人給出了解決方案,但總是有一些問題,基本如下:
、VC6不支援。
、VS2005的Debug版正常,Release版不正常。
、VS2005的Release版正常,Debug版不正常。
VC6不支援的原因是VC6帶的TLSSUP.OBJ有問題,它已定義了回調表的第一項,并且為,意味着回調表的結束,是以我們加的函數都不會被調用。[INDENT]對于第個問題,我沒遇到,倒是遇到了第個問題。對這個問題進行了一下研究,發現問題所在:在Link過程中節.CRT$XLA和.CRT$XLB合并時,應該是按字母順序無間隙合并,但在DEBUG版的輸出中實事并非如此,順序沒錯,但卻産生了很大的間隙,間隙填,相當于在我們的回調表前加若幹個,又是回調表提前結束,這也許是BUG。針對第二種情況,我沒有遇到,不知道是否是這個原因,如果是,則我想應是LINK的BUG。
針對上述問題,本來我想可以使用VS2008的tlssup.obj,但是它與VC6的不相容,改起來比較麻煩,後來我突然想到,也許我們可以自己建立一個tlssup.obj,基于這個思路,寫了自己的tlssup,目前測試結果顯示,它可以相容VC6,VS2005,VS2008,代碼如下:
/*檔案名:tlssup.c, 要求以C方式編譯, 如果你的工程是CPP工程,請針對此源檔案取消預編譯頭*/
#include <windows.h>
#include <winnt.h>
int _tls_index=;
#pragma data_seg(".tls")
int _tls_start=;
#pragma data_seg(".tls$ZZZ")
int _tls_end=;
#pragma data_seg(".CRT$XLA")
int __xl_a=;
#pragma data_seg(".CRT$XLZ")
int __xl_z=;
#pragma data_seg(".rdata$T")
extern PIMAGE_TLS_CALLBACK my_tls_callbacktbl[];
IMAGE_TLS_DIRECTORY32 _tls_used={(DWORD)&_tls_start,(DWORD)&_tls_end,(DWORD)&_tls_index,(DWORD)my_tls_callbacktbl,,};
/*tlssup.c結束*/
然後,我們在其它CPP檔案中定義my_tls_callbacktbl如下即可:
extern "C" PIMAGE_TLS_CALLBACK my_tls_callbacktbl[] = {my_tls_callback1,}; //可以有多個回調,但一定要在最後加一個空項,否則很可能出錯。
當然下面一行也不能少:
#pragma comment(linker, "/INCLUDE:__tls_used")
自己一看,VC6.0下的那個程式回調函數位址數組确實第一個為0了,修改了之後,發現建立線程的tls回調沒問題,但是main函數前并沒有tls_callback執行,想不出來原因。反調試最好的方式就是在EP前檢測,是以VC6.0按照這個方法的做不了。後來幹脆用vs2008了。
vs2008 正确版本的執行結果:
C:\VC6\MyProjects\tls_test\Release>tls_test.exe
Tls_callback1 :dllhandle=,reason= //程式啟動(但是還未進入EP)
Tls_callback2 :dllhandle=,reason=
Main Start
Tls_callback1 :dllhandle=,reason= //建立的線程啟動
Tls_callback2 :dllhandle=,reason=
ThreadProc() Start
ThreadProc() end
Tls_callback1 :dllhandle=,reason= //建立的線程執行完畢
Tls_callback2 :dllhandle=,reason=
Main end
Tls_callback1 :dllhandle=,reason= //程式退出
Tls_callback2 :dllhandle=,reason=
有兩個疑問:
1. TLS回調函數和DLLMain函數的參數一緻,那麼他們的
ReasonForCall
代表的意思一樣嗎?
比如
dll_process_attach
表示dll建立,還是process建立。
還有,一個程式先建立程序,tls一次,再建立主線程,按理說應該tls一次呢,怎麼沒有呢。main函數是運作在主線程上的吧。
2. <逆向工程核心原理> p457,講到如果建立了一個線程,那麼TLS就在建立線程之前執行。可《黑客免殺技術》又說是執行前,也就是建立了之後,執行前。郁悶,覺得不對啊,應該是建立了線程之後,此時線程未啟動,而先啟動TLS,也就是說線程已經建立了。從我的了解來說或,可能是作者認為:線程的建立成功是以線程啟動了作為辨別。
我感覺這些其實就是線程建立後的回調函數(當然不是執行函數),是以應該是建立成功了。不過還沒有執行而已。回調函數嘛,不就是完事之後給你一個通知,然後我去自定義回調函數嘛。。
3. 事實上,這個PE檔案有.tls段。tls directory存放在.rdata區,回調函數位址在.rdata區,回調函數内容在.text區,和tls區有毛線關系?是以,不知道這個.tls段有毛用啊。
哎,說多了都是淚,對線程還是一竅不通啊。
update2 5.10
第3點中,tls到底有沒有用,驗證期間可以将tls段和PE結構給删除修改了,看有無影響,我猜測應該是沒有影響的。
關于第3點的解釋,《黑免殺》講的很清楚。
因為tls不完全是為反調試準備的,它的主要用處還是線程局部存儲,詳細說就是每個線程都可以用到的初始化局部變量,線上程1中對此聲明的tls變量修改,不會影響到線程2的此tls變量值。
之是以上述實驗沒有用到tls段,是因為我們隻用到了tls回調函數,而沒有用到tls變量。但是編譯器為了統一,把tls段給加上了。(tls段中存放tls變量,有些全局變量的意思了。)是以tls段存在是有意義的,隻是我們這個程式中沒有用到而已。