【关键字】VC++,修改EXE文件的图标
在很多年前很著名的熊猫烧香病毒,就有这样一个行为,是搜索硬盘上的可执行文件并感染它们,其典型外观症状就是程序的图标变成了熊猫烧香。本文讲解的是修改EXE文件(可执行文件)的图标,可以看做是我写PE文件的Directoried相关文章的一个后续应用,本文性质属于技术可行性研究。考察下windows系统上的文件可以发现下面的特征:
(1)应用程序的可执行文件可以有自己定义的图标,通常位于其资源中,资源管理器使用资源中的图标显示它们,如果没有资源,就显示成系统默认的可执行文件图标。
(2)DLL中虽然可以含有图标资源,但系统一律都是用统一的DLL图标去显示他们。
(3)快捷方式的图标可以随意指定来源,通常是来自其指向的目标文件。用户可以随意修改快捷方式的图标。
(4)就是系统中注册的文档类型,比如doc,txt等等后缀名的文件,其图标是注册在注册表中的,通常也就是来自于打开他们的默认程序中。
再考察下上面的四种显示的图标,其中(2)取决于系统,无法修改也没有必要修改。(1)的图标修改比较困难,因为必须修改PE文件本身,我们通常需要借助专门的资源编辑工具(例如vs.net)打开才能修改。本文讲述的是修改(1)也就是EXE文件的图标。
现在介绍一下我的代码的原理,就是图标是多个图像组成的,每个图像的数据被存放在PE文件中。我在PE文件中定位到这些数据,然后用用户选择的其他图标数据,把这里的数据覆盖。这样PE文件里的图标就被彻底换掉了。我在这里并未校验内存是否写越界,因为如果不出意外,我替换的是标准图标,(通常索引图像的调色板都是满的,比如4bpp的图像调色板是16个颜色,8bpp的图像调色板是256个颜色),所以基本不会发生越界导致破坏PE文件的问题。由于只改动图标,所以PE文件修改后依然能正常运行。为了防止意外,在修改之前我会对PE文件进行备份。还原时就用备份的文件重新覆盖回来即可。
下图显示的是用我写的工具查看 PE 文件的一些Directory,用它打开windows的记事本程序,可以看到记事本文件中的图标在PE文件的资源表中的位置:
注意,图标节点(Icon)是位于第一级子结点深度,其 Entry.Id 为 3。下面含有多个子结点,每一个子结点都是一个图像,每个子结点到达叶子以后,就是图像数据,它们由三部分组成,分别是 BitmapInfoHeader , 调色板(如果是索引图像), XOR Mask 数据, AND Mask 数据(Mask 数据的扫描行宽度都必须以 4 Bytes 对齐)。如果你熟悉 ICO 的文件格式,这里就不用我介绍了,可以参考 ICO 文件格式的资料。
当我定位到这里以后,剩下的任务就是把这里的数据替换掉。为此,我把PE文件的内容先整体加载到内存中,然后定位到图像数据,并用已经选择的一个图标尝试替换其数据,这里我使用的方法是,用户选择的图标中的图像,比如和PE文件中的图像一致才替换,所谓一致,是指三个数据的完全相同(宽度,高度,位深度),实际上如果用户选择的图标图像(复制的源)如果小于被覆盖的数据的大小,是没有问题的,即相当于PE文件中在图像数据后面会有一些字节成了无用的垃圾数据),比如我们用 32 * 32, 8 BPP 的图像数据 覆盖掉 48 * 48, 8 BPP 的数据是不会出问题的,但是这里我们也应该同时修改 GroupIcon 中的图标高度和宽度信息,以和实际图像数据相符。但反过来,如果用大数据覆盖小数据是绝对不行的,因为我们绝对不能在超出我们已知内容的范围以外去写东西,那将可能产生对PE文件的破坏,甚至使他无法运行。
每个Icon节点下面还有一层“语言”目录,然后才到达真正的叶子节点(即图像的BitmapInfoHeader入口处)。所以这里我依然使用一个递归函数去找到最终的叶子节点,然后用 memcpy 覆写这里的数据。最后,我们把整个PE文件重写保存到硬盘上即可。
这里提供关键代码,即以下两个函数,在第一个函数中,我使用了 ImageRvaToVa 去在PE文件中定位到相应的数据结构(例如Directory)的地址。为了显示的更清楚,UI 和 验证相关的部分代码已被略去。
//换图标
BOOL ChangeIcon(HWND hDlg)
{
TCHAR szPath_App[MAX_PATH];
TCHAR szPath_Ico[MAX_PATH];
//...此处已省略部分不重要代码 -- hoodlum1980
//先把程序全部读入内存
HANDLE hFile = CreateFile(szPath_App,
GENERIC_READ,
FILE_SHARE_READ,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL);
DWORD dwBytesRead = 0;
DWORD dwAppFileSize = GetFileSize(hFile, NULL);
PBYTE lpBaseAddress = (PBYTE)malloc(dwAppFileSize);
if(!ReadFile(hFile, lpBaseAddress, dwAppFileSize, &dwBytesRead, NULL) || dwBytesRead != dwAppFileSize)
{
CloseHandle(hFile);
free(lpBaseAddress);
return FALSE;
}
CloseHandle(hFile); //关闭文件
//
// 加载图标
//
if(!g_IcoFile.LoadFile(szPath_Ico))
{
free(lpBaseAddress);
return FALSE;
}
//
// 在内存中替换 ICON (覆写内存数据)
//
int totalImgCount = 0, replacedImgCount = 0;
PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)lpBaseAddress;
PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)(lpBaseAddress + pDosHeader->e_lfanew);
BOOL bFindIcon = FALSE;
//资源表的rva
DWORD rva_resTable = pNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_RESOURCE].VirtualAddress;
if(rva_resTable > 0)
{
//this->LoadResTable(lpBaseAddress, pNtHeaders, rva_resTable);
PIMAGE_RESOURCE_DIRECTORY pResTable = (PIMAGE_RESOURCE_DIRECTORY)ImageRvaToVa(
pNtHeaders,
lpBaseAddress,
rva_resTable,
NULL);
PIMAGE_RESOURCE_DIRECTORY_ENTRY pEntries = (PIMAGE_RESOURCE_DIRECTORY_ENTRY)((DWORD)pResTable + sizeof(IMAGE_RESOURCE_DIRECTORY));
//寻找 Icon 子节点(在第一层上)
for(int i = 0; i < (pResTable->NumberOfNamedEntries + pResTable->NumberOfIdEntries); i++)
{
//this->AddChildNode(hItem_Res, lpBaseAddress, pNtHeaders, (DWORD)pResTable, pEntries + i, 1);
if(!pEntries[i].NameIsString && pEntries[i].Id == 3) //找到了Icon?
{
bFindIcon = TRUE;
//
// 现在遍历每个Icon 并尝试替换叶子节点上的图标数据
//
DWORD tableAddress = (DWORD)pResTable;
PIMAGE_RESOURCE_DIRECTORY pDir = (PIMAGE_RESOURCE_DIRECTORY)(tableAddress + pEntries[i].OffsetToDirectory);
PIMAGE_RESOURCE_DIRECTORY_ENTRY pEntries_Icon = (PIMAGE_RESOURCE_DIRECTORY_ENTRY)((DWORD)pDir + sizeof(IMAGE_RESOURCE_DIRECTORY));
totalImgCount = pDir->NumberOfNamedEntries + pDir->NumberOfIdEntries;
for(int j = 0; j < totalImgCount; j++)
{
OverwriteIcon(lpBaseAddress, pNtHeaders, tableAddress, pEntries_Icon + j, &replacedImgCount);
}
break;
}
}
}
if(!bFindIcon)
{
free(lpBaseAddress);
MessageBox(hDlg, _T("程序文件中没有图标,所以无须替换。"), _T("warning"), MB_OK | MB_ICONWARNING);
return FALSE;
}
//保存到修改后的内容到 APP.exe.Modified
HANDLE hFile2 = CreateFile(szPath_AppModified,
GENERIC_WRITE,
FILE_SHARE_READ,
NULL,
CREATE_ALWAYS,
FILE_ATTRIBUTE_NORMAL,
NULL);
DWORD dwBytesTotal = dwAppFileSize;
DWORD dwBytesToWrite = 0, dwBytesWritten = 0;
PBYTE pBytePos = lpBaseAddress;
while(dwBytesTotal > 0)
{
dwBytesToWrite = min(1024, dwBytesTotal);
WriteFile(hFile2, pBytePos, dwBytesToWrite, &dwBytesWritten, NULL);
dwBytesTotal -= dwBytesToWrite;
pBytePos += dwBytesToWrite;
}
CloseHandle(hFile2);
//释放内存
free(lpBaseAddress);
lpBaseAddress = NULL;
g_IcoFile.Clear();
if(!CopyFile(szPath_AppModified, szPath_App, FALSE))
{
return FALSE;
}
DeleteFile(szPath_AppModified);
return TRUE;
}
//【递归函数】一个Icon的子结点,但不是叶子节点,递归到叶子节点后进行数据覆盖
void OverwriteIcon(LPVOID lpBaseAddress,
PIMAGE_NT_HEADERS pNtHeaders,
DWORD tableAddress,
PIMAGE_RESOURCE_DIRECTORY_ENTRY pEntry,
int* pReplacedCount)
{
//再确定节点类型(目录还是叶子)
if(pEntry->DataIsDirectory)
{
PIMAGE_RESOURCE_DIRECTORY pDir = (PIMAGE_RESOURCE_DIRECTORY)(tableAddress + pEntry->OffsetToDirectory);
PIMAGE_RESOURCE_DIRECTORY_ENTRY pEntries = (PIMAGE_RESOURCE_DIRECTORY_ENTRY)((DWORD)pDir + sizeof(IMAGE_RESOURCE_DIRECTORY));
for(int i = 0; i <(pDir->NumberOfNamedEntries + pDir->NumberOfIdEntries); i++)
{
OverwriteIcon(lpBaseAddress, pNtHeaders, tableAddress, pEntries + i, pReplacedCount);
}
}
else
{
//已经叶子节点!
PIMAGE_RESOURCE_DATA_ENTRY pDataEntry = (PIMAGE_RESOURCE_DATA_ENTRY)(tableAddress + pEntry->OffsetToData);
//具体的资源属于位于:pData->OffsetToData,这是一个RVA(不是相对于资源表头部的偏移!!!)
//去定位到实际的资源数据
PBYTE lpIconData = (PBYTE)ImageRvaToVa(pNtHeaders, lpBaseAddress, pDataEntry->OffsetToData, NULL);
PBITMAPINFOHEADER pBmInfoHdr = (PBITMAPINFOHEADER)lpIconData;
int imgIndex = g_IcoFile.GetImageIndex(pBmInfoHdr->biWidth, pBmInfoHdr->biHeight, pBmInfoHdr->biBitCount);
if(imgIndex >= 0)
{
//覆盖BitmapInfoHeader部分
int bytesCopy = sizeof(BITMAPINFOHEADER) + g_IcoFile.m_pImages[imgIndex].nPaletteSize * sizeof(RGBQUAD);
memcpy(lpIconData, g_IcoFile.m_pImages[imgIndex].lpInfo, bytesCopy);
lpIconData += bytesCopy;
//覆盖XOR Mask部分
bytesCopy = g_IcoFile.m_pImages[imgIndex].nSize_XOR;
memcpy(lpIconData, g_IcoFile.m_pImages[imgIndex].lpXOR, bytesCopy);
lpIconData += bytesCopy;
//覆盖And Mask部分
bytesCopy = g_IcoFile.m_pImages[imgIndex].nSize_AND;
memcpy(lpIconData, g_IcoFile.m_pImages[imgIndex].lpAND, bytesCopy);
lpIconData += bytesCopy;
//增加替换图像数
*pReplacedCount = *pReplacedCount + 1;
}
}
}
下面是该Demo程序的截图:点击替换图标按钮后,尝试替换,替换后程序依然可以正常执行,并未对程序本身有其他任何破坏作用。点击还原按钮,用于还原成原来的EXE文件。当然,由于重要系统文件有系统服务进行文件保护,所以通常不能替换。可以选择一些不重要的EXE文件进行测试,并且本程序仅修改的是 EXE 在资源管理器里的视图图标。而程序运行起来以后的图标(例如标题栏,系统托盘等图标可能是运行时设置,其来源也可能不位于被修改的PE文件,例如来自其他DLL,则这些运行时图标是未能替换的,除非你找到了它们的所在并真正换掉了它们)。
在本范例中,我同时使用了我在 Photoshop ICO 文件格式插件中编写的一个类(CIcoFile),它早期也曾用于图像文件数据查看器,在本例中用于加载 ICO 文件。
下面是本范例程序的完整源码下载连接,为了方便测试,我同时提供了一个含有多个图像(从 128 * 128 到 16 * 16 )的绿色小青蛙的 ICO 文件,位于Debug输出目录。以下源码由VS2005,使用VC++,Win32 SDK开发(无MFC):
http://files.cnblogs.com/hoodlum1980/ChgAppIcon.rar
--------------------------------------------
【后续补充】: 一个方便查找图标来源的小工具 -- ListIcons
在开发完成 Photoshop ICO 格式插件后,我发现有一个不方便的地方,就是如果一个程序下面包含的 DLL 和 EXE 比较多,那么就很难确定到底图标位于那个文件中,如果一个一个打开来试验,显然效率很低。在文本范例中,我提到了如果要修改某些图标,必须找到它们位于何处。如果我们知道这个图标来自哪里(哪个DLL/EXE文件),哪么我们根据资源表的结构就可以很方便的定位到其数据,然后有针对性的修改它。(我在这个范例中是暴力的一股脑的把一个PE文件中的所有图标都替换掉了)。因此我又开发了下面这个工具。
这个工具可以打开一个文件夹,然后尝试在这个文件夹下面的所有PE文件中查找图标,并加载到一个ListView(根据来自的文件启用了分组)中显示出来。由于需要ListView的分组功能,因此操作系统要求在WinXP以上。如下图所示是我加载 XP 系统的 system32 文件夹下的所有PE文件中的图标:
通过这个工具可以更快的找到图标的位置。在这里我没有实现把图标导出的功能,如果你需要导出图标,可以使用 ICONPRO 或者我开发的 PS ICO 插件(当然使用该插件需要先安装Photoshop)。
为了效率考虑,我在实现这个工具时,没有递归遍历子文件夹。也就是说,程序只会尝试加载用户选定的文件夹根目录下的文件。如果下面的文件过多,例如 system32 下面有 1700 多个 PE 文件,这时我会启用多线程,为用户显示一个进度条对话框。
该工具的源码和编译版本下载连接如下:
http://files.cnblogs.com/hoodlum1980/ListIcons.rar
--hoodlum1980, ON 2011-8-11。
【补充:已知问题 (已解决) 】
ListIcons 工具中使用 CreateIconFromResource 函数创建一个图标后,会产生 GDI 对象泄露,具体原因不详。根据 MSDN 的描述,暂时没有找到产生 GDI 对象泄露的原因。该问题导致反复添加含有大量图标的文件夹时,系统会显示异常。
而且MSDN上的例子 ICONPRO 也有该 GDI 对象泄露问题(只是这个问题不显著而已,只要不停切换导入图标对话框上的选中图标,即可看到GDI对象个数缓慢上升到 10000 )。看来这个问题是由 CreateIconFromResource 这个 API 函数引入的。
【补充2:对GDI对象泄露的问题的解决】
经过我的研究发现,实际上GDI对象泄露的现象是由于 CreateIconFromResource 这个函数默认的把图标当作 Shared 方式导致的,一旦图标被其他进程用“共享”方式获取(备注:"共享" 在此处的含义是指多个进程创建、获取、申请和使用的对象在底层上是 同一个对象 ),哪么这个进程不管是否调用 DestroyIcon 之类的函数都是不起作用的。
我估计,系统可能把共享方式的图标当作“长期使用”的,也就是可能多个进程都需要频繁的使用他们,因此共享方式的GDI对象不会被释放。这对于系统级别的GDI对象是有意义的,例如 GetStockObject 等获取到的 GDI 对象(例如白色话刷,黑色画笔等)实际上是长期在系统中生存的。这是由于这些对象的使用非常频繁,因此长期持有他们可以节省系统运行时效率。
因此,共享方式的潜在要求是:程序通过共享方式申请或创建得到的GDI对象的数量不宜太多(备注:不要太多,根据 GDI 对象达到 10000 以后即造成系统绘制异常考虑,这里我认为数量在1000以下是可以的,当然这还和具体环境有关),如果太多,本质上等同于 GDI 对象泄露。因为共享对象是当进程退出时才被释放。所以小批量的共享对象不会产生问题,但是大批量(成百上千)的共享 GDI 对象持有在某个进程中,并随着运行持续增加,(当GDI对象达到和接近10000时)则一定会导致如同 GDI 对象泄露所产生的系统无法正常绘制的现象(例如弹出的菜单背景呈现黑色,标题栏等界面元素无法正常重绘等,这可能是由于系统无法成功创建新的 GDI 对象资源导致的)。
而 CreateIconFromResource 这个函数无法指定不用 Shared 方式创建图标,换句话说,这个API创建的图标一定是 Shared 方式的,即使你已经调用 FreeLibary 释放了相应的 Module, 这些对象依然在你的进程中存在。因此当大批量的从大量 DLL 中加载图标时,就会产生GDI对象泄露的现象。
解决方法:
对这个问题的解决办法时,改用其他 API 函数创建图标,这里我使用的是 LoadImage 函数。这个函数的最后一个参数是一个 Flag,可以指定 LR_SHARED 标志,即相当于共享方式,适用于需要频繁加载的对象(不释放)。如果在代码中指定了 LR_SHARED ,则同样会产生 CreateIconFromResource 遇到的问题。
因此解决方法是,把 CreateIconFromResouce 函数换成 LoadImage,并一定不要使用 LR_SHARED 标志。
但在MSDN中指出,某些图标如果不用 Shared 方式加载,则不会加载成功。因此我在代码中先用非共享方式加载图像,如果失败,再尝试用一次共享方式。但测试发现,还没有发现这种比较特殊的只能用共享方式加载的图标。
其他事项:
有的资源中的图标不包含任何图像,因此不管用什么 API 和方法都无法导出,例如:
c:\windows\system32\ieframe.dll 中 ID 为 101,104,108,109,114,115,117
c:\windows\system32\shdocvw.dll 中 ID 为 101,104,107,108,109,114,115,117
【补充3】关于程序标题栏图标的说明
一般程序在资源管理器中显示的静态图标,是位于其PE文件中的Icon。而程序运行起来以后,在标题栏以及任务栏按钮左侧也会有一个图标,这个即是运行时的图标,通常可以通过 SetIcon 设置这个图标,系统用这个图标绘制到标题栏上。因此,只要找到这些图标的位置,我们就可以修改标题栏和任务栏按钮上的图标。
但对有一种情况是例外的,即不是系统绘制的非客户区(NC)。例如,完全 DirectUI 的窗口,其整个窗口都是用户自己绘制的,这种窗口容易观察出来,因为这些窗口在外观上会体现的比较个性化,和系统中的统一外观有所不同。这样我们可能无法替换其“标题栏图标”,因为其可能是“伪标题栏”。用户自己绘制,当然不一定要使用一个 Icon。如果不是Icon,哪么我想很可能是使用了其他 Bitmap 图像资源,或者是从自带的一些图片文件在运行时加载的。因此这种情况下,即无法使用替换 Icon 的方式改变图标。(本质原因是因为非系统绘制)
系统标题栏的一个特征是:标题栏图标是一个点击区域,在标题栏图标上 左键 单击(或者在标题栏的非图标区域 右键 单击)会弹出系统菜单。
-- hoodlum1980 2011-8-22。