PE文件結構深入詳解


一、PE結構基礎

看了很多PE結構類的東東,要不上來就是整體結構,要不就是一大堆ASM代碼,看的我等菜鳥有點難受!所以自己寫個帖·學習PE我們先來弄懂幾個問題!
1:幾個地址的概念
VA:虛擬地址,也就是內存中的地址!
RVA:相對虛擬地址,等於VA-ImageBase
Offset:物理地址,磁盤上文件的地址,等於RVA-ImageBase-節偏移!
PE裝入器:程序需要裝入內存后才可以運行,PE裝入器就是為了裝入PE文件
裝入:裝入是將磁盤上程序的指令數據轉的地址載入內存並進行地址轉換的過程!
連接:連接是將多個OBJ文件制作成可供PE裝載器裝入模塊的過程!
對於裝入和連接的理解我們在下面進行!
2:OD中的地址為什么是虛擬地址
很多朋友都知道內存中的地址為VA,那么為什么是VA呢?這是有裝入概念引起的,上面說明了裝入的概念,在DOS年代,內存中任何時刻只有一個任務, 所以將程序直接載入內存就可以了,這稱為靜態裝入,當多任務操作系統出現后,所謂的多任務是人類不可感知的CPU時間片下的單任務,多個任務需要同時裝 入,這時候每一個任務的必須在前一個任務的后面,所以磁盤上的程序裝入內存進行地址轉換的時候必須知道前一個任務的結束的內存地址,這就是可重定位裝入方 式,但是伴着X86保護模式的產生,動態可重定位方式產生,在WINDOWS中是一種運行時動態可重定位方式,也就是說當程序真正運行的時候才會去進行地 址轉換,程序載入內存的時候只是一種內存映射,執行程序的時候系統會通過內存中的頁表去查找數據和指令的真實地址!這里在說一下分頁機制中的兩個基本概 念,分頁是分的進程,而內存是分塊的,頁表要解決的就是進程的分頁的號碼與內存分塊號碼的對應,一定要學會用集合的概念去理解東西!
3:DLL文件是怎么來的
朋友都知道DLL文件是動態連接庫,不過為了說明的更清楚,我們還是看一下它的起源,世界是一個完全不相同又存在眾多交集的矛盾,每個程序雖然不同, 但是它們總有交集——功能相同的地址,從統計學上來說,我們如果將很多程序的交集提取出來,當程序需要的時候我們就不用手動的去輸入了,直接引入它就可以 了,這就是庫概念的引入的原因!最早使用庫的方法就是STATIC LINKING,靜態連接方式,這與動態的連接的根本區別在於前者每個程序都有一個庫COPY,而動態連接只會有一個庫的引用說明,並沒有將庫連接的時候 寫進每一個程序,所以WINDOWS缺少DLL文件的時候,某些程序會不能執行,這時候PE裝入器會自動告訴我們,而且是運行程序時候的才報錯,顯然這說明Windows的DLL文件是一種運行時動態連接的工作方式!
那么它到底有什么好處呢?庫是一個程序相同功能的交集,相同功能到底有多少,我們是不知道的,相同功能的代碼進行升級的時候我們也不知道,采用靜態連 接如果庫作了更新顯然程序就得重寫了,靜態連接庫的缺點,恰恰就是動態連接的優點,這也是引入的根本動機,整個過程就是一個出現問題解決問題的過程!運行 時動態連接這種方式的優點解決了將庫都載入內存的麻煩,一個程序所需要的庫也許並不是一個必然事件,所以單純的動態連接只會讓程序在宏觀上不效率!所以 WINDOWS使用了運行時動態連接的方法!


二、PE結構
PE結構的一切元素,只有一個目的就是為了讓程序載入內存!這是一個根本解決也是伴隨為什么產生PE這種結構的原因!說白了它要解決的問題就是一個地 址轉換的問題,怎么將磁盤上的地址轉換為內存中的地址並利於程序的執行!為了更好的學習,我們先來把握重要東西,然后說一下一般有什么用!
1:整體結構
IMAGE_DOS_HEADER
DOS_STUB
IMAGE_NT_HEADERS
IMAGE_SECTION_HEADER
SECTION 1
SECTION 2
SECTION ……
SECTION N

IMAGE_DOS_HEADER  64Byte的大小和DOS_STUB
這是DOS產物,因為PE結構產生的時候比較早,第一個WINDOWS是運行在DOS環境中的,所以為了和DOS兼容PE結構引入了這個東東!
IMAGE_DOS_HEAER只有兩個比較有效,一個是大家熟知的MZ,它在編程的時候稱為e_magic,還有一個就是e_lfanew,也就是用 C32ASM打開PE文件時候對應的3c處,它指向的是IMAGE_NT_HEADER的offset,是一個地址!好了,對於 IMAGE_DOS_HEADER的東東就說這些,
DOS_STUB是一個DOS下標准的EXE文件,類似於用MASM寫的DOS APP!

