天天看點

TLS (Thread Local Storage)反調試原理

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 (Thread Local Storage)反調試原理

從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;
           
TLS (Thread Local Storage)反調試原理

AddressOfcallBacks是一個指向指針數組的指針,指向的指針數組是TLS注冊的回調函數位址。回調函數以數組形式連續分布,并以一個全為0的DWORD值來表示結束。

一個TLS可以有多個回調函數,這些回調函數都會被調用。

TLS (Thread Local Storage)反調試原理

是以

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段存在是有意義的,隻是我們這個程式中沒有用到而已。