lyl610abc 发表于 2021-8-8 20:30

【原创】反调试实战系列二 TLS反调试+CheckRemoteDebuggerPresent原理

本帖最后由 lyl610abc 于 2021-8-8 20:31 编辑

# 系列

[【原创】反调试实战系列一 x64dbg+IDA 过IsDebuggerPresent](https://www.52pojie.cn/thread-1432590-1-1.html)

[【原创】反调试实战系列二 TLS反调试+CheckRemoteDebuggerPresent原理](https://www.52pojie.cn/thread-1490663-1-1.html)

------

# 前言

在反调试系列开篇中介绍了简单的反调试的手段:IsDebuggerPresent,这次继续介绍常用的反调试手段:TLS反调试

PS:在第一篇反调试实战中是通过**不断创建线程**来检测当前进程是否被调试,而使用TLS反调试则**无需我们自己去不断创建线程来进行检测**,因为TLS也是基于线程的

------

# TLS反调试

## 什么是TLS

### 官方版

给出来源于官方的文档释义:(https://docs.microsoft.com/en-us/cpp/parallel/thread-local-storage-tls?view=msvc-160)

TLS全称`Thread Local Storage`,即**线程局部存储**。TLS是一种**方法**,通过这种方法,给定多线程进程中的每个线程可以分配位置来**存储特定于线程的数据**。通过TLS API (TlsAlloc)支持**动态绑定(运行时)**特定于线程的数据。Win32和Microsoft c++编译器现在除了现有的API实现外,还支持**静态绑定(加载时)**每个线程数据。

### 通俗版

抓住官方版中的关键词:**方法、存储特定于线程数据、绑定**

#### 方法

TLS是一种编程方法

#### 存储特定于线程的数据

TLS使用静态(static)或全局局部内存(global memory local)对线程数据进行存储,TLS变量是使用(GS/FS)J扩展段寄存器访问的,而非DS数据段寄存器(有关段寄存器可回顾:[保护模式笔记二 段寄存器](https://www.52pojie.cn/thread-1415421-1-1.html))

> PS:是不是还是有点懵逼,不要紧,这部分释义大致看看就好,后面才是重点

#### 绑定

绑定分为动态绑定和静态绑定

##### 动态绑定

给出动态绑定官方文档:(https://docs.microsoft.com/en-us/windows/win32/procthread/using-thread-local-storage)

所谓动态绑定就是使用:(https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-tlsalloc)、 (https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-tlssetvalue)、 (https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-tlsgetvalue) 、(https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-tlsfree)四个API对创建的线程进行绑定

由于本次的TLS反调试并没有用到动态绑定,并且官方给出的文档还附有实例,故这里不再赘述,有兴趣的小伙伴可以自行研究= ̄ω ̄=

##### 静态绑定

静态绑定则是本次反调试所用到的方法,在之后会具体说明

------

## TLS的意义

TLS主要是为了解决多线程中变量的**同步**问题

进程中的全局变量和函数内定义的静态(static)变量,是每个线程都可以访问的**共享变量**。只要有任何一个线程修改了共享变量,其他所有线程中的共享变量也会**同步被修改**

乍看之下,这种方式使得**数据交换十分的便捷**,无需额外的通信就可以实现数据之间的交换。但`命运赠送的礼物,早已在暗中标好了价格( $ _ $ )`。这里的价格就是**多线程访问时所耗费的同步开销**

比如当共享变量要修改前,需要对该变量上锁,其他要访问该共享变量的线程必须等共享变量修改完成释放锁后才能继续执行。这期间线程等待所耗费的资源就是所谓的开销。关于同步异步、并发并行、互斥信号量等基础知识这里不再赘述

------

## TLS变量

TLS变量即:同一个线程里面调用的各个函数都可以访问、但其他线程无法访问的变量(被称为static memory local to a thread 线程局部静态变量)

举个例子:线程A 修改TLS变量后线程B中的TLS变量不受影响,因为每个线程中都有一个TLS变量副本

### TLS变量实例

#### TLS变量的声明

```c
_declspec(thread) int global=0x610;
```

#### TLS变量演示代码

```c
#include <Windows.h>
#include <stdio.h>
//声明TLS变量
_declspec(thread) int global = 0x610;
//线程1,修改TLS变量的值,并输出修改后的结果
DWORD WINAPI threadFunc1(LPVOID lpThreadParameter) {
    global = 0x666;
    printf("global value set to %x\n", global);
    return 0;
}
//线程2,在线程1后执行,输出TLS变量
DWORD WINAPI threadFunc2(LPVOID lpThreadParameter) {
    //让线程2进入睡眠状态,让出执行机会给线程1,以此确保线程1先执行
    Sleep(500);
    printf("global value is %x\n", global);
    return 0;
}
int main()
{
    HANDLE hThread1=CreateThread(NULL, NULL, threadFunc1, NULL, NULL, NULL);
        HANDLE hThread2 = CreateThread(NULL, NULL, threadFunc2, NULL, NULL, NULL);
    system("pause");
}
```

#### TLS演示结果

!(https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/image-20210726202808017.png)

验证了TLS变量在线程1中被修改,但在线程2中并没有受到影响

------

## TLS静态绑定

所谓的TLS静态绑定主要体现在**TLS回调函数**上

### TLS回调函数

官方文档:(https://docs.microsoft.com/en-us/windows/win32/debug/pe-format#tls-callback-functions)

程序可以提供**一个或多个**TLS回调函数,以支持TLS数据对象的附加初始化和终止

尽管通常只有一个回调函数,但回调函数是作为**数组**实现的,以便在需要时可以添加额外的回调函数

如果有多个回调函数,则**按其地址在数组中出现的顺序**调用每个函数。**空指针终止数组**。空列表是完全有效的(不支持回调),在这种情况下,回调数组只有一个成员—— null ptr(空指针)

------

### TLS回调函数的使用

下面关于TLS回调函数的使用参考了:(https://stackoverflow.com/questions/14538159/about-tls-callback-in-windows)

要在C/C++ 中使用TLS函数步骤如下:

1. 编译器声明使用TLS
2. 定义TLS回调函数
3. 注册TLS回调函数

------

#### 编译器声明

通过下列代码告诉编译器要使用TLS

```c
#ifdef _WIN64                //64位
   #pragma comment (linker, "/INCLUDE:_tls_used")
   #pragma comment (linker, "/INCLUDE:tls_callback_func")
#else                                //32位
   #pragma comment (linker, "/INCLUDE:__tls_used")
   #pragma comment (linker, "/INCLUDE:_tls_callback_func")
#endif
```

注意64位和32位的TLS编译器声明不同,所以上述代码使用了条件编译

------

#### 定义TLS回调函数

TLS回调函数的定义如下:

```c
typedef VOID
(NTAPI *PIMAGE_TLS_CALLBACK) (
    PVOID DllHandle,
    DWORD Reason,
    PVOID Reserved
    );
```

------

|       | 数据类型 | 数据含义            |
| ----- | -------- | --------------------- |
| 参数1 | PVOID    | 指向DLL本身的实例句柄 |
| 参数2 | DWORD    | 调用原因            |
| 参数3 | PVOID    | 保留,固定为0         |

编写过DLL的同学们不难发现它与DLL的入口点函数:`DllMain`相同

这里给出第二个参数调用原因的含义:

| 宏定义             | 值   | 描述                                                   |
| :----------------- | :--- | :------------------------------------------------------- |
| DLL_PROCESS_ATTACH | 1    | 一个新进程已经启动,包括第一个线程                     |
| DLL_THREAD_ATTACH| 2    | 创建了一个新线程。此通知已发送给除第一个线程外的所有线程 |
| DLL_THREAD_DETACH| 3    | 线程即将被终止。此通知已发送给除第一个线程外的所有线程   |
| DLL_PROCESS_DETACH | 0    | 进程即将终止,包括原始线程                               |

------

#### 注册TLS回调函数

```c
#ifdef _WIN64                                                        //64位
    #pragma const_seg(".CRT$XLF")
    EXTERN_C const
#else
    #pragma data_seg(".CRT$XLF")                //32位
    EXTERN_C
#endif
PIMAGE_TLS_CALLBACK tls_callback_func[] = { tls_callback,0 };
#ifdef _WIN64                                                        //64位
    #pragma const_seg()
#else
    #pragma data_seg()                                        //32位
#endif //_WIN64
```

注意这里也要使用条件编译来区别对待64位和32位

`const_seg`和`data_seg`都带了个后缀seg,即segment(段),分别用来指定const段和data段,这部分内容暂时不深入,以后有机会再说(挖坑o(一︿一+)o)

想了解的可参考:

(https://docs.microsoft.com/en-us/cpp/preprocessor/const-seg?view=msvc-160)

(https://docs.microsoft.com/en-us/cpp/preprocessor/data-seg?view=msvc-160)

再来说说括号中的内容:`".CRT$XLF"`的含义:

CRT表示使用C Runtime 机制

X表示 标识名随机

L表示 TLS Callback section

F也可以替换成B~Y的任意一个字符

------

### TLS回调函数实例

```c
#include <stdio.h>
#include<windows.h>
//编译器声明使用TLS
#ifdef _WIN64                //64位
#pragma comment (linker, "/INCLUDE:_tls_used")
#pragma comment (linker, "/INCLUDE:tls_callback_func")
#else                                //32位
#pragma comment (linker, "/INCLUDE:__tls_used")
#pragma comment (linker, "/INCLUDE:_tls_callback_func")
#endif

//定义TLS回调函数
void NTAPI tls_callback(PVOID Dllhandle, DWORD Reason, PVOID Reserved) {
        switch (Reason)
        {
        case DLL_PROCESS_ATTACH: {
                printf("DLL_PROCESS_ATTACH \n");
                break;
        }
        case DLL_THREAD_ATTACH: {
                printf("DLL_THREAD_ATTACH\n");
                break;
        }
        case DLL_THREAD_DETACH: {
                printf("DLL_THREAD_DETACH\n");
                break;
        }
        case DLL_PROCESS_DETACH: {
                printf("DLL_PROCESS_DETACH\n");
                break;
        }
        default:
                break;
        }
}

#ifdef _WIN64                                                        //64位
#pragma const_seg(".CRT$XLF")
EXTERN_C const
#else
#pragma data_seg(".CRT$XLF")                //32位
EXTERN_C
#endif
PIMAGE_TLS_CALLBACK tls_callback_func[] = { tls_callback,0 };
#ifdef _WIN64                                                        //64位
#pragma const_seg()
#else
#pragma data_seg()                                        //32位
#endif //_WIN64

DWORD WINAPI threadFunc(LPVOID param) {
        while (true) {
                printf("线程运行\n");
                Sleep(200);
        }
        return 0;
}

int main() {
        CreateThread(0, 0, threadFunc, 0, 0, 0);
        while (true)
        {
                Sleep(-1);
        }
}
```

------

#### 代码说明

代码看似长,但其实很简单,就是注册了TLS函数,TLS函数输出调用原因

main函数里则创建一个线程,然后主线程(执行main函数的线程)不断Sleep,防止主程序执行完毕后退出

------

#### 运行结果

!(https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/image-20210808194249798.png)

可以看到先是主进程加载,然后是线程加载(main函数里的那个CreateThread),最后才是线程运行

也就是使用TLS回调函数后,每个进程/线程 的 启动/退出 都会调用到TLS回调函数,于是在TLS回调函数中检测当前程序是否被调试即可

------

## TLS反调试实例

TLS反调试在了解了TLS回调函数后就十分简单,只需要在TLS回调函数中检测调试就行了

将前面TLS回调函数实例中的TLS回调函数替换成如下即可实现反调试:

```c
void NTAPI tls_callback(PVOID Dllhandle, DWORD Reason, PVOID Reserved) {
        BOOL ret;
        CheckRemoteDebuggerPresent(GetCurrentProcess(), &ret);
        if (ret) {
                ExitProcess(0);
        }       
}
```

稍微介绍一下`CheckRemoteDebuggerPresent`这个检测,其检测原理和`IsDebuggerPresent`差不多,`IsDebuggerPresent`在[【原创】反调试实战系列一 x64dbg+IDA 过IsDebuggerPresent](https://www.52pojie.cn/thread-1432590-1-1.html) 中已经说明,没看过的可以前往回顾

与`IsDebuggerPresent`不同的是`CheckRemoteDebuggerPresent`可以检测其他进程的调试情况,关于`IsDebuggerPresent`和`CheckRemoteDebuggerPresent`的检测原理会在后续补充,这里给出`CheckRemoteDebuggerPresent`的函数原型

```c
BOOL CheckRemoteDebuggerPresent(
HANDLE hProcess,
PBOOLpbDebuggerPresent
);
```

|      | 数据类型 | 数据说明               |
| ------ | -------- | ---------------------- |
| 参数1| HANDLE   | 待检测的进程句柄       |
| 参数2| PBOOL    | 用于接收检测的结果   |
| 返回值 | BOOL   | 返回检测的执行是否成功 |

------

## TLS在PE文件中体现

待补充…………

------

## DebuggerPresent检测原理

待补充…………

------

# 总结

本篇主要介绍了TLS以及TLS回调函数的使用,反调试在本篇中的占比相对较小

但新引入了`CheckRemoteDebuggerPresent`这个函数,并补充了`IsDebuggerPresent`和`CheckRemoteDebuggerPresent`的检测原理,算是将Windows API中提供的两个用于检测是否被调试的函数讲完了

本次的内容也延申到了PE文件结构,旨在拓展各个知识的联系程度,希望能够对大家有所帮助( •̀ ω •́ )✧

> PS:最近忙着社畜和自我充电,所以随缘佛系更新,很多大家的回复都没精力去一一细看和回复,这里先说声抱歉啦(>人<;)
>
> 如果有实在严重的问题(例如:文章内容有谬误,会给大家带来困扰之类的),可以私信我,指出问题后私信花的CB可以找我报销(ノω<。)ノ))☆.。
>
> 还有本人暂时没有精力接单,不用私信我了,目前还是以分享知识和个人成长为主(^^ゞ

IBinary 发表于 2021-8-10 09:52

总结下,就是定义了一个表. 表里面有回调. 回调函数里面就是编写我们的检测功能.更甚者可以触发异常自己来处理. (没尝试)
Tls 局部存储 之前写的,大家可以看看

jbd666888 发表于 2021-8-22 20:48

hi,这是我用百度网盘分享的文件~复制这段内容打开「百度网盘」APP即可获取。
链接:https://pan.baidu.com/s/1fhnUbMUA3jLWHnlnW4dC2Q
提取码:7Mj7 求更新,无法使用了

侃遍天下无二人 发表于 2021-8-8 23:26

我最近也忙着干活呢

温柔的一哥 发表于 2021-8-9 16:56

感谢分享!辛苦了~吾爱有你更精彩!期待楼主分享更多好文章,好技术!

努力加载中 发表于 2021-8-9 17:20

好东西感谢大佬{:1_893:}

cjc3528 发表于 2021-8-10 06:09

太厉害了,膜拜大神啊

sunnyAlvis 发表于 2021-8-10 18:22

学习了很多加油

国际豆哥 发表于 2021-9-15 23:31

久违的上线看看我的好兄弟顺便打上一波评分

11987GENIUS 发表于 2022-6-6 15:39

牛啊牛啊
页: [1] 2 3
查看完整版本: 【原创】反调试实战系列二 TLS反调试+CheckRemoteDebuggerPresent原理