這兩個東東因為用處不大,所以常被我們用於修改PE頭,目的是為了防調試、免殺、保存輸入表等等! PE變形技術是一種有意思的東東,而IMAGE_DOS_HEADER與IMAGE_NT_HEADER的重疊又是最常見的!大家可以搜索一篇打造微型PE的文章看看!

IMAGE_NT_HEADERS 248 Byte
通過它的名字我們就知道這里面還有HEADER,因為它是一個HEADERS,那么有什么?
它包括三部分,IMAGE_NT_SIGNATURE IMAGE_FILE_HEADER IMAGE_OPTINAL_HEADER
IMAGE_FILE_HEADER 這個是定位物理信息
IMAGE_OPTIONAL_HEADER 這個是定位內存信息,所以這里一般都是一些RVA地址!


上面提到了,PE結構的根本問題就是解決地址轉換!要實現這個根本問題它有幾個步驟,第一個問題就是必須知道PE是不是有效,作為一個有效的PE一般 來說是驗證IMAGE_DOS_HEADER的e_magic是不不為IMAGE_DOS_SIGNATURE(4BYTE),然后驗證 IMAGE_NT_SIGNATRUE,不過我在做實驗的時候發現有時候並不是這樣的,在PE變形的時候有時候會出錯,看來真是人們說的,近信書,不如無 書!這時我們先簡單的理解IMAGE_NT_SIGNATURE和IMAGE_DOS_SIGNAUTRE的作用就是為了驗證PE文件是否有效,這是我們 說的PE結構第一個解決的問題!

IMAGE_FILE_HEADER 20字節
它的重要結構一般有兩個一個是SizeOfOptionalSection,它指是可選頭的大小,PE結構實現的是自動裝載過程,那么第一個結構與下 一個結構必須有一定的聯系,PE編程就是利用這些聯系進行一些簡單的算術運算!另一個是NumberOfSections,這個指明了節表的數目,因為這 里存在一種套子思想,我一直很喜歡這種思想。先來描述一下我的套子思想,此思想來源於陰陽太極圖,如果你也喜歡太極圖,你會看到陰中或者陽中都有一個圈, 李小龍傳奇中說那是眼睛,那豎直是操蛋,這是陰陽太極圖的精妙所在, 它描述的是每一個陰中還有陰陽,每一個陽中還有陰陰。就是一種套子思想,大家慢慢提會,這里用它來說明結構數組,比如節有很多,每一個節都是一個節表結 構,它們合起來就是一種結構數組,這顯然是一種最簡單的數組思想,數組思想是什么,就是上面說的套子思想,所以這個NumberOfSecions其實就 是在告訴PE裝入器數組的大小!

IMAGE_OPTIONAL_HEADER  224字節
IMAGE_OPTIONAL_HEADER,這個常用的我先列出來然后告訴大家怎么記住它們!
AddressOfEntryPoint:程序的入口點,這個大家比較熟悉,免殺的最后一步
ImageBase :基址,上面我在基本概念中作了說明
SectionAligment :內存對齊粒度(這個用GetSystemInfo()就可以找到
FileAlignment:文件對齊粒度(碰到陌生名字,就把它們當成美女的名字,多記幾次就成了熟人了)
SizeOfImage:內存中的鏡象大小
SizeOfHeaders:所有的頭大小,這個可以通過IMAGE_BASE+SIZEOF_HEADERS來定位IMAGE_SECTION_HEADER的位置
DataDirectory:目錄,這里面保存的是需要操作系統提供的東東比如DLL文件有128個字節
  
好了,上面列出這些比較重要的東東,那么它們的作用是什么呢?茫然的時候請回歸根本,我們的問題是解決如何載入內存,那么要載入內存,我們首先要找到 第一個要載入的指令或者數據吧,這個就是入口點,找到了載入誰,我們要解決的問題就是載入到哪里? 載入到哪里呢?這就是有基址來說明,程序很大,怎么將它們分配的更加有規律呢》我們必須知道內存中最小的規律單位大小,這個就是內存對齊的粒度,我們知道 了載入內存的基本單位,要載入還得有一個前提,就是找到文件中的粒度,這有什么用,這可以算地址的!在上面我還說了一個現象,就是每一個結構和下一個結構 都是有聯系的到這里我們來總結一下這些聯系!
IMAGE_DOS_HAEDER的e_lfanew定位到了IMAGE_NT_HEADER的物理偏移,IMAGE_FILE_HEADER的NumberOfSecion指出節表結構數組的大小,同時指出為Opional_Header的大小!
OPTIONAL_HEADER指明了程序第一個要載入的地址,指明了它載入哪,指明了其它指令以多大單位來載入,上面只是初步工作,它與 IMAGE_SECTION_HEADER的聯系,再於如何找定位IMAGE_SECTION_HEADER的位置!有e_lfanew找到了 IMAGE_NT_HEADERS,有IMAGE_NT_HEADERS的最后一個頭也就是IMAGE_OPTIONAL_HEADER指明了 IMAGE_SECTION_HEADER的地址!

好了,下面讓大家記住這些位置!要記住這些位置只要記住兩個數字,16和32
IMAGE_NT_SIGNATURE也就是PE,這兩個字符是PE頭開始的標志!找到它你就找到了IMAGE_NT_HEADER的起始!

后面20個字節就是IMAGE_FILE_HEADER的內容,下面說重點,從IMAGE_FILE_HEADER起
加16個字節,就是AddressOfEntryPoint
加32個字節  左邊是ImageBase右邊依次是SecionAlignmen FileAlignment
從FileAlignment開始,大家注意這里不是從IMGA_FILE_HEADER的結尾算了,從FileAlignment開始加16個字節就是SizeOfImage,后面接的就是SizeOfHeaders!

相信記住16和32你就記住了大部分的內容!
對於DataDirectory它是128個字節,當你看到.text或者是.code節的時候,回推128個字節就是DataDirectory了!

IMAGE_DATA_DIRECTORY
這也是一個結構數組,它的定位方式也是通過宏來進行的,我這里只說輸入表和它的關系!IMAGE_DATA_DIRECTORY只有兩個重要的元素, 第一個是所指向元素的RVA,第二個指向元素的大小,RVA就可確定出所指向的元素的地址,大小隊以元素的大小就是指向的結構數組的大小!
輸入表是一個IMAGE_IMPORT_DESCRIPOR的結構!IMAGE_DATA_DIRECTORY的RVA值指向的就是它的RVA,SIZE/它的結構大小就是指向的IMAGE_IMPORT_DESCRIPTOR的數組大小!


IMAGE_IMPORT_DESCRIPOR的主要內容如下
OrignalThunk HNT的RVA
FirstThunk IAT的RVA
Name DLL文件名的RVA


大家明白這里面的值都是RVA就可以了,這個RVA指向的並是DLL中導出函數的RVA,它們指向一個 IMAGE_DATA_THUNK的結構,這個結構保存了導入函數的RVA,也就是說要定位一個DLL文件中的函數,必須經歷三次RVA才可找到!

IMAGE_SECTION_HEADER
這個結構的主要內容有兩個
VirtualAddress:這個是LORDPE中的Roffset的地址
PointToRawData:這個是LORDPE中的Voffset的地址
VirtualSize:是內存中的大小,除以上面說的粒度就知道需要幾個基本功能單位了
SizeOfRawData:這個文件中的大小,除以上面說的文件粒度就知道有幾個基本功能單位了!
它的作用是計算節偏移!  它的結構大小為20個字節,在PE文件中它是一個結構數組,數組大小有IMAGE_FILE_HEADER的NumberOfSection來決定,
這個大家打開一個PE文件自己找一次就可以了!下面直接給出一個上面所述內容的C++版的編程實現代碼!

#include <windows.h>
#include <iostream>
using namespace std;
int main(int argc,char *argv[])
{
        //定義變量
        IMAGE_DOS_HEADER DosHeader;
        IMAGE_NT_HEADERS NtHeader;
        IMAGE_SECTION_HEADER SecHeader;

        HANDLE Hfile;
        char FileName[256];
        DWORD Dwsize;
        int OffsetSection=0,NumSection=0;
        int i=0,j=0;
        int Offset=0,int Num=0;


        //以系統自帶的CMD程序為例進行說明
        GetSystemDirectory(FileName,256);
        strcat(FileName,"\\cmd.exe");
        if((Hfile=CreateFile(FileName,GENERIC_WRITE|GENERIC_READ,FILE_SHARE_READ|FILE_SHARE_WRITE,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL))==INVALID_HANDLE_VALUE)
        {
                cout<<"INVALID_HANDLE_VALUE";
                return 0;
        }

        //SetFilePointer()
        SetFilePointer(Hfile,0,0,FILE_BEGIN);

        //ReadFile()
        ReadFile(Hfile,&DosHeader,sizeof(DosHeader),&Dwsize,NULL);
       
        if(DosHeader.e_magic!=IMAGE_DOS_SIGNATURE)
        {
                cout<<"沒有DOS頭"<<endl;
                CloseHandle(Hfile);
                return 0;
        }
        else
        {
                cout<<"有DOS頭"<<endl;
        }

        SetFilePointer(Hfile,DosHeader.e_lfanew,0,FILE_BEGIN);

        ReadFile(Hfile,&NtHeader,sizeof(NtHeader),&Dwsize,NULL);

        if(NtHeader.Signature!=IMAGE_NT_SIGNATURE)
        {
                cout<<"沒有PE頭"<<endl;
                CloseHandle(Hfile);
        }


        else
        {
                cout<<"PE有效"<<endl;
                cout<<"######## IMAGE_FILE_HEADER的信息############"<<endl;
                cout<<"Machine:"<<NtHeader.FileHeader.Machine<<endl;
                cout<<"NumberOfSections:"<<NtHeader.FileHeader.NumberOfSections<<endl;
                cout<<"SizeOfOptionalHeader:"<<NtHeader.FileHeader.SizeOfOptionalHeader<<endl;
                cout<<endl;
                cout<<"######## IMAGE_OPTIONAL_HEADER的信息########"<<endl;
                cout<<"AddresssOfEntryPoint:"<<NtHeader.OptionalHeader.AddressOfEntryPoint<<endl;
                cout<<"ImageBase:"<<NtHeader.OptionalHeader.ImageBase<<endl;
                cout<<"SectionAlignment:"<<NtHeader.OptionalHeader.SectionAlignment<<endl;
                cout<<"FileAlignment:"<<NtHeader.OptionalHeader.FileAlignment<<endl;
                cout<<"SizeOfImage:"<<NtHeader.OptionalHeader.SizeOfImage<<endl;
                cout<<"NumberOfHeaders:"<<NtHeader.OptionalHeader.SizeOfHeaders<<endl;
                cout<<endl;
                cout<<"######## IMAGE_DESCRITOR結構數組的RVA地址####"<<endl;
                cout<<"IMAGE_IMPORT_DESCRIPTOR的 RVA:"<<hex<<NtHeader.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress<<endl;
               
        }


        //采用IMAGE_DOS_HEADER.e_lfanew+sizeof(IMAGE_NT_SIGNATURE)+sizeof(IMAGE_FILE_HEADER)+sizeof(IMAGE_OPTIONAL_HEADER)的算法
        NumSection=NtHeader.FileHeader.NumberOfSections;
        OffsetSection=DosHeader.e_lfanew+0x18+sizeof(IMAGE_OPTIONAL_HEADER);
        for(i=0;i<NumSection;i++)
        {
                SetFilePointer(Hfile,OffsetSection+sizeof(IMAGE_SECTION_HEADER)*i,0,NULL);
                ReadFile(Hfile,&SecHeader,sizeof(IMAGE_SECTION_HEADER),&Dwsize,NULL);
                for(j=0;j<8;j++)
                {
                        //輸出每一個節頭
                        cout<<SecHeader.Name[j];
                }
                cout<<endl;
                //輸出每一個節的信息
                cout<<"PointToRawOfData:"<<hex<<SecHeader.PointerToRawData<<endl;
                cout<<"VirtualAddress:"<<hex<<SecHeader.VirtualAddress<<endl;
                //輸出第一個節的節偏移並計算IMAGE_IMPORT_DESCRIPTOR結構數組的物理偏移
                if(i==0)
                {
                        //offset=va-ImageBase-節偏移
                        cout<<".text段的節偏移:"<<hex<<SecHeader.VirtualAddress- SecHeader.PointerToRawData<<endl;
                        cout<<"IMAGE_IMPORT_DESCRIPTOR的物理偏 移:"<<NtHeader.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress-(SecHeader.VirtualAddress-SecHeader.PointerToRawData)<<endl;
                }
        }
       
       
        CloseHandle(Hfile);
        return 0;

}

 

三、思想總結
對比一下代碼我想你就理解了,下一張看雪的圖仔細看,一定要仔細,好了帖就到這吧!有了這些內容,基本上改個變態的PE就有了基礎!
總結一下我們用了一些什么思想!
1:集合的思想
將不同的內容規到一個集合中然后讓它們產生對應這就是一種函數的思想
2:結構數組套子思想
套子思想從太極圖中得出的一個結論
3:回歸思想
不容易理解時候回歸根本,從根本問題中去理解為什么
4:存在就有道理
我個人覺得這個思想比較重要,多問個為什么,本文開始我就作了說明,它產述的就是這個思想,為什么是虛擬地址,一個簡單的現象,卻隱藏一個事物發展的過程!
5:過程理解
理解一個過程后再去理解它的不足或者說具體內容,這是一種整體把握思想


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM