純手工編寫的PE可執行程序


【文章標題】: 純手工編寫的PE可執行程序
【文章作者】: Kinney
【下載地址】: 自己搜索下載
【使用工具】: C32
【操作平台】: win 7
【作者聲明】: 只是感興趣,沒有其他目的。失誤之處敬請諸位大俠賜教!
 最近,學習PE結構的知識。之后深有感觸,隨即便萌發了不依賴任何開發環境和編譯器,純手工寫一個小程序的念頭。所以我打算就寫一個彈出MessageBox的小程序吧(彈出“Hello Kinney!This is the first PE program!”)。
 
在這里,我們首先復習一下Win32可執行程序的大體結構,就是通常所說的PE結構。PE 的意思就是Portable Executable(可移植的執行體)。

 PE結構如下圖:
    
    ︱ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄︱
    ︱     MS-DOS       ︱
    ︱    MZ  頭部      ︱--------------> 64 byte
    ︱                  ︱
    ︱  ̄  ̄  ̄  ̄  ̄  ̄︱
    ︱     MS-DOS       ︱
    ︱ 實模式殘余程序   ︱--------------> 112 byte 
    ︱                  ︱
    ︱ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄︱
    ︱    PE文件標志    ︱--------------> 4 byte
    ︱                  ︱
    ︱ ̄  ̄  ̄  ̄  ̄  ̄ ︱
    ︱     PE文件頭     ︱--------------> 20 byte
    ︱                  ︱
    ︱ ̄  ̄  ̄  ̄  ̄  ̄ ︱
    ︱   PE文件可選頭   ︱--------------> 224 byte
    ︱                  ︱
    ︱ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄︱
    ︱     各段頭部     ︱--------------> n * 40 byte
    ︱                  ︱
      ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄


IMAGE_DOS_HEADER:
每一個PE文件是以一個DOS程序開始的,有了它,一旦程序在DOS下執行,DOS就能識別出這是有效的執行體,然后運行緊隨MZ Header之后的DOS stub(DOS殘余塊,其實是一個有效的EXE,就是我們可以看到的一個錯誤提示:This program cannot be run in MS-DOS mode)。我們對於這個DOS stub可以忽略,所以我在下面手寫PE的時候,將DOS stub處填充為0。下面來看看IMAGE_DOS_HEADER的結構體定義,我將一些手寫PE需要注意的幾項給注釋了下:

struct _IMAGE_DOS_HEADER {
0x00   WORD e_magic; ;DOS可執行文件標記“MZ”,被#define IMAGE_DOS_SIGNATURE 0x5A4Dh
0x02   WORD e_cblp;
0x04   WORD e_cp;
0x06   WORD e_crlc;
0x08   WORD e_cparhdr;
0x0a   WORD e_minalloc;
0x0c   WORD e_maxalloc;
0x0e   WORD e_ss;
0x10   WORD e_sp;
0x12   WORD e_csum;
0x14   WORD e_ip; ;DOS代碼入口IP
0x16   WORD e_cs; ;DOS代碼的入口CS
0x18   WORD e_lfarlc;
0x1a   WORD e_ovno;
0x1c   WORD e_res[4];
0x24   WORD e_oemid;
0x26   WORD e_oeminfo;
0x28   WORD e_res2[10];
0x3c   DWORD e_lfanew; ;指向PE文件頭“PE”,0,0
};

MZ-DOS頭部占64個字節,所以我們在C32中選擇插入64個0:


圖一

在IMAGE_DOS_HEADER中,有兩個字段比較重要,分別是e_magic和e_lfanew字段(一個字大小)需要被設置為5A4Dh,這個值是#define的,在ASCII里,為“MZ”,是MS-DOS的最初創建者之一Mark Zbikowski字母的縮寫,e_lfanew字段是真正PE文件頭的想對偏移(RVA),作用是指出真正PE頭的文件偏移位置(如圖二):

圖二

從上面的結構體可以看出,它占4個字節,位於文件開始偏移3Ch字節中。 “PE文件標志”緊隨“MS-DOS 實模式殘余程序”其后。知道這一點,我們就可以計算一下了,我們的“DOS MZ header”總共64 byte,后面的“MS-DOS 實模式殘余程序”占112 byte, 64 + 112 = 176 byte,但是要注意,我們這里的176可是十進制的,轉化成十六進制是B0,對了,就是這個值,因為是4個字節,所以我們應該填“B0000000”。看上面的截圖,為B8000000,所以,保險起見,我們這里也填充為B8000000.所以我們現在將前兩個字節填充為4D5A,在3C處填充為B8000000。如圖:


