TLS回调函数–一文看懂(详)

TLS回调函数–一文看懂(详)


跳转:chillysome‘s home

学习的时候看到了很多博客,虽然大同小异,但是受益匪浅。

在此总结一下(缝补匠),并以一道CTF例题总结

耐心看就会懂,可能需要一些PE文件前置知识。

另外在搜索资料过程中,发现一些师傅的代码只能在x86下生成才有效,因此更新了一下代码,使其可以运行在x86/x64下运行。

一、初识TLS

Thread Local Storage,线程局部存储:各线程独立的数据存储空间。使用TLS技术可以在线程内部独立使用或修改进程的全局数据或静态数据, 就像对待自身的局部变量一样

TLS回调函数常用于反调试,主要是利用了TLS回调函数的调用要先于EP代码的执行

如果开启了TLS功能,PE文件头就会设置TLS表,IMAGE_NT_HEADERS-IMAGE_OPTIONAL_HEADER-IMAGE_DATA_DIRECTORY[9]描述了IMAGE_TLS_DIRECTORY结构体的位置。

IMAGE_TLS_DIRECTORY:

图片描述
对其中各参数的描述:

StartAddressOfRawData:tls模板在内存中的起始VA,模板是用于创建线程时初始化TLS数据的,可以看到模板中的内容其实就是TLS中创建的变量

EndAddressOfRawDataL:tls模板在内存中的结束VA

AddressOfIndex:存储TLS索引的位置

AddressOfCallBacks指向TLS注册的回调函数的函数指针(地址)数组

SizeOfZeroFill:用于指定非零初始化数据后面的空白空间的大小

Characteristics:属性

逆向过程中比较重要的成员就是AddressOfCallBacks,指向含有TLS回调函数地址的数组,进程在启动运行时,系统会逐一调用储存在该数组中的函数。


二、TLS回调函数

每当创建或终止进程的线程时会自动调用执行的函数。当然,创建进程的主线程的时候也会自动调用回调函数,且其执行先于EP代码。反调试技术就是利用的TLS回调函数的这一特征。

图片描述
参数顺序和定义都是一样的。第一个参数表示模块句柄,第二个参数表示调用TLS回调函数的原因。

对于第二个参数Reaseon:

图片描述

调用原因有四种<重要>

#define DLL_PROCESS_ATTACH 1
#define DLL_THREAD_ATTACH 2
#define DLL_THREAD_DETACH 3
#define DLL_PROCESS_ATTACH 0

主线程调用main前调用TLS回调函数,调用原因为DLL_PROCESS_ATTACH

子线程启动前调用TLS,原因为DLL_THREAD_ATTACH

子线程结束后调用TLS,原因为DLL_THREAD_DETACH

主线程结束后调用TLS 原因为DLL_PROCESS_DETACH


三、实例代码 x86/64

下述代码可以用x64/x86模式下进行调试.

x86和x64下对于Tls的使用有所不同,如以下代码所示。

#include <windows.h>



//
/
// 
//告知连接器使用TLS,在x86和x64链接方式有所不同

#ifdef _WIN64
#pragma comment(linker,"/INCLUDE:_tls_used")
#else
#pragma comment(linker,"/INCLUDE:__tls_used")
#endif // _WIN64

/
//



void print_console(const char* szMsg)
{
	HANDLE hStdout = GetStdHandle(STD_OUTPUT_HANDLE);
	//先于主线程调用执行的TLS回调函数中使用printf可能会发生Runtime Error,可直接调用WriteConsole API
	WriteConsoleA(hStdout, szMsg, strlen(szMsg), NULL, NULL);
}

void NTAPI TLS_CALLBACK1(PVOID DllHandle, DWORD Reason, PVOID Reserved)
{
	char szMsg[80] = { 0, };
	wsprintfA(szMsg, "TLS_CALLBACK1() : DllHandle = %X, Reason = %d\n", DllHandle, Reason);
	print_console(szMsg);
}

void NTAPI TLS_CALLBACK2(PVOID DllHandle, DWORD Reason, PVOID Reserved)
{
	char szMsg[80] = { 0, };
	wsprintfA(szMsg, "TLS_CALLBACK2() : DllHandle = %X, Reason = %d\n", DllHandle, Reason);
	print_console(szMsg);
}


//
/
					/*
						注册TLS函数
						.CRT$XLX的作用
						CRT表示使用C Runtime 机制
						X表示表示名随机
						L表示TLS Callback section
						X也可以换成B~Y任意一个字符
					*/
extern "C"
#ifdef _WIN64
#pragma const_seg(".CRT$XLX")
const
#else
#pragma data_seg(".CRT$XLX")
#endif
/
//


//存储回调函数地址
PIMAGE_TLS_CALLBACK pTLS_CALLBACKs[] = { TLS_CALLBACK1, TLS_CALLBACK2, 0 };
#pragma data_seg()

//线程入口函数
DWORD WINAPI ThreadProc(LPVOID lParam)
{
	print_console("ThreadProc() start\n");

	print_console("ThreadProc() end\n");

	return 0;
}

