一开始想这节的名字叫做加密TLS表,后来感觉TLS表的加密实在是有些不符合。主要是出于TLS节加密的必要性考虑。所以改成处理TLS表了。我自身喜欢把TLS利用成模糊入口点来利用。此节规划是这样的,我们先理解一下什么是TLS以及它的结构。接下来汇编一个TLS节及相关在MASM中的应用。最后,我们讲解如何修改现有的TLS以及制作TLS入口点。
什么是TLS表
这个问题最有力的参考莫过于MSDN的文档了。这种源的东西比起用我的语言归纳要清楚的多。
MSDM文档的连接为
ms-help://MS.MSDNQTR.v80.chs/MS.MSDN.v80/MS.VisualStudio.v80.chs/dv_vclang/html/a0f1b109-c953-4079-aa10-e47f5483173d.htm
引用自MSDN
文档中还有些关于TLS变量的使用地点这里总结一下:
TLS变量只能定义变量
TLS变量只能声明为公共静态的
TLS变量不能修饰函数
不能在函数内定义,不能作为函数的参数
在正常代码内不能引用TLS变量
这些编译方面上的限制,应该是为代码安全与稳定性上设定的。毕竟,TLS是作为多线程一部分使用的。在内存访问上没有明确的限制(在运行代码中自行定位TLS变量并引用是没有问题的)。
下面我们了解一下TLS表的结构。
TLS表的目录索引处在数据目录的第9个索引上,可以使用微软提供的宏IMAGE_DIRECTORY_ENTRY_TLS来直接定位。
这个目录的VirtualAddress指向一个IMAGE_TLS_DIRECTORY32的结构,如下所示:
IMAGE_TLS_DIRECTORY32 STRUCT
StartAddressOfRawData dd ?
EndAddressOfRawData dd ?
AddressOfIndex dd ?
AddressOfCallBacks dd ?
SizeOfZeroFill dd ?
Characteristics dd ?
IMAGE_TLS_DIRECTORY32 ENDS
这里解释一下
StartAddressofRawData为TLS数据起始的地址
EndAddressOfRawData为结束的地址
AddressOfIndex是TLS索引的地址
AddressOfCallBacks指向一组以NULL结尾的IMAGE_TLS_CALLBACK函数的地址数组
以上都为地址,这里使用的是VA,而不是RVA了。当修改时这是要注意的地方。
SizeOfZeroFill表明填充TLS变量区域的大小
Characteristics用作保留,可能用作TLS标志。
在一个EXE程序访问TLS变量时要经过以下步骤。
(这里需要注意的是DLL访问TLS变量除非这个DLL是静态链接到这个EXE中,否则使用LoadLibrary动态加载的DLL是不会加载TLS变量的)
1.链接时。连接器设置TLS目录的AddressOfIndex字段。这个字段指向一个地址。在这个地址保存了程序所用的TLS索引。
2.当线程创建的时候,通常在TEB开得2ch处位置防止一个指向TLS数组的指针。TEB的地址放入FS中保存所以可以使用FS[2ch]引用此TLS数组。
3.我们在可执行代码中使用1提供的TLS索引与2提供的TLS数组的地址来使用TLS所提供的数据。
需要注意的是TLS数组在每条线程都有维护的一个区域。这个数组的每个地址指出了程序中给定模块的TLS数据区的位置。TLS索引指出了这个数组的哪个元素。
这里给出一段使用TLS的代码。读者可以自行参阅MSDN的文档深入理解。
代码:
Start: invoke MainFunc invoke ExitProcess, 1 CommonFunc proc LOCAL lpvData : LPVOID LOCAL dwThreadId : DWORD invoke TlsGetValue, dwTlsIndex .IF eax == 0 ret .ENDIF mov lpvData, eax invoke Sleep, 5000 ;; 显示信息 invoke GetCurrentThread mov dwThreadId, eax invoke PrintLine, offset g_szOutThread, addr dwThreadId invoke PrintLine, offset g_szOutData, addr lpvData ret CommonFunc endp ThreadFunc proc LOCAL lpvData : LPVOID LOCAL dwThreadId : DWORD invoke LocalAlloc, LPTR, 256 mov lpvData, eax invoke TlsSetValue, dwTlsIndex, eax .IF eax == 0 ret .ENDIF invoke GetCurrentThread mov dwThreadId, eax invoke PrintLine, offset g_szOutThread, addr dwThreadId invoke PrintLine, offset g_szOutData, addr lpvData ;; 调用公共函数 invoke CommonFunc ;; 释放内存 invoke TlsGetValue, dwTlsIndex .IF eax != 0 invoke LocalFree, lpvData .ENDIF ret ThreadFunc endp MainFunc proc LOCAL IDThread : DWORD LOCAL hThread[THREADCOUNT] : HANDLE invoke TlsAlloc .IF eax == TLS_OUT_OF_INDEXES ret .ENDIF mov dwTlsIndex, eax xor ecx, ecx mov ebx, THREADCOUNT CreateThreadLoop: cmp ecx, ebx jz EndCreateThreadLoop push ecx invoke CreateThread, NULL, 0, ThreadFunc, NULL, 0, addr IDThread pop ecx mov hThread[ecx*sizeof HANDLE], eax inc ecx jmp CreateThreadLoop EndCreateThreadLoop: xor ecx, ecx mov ebx, THREADCOUNT WaitThreadLoop: cmp ecx, ebx jz EndWaitThreadLoop push ecx invoke WaitForSingleObject, hThread[ecx*sizeof HANDLE], INFINITE pop ecx inc ecx jmp WaitThreadLoop ;; 是否TLS索引 invoke TlsFree, dwTlsIndex ret EndWaitThreadLoop: ret MainFunc endp PrintLine proc uses eax ebx ecx edx edi esi pFormat : LPSTR, pBuffer : LPSTR ;; it's must be include masm32.inc and masm32.lib LOCAL szBuf[MAX_PATH] : BYTE mov ebx, pFormat mov edx, pBuffer invoke wsprintf, addr szBuf, ebx, edx invoke StdOut, addr szBuf mov ebx, offset CRLF invoke StdOut, ebx ret CRLF db 0Dh,0Ah,0 PrintLine endp end Start
接下来我们介绍一下关于TLS回调函数这个东西,这个东西对于编写病毒来说直安逸的。大多数调试器是从PE加载器开始加载,几乎都跳过读取此入口的。呵呵。话说壳技术也是从病毒技术发展而来的。是先有病毒后有壳的,他们两者有太多相似的地方。不过貌似安全圈里很多的技术都是从Anti-安全到安全过渡的。想想rootkit的技术不也一样吗!
这个回调函数的地址是由AddressOfCallBacks字段指出的。指向的地址是一个由IMAGE_TLS_CALLBACK函数组成的数组并以 NULL做结束符。回调函数的顺序是依次调用。这个TLS回调函数是在加载引入表之后,所以在引入表中的函数都可以直接使用。如果加密了引入表,在这里就需要做一些手段了。不过要注意的是切不可在这里解密引入表。否则,加密引入表就没有意义了。
回调函数的原型与DLL启动函数的样子很像。
TLS_CALLBACK proto Dllhandle : LPVOID, Reason : DWORD, Reserved : LPVOID
参数如下:
Dllhandle : 为模块的句柄
Reason可取以下值:
DLL_PROCESS_ATTACH 1 : 启动一个新进程被加载
DLL_THREAD_ATTACH 2 : 启动一个新线程被加载
DLL_THREAD_DETACH 3 : 终止一个新线程被加载
DLL_PROCESS_DETACH 0 : 终止一个新进程被加载
Reserverd:用于保留,设置为0
函数的参数是被加载器设置。我们的函数只管判断就可以。
下面就让我们使用MASM打造一个带有回调功能的TLS节的程序,最初使用见到使用MASM编写TLS相关是在一个俄国人的论坛的代码里。随后的附件中也会附带此代码名为:TlsInAsm。这里我们还是自己打造一个。在TlsInAsm中我学习到了一些关于MASM的编译选项。
代码如下
代码:
.data? dwTLS_Index dd ? ;; 这里的OPTION DOTNAME的意思为让ML可以使用以“.”开头的变量,结构,联合,标号等 ;; 开启这个选项也可以在ML中使用/Zm选项来启动。 OPTION DOTNAME ;; 定义一个TLS节 ;; SEGMENT标志在ML里是用于定义一个新节。 .tls SEGMENT TLS_Start LABEL DWORD dd 0100h dup ("slt.") TLS_End LABEL DWORD .tls ENDS OPTION NODOTNAME .data TLS_CallBackStart dd TlsCallBack0 TLS_CallBackEnd dd 0 g_szTitle db "Hello TLS",0 g_szInTls db "我在TLS里",0 g_szInNormal db "我在正常代码内",0 ;; 这里需要注意的是, ;; 必须要将此结构声明为PUBLIC,用于让连接器连接到指定的位置, ;; 其次结构名必须为_tls_uesd这是微软的一个规定。编译器引入的位置名称也如此。 PUBLIC _tls_used _tls_used IMAGE_TLS_DIRECTORY <TLS_Start, TLS_End, dwTLS_Index, TLS_CallBackStart, 0, ?> .code Start: ;; 在正常代码中使用 invoke MessageBox, NULL, addr g_szInNormal, addr g_szTitle, MB_OK invoke ExitProcess, 1 ret ;; TLS的回调函数 TlsCallBack0 proc Dllhandle : LPVOID, dwReason : DWORD, lpvReserved : LPVOID ;; 这里可以判断dwReason发生的条件 mov eax, dwReason ;; 在进行加载时被调用 cmp eax, DLL_PROCESS_ATTACH jnz ExitTlsCallBack0 invoke MessageBox, NULL, addr g_szInTls, addr g_szTitle, MB_OK mov dword ptr[TLS_Start],0 xor eax, eax inc eax ExitTlsCallBack0: ret TlsCallBack0 ENDP end Start
下面该轮到修改TLS了。这段也可以作为加解密TLS用,但是个人感觉不到TLS加解密的必要,纯属一个可选的选项。不过用作Anti-debug是个用途。yC壳中也是将TLS进行了一个简单的移动。而在加密时掠过以.tls为名的节,这个就会造成不稳定性。而完美的处理TLS比较麻烦,如果TLS回调函数不止一个,我们必须要读取所有的TLS回调函数并判断它的结束当然这需要一个反汇编引擎来做。而有些加入Anti-反汇编代码的也会产生读取错误,最后还要做一些重定位的工作。考虑至此我们此刻的代码功能是这样的,首先复制TLS表,然后进行一个TLS入口点的替换。最后跳转回TLS真正的回调函数中。由于TLS程序并不常见,读者可以使用以上提供的程序进行测试。
现在我们来描述一下算法:
现在我们有这样几种情况
1.有TLS表
1.1有TLS回调函数
修改AddressOfCallBacks字段指向区域的第一个回调函数的VA为我们自己回调函数的VA
1.2无TLS回调函数
设置一个新的TLS回调函数地址表,并将新的回调函数地址表的VA设置给它
2.无TLS表
汇编一个新的TLS表
在我们自己的回调函数中也有不一样的地方。
如果是1.1的情况那么要记录原先第一个回调函数的VA并且最后进行跳转
如果是1.2的情况那么直接返回即可.(最后不要忘记清除参数所造成的堆栈开销)
代码:
ModifyTLS proc szFileName : LPSTR LOCAL hFile : HANDLE LOCAL hMap : HANDLE LOCAL pMem : LPVOID LOCAL dwOrigFileSize : DWORD LOCAL dwNTHeaderAddr : DWORD LOCAL dwNewTlsSectionOffset : DWORD ;; init data xor eax, eax mov g_bError, al mov eax, OWNTLS_SIZE mov g_dwNewSectionSize, eax ;; open file invoke CreateFile, szFileName,\ GENERIC_WRITE + GENERIC_READ,\ FILE_SHARE_WRITE + FILE_SHARE_READ,\ NULL,\ OPEN_EXISTING,\ FILE_ATTRIBUTE_NORMAL,\ 0 .IF eax == INVALID_HANDLE_VALUE jmp OpenFileFailed .ENDIF mov hFile, eax invoke GetFileSize, hFile, NULL .IF eax == 0 invoke CloseHandle, hFile jmp GetFileSizeFailed .ENDIF mov dwOrigFileSize, eax add eax, APPEND_SIZE xchg eax, ecx ;; create memory map xor ebx, ebx invoke CreateFileMapping, hFile, ebx, PAGE_READWRITE, ebx, ecx, ebx .IF eax == 0 invoke CloseHandle, hFile jmp CreateMapFailed .ENDIF mov hMap, eax ;; map file to memory invoke MapViewOfFile, hMap, FILE_MAP_WRITE+FILE_MAP_READ+FILE_MAP_COPY, ebx, ebx, ebx .IF eax == 0 invoke CloseHandle, hMap invoke CloseHandle, hFile jmp MapFileFailed .ENDIF mov pMem, eax ;; check it's PE file or not ? xchg eax, esi assume esi : ptr IMAGE_DOS_HEADER .IF [esi].e_magic != 'ZM' invoke UnmapViewOfFile, pMem invoke CloseHandle, hMap invoke CloseHandle, hFile jmp InvalidPE .ENDIF add esi, [esi].e_lfanew assume esi : ptr IMAGE_NT_HEADERS .IF word ptr [esi].Signature != 'EP' invoke UnmapViewOfFile, pMem invoke CloseHandle, hMap invoke CloseHandle, hFile jmp InvalidPE .ENDIF mov dwNTHeaderAddr, esi ;; 增加一个新节,此节为我们自己的TLS节 invoke AddSection, pMem, offset g_szNewSectionName, g_dwNewSectionSize mov dwNewTlsSectionOffset, eax ;; 寻找原始的TLS节并且记录它 mov eax, dword ptr [esi].OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_TLS * sizeof IMAGE_DATA_DIRECTORY].VirtualAddress .IF eax == 0 ;; 如果没有TLS节则汇编一个新的 ;; 设置标志变量 mov eax, offset OwnTLS_OrigYes push 0 pop dword ptr [eax] jmp NoTLSCallBack .ENDIF invoke RVA2Offset, pMem, eax add eax, pMem ;; 复制这个表到我们自己的内存 mov esi, eax lea edi, Orig_TLS mov ecx, sizeof IMAGE_TLS_DIRECTORY cld rep movsb ;; 汇编一个新的TLS节并设置回调函数 ;; 这里检验是否存在回调函数 mov eax, offset Orig_TLS assume eax : ptr IMAGE_TLS_DIRECTORY mov eax, dword ptr [eax].AddressOfCallBacks ;; 首先将VA转换为RVA然后转换为FVA mov esi, dwNTHeaderAddr assume esi : ptr IMAGE_NT_HEADERS sub eax, dword ptr [esi].OptionalHeader.ImageBase invoke RVA2Offset, pMem, eax add eax, pMem mov ebx, dword ptr [eax] test ebx, ebx jz NoTLSCallBack ;; 保存原回调函数的地址 mov edx, offset OwnTLS_OrigCallBackEntry mov dword ptr [edx], ebx ;; 有回调函数这修改,修改位置为eax指向的地方 mov ebx, dwNewTlsSectionOffset assume ebx : ptr IMAGE_SECTION_HEADER mov ebx, dword ptr [ebx].VirtualAddress add ebx, dword ptr [esi].OptionalHeader.ImageBase mov dword ptr [eax], ebx ;; 设置TLS回调跳转标志 mov eax, offset OwnTLS_OrigYes push 1 pop dword ptr [eax] jmp ContinueNoTLSCallBack NoTLSCallBack: ;; 如果没有回调函数,则重新建立一个回调函数地址表 ;; 并且设置新的TLS目录的字段 mov ebx, dwNewTlsSectionOffset assume ebx : ptr IMAGE_SECTION_HEADER mov edx, dword ptr [ebx].VirtualAddress add edx, offset OwnTLS_CallBackStart - offset OwnTLS_Section add edx, dword ptr [esi].OptionalHeader.ImageBase mov eax, offset Orig_TLS assume eax : ptr IMAGE_TLS_DIRECTORY mov dword ptr [eax].AddressOfCallBacks, edx ;; 设置AddressOfCallBacks sub edx, offset OwnTLS_CallBackStart - offset OwnTLS_CallBack mov ebx, offset OwnTLS_CallBackStart mov dword ptr [ebx], edx ;; 设置AddressOfIndex add edx, offset OwnTLS_Index - offset OwnTLS_CallBack mov dword ptr [eax].AddressOfIndex, edx ;; 设置StartAddressOfRawData add edx, offset OwnTLS_Start - offset OwnTLS_Index mov dword ptr [eax].StartAddressOfRawData, edx ;; 设置EndAddressOfRawData add edx, offset OwnTLS_End - offset OwnTLS_Start mov dword ptr [eax].EndAddressOfRawData, edx ;; 设置SizeOfZeroFill push 0 pop dword ptr[eax].SizeOfZeroFill ContinueNoTLSCallBack: ;; 这里复制TLS到新的TLS表中 mov eax, dwNewTlsSectionOffset assume eax : ptr IMAGE_SECTION_HEADER ;; 将数据目录设置到我们自己的TLS节内 mov eax, dword ptr [eax].VirtualAddress add eax, offset Orig_TLS - offset OwnTLS_Section mov dword ptr [esi].OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_TLS * sizeof IMAGE_DATA_DIRECTORY].VirtualAddress, eax push sizeof IMAGE_TLS_DIRECTORY pop dword ptr [esi].OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_TLS * sizeof IMAGE_DATA_DIRECTORY].isize ;; 复制内容到新的TLS节 mov edi, dwNewTlsSectionOffset assume edi : ptr IMAGE_SECTION_HEADER mov edi, dword ptr [edi].PointerToRawData add edi, pMem mov esi, offset OwnTLS_Section mov ecx, g_dwNewSectionSize cld rep movsb ;; close handle & write it invoke UnmapViewOfFile, pMem invoke CloseHandle, hMap invoke CloseHandle, hFile LogicShellExit: .IF g_bError == 0 ;; show success message invoke MessageBox, NULL, offset g_szDone, offset g_szDoneCap, MB_ICONINFORMATION .ENDIF assume eax : nothing assume ebx : nothing assume edx : nothing assume esi : nothing assume edi : nothing ret ;; ----- Show error message ----- OpenFileFailed: lea eax, g_szOpenFileFailed jmp ShowErr GetFileSizeFailed: lea eax, g_szGetFileSizeFailed jmp ShowErr CreateMapFailed: lea eax, g_szCreateMapFailed jmp ShowErr MapFileFailed: lea eax, g_szMapFileFailed jmp ShowErr InvalidPE: lea eax, g_szInvalidPE jmp ShowErr ShowErr: invoke MessageBox, NULL, eax, offset g_szErr, MB_ICONERROR mov al, 1 mov g_bError, al jmp LogicShellExit ;; ----- 自己的TLS数据 ----- OwnTLS_Section: ;; 我们自己的TLS回调函数 OwnTLS_CallBack: ;; 只判断它的启动状态 OwnTLS_CallBackArg_Reason equ 08h ;; 这里没有做堆栈帧所以esp指向的是返回地址和 ;; 而我们所用的参数Reason是第2个所以这里加08h mov eax, dword ptr [esp+OwnTLS_CallBackArg_Reason] cmp eax, DLL_PROCESS_ATTACH jnz ExitOwnTLS_CallBack ;; 在这里停下,把注释停掉,可以在ollydbg里调试查看 ;int 3 ExitOwnTLS_CallBack: ;; 这里做一个判断,如果是新的TLS则直接退出如果是修改的TLS则跳入原来的 call GetEip GetEip: pop eax mov edx, eax add eax, offset OwnTLS_OrigYes - offset GetEip mov eax, dword ptr [eax] test eax, eax ; 0 为原来没有TLS,1为有TLS回调 ;; 这里取出原始的TLS回调函数入口并跳入 jz NotHaveOrigTlsCallBack add edx, offset OwnTLS_OrigCallBackEntry - offset GetEip jmp dword ptr [edx] ; 跳入正常的回调函数入口 NotHaveOrigTlsCallBack: retn 03h ; 清除参数造成的堆栈开销 ;; ----- 原始回调函数入口 ----- ;; 原来的程序是否有回调函数 OwnTLS_OrigYes dd 0 ;; 原TLS回调函数入口点 OwnTLS_OrigCallBackEntry dd 0 ;; TLS变量索引 OwnTLS_Index dd 0 ;; TLS回调函数地址表 OwnTLS_CallBackStart dd 0 OwnTLS_CallBackEnd dd 0 ;; TLS变量区域 OwnTLS_Start: db 80h dup ("tls") OwnTLS_End: ;; 保存原始的TLS节目录 Orig_TLS IMAGE_TLS_DIRECTORY <0> EndOwnTLS_Section: ModifyTLS endp
这个程序在加有TLS节的程序会修改TLS回调函数地址。
对无TLS节的程序会给他添加一个。
这节对TLS节本身的使用并没有过多的探讨。TLS回调函数与加密解思路是在这里了。怎样使用就看读者的想象力了。