圖三
接下來我們來完成“MS-DOS 實模式殘余程序”,我們已經知道,他是用在DOS下執行的,我們這里可以直接用“00”來填充,注意總共112 byte。 這兩部分完成之后代碼如下:


圖四
填充好后,如圖:


圖五
在將准備工作做完以后,我們開始進入我們的重要部分,開始寫真正的PE結構部分:
微軟將“PE文件標志”,“PE文件頭”,“PE文件可選頭”這三個部分用一個結構來定義,即:IMAGE_NT_HEADERS32(WINNT.H中有定義,后面象這樣的結構均在WINNT.H中有定義),

struct _IMAGE_NT_HEADERS {
0x00   DWORD Signature; ;PE文件標識
0x04   _IMAGE_FILE_HEADER FileHeader;
0x18   _IMAGE_OPTIONAL_HEADER OptionalHeader;
};
這個結構含有3個成員:
第一個成員表示“PE文件標識”,可以看到他是一個DWORD類型,因此占4個字節,它是PE開始的標記,是一個#define IMAGE_NT_SIGNATURE定義了這個值,對Windows程序這個值必須為“50450000”。DOS頭部的e_lfanew字段正是指向“PE\0\0”:
#define IMAGE_NT_SIGNATURE 0x00004550

第二個成員表示“PE文件頭 ”,他的類型是一個IMAGE_FILE_HEADER的結構。也就是說“PE文件頭”的20個字節被定義為IMAGE_FILE_HEADER結構,

struct _IMAGE_FILE_HEADER {
0x00   WORD Machine; ;運行平台
0x02   WORD NumberOfSections; ;文件的區塊數目
0x04   DWORD TimeDateStamp; ;文件創建日期和時間
0x08   DWORD PointerToSymbolTable; ;指向符號表(用於調試)
0x0c   DWORD NumberOfSymbols; ;符號表中符號個數(用於調試)
0x10   WORD SizeOfOptionalHeader; ;IMAGE_OPTIONAL_HEADER32結構的大小
0x12   WORD Characteristics; ;文件屬性
};

  這個結構具有7個成員(如圖):

        
        成員Machine,占2個字節,表示該文件運行所要求的CPU。對於Intel i386平台,該值是“4C01”。     成員NumberOfSections,占2個字節,表示該文件中段的總數,我們這里計划寫3個段,(.text(代碼段)、.rdata(只讀數據段)、 .data(全局變量數據段))。所以此處值是“0300”。
        成員TimeDateStamp,占4個字節,表示文件創建日期和時間,從1970.1.1 00:00:00以來的秒數,我們這里填“0000”即可。
        成員PointerToSymbolTable,占4個字節,表示符號表的指針,主要用於調試,在這里填“0000”。     成員NumberOfSymbols,占4個字節,表示符號的數目,主要用於調試,在這里填“0000”。
        成員SizeOfOptionalHeader,占2個字節,表示后面的“PE文件可選頭 ”部分所占空間大小,我們已經知道“PE文件可選頭 ”的大小是224 byte,轉換成十六進制就是E0,所以這里的值為“E000”
        成員Characteristics,占2個字節,表示關於文件信息的標記,比如文件是exe還是dll。這個值實際上是二進制位進行或運算得到的值。
        
各二進制位表示的意義如下:
    
         Bit 0 :置1表示文件中沒有重定向信息。每個段都有它們自己的重定向信息。這個標志在可執行文件中沒有使用,在可執行文件中是用一個叫做基址重定向目錄表來表示重定向信息的,這將在下面介紹。
         Bit 1 :置1表示該文件是可執行文件(也就是說不是一個目標文件或庫文件)。
         Bit 2 :置1表示沒有行數信息;在可執行文件中沒有使用。
         Bit 3 :置1表示沒有局部符號信息;在可執行文件中沒有使用。
         Bit 4 :
         Bit 7 
         Bit 8 :表示希望機器為32位機。這個值永遠為1。
         Bit 9 :表示沒有調試信息,在可執行文件中沒有使用。
         Bit 10:置1表示該程序不能運行於可移動介質中(如軟驅或CD-ROM)。在這種情況下,OS必須把文件拷貝到交換文件中執行。
         Bit 11:置1表示程序不能在網上運行。在這種情況下,OS必須把文件拷貝到交換文件中執行。 
         Bit 12:置1表示文件是一個系統文件例如驅動程序。在可執行文件中沒有使用。
         Bit 13:置1表示文件是一個動態鏈接庫(DLL)。
         Bit 14:表示文件被設計成不能運行於多處理器系統中。
         Bit 15:表示文件的字節順序如果不是機器所期望的,那么在讀出之前要進行交換。在可執行文件中它們是不可信的(操作系統期望按正確的字節順序執行程序)。
    
         注意,因為我們寫的是可執行程序,所以Bit 1必須置為1,其他的按照需要置位即可,這里我們僅將第二位置位,由此得到成員7的值為“0200”。

第三個成員,表示“PE文件可選頭 ”,他的類型是一個IMAGE_OPTIONAL_HEADER32結構。也就是說“PE文件頭 ”的224個字節被定義為IMAGE_OPTIONAL_HEADER32結構,

struct _IMAGE_OPTIONAL_HEADER {
0x00   WORD Magic; ;標志字
0x02   BYTE MajorLinkerVersion; ;鏈接器主版本號
0x03   BYTE MinorLinkerVersion; ;鏈接器次版本號
0x04   DWORD SizeOfCode; ;所有含有代碼區塊的總大小
0x08   DWORD SizeOfInitializedData; ;所有初始化數據區塊總大小
0x0c   DWORD SizeOfUninitializedData; ;所有未初始化數據區塊總大小
0x10   DWORD AddressOfEntryPoint; ;程序執行入口的RVA
0x14   DWORD BaseOfCode; ;代碼區塊起始RVA
0x18   DWORD BaseOfData; ;數據區塊起始RVA
0x1c   DWORD ImageBase; ;程序默認裝入基地址
0x20   DWORD SectionAlignment; ;內存中區塊的對齊值
0x24   DWORD FileAlignment; ;文件中區塊的對齊值
0x28   WORD MajorOperatingSystemVersion; ;操作系統主版本號
0x2a   WORD MinorOperatingSystemVersion; ;操作系統此版本號
0x2c   WORD MajorImageVersion; ;用戶自定義主版本號
0x2e   WORD MinorImageVersion; ;用戶自定義次版本號
0x30   WORD MajorSubsystemVersion; ;所需要子系統主版本號
0x32   WORD MinorSubsystemVersion; ;所需要子系統次版本號
0x34   DWORD Win32VersionValue; ;保留,通常被設置為0
0x38   DWORD SizeOfImage; ;影響裝入內存后的總尺寸
0x3c   DWORD SizeOfHeaders; ;DOS頭、PE頭部、區塊表總大小
0x40   DWORD CheckSum; ;影響校驗和
0x44   WORD Subsystem; ;文件子系統
0x46   WORD DllCharacteristics; ;顯示DLL特性的旗標
0x48   DWORD SizeOfStackReserve; ;初始化堆棧大小
0x4c   DWORD SizeOfStackCommit; ;初始化實際提交堆棧大小
0x50   DWORD SizeOfHeapReserve; ;初始化保留堆棧大小
0x54   DWORD SizeOfHeapCommit; ;初始化實際保留堆棧大小
0x58   DWORD LoaderFlags; ;與調試有關,默認值為0
0x5c   DWORD NumberOfRvaAndSizes; ;數據目錄表的項數
0x60   _IMAGE_DATA_DIRECTORY DataDirectory[16];
};

具有31個成員:
  
         成員1,占2個字節,表示文件的格式,值為0x010B表示.EXE文件,為0x0107表示ROM映像,因為我們寫的是一個可執行程序,所以此值應該為“0B01”。
         成員2,占1個字節,表示鏈接器的主版本號,此值不會影響程序的執行,我們這里填充零,此值為“00”。
         成員3,占1個字節,表示鏈接器的幅版本號,此值不會影響程序的執行,我們這里填充零,此值為“00”。
         成員4,占4個字節,表示可執行代碼的長度,此值不會影響程序的執行,我們這里填充零,此值為“00000000”。
         成員5,占4個字節,表示初始化數據的長度(數據段)。此值不會影響程序的執行,我們這里填充零,此值為“00000000”。
         成員6,占4個字節,表示未初始化數據的長度(bss段)。此值不會影響程序的執行,我們這里填充零,此值為“00000000”。

         (在介紹成員7之前,有必要了解一個很重要的知識------文件映射到內存。在可執行程序運行之前,PE加載器將把PE文件加載到進程空間的內存中去,並且初始化每個段實體。那么加載到內存中的哪個地址去呢?這將由IMAGE_OPTIONAL_HEADER32結構的成員10的值指出加載的起始地址(又叫基地址)。這個值通常是“00400000”, 那么PE文件的首地址“00000”就被映射到內存地址“00400000”處,那么相對於文件偏移10個字節的地址為“00010”,被映射到內存后的偏移也應該是10個字節,映射后的地址應該為“00400010”。)
        成員7,4個字節,表示代碼的入口RVA(文件映射到內存的偏移地址)地址,程序從這兒開始執行。PE裝載器准備運行的PE文件的第一個指令的RVA。若您要改變整個執行的流程,可以將該值指定到新的RVA,這樣新RVA處的指令首先被執行。那么這個值我們怎么得到呢?我們知道在文件中有個.text段,他包含了所有的代碼,我們可以從中找到我們的入口地址,在這里就是.text段里的第一行代碼,也就是.text段的首地址,而在.text段頭部就給出了他映射到內存后的首地址的偏移,我們找到他取出添到此處,這里為“00100000”。(此處不理解沒關系,我們講完段結構后自能迎刃而解。)
        成員8,4個字節,表示可執行代碼起始位置。當然就是.text段的首地址,此值不會影響程序的執行,我們這里填充零,此值為“00000000”。
        成員9,4個字節,表示初始化數據的起始位置,此值不會影響程序的執行,我們這里填充零,此值為“00000000”。
        成員10,4個字節,就是上面所講的文件映射到內存是的基地址。PE文件的優先裝載地址。通常設為“00400000”,PE裝載器將嘗試把文件裝到虛擬地址空間的00400000h處。字眼"優先"表示若該地址區域已被其他模塊占用,那PE裝載器會選用其他空閑地址。我們這里的值設為“00400000”。
        成員11,4個字節,表示段加載后在內存中的對齊方式。內存中節對齊的粒度。例如,如果該值是4096 (1000h),那么每節的起始地址必須是4096的倍數。若第一節從401000h開始且大小是10個字節,則下一節必定從402000h開始,即使401000h和402000h之間還有很多空間沒被使用。因為Windows管理內存采用分頁管理的方式,而每頁的大小為4k,也就是1000h,所以我們這個值為“00100000”。
        成員12,4個字節,表示段在文件中的對齊方式。文件中節對齊的粒度。例如,如果該值是(200h),,那么每節的起始地址必須是512的倍數。若第一節從文件偏移量200h開始且大小是10個字節,則下一節必定位於偏移量400h: 即使偏移量512和1024之間還有很多空間沒被使用。此值最好設為200h,所以該成員的值為“00020000”。
        成員13,2個字節,表示操作系統主版本號,此值不會影響程序的執行,我們這里填充零,此值為“0000”。
        成員14,2個字節,表示操作系統副版本號,此值不會影響程序的執行,我們這里填充零,此值為“0000”。
        成員15,2個字節,表示程序主版本號,此值不會影響程序的執行,我們這里填充零,此值為“0000”。
        成員16,2個字節,表示程序副版本號,此值不會影響程序的執行,我們這里填充零,此值為“0000”。
        成員17,2個字節,表示子系統主版本號。win32子系統版本。PE文件是專門為Win32設計的,該子系統版本必定是4.0那么此處值為“04”。
        成員18,2個字節,表示子系統副版本號,根據上面所說,此值應為“00”。
        成員19,2個字節,此值一般為“00”。
        成員20,4個字節,表示程序調入后占用內存大小(字節),等於所有段的長度之和。所有頭和節經過節對齊處理后的大小。我們知道,我們文件PE結構總長小於1000h,但是內存中的對齊粒度是1000h,所以PE結構被映射后要占1000h,盡管很多空間沒有使用,另外我們有3個段,每個段的長度小於1000h,但是被映射后同樣要占1000h,所以總共占用內存的大小為1000h + 3 * 1000h = 4000h,因此此值為“00400000”。
        成員21,4個字節,表示所有文件頭的長度之和(從文件開始到第一個段之間的大小)。所有頭+節表的大小,也就等於文件尺寸減去文件中所有節的尺寸。可以以此值作為PE文件第一節的文件偏移量。那么我們怎么得到這個值呢?我們的PE文件結構總大小為:64 + 112 + 4 + 20 + 224 + 3 * 40 = 544 byte 轉化成十六進制為220h,那么此值就是220h嗎?
  不是的,因為我們文件中的對齊粒度是200h,那么220h實際上要占用400h的空間,所以此值為“00040000”。
        成員22,4個字節,表示校驗和。它僅用在驅動程序中,在可執行文件中可能為0。它的計算方法Microsoft不公開,在imagehelp.dll中的CheckSumMappedFile()函數可以計算它,此處我們設為填充零,此值為“00000000”。
      成員23,2個字節,表示NT子系統,可能是以下的值:
      IMAGE_SUBSYSTEM_NATIVE (1) 不需要子系統。用在驅動程序中。
      IMAGE_SUBSYSTEM_WINDOWS_GUI(2) WIN32 graphical程序(它可用AllocConsole()來打開一個控制台,但是不能在一開始自動得到)。
      IMAGE_SUBSYSTEM_WINDOWS_CUI(3) WIN32 console程序(它可以一開始自動建立)。
      IMAGE_SUBSYSTEM_OS2_CUI(5) OS/2 console程序(因為程序是OS/2格式,所以它很少用在PE)。
      IMAGE_SUBSYSTEM_POSIX_CUI(7) POSIX console程序。
      Windows程序總是用WIN32子系統,所以只有2和3是合法的值。也就是說此值必須為2或3,如果是3,那么程序運行后會自動打開一個控制台,我們為了看一下效果,這里設為3,此值為“0300“。
        成員24,2個字節,表示Dll狀態,我們這里填充零,此值為“0000”。
        成員25,4個字節,保留堆棧大小,我們這里填充零,此值為“00000000”。
        成員26,4個字節,啟動后實際申請的堆棧數,可隨實際情況變大,我們這里填充零,此值為“00000000”。
        成員27,4個字節,保留堆大小,我們這里填充零,此值為“00000000”。
        成員28,4個字節,實際堆大小,我們這里填充零,此值為“00000000”。
        成員29,4個字節,裝載標志,我們這里填充零,此值為“00000000”。
        成員30,4個字節,在講這個成員之前,我們應該先了解成員31,成員31實際上是一個IMAGE_DATA_DIRECTORY結構的數組,成員30的值就是表示該數組的大小。通常有16個元素,所以此值為:“10000000”。

IMAGE_DIRECTORY_ENTRY_EXPORT
struct _IMAGE_DATA_DIRECTORY {
0x00   DWORD VirtualAddress;
0x04   DWORD Size;
};

 成員31,128個字節,上面說過他是一個IMAGE_DATA_DIRECTORY結構的數組,通常具有16個元素。
  IMAGE_DATA_DIRECTORY結構有兩個成員,各占4個字節,那么也就得到成員31的總大小:2 * 4 * 16 = 128byte。

中每個元素代表一個目錄表,每個目錄表表示的目錄如下:
  
      IMAGE_DIRECTORY_ENTRY_EXPORT (0)導出目錄用於DLL    
      IMAGE_DIRECTORY_ENTRY_IMPORT (1導入目錄    
      IMAGE_DIRECTORY_ENTRY_RESOURCE (2)資源目錄    
      IMAGE_DIRECTORY_ENTRY_EXCEPTION (3)異常目錄    
      IMAGE_DIRECTORY_ENTRY_SECURITY (4)安全目錄    
      IMAGE_DIRECTORY_ENTRY_BASERELOC (5)重定位表    
      IMAGE_DIRECTORY_ENTRY_DEBUG (6)調試目錄
      IMAGE_DIRECTORY_ENTRY_COPYRIGHT (7)描述版權串    
      IMAGE_DIRECTORY_ENTRY_GLOBALPTR (8)機器值    
      IMAGE_DIRECTORY_ENTRY_TLS (9)Thread local storage目錄
      IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG (10)Load configuration 目錄    
      IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (11)Bound import directory目錄    
      IMAGE_DIRECTORY_ENTRY_IAT (12)Import Address Table輸入地址表目錄
      IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT   13   // Delay Load Import Descriptors
      IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14   // COM Runtime descriptor
  
  是不是所有的目錄表都要關心呢?其實要把這些目錄表都研究清楚是個很大的課題,對於我們這個程序,只需關心第2個元素,導入目錄,它標識了我們的程序從其他模塊導入的函數信息。因為我們要顯示一個消息框,所以要導入user32.dll庫中的MessageBoxA函數,程序退出,又要導入kernel32.dll庫中的ExitProcess函數,這個目錄表需要使用。然而上面已說明每個目錄是一個IMAGE_DATA_DIRECTORY結構,該結構具有兩個成員,第一個成員表示目錄表的起始RVA地址,第二個成員表示目錄表的長度。這兩個值要根據.rdata段實體來確定,暫時先不填寫。為了記錄該位置,我們先都填寫為x,即:
  “xxxxxxxx","xxxxxxxx"。其余的統統添零即可。
  
       接下來是各段頭部,我們這里有3個段,.text(代碼段), .rdata(只讀數據段),data(全局變量數據段)。每段是一個IMAGE_SECTION_HEADER 結構,具有10個成員。首先我們來看.text段。

typedef struct _IMAGE_SECTION_HEADER {
0x00   BYTE  Name[IMAGE_SIZEOF_SHORT_NAME];
union {
0x08  DWORD PhysicalAddress;
0x08 DWORD VirtualSize;
} Misc;
0x0c   DWORD VirtualAddress; ;區塊的RVA地址
0x10   DWORD SizeOfRawData; ;在文件中對齊后的尺寸
0x14   DWORD PointerToRawData; ;在文件中偏移
0x18   DWORD PointerToRelocations; ;在OBJ文件中使用,重定位的偏移
0x1c   DWORD PointerToLinenumbers; ;行號表的便宜(供調試用)
0x20   WORD  NumberOfRelocations; ;在OBJ文件中使用,重定位項數目
0x22   WORD  NumberOfLinenumbers; ;行號表中行號的數目
0x24   DWORD Characteristics; ;區塊的屬性
};

如圖,各個成員分布如圖:


 成員1,8個字節,表識該段的名稱,我們這里是.text,那么此值是他的ASCII碼應該為“2E74657874000000”。
       成員2,4個字節,表示有效代碼所占的字節數。我們這里所有代碼數一下總共26h個,固此值為“26000000”。
       成員3,4個字節,表示在.text段映射到內存中的起始地址,那么這個值如何得來呢?我們知道.text是緊跟PE結構后的,然后整個PE結構映射到內存后占的大小為1000h(因為PE結構小於1000h個字節,而對齊力度粒度是1000h),那么此值便得到了,為“00100000”。
       成員4,4個字節,表示.text段在文件中所占的大小。因為我們的實際代碼只有26h個字節,那么這個值是不是26h呢?並不是,一定要注意段在文件中的對齊粒度是200h,所以此值為“00020000”。   成員5,4個字節,表示.text段在文件中的起始地址,上面已經計算過PE文件的總長度為400h,他實際上也就是.text的起始偏移地址,此值為“00040000”。
       成員6,7,8,9,均占4個字節,都僅用於目標文件,我們這里統統填為零。
       成員10,4個字節。包含標記以指示節屬性,比如節是否含有可執行代碼、初始化數據、未初始數據,是否可寫、可讀等。這個值實際上是二進制位進行或運算得到的值。各二進制位表示的意義如下:
  
      bit 5 (IMAGE_SCN_CNT_CODE),置1,節內包含可執行代碼。    
      bit 6 (IMAGE_SCN_CNT_INITIALIZED_DATA)置1,節內包含的數據在執行前是確定的。    
      bit 7 (IMAGE_SCN_CNT_UNINITIALIZED_DATA) 置1,本節包含未初始化的數據,執行前即將被初始化為0。一般是BSS.
      bit 9 (IMAGE_SCN_LNK_INFO) 置1,節內不包含映象數據除了注釋,描述或者其他文檔外,是一個目標文件的一部分,可能是針對鏈接器的信息。比如哪個庫被需要。
      bit 11 (IMAGE_SCN_LNK_REMOVE) 置1,在可執行文件鏈接后,作為文件一部分的數據被清除。
      bit 12 (IMAGE_SCN_LNK_COMDAT) 置1,節包含公共塊數據,是某個順序的打包的函數。
      bit 15 (IMAGE_SCN_MEM_FARDATA) 置1,不確定。
      bit 17 (IMAGE_SCN_MEM_PURGEABLE) 置1,節的數據是可清除的。
      bit 18 (IMAGE_SCN_MEM_LOCKED) 置1,節不可以在內存內移動。
      bit 19 (IMAGE_SCN_MEM_PRELOAD)置1, 節必須在執行開始前調入。
      Bits 20 to 23指定對齊。一般是庫文件的對象對齊。
      bit 24 (IMAGE_SCN_LNK_NRELOC_OVFL) 置1, 節包含擴展的重定位。
      bit 25 (IMAGE_SCN_MEM_DISCARDABLE) 置1,進程開始后節的數據不再需要。
      bit 26 (IMAGE_SCN_MEM_NOT_CACHED) 置1,節的 數據不得緩存。
      bit 27 (IMAGE_SCN_MEM_NOT_PAGED) 置1,節的 數據不得交換出去。
      bit 28 (IMAGE_SCN_MEM_SHARED) 置1,節的數據在所有映象例程內共享,如DLL的初始化數據。
      bit 29 (IMAGE_SCN_MEM_EXECUTE) 置1,進程得到“執行”訪問節內存。
      bit 30 (IMAGE_SCN_MEM_READ) 置1,進程得到“讀出”訪問節內存。
      bit 31 (IMAGE_SCN_MEM_WRITE)置1,進程得到“寫入”訪問節內存。
  
      在我們這里,因為這是代碼段,所以bit 5 (IMAGE_SCN_CNT_CODE)位置1,一般代碼段都含有初始化數據,那么bit 6 (IMAGE_SCN_CNT_INITIALIZED_DATA)位置1,有因為代碼段的代碼可以執行的,所以
 bit 29 (IMAGE_SCN_MEM_EXECUTE) 置1,那么這3個二進制位進行或運算最終得到此成員值“20000060”。

這個整個.text頭就編寫完畢,按照上面的方法,分別在編寫.rdata段和.data段。因為要對齊,所以后面的代碼用零補齊。
  最后的編寫結果如下:

 至此,我們已經完成了PE結構的編寫。但是,此時的程序還不能夠運行,我們還需要再耐心的補上點東西。
為了讓我們寫的程序可以運行,我們還要完成.text(代碼段), .rdata(只讀數據段),data(全局變量數據段)三個段的實體部分。
     首先編寫.text段,他緊接着PE結構后面,但是我們如何編寫這些內容呢?前面已經說過,.text段中存放所有的可執行代碼(機器碼),我們可以通過先編寫匯編指令(調用MessageBoxA和ExitProcess兩個函數),然后反匯編出機器代碼抄到這里就可以了。這里有一點要注意,我們在為MessageBoxA函數傳遞參數時,如何將“Hello Kinney!This is the first PE program!”字符串這就要用到我們的.data(全局變量數據段)了,我們可以把這兩個字符串放到這個段中,然后把字符串的偏移首地址作為參數傳給MessageBoxA即可。因為要以200h對齊,所以剩余部分用零補齊,最終得到的代碼如下:


 
接下來完成.rdata段,這個段非常重要,也有些繁瑣。要寫入導入表(_IMAGE_IMPORT_DESCRIPTOR),相當的繁瑣,我們找一個C++編譯過的程序,載入C32中,分析下它的導入表,如圖:





由此,我們可以完成我們的.rdata段的寫入了,寫好的代碼如下:

 最后一個是.data段,這個段非常簡單,就是MessageBoxA所需的參數,消息框的內容,最終代碼如下:
     (注意對齊問題,補足200h字節。)


  好了,到此為止一個完整的顯示Hello Kinney!This is the first PE program!的可執行程序就完成了,木有用到編譯器和鏈接器等等,爽吧?但是很累的說~!趕快雙擊運行一下吧...費了這么大勁就這么個小功能,是不是有點事倍功半呢?其實手寫這么個程序只是為了更加熟練掌握PE結構,相信當您真正的手工完成了這個程序,您對PE結構一定有一個非常深刻的理解。(提示:請按照以上步驟完成這個程序,之后再回頭從頭到尾聯系上下文仔細看一遍,因為很多地方都是前后關聯緊密的,只作一遍,或只讀一遍是很難融匯貫通的。)
    
我手寫的PE文件會打包上去,由於水平和時間的原因,難免有許多問題,或者表述不清楚的地方,請高手不吝賜教。

--------------------------------------------------------------------------------
http://www.2cto.com/uploadfile/2012/1205/20121205073143392.zip


免責聲明!

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



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