现在的位置: 首页 > 综合 > 正文

关于PE图标的一点点猜想

2013年06月30日 ⁄ 综合 ⁄ 共 4515字 ⁄ 字号 评论关闭
问题提出的原因我就不在这里提了,题外话,总是因为需要吧,呵呵。

 

先简单对我要阐述的问题做一个“题前阐述”。PE文件的头部一直是人们关心的主题,不过因为头部各结构的相对固定,早在n年前就被人们研究烂了,(不过话又说回来,被别人研究烂了的东西,我们还在伤脑筋,很遗憾哪!)大虾们发表的文章太多了,而且都非常系统,非常清晰。网上随便一搜都能找到很多很多。我也是从网上的那些著名和没著名的先人们的文章中学到很多东西,再加上自己的一点点思考,所以才有了今天的这点“猜想”。

废话少说,开讲!

深度剖析pe之前,不得不提点“方法论”,只为了其他人再走我这条路的时候,不要再披荆斩棘,可以走一点捷径。其他文章讲到pe的时候,大多都是使用32位汇编,不是说汇编不好,只是我用惯了c,所以也就讲一点用c来实现对pe文件操作的方法。(奇怪了,网上讲pe的不少,但是用c实现的不多,唉,天下文章一大抄,无怪乎此。)我们要对pe操作,首先要把pe读到内存中,然后对其动刀子,首先想到的就是CreateFile打开pe文件,ReadFile读(文件的一部分)到内存,改变之后再WriteFile回去,麻烦!最简单的就是使用内存映射文件,把pe直接映射到内存中,更改后接触映射即可,非常简单。代码:

void *pBase = NULL;

HANDLE hSrcRsrc = NULL;

HANDLE hsrcMapFile = NULL;

do{

hSrcRsrc = CreateFile(pFile,// file to get ico

GENERIC_READ,

FILE_SHARE_READ,

NULL,

OPEN_EXISTING,

FILE_ATTRIBUTE_NORMAL,

NULL);

if (hSrcRsrc == INVALID_HANDLE_VALUE) { break; }

 

//create map file

hsrcMapFile = CreateFileMapping(hSrcRsrc, NULL, PAGE_READONLY, 0, 0, NULL);

if (hsrcMapFile == NULL) { break; }

pBase = MapViewOfFile(hsrcMapFile, FILE_MAP_READ, 0, 0, 0);

if (pBase == NULL) { break; }

;//now, pBase is pointing the 'MZ

}while(0);

if (pBase!=NULL) { UnmapViewOfFile(pBase); }

if (hsrcMapFile != NULL) { CloseHandle(hsrcMapFile); }

if (hSrcRsrc != INVALID_HANDLE_VALUE) { CloseHandle(hSrcRsrc); }

这样处理之后,从pBase开始往下n长的内存空间中就全部是pe文件的内容了。

找到了“阑尾”的位置,就可以动刀割阑尾了,不会割到盲肠或者其他部位去了。

大多数人头疼pe,其实是头疼pe下去之后那么多的IMAGE_*结构,一层层下去,到资源的最底层(因为资源文件的结构最深),林林总总大概有七八层深度吧,技术上不存在什么,最最关键是要有耐心。结构比较多,我们得到pe首地址pBase之后,一层一层下剥,最笨最实用的方法就是多定义几个结构指针变量,然后使用强制类型转换就可以了。【这里注意,pBase开始是 void *类型,每次往下加偏移的时候,都应该转换成最小sizeof类型,比如char *型,这样才不会加错。】。这里不给代码了,后边给出转换RVA到偏移的函数时候有例子。

提到了RVA,引用别人的话来解释一下吧。“RVA是相对虚拟地址(Relative Virtual Address)的缩写,顾名思义,它是一个“相对”地址,也可以说是“偏移量”,pe文件的各种数据结构中涉及到地址的字段大部分都是以RVA表示的”【注意,是大部分,不是全部。用的时候一定要查看仔细了。】【文引自罗云彬的*****,不用我说什么书了吧J】

RVA是等exe载入到内存中之后才有效的值,在这之前的任何引用都是错误的,这个载入是有windows的loader载入,而不是像我们刚才用CreateFile那么载入,所以这里的RVA值还是不能用,必须把RVA转换为以pe头为基址的相对偏移量,这样加上pBase之后就是我们要找的东东所在的内存地址。话是这么简单,但是微软没有提供什么有效的手段来获取这个偏移(offset),不过网上已经有很多大牛小牛们提供了算法,根据算法,我也弄了个c版本的RVAtoOffset。(网上用的基本上没有c代码,都是汇编代码,郁闷了。)代码:

//rva to offset

DWORD _RVA2Offset(void *pHead, DWORD dwRVA)

{

PIMAGE_DOS_HEADER pTmp;

PIMAGE_NT_HEADERS32 pnth;

pTmp = (PIMAGE_DOS_HEADER)pHead;

pnth = (PIMAGE_NT_HEADERS32)((const char *)pHead + pTmp->e_lfanew);

PIMAGE_SECTION_HEADER pp = (PIMAGE_SECTION_HEADER)((const char *)pnth + sizeof(IMAGE_NT_HEADERS32));

int iCount = pnth->FileHeader.NumberOfSections;

DWORD dwTmp = 0;

while (iCount>0)

{

if (dwRVA >= pp->VirtualAddress)

{

dwTmp = pp->VirtualAddress;

dwTmp += pp->SizeOfRawData;

if (dwRVA < dwTmp)

{

dwTmp = pp->VirtualAddress;

dwRVA -= dwTmp;

dwTmp = pp->PointerToRawData;

return dwTmp+dwRVA;

}

}

pp = (PIMAGE_SECTION_HEADER)((const char*)pp + sizeof(IMAGE_SECTION_HEADER));

iCount--;

}

return -1;

}

