系列
【原创】反调试实战系列一 x64dbg+IDA 过IsDebuggerPresent
【原创】反调试实战系列二 TLS反调试+CheckRemoteDebuggerPresent原理
前言
在反调试系列开篇中介绍了简单的反调试的手段:IsDebuggerPresent,这次继续介绍常用的反调试手段:TLS反调试
PS:在第一篇反调试实战中是通过不断创建线程来检测当前进程是否被调试,而使用TLS反调试则无需我们自己去不断创建线程来进行检测,因为TLS也是基于线程的
TLS反调试
什么是TLS
官方版
给出来源于官方的文档释义:Thread Local Storage (TLS) | Microsoft Docs
TLS全称Thread Local Storage
,即线程局部存储。TLS是一种方法,通过这种方法,给定多线程进程中的每个线程可以分配位置来存储特定于线程的数据。通过TLS API (TlsAlloc)支持动态绑定(运行时)特定于线程的数据。Win32和Microsoft c++编译器现在除了现有的API实现外,还支持静态绑定(加载时)每个线程数据。
通俗版
抓住官方版中的关键词:方法、存储特定于线程数据、绑定
方法
TLS是一种编程方法
存储特定于线程的数据
TLS使用静态(static)或全局局部内存(global memory local)对线程数据进行存储,TLS变量是使用(GS/FS)J扩展段寄存器访问的,而非DS数据段寄存器(有关段寄存器可回顾:保护模式笔记二 段寄存器)
PS:是不是还是有点懵逼,不要紧,这部分释义大致看看就好,后面才是重点
绑定
绑定分为动态绑定和静态绑定
动态绑定
给出动态绑定官方文档:Using Thread Local Storage - Win32 apps | Microsoft Docs
所谓动态绑定就是使用:TlsAlloc、 TlsSetValue、 TlsGetValue 、TlsFree四个API对创建的线程进行绑定
由于本次的TLS反调试并没有用到动态绑定,并且官方给出的文档还附有实例,故这里不再赘述,有兴趣的小伙伴可以自行研究= ̄ω ̄=
静态绑定
静态绑定则是本次反调试所用到的方法,在之后会具体说明
TLS的意义
TLS主要是为了解决多线程中变量的同步问题
进程中的全局变量和函数内定义的静态(static)变量,是每个线程都可以访问的共享变量。只要有任何一个线程修改了共享变量,其他所有线程中的共享变量也会同步被修改
乍看之下,这种方式使得数据交换十分的便捷,无需额外的通信就可以实现数据之间的交换。但命运赠送的礼物,早已在暗中标好了价格( $ _ $ )
。这里的价格就是多线程访问时所耗费的同步开销
比如当共享变量要修改前,需要对该变量上锁,其他要访问该共享变量的线程必须等共享变量修改完成释放锁后才能继续执行。这期间线程等待所耗费的资源就是所谓的开销。关于同步异步、并发并行、互斥信号量等基础知识这里不再赘述
TLS变量
TLS变量即:同一个线程里面调用的各个函数都可以访问、但其他线程无法访问的变量(被称为static memory local to a thread 线程局部静态变量)
举个例子:线程A 修改TLS变量后线程B中的TLS变量不受影响,因为每个线程中都有一个TLS变量副本
TLS变量实例
TLS变量的声明
_declspec(thread) int global=0x610;
TLS变量演示代码
#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演示结果
验证了TLS变量在线程1中被修改,但在线程2中并没有受到影响
TLS静态绑定
所谓的TLS静态绑定主要体现在TLS回调函数上
TLS回调函数
官方文档:PE Format - Win32 apps | Microsoft Docs
程序可以提供一个或多个TLS回调函数,以支持TLS数据对象的附加初始化和终止
尽管通常只有一个回调函数,但回调函数是作为数组实现的,以便在需要时可以添加额外的回调函数
如果有多个回调函数,则按其地址在数组中出现的顺序调用每个函数。空指针终止数组。空列表是完全有效的(不支持回调),在这种情况下,回调数组只有一个成员—— null ptr(空指针)
TLS回调函数的使用
下面关于TLS回调函数的使用参考了:c++ - about TLS Callback in windows - Stack Overflow
要在C/C++ 中使用TLS函数步骤如下:
- 编译器声明使用TLS
- 定义TLS回调函数
- 注册TLS回调函数
编译器声明
通过下列代码告诉编译器要使用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
注意64位和32位的TLS编译器声明不同,所以上述代码使用了条件编译
定义TLS回调函数
TLS回调函数的定义如下:
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回调函数
#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)
想了解的可参考:
const_seg pragma | Microsoft Docs
data_seg pragma | Microsoft Docs
再来说说括号中的内容:".CRT$XLF"
的含义:
CRT表示使用C Runtime 机制
X表示 标识名随机
L表示 TLS Callback section
F也可以替换成B~Y的任意一个字符
TLS回调函数实例
#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,防止主程序执行完毕后退出
运行结果
可以看到先是主进程加载,然后是线程加载(main函数里的那个CreateThread),最后才是线程运行
也就是使用TLS回调函数后,每个进程/线程 的 启动/退出 都会调用到TLS回调函数,于是在TLS回调函数中检测当前程序是否被调试即可
TLS反调试实例
TLS反调试在了解了TLS回调函数后就十分简单,只需要在TLS回调函数中检测调试就行了
将前面TLS回调函数实例中的TLS回调函数替换成如下即可实现反调试:
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 中已经说明,没看过的可以前往回顾
与IsDebuggerPresent
不同的是CheckRemoteDebuggerPresent
可以检测其他进程的调试情况,关于IsDebuggerPresent
和CheckRemoteDebuggerPresent
的检测原理会在后续补充,这里给出CheckRemoteDebuggerPresent
的函数原型
BOOL CheckRemoteDebuggerPresent(
HANDLE hProcess,
PBOOL pbDebuggerPresent
);
|
数据类型 |
数据说明 |
参数1 |
HANDLE |
待检测的进程句柄 |
参数2 |
PBOOL |
用于接收检测的结果 |
返回值 |
BOOL |
返回检测的执行是否成功 |
TLS在PE文件中体现
待补充…………
DebuggerPresent检测原理
待补充…………
总结
本篇主要介绍了TLS以及TLS回调函数的使用,反调试在本篇中的占比相对较小
但新引入了CheckRemoteDebuggerPresent
这个函数,并补充了IsDebuggerPresent
和CheckRemoteDebuggerPresent
的检测原理,算是将Windows API中提供的两个用于检测是否被调试的函数讲完了
本次的内容也延申到了PE文件结构,旨在拓展各个知识的联系程度,希望能够对大家有所帮助( •̀ ω •́ )✧
PS:最近忙着社畜和自我充电,所以随缘佛系更新,很多大家的回复都没精力去一一细看和回复,这里先说声抱歉啦(>人<;)
如果有实在严重的问题(例如:文章内容有谬误,会给大家带来困扰之类的),可以私信我,指出问题后私信花的CB可以找我报销(ノω<。)ノ))☆.。
还有本人暂时没有精力接单,不用私信我了,目前还是以分享知识和个人成长为主(^^ゞ