PE 快餐车
本帖最后由 whypro 于 2010-5-23 13:36 编辑网友说我的文章太长了我只好再起一贴了,跟随PE快速入门继续讨论!
004017AD|.8B3D 64114000 mov edi,dword ptr ds:
004017B3|.66:813F 4D5Acmp word ptr ds:,5A4D
004017B8|.74 1B je short yC.004017D5
004017BA|.FF35 64114000 push dword ptr ds: ; /hMem = 01050020
004017C0|.E8 AB130000 call <jmp.&KERNEL32.GlobalFree> ; \GlobalFree
004017C5|.FF35 84114000 push dword ptr ds: ; /hObject = 000000B4 (window)
004017CB|.E8 76130000 call <jmp.&KERNEL32.CloseHandle> ; \CloseHandle
004017D0|.E9 C8020000 jmp yC.00401A9D
004017D5|>037F 3C add edi,dword ptr ds:
004017D8|.66:813F 5045cmp word ptr ds:,4550
004017DD|.74 1B je short yC.004017FA
004017DF|.FF35 64114000 push dword ptr ds: ; /hMem = 01050020
004017E5|.E8 86130000 call <jmp.&KERNEL32.GlobalFree> ; \GlobalFree
004017EA|.FF35 84114000 push dword ptr ds: ; /hObject = 000000B4 (window)
004017F0|.E8 51130000 call <jmp.&KERNEL32.CloseHandle> ; \CloseHandle
004017F5|.E9 A3020000 jmp yC.00401A9D
004017FA|>893D 74114000 mov dword ptr ds:,edi
00401800|.FFB7 80000000 push dword ptr ds:
00401806|.8F05 80114000 pop dword ptr ds:
0040180C|.66:FF77 06 push word ptr ds:
00401810|.66:8F05 78114>pop word ptr ds:
00401817|.833D 78114000>cmp dword ptr ds:,14
0040181E|.76 05 jbe short yC.00401825
00401820|.E9 94020000 jmp yC.00401AB9
00401825|>FF77 28 push dword ptr ds:
00401828|.8F05 68254000 pop dword ptr ds:
0040182E|.FF77 34 push dword ptr ds:
00401831|.8F05 64254000 pop dword ptr ds:
艰苦的旅程才刚刚开始:
经过CreateFile->GetFileSize->GlobalAlloc->ReadFile操作后我们来到PE文件头:
PE文件结构布局
找到文件中某一结构信息有两种定位方法。第一种是通过链表方法,对于这种方法,数据在文件的存放位置比较自由。第二种方法是采用紧凑或固定位置存放,这种方法要求数据结构大小固定,它在文件中的存放位置也相对固定。在PE文件结构中同时采用以上两种方法。
因为在PE文件头中的每个数据结构大小是固定的,因此能够编写计算程序来确定某一个PE文件中的某个参数值。在编写程序时,所用到的数据结构定义,包括数据结构中变量类型、变量位置和变量数组大小都必须采用Windows提供的原型。如图所示的PE文件结构的总体层次分布如下:
PE文件结构总体层次分布
· DOS MZ Header
所有 PE文件(甚至32位的DLLs)必须以简单的DOS MZ header开始,它是一个IMAGE_DOS_HEADER结构。有了它,一旦程序在DOS下执行,DOS就能识别出这是有效的执行体,然后运行紧随MZ Header之后的DOS Stub。
· DOS Stub
DOS Stub实际上是个有效的EXE,在不支持PE文件格式的操作系统中,它将简单显示一个错误提示,类似于字符串“This program requires Windows”或者程序员可根据自己的意图实现完整的DOS代码。大多数情况下DOS Stub由汇编器/编译器自动生成。
· PE Header
紧接着DOS Stub的是PE Header。它是一个IMAGE_NT_HEADERS结构。其中包含了很多PE文件被载入内存时需要用到的重要域。执行体在支持PE文件结构的操作系统中执行时,PE装载器将从DOS MZ header中找到PE header的起始偏移量。因而跳过DOS Stub直接定位到真正的文件头 PE header。
· Section Table
PE Header之后是数组结构Section Table(节表)。如果PE文件里有5个节,那么此Section Table结构数组内就有5个(IMAGE_SECTION_HEADER)成员,每个成员包含对应节的属性、文件偏移量、虚拟偏移量等。排在节表中的最前面的第一个默认成员是text,即代码节头。通过遍历查找方法可以找到其他节表成员(节表头)。
· Sections
PE文件的真正内容划分成块,称为Sections(节)。每个标准节的名字均以圆点开头,但也可以不以圆点开头,节名的最大长度为8个字节。Sections是以其起始位址来排列,而不是以其字母次序来排列。通过节表提供的信息,可以找到这些节。程序的代码,资源等就放在这些节中。
节的划分是基于各组数据的共同属性,而不是逻辑概念。每节是一块拥有共同属性的数据,比如代码/数据、读/写等。如果PE文件中的数据/代码拥有相同属性,它们就能被归入同一节中。节名称仅仅是个区别不同节的符号而已,类似“data”,“code”的命名只为了便于识别,唯有节的属性设置决定了节的特性和功能。 本帖最后由 whypro 于 2010-5-23 07:28 编辑
PE文件内存映射
在Windows系统下,当一个PE应用程序运行时,这个PE文件在磁盘中的数据结构布局和内存中的数据结构布局是一致的。系统在载入一个可执行程序时,首先是Windows装载器(又称PE装载器)把磁盘中的文件映射到进程的地址空间,它遍历PE文件并决定文件的哪一部分被映射。其方式是将文件较高的偏移位置映射到较高的内存地址中。磁盘文件一旦被装入内存中,其某项的偏移地址可能与原始的偏移地址有所不同,但所表现的是一种从磁盘文件偏移到内存偏移的转换,如图2.2所示。
PE文件内存映射
当PE文件被加载到内存后,内存中的版本称为模块(Module),映射文件的起始地址称为模块句柄(hModule),可以通过模块句柄访问内存中的其他数据结构。这个初始内存地址也称为文件映像基址(ImageBase)。载入一个PE程序的主要步骤如下:
(1)当PE文件被执行时,PE装载器首先为进程分配一个4GB的虚拟地址空间,然后把程序所占用的磁盘空间作为虚拟内存映射到这个4GB的虚拟地址空间中。一般情况下,会映射到虚拟地址空间中0x400000的位置。装载一个应用程序的时间比一般人所设想的要少,因为装载一个PE文件并不是把这个文件一次性地从磁盘读到内存中,而是简单地做一个内存映射,映射一个大文件和映射一个小文件所花费的时间相差无几。当然,真正执行文件中的代码时,操作系统还是要把存在于磁盘上的虚拟内存中的代码交换到物理内存(RAM)中。但是,这种交换也不是把整个文件所占用的虚拟地址空间一次性地全部从磁盘交换到物理内存中,操作系统会根据需要和内存占用情况交换一页或多页。当然,这种交换是双向的,即存在于物理内存中的一部分当前没有被使用的页,也可能被交换到磁盘中。
(2)PE装载器在内核中创建进程对象和主线程对象以及其他内容。
(3)PE装载器搜索PE文件中的Import Table(引入表),装载应用程序所使用的动态链接库。对动态链接库的装载与对应用程序的装载方法完全类似。
(4)PE装载器执行PE文件首部所指定地址处的代码,开始执行应用程序主线程。 本帖最后由 whypro 于 2010-5-23 07:32 编辑
MS-DOS头部
MS-DOS头部占据了PE文件的头64个字节,描述它内容的结构如下:
l
// 此结构包含于WINNT.H中
//
typedef struct _IMAGE_DOS_HEADER { // DOS的.EXE头部
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; // 保留字
WORD e_oemid; // OEM标识符(相对e_oeminfo)
WORD e_oeminfo; // OEM信息
WORD e_res2; // 保留字
LONG e_lfanew; // 新exe头部的文件地址
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
l
其中第一个域e_magic,被称为魔术数字,它用于表示一个MS-DOS兼容的文件类型。所有MS-DOS兼容的可执行文件都将这个值设为0x5A4D,表示ASCII字符MZ。MS-DOS头部之所以有的时候被称为MZ头部,就是这个缘故。还有许多其他的域对于MS-DOS操作系统来说都有用,但是对于Windows NT来说,这个结构中只有一个有用的域——最后一个域e_lfnew,一个4字节的文件偏移量,PE文件头部就是由它定位的。
对应这句,判断MS-DOS头部
004017B3|.66:813F 4D5Acmp word ptr ds:,5A4D
004017B8|.74 1B je short yC.004017D5 本帖最后由 whypro 于 2010-5-23 08:40 编辑
PE Header是紧跟在MS-DOS头部和实模式程序残余之后的,描述它内容的结构 如下:
l
typedef struct_IMAGE_NT_HEADERS {
DWORD Signature; // PE文件头标志:"PE\0\0"
IMAGE_FILE_HEADER FileHeader; // PE文件物理分布的信息
IMAGE_OPTIONAL_HEADER32 OptionalHeader; // PE文件逻辑分布的信息
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
利用DWORD Signature;// PE文件头标志:"PE\0\0"判断是否是pe文件。
004017D5|> \037F 3C add edi,dword ptr ds:
004017D8|.66:813F 5045cmp word ptr ds:,4550
004017DD|.74 1B je short yC.004017FA
004017FA|> \893D 74114000 mov dword ptr ds:,edi<-------PE文件头标志
00401800|.FFB7 80000000 push dword ptr ds:<------------虚拟地址,相对于加载地址的偏移量,以后用来配合找到引入表。
00401806|.8F05 80114000 pop dword ptr ds:<----保存起来
0040180C|.66:FF77 06 push word ptr ds:<------查查有多少个节表
00401810|.66:8F05 78114>pop word ptr ds:<----保存起来
00401817|.833D 78114000>cmp dword ptr ds:,14<---节表不太多就好!
0040181E|.76 05 jbe short yC.00401825
00401820|.E9 94020000 jmp yC.00401AB9
00401825|> \FF77 28 push dword ptr ds:<-------32位的RVA.是入口点的偏移量。(AddressOfEntryPoint).执行从此开始。
00401828|.8F05 68254000 pop dword ptr ds:
0040182E|.FF77 34 push dword ptr ds:<---------ImageBase作为整个文件的优先加载地址,包括所有头在内。
00401831|.8F05 64254000 pop dword ptr ds:
寻找引入表的文字算法
1.从 DOS header 找到 PE header
2.从 数据目录中 读取 data directory 的地址。 第二个索引就为引入表的地址。
3.IMAGE_DATA_DIRECTORY的虚拟地址偏移转化为文件偏移
4.文件偏移加上文件映射基址就为引入表的地址 本帖最后由 whypro 于 2010-5-23 08:52 编辑
引入表的操作
XOREAX, EAX
MOVECX, 4
LEAEDI, .OptionalHeader.DataDirectory.VirtualAddress
assume edi : nothing
DirDelLoop:
STOSD
LOOP DirDelLoop
IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT 用零填充!
IMAGE_DATA_DIRECTORY比较常用
每个目录描述了节内特定信息位置,32 bits RVA VirtualAddress 和尺寸32 bit,各个成员索引如下(括号内为索引值):
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输入地址表目录
我给个例子吧: PE增加区段......
//**********************************************
// Method: AddEmptySection
// Returns: BOOL
// Parameter: PCTSTR ptFile 要添加空节的文件路径
// Parameter: UINT uSize 空节的大小
//**********************************************
BOOL AddEmptySection(PCTSTR ptFile,UINT uSize)
{
HANDLE hFile = NULL;
HANDLE hMapping = NULL;
LPVOID bPointer = NULL;
PBYTE pData = NULL;
// 打开源文件
hFile = CreateFile(
ptFile,
GENERIC_READ|GENERIC_WRITE,
FILE_SHARE_READ|FILE_SHARE_WRITE,
NULL,
OPEN_EXISTING,
FILE_FLAG_SEQUENTIAL_SCAN,
NULL);
if (hFile == INVALID_HANDLE_VALUE)
return FALSE;
//内存映射,创建一个有名的共享内存
if (!(hMapping = CreateFileMapping(hFile, 0, PAGE_READWRITE | SEC_COMMIT, 0, dwSize, NULL)))
{
CloseHandle(hFile);
return FALSE;
}
//映射对象视图,进行读写操作
if (!(bPointer = MapViewOfFile(hMapping, FILE_MAP_ALL_ACCESS, 0, 0, dwSize)))
{
CloseHandle(hMapping);
CloseHandle(hFile);
return FALSE;
}
pData = (PBYTE)bPointer;
//检查 DOS特征
if (((PIMAGE_DOS_HEADER) pData)->e_magic != IMAGE_DOS_SIGNATURE)
{
return FALSE;
}
/ /检查文件是否被感染过
if( *(DWORD*)(((PIMAGE_DOS_HEADER) pData)->e_res2) == 19861001)
{ //已感染,跳过
UnmapViewOfFile(bPointer);
CloseHandle(hMapping);
CloseHandle(hFile);
return FALSE;
}
else
{
//设置感染标志
*(DWORD*)(((PIMAGE_DOS_HEADER) pData)->e_res2) = 19861001;
}
//检查 PE 特征
PIMAGE_NT_HEADERS pNTHdr = (PIMAGE_NT_HEADERS) (pData + ((PIMAGE_DOS_HEADER) bPointer)->e_lfanew);
if (pNTHdr->Signature != IMAGE_NT_SIGNATURE)
return FALSE;
// 检查节头(节描述)空间
if ((pNTHdr->FileHeader.NumberOfSections + 1) * sizeof(IMAGE_SECTION_HEADER) > pNTHdr->OptionalHeader.SizeOfHeaders)
return FALSE;
// Calculate code and file delta
DWORD uCodeDelta = ZALIGN(uSize, pNTHdr->OptionalHeader.SectionAlignment);
DWORD dwFileDelta = ZALIGN(uSize, pNTHdr->OptionalHeader.FileAlignment);
// 获得新节头 和前一个节头
PIMAGE_SECTION_HEADER pNewSec = (PIMAGE_SECTION_HEADER) (pNTHdr + 1) + pNTHdr->FileHeader.NumberOfSections;
PIMAGE_SECTION_HEADER pLastSec = pNewSec - 1;
//这里是填充新节头
memcpy(pNewSec->Name, ".Qing", 6);
pNewSec->VirtualAddress = pLastSec->VirtualAddress + ZALIGN(pLastSec->Misc.VirtualSize, pNTHdr->OptionalHeader.SectionAlignment);
pNewSec->PointerToRawData = pLastSec->PointerToRawData + pLastSec->SizeOfRawData;
pNewSec->Misc.VirtualSize = uSize;
pNewSec->SizeOfRawData = 0;//uCodeDelta;
pNewSec->Characteristics = IMAGE_SCN_MEM_READ | IMAGE_SCN_MEM_WRITE ;//节属性
// 修改下IMAGE_NT_HEADERS,增加新节
pNTHdr->FileHeader.NumberOfSections++;
pNTHdr->OptionalHeader.SizeOfCode += uCodeDelta;
pNTHdr->OptionalHeader.SizeOfImage += dwFileDelta;
// pNTHdr->OptionalHeader.AddressOfEntryPoint;//no change here
pNTHdr->OptionalHeader.DataDirectory.Size = 0;
pNTHdr->OptionalHeader.DataDirectory.VirtualAddress = 0;
UnmapViewOfFile(bPointer); //解除映射
CloseHandle(hMapping);
CloseHandle(hFile);
return TRUE;
} 占位学习, 这么难的东西啊。 长见识了,谢谢楼主!
页:
[1]