欢迎来到 嗅灵易学

零基础也能上手的脚本技术课,一对一答疑带你入门

[原创]PE文件编辑器

[原创]PE文件编辑器

概述
预备知识
    MS-DOS头
    PE文件头
        IMAGE_FILE_HEADER
        IMAGE_OPTIONAL_HEADER32
        IMAGE_DATA_DIRECTORY
            IMAGE_DIRECTORY_ENTRY_EXPORT
            IMAGE_IMPORT_DESCRIPTOR
            IMAGE_BASE_RELOCATION
        节表信息
总体设计
    不足
    改进
详细设计
    文件保存
    关联信息的修改
    文件位置计算器(FLC)
    节信息(Section)
    目录信息
        导出表
            导出表信息的展示
            导出表信息的修改
        导入表
    资源
        资源信息的获取
        资源信息的展示
    重定位信息
控件设计
    Edit控件
        信息的展示
        限制用户输入
        信息的获取
    单选框控件
    复选框与下拉框控件
        复选框的设置
        下拉框的设置
    列表控件(ListCtrl)
        列表右键弹出菜单
总结
概述
最近学习了PE文件的知识,为了加深对知识的理解,任老师给我们布置了一个任务,自己完成一个PE编辑器。在写之前还觉得挺简单,到真正开始写的时候才发现有好多东西都不是想象中那么简单,有许多需要注意的地方。也多亏了这次写PE编辑器积累的经验,在写壳时给了我很大的帮助。
先说明一下文章中用到的一些词汇,不一定用的对。
文件缓存:读取磁盘上的一个PE文件,然后将这个PE文件保存到内存中的一段空间,使用new一段堆空间,或者使用文件映射的方式都行。
区段,节:都是指Section
偏移:一个特定位置基于某个指定位置的差值
FOA(File Offset Address):数据在PE文件中的地址叫文件偏移地址。这是文件在磁盘上存放时相对于文件开头的偏移
RVA(Relative Virtual Address):相对虚拟地址是内存地址相对于映射基址的偏移量。
映射基址:PE文件被系统PE加载器加载到内存后的首地址。
预备知识
关于PE文件的知识,网上有很多资料,这里就不做深入的解释了,只说一些我写程序时遇到的问题,有一部分问题是写壳时遇到的,这里也一起总结一下。
●
[*]MS-DOS头
将PE文件读入内存中后,将文件缓存的首地址强制转换为IMAGE_DOS_HEADER 类型的指针就可以获取MS-DOS头的内容,在这个结构体中只需要关注两个字段e_magic 字段和e_lfanew字段
●
[*]e_magic字段用来检测是否有效的PE文件

●
[*]e_lfanew字段表示windows下PE文件头的偏移位置


●
[*]PE文件头
IMAGE_NT_HEADERS 用这个结构体来表示,通过MS-DOS头的e_lfanew字段的值加上文件缓存的起始地址可以获取该结构体类型的指针。
这个头在32位和64位系统上是不一样的,上面的结构体是一个宏,根据不同的编译选项生成不同的结构体