算法我就不重复了,重复也是抄别人的,感兴趣的可以查查,罗云彬的书上有,同时他还提供了一个汇编版本的代码。

说到这里,其实还没有说到我们的主题上呢。L

有了上面的准备知识,再找一本资料书,你就可以拿刀解牛了,哪是牛头,哪是牛脖子,哪是牛肝牛肺。我比较无聊,只要牛尾巴。

Pe中的资源占的空间不多,但是资源的组织方式倒是真是又简单又复杂,说简单是因为通共只涉及三个结构,就把多达15种的资源高效的组织起来了,说它复杂吧,你想想如果没有一种高效的算法,怎么可能实现呢。

具体的组织图我就不在这里画出来了,很多地方有,其中我最欣赏罗云彬画的那个,不过,他说是三层结构,没错,是三层。但依我各人的意思,理解做四层合适,第四层才指向具体的数据(偏移+大小,而具体的数据,可以认为在第五层了。)。

我今天主要要讲的是pe文件的图标,某日我突然对windows资源管理器里显示的图标发生了兴趣。资源管理器explorer根据什么来判断pe文件到底要用哪个图标来做显示呢,也就是我们看到的exe文件的“样子”。根据pe文件的结构是没有提供这些信息的,可能MSDN上有说到吧,我的e文臭臭的,就不去自讨苦吃了(最只要是怕最后什么都查不到或者本来有自己翻不出来更痛苦)。经过一系列比查MSDN还要痛苦的折磨思考求证假想之后,我终于有了一点点猜想。【以下内容,纯属假想,仅供参考!】

从pe的资源的第三层,我们得到了图标组,根据程序的不同可能得到不同的组,比如我们常用的WORD,就有多达18个组,而我们用的QQ只有两个组,每个组都有不同规格,或者同规格但是不同风格的各种图标。这里补充一点为什么差距那么大,这主要是看程序的类型来决定的,比如基于对话框的程序图标相对少一些,一般只有一组;含有工具栏的可能要一些,而对于MDI的类型来说,要处理的图标数量可能就更多。但这个不是必然的,图标多少,看你的程序,也看这个程序的组织方式,还有一点,如果你使用微软提供给你的标准图标组(打开保存复制粘贴等等工具栏图标),可能又要少一些。

编译器总是把第一组图标用来作为程序的“脸”,也就是我们在资源管理器里看到的图标。

是第一组,但不是第一组的第一个,或者说不总是。

图标的规格:现在常用到的图标一般来说有16*16,32*32,48*48,64*64(256色)的,如果你使用自己画的图标,而且不是以上规格,(我不知道系统怎么处理的,我自己画了几个非以上规格的ico,总是编译不通过)。如果你成功了,别忘了告诉我。

以上四类rc编译之后得到二进制代码,其大小都是固定的:

16*16 size = 872字节

32*32 size = 3240 字节

48*48 size = 7336 字节

64*64 size = 12840 字节

以上的大小是二进制代码的大小,不是ico的大小。

这里要补充说一点,如果是规格的ico图标,其实大小也差不多固定了。我不知道它们是怎么做到的,使用vc导入一个ico图标之后,我发现有些图片包含有低规格的图标信息,当然不是全部。举例来说详细一些。如果一个16*16的图标大小为 sizeA,32*32的图标的大小为sizeB,那么分别用这两个图标编译出来的二进制代码的大小分别为872字节和3240字节,但是,有那么一些图标,外表看起来是32*32的,可是其中却包含16*16的小图标,“图标嵌套”!我不知道那些人是怎么实现的,但这样的情况是存在的。比如我手头有一些24358字节的ico图标,我惊奇的发现里面居然同时含有以上四种规格的图标。用这种图标编译进工程之后,我发现得到的pe文件,第一个图标组里居然同时含有这四种图标的二进制信息。

我不知道编译器是如何做到的,不过这确实很神奇。

最后将这个信息扩展一下,仁者见仁,智者见智,说了这么多“废话”,对于没用它的人来说,其实都是没有意义的,这一切,其实编译器都帮我们悄悄的做了。除非做特殊事情(eg.filebindJ),否则,根本无需关注。

思路扩展:并非每个pe都在第一组图标组里提供完整的从16*16到64*64的四种图标信息,如果我们要处理这些信息,该怎么办呢。最完美嘛莫过于查看第一组有几个图标,然后挨个处理,可是太麻烦了。通过观察,我发现这第一组的图标里总的来说都是按从小到大的顺序来存放的,我们先找到第一个图标,对比size==12840,如果等于,说明是64*64,处理完后跳出即可,第一组可能只有1个,如果不等,则比较size与7336的大小,相等说明是48*48的,处理完后找第一组第二个图标,看是否等于12840,等则…,不等则说明第一组处理完。以此类推,我就不往下说了,你自己往下想吧。

说来说去,这只是猜想,没有资料证明它对,(好像也没有资料证明它不对。呵呵)但却有太多的实例与这个猜想相反(比如word.exe,完全和我的想法相反!!气死我了!)。我对它的思考,仅止与此。

抱歉!评论已关闭.