int main(void)
{
	HANDLE hThread = NULL;

	print_console("main() start\n");
	//创建子线程
	hThread = CreateThread(NULL, 0, ThreadProc, NULL, 0, NULL);
	//等待子线程结束
	WaitForSingleObject(hThread, 60 * 1000);
	CloseHandle(hThread);

	print_console("main() end\n");

	return 0;
}

运行结果:

image-20240314232017687


四、Tls反调试

#include <Windows.h>
#include <stdio.h>

// linker spec 通知链接器PE文件要创建TLS目录
#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

void NTAPI __stdcall TLS_CALLBACK(PVOID DllHandle, DWORD dwReason, PVOID Reserved)
{
	if (IsDebuggerPresent())
	{
		MessageBoxW(NULL, L" TLS_CALLBACK: 请勿调试本程序 !", L"TLS Callback", MB_ICONSTOP);
		ExitProcess(0);
	}
}

// 创建TLS段
EXTERN_C
#ifdef _M_X64
#pragma const_seg (".CRT$XLB")
PIMAGE_TLS_CALLBACK _tls_callback = TLS_CALLBACK;
#else
#pragma data_seg (".CRT$XLB")
PIMAGE_TLS_CALLBACK _tls_callback = TLS_CALLBACK;
#endif

int main(int argc, char* argv[])
{
	system("pause");
	return 0;
}

五、调试Tls回调函数

如何找到Tls

若在编程中启用了TLS功能,PE头文件中就会设置TLS表(IMAGE_NT_HEARDERS->IMAGE_OPTIONAL_HEADER->IMAGE_DATA_DIRECTORY[9])

img

可以看到TLS Table的RVA是00009310,找到对应位置如下

img

TLS Table中比较重要的成员为AddressOfCallbacks,该值指向含有TLS回调函数地址(VA)的数据(一个程序中可以注册多个TLS回调函数)

Ollydbg

  1. 修改OllyDbg(2.0)选项就可以调试TLS回调函数。
    在这里插入图片描述

  2. 重启应用程序,程序会暂停在TLS回调函数,
    在这里插入图片描述

ida.pro

在函数窗口中可以直接找到TLS回调函数

image-20240315000127139

六、手动去除TLS函数

此时,在IDA中的导出窗口中,我们可以看到两个TLS函数以及start

下面就进行 TLS的手动去除

1.首先使用PEview打开目标文件

找到了TLS在头部的位置,在winhex中打开,找到1B0位置

将这一块填充为0

2.之后再找到TLS在数据段里的位置,并在winhex里将其填充为0

很明显,就是这一段

之后就可以保存文件,大功告成了


七、手工添加TLS回调函数

使用的工具

  • 010editor
  • OllyDgbg

规划设计

手动添加TLS,需要知道IMAGE_TLS_DIRECTORY结构体与TLS回调函数放到PE文件的哪些位置。向某个PE文件添加代码或者数据时候有如下三种来查找合适的位置。

  • 增加最后一个节区域的大小
  • 添加到节区域末尾的空白处
  • 在最后添加新节区
    本次使用第一种增加最后一个节区的大小。

手动添加TLS回调函数

更改节区大小

使用010editor可以看到最后一个节区的基本信息,Pointer to Raw Date = 9000,size of Raw Data =200.所以PE头中定义的文件整体大小为9200h字节。

本次实验我们将最后一个节区增加200h字节,使最后一个节区大小为9400h。

更改节区属性

节区的属性更改为可读可写可执行
增加200h字节后VA,是按照section Alignment值对其,因该值未超过1000,所以该值不需要改变。

扩大节区200个字节

在9200处添加200个字节大小的空间。

在新增加的空间中增加TLS结构体

做完以上基本信息后,需要在9200地址处创建IMAGE_TLS_DIRECTORY结构体,AddressOfCallBacks成员的值为VA 40c280.(文件偏移为9280h)。他是Array of TLS callback function的起始地址。只要把TLS回调函数的地址9290h放入到40c280回调函数数组中(文件偏移为9280h),即可成功注册TLS回调函数。

利用OllyDebug的汇编功能,在程序的回调函数地址处编写回调函数。在下图中按空格键,编写汇编语言。

利用OD编写回调函数代码

由上文中介绍的IMAGE_TLS_CALLBACK代码中可以知道,在esp+8的位置是reason参数。跟踪函数可以知道FS:[30]处指向PEB函数地址。通过PEB的结构体可以知道eax+2 指向了PEB.Beingbugged。

将编写的程序另存为新文件

将修改的内存全部选中,右键单击代码-弹出选择—保存文件菜单。另存为Manual_hello_Tls1.exe


八、CTF re [NewStarCTF 2023 公开赛道]k00h_slT

