用户名: 密   码:
   飞诺网 加入收藏
飞诺网 软件编程 C C++ Java VB Delphi Foxpro 汇编语言 游戏开发 移动开发 软件工程师 软工与管理 VC shell编程 C#
C++系列教程 C++实例 C++技术文档 C++/C语言函数 Mangos

您当前的位置:飞诺网 >> c/c++ >> C++实例

PE格式文件的代码注入

www.diybl.com    时间 : 2008-11-24  作者:佚名   编辑:辉辉 点击:   [ 评论 ]

 
PE格式文件的代码注入
 
 
 
本文演示了在不需要重新编译源代码的情况下,怎样向Windows PE(Portable Executable)格式的文件(包括EXE、DLL、OCX)中注入自己的代码。
 

 
       前言
       或许,你想了解一个病毒程序是怎样把自身注入到一个正常的PE文件中的,又或者是,你为了保护某种数据而加密自己的PE文件,从而想实现一个打包或保护程序;而本文的目的,就是为了向大家展示,通常的EXE工具或某种恶意程序,是怎样实现上述目的的。
       可以基于本文中的代码创建一个你自己的EXE修改器,如果运用得当,它可以是一个EXE保护程序,相反的话,也可能发展成某种病毒。不管怎样,本文写作的意图是前者,对于任何不适当的用法,本文作者概不负责。
 
 
       首要条件
       对于阅读本文,不存在任何特定强制性的先决条件——即基础知识,如果你对调试器或者PE文件格式非常熟悉,可以跳过下面的两节,而这两节是为不具备调试器或PE文件格式知识的读者准备的。
 
 
       PE文件格式
       PE文件格式被定义用于在Windows操作系统上执行代码或存储运行程序所需的基本数据,例如:常量、变量、引入库的链接、资源数据等等,其由MS-DOS文件头信息、Windows NT文件信息、节头部及节映像等组成,见表1。
 
 
       MS-DOS头部数据
       MS-DOS头部数据让人回想起最初在Windows操作系统上部署程序的那些日子,那些在如Windows NT 3.51这类系统上部署程序的日子——这里是说,Win3.1、Win95、Win98不是开发所需的理想操作系统。这些MS-DOS数据的功能就是在你运行一个可执行文件时,调用一个函数显示出“This program can not be run in MS-DOS mode”或者“This program can be run only in Windows mode”,或者当你想在MS-DOS 6.0中运行一个Windows的EXE文件时,显示出一些类似的信息,所以这些数据是被保留在代码中以作提示之用,但其中最让人感兴趣的还是数据中的“MZ”,不管你相不相信,它是微软的一个程序员“Mark Zbikowski”的首字母缩写。
       对本文来说,MS-DOS头部数据中,只有PE标志的偏移量最重要,可据此来找出Windows NT信息数据的实际位置。在此,建议读者先看一下表1,再查看一下<Microsoft Visual Studio .NET>\VC7\PlatformSDK\include\目录中,或<Microsoft Visual Studio 6.0>\VC98\include\目录中的<winnt.h>头文件中的IMAGE_DOS_HEADER结构。
 
typedef struct _IMAGE_DOS_HEADER { // DOS .EXE头"MZ"
    WORD   e_magic;                //神奇数字
    WORD   e_cblp;                 //文件尾页的字节数
    WORD   e_cp;                  //文件页数
    WORD   e_crlc;                 //重定位信息
    WORD   e_cparhdr;              //段落中头部信息的大小
    WORD   e_minalloc;             //所需的最小额外段
    WORD   e_maxalloc;             //所需的最大额外段
    WORD   e_ss;                   //初始(相对)SS值
    WORD   e_sp;                   //初始SP 值
    WORD   e_csum;                //校验和
    WORD   e_ip;                   //初始IP值
    WORD   e_cs;                   //初始(相对)CS值
    WORD   e_lfarlc;                //重定位表的文件地址
    WORD   e_ovno;                //覆盖数
    WORD   e_res[4];               //保留字数
    WORD   e_oemid;               // OEM标识符(为e_oeminfo准备)
    WORD   e_oeminfo;             // OEM信息;e_oemid详细说明
    WORD   e_res2[10];             //保留字数
    LONG   e_lfanew;               //新exe头中文件的地址
 } IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
 
       结构中e_lfanew代表了Windows NT数据位置的偏移量。以下程序演示了如何从一个EXE文件中获取相关的头部信息。
 



 

 
 
       请留心观察,这些例子对全文来说,都是很重要的。
 
 

MS-DOS头信息

IMAGE_DOS_
HEADER

DOS EXE Signature

00000000 ASCII "MZ"
00000002 DW 0090
00000004 DW 0003
00000006 DW 0000
00000008 DW 0004
0000000A DW 0000
0000000C DW FFFF
0000000E DW 0000
00000010 DW 00B8
00000012 DW 0000
00000014 DW 0000
00000016 DW 0000
00000018 DW 0040
0000001A DW 0000
0000001C DB 00


0000003B DB 00
0000003C DD 000000F0

DOS_PartPag

DOS_PageCnt

DOS_ReloCnt

DOS_HdrSize

DOS_MinMem

DOS_MaxMem

DOS_ReloSS

DOS_ExeSP

DOS_ChkSum

DOS_ExeIPP

DOS_ReloCS

DOS_TablOff

DOS_Overlay


Reserved words

Offset to PE signature

MS-DOS Stub
程序

00000040 
­º
.´.Í!¸\LÍ!This program canno
00000060 t be run in DOS mode....$.......

Windows NT信息
IMAGE_
NT_HEADERS

Signature

PE signature (PE)

000000F0 ASCII "PE"

IMAGE_
FILE_HEADER

Machine

000000F4 DW 014C
000000F6 DW 0003
000000F8 DD 3B7D8410
000000FC DD 00000000
00000100 DD 00000000
00000104 DW 00E0
00000106 DW 010F

NumberOfSections

TimeDateStamp

PointerToSymbolTable

NumberOfSymbols

SizeOfOptionalHeader

Characteristics

IMAGE_
OPTIONAL_
HEADER32

MagicNumber

00000108 DW 010B
0000010A DB 07
0000010B DB 00
0000010C DD 00012800
00000110 DD 00009C00
00000114 DD 00000000
00000118 DD 00012475
0000011C DD 00001000
00000120 DD 00014000
00000124 DD 01000000
00000128 DD 00001000
0000012C DD 00000200
00000130 DW 0005
00000132 DW 0001
00000134 DW 0005
00000136 DW 0001
00000138 DW 0004
0000013A DW 0000
0000013C DD 00000000
00000140 DD 0001F000
00000144 DD 00000400
00000148 DD 0001D7FC
0000014C DW 0002
0000014E DW 8000
00000150 DD 00040000
00000154 DD 00001000
00000158 DD 00100000
0000015C DD 00001000
00000160 DD 00000000
00000164 DD 00000010
 
 
 
 

MajorLinkerVersion

MinorLinkerVersion

SizeOfCode

SizeOfInitializedData

SizeOfUninitializedData

AddressOfEntryPoint

BaseOfCode

BaseOfData

ImageBase

SectionAlignment

FileAlignment

MajorOSVersion

MinorOSVersion

MajorImageVersion

MinorImageVersion

MajorSubsystemVersion

MinorSubsystemVersion

Reserved

SizeOfImage

SizeOfHeaders

CheckSum

Subsystem

DLLCharacteristics

SizeOfStackReserve

SizeOfStackCommit

SizeOfHeapReserve

SizeOfHeapCommit

LoaderFlags

NumberOfRvaAndSizes

IMAGE_
DATA_DIRECTORY[16]

Export Table

Import Table

Resource Table

Exception Table

Certificate File

Relocation Table

Debug Data

Architecture Data

Global Ptr

TLS Table

Load Config Table

Bound Import Table

Import Address Table

Delay Import Descriptor

COM+ Runtime Header

Reserved

节信息

IMAGE_
SECTION_
HEADER[0]

Name[8]

000001E8 ASCII".text"
000001F0 DD 000126B0
000001F4 DD 00001000
000001F8 DD 00012800
000001FC DD 00000400
00000200 DD 00000000
00000204 DD 00000000
00000208 DW 0000
0000020A DW 0000
0000020C DD 60000020
    CODE|EXECUTE|READ

VirtualSize

VirtualAddress

SizeOfRawData

PointerToRawData

PointerToRelocations

PointerToLineNumbers

NumberOfRelocations

NumberOfLineNumbers

Characteristics




IMAGE_
SECTION_
HEADER[n]

00000210 ASCII".data"; SECTION
00000218 DD 0000101C ; VirtualSize = 0x101C
0000021C DD 00014000 ; VirtualAddress = 0x14000
00000220 DD 00000A00 ; SizeOfRawData = 0xA00
00000224 DD 00012C00 ; PointerToRawData = 0x12C00
00000228 DD 00000000 ; PointerToRelocations = 0x0
0000022C DD 00000000 ; PointerToLineNumbers = 0x0
00000230 DW 0000     ; NumberOfRelocations = 0x0
00000232 DW 0000     ; NumberOfLineNumbers = 0x0
00000234 DD C0000040 ; Characteristics =
                        INITIALIZED_DATA|READ|WRITE
00000238 ASCII".rsrc"; SECTION
00000240 DD 00008960 ; VirtualSize = 0x8960
00000244 DD 00016000 ; VirtualAddress = 0x16000
00000248 DD 00008A00 ; SizeOfRawData = 0x8A00
0000024C DD 00013600 ; PointerToRawData = 0x13600
00000250 DD 00000000 ; PointerToRelocations = 0x0
00000254 DD 00000000 ; PointerToLineNumbers = 0x0
00000258 DW 0000     ; NumberOfRelocations = 0x0
0000025A DW 0000     ; NumberOfLineNumbers = 0x0
0000025C DD 40000040 ; Characteristics =
                        INITIALIZED_DATA|READ

SECTION[0]

00000400 EA 22 DD 77 D7 23 DD 77 ê"Ýw×#Ýw
00000408 9A 18 DD 77 00 00 00 00 šÝw....
00000410 2E 1E C7 77 83 1D C7 77 .?ÇwƒÇw
00000418 FF 1E C7 77 00 00 00 00 ÿ?Çw....
00000420 93 9F E7 77 D8 05 E8 77 “ŸçwØèw
00000428 FD A5 E7 77 AD A9 E9 77 ý¥çw­©éw

00000430 A3 36 E7 77 03 38 E7 77 £6çw 8çw
00000438 41 E3 E6 77 60 8D E7 77 Aãæw`?çw
00000440 E6 1B E6 77 2B 2A E7 77 ææw+*çw
00000448 7A 17 E6 77 79 C8 E6 77 zæwyÈæw
00000450 14 1B E7 77 C1 30 E7 77 çwÁ0çw






SECTION[n]


0001BF00 63 00 2E 00 63 00 68 00 c...c.h.
0001BF08 6D 00 0A 00 43 00 61 00 m...C.a.
0001BF10 6C 00 63 00 75 00 6C 00 l.c.u.l.
0001BF18 61 00 74 00 6F 00 72 00 a.t.o.r.
0001BF20 11 00 4E 00 6F 00 74 00 .N.o.t.
0001BF28 20 00 45 00 6E 00 6F 00   .E.n.o.
0001BF30 75 00 67 00 68 00 20 00 u.g.h. .
0001BF38 4D 00 65 00 6D 00 6F 00 M.e.m.o.
0001BF40 72 00 79 00 00 00 00 00 r.y.....
0001BF48 00 00 00 00 00 00 00 00 ........
0001BF50 00 00 00 00 00 00 00 00 ........
0001BF58 00 00 00 00 00 00 00 00 ........
0001BF60 00 00 00 00 00 00 00 00 ........
0001BF68 00 00 00 00 00 00 00 00 ........
0001BF70 00 00 00 00 00 00 00 00 ........
0001BF78 00 00 00 00 00 00 00 00 ........

表1:PE文件格式结构
 
 
       Windows NT信息数据
       正如前一部分所提到的,MS-DOS头数据结构中,e_lfanew代表了Windows NT头数据位置的偏移量。因此,如果假定pMem是指向特定PE文件内存空间起始点的指针,那么以下代码就可以获得MS-DOS头与Windows NT头。
 
IMAGE_DOS_HEADER        image_dos_header;
IMAGE_NT_HEADERS        image_nt_headers;
PCHAR pMem;

memcpy(&image_dos_header, pMem,
       sizeof(IMAGE_DOS_HEADER));
memcpy(&image_nt_headers,
       pMem+image_dos_header.e_lfanew,
       sizeof(IMAGE_NT_HEADERS));
 
 
       取得PE文件的头部信息,看起来似乎很简单,不过还是推荐查阅一下MSDN中IMAGE_NT_HEADERS的结构定义,以领会NT头部映像中究竟包含了什么,以用于支持在Windows系列操作系统中运行代码。现在,你应该对Windows NT头部结构非常熟悉了吧,它由PE标志、文件头信息、可选头信息组成;另外别忘了,要看一下它们在MSDN中的注释。以下是IMAGE_NT_HEADERS的大体结构:
 
FileHeader->NumberOfSections
OptionalHeader->AddressOfEntryPoint
OptionalHeader->ImageBase
OptionalHeader->SectionAlignment
OptionalHeader->FileAlignment
OptionalHeader->SizeOfImage
OptionalHeader->
 DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]->VirtualAddress
OptionalHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]->Size
 
 
       由上,可清楚地看到,当Windows操作系统分配虚拟内存空间时,这些值和它们所扮演的角色的主要作用,在此不便赘述,MSDN中的注释已经非常详细。
 
       不过,对于PE头部的数据节,或OptionalHeader->DataDirectory[],也必须作一个简短的介绍。当你在Windows NT数据信息中查看Optional Header时,将会发现,在Optional Header的结尾处,有16个索引说明,并且包括相关的虚拟地址和大小。<winnt.h>文件中也清楚地说明了这些。
 
#define IMAGE_DIRECTORY_ENTRY_EXPORT           0   // Export Directory
#define IMAGE_DIRECTORY_ENTRY_IMPORT           1   // Import Directory
#define IMAGE_DIRECTORY_ENTRY_RESOURCE        2   // Resource Directory
#define IMAGE_DIRECTORY_ENTRY_EXCEPTION        3   // Exception Directory
#define IMAGE_DIRECTORY_ENTRY_SECURITY         4   // Security Directory
#define IMAGE_DIRECTORY_ENTRY_BASERELOC       5   // Base Relocation Table
#define IMAGE_DIRECTORY_ENTRY_DEBUG            6   // Debug Directory
#define IMAGE_DIRECTORY_ENTRY_ARCHITECTURE   7   // Architecture Specific Data
#define IMAGE_DIRECTORY_ENTRY_GLOBALPTR       8   // RVA of GP
#define IMAGE_DIRECTORY_ENTRY_TLS               9   // TLS Directory
#define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG 10 // Load Configuration Directory
#define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT   11 // Bound Import Directory in headers
#define IMAGE_DIRECTORY_ENTRY_IAT                 12   // Import Address Table
#define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT   13 // Delay Load Import Descriptors
#define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14 // COM Runtime descriptor
 
       注意,此处最后一个,也就是第15个,是否会被用于PE64(Windows x64可执行文件所用格式),还不得而知。
       举例来说,下列代码,就可以获取相对虚拟地址(RVA)和资源数据的大小。
 
DWORD dwRVA = image_nt_headers.OptionalHeader->
 DataDirectory[IMAGE_DIRECTORY_ENTRY_RESOURCE]->VirtualAddress;
DWORD dwSize = image_nt_headers.OptionalHeader->
 DataDirectory[IMAGE_DIRECTORY_ENTRY_RESOURCE]->Size;
 
       要理解这些数据索引的重要性,建议参考微软的Microsoft Portable Executable and the Common Object File Format Specification,此外,在本文后叙部分,也会讨论到这些索引的作用。
 
 
       节头部信息
       我们刚才也看到了,一个PE格式文件,是怎样在磁盘上申请位置和区域大小的,是怎样通过IMAGE_NT_HEADERS->OptionalHeader->SizeOfImage指示Windows任务管理器为程序分配虚拟内存的,同样,为了更好地理解,建议还是查阅一下MSDN中IMAGE_SECTION_HEADER的结构定义。如果要开发一个EXE包装器,那么VirtualSize、VirtualAddress、SizeOfRawData、PointerToRawData和Characteristics这些部分无疑具有非常重要意义;并且在开发期间,你必须熟练运用它们,不但要根据OptionalHeader->SectionAlignment排列VirtualSize和VirtualAddress,还要OptionalHeader->FileAlignment排列SizeOfRawData和PointerToRawData,否则,有可能损坏目标EXE文件,导致其不能运行。关于Characteristics,我把大部分的精力都放在了通过IMAGE_SCN_MEM_READ | IMAGE_SCN_MEM_WRITE | IMAGE_SCN_CNT_INITIALIZED_DATA来确定一个节上,并且更倾向于在处理目标EXE文件时,新的节可以得到初始化,如导入表(import table);此外,如果也需要它可以通过加载器修改其自身,那此时Characteristics需设置为可写。
 
       另外,也需要多留意节名,因为可以通过名字知道其含义,此外还是建议参考Microsoft Portable Executable and the Common Object File Format Specification文档,表2中列出了一些重要的节名及其含义:
 

".text"

代码节

"CODE"

通过Borland DelphiBorland Pascal链接的代码节

".data"

数据节

"DATA"

通过Borland DelphiBorland Pascal链接的数据节

".rdata"

常量节

".idata"

导入表

".edata"

导出表

".tls"

TLS

".reloc"

重定位信息

".rsrc"

资源信息

表2:节名
 
     最后要提醒一点的是,IMAGE_NT_HEADERS->FileHeader-><CODE>NumberOfSections代表了一个PE文件中节的数目,如果你添加或删除了一个PE文件中的节,请不要忘记对它作相应的调整,这就是所谓的“节注入”!
 
 
       调试器
       要开发一个PE文件工具,首要条件是有足够的跟踪调试工具的使用经验,此外,还必须熟悉汇编指令,一般来说,Intel的官方文档是最好的参考资料。
       要跟踪一个PE文件,SoftICE可能是大家所知道的最好的工具了,它可在不使用Windows API方式的情况下,使用内核模式来进行跟踪。另外,在此也准备向大家介绍一款工作在用户模式下的堪称完美的调试器——“OllyDbg”,它利用了Windows的调试API来跟踪一个PE文件,并且可把自身附加到一个活动的进程;这些API在Windows Kernel32库中,由Microsoft Team提供,你可以使用它们来跟踪特定的进程,或者,也许能够做出自己的调试器,以下是其中一些API:CreateThread()、CreateProcess()、OpenProcess()、DebugActiveProcess()、GetThreadContext()、SetThreadContext()、ContinueDebugEvent()、DebugBreak()、ReadProcessMemory()、WriteProcessMemory()、SuspendThread()及ResumeThread()。
 
 
       SofeICE
       在1987年,Frank Grossman和Jim Moskun决定在新罕布什尔州的纳舒厄创办一家名为NuMega Technologies的公司,以开发一些工具,用于跟踪Microsoft Windows软件程序及测试其的可靠性。现在,这家公司已成为了Compuware Corporation的一部分,而且,它的产品加速了Windows软件产品的可靠性进程;另外,在Windows驱动程序开发方面,Compuware DriverStudio可谓是人人皆知,它通过Windows Driver Development Kit (DDK),实现了一个带有着内核驱动或系统文件详尽细节的环境,Windows系统软件开发人员由此不必再涉及到DDK,就可以直接开发内核模式级别的PE文件了。而在本文中,只用到了DriverStudio中的一个产品——SoftICE,这个调试器可用于跟踪每一个PE文件,而不管它是处在内核模式或用户模式下。
 
 

EAX=00000000 EBX=7FFDD000 ECX=00, 07FFB0 EDX=7C90EB94 ESI=FFFFFFFF
EDI=7C919738
EBP=0007FFF0 ESP=0007FFC4 EIP=010119E0 o d i s z a p c
CS=0008 DS=0023 SS=0010 ES=0023 FS=0030 GS=0000
SS:0007FFC4=87C816D4F

0023:01013000 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
0023:01013010 01 00 00 00 20 00 00 00-0A 00 00 00 0A 00 00 00 ................
0023:01013020 20 00 00 00 00 00 00 00-53 63 69 43 61 6C 63 00 ........SciCalc.
0023:01013030 00 00 00 00 00 00 00 00-62 61 63 6B 67 72 6F 75 ........backgrou
0023:01013040 6E 64 00 00 00 00 00 00-2E 00 00 00 00 00 00 00 nd..............

0010:0007FFC4 4F 6D 81 7C 38 07 91 7C-FF FF FF FF 00 90 FD 7F Om |8 ‘| .
0010:0007FFD4 ED A6 54 80 C8 FF 07 00-E8 B4 F5 81 FF FF FF FF T .
0010:0007FFE4 F3 99 83 7C 58 6D 81 7C-00 00 00 00 00 00 00 00 Xm |........
0010:0007FFF4 00 00 00 00 E0 19 01 01-00 00 00 00 00 00 00 00 .... ....

010119E0 PUSH EBP
010119E1 MOV EBP,ESP
010119E3 PUSH -1
010119E5 PUSH 01001570
010119EA PUSH 01011D60
010119EF MOV EAX,DWORD PTR FS:[0]
010119F5 PUSH EAX
010119F6 MOV DWORD PTR FS:[0],ESP
010119FD ADD ESP,-68
01011A00 PUSH EBX
01011A01 PUSH ESI
01011A02 PUSH EDI
01011A03 MOV DWORD PTR SS:[EBP-18],ESP
01011A06 MOV DWORD PTR SS:[EBP-4],0

图1:SoftICE窗口
 
 
       OllyDbg
       大概是在4年前,一个很偶然的机会发现了这个调试器,从此就对它爱不释手,一方面因为SoftICE实在是价格太贵,另一方面,它只对DOS、Windows 98、Windows 2000支持得比较好,但OllyDbg几乎支持所有的Windows版本,它是一个可用于跟踪调试在用户模式下所有类型PE文件的调试器,但除了通用语言基础架构(CLI)程序。OllyDbg的作者Oleh Yuschuk是乌克兰人,现居住在德国,还要多提一点的是,他的调试器简直就是黑客和破解者的最佳选择,并且是免费软件,可以从http://www.ollydbg.de/处下载试用。
 
 
2OllyDbgCPU窗口
 
 
       调试器中哪一部分最重要
       前面已经介绍过两款调试器了,而没有说到怎样使用它们或该多留意哪一部分,关于怎样使用调试器,还是建议参考它们的帮助文档。不过在此,还是要讲一些调试器中的重要部分,当然,我们说的是系统低级调试器,换句话来说,我们在此谈得是x86 CPU机器语言调试器。
 
所有的系统低级调试器都由以下部分组成:
 
1、 寄存器部分

EAX

ECX

EDX

EBX

ESP

EBP

ESI

EDI

EIP

o d t s z a p c

 
 
2、 反汇编部分

010119E0 PUSH EBP
010119E1 MOV EBP,ESP
010119E3 PUSH -1
010119E5 PUSH 01001570
010119EA PUSH 01011D60
010119EF MOV EAX,DWORD PTR FS:[0]
010119F5 PUSH EAX
010119F6 MOV DWORD PTR FS:[0],ESP
010119FD ADD ESP,-68
01011A00 PUSH EBX
01011A01 PUSH ESI
01011A02 PUSH EDI
01011A03 MOV DWORD PTR SS:[EBP-18],ESP
01011A06 MOV DWORD PTR SS:[EBP-4],0

 
 
3、 内存查看部分

0023:01013000 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
0023:01013010 01 00 00 00 20 00 00 00-0A 00 00 00 0A 00 00 00 ................
0023:01013020 20 00 00 00 00 00 00 00-53 63 69 43 61 6C 63 00 ........SciCalc.
0023:01013030 00 00 00 00 00 00 00 00-62 61 63 6B 67 72 6F 75 ........backgrou
0023:01013040 6E 64 00 00 00 00 00 00-2E 00 00 00 00 00 00 00 nd..............

 
 
4、 堆栈查看部分

0010:0007FFC4 4F 6D 81 7C 38 07 91 7C-FF FF FF FF 00 90 FD 7F Om |8 ‘| .
0010:0007FFD4 ED A6 54 80 C8 FF 07 00-E8 B4 F5 81 FF FF FF FF T .
0010:0007FFE4 F3 99 83 7C 58 6D 81 7C-00 00 00 00 00 00 00 00 Xm |........
0010:0007FFF4 00 00 00 00 E0 19 01 01-00 00 00 00 00 00 00 00 .... ....

 
 
5、 用于调试过程的命令行、按钮、快捷键

Command

SoftICE

OllyDbg

Run

F5

F9

Step Into

F11

F7

Step Over

F10

F8

Set Break Point

F8

F2

 
 
       可以对比图1与图2,看一下SoftICE与OllyDbg有哪些不同之处。当开始跟踪一个PE文件时,以上五部分是最需要多留意的地方,此外,每一个调试器都有一些其他的有用工具,这就需要靠自己去摸索了。
 
 
       反汇编工具
       尽管SoftICE和OllyDbg都具有极其优秀的反汇编功能,但还是要介绍一下在逆向工程领域一些其他知名的反汇编工具。
 
² Proview或PVDasm:它们都是逆向工程社区中极受称赞的反汇编工具,你甚至可利用它的反汇编引擎,创建自己的反汇编工具。
² W32Dasm:可对16位及32位的可执行文件进行反汇编操作,另外,利用它的反汇编功能,还可以分析文件的导入表、导出表、资源数据目录等等。
² IDA Pro:所有逆向工程方面的高手都知道IDA Pro不但能用于x86指令集的CPU,而且能用于其他如AVR、PIC指令集的CPU;并且以图形和表格的形式,表示出PE格式文件的汇编资源,这对新手来说,是非常有用的。此外,它和OllyDbg一样,也可在用户模式级别对可执行文件进行跟踪。
 
 
       其他可用工具
       在开发PE文件工具软件之前,首先需要了解一些有关PE文件格式的信息,以下是一些可用到的软件,用于找出隐藏在PE文件下的基本信息。
 
       LordPE
       LordPE是查看PE格式文件信息的首选工具,并且可以修改相关信息。
 
 
 
       PEiD
       PE iDentifier在确定PE文件的编译器类型、打包器、加密器方面,有极高的实用价值;到目前为止,它可以探测出超出500种不同的PE文件标志。
 
 
 
       Resource Hacker
       Resource Hacker可用于修改资源索引信息、图标、菜单、版本号、字符串表等等。
 
 
 
       WinHex
       WinHex,只要你能想到的,它都能做到。
 
 
 
       CFF Explorer
       最后,隆重出场的是CFF Explorer,它简直是梦想中的PE工具软件,它同时支持PE32/64、及包括CLI文件在内的PE格式文件——换句话说,可用于修改 .NET文件的资源,它具有其他工具所不具有的多种功能,只有亲身使用,才能体会到它的种种不可思议的功能。
 
 
 
 
       添加新节并修改OEP
       现在,我们要开始第一步了,在开始之前,必须要非常熟悉PE文件的头部信息,在此可使用OllyDbg,先打开一个PE文件,然后点击菜单“View->Executable file”,再在弹出菜单中选择“Special->PE header”,将会看到类似图3的界面;再回到主菜单中“View->Memory”,尝试在“Memory map”窗口中找出节信息。
 

00000000
00000002
00000004
00000006
00000008
0000000A
0000000C
0000000E
00000010
00000012
00000014
00000016
00000018
0000001A
0000001C
0000001D
0000001E
0000001F
00000020
00000021
00000022
00000023
00000024
00000025
00000026
00000027
00000028
00000029
0000002A
0000002B
0000002C
0000002D
0000002E
0000002F
00000030
00000031
00000032
00000033
00000034
00000035
00000036
00000037
00000038
00000039
0000003A
0000003B
0000003C

 4D 5A
 9000
 0300
 0000
 0400
 0000
 FFFF
 0000
 B800
 0000
 0000
 0000
 4000
 0000
 00
 00
 00
 00
 00
 00
 00
 00
 00
 00
 00
 00
 00
 00
 00
 00
 00
 00
 00
 00
 00
 00
 00
 00
 00
 00
 00
 00
 00
 00
 00
 00
 F0000000 

 ASCII "MZ"
 DW 0090
 DW 0003
 DW 0000
 DW 0004
 DW 0000
 DW FFFF
 DW 0000
 DW 00B8
 DW 0000
 DW 0000
 DW 0000
 DW 0040
 DW 0000
 DB 00
 DB 00
 DB 00
 DB 00
 DB 00
 DB 00
 DB 00
 DB 00
 DB 00
 DB 00
 DB 00
 DB 00
 DB 00
 DB 00
 DB 00
 DB 00
 DB 00
 DB 00
 DB 00
 DB 00
 DB 00
 DB 00
 DB 00
 DB 00
 DB 00
 DB 00
 DB 00
 DB 00
 DB 00
 DB 00
 DB 00
 DB 00
 DD 000000F0

 DOS EXE Signature
 DOS_PartPag = 90 (144.)
 DOS_PageCnt = 3
 DOS_ReloCnt = 0
 DOS_HdrSize = 4
 DOS_MinMem = 0
 DOS_MaxMem = FFFF (65535.)
 DOS_ReloSS = 0
 DOS_ExeSP = B8
 DOS_ChkSum = 0
 DOS_ExeIP = 0
 DOS_ReloCS = 0
 DOS_TablOff = 40
 DOS_Overlay = 0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 Offset to PE signature

3
 
       在此拿Windows XP的的计算器“calc.exe”作示例程序,讲解一下怎样修改OEP(Offset of Entry Point)。首先,可使用PE Viewer,查找OEP,在0x00012475,再找到映像基地址,在0x01000000,此OEP值是相对虚地址,因此,映像基地址被用于转换为虚地址。
 
虚地址=映像基地址+相对虚地址
 
DWORD OEP_RVA = image_nt_headers->OptionalHeader.AddressOfEntryPoint ;
// OEP_RVA = 0x00012475
DWORD OEP_VA = image_nt_headers->OptionalHeader.ImageBase + OEP_RVA ;
// OEP_VA = 0x01000000 + 0x00012475 = 0x01012475
 
 
      在我们的PE Maker程序中,DynLoader()保留作新节的数据。
 
__stdcall void DynLoader()
{
_asm
{
//----------------------------------
    DWORD_TYPE(DYN_LOADER_START_MAGIC)
//----------------------------------
    MOV EAX,01012475h //原始OEP
    JMP EAX
//----------------------------------
    DWORD_TYPE(DYN_LOADER_END_MAGIC)
//----------------------------------
}
}
 
 
       获取并再生成PE文件
      此处,有一个简单的类,可用于恢复PE信息,并可把它用在一个新的PE文件中。
 
class CPELibrary
{
private:
    //-----------------------------------------
    PCHAR                   pMem;
    DWORD                   dwFileSize;
    //-----------------------------------------
protected:
    //-----------------------------------------
    PIMAGE_DOS_HEADER       image_dos_header;
    PCHAR                   pDosStub;
    DWORD                   dwDosStubSize, dwDosStubOffset;
    PIMAGE_NT_HEADERS       image_nt_headers;
    PIMAGE_SECTION_HEADER   image_section_header[MAX_SECTION_NUM];
    PCHAR                   image_section[MAX_SECTION_NUM];
    //-----------------------------------------
protected:
    //-----------------------------------------
    DWORD PEAlign(DWORD dwTarNum,DWORD dwAlignTo);
    void AlignmentSections();
    //-----------------------------------------
    DWORD Offset2RVA(DWORD dwRO);
    DWORD RVA2Offset(DWORD dwRVA);
    //-----------------------------------------
    PIMAGE_SECTION_HEADER ImageRVA2Section(DWORD dwRVA);
    PIMAGE_SECTION_HEADER ImageOffset2Section(DWORD dwRO);
    //-----------------------------------------
    DWORD ImageOffset2SectionNum(DWORD dwRVA);
    PIMAGE_SECTION_HEADER AddNewSection(char* szName,DWORD dwSize);
    //-----------------------------------------
public:
    //-----------------------------------------
    CPELibrary();
    ~CPELibrary();
    //-----------------------------------------
    void OpenFile(char* FileName);
    void SaveFile(char* FileName);   
    //-----------------------------------------
};
 
 
      在表1中,image_dos_headerpDosStubimage_nt_headersimage_section_header[MAX_SECTION_NUM]、和image_section[MAX_SECTION_NUM]的用法已十分清楚,我们使用OpenFile()SaveFile()来获取并再生成PE文件,此外,AddNewSection()用于创建新节。
 
 
       为新节创建数据
       下面的类中,包含了新节的数据,然而,新节却是由DynLoader()创建的,我们利用CPECryptor类为新节引入数据和一些其他的东西。
 
class CPECryptor: public CPELibrary
{
private:
    //----------------------------------------
    PCHAR pNewSection;
    //----------------------------------------
    DWORD GetFunctionVA(void* FuncName);
    void* ReturnToBytePtr(void* FuncName, DWORD findstr);
    //----------------------------------------
protected:
    //----------------------------------------
public:   
    //----------------------------------------
    void CryptFile(int(__cdecl *callback) (unsigned int, unsigned int));
    //----------------------------------------
};
 
 
       创建新PE文件的注意事项
 
² 根据SectionAlignment调整每节的VirtualAddress和VirtualSize:
image_section_header[i]->VirtualAddress=PEAlign(image_section_header[i]->VirtualAddress,image_nt_headers->OptionalHeader.SectionAlignment);
image_section_header[i]->Misc.VirtualSize=PEAlign(image_section_header[i]->Misc.VirtualSize,image_nt_headers->OptionalHeader.SectionAlignment);
² 根据FileAlignment调整每节的PointerToRawData和SizeOfRawData:
image_section_header[i]->PointerToRawData=PEAlign(image_section_header[i]->PointerToRawData,image_nt_headers->OptionalHeader.FileAlignment);
image_section_header[i]->SizeOfRawData=PEAlign(image_section_header[i]->SizeOfRawData,image_nt_headers->OptionalHeader.FileAlignment);
² 根据最后一节的虚地址与虚尺寸调整SizeofImage:
image_nt_headers->OptionalHeader.SizeOfImage=image_section_header[LastSection]->VirtualAddress+image_section_header[LastSection]->Misc.VirtualSize;
² 把引入目录索引界限头部设置为零,因为此目录索引对执行一个PE文件来说,不是特别重要:
image_nt_headers->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT].VirtualAddress = 0;
image_nt_headers->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT].Size = 0;
 
 
       链接此VC工程时的注意事项
       设置Linker->General->Enable Incremental Linking为No (/INCREMENTAL:NO),即取消增量链接。
 
 
      可通过下图来比较一下增量链接与非增量链接的区别。
 

 
      为取得DynLoader()的虚地址,必须先知道增量链接中的JMP pemaker.DynLoader的虚地址,但如果是非增量链接,那么可通过下一行代码来取得真实的虚地址:
 
DWORD dwVA= (DWORD) DynLoader;
 
       当你想通过下例的CPECryptor::ReturnToBytePtr()来找到Loader、DynLoader()的开始与结束处,此设置就非常重要了:
 
void* CPECryptor::ReturnToBytePtr(void* FuncName, DWORD findstr)
{
    void* tmpd;
    __asm
   {
        mov eax, FuncName
        jmp df
hjg:    inc eax
df:     mov ebx, [eax]
        cmp ebx, findstr
        jnz hjg
        mov tmpd, eax
    }
    return tmpd;
}
 
 
       存储重要数据并取得原始OEP
       现在,我们必须保存原始OEP与映像基地址,以便取得OEP的虚地址,已经在DynLoader()的末尾保留了一块空间用于存储它。
 
__stdcall void DynLoader()
{
_asm
{
//----------------------------------
    DWORD_TYPE(DYN_LOADER_START_MAGIC)
//----------------------------------
Main_0:
    PUSHAD
    // get base ebp
    CALL Main_1
Main_1:   
    POP EBP
    SUB EBP,OFFSET Main_1
    MOV EAX,DWORD PTR [EBP+_RO_dwImageBase]
    ADD EAX,DWORD PTR [EBP+_RO_dwOrgEntryPoint]
    PUSH EAX
    RETN // >>跳到原始OEP
//----------------------------------
    DWORD_TYPE(DYN_LOADER_START_DATA1)
//----------------------------------
_RO_dwImageBase:                DWORD_TYPE(0xCCCCCCCC)
_RO_dwOrgEntryPoint:            DWORD_TYPE(0xCCCCCCCC)
//----------------------------------
    DWORD_TYPE(DYN_LOADER_END_MAGIC)
//----------------------------------
}
}
 
 
       新的函数CPECryptor::CopyData1()将会实现复制映像基地址值与进入点偏移量的值到loader末尾的8字节空间中。
 
 
       恢复第一个寄存器上下文
       在线程中恢复原始上下文非常重要,在前面的DynLoader()中还未完成此功能,现在修改源代码以恢复第一个上下文。
 
__stdcall void DynLoader()
{
_asm
{
//----------------------------------
    DWORD_TYPE(DYN_LOADER_START_MAGIC)
//----------------------------------
Main_0:
    PUSHAD//把寄存器上下文保存在堆栈中
    CALL Main_1
Main_1:   
    POP EBP//取得Base EBP
    SUB EBP,OFFSET Main_1
    MOV EAX,DWORD PTR [EBP+_RO_dwImageBase]
    ADD EAX,DWORD PTR [EBP+_RO_dwOrgEntryPoint]
    MOV DWORD PTR [ESP+1Ch],EAX // pStack.Eax <- EAX
    POPAD //从堆栈中恢复第一个寄存器上下文
    PUSH EAX
    XOR EAX, EAX
    RETN // >>跳到原始OEP
//----------------------------------
    DWORD_TYPE(DYN_LOADER_START_DATA1)
//----------------------------------
_RO_dwImageBase:                DWORD_TYPE(0xCCCCCCCC)
_RO_dwOrgEntryPoint:            DWORD_TYPE(0xCCCCCCCC)
//----------------------------------
    DWORD_TYPE(DYN_LOADER_END_MAGIC)
//----------------------------------
}
}
 
 
       恢复原始堆栈
       把堆栈开始值加上0x34所得到的位置值设置为原始OEP值,可恢复原始堆栈,但此步骤不是非常重要的。然而,在下例代码中,通过一点小技巧完成了加载器代码,并取得OEP,另外对堆栈也作了一点修饰,可在OllyDbg或SoftICE中跟踪观察此代码的动作。
 
__stdcall void DynLoader()
{
_asm
{
//----------------------------------
    DWORD_TYPE(DYN_LOADER_START_MAGIC)
//----------------------------------
Main_0:
    PUSHAD //把寄存器上下文保存在堆栈中
    CALL Main_1
Main_1:   
    POP EBP
    SUB EBP,OFFSET Main_1
    MOV EAX,DWORD PTR [EBP+_RO_dwImageBase]
    ADD EAX,DWORD PTR [EBP+_RO_dwOrgEntryPoint]
    MOV DWORD PTR [ESP+54h],EAX // pStack.Eip <- EAX
POPAD //从堆栈中恢复第一个寄存器上下文
CALL _OEP_Jump
    DWORD_TYPE(0xCCCCCCCC)
_OEP_Jump:
    PUSH EBP
    MOV EBP,ESP
    MOV EAX,DWORD PTR [ESP+3Ch] // EAX <- pStack.Eip
    MOV DWORD PTR [ESP+4h],EAX // _OEP_Jump RETURN pointer <- EAX
    XOR EAX,EAX
    LEAVE
    RETN
//----------------------------------
    DWORD_TYPE(DYN_LOADER_START_DATA1)
//----------------------------------
_RO_dwImageBase:                DWORD_TYPE(0xCCCCCCCC)
_RO_dwOrgEntryPoint:            DWORD_TYPE(0xCCCCCCCC)
//----------------------------------
    DWORD_TYPE(DYN_LOADER_END_MAGIC)
//----------------------------------
}
}
 
 
       通过结构化异常处理(SEH)取得OEP
       当程序执行了有缺陷的代码或有错误发生时,此时就会产生异常,在特殊条件下,程序会直接跳到线程信息块(TIB)列出的异常处理中。下例的try-except语句阐明了结构化异常处理的操作步骤,除去代码中的汇编语句,下例还说明了怎样启用结构化异常处理,抛出异常及异常处理函数。
 
#include "stdafx.h"
#include "windows.h"
 
void RAISE_AN_EXCEPTION()
{   
_asm
{
    INT 3
    INT 3
    INT 3
    INT 3
}
}
 
int _tmain(int argc, _TCHAR* argv[])
{
    __try
    {
        __try{
            printf("1: Raise an Exception\n");
            RAISE_AN_EXCEPTION();
        }
        __finally
        {
            printf("2: In Finally\n");
        }
    }
    __except( printf("3: In Filter\n"), EXCEPTION_EXECUTE_HANDLER )
    {
        printf("4: In Exception Handler\n");
    }
    return 0;
}
 
 
; main()
00401000: PUSH EBP
00401001: MOV EBP,ESP
00401003: PUSH -1
00401005: PUSH 00407160
; __try {
; the structured exception handler (SEH) installation
0040100A: PUSH _except_handler3 
0040100F: MOV EAX,DWORD PTR FS:[0]
00401015: PUSH EAX
00401016: MOV DWORD PTR FS:[0],ESP
0040101D: SUB ESP,8
00401020: PUSH EBX
00401021: PUSH ESI
00401022: PUSH EDI
00401023: MOV DWORD PTR SS:[EBP-18],ESP
;     __try {
00401026: XOR ESI,ESI
00401028: MOV DWORD PTR SS:[EBP-4],ESI
0040102B: MOV DWORD PTR SS:[EBP-4],1
00401032: PUSH OFFSET "1: Raise an Exception"
00401037: CALL printf
0040103C: ADD ESP,4
; the raise a exception, INT 3 exception
; RAISE_AN_EXCEPTION()
0040103F: INT3     
00401040: INT3
00401041: INT3
00401042: INT3
;     } __finally {
00401043: MOV DWORD PTR SS:[EBP-4],ESI
00401046: CALL 0040104D
0040104B: JMP 00401080
0040104D: PUSH OFFSET "2: In Finally"
00401052: CALL printf
00401057: ADD ESP,4
0040105A: RETN
;     }
; }
; __except(
0040105B: JMP 00401080
0040105D: PUSH OFFSET "3: In Filter"
00401062: CALL printf
00401067: ADD ESP,4
0040106A: MOV EAX,1 ; EXCEPTION_EXECUTE_HANDLER = 1
0040106F: RETN
;     , EXCEPTION_EXECUTE_HANDLER )
; {
; the exception handler funtion
00401070: MOV ESP,DWORD PTR SS:[EBP-18]
00401073: PUSH OFFSET "4: In Exception Handler"
00401078: CALL printf
0040107D: ADD ESP,4
; }
00401080: MOV DWORD PTR SS:[EBP-4],-1
0040108C: XOR EAX,EAX
; restore previous SEH
0040108E: MOV ECX,DWORD PTR SS:[EBP-10]
00401091: MOV DWORD PTR FS:[0],ECX
00401098: POP EDI
00401099: POP ESI
0040109A: POP EBX
0040109B: MOV ESP,EBP
0040109D: POP EBP
0040109E: RETN
 
 
       用上述代码生成一个Win32控制台项目,并链接运行,以下是输出:
1: Raise an Exception
3: In Filter
2: In Finally
4: In Exception Handler
 
       示例中引发了INT 3异常,当此异常发生时,程序会运行到代表异常发生的表达式中:printf("3: In Filter\n");,也可尝试其他种类的异常,在OllyDbg中,Debugging options->Exceptions,在此可看到各种不同类型异常的一个简短列表。
 
 
 
       实现异常处理程序
      我们期望构造一个结构化异常处理来取得OEP,现在,通过前述的汇编代码,大家应该可以分清SEH的启用、异常抛出、异常处理了。为建立我们自己的异常处理手段,可采用以下代码:
 
² 启用SEH
LEA EAX,[EBP+_except_handler1_OEP_Jump]
PUSH EAX
PUSH DWORD PTR FS:[0]
MOV DWORD PTR FS:[0],ESP
² 抛出异常
INT 3
² 异常处理程序
_except_handler1_OEP_Jump:
PUSH EBP
MOV EBP,ESP
...
MOV EAX, EXCEPTION_CONTINUE_SEARCH // EXCEPTION_CONTINUE_SEARCH = 0
LEAVE
RETN
 
 
       接着,我们要把C++代码变成汇编代码,以便通过SEH取得进入点的偏移量。
 
C++代码:
__try //启用SEH
{
    __asm
    {
        INT 3 //抛出异常
    }
}
__except( ..., EXCEPTION_CONTINUE_SEARCH ){}
//异常处理程序
 
 
汇编代码:
; ----------------------------------------------------
    ;开始启用SEH
    ; __try {
    LEA EAX,[EBP+_except_handler1_OEP_Jump]
    PUSH EAX
    PUSH DWORD PTR FS:[0]
    MOV DWORD PTR FS:[0],ESP
    ; ----------------------------------------------------
; 抛出一个INT 3异常
 1 2
如果图片或页面不能正常显示请点击这里
C++实例推荐文章

文章评论