C++實現PE文件添加新節區(加殼器)

最近潛心研究二進制安全,接觸到了shellcode還有加殼脫殼有關的內容,於是心血來潮,想用自己不怎麼成熟的編程功夫來實現寫一個加殼器,並記錄下代碼編寫過程中遇到的坑。
(以下文章中區段==節區)

PE文件格式

pe(Portable Executable)其實就是在windows系統上的程序文件,這種文件格式在windows系列操作系統上基本上是通用的,我們平時見到的.exe、.dll、.obj、.sys都是PE文件
PE文件格式大致如下
在這裏插入圖片描述其中第一個是DOS頭,DOS頭中聲明用的寄存器
結構體

struct _IMAGE_DOS_HEADER{
    0X00 WORD e_magic;      //標記是否是可執行文件,我們常見的exe程序這一字段標識就是"MZ"
    0X02 WORD e_cblp;     //Bytes on last page of file
    0X04 WORD e_cp;       //Pages in file
    0X06 WORD e_crlc;     //Relocations
    0X08 WORD e_cparhdr;  //Size of header in paragraphs
    0X0A WORD e_minalloc; //Minimun extra paragraphs needs
    0X0C WORD e_maxalloc; //Maximun extra paragraphs needs
    0X0E WORD e_ss;       //intial(relative)SS value
    0X10 WORD e_sp;       //intial SP value
    0X12 WORD e_csum;     //Checksum
    0X14 WORD e_ip;       //intial IP value
    0X16 WORD e_cs;       //intial(relative)CS value
    0X18 WORD e_lfarlc;   //File Address of relocation table
    0X1A WORD e_ovno;     //Overlay number
    0x1C WORD e_res[4];   //Reserved words
    0x24 WORD e_oemid;    //OEM identifier(for e_oeminfo)
    0x26 WORD e_oeminfo;  //OEM information;e_oemid specific
    0x28 WORD e_res2[10]; //Reserved words
    0x3C DWORD e_lfanew;    //PE頭相對於文件的偏移量
};

其中有中文註釋的結構體成員是比較重要,也是我們之後要用到的結構體成員

緊跟在DOS頭後面的一段是實模式的殘餘程序,這一段主要是爲了兼容16位的DOS系統,現在已經沒有太大用處,暫且忽略