typedef struct _IMAGE_NT_HEADERS {
    DWORD Signature;
    IMAGE_FILE_HEADER FileHeader;
    IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;

●
[*]Signature
字段表示PE标示,用来检测是否是有效的PE文件。
[*]IMAGE_FILE_HEADER
typedef struct _IMAGE_FILE_HEADER {
    WORD    Machine;
    WORD    NumberOfSections;
    DWORD   TimeDateStamp;
    DWORD   PointerToSymbolTable;
    DWORD   NumberOfSymbols;
    WORD    SizeOfOptionalHeader;
    WORD    Characteristics;
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;

●
[*]NumberOfSections:表示当前PE文件拥有的节的数量,在对PE文件添加节时,这里的值要进行相应的增加。
[*]SizeOfOptionalHeader:扩展头大小


●
[*]IMAGE_OPTIONAL_HEADER
该结构由固定大小(0xE0)加上数据目录IMAGE_DATA_DIRECTORY类型的数组组成。
typedef struct _IMAGE_OPTIONAL_HEADER {
    WORD    Magic;
    BYTE    MajorLinkerVersion;
    BYTE    MinorLinkerVersion;
    DWORD   SizeOfCode;
    DWORD   SizeOfInitializedData;
    DWORD   SizeOfUninitializedData;
    DWORD   AddressOfEntryPoint;
    DWORD   BaseOfCode;
    DWORD   BaseOfData;
    DWORD   ImageBase;
    DWORD   SectionAlignment;
    DWORD   FileAlignment;
    WORD    MajorOperatingSystemVersion;
    WORD    MinorOperatingSystemVersion;
    WORD    MajorImageVersion;
    WORD    MinorImageVersion;
    WORD    MajorSubsystemVersion;
    WORD    MinorSubsystemVersion;
    DWORD   Win32VersionValue;
    DWORD   SizeOfImage;
    DWORD   SizeOfHeaders;
    DWORD   CheckSum;
    WORD    Subsystem;
    WORD    DllCharacteristics;
    DWORD   SizeOfStackReserve;
    DWORD   SizeOfStackCommit;
    DWORD   SizeOfHeapReserve;
    DWORD   SizeOfHeapCommit;
    DWORD   LoaderFlags;
    DWORD   NumberOfRvaAndSizes;
    IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;

●
[*]SizeOfCode:所有代码节的总和,这个大小是基于文件对齐后的大小,而不是内存对齐。
[*]AddressOfEntryPoint:程序的入口点OEP,写壳时就是通过修改程序的入口点,从而运行我们的Stub部分实现对宿主程序的解密。
[*]ImageBase:默认载入基址,在生成程序时如果将编译选项改为可变基址,则这个值是没有用的。获取一个模块的句柄,这个句柄和模块的实际载入基址是一样的值。
[*]SectionAlignment:内存对齐
[*]FileAlignment:文件对齐
[*]SizeOfImage:映像文件的大小,这个数值是基于内存对齐
[*]SizeOfHeaders:PE文件头+节表的总大小,是基于文件对齐的,这个数据比较严格,数值只能是文件对齐大小的整数倍,在打造比较小的PE文件时,可以将文件对齐调整的小一些。
[*]NumberOfRvaAndSize:数据目录的个数,取值为2~16,一般正常的PE文件都是16个,如果多于2个,那么多出的目录存在于固定位置,前面不存在的数据目录会被补0.
[*]IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]数据目录数组。可以使用下标来访问不同的数据目录。数组中的每个元素都有两个DWORD,一个代表指向数据结构的大小,一个代表数据结构在PE文件中的偏移。


●
[*]IMAGE_DATA_DIRECTORY
扩展头大小为0xE0,这个大小会随着PE文件中目录的个数的不同而改变,但是前面0x60个字节大小是固定的。数据目录数组的起始位置位于扩展头的0x60处,可以根据系统提供的宏(代表下标)去访问不同的数据目录。
●
[*]IMAGE_EXPORT_DIRECTORY
PE文件的导出表,一般存在于Dll文件中。数据目录的RVA指向下面这个结构体
typedef struct _IMAGE_EXPORT_DIRECTORY {
    DWORD   Characteristics;
    DWORD   TimeDateStamp;
    WORD    MajorVersion;
    WORD    MinorVersion;
    DWORD   Name;
    DWORD   Base;
    DWORD   NumberOfFunctions;
    DWORD   NumberOfNames;
    DWORD   AddressOfFunctions;     
    DWORD   AddressOfNames;         
    DWORD   AddressOfNameOrdinals;  
} IMAGE_EXPORT_DIRECTORY;

●
[*]NumberOfFunctions 导出函数的总数
[*]NumberOfNames   有名称函数的总数,该值小于等于上面的字段的值
[*]特别要注意AddressOfNameOrdinals 指向的是一个WORD类型的数组,数组的每一个元素都是一个下标,根据这个下标可以去AddressOfFunctions 指向的数组中进行取值,获得导出函数的RVA。
[*]AddressOfNames 指向一个有名称导出函数的名称RVA数组,数组中的每一个元素都是一个RVA,根据这个RVA可以得到导出函数的名称字符串,字符串是ASCII类型,以0结尾。
[*]AddressOfNames与AddressOfNameOrdinals 这两个RVA指向的数组中元素是一一对应的关系。
[*]导出表也可以获取导出的全局变量,在加壳时,就使用Stub部分的导出全局变量对壳运行进行了控制。


●
[*]IMAGE_IMPORT_DESCRIPTOR
一个PE中可能使用了多个Dll模块,每个Dll模块使用一个IMAGE_IMPORT_DESCRIPTOR结构来描述。
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
    union {
        DWORD   Characteristics; 
        DWORD   OriginalFirstThunk;
    } DUMMYUNIONNAME;
    DWORD   TimeDateStamp; 
    DWORD   ForwarderChain; 
DWORD   Name;
    DWORD   FirstThunk; 
} IMAGE_IMPORT_DESCRIPTOR;

●
[*]Name 字段表示导入模块的名称,ASCII码
[*]在文件中OriginalFirstThunk和FirstThunk指向相同的结构,但是当PE文件被加载器加载到内存后,FirstThunk指向的结构(IAT)会被修复成函数的真正地址,这个结构在数据目录中也有存在。


●
[*]IMAGE_BASE_RELOCATION
基址重定位表,描述PE文件中需要修复的地址,PE文件不一定会加载到默认基址,因此一些基于默认基址产生的地址需要进行修复。重定位表有多个,每个重定位表负责描述0x1000空间内的重定位信息。
typedef struct _IMAGE_BASE_RELOCATION {
    DWORD   VirtualAddress;
    DWORD   SizeOfBlock;
//  WORD    TypeOffset[1];
} IMAGE_BASE_RELOCATION;

●
[*]VirtualAddress:指向描述的0x1000空间的起始RVA
[*]SizeOfBlock:IMAGE_BASE_RELOCATION加上TypeOffset数组的总大小,根据这个大小减去IMAGE_BASE_RELOCATION的大小就可以算出TypeOffset数组中元素的个数
[*]TypeOffset:一个WORD类型的数组。数组的每个元素指向描述空间中需要修复的数据,这是一个偏移量,相对于VirtualAddress。这里需要注意一下。在对需要修复的数据进行修复时,会根据实际加载基址减去默认加载基址,得出一个增量,然后使用这个增量与修复数据相加得到修复后的数据。
[*]在设计加壳程序时,会对Stub部分的重定位信息进行修复,由于Stub部分存在于宿主程序,节的RVA发生了变化,因此还需要使用Stub在宿主程序中的RVA减去原RVA获取一个增量,使用该增量与修复数据进行相加才能得到正确的地址数据。



●
[*]节表信息
描述PE文件中节的信息
typedef struct _IMAGE_SECTION_HEADER {
    BYTE    Name[IMAGE_SIZEOF_SHORT_NAME];
    union {
            DWORD   PhysicalAddress;
            DWORD   VirtualSize;
    } Misc;
    DWORD   VirtualAddress;
    DWORD   SizeOfRawData;
    DWORD   PointerToRawData;
    DWORD   PointerToRelocations;
    DWORD   PointerToLinenumbers;
    WORD    NumberOfRelocations;
    WORD    NumberOfLinenumbers;
    DWORD   Characteristics;
} IMAGE_SECTION_HEADER;

●
[*]VirtualSize:描述节的实际尺寸。
[*]VirtualAddress:描述节的起始RVA
[*]SizeOfRawData:描述在文件对齐后的大小
[*]PointerToRawData;:节的FOA
[*]Characteristics:节的属性,在PE文件被执行时,这个属性规定了节在内存中可读可写可执行等属性,在进行加壳时,会对宿主文件被加密部分进行属性更改,改为可写,这样才能在程序运行时对宿主程序进行解密。



总体设计
●
[*]流程图

[*]不足
在开始时并没有将界面与逻辑分开,PE文件只负责加载文件,并获取文件在内存中的指针,将指针传给界面,界面拿到文件缓存指针后,根据指针获取要显示的信息,然后在将信息显示到界面。
[*]改进
在后面的界面设计中,将数据的获取放在PE文件操作类中,通过一个结构体传递数据,这样界面只负责显示数据即可。
对于文件的修改保存也是这样,获取界面的信息,保存到一个结构体中,然后调用PE文件操作类的接口对数据进行保存,这时的界面只是负责获取数据。在使用PE文件的结构体时,注意使用指针,不要使用值拷贝,这样在修改时只需要保存文件即可,不需要在修改内存中的信息。

详细设计
●
[*]文件保存
文件保存时,如果保存的是一段连续数据,比如导入表信息(一个结构体),目录信息,可以调用同一个方法:
/**
*@brief 将指针指向的内容保存到文件中
*/
DWORD SavePointerContent(LPVOID pContent,DWORD dwSize);
传入数据指针,然后传入保存数据的大小,方法内部会根据传入指针和保存的文件指针进行计算,获取保存数据在文件中的偏移,然后写入文件。
对于非连续数据则需要单独写一个方法,如果调用SavePointer… 方法,则存一段信息就要调用一次方法,会对文件进行多次打开关闭操作。
当初考虑过打开的文件句柄不关闭,每次写入只做一次刷新操作。但是这样不太好,文件一直是打开的。

●
[*]关联信息的修改
在对PE文件进行修改时,往往修改一个地方一些其他的关联信息也需要修改,如果只修改了一个地方,关联信息没有改变,则会出错。比如给PE文件添加一个节,此时PE文件中需要改变的信息:

可选头的SizeOfImage字段也需要进行相应的修改。

●
[*]文件位置计算器(FLC)
RVA 转换 FOA :遍历节表向量,用节的起始RVA加上节的实际大小Misc.VirtualSize得到节的范围,然后判断给出的RVA是否落在这个范围中。如果在这个范围中则使用RVA – 节的起始RVA 获得偏移,然后再加上该节在文件中的起始FOA得到给定RVA 对应的FOA
有两个地方需要注意一下
●
[*]注意1:给定的RVA位于文件头部时遍历节表是得不到的。需要加一个判断
[*]注意2:不要使用对齐后的大小,对齐后大小比实际大小要大,因此如果一个错误的RVA也可能正确的转换。

界面

[*]节信息(Section)
PE文件操作类中存储了节表指针向量,遍历节表指针向量获取节表的指针,获取信息,添加到列表中。
注意:名称在文件中是以ASCII码存储的,因此需要对Name字段进行转换。取出的Name字段不一定以0 结尾,因此取出字符数组后在末尾添加一个0.
在写壳时,向宿主文件添加了一个节,节表也增加了一个,新添加的节表存有对添加节进行描述的信息。

界面

节属性的修改

●
[*]目录信息
正常情况下目录信息有16个,但是最小PE文件目录个数就不是16个,因此在处理目录信息时如果写成固定个数就会出错,要结合可选头大小信息来计算目录个数。这是需要改进的地方。
在设计目录信息界面时,因为Edit控件个数太多,添加数据或者获取数据时如果使用控件变量会很麻烦。目录信息都是8个字节,是有规律的,因此可以使用控件资源ID来对应目录信息。使用SDK获取控件句柄,进行信息的添加和获取
TCHAR szGet[9] = {0};
for (DWORD i=0,j=0;i<16;i++,j+=2,pData++)
{
    // 将::GetDlgItem放到GetWindow中会获取不正确
    hEdit = ::GetDlgItem(this->m_hWnd,EXPORT_RVA_EDIT+j);
    ::GetWindowText(hEdit,szGet,9);
    swscanf_s(szGet,L"%x",&dwGet);
    pData->VirtualAddress = dwGet;
    hEdit = ::GetDlgItem(this->m_hWnd,EXPORT_RVA_EDIT+j+1);
    ::GetWindowText(hEdit,szGet,9);
    swscanf_s(szGet,L"%x",&dwGet);
    pData->Size           = dwGet;
}
这段代码有两个需要注意的地方

    [*]TCHAR szGet[9] = {0};这里将数组的大小设置为9.因为字符串会以0结尾。比如字符串“00010548”如果使用szGet[8]来接收,在内存中是这样一个结构 30 00 30 00 30 00 31 00 30 00 35 00 34 00 00 00 。 宽字符0 在内存中使用两个字节来表示30 00 。最后的38 00 被字符串结尾 00 00 覆盖。
    [*]hEdit = ::GetDlgItem(this->m_hWnd,EXPORT_RVA_EDIT+j);::GetWindowText(hEdit,szGet,9);如果写成::GetWindowText(::GetDlgItem(this->m_hWnd,EXPORT_RVA_EDIT+j),szGet,9);就会获取不正确

界面

●
[*]导出表
导出表中有三个比较重要的数组:
DWORD  AddressOfFunctions;        // RVA from base of image
DWORD  AddressOfNames;          // RVA from base of image
DWORD  AddressOfNameOrdinals;    // RVA from base of image
第一个数组中每个元素是一个RVA,代表一个导出函数的RVA,数量是最多的。
第二个和第三个数组是一一对应的,代表导出函数中有名称的函数,第三个数组中的元素值代表导出函数地址数组的索引,通过这个索引值可以得到有名称导出函数的地址。
第二个数组存储的是名称字符串的RVA值。
●
[*]导出表信息的展示
按照LoadPE展示方式

序号从小到大开始展示,那就需要先遍历AddressOfFunctions 指向的数组。在拿到地址序号去第二个第三个数组寻找对应信息时,由于第二第三个数组很可能不按照顺序排列,因此只能根据地址数组索引号去第三个数组中查找。可以使用两个for循环进行查找,但是效率比较低,可以在初始化PE文件时就将第二第三个数组做成键值对存入到一个map集合中,这样在遍历第一个数组时,就可以快速查找该值是否有名称,名称的地址信息。

●
[*]导出表信息的修改

对于有名称的导出函数可以修改第三个数组中存储的索引值,和名称的RVA,名称字符串。对于没有名称的导出函数只能修改地址RVA
●
[*]修改索引信息
将第三个数组中元素的值改变后可能有2个相同的地址数组索引,这两个元素在第二个数组中又对应2个不同的名称,在显示时按照前面的元素对应的名称进行显示,后面元素对应的名称不显示。比如将Index从 2 修改为 3 则AddressOfNameOrdinals 指向的数组中有两个 3 ,假设 原先2所在的位置比 3所在位置靠前(第二第三数组不一定是有序的),那么地址数组中索引为2的导出函数将失去名称,并且索引为3的导出函数名称变为原先2号对应的名称。
修改前:

修改后:

修改索引后需要对map集合做处理,根据两个索引的位置对map中的值做修改,这里就偷懒没做,重新构造了一次map,因为map在insert插入时相同的不会更新, 因此在map中只会存在一个索引,后面的那个重复的不会存入。如果要更新需要使用  map[key] = value 这种方式更新。

●
[*]修改导出函数名称
在用户修改名称时需要对名称的长度做限制,最大值为原先字符串的长度,可以比这个长度小。如果大于这个长度的话,会破坏后面的信息。在修改缓存中的信息和文件中的信息时要注意多拷贝一个字符,0结尾。
// 改变缓存中名称的值
memcpy_s(pName,pOri->strAPI.GetLength()+sizeof(char),
strOut,stcSave.strAPI.GetLength()+sizeof(char));
// 改变文件中的名称信息
dwOffset = (PBYTE)pName - m_pFileBuffer;
SetFilePointer(hFile,dwOffset,0,FILE_BEGIN);
WriteFile(hFile,pName,stcSave.strAPI.GetLength()+sizeof(char),
&dwWrite,NULL);




[*]导入表
导入表是由多个导入表描述符组成IMAGE_IMPORT_DESCRIPTOR,每个导入表描述符对应一个模块(Dll),包含了模块的名称信息,INT,IAT信息,在磁盘文件中INT和IAT是两个结构相同的数组,数组中的每个元素都会指向一个IMAGE_IMPORT_BY_NAME结构,这个结构保存导入函数的序号和名称信息。IAT在PE文件被系统的PE加载器加载到内存后会被模块的导出函数地址(VA)所覆盖掉。
注意:由于编译器使用的序号是在SDK中库文件中保存的,可能与当前运行系统的Dll中序号不一致,因此需要根据PE文件中导入函数的序号与运行系统中模块中该序号对应的函数名称做比较,如果相同则调用,不同则根据名称来调用。

界面

●
[*]资源
资源是由3层目录构成,每一层目录都是下面这张结构:
IMAGE_RESOURCE_DIRECTORY
IMAGE_RESOURCE_DATA_ENTRY
IMAGE_RESOURCE_DATA_ENTRY
…..
每层目录都是这样一个结构,IMAGE_RESOURCE_DIRECTORY中含有对IMAGE_RESOURCE_DATA_ENTRY个数的描述(个数包含标准名称和自定义名称,出现在第一级目录)
IMAGE_RESOURCE_DATA_ENTRY 包含数据的偏移和数据的类型(是否是目录).资源的结构可以使用递归函数来遍历,展示时使用的是一个树控件,用户点击时需要显示这个节点的相关信息,因此需要对节点信息进行保存,使用一个map结构来保存节点信息,map中使用插入节点后获取的节点句柄作为key值,节点的相关信息作为value.
注意:上面提到的偏移不是RVA而是相对于资源目录RVA的偏移,与基址重定位中的TypeOffset有点像
●
[*]资源信息的获取
进行递归的函数:
InitTreeInfo(PIMAGE_RESOURCE_DIRECTORY_ENTRY pDirEntry,HTREEITEM hParent,DWORD dwLevel)
主要使用两个判断:
1、判断if (pDirEntry->NameIsString) 如果为真则是自定义名称,为假则是标志名称或资源ID,根据不同的目录层级会有不同的含义。
2、判断是否是目录if (pDirEntry->DataIsDirectory),根据这个判断来决定是否继续进行递归。获取下一级目录的指针时,注意也是一个偏移。
注意1:
当NameIsString为真时会获得一个指向IMAGE_RESOURCE_DIR_STRING_U结构的偏移(offset),这个偏移也不是一个RVA。
WORD    Length;
WCHAR   NameString[ 1 ];  
[CODE]
注意:NameString不是以0结尾,要根据Length来截取字符串,如果截取会得到乱码。Str.left()
[*][U]资源信息的展示[/U]
每次递归都会将Entry作为value插入到map中,在用户点击时可以根据点击节点获取Entry,然后将信息进行展示。Value中还包含该节点的父节点句柄,这样可以实现信息的关联显示。
[/LIST]
[U]界面[/U]
[IMG]http://bbs.hackav.com/data/attachment/album/201312/03/173137wocbgwot68ogzvzc.png[/IMG]
[*]重定位信息
反汇编代码中一些硬编码信息在加载基址改变时也需要改变。
基址重定位表由多个 IMAGE_BASE_RELOCATION 结构组成,每个结构描述1000h(4kb) 个字节的区域中需要重定位的信息。
每个IMAGE_BASE_RELOCATION 结构后都跟有一个数组
[CODE]typedef struct _TYPE_OFFSET
{
    WORD Offset:12;
    WORD Type  :4 ;
}TYPE_OFFSET,*PTYPE_OFSSET;
数组中的每个元素都是一个上述结构对应的类型,Type字段描述了需要重定位信息的类型。Offset描述了需要重定位信息的偏移,根据该偏移可以得到需要重定位的地址(Type为 0x3 时)。
IMAGE_BASE_RELOCATION 结构的SizeOfBlock字段
描述了结构本身和TYPE_OFFSET数组的总大小,可以通过这个信息计算出数组的个数。
注意:上面说的偏移是相对于IMAGE_BASE_RELOCATION中VirtualAddress字段的值

界面

控件设计

●
[*]Edit控件
●
[*]信息的展示
strTemp.Format(_T("%08X"),dwFOA) 以十六进制大小字母进行展示
[*]限制用户输入
Edit控件展示的信息基本上就是DWORD类型和WORD类型,因此在用户修改信息时就要做一些限制,不能DWORD不能超过8个字节,WORD不能超过4个字节,并且输入的字符只能是0-9 或者是a-f A-F
编写了一个类,继承自CEdit
响应WM_CHAR消息:

具体代码:
BEGIN_MESSAGE_MAP(CHexNumEdit, CEdit)
    ON_WM_CHAR()
END_MESSAGE_MAP()
// nChar = VK_BACK 不屏蔽退格键
BOOL isRight  =
 (nChar >= '0' && nChar <= '9') || (nChar >='a' && nChar<='f') ||(nChar>='A' &&nChar<='F') || nChar==VK_BACK
                || (nChar==1 && nFlags==0x1e) || (nChar==3 && nFlags==0x2e) || (nChar==0x16 && nFlags==0x2f)
if (isRight)
{
   CEdit::OnChar(nChar,nRepCnt,nFlags);
}
上面的判断中后面的那几个对应对应快捷键Ctrl+a,Ctrl+c,Ctrl+v。调试时按下这些键一个一个试出来的,方法比较笨,不知道是不是有具体的宏可以使用。

●
[*]信息的获取
从界面获取字符串后对字符串进行格式化
WORD wIndex = 0;
m_editHint.GetWindowText(strTemp);
swscanf_s(strTemp,L"%hx",&wIndex);
注意:当信息是WORD类型时在格式化时需要是h 代表获取两个字节,如果不使用则会覆盖后面的信息。 hh 表示取一个字节。

[*]单选框控件

使用工具箱中的Group Box控件 将Radio Button 控件包起来。将第一个Radio Button 的Group属性改为True。这样就可以实现单选功能。

●
[*]复选框与下拉框控件

点击每个复选框都会改变Current Value的值,这个值代表节的属性。点击下拉框也会改变这个值。
●
[*]复选框的设置
BOOL CSectionFlagsDlg::OnCommand(WPARAM wParam, LPARAM lParam)
{
    // TODO: 在此添加专用代码和/或调用基类
    DWORD wmEvent = 0;
    wmEvent = HIWORD(wParam);
    // 如果是点击事件(有可能是OK按钮的点击)
    if (wmEvent==BN_CLICKED)
    {
        WORD wmId      = LOWORD(wParam);
        // 获取单选框控件的状态
        BOOL bIsCheck = ::SendMessage(::GetDlgItem(this->m_hWnd,wmId),BM_GETCHECK,0,0);
        // 在这里做一次判断,看资源ID号是否在复选框ID号之间。因为BN_CLICKED会响应所有的点击
        if (wmId>= IDC_CHECK1 && wmId<=IDC_CHECK15)
        {
            // 得到点击的复选框的索引值
            DWORD dwIndex  = wmId-IDC_CHECK1;
            DWORD dwCurVal = 0;
            CString strCurVal;
            m_editCurrentVal.GetWindowText(strCurVal);
            swscanf_s(strCurVal,L"%x",&dwCurVal);
            // 根据是否选中,对标志进行加或减
            if (bIsCheck)
            {
                dwCurVal += g_dwFlagsAry[dwIndex];
            }
            else
            {
                dwCurVal -= g_dwFlagsAry[dwIndex];
            }
            // 改变文件缓存中的值
            m_pSecHeader->Characteristics = dwCurVal;
            strCurVal.Format(_T("%08X"),dwCurVal);
            m_editCurrentVal.SetWindowText(strCurVal);
        }
    }
    return CDialogEx::OnCommand(wParam, lParam);
}
Current Value 这个值在传入时会有默认值需要对响应的复选框进行勾选
CString strTemp;
strTemp.Format(_T("%08X"),m_pSecHeader->Characteristics);
m_editCurrentVal.SetWindowText(strTemp);
for (DWORD j=0;j<15;j++)
{
     if (g_dwFlagsAry[j] & m_pSecHeader->Characteristics)
     {
          // 设置选中状态
          ::SendMessage(::GetDlgItem(this->m_hWnd,IDC_CHECK1+j),BM_SETCHECK,BST_CHECKED,0);
     }
}


●
[*]下拉框的设置
在设置下拉框时,对Sort属性要设置为false,否则控件会按照字符串进行排序,而不是插入顺序
初始化下拉框
    DWORD dwShow = 1;
    CString strShow = _T("默认");
    m_cbAlignment.AddString(strShow);
    m_cbAlignment.SetCurSel(0);
    strShow.Format(_T("%d"),dwShow);
    m_cbAlignment.AddString(strShow);
    for (DWORD j=1;j<14;j++)
    {
        dwShow *= 2;// 下拉框代表的节对齐大小
        strShow.Format(_T("%d"),dwShow);
        m_cbAlignment.AddString(strShow);
    }
响应下拉框change消息:
int           j  = m_cbAlignment.GetCurSel();
DWORD dwVal  = m_pSecHeader->Characteristics;
CString  strTemp;
// 清空原来位置的值
dwVal  = dwVal&(IMAGE_SCN_ALIGN_MASK^-1);
dwVal  += g_dwAlignmentAry[j];
m_pSecHeader->Characteristics = dwVal;
strTemp.Format(_T("%08X"),dwVal);
m_editCurrentVal.SetWindowText(strTemp);


[*]列表控件(ListCtrl)
m_listSecInfo.SetExtendedStyle(LVS_EX_FULLROWSELECT|LVS_EX_GRIDLINES);
使用这个函数可以设置控件的扩展属性,上面的宏代表网格和选一行。这个属性在属性编辑时是找不到的。
●
[*]列表右键弹出菜单
有两种方法可以实现,第一种是子类化ListCtrl控件,第二种则是在父窗口响应右键点击事件。
第二种方法
在类上添加两个成员变量
CMenu m_menu,*m_pSub;
在对话框的初始化函数中
m_menu.LoadMenu(IDR_MENU1); // IDR_MENU1 弹出菜单的资源号
m_pSub = m_menu.GetSubMenu(0);
在对话框中右键点击列表控件,选择添加事件处理程序

在底部还有对消息的说明,NM_CLICK代表控件内点击鼠标左键。在事件处理程序中添加下列代码
int nIndex = m_listSecInfo.GetSelectionMark();
m_pSel = g_objPe.m_vecPSection[nIndex];
CPoint   point;
GetCursorPos(&point);
m_pSub->TrackPopupMenu(TPM_LEFTALIGN|TPM_RIGHTBUTTON,point.x,point.y,this,NULL);
然后对菜单项添加事件处理程序,注意添加要添加对话框到类上,这里默认会是这个,不要急着点确定

然后在响应程序添加处理代码。


总结
通过写PE编辑器我对PE文件理解更进一步,也发现了一些自己在编程方面的不足,一些看似简单的东西,往往有很多东西需要挖掘。印证了古人的一句话"纸上得来终觉浅,绝知此事要躬行"。

上传的附件 LoadPe.rar

注意:上传附件及图片大小不得大于30M。

⚠️ 版权声明:
本博客所有内容(含教程、源码、工具)仅供个人技术学习与研究交流使用,严禁商用、倒卖、二次分发及非法用途。
未经作者书面授权,任何组织或个人不得转载、复制或用于其他平台,违者将追究相关责任。

0 0 0 举报
复制成功