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技术作为反调试技术的载体,来实现一种在程序入口之前就执行反调试代码的技术。技术本身不会影响程序的执行,但能有效地防止调试器的调试。可以大大增强程序软件的反盗版能力。如果能结合传统技术,将使反调试技术发展至一新高度。

猜你喜欢

转载自blog.csdn.net/liuyez123/article/details/51258177