實模式殘餘程序之後就是PE頭也可以叫做NT頭
結構體IMAGE_NT_HEADERS
PE頭中又包括三部分:

  • PE簽名(即“PE"兩個字符)
  • PE文件頭(結構體IMAGE_FILE_HEADER)
  • PE可選頭(結構體IMAGE_OPTIONAL_HEADER)

IMAGE_NT_HEADERS

typedef struct _IMAGE_NT_HEADERS {
    DWORD Signature;                   //PE標識
    IMAGE_FILE_HEADER FileHeader;      //文件頭
    IMAGE_OPTIONAL_HEADER32 OptionalHeader;//擴展頭
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;

IMAGE_FILE_HEADER

typedef struct _IMAGE_FILE_HEADER {
    WORD    Machine;                        
    /*機器型號.表名了我們CPU執行的這個PE文件是x86的還是x64的.
    有一系列宏標識.*/
    WORD    NumberOfSections;                  
     /*節表個數. 此成員很重要.標識着我們的節表有多少個.
     如果節個數小於節的總數那麼程序就不能運行*/
    DWORD   TimeDateStamp;                     
    //文件時間.不重要.與文件屬性裏面的創建事件修改時間無關.編譯器填寫的
    DWORD   PointerToSymbolTable;                           
    //調試器相關
    DWORD   NumberOfSymbols;                                 
    //調試器相關.
    WORD    SizeOfOptionalHeader;                          
    //擴展PE頭大小,此成員很重要.表明了我們的擴展頭總體大小.
    WORD    Characteristics;                                
    //文件屬性
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;

IMAGE_OPTIONAL_HEADER

typedef struct _IMAGE_OPTIONAL_HEADER {
    //
    // Standard fields.
    //

    WORD    Magic;              
    //標誌.表名了我們的PE是x86還是x64
    BYTE    MajorLinkerVersion;          
    //連接器主要版本號
    BYTE    MinorLinkerVersion;          
    //連接器次要版本號 例如 3.54 主要版本就是3.次要就是54
    DWORD   SizeOfCode;                  
    //代碼段大小,以字節爲單位.
    DWORD   SizeOfInitializedData;       
    //初始化數據部分的大小.
    DWORD   SizeOfUninitializedData;     
    //未知初始化數據的大小
    DWORD   AddressOfEntryPoint;         
    /*OEP 程序入口點,驅動程序也是入口點.
    對於DLL而言.是可選的.沒有入口則爲0*/
    DWORD   BaseOfCode;                  
    //指向代碼部分的指針              
    DWORD   BaseOfData;                  
    //指向數據部分開頭的指針

    //
    // NT additional fields.
    //

    DWORD   ImageBase;                  
    /*基址.PE文件加載到內存中的基址.這個值是64k的倍數.
    DLL默認值是0x100000000,應用程序默認是0x00400000*/
                                         windows CE除外.他是0x00010000
    DWORD   SectionAlignment;           
    //PE文件加載到內存中.的內存對齊.按照這個成員進行對齊
    DWORD   FileAlignment;              
    //文件對齊,PE存數據存放在文件中.按照文件對其值對其
    WORD    MajorOperatingSystemVersion;
    //所需要操作系統的主要版本號.
    WORD    MinorOperatingSystemVersion;
    //所需要操作系統的次要版本號.
    WORD    MajorImageVersion;          
    //PE主版本號
    WORD    MinorImageVersion;          
    //PE次版本號
    WORD    MajorSubsystemVersion;      
    //子系統主要版本號.
    WORD    MinorSubsystemVersion;      
    //子系統次要版本號.
    DWORD   Win32VersionValue;          
    //保留成員,必須爲0
    DWORD   SizeOfImage;                
    /*PE鏡像大小.
    必須是內存對齊的倍數. sizeofImage/SectionAllignment == 0 纔可以*/
    DWORD   SizeOfHeaders;               
    /* DOS頭+NT頭+節表的總大小.
    按照文件對齊存放 sizeofHeaders / FileAlignment == 0*/
   DWORD   SubSystem             
   //表名PE文件是什麼程序. 1驅動程序2窗口程序3控制檯程序(DLL)
    DWORD   CheckSum;                   
    WORD    DllCharacteristics;         //P的文件屬性
    DWORD   SizeOfStackReserve;         
    /*堆棧保留字節數.我們的程序使用的棧空間多大靠這個成員.
    不過操作系統只作爲參考*/
    DWORD   SizeOfStackCommit;          //要爲堆棧提交的字節數.不做參考
    DWORD   SizeOfHeapReserve;          //堆保留字節數.
    DWORD   SizeOfHeapCommit;           
    /*本地堆提交的字節數.
     PS: 棧堆保留數值.鬥魚自己的sizeof(Head/stack)Commit成員有關.*/
    DWORD   LoaderFlags;                
    //成員已經過時
    DWORD   NumberOfRvaAndSizes;        
    //數據目錄數組的大小
    IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
    //數據目錄
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;

PE頭之後就是各節表,我們想要實現程序的加殼,就是要在原有的節區的基礎上再添加一個新的節區,然後將程序的入口點重定到我們的新節區去(原本在.text節區)然後在我們的新節區內對原有的text節區進行解密或者解壓縮的操作,最後將程序的控制權在交還給text節區。
節區頭結構體IMAGE_SECTION_HEADER

typedef struct _IMAGE_SECTION_HEADER 

{
+0h BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; 
// 節表名稱,如“.text” 
//IMAGE_SIZEOF_SHORT_NAME=8
union
+8h {
DWORD PhysicalAddress; 
// 物理地址
DWORD VirtualSize; 
// 真實長度,這兩個值是一個聯合結構,可以使用其中的任何一個,一
// 般是取後一個
} Misc;
+ch DWORD VirtualAddress; 
// 節區的 RVA 地址
+10h DWORD SizeOfRawData; 
// 在文件中對齊後的尺寸
+14h DWORD PointerToRawData; 
// 在文件中的偏移量
+18h DWORD PointerToRelocations; 
// 在OBJ文件中使用,重定位的偏移
+1ch DWORD PointerToLinenumbers; 
// 行號表的偏移(供調試使用地)
+1eh WORD NumberOfRelocations; 
// 在OBJ文件中使用,重定位項數目
+20h WORD NumberOfLinenumbers; 
// 行號表中行號的數目
+24h DWORD Characteristics; 
// 節屬性如可讀,可寫,可執行等
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;

下面就來先實現程序加殼的第一步,給一個程序添加一個新的區段,我在這裏寫了一個簡單的小程序入口函數爲main函數,在main函數中調用MessageBox函數實現一個彈窗,程序名ect1.exe,運行效果
在這裏插入圖片描述點擊確認後程序退出,下面就來給這個程序加一個節區,先用010Editor打開他看他的二進制數據,大致如下
在這裏插入圖片描述我們可以看到他有五個節區,分別是.text、.data、.rdata、.rsrc、.reloc。我們再來看看這五個節區節區表的二進制數據
在這裏插入圖片描述選中的這一塊就是節區表區,節區數據區因爲佔的空間比較大不方便截圖,我就不截圖了,他就在節區表後這一大片數據00後面,現在我們要添加一個新的節區,肯定要先添加新節區的節區表,這個截取表應該放在哪呢?沒錯!就是這一大片沒人用的00數據區,現在知道了要在哪添加數據,我們就該準備寫代碼了,首先肯定是先要解析PE文件,來獲取各個區段
獲取DOS頭

PIMAGE_DOS_HEADER PefileParsing::GetFile_Dosheader
(
	_In_ BYTE* File_Data,
	_Inout_ DWORD& back
)
{
	PIMAGE_DOS_HEADER FH= (PIMAGE_DOS_HEADER)File_Data;
	if (FH->e_magic != IMAGE_DOS_SIGNATURE)
	{
		back = __FILE_NO_PE;
		return NULL;
	}
	back = __SUCCESS;
	return FH;
}

在這裏我創建了一個類來實現節區添加,其中File_Data就是我們讀出來要進行加殼的程序文件數據,第二個參數是我用來給類的調用者反饋錯誤原因的參數,錯誤原因在我自定義的一個頭文件中,我會將完整項目上傳到我的git裏,最後我會附上git地址

獲取到DOS頭後就要獲取PE頭了

PIMAGE_NT_HEADERS PefileParsing::GetFile_NTheader
(
	_In_ BYTE* File_Data,
	_Inout_ DWORD& back
)
{
	//獲取NT頭,取到偏移後基址與相加=絕對地址
	DWORD B = 0;
	PIMAGE_NT_HEADERS NTH= (PIMAGE_NT_HEADERS)
		((GetFile_Dosheader(File_Data,B)->e_lfanew)+(DWORD)File_Data);
	if (B == __FILE_NO_PE)
	{
		back = __FILE_NO_PE;
		return NULL;
	}
	if (NTH->Signature != IMAGE_NT_SIGNATURE)
	{
		back = __FILE_NO_NT;
		return NULL;
	}
	back = __SUCCESS;
	return NTH;
}

參數解釋同獲取DOS頭,e_lfanew存放的是PE頭的偏移地址,取到他就能很容易的獲取PE頭,獲取到PE頭後就來獲取文件頭與擴展頭

文件頭

PIMAGE_FILE_HEADER PefileParsing::GetFile_header
(
	_In_ BYTE* File_Data,
	_Inout_ DWORD& back
)
{
	//獲取文件頭,FileHeader結構體成員類型是一個變量,
	//需要引用後返回一個指針
	DWORD U = 0;
	PIMAGE_FILE_HEADER FFH = &GetFile_NTheader(File_Data,U)->FileHeader;
	if (U == __FILE_NO_NT)
	{
		back = __FILE_NO_NT;
		return NULL;
	}
	if (U == __FILE_NO_PE)
	{
		back = __FILE_NO_PE;
		return NULL;
	}
	back = __SUCCESS;
	return FFH;
}

擴展頭

PIMAGE_OPTIONAL_HEADER  PefileParsing::GetFile_Optheader
(
	_In_ BYTE * File_Data,
	_Inout_ DWORD& back
)
{
	//獲取擴展頭,OptionalHeader;結構體成員類型是一個變量,
	//要引用後返回一個指針
	DWORD O = 0;
	PIMAGE_OPTIONAL_HEADER OH = &GetFile_NTheader(File_Data,O)->OptionalHeader;
	if (O == __FILE_NO_NT)
	{
		back = __FILE_NO_NT;
		return NULL;
	}
	if (O == __FILE_NO_PE)
	{
		back = __FILE_NO_PE;
		return NULL;
	}
	back = __SUCCESS;
	return OH;
}

獲取完我們需要的單元,我們還需要獲取到最後一個節區表,因爲我們要在他後面添加我們的新節區表

PIMAGE_SECTION_HEADER  PefileParsing::GetLastSection
(
	_In_ BYTE * File_Data,
	_Inout_ DWORD& back
)
{
	//獲取最後一個區段以便添加新區段
	//獲取區段數
	DWORD backNum = 0;
	DWORD SecNum = GetFile_header(File_Data,backNum)->NumberOfSections;
	if (SecNum == 0)
	{
		back = backNum;
		return NULL;
	}
	//獲取第一個區段地址
	PIMAGE_SECTION_HEADER Fsec = IMAGE_FIRST_SECTION(GetFile_NTheader(File_Data,backNum));

	//返回最後一個區段偏移(=第一個區段偏移+區段數-2)
	//在這裏獲取最後一個區段的後一部分,並判斷這其中是否存在數據
	//如果區段數是5,那最後一個區段的下標即是0~4,而獲取到的值會是6
	PIMAGE_SECTION_HEADER s = Fsec + (SecNum - 1);
	if ((s->Characteristics != 0) || (s->Name[0] != 0) || (s->SizeOfRawData != 0))
	{
		back = __LASTSECTION_NO_NULL;
		return NULL;
	}
	back = __SUCCESS;
	return s;
}

那塊if判斷我本來是想用來判斷最後一個節區表後的空間是否能夠讓我們用來添加新節區表的,但明顯有點邏輯錯誤,我後期會進行改正,但這個代碼在對大部分可執行程序的時候都是有效的,至於如何判斷最後一個節區表後能否添加新的節區表,就只需要判斷後面數據爲00的空間夠不夠0x28就行了

這樣我們就獲取到了最後一個節區表的後一塊空白數據區,現在我們還需要知道添加新節區要更改或者增加那些成員值

首先我們要添加一個新的節區表,肯定要更新節區表個數
在這裏插入圖片描述也就是這一成員值,他位於PE文件頭結構體中,代碼實現可爲

GetFile_header(OldData,backNum)->NumberOfSections++;

z之後我們還需要給新節區表一個名字,就像.text那些區段一樣,區段名可選
在這裏插入圖片描述這以成員位於IMAGE_SECTIONAL_HEADER中,所以在賦名稱前,先要調用剛剛我們定義的獲取最後一個節區後空白數據區的那個方法函數先來獲取一個IMAGE_SECTIONAL_HEADER結構體變量,獲取到後直接

memcpy(nSection->Name, Section_name, 8);

在這裏Section_name是我通過函數參數傳進來的一個字符串值,最後的8代表此成員值最大不得超過8

現在我們還需要設置幾個成員值(以下值都位於節區表結構體中):

  • VirtualSize(區段數據實際大小,之後加殼會將殼代碼複製到新節區數據區內,這裏就是那個殼代碼二進制數據sizeof獲取到的大小值)
  • SizeOfRawData(區段對齊後的大小)
  • VirtualAddress(區段虛擬內存偏移)
  • PointerToRawData(區段文件偏移)
  • Characteristics(區段屬性)

在這裏我們看到有些成員值需要進行對齊,所以我們還要提供一個用於對齊的函數
代碼實現

SIZE_T PefileParsing::Section_Alignment
(
	_In_ SIZE_T File_Size,
	_In_ SIZE_T Alignment
)
{
	//對齊區段
	return ((File_Size%Alignment == 0) ? 
		File_Size : File_Size / (Alignment - 1)*Alignment);
}

很簡單一段代碼,不用解釋什麼

設置成員值

//設置新區段頭物理大小與區段對齊後大小
	nSection->Misc.VirtualSize = (DWORD)Section_size;
	nSection->SizeOfRawData = 
		Section_Alignment(Section_size, OptHeader->FileAlignment);
	//區段內存偏移=上一個區段的偏移+上一個區段對齊後的大小還要加0x1000
	//否則虛擬地址與前一個區段的虛擬地址還是一樣,會導致程序無法執行
	nSection->VirtualAddress =
		(nSection - 1)->VirtualAddress + Section_Alignment(
		(nSection - 1)->SizeOfRawData, OptHeader->SectionAlignment)+0x1000;
	//設置新區段數據存放位置
	nSection->PointerToRawData =
		(nSection - 1)->PointerToRawData + (nSection - 1)->SizeOfRawData;
	//設置區段屬性
	nSection->Characteristics = 0xE00000E0;

r然後我們還需要設置擴展頭的文件映像大小,來將原來的文件數據空間擴大,畢竟我們添加了新的數據內容舊的空間肯定是存不下的

PIMAGE_OPTIONAL_HEADER OptHeader=GetFile_Optheader(OldData,backNum);
//修改擴展頭映像大小,舊的加上新的
OptHeader->SizeOfImage += nSection->SizeOfRawData;

在這裏我們要加上對齊後的節區數據內存大小,而不是節區的實際大小
然後我們還要更新新的文件數據大小,新的文件數據肯定已經變得比老的文件數據大了

//修改文件數據大小,舊文件大小加上對齊後的區段大小

	SIZE_T NewSize = 
		nSection->PointerToRawData + nSection->SizeOfRawData;
	NewData = new BYTE[NewSize];

y因爲老的讀入內存的文件數據緩衝區大小以及遠不夠存放我們的區段數據了,所以我們需要申請一塊新的足夠大的內存存放老的數據,並在老數據之後的空白數據區填充數據(如果只有節區表沒有節區數據的話,這個PE文件會是一個無法執行的無效PE文件)

//修改文件數據大小,舊文件大小加上對齊後的區段大小
SIZE_T NewSize = 
		nSection->PointerToRawData + nSection->SizeOfRawData;
	//申請新的數據空間
	NewData = new BYTE[NewSize];
	memcpy(NewData, OldData, fsize);
	//刪除舊的緩衝區,更新文件大小,返回新的文件數據指針
	delete[] OldData;

注意爲了避免內存泄漏,記得把不用的舊的數據緩衝區釋放
z最後更新文件數據大小,並將區段數據拷貝填充進去,並且將新申請的數據緩衝區指當作函數返回值返回

	fsize = NewSize;
	//這裏模擬實現將殼代碼數據拷貝進新區段數據區
	//我在這其實只是用00來填充,並沒有代碼數據
	memcpy((NewData + nSection->PointerToRawData), Section_data, Section_size);
	return NewData;

記得最後在寫一個函數釋放文件數據緩衝區,防止內存泄漏!!!

void PefileParsing::ClearPEBuff(BYTE * File_Data)
{
	delete[] File_Data;
}

再來看看添加完新節區的程序
在這裏插入圖片描述在這裏插入圖片描述可以看到新節區已經添加進去了
再運行一下添加了新節區的程序看看能否正常運行,能正常運行就說明沒有問題了
在這裏插入圖片描述運行成功,寫完收工 !

github完整項目地址

參考文章:
加殼器編寫

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章