大家好,现在继续我们的PE文件之旅第二篇, DLL的秘密 -- 输出表。
首先看一下预备知识。输出表在<<加密解密II>>等资料中并没有太多的笔墨,居我的接触,它确实比较直观,没有象输入表那样复杂的结构,所以这也可能是介绍的少的缘故。关于输出表的结构可以参考<<Matt Pietrek's PE Tutorial>>。
首先定位输出表。在IMAGE_OPTIONAL_HEADER.DataDirectory结构数组中,注意这也是个结构数组。它的每一个元素都是IMAGE_DATA_DIRECTORY结构。该结构定义如下:
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress;
DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
DataDirectory结构数组的第一个元素描述的就是输出表,第二个元素描述的是输入表,呵呵,这个是我们下一篇研究对象。所以,这里的VirtualAddress就是输出表的偏移地址,Size就不用说了,输出表的大小。
注意:以下代码的一些变量在前一篇文章<<PE文件之旅 -- Sections>>中已经定义过,这里不再重新定义,请大家参考前面的文章。
核心代码:
[CODE]// 首先定位IMAGE_OPTIONAL_HEADER32结构
IMAGE_OPTIONAL_HEADER32 myOptionalHeader;
fseek(pFile, (e_lfanew + sizeof(DWORD) + sizeof(IMAGE_FILE_HEADER)), SEEK_SET); // 如此定位的原因请参考PE文件结构图
fread(&myOptionalHeader, sizeof(IMAGE_OPTIONAL_HEADER32), 1, pFile);
// 数据目录表的第一个元素为输出表
printf("输出表的偏移地址:%08x\n", myOptionalHeader.DataDirectory[0].VirtualAddress);
printf("输出表的大小:%04x\n", myOptionalHeader.DataDirectory[0].Size);
DWORD dwFileOffset;
if (myOptionalHeader.DataDirectory[0].VirtualAddress != 0)
{
// OnConvertVAToRawA()函数是进行VirtualAddress到文件偏移转换的函数,具体代码见下面
dwFileOffset = OnConvertVAToRawA(myOptionalHeader.DataDirectory[0].VirtualAddress);
if (-1 == dwFileOffset)
{
printf("输出表定位错误\n");
}
else
{
printf("输出表的文件偏移为:%08x\n", dwFileOffset);
// ------------------ 这里已经获得了输出表的文件偏移,输出表是由IED数组开始 ------------------ //
int nNumberOfFunctions = 0;
int nNumberOfNames = 0;
// 由输出表的偏移(VA)计算出文件偏移(RawAddress),然后定位到这里就是输出表了
// 输出表由IMAGE_EXPORT_DIRECTORY结构开始,
IMAGE_EXPORT_DIRECTORY *pIED = (IMAGE_EXPORT_DIRECTORY *)malloc(sizeof(IMAGE_EXPORT_DIRECTORY)); // Memory 17
fseek(pFile, dwFileOffset, SEEK_SET);
fread(pIED, sizeof(IMAGE_EXPORT_DIRECTORY), 1, pFile);
printf("IMAGE_EXPORT_DIRECTORY结构如下: \n");
printf("Characteristics:%08x(没有用途,总是为0)\n", pIED->Characteristics);
printf("TimeDateStamp:%08x(文件被产生时刻)\n", pIED->TimeDateStamp);
printf("MajorVersion:%08x(没有用途,总是为0)\n", pIED->MajorVersion);
printf("MinorVersion:%08x(没有用途,总是为0)\n", pIED->MinorVersion);
printf("Name:%08x(RVA,指向一个DLL文件名称)\n", pIED->Name);
// 读取DLL名称
char chDllName[64] = {0}; // 这里多留点空间给函数名字,防止出错,我所见过的函数名称最长也就30个char,呵呵
// 将指向DLL文件名称的VA转化为文件偏移
DWORD dwTemp = OnConvertVAToRawA(pIED->Name);
// 读取DLL文件名称
fseek(pFile, dwTemp, SEEK_SET);
fread(chDllName, 64, 1, pFile);
printf("DLL文件名称为:%s\n", chDllName);
printf("Base:%08x(起始序号)\n", pIED->Base);
// 被此模块输出的函数的起始序号
WORD wBase = (WORD)pIED->Base;
printf("NumberOfFunctions:%d(输出函数个数)\n", pIED->NumberOfFunctions);
// AddressOfFunctions 数组(稍后描述)中的元素个数。此值同时也是输出函数的个数。
// 通常这个值和 NumberOfNames 字段相同,但也可以不同。
nNumberOfFunctions = pIED->NumberOfFunctions;
printf("NumberOfNames:%d(以名称输出的函数个数)\n", pIED->NumberOfNames);
// AddressOfNames 数组(稍后描述)中的元素个数。此值表示以名称输出的函数个数。
// 通常(但不总是)和输出函数的总数相同。
nNumberOfNames = pIED->NumberOfNames;
// 这是一个 RVA (Relative Virtual Address )值,指向一个由函数地址所构成的数组。
// 我所谓的函数地址是指此一模块中的每一个输出函数的进入点的 RVA 值。
printf("AddressOfFunctions:%08x(RVA, 指向一个由函数地址构成的数组)\n", pIED->AddressOfFunctions);
// 这里将其转化为文件的偏移地址
DWORD dwAddressOfFunctions = OnConvertVAToRawA(pIED->AddressOfFunctions);
// 这是一个 RVA 值,指向一个由字符串指针所构成的数组。
// 字符串的内容是此一模块中的每一个"以名称输出的输出函数"的名称
printf("AddressOfNames:%08x(RVA, 指向一个由字符串指针所构成的数组)\n", pIED->AddressOfNames);
// 转化为文件偏移地址
DWORD dwNameAddressOfFunctions = OnConvertVAToRawA(pIED->AddressOfNames);
// 这是一个 RVA 值,指向一个 WORD 数组。
// WORD 的内容是此一模块中的每一个"以名称输出的输出函数"的序号。别忘了要加上 Base 字段中的起始序号。
printf("AddressOfNameOrdinals:%08x(RVA, 指向一个DWORD数组)\n", pIED->AddressOfNameOrdinals);
// 转化为文件偏移地址
DWORD dwAddressOfNameOrdinals = OnConvertVAToRawA(pIED->AddressOfNameOrdinals);
if (pIED != NULL) // release Memory 17
{
free(pIED);
pIED = NULL;
}
// 动态开辟内存用来存储函数的VirtualAddress
DWORD *pdwAddressOfFunctions = (DWORD *)calloc(nNumberOfFunctions, sizeof(DWORD)); // Memory 18
fseek(pFile, dwAddressOfFunctions, SEEK_SET);
fread(pdwAddressOfFunctions, sizeof(DWORD), nNumberOfFunctions, pFile);
// 动态开辟内存用来存储函数的RawAddress
DWORD *pRowAddressOfFunctions = (DWORD *)calloc(nNumberOfFunctions, sizeof(DWORD)); // Memory 19
for (int i = 0; i < nNumberOfFunctions; i++, pdwAddressOfFunctions++, pRowAddressOfFunctions++)
{
*pRowAddressOfFunctions = OnConvertVAToRAwA(*pdwAddressOfFunctions);
}
// 恢复指针
pdwAddressOfFunctions -= nNumberOfFunctions;
pRowAddressOfFunctions -= nNumberOfFunctions;
// 动态开辟内存用来存储函数名称地址
DWORD *pdwNameAddressOfFunctions = (DWORD *)calloc(nNumberOfNames, sizeof(DWORD)); // Memory 20
fseek(pFile, dwNameAddressOfFunctions, SEEK_SET);
fread(pdwNameAddressOfFunctions, sizeof(DWORD), nNumberOfNames, pFile);
// VA TO RAWA
for (i = 0; i < nNumberOfNames; i++, pdwNameAddressOfFunctions++)
{
*pdwNameAddressOfFunctions = OnConvertVAToRowA(*pdwNameAddressOfFunctions);
}
// 恢复指针
pdwNameAddressOfFunctions -= nNumberOfNames;
// 二重指针,用来存储函数名称指针,因为函数个数不固定
DWORD **pFunctionsName = (DWORD **)calloc(nNumberOfNames, sizeof(char *)); // Memory 21
for (i = 0; i < nNumberOfNames; i++, pFunctionsName++)
{
// 这块内存是用来存放函数名称的
char *pchFunctionName = (char *)malloc(sizeof(char) * 64);
*pFunctionsName = (DWORD *)pchFunctionName;
}
// 恢复指针
pFunctionsName -= nNumberOfNames;
// 读出函数名称
for (i = 0; i < nNumberOfNames; i++, pdwNameAddressOfFunctions++, pFunctionsName++)
{
fseek(pFile, *pdwNameAddressOfFunctions, SEEK_SET);
fread(*pFunctionsName, 64, 1, pFile);
}
// 恢复指针
pdwNameAddressOfFunctions -= nNumberOfNames;
pFunctionsName -= nNumberOfNames;
if (pdwNameAddressOfFunctions != NULL) // release Memory 20
{
free(pdwNameAddressOfFunctions);
pdwNameAddressOfFunctions = NULL;
}
// 读出函数的序号
WORD *pwFunctionsOrdinals = (WORD *)calloc(nNumberOfNames, sizeof(WORD)); // Memory 22
fseek(pFile, dwAddressOfNameOrdinals, SEEK_SET);
for (i = 0; i < nNumberOfNames; i++, pwFunctionsOrdinals++)
{
fread(pwFunctionsOrdinals, sizeof(WORD), 1, pFile);
}
// 恢复指针
pwFunctionsOrdinals -= nNumberOfNames;
// 这里需要注意的是AddressOfFunctions所指的数组。这是一个DWORD数组,每一个DWORD都是
// 一个输出函数的RVA地址。每一个输出函数的序号都与函数在此数组中的位置一致。假使起始序号
// 为1,那么输出序号为1者其函数地址在此数组中排在第一个位置。输出序号为2者其函数地址在此
// 函数地址中则排在第二个位置,依次类推。
// 由以上可知,我们必须对AddressOfNameOrdinals所指向的WORD数组进行升序排序,同时进行的还有
// 函数名称存放位置的排序,因为Ordinals和函数名称存放的位置是对应的。
// 这里采用效率较高的快速排序(详细代码见下面)
QuickSort(pwFunctionsOrdinals, pFunctionsName, nNumberOfNames);
// 打印信息
for (i = 0; i < nNumberOfNames; i++, pwFunctionsOrdinals++, pFunctionsName++, pRowAddressOfFunctions++, pdwAddressOfFunctions++)
{
printf("Ordinals: %04x\n", *pwFunctionsOrdinals + wBase);
printf("函数%d: %s\n", i + 1, *pFunctionsName);
printf("RowAddress: %08x\n", *pRowAddressOfFunctions);
printf("VirtulAddress: %08x\n", *pdwAddressOfFunctions);
}
// 恢复指针
pwFunctionsOrdinals -= nNumberOfNames;
pFunctionsName -= nNumberOfNames;
pRowAddressOfFunctions -= nNumberOfNames;
pdwAddressOfFunctions -= nNumberOfNames;
if (pdwAddressOfFunctions != NULL) // release Memory 18
{
free(pdwAddressOfFunctions);
pdwAddressOfFunctions = NULL;
}
if (pRowAddressOfFunctions != NULL) // release Memory 19
{
free(pRowAddressOfFunctions);
pRowAddressOfFunctions = NULL;
}
for (i = 0; i < nNumberOfNames; i++, pFunctionsName++) // release Memory 21
{
if (*pFunctionsName != NULL)
{
free(*pFunctionsName);
*pFunctionsName = NULL;
}
}
pFunctionsName -= nNumberOfNames;
if (pFunctionsName != NULL) // release prince 21
{
free(pFunctionsName);
pFunctionsName = NULL;
}
if (pwFunctionsOrdinals != NULL) // release prince 22
{
free(pwFunctionsOrdinals);
pwFunctionsOrdinals = NULL;
}
}
}
else
{
printf("此文件无输出表\n");
} // VA到RAWA的转换程序(原理请参考<<加密解密II>>中关于输出表的部分)
DWORD OnConvertVAToRawA(DWORD dwFileOffset)
{
IMAGE_SECTION_HEADER *pmySectionHeader = (IMAGE_SECTION_HEADER *)calloc(nSectionCount, sizeof(IMAGE_SECTION_HEADER)); // prince 14
fseek(pFile, (e_lfanew + 4 + sizeof(IMAGE_FILE_HEADER) + sizeof(IMAGE_OPTIONAL_HEADER32)), SEEK_SET);
fread(pmySectionHeader, sizeof(IMAGE_SECTION_HEADER), nSectionCount, pFile);
DWORD dwFilePos;
DWORD dwOffset;
DWORD *pdwVA = (DWORD *)malloc(sizeof(DWORD) * nSectionCount); // prince 15
DWORD *pdwRowA = (DWORD *)malloc(sizeof(DWORD) * nSectionCount); // prince 16
for (int i = 0; i < nSectionCount; i++, pmySectionHeader++, pdwVA++, pdwRowA++)
{
*pdwVA = pmySectionHeader->VirtualAddress;
*pdwRowA = pmySectionHeader->PointerToRawData;
}
pmySectionHeader -= nSectionCount;
pdwVA -= nSectionCount;
pdwRowA -= nSectionCount;
for (i = 0; i < nSectionCount; i++, pdwVA++, pdwRowA++)
{
if ((dwFileOffset >= *pdwVA) && (dwFileOffset < *(pdwVA + 1)))
{
dwOffset = *pdwVA - *pdwRowA;
dwFilePos = dwFileOffset - dwOffset;
pdwVA -= i;
pdwRowA -= i;
if (pdwVA != NULL) // release prince 15
{
free(pdwVA);
pdwVA = NULL;
}
if (pdwRowA != NULL) // release prince 16
{
free(pdwRowA);
pdwRowA = NULL;
}
if (pmySectionHeader != NULL) // release prince 14
{
free(pmySectionHeader);
pmySectionHeader = NULL;
}
return dwFilePos;
}
}
pdwVA -= nSectionCount;
pdwRowA -= nSectionCount;
if (pmySectionHeader != NULL) // release prince 14
{
free(pmySectionHeader);
pmySectionHeader = NULL;
}
if (pdwVA != NULL) // release prince 15
{
free(pdwVA);
pdwVA = NULL;
}
if (pdwRowA != NULL) // release prince 16
{
free(pdwRowA);
pdwRowA = NULL;
}
return -1;
}
void QuickSort(WORD *pwSourceData1, DWORD **pdwSourceData2, int nNumberOfArray)
{
SortRun(pwSourceData1, pdwSourceData2, 0, nNumberOfArray - 1);
}
void SortRun(WORD *pwSourceData1, DWORD **pdwSourceData2, int nLeft, int nRight)
{
WORD wMiddle, wTemp;
DWORD *pdwTemp;
int i, j;
wMiddle = pwSourceData1[(nLeft + nRight) / 2];
i = nLeft;
j = nRight;
while (i < j)
{
while ((pwSourceData1[i] < wMiddle) && (i < j))
{
i++;
}
while ((pwSourceData1[j] > wMiddle) && (j > i))
{
j--;
}
if (i <= j)
{
wTemp = pwSourceData1[i];
pwSourceData1[i] = pwSourceData1[j];
pwSourceData1[j] = wTemp;
pdwTemp = *(pdwSourceData2 + i);
*(pdwSourceData2 + i) = *(pdwSourceData2 + j);
*(pdwSourceData2 + j) = pdwTemp;
i++;
j--;
}
}
// 如果左边还有元素,对左边进行快速排序
if (nLeft < j)
{
SortRun(pwSourceData1, pdwSourceData2, 0, j);
}
// 如果右边还有元素,对右边进行快速排序
if (nRight > i)
{
SortRun(pwSourceData1, pdwSourceData2, i, nRight);
}
}
呼~,好累啊!也感谢你坚持看完。接下来的一篇就是大家最关心,也是最重要的输出表了,敬请关注下一篇:PE文件之旅 -- 加壳与脱壳的战场 输入表。
prince 2005.01.06
写好的WIN32程序:附件:PEExport.rar
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)