-
-
[原创]PE解析逆向LoadString
-
发表于:
2015-8-27 21:27
12815
-
通过对LoadString的深入理解,也可加强对PE中资源表的理解
熟悉windows编程的朋友相信对资源都不陌生,在使用PE资源的时候,字符串资源也是大家经常使用的一种。下面就开始了解它的来龙去脉.
我们知道,LoadString的函数原型如下:
int LoadString( HINSTANCE hInstance, // handle to resource module
UINT uID, // resource identifier
LPTSTR lpBuffer, // resource buffer
int nBufferMax // size of buffer);
最后两个参数自不必说,第一个参数相信熟悉PE的朋友都能猜到它的意义,作为一个输入参数,它是必须的,我们需要模块基址从而定位到资源表。那么,问题来了,假如一个PE文件有多个字符串资源,那么怎么准确定位呢? 一个uID可以吗?
以我手上一个DEMO程序为例,
定义的资源如下:
两个字符串,ID 分别是101 和1000
那么,在我们逆向之前,不妨先手工在PE文件找到对应的资源,顺便复习下PE中的资源表
首先,通过查询数据目录项中的第二项(从0开始) , 我们获取到资源表的RVA(相对基址偏移), 转成FOA(相对文件偏移)后,可以在文件中定位到它
以我的程序为例,
RVA = 0x1A000
转成FOA后, 在文件偏移7000h处
我们知道,资源表主要由三层目录以及相应的详细数据构成,下图有详细说明:
目录项如下:
typedef struct _IMAGE_RESOURCE_DIRECTORY {
DWORD Characteristics;
DWORD TimeDateStamp;
WORD MajorVersion;
WORD MinorVersion;
WORD NumberOfNamedEntries;
WORD NumberOfIdEntries;
// IMAGE_RESOURCE_DIRECTORY_ENTRY DirectoryEntries[];
} IMAGE_RESOURCE_DIRECTORY, *PIMAGE_RESOURCE_DIRECTORY;
以及紧随其后的
typedef struct _IMAGE_RESOURCE_DIRECTORY_ENTRY {
union {
struct {
DWORD NameOffset:31;
DWORD NameIsString:1;
} DUMMYSTRUCTNAME;
DWORD Name;
WORD Id;
} DUMMYUNIONNAME;
union {
DWORD OffsetToData; //这里的OFFSET是相对资源节的偏移
struct {
DWORD OffsetToDirectory:31;
DWORD DataIsDirectory:1;
} DUMMYSTRUCTNAME2;
} DUMMYUNIONNAME2;
} IMAGE_RESOURCE_DIRECTORY_ENTRY, *PIMAGE_RESOURCE_DIRECTORY_ENTRY;
先看第一层,
ID OffsetToData
6 80000020
18 80000040
字符串类型的 ID编号为6 , 取offset的低位,定位到节偏移0x20处,我们来到第二层
上图第一行末尾的02说明本层有两个字符串,
ID OffsetToData
07 80 00 00 58
3f 80 00 00 70
两个字符串编号别分为07 和 3F , 这个信息很重要,下面逆向要用到
最终通过第三层目录定位到实际的字符串在文件偏移的地址后,可以看到如下:
也就是说,我们手工定位,只能获取所有的 字符串,但是具体哪一个,还需要研究LoadString的第二个参数意义
下面开始进入逆向
我们只关心StringID这个参数,其余不做参考
向下走
又会调用LoadStringBaseExW这个函数 , 继续跟
又将参数压栈后调用RtlLoadString这个函数,继续跟入
在RtlLoadString里, 代码将字符串ID 右移 4位后 加一,写入到一个局部变量中,这个局部变量是我们需要关注的重点
继续向下:
刚才写入的位置是ebp-38 , 而ebp – 3c 是字符串资源的类型ID ,也就是 6
Ebp – 3c 和 ebp -38 在内存上连续,当做结构体传入第二个参数,进入内部call
在这个内部call里,代码首先调用RtlImageDirectoryEntryToData获得资源表的VA
然后进入三次循环,因为资源表的目录就是三层 ,所以我们还是只关注在第二层时,代码如何将上次stringID运算的结果与之匹配
代码到此,此时ESI = 0x 0018FC98 , 也就是指向stringID 右移运算的结果,作为第四个参数传入,我们跟进去这个call
进入之后,又将结果作为参数,传入到下一个call ,还是一样,继续跟
到此,思路就很清晰了,函数枚举每个IMAGE_RESOURCE_DIRECTORY_ENTRY项,然后取每一项的ID值和传入的ID值做比较,相等就是找到了目标
到了这里,离成功已经比较接近了,我们还是回到上层RtlLoadString中
此时edi 保存字符串块的首地址(0041A238),在pe中可以定位到如下:
在0x7238处,似乎离目标”ByeBye”还有一段距离
在RtlLoadString中继续向下找
这个时候看到,stringID 的 低四位发挥了作用 ,将算法简单还原一下如下
//edi 指向字符串块(第三层遍历之后的结果)
do
{
edx = cx
edx = *(word*)(edi + edx * 2)
ecx += edx + 1
esi -= 1
} while (esi >=0);
char *szFinalString = (char*)edi + (ecx - edx) * sizeof(word)
此时szFinalString 指向”ByeBye”, 也就是我们定义的目标字符串
总结:
LoadString 先通过第二个参数uID ,将之右移加一后,和资源表第二层的每一项ID做对比,成功后可以通过第三层直接定位到目标字符串块
再通过uID的低四位获取到偏移, 加上字符串块的首地址,从而精确定位
写的匆忙,一些细节还没体现,不足和谬误之处请大家多多包涵
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)