作者:冒险王
这是我的这一系列技术文章的第一篇.我们先来谈谈熊猫烧香病毒.大家对这个病毒一定不陌生.作为一款PE文件感染型木马,它算是比较创新的.但是如果你的电脑上的文件图标都变成了"熊猫",那么你还能放心的上QQ或是玩游戏吗?傻子也知道自己中毒了,所以作为一款木马,它不免有其失败之处的.不仅因为中毒特征太明显,而且中毒后对系统速度的影响也让人失望.一般中毒后机子很卡甚至系统崩溃.这就很难达到作者当初编写目的了!要知道,熊猫病毒的定位不是杀伤系统,而是控制监视系统.其它的我暂不说,但这几点足以说明它的失败.
那么我先来说说它的败笔之作-熊猫图标,借此向大家揭开病毒的"易容术".我猜熊猫烧香病毒的作者李俊在写此病毒时一定想过要让自己的病毒具有高超的"易容术",但最后他失败了.此病毒最后没有具备"易容"功能。
(一) 何为病毒易容术
在金庸的武侠小说<<天龙八部>>中的阿朱就精通"易容术",她可以变成任何人的模样.因此几乎可以瞒过任何人的眼睛.而通常高明的病毒也会使用这一招!比如威金蠕虫,当Windows操作系统下的正常的可执行文件(简称PE文件)被此类病毒感染后,我们会发现其原本图标并未发生改变.(如果图标出现锯齿,那只能说明技术问题).但实际上该文件已经被病毒感染,你所看到的只是易容后的程序.程序本身照样可以正常运行,但你一运行后病毒也就悄悄运行了。中了这样的病毒,一般是很难发现的。
那么大家最在乎的肯定是病毒易容的过程和原理,冒险王说了那么多废话无非是想引出这一技术话题.别小看这小小图标,里面的技术含量可不小!这涉及到了PE文件结构.(在这里我不再假设你对PE文件结构一无所知,如果确实如此,那么你还不适合阅读此文。)
假设你是病毒,假设刚刚你已成功入侵到某一PE文件体内,那么接下来你会做什么?你当然不希望自己被别人发现,但一不留神你的身份暴露了!因为被你感染的宿主文件的图标变了(当然也有些病毒在感染文件时不会破坏其图标,技术原理不同罢了。在这我只讨论改变图标的那种)。那么你一定会想尽办法乔装打扮成宿主文件原来的模样来掩饰自己。可是问题的关键在于如何乔装打扮?正如你所猜想的:先将原宿主文件的图标提取出来保存,在感染后又将提取的图标写回去!
(二)易容过程
是的,我很赞同并认为这是非常明智的选择。熊猫病毒的作者曾经尝试过这种做法,但最终还是失败了。他不得不画个熊猫来掩饰这一点。这一技术真有那么难吗?其实不然,经过我的研究,我得出了一个很好的解决方案。既然要提取,那么总该找到其图标的位置。没错,位置很关键!其实PE文件的图标资源就藏在PE文件的资源段“.rsrc”段内。我们要找到图标资源就必须先找到.rsrc资源段的位置。.rsrc资源段的结构如下:
//WINNT.H #define IMAGE_SIZEOF_SHORT_NAME 8 typedef struct _IMAGE_SECTION_HEADER { UCHAR Name[IMAGE_SIZEOF_SHORT_NAME]; union { ULONG PhysicalAddress; ULONG VirtualSize; } Misc; ULONG VirtualAddress; ULONG SizeOfRawData; ULONG PointerToRawData; ULONG PointerToRelocations; ULONG PointerToLinenumbers; USHORT NumberOfRelocations; USHORT NumberOfLinenumbers; ULONG Characteristics; } IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;此结构在WINNT.H中定义,所以要使用它你必须先包含Windows.h(此头文件包含了winnt.h)。
/* *此代码功能:获取PE文件中图标所在位置 *冒险王 */ #include <windows.h> #include <iostream.h> DWORD dwSize1[256],dwSize2[256]; DWORD dwPos1[256],dwPos2[256]; int count1,count2; bool GetPos(TCHAR m_path[MAX_PATH],int a) { DWORD dwIconSize=0; DWORD dwWritePos=0; _IMAGE_DOS_HEADER dosHead; _IMAGE_NT_HEADERS ntHead; _IMAGE_SECTION_HEADER secHead; DWORD dwBytesRead;//读取PE Header HANDLE fp = CreateFile(m_path, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); if ((int)fp==-1) { cout<<"文件不存在"<<endl; CloseHandle(fp); return false; } ReadFile(fp,&dosHead,sizeof(_IMAGE_DOS_HEADER), &dwBytesRead, NULL); SetFilePointer(fp,dosHead.e_lfanew,NULL,FILE_BEGIN); ReadFile(fp,&ntHead,sizeof(_IMAGE_NT_HEADERS),&dwBytesRead,NULL); /*查找.rsrc节,移动文件指针到.rsrc节开始的位置*/ for (int i=0;i<ntHead.FileHeader.NumberOfSections;i++) { ReadFile(fp,&secHead,sizeof(_IMAGE_SECTION_HEADER),&dwBytesRead,NULL); if (strcmp((char*)secHead.Name,".rsrc")==0) { break; } } _IMAGE_RESOURCE_DIRECTORY dirResource; //读取指针指向资源根节点开始的位置 SetFilePointer(fp,secHead.PointerToRawData,NULL,FILE_BEGIN); //读取资源根节点开始的位置(在文件中的位置 ) DWORD pos=secHead.PointerToRawData; ReadFile(fp,&dirResource,sizeof(_IMAGE_RESOURCE_DIRECTORY),&dwBytesRead,NULL); _IMAGE_RESOURCE_DIRECTORY_ENTRY entryResource; //第二层资源入口 _IMAGE_RESOURCE_DIRECTORY dirTemp; _IMAGE_RESOURCE_DIRECTORY_ENTRY entryTemp; //第三层资源入口 _IMAGE_RESOURCE_DIRECTORY dirTempICON; _IMAGE_RESOURCE_DIRECTORY_ENTRY entryTempICON; _IMAGE_RESOURCE_DATA_ENTRY entryData; for (i=0;i<dirResource.NumberOfIdEntries+dirResource.NumberOfNamedEntries;i++) { ReadFile(fp,&entryResource,sizeof(_IMAGE_RESOURCE_DIRECTORY_ENTRY),&dwBytesRead,NULL); if (entryResource.Name==3)//3说明该资源是图标 { //读取指针指向下一层的IMAGE_RESOURCE_DIRECTORY结构 SetFilePointer(fp,pos+entryResource.OffsetToDirectory,NULL,FILE_BEGIN); ReadFile(fp,&dirTemp,sizeof(_IMAGE_RESOURCE_DIRECTORY),&dwBytesRead,NULL); if (dirTemp.NumberOfIdEntries>256) { cout<<">256失败"<<endl; return false; } //遍历各个入口点指示的目录 for (int k=0;k<dirTemp.NumberOfIdEntries;k++) { ReadFile(fp,&entryTemp,sizeof(_IMAGE_RESOURCE_DIRECTORY_ENTRY),&dwBytesRead,NULL); //如果还有子目录 if (entryTemp.DataIsDirectory>0) { //读取指针指向下一层的IMAGE_RESOURCE_DIRECTORY结构 SetFilePointer(fp,pos+entryTemp.OffsetToDirectory,NULL,FILE_BEGIN); ReadFile(fp,&dirTempICON,sizeof(_IMAGE_RESOURCE_DIRECTORY),&dwBytesRead,NULL); ReadFile(fp,&entryTempICON,sizeof(_IMAGE_RESOURCE_DIRECTORY_ENTRY),&dwBytesRead,NULL); SetFilePointer(fp,pos+entryTempICON.OffsetToData,NULL,FILE_BEGIN); ReadFile(fp,&entryData,sizeof(_IMAGE_RESOURCE_DATA_ENTRY),&dwBytesRead,NULL); //列出该目录下所有图标资源 for (i=0;i<dirTemp.NumberOfIdEntries;i++) { if (entryData.Size >=44) { //图标大小 dwIconSize=entryData.Size; //图标起始位置 dwWritePos=pos+entryData.OffsetToData - secHead.VirtualAddress; //将图标资源信息存入全局数组 if (a==1) { dwSize1[i]=dwIconSize; dwPos1[i]=dwWritePos; } if (a==2) { dwSize2[i]=dwIconSize; dwPos2[i]=dwWritePos; } } SetFilePointer(fp,pos+entryTempICON.OffsetToData+ (i+1)*sizeof(_IMAGE_RESOURCE_DATA_ENTRY),NULL,FILE_BEGIN); ReadFile(fp,&entryData,sizeof(_IMAGE_RESOURCE_DATA_ENTRY),&dwBytesRead,NULL); } } } } } CloseHandle(fp); if (a==1) { count1=dirTemp.NumberOfIdEntries; if (count1>256) return false; //实现按从大到小排序 int i,j; DWORD tempS,tempP; for (i=0;i<count1-1;i++) for (j=count1-1;j>i;j--) if (dwSize1[j]>dwSize1[j-1]) { tempS=dwSize1[j]; tempP=dwPos1[j]; dwSize1[j]=dwSize1[j-1]; dwPos1[j]=dwPos1[j-1]; dwSize1[j-1]=tempS; dwPos1[j-1]=tempP; } } if (a==2) { count2=dirTemp.NumberOfIdEntries; if (count2>256) return false; //实现按从大到小排序 int i,j; DWORD tempS,tempP; for (i=0;i<count2-1;i++) for (j=count2-1;j>i;j--) if (dwSize2[j]>dwSize2[j-1]) { tempS=dwSize2[j]; tempP=dwPos2[j]; dwSize2[j]=dwSize2[j-1]; dwPos2[j]=dwPos2[j-1]; dwSize2[j-1]=tempS; dwPos2[j-1]=tempP; } } return true; }
这是前半段,我不指望你一下子就能把它看懂,当然我不怀疑你的天赋和能力。即使你有这个能力,我还是得把它拆开来分析。所以就算你暂时看不懂也不要灰心!首先,请你忽略代码的自然顺序。其次,请不要太在意变量的名称和类型。否则可能导致你分心。
那么,我们从打开一个宿主文件开始。正如程序第14行所描述,我们打开了某个PE文件,这只是个开始,接下来我们读取了其PE文件头部信息,然后根据头部结构的e_lfanew指针我们很容易就ReadFile得到其核心躯干结构_IMAGE_NT_HEADERS,这是一个大杂烩。其结构在winnt.h中描述如下:
typedef struct _IMAGE_NT_HEADERS { DWORD Signature; IMAGE_FILE_HEADER FileHeader; IMAGE_OPTIONAL_HEADER32 OptionalHeader; } IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
成员FileHeader属于IMAGE_FILE_HEADER 结构,专业术语称之为PE文件头结构。OptionalHeader属于IMAGE_OPTIONAL_HEADER32结构,专业术语称之为PE可选头。而Signature为PE头文件头标志。再开展下去我头都大了哈哈。言归正传,我们要找的是.rsrc资源段,注意前面有个".",名字其实不重要的。.rsrc资源段不包含在_IMAGE_NT_HEADERS结构内,所以我在开头专门定义了_IMAGE_SECTION_HEADER类型变量来储存这一结构信息。
在一个PE文件内会有很多个段,.rsrc资源段只是其中之一。其他段与本文无关的我就不作介绍了。现在我们在乎的是要得到我们所打开文件内段的数量,然后以这个数量为限进行循环,以.rsrc名字为条件进行搜索。从而定位到.rsrc段的位置。而PE文件内段的个数恰恰保存在_IMAGE_NT_HEADERS结构这个大杂烩里的子结构IMAGE_FILE_HEADER (PE文件头结构)中的NumberOfSections变量里。这就是我为什么要在开头读取_IMAGE_NT_HEADERS结构的原因。醉翁之意不在酒!看到此,你的思路是否已经渐渐清晰了。
把目光定位在第一个for语句,我们历遍了所有的段来寻找名字为.rsrc的资源段。程序到了这里,我们的第一个问题便解开了!我前面说过,PE文件的图标资源被包含于.rsrc资源段中,所以我们找到了.rsrc资源段,自然就离图标资源不远了。如果把资源段比作一幢学生公寓,那么图标资源就是这幢公寓里的某个寝室了。就拿寝室来说,一幢公寓楼里的所有寝室的内部结构和布局都是一致的。在资源段内也是一样的,我定义了一个通用的资源结构组,但其内部结构比较复杂。它至少有三层入口,并且每个入口都是以树型结构排列的。层下有层,层层相扣。你从代码中可以看出,最终的图标资源信息就是在第三层指向的一个资源结构体中。遗憾的是我们无法直接定位到资源结构的第三层,而是必须从.rsrc资源段的头部找到第一层_IMAGE_RESOURCE_DIRECTORY 资源结构,再通过第一层来过滤直到找到图标资源的名字,然后进入第二层去寻找图标资源中图标的个数。要知道通常一个程序文件不止一个图标,虽然最终显示的只有一个,但为了程序在不同平台下的兼容性,通常程序员会备选多个不同大小和分辨率的图标。最后我们还要找到第三层资源结构,只有在第三层我们才能找到通向真正图标资源的入口。但细心的你一定会发现每一个资源层都分为上下两小层。
总之,资源一般使用树来保存,通常包含3层,最高层是类型,其次是名字,最后是语言。在资源节开始的位置,首先是一个IMAGE_RESOURCE_DIRECTORY结构,后面紧跟着IMAGE_RESOURCE_DIRECTORY_ENTRY数组,这个数组的每个元素代表的资源类型不同;通过每个元素,可以找到第二层另一个IMAGE_RESOURCE_ DIRECTORY,后面紧跟着IMAGE_RESOURCE_DIRECTORY_ENTRY数组。这一层的数组的每个元素代表的资源名字不同;然后可以找到第三层的每个IMAGE_ RESOURCE_DIRECTORY,后面紧跟着IMAGE_RESOURCE_DIRECTORY_ENTRY数组。这一层的数组的每个元素代表的资源语言不同;最后通过每个IMAGE_RESOURCE_ DIRECTORY_ENTRY可以找到每个IMAGE_RESOURCE_DATA_ENTRY。通过每个IMAGE_RESOURCE_DATA_ENTRY,就可以找到每个真正的资源。
关于一些细节我解释一下,在第一层中我得到了第二层的数量,在第二层中我找到了entryResource.Name为3的图标资源入口。关于为什么dirTemp.NumberOfIdEntries>256时就退出呢?其实在一般情况下一个PE文件内的图标再多也不会超过256个,如果超过了这个数那就说明程序错误了。然后我进入第三层。在经过层层过滤后我们终于找到了真正的图标资源。但真正的图标资源内可能会有很多图标,所以我用数组将其保存,为什么要定义两个数组还要排序呢?我将在下半部分来说明!在下半部分,我将阐述“易容术”的核心:图标改写技术。今天先到这啦!