前情提要
Demo代碼
#include <windows.h>
int tls(){
MessageBox(NULL,"This is TLS CallBack!","TLS Success",MB_OK);
return 0;
}
int main(){
MessageBox(NULL,"This is Main!","Main",MB_OK);
return 0;
}
測試思路
讓tls()
中的彈窗先於main()
中的彈窗!關於整個 tls 的原理就不細說了,你需要有些PE格式的基礎以及tls的瞭解
,不然看着很頭疼。P.S.實際操作比看別人操作要困難很多,光看是不會進步的
具體方法
使用微軟VisualStudio
的話很簡單,網上教程很多。先定義一個 TLS 回調函數void NTAPI TLS_CALLBACK
,再添加節區#pragma data_seg (".tls")
就行了!不過我們這篇講得是手動添加.tls節區以及手動定義回調函數
。因爲 MinGW 和 MSVC 編譯器編譯的 PE 文件格式有所區別,所以需要分這兩種情況
編譯器對比
- GCC
使用gcc -m32 gcc.c -o gcc
編譯。由於 MinGW 的特性使得 GCC 編譯的程序含有.tls
節區以及關於程序運行時的 tls 回調函數
- MSVC
使用VC++6.0
編譯 Demo 代碼。MSVC 編譯器編譯的程序節區就很少了,並且沒有.tls
節以及 tls 回調函數
GCC
前置知識點
一、數據目錄表對應的表項及其宏定義
#define IMAGE_DIRECTORY_ENTRY_EXPORT 0.導出表
#define IMAGE_DIRECTORY_ENTRY_IMPORT 1.導入表
#define IMAGE_DIRECTORY_ENTRY_RESOURCE 2.資源目錄
#define IMAGE_DIRECTORY_ENTRY_EXCEPTION 3.異常目錄
#define IMAGE_DIRECTORY_ENTRY_SECURITY 4.安全目錄
#define IMAGE_DIRECTORY_ENTRY_BASERELOC 5.重定位基本表
#define IMAGE_DIRECTORY_ENTRY_DEBUG 6.調試目錄
#define IMAGE_DIRECTORY_ENTRY_COPYRIGHT 7.描術字串
#define IMAGE_DIRECTORY_ENTRY_GLOBALPTR 8.機器值
#define IMAGE_DIRECTORY_ENTRY_TLS 9.TLS目錄
#define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG 10.載入配值目錄
#define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT 11.綁定輸入表
#define IMAGE_DIRECTORY_ENTRY_IAT 12.導入地址表
#define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT 13.延遲載入描述
#define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14.COM信息
二、節區 Header 的結構體
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; /* 節區名 */
union {
DWORD PhysicalAddress;
DWORD VirtualSize; /* 節區在內存中佔用的大小與文件中的大小不同 */
} Misc;
DWORD VirtualAddress; /* 節區在內存中的偏移 */
DWORD SizeOfRawData; /* 節區在文件在佔用的大小 */
DWORD PointerToRawData; /* 節區在文件中的偏移 */
DWORD PointerToRelocations;
DWORD PointerToLinenumbers;
WORD NumberOfRelocations;
WORD NumberOfLinenumbers;
DWORD Characteristics; /* 節區的屬性 */
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
三、TLS 的結構體
typedef struct _IMAGE_TLS_DIRECTORY32 {
DWORD StartAddressOfRawData; /* tls節區的起始地址 */
DWORD EndAddressOfRawData; /* tls節區的最終地址 */
DWORD AddressOfIndex; /* tls節區的索引 */
DWORD AddressOfCallBacks; /* 指向回調函數的指針 */
DWORD SizeOfZeroFill;
DWORD Characteristics;
} IMAGE_TLS_DIRECTORY32;
四、內存偏移手動轉文件偏移
根據節區頭部的內存偏移量定位某個 RVA 在哪個節區中,然後利用該節區的文件偏移加上相對於該節區的內存偏移即可得到任意內存偏移相對應的文件偏移
具體操作
首先了解下程序的對齊值,gcc編譯的程序內存對齊是0x1000,文件對齊是0x200
然後找到NT Header->Optional Header->Data[9]
,由VirtualAddr
定位到TLS的整個結構體
,要將 RVA 轉爲文件偏移
根據 TLS 的結構體定義可以知道,tls 節區的內存偏移在 0x408000,轉化成文件偏移就是 0x2A00,回調函數的指針指向0x407020,轉換成文件偏移就是0x2820
由IDA
看到我們自寫的tls()
函數的地址是0x401510
,我們將這個地址調到0x407020對應的文件偏移0x2820後面
,P.S.前面的那兩個回調函數應該是運行時相關的,編譯器自定義的 tls 回調與我們無關
保存之後運行,可以看到tls函數會先於main函數運行
MSVC
前置知識點
見上方
具體操作
由於 MSVC 編譯的程序是沒有 tls 節區的所以我們需要添加一個新節區,添加之前先看看文件的對齊值,內存對齊爲0x1000,文件對齊爲0x1000
,但並不一定所有節區的內存偏移等同於文件偏移,因爲可能有的節區內存所佔大小大於文件所佔大小
,這就導致該節區之後節區的內存和文件偏移不一致
先將NT Header->File Header->NumberOfSection
的數量加1,然後將NT Header->Optional Header->SizeOfImage
的值加0x1000
然後在文件最後添加0x1000個00
,作爲新節區
再去修改新節區的頭部,偏移值要根據前面的節區填寫,比如:前一個節區的內存偏移爲0x2B000,文件偏移爲0x29000,那麼新節區的內存偏移爲0x2C000,文件偏移爲0x2A000
,注意還需要可寫的這個屬性
再然後就是填寫數據目錄表,首先我是把TLS的結構體放在.tls節區起始地址的
,所以NT Header->Optional Header->Data[9]
中的VirtualAddress
填寫0x2C000
即 .tls 節區的內存偏移值,然後Size
填寫0x18
最後就是填寫 TLS 結構體的內容了,自寫的 tls 函數地址在0x401020
。TLS 結構體的前三項可以直接填寫當前位置的內存偏移,第四項需要注意的是指針類型
雙擊運行,tls 函數先於 main 函數運行
總結
略微總結一下 PE 文件怎麼找到的 TLS 回調函數
- 先是數據目錄表找到 TLS 結構體所在的位置
- TLS 結構體會指示 TLS 的起始地址,這個地址通常是 .tls 所在的地址
- TLS 結構體中還會有最重要的回調函數指針,根據指針的地址就能定位到回調函數的地址
關於 .tls 節區的說明,其實並不一定需要 .tls 節區,將 TLS 結構體的Start
和End
地址填寫爲其它有效內存區域也是可以的。.tls
節區主要是用來存儲 tls 回調函數需要用到的一些數據