[题目链接](https://buuoj.cn/challenges#[NewStarCTF 2023 公开赛道]k00h_slT)

Main

进入主函数,发现输出和输入函数,将其重命名 (快捷键N)

sub_401950 -> printf

sub_401990 -> scanf

重点分析StartAddress函数(其中的sub_401500是base64编码)

image-20240122210547921

StartAddress

image-20240122210916982

发现并没有做什么,Tab查看汇编。

image-20240122211040308

发现存在IDA未检测出的灰色区域,其原因是在函数结束之前retn了.

.text:00401773 C后,发现是花指令。用nop的方式一直解决到函数结束即可。其间藏了个死循环,若不解决无法动态调试。解决后F5查看反汇编代码如下图所示。

image-20240122212729960

其中的sub_401500是base64编码.

image-20240122213700490

TlsCallback_0

发现函数列表中还有 TlsCallback_0

image-20240122213905549

分析可以知道:

  • case0: check
  • case1: 反调试
  • case2: 注意到144=0x90 所以这里检测的是Tlscallback函数自身有没有被nop 若有则修改一些关键数据 所以我们不能nop来反调试
  • case3: 这里其实是对sub401500中调用的函数进行了修改

image-20240122214421663

看到VirtualProtect函数就知道有修改代码的可能。

关于Tls_callback参数:
Tls_callback的第二个参数Reason可以有四个值

分别是DLL_PROCESS_ATTACHDLL_THREAD_ATTACHDLL_THREAD_DETACHDLL_PROCESS_DETACH

因此对应顺序为 1-2-3-0

整个程序流程:

  • 首先程序启动,先通过case 1检测是否存在调试器,如果存在则直接卡死进程
  • 然后通过main函数中的CreateThread创建了一个线程,然后由于有WaitForSingleObject的存在,main函数卡在这条指令等待线程执行结束。线程进入后先通过case 2nop检测,然后对4A7000进行修改。
    动调进入sub_4013B0 运行到此处即可得到key的值
  • 随后反调试之进程检测,然后调用了sub_401500,此时这个函数的内容对应的是sub_401170函数即base64函数。
  • 调用完后退出线程的时候进入case 3sub_401500的call被修改成为了sub_4013B0
  • 退出线程后,进入main函数中的sub_401500,进行了xxtea的加密。下面比较不相同因此不会触发假flag的提示。
  • 程序所有进程退出,进入了Tlscase 0case 0是最后的check函数,通过检测后程序输出Congratulations

动调过程

其中会碰到.text:004017A9 jmp loc_4017A9,直接跳到下一句就好。

image-20240122220246608

case3和主函数sub_401500处下断点,进入观察再经过修改后的sub_401500中的call被修改成为了sub_4013B0

image-20240122220452548

P命令创建一个函数,发现是xxtea加密。

image-20240122220641371

密文在case0中可以找到,可同时找到base64变种密码表

image-20240122221339424

找key:

img

加密解密过程

加密

  1. 改变base64密码表
  2. 进行魔改base64加密
  3. xxtea加密

解密

  1. 找到魔改密码表和key
  2. xxtea解密
  3. 魔改base64解密
#include <stdio.h>
#include <stdint.h>
#define DELTA 0x9e3779b9
#define MX (((z>>5^y<<3) + (y>>3^z<<4)) ^ ((sum^y) + (key[(p&3)^e] ^ z)))
 
void btea(uint32_t *v, int n, uint32_t const key[4])
{
    uint32_t y, z, sum;
    unsigned p, rounds, e;
    if (n > 1)            /* Coding Part */
    {
        rounds = 6 + 52/n;
        sum = 0;
        z = v[n-1];
        do
        {
            sum += DELTA;
            e = (sum >> 2) & 3;
            for (p=0; p<n-1; p++)
            {
                y = v[p+1];
                z = v[p] += MX;
            }
            y = v[0];
            z = v[n-1] += MX;
        }
        while (--rounds);
    }
    else if (n < -1)      /* Decoding Part */
    {
        n = -n;
        rounds = 6 + 52/n;
        sum = rounds*DELTA;
        y = v[0];
        do
        {
            e = (sum >> 2) & 3;
            for (p=n-1; p>0; p--)
            {
                z = v[p-1];
                y = v[p] -= MX;
            }
            z = v[n-1];
            y = v[0] -= MX;
            sum -= DELTA;
        }
        while (--rounds);
    }
}
 
 
int main()
{
    uint32_t v[]= {0x3400A0D0, 0xB23CFFEB, 0xCDE69111, 0x032D0771, 0xFA1D9E6C, 0x9D15360A, 0x933EBF03, 0x9F12DDA6, 
    0x8C58DDA1, 0x46BEE3E0, 0x04476F65, 0x3C44CEF9};
    uint32_t const k[4]= {0x75,0x404,0xBF,0X2652};
    int n= -12; 
    btea(v, n, k);
    for(int i=0;i<12;i++)
    	for(int j=0;j<4;j++){
    		printf("%c",(v[i]>>(8*j))&0xff);
		}
    return 0;
}

拿到base变表(一样的动调提取即可 )加密后的
48Pt4WXo+yhqh0GzFAbRg0XqgFhq4UOl+UwqL8wUHy6rEFSQ
用base64变表解一下就可以得到flag
img

  • 32
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值