前些日子做了一些PE文件分析,查阅了包括《Windows PE 权威指南》在内的不少文章及源码。可能出于实用的需要,大家分析的目标通常集中在区段、输入输出表方面的缘故,很少有文章对资源部分做很详细的介绍,即便是《Windows PE 权威指南》,在菜单资源部分说的也很简练,只分析了第一个简单菜单项,其他的一带而过。为了正确读取菜单资源的内容,不得已查了些资料,做了些分析推测,记录一下。当然,这些分析没有什么实际价值,内容非常初级,并且不带来任何效益,菜鸟以上级别请自行闪退。
菜单资源的存放实质上是一种顺序储存的多叉树式数据结构,用标志位来记录树节点间的关系。
图1 深度优先存储的多叉树结构
菜单项有两种:弹出菜单(POPUP)和普通菜单项(MENUITEM)。
弹出菜单项
typedef struct
{
WORD fItemFlag; //菜单项标志
WCHAR[] szItemText;//菜单名
}PopupMenuItem;
普通菜单项
typedef struct
{
WORD fItemFlag; //菜单项标志
WORD wMenuID; //菜单ID
WCHAR[] szItemText;//菜单名
}NormalMenuItem;
fItemFlag是描述菜单项的标志集合,常见标志及对应的值如下(数据来源:WinUser.h):
#define MF_ENABLED 0x00000000L
#define MF_GRAYED 0x00000001L
#define MF_DISABLED 0x00000002L
#define MF_UNCHECKED 0x00000000L
#define MF_CHECKED 0x00000008L
#define MF_USECHECKBITMAPS 0x00000200L
#define MF_STRING 0x00000000L
#define MF_BITMAP 0x00000004L
#define MF_OWNERDRAW 0x00000100L
#define MF_POPUP 0x00000010L
#define MF_MENUBARBREAK 0x00000020L
#define MF_MENUBREAK 0x00000040L
#define MF_END 0x00000080L
以上这些常量定义是windows惯常使用的方式,以二进制不同的标志位代表不同的含义,通过逻辑运算可以很方便的做出不同的参数设置。搞过Windows编程的,对这种方法早已烂熟于心。简单举例,一个选中的(Checked)、非活动的(Disabled)、分栏断开(MenuBarBreak)的普通菜单项的fItemFlag:
MF_DISABLED | MF_CHECKED | MF_MENUBARBREAK = 0x0002| 0x0008 | 0x0020 = 0x002A
以上定义的中文名参考了ResScope中的翻译。你可以用ResScope或者ExeScope很轻松的用修改菜单项,然后用WinHex 或者UltraEdit验证。
以实例来分析相对会比较简单,因此自己搞了个菜单来分析。
图2 实验用的菜单资源
WinHex打开PE文件或者直接在VS里直接读取都没问题。如何定位到菜单资源是PE文件分析中另一层面的内容,同时也是很简单的问题,在此不做分析。图2中的菜单资源Hex视图显示如图3。
图3 资源文件的Hex视图
从弹出菜单项说起(图3中的红色部分):
如何才能知道,一个菜单项是弹出菜单还是普通菜单项呢?WinUser.h给出了说明。弹出菜单项的fItemFlag为0x0010,如果弹出菜单项是本层级菜单的最后一个菜单项,则:
MF_POPUP | MF_END = 0x0010 | 0x0080 = 0x0090,如图3中的菜单项test5。
同理,菜单项 test3 即使不查看图2,也可知该菜单项为 MF_POPUP | MF_GRAYED,也即一个灰色弹出菜单项。ResScope 查看到的菜单资源样式也是用这种方法分析得来的。
菜单项的名称为Unicode字符串,长度是不固定的,但无论C语言程序还是Delphi程序,菜单资源中的字符串定义方式都是C方式,字符串的结束符为标准的0x0000。
普通菜单项:
图3中的蓝色部分为普通菜单项的fItemFlag,0x0080、0x0081之类的一目了然,0x0000的没有设置标志位,为“一般”普通菜单项。绿色部分为每个普通菜单项的ID,和Resourse.h 中的菜单ID一致。
需要说明的是菜单项test13,资源设置的时候设定了PopUp属性,因此它的资源ID为不可编辑状态,但因为没有给它下一层级的菜单项,程序编译的时候认为它不是一个弹出菜单项,但又没有资源ID,编译器把它的资源ID定义为 -1(0xFFFF)。
特殊的菜单项 -- 分隔符也是一个普通菜单项,十六进制代码如图3中紫色部分,只是它的fItemFlag、wMenuID均为0,字符串为Unicode的‘\0’,因此分隔符菜单项共占6个字节。
由以上分析可知,每级菜单的结束条件均为 fItemFlag 低八位的最高位为1,可以很简练的用递归方法遍历。这里没有统计菜单的层级,也没有写修改菜单的函数,因为多叉树写起来比较麻烦。
void PEResourse::GetMenu(PBYTE *begin,int level)
{
WORD fItemFlag = **(WORD **)begin;
if (level < 0)
return;
if (fItemFlag & 0x10)//弹出菜单项
{
GetPopMenuItem(begin);
++ level;
}
else
GetNormalMenuItem(begin);//普通菜单项
if (fItemFlag & 0x80)
-- level;
GetMenu(begin,level);
}
两个读取菜单项的函数:
void PEResourse::GetNormalMenuItem( PBYTE *begin)//**为移动地址的指针
{
CString out_str,name_str;
*begin += sizeof(WORD);
if (!(**(DWORD **)begin))//分隔符,跳过
*begin += sizeof(WORD);
else
{
out_str.Format(_T("MENU : %04X\t"),**(WORD **)begin);
*begin += sizeof(WORD);
name_str = (PWCHAR)(*begin);
out_str += name_str;
m_menu_str.push_back(out_str); //类成员变量vector <CString> m_menu_str
}
*begin += (name_str.GetLength() + 1 )* sizeof(WCHAR);
}
void PEResourse::GetPopMenuItem( PBYTE *begin)
{
CString out_str,name_str;
out_str = _T("POPUP:\t\t");
*begin += sizeof(WORD);
name_str = (PWCHAR)(*begin);
out_str += name_str;
m_menu_str.push_back(out_str);
*begin += (name_str.GetLength() + 1 )* sizeof(WCHAR);
}
结语:其实PE文件的资源组织方式都差不多,而且和Windows资源管理器的数据组织方式大同小异,窥一斑可见全豹吧。文中错误之处欢迎指正。
参考资料:
1. 《Windows PE 权威指南》
2. Msdn
3. 《C++基于顺序储存的多叉树实现》
4. internet
[CTF入门培训]顶尖高校博士及硕士团队亲授《30小时教你玩转CTF》,视频+靶场+题目!助力进入CTF世界
上传的附件: