今天手撸一下加载PE文件,并执行加载的PE文件。看完这一节之后相信大家会对PE文件的结构和在内存中的加载顺序有一个比较深刻的理解。
本文中可能对PE文件的基础知识介绍的不是很详细,建议大家先看看PE文件的基础结构,了解了这些基础知识后再看本文会简单许多。废话不多说,下边让我们进入正是环节吧~
主要流程分为这么几步:
1、读取PE文件到内存中;
2、申请用于加载PE文件的内存;
3、复制PE文件的所有节表到内存中;
4、修复IAT表;
5、修复重定位表;
6、转换并执行PE文件的入口函数。
读取PE文件
这块太简单了,就不多说了,直接上代码:
#include <Windows.h>
HANDLE hFile = CreateFile(fileName,GENERIC_READ,FILE_SHARE_READ,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,0);
if (hFile)
{
DWORD dwFileSize = GetFileSize(hFile,NULL);
LPSTR fileData = new CHAR[dwFileSize];
if(fileData )
{
RtlZeroMemory(fileData, dwFileSize);
DWORD dwReadSize = 0;
ReadFile(hFile,fileData ,dwFileSize,&dwReadSize,NULL);
}
CloseHandle(hFile);
hFile = NULL;
}
申请内存
申请内存之前我们需要先解析PE文件中的DOS头和NT头(PS:不知道这是啥的同学,强烈建议去翻翻PE基础结构的知识,务必!务必!务必!),从中获取到PE文件加载后的大小。
我们使用VirtualAlloc函数申请内存,获取到申请的内存地址后,需要将PE的DOS头和NT头复制到此块内存中,这也是PE文件加载的基础。
#include <Windows.h>
#include <winternl.h>
BOOL AllocateMemory()
{
//这里的fileData就是第一步加载的PE文件数据
//Dos头
PIMAGE_DOS_HEADER DosHeader = (PIMAGE_DOS_HEADER)fileData;
//Nt头
PIMAGE_NT_HEADERS NtHeaders = (PIMAGE_NT_HEADERS)((ULONG_PTR)fileData + DosHeader->e_lfanew);
//申请内存,返回的是申请到内存地址
PVOID ImageBase = VirtualAlloc(NULL, NtHeaders->OptionalHeader.SizeOfImage,MEM_COMMIT|MEM_RESERVE,PAGE_EXECUTE_READWRITE);
if (!ImageBase)
return FALSE;
//dos头、nt头和节表头复制到新申请的内存中
RtlCopyMemory(ImageBase,DosHeader,NtHeaders->OptionalHeader.SizeOfHeaders);
return TRUE;
}
复制节表到内存中
要想复制PE文件中的所有节表,我们需要知道两个数据,一个是节表的大小,另一个则是第一个节表的地址。找第一个节表的地址时,我们可以使用微软提供的宏IMAGE_FIRST_SECTION,传参就是我们的NT头指针。当然,我们也可以手动查找,就在NT头的后边。
找到所有节表之后,我们需要循环将节表的数据复制到我们申请的内存中。
#include <Windows.h>
#include <winternl.h>
VOID CopyAllSections()
{
//第一个节表
PIMAGE_SECTION_HEADER section_header = IMAGE_FIRST_SECTION(NtHeaders);
//节表的大小
DWORD dwSectionsSize = NtHeaders->FileHeader.NumberOfSections;
for (DWORD i = 0; i < dwSectionsSize; i++)
{
RtlCopyMemory((PVOID)((ULONG_PTR)ImageBase+ section_header->VirtualAddress),(PVOID)((ULONG_PTR)DosHeader + section_header->PointerToRawData),
section_header->SizeOfRawData);
section_header++;
}
//DOS头
PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)ImageBase;
//保存申请内存中的NT头
PIMAGE_NT_HEADERS MemNtHeaders = (PIMAGE_NT_HEADERS)((ULONG_PTR)pDosHeader + pDosHeader->e_lfanew);
//PE文件的入口地址
ULONG_PTR EntryPointer = (ULONG_PTR)ImageBase + MemNtHeaders->OptionalHeader.AddressOfEntryPoint;
return VOID();
}
修复IAT表
好了,我们正式进入修复IAT表的阶段,这是加载PE文件两个重要过程之一。
IAT表也被叫做导入表,它包含了运行这个PE文件需要用到的库(dll),由于我们的基址已经改变了,所以我们需要将用到的库函数地址进行修复,使其正确的指向要调用的函数,不然会导致PE文件无法正常加载。
导入表从上到下可以分为两层,库名-函数名。
简单解析一下就是,一个PE文件包含多个库,每个库都包含多个函数。
1、我们可以从NT头的目录结构中找到导入表的地址。
2、需要注意的是,PE文件加载导入表的时候有两种情况,一种是根据函数序号获取函数地址,一种是根据函数名获取函数地址。而判断究竟使用了哪种方式。主要是根据OriginalFirstThunk字段的最高位判断。
BOOL RepairIAT()
{
//MemNtHeaders 内存中的NT头数据
if (MemNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].Size == 0)
return TRUE;
#ifdef _WIN64
union
{
struct
{
ULONGLONG low : 63;
ULONGLONG high : 1;
}BitField;
ULONGLONG Value;
}Temp = { 0 };
#else
union
{
struct
{
ULONG low : 31;
ULONG high : 1;
}BitField;
ULONG Value;
}Temp = { 0 };
#endif
PIMAGE_IMPORT_DESCRIPTOR pIID = (PIMAGE_IMPORT_DESCRIPTOR)((ULONG_PTR)ImageBase+ MemNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);
while (pIID->Name)
{
PIMAGE_THUNK_DATA pITD = (PIMAGE_THUNK_DATA)((ULONG_PTR)ImageBase+ pIID->OriginalFirstThunk);
//记录真是函数地址的字段
PULONG_PTR pFuncAddr = (PULONG_PTR)((ULONG_PTR)ImageBase + pIID->FirstThunk);
while (*pFuncAddr != 0)
{
HMODULE hModule = LoadLibraryA((LPSTR)((ULONG_PTR)ImageBase + pIID->Name));
if (!hModule)
return FALSE;
Temp.Value = pITD->u1.AddressOfData;
//根据函数序号获取函数地址
if (Temp.BitField.high == 1)
{
//修复函数地址
*pFuncAddr = (ULONG_PTR)GetProcAddress(hModule, (LPCSTR)Temp.BitField.low);
}
//根据函数名获取函数地址
else
{
//函数名
PIMAGE_IMPORT_BY_NAME pFuncName = (PIMAGE_IMPORT_BY_NAME)((ULONG_PTR)ImageBase+ pITD->u1.AddressOfData);
//修复函数地址
*pFuncAddr = (ULONG_PTR)GetProcAddress(hModule, pFuncName->Name);
}
pITD++;
pFuncAddr++;
}
pIID++;
}
return TRUE;
}
修复重定位表
对于可执行文件来说,一般没有重定位表,而对于动态库(dll)文件来说,基本都会有重定位表。重定位表就是对导出函数的地址修正。
要修复重定位表,需要了解它的结构,一般是一个头+一组数据。
BOOL RepairReloc()
{
//MemNtHeaders 内存中Nt头数据
if (MemNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].Size == 0)
{
return TRUE;
}
//一个重定位的数据是2字节
typedef struct BASE_RELOCATION_ENTRY
{
USHORT offset : 12;
USHORT type : 4;
}BASE_RELOCATION_ENTRY,*PBASE_RELOCATION_ENTRY;
//NtHeaders是PE文件中的Nt头
//这里是为了计算我们内存中加载PE文件的基址和PE文件默认基址的一个偏移
INT_PTR offset = (INT_PTR)((ULONG_PTR)ImageBase - NtHeaders->OptionalHeader.ImageBase);
PIMAGE_BASE_RELOCATION pIBR = (PIMAGE_BASE_RELOCATION)((ULONG_PTR)ImageBase+m_MemNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress);
while (pIBR->VirtualAddress != 0)
{
//重定位表的一个数据
PBASE_RELOCATION_ENTRY pBlock = (PBASE_RELOCATION_ENTRY)((ULONG_PTR)pIBR+sizeof(IMAGE_BASE_RELOCATION));
//一组重定位数据的个数
DWORD NumberOfBlocks = (pIBR->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(BASE_RELOCATION_ENTRY);
for (DWORD i = 0; i < NumberOfBlocks; i++, pBlock++)
{
//类型是IMAGE_REL_BASED_ABSOLUTE的不需要进行重定位
if (pBlock->type != IMAGE_REL_BASED_ABSOLUTE)
{
//需要重定位的地址
PINT_PTR RepairAddr = (PINT_PTR)((ULONG_PTR)ImageBase + pIBR->VirtualAddress + pBlock->offset);
if (*RepairAddr <= 0)
return FALSE;
//重定位
*RepairAddr += offset;
}
}
pIBR = (PIMAGE_BASE_RELOCATION)((ULONG_PTR)pIBR + pIBR->SizeOfBlock);
}
return TRUE;
}
转换函数入口
从第二步中我们可以得到函数入口地址,直接执行即可。这是对于可执行文件而言的,如果是动态库文件,我们需要转换一下
VOID CallEntryPoint()
{
if (IsDllFile())
{
typedef BOOL(APIENTRY *_DllMain)(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved);
//EntryPointer 是第二步获取到的函数地址
((_DllMain)EntryPointer)((HMODULE)ImageBase, DLL_PROCESS_ATTACH,NULL);
//如果动态库中有导出函数,则可以使用这个转换一下,获取导出函数地址(MemGetProcAddress)的下边再说
typedef int(*_fntestdll)(void);
_fntestdll fntestdll = (_fntestdll)MemGetProcAddress("fntestdll");
fntestdll();
}
else
{
((void(*)())(EntryPointer))();
}
return VOID();
}
下边我们说一下如何获取动态库中导出函数,简单的原理就是从修复完的PE文件内存中解析导出表,然后从导出表中获取相应的函数。
其中我们需要知道导出表的三个地址:函数名地址、导出函数序号地址、导出函数地址
导出函数序号地址记录的是相应函数名的函数序号,再根据函数序号从导出函数地址中获取函数的地址。
原因嘛,导出函数地址是按函数序号从小到大顺序记录的,但是函数名地址中保存地址和导出函数地址顺序是不一样的。一般函数名地址排序方式是按首字母排序的。
PVOID MemGetProcAddress(LPCSTR funcName)
{
if (MemNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].Size == 0)
return NULL;
PIMAGE_EXPORT_DIRECTORY pIED = (PIMAGE_EXPORT_DIRECTORY)((ULONG_PTR)ImageBase + m_MemNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
PDWORD pNameAddress = (PDWORD)((ULONG_PTR)ImageBase + pIED->AddressOfNames);
PDWORD pFuncAddress = (PDWORD)((ULONG_PTR)ImageBase + pIED->AddressOfFunctions);
PWORD pNameOrdinalsAddress = (PWORD)((ULONG_PTR)ImageBase + pIED->AddressOfNameOrdinals);
for (DWORD i = 0; i < pIED->NumberOfNames; i++)
{
if (strcmp(funcName, (LPCSTR)(ULONG_PTR)ImageBase + *pNameAddress) == 0)
{
return (PVOID)((ULONG_PTR)ImageBase + pFuncAddress[pNameOrdinalsAddress[i]]);
}
pNameAddress++;
}
return PVOID();
}
好了,手动加载PE文件并执行的全部流程就到此为止了,刚开始的时候大家可能感觉理解起来有点费劲,但是熟悉了之后就感觉还好,为了方便大家,下边给大家附上全部代码(PS:使用C++写的)
DealPEFile.h
#pragma once
#include <Windows.h>
#include <winternl.h>
class DealPEFile
{
public:
DealPEFile(LPCWSTR fileName);
~DealPEFile();
VOID LoadMemory();
private:
//判断是否是PE文件
BOOL IsValidPE();
//申请内存
BOOL AllocateMemory();
//拷贝所有节表到内存中
VOID CopyAllSections();
//修复IAT表
BOOL RepairIAT();
//修复重定位表
BOOL RepairReloc();
//获取导出函数
PVOID MemGetProcAddress(LPCSTR funcName);
//判断是否是Dll文件
BOOL IsDllFile();
//执行PE文件的入口函数
VOID CallEntryPoint(DealPEFile* pDealPEFile);
private:
//文件数据
LPSTR m_fileData;
//PE基地址
PVOID m_ImageBase;
//Dos头
PIMAGE_DOS_HEADER m_DosHeader;
//Nt头
PIMAGE_NT_HEADERS m_NtHeaders;
//内存中Nt头
PIMAGE_NT_HEADERS m_MemNtHeaders;
//PE文件入口地址
ULONG_PTR m_EntryPointer;
};
DealPEFile.cpp
#include "DealPEFile.h"
DealPEFile::DealPEFile(LPCWSTR fileName)
{
m_fileData = NULL;
m_DosHeader = NULL;
m_NtHeaders = NULL;
if (fileName != NULL && fileName[0] != L'\0')
{
HANDLE hFile = CreateFile(fileName,GENERIC_READ,FILE_SHARE_READ,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,0);
if (hFile)
{
DWORD dwFileSize = GetFileSize(hFile,NULL);
m_fileData = new CHAR[dwFileSize];
RtlZeroMemory(m_fileData, dwFileSize);
DWORD dwReadSize = 0;
ReadFile(hFile,m_fileData,dwFileSize,&dwReadSize,NULL);
CloseHandle(hFile);
hFile = NULL;
}
if (m_fileData)
{
m_DosHeader = (PIMAGE_DOS_HEADER)m_fileData;
m_NtHeaders = (PIMAGE_NT_HEADERS)((ULONG_PTR)m_DosHeader + m_DosHeader->e_lfanew);
}
}
}
DealPEFile::~DealPEFile()
{
if (m_fileData)
{
delete[] m_fileData;
m_fileData = NULL;
}
m_DosHeader = NULL;
m_NtHeaders = NULL;
}
VOID DealPEFile::LoadMemory()
{
if (m_fileData == NULL)
return;
//判断是否是ie文件,申请加载PE文件的内存
if (!IsValidPE() || !AllocateMemory())
return;
//复制所有节表到内存中
CopyAllSections();
//修复IAT表
if (!RepairIAT())
return;
//修复重定位表
if (!RepairReloc())
return;
//执行PE文件的入口函数
CallEntryPoint(this);
return;
}
BOOL DealPEFile::IsValidPE()
{
if (m_DosHeader->e_magic != IMAGE_DOS_SIGNATURE || m_NtHeaders->Signature != IMAGE_NT_SIGNATURE)
return FALSE;
#ifdef _WIN64
if (m_NtHeaders->FileHeader.Machine != IMAGE_FILE_MACHINE_AMD64)
{
return FALSE;
}
#else
if (m_NtHeaders->FileHeader.Machine != IMAGE_FILE_MACHINE_I386)
{
return FALSE;
}
#endif
return TRUE;
}
BOOL DealPEFile::AllocateMemory()
{
m_ImageBase = VirtualAlloc(NULL, m_NtHeaders->OptionalHeader.SizeOfImage,MEM_COMMIT|MEM_RESERVE,PAGE_EXECUTE_READWRITE);
if (!m_ImageBase)
return FALSE;
RtlCopyMemory(m_ImageBase,m_DosHeader,m_NtHeaders->OptionalHeader.SizeOfHeaders);
return TRUE;
}
//拷贝所有节表到内存中
VOID DealPEFile::CopyAllSections()
{
PIMAGE_SECTION_HEADER section_header = IMAGE_FIRST_SECTION(m_NtHeaders);
//节表数量
DWORD dwSectionsSize = m_NtHeaders->FileHeader.NumberOfSections;
for (DWORD i = 0; i < dwSectionsSize; i++)
{
RtlCopyMemory((PVOID)((ULONG_PTR)m_ImageBase+ section_header->VirtualAddress),(PVOID)((ULONG_PTR)m_DosHeader + section_header->PointerToRawData),
section_header->SizeOfRawData);
section_header++;
}
PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)m_ImageBase;
m_MemNtHeaders = (PIMAGE_NT_HEADERS)((ULONG_PTR)pDosHeader + pDosHeader->e_lfanew);
m_EntryPointer = (ULONG_PTR)m_ImageBase + m_MemNtHeaders->OptionalHeader.AddressOfEntryPoint;
return VOID();
}
//修复IAT表
BOOL DealPEFile::RepairIAT()
{
if (m_MemNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].Size == 0)
return FALSE;
#ifdef _WIN64
union
{
struct
{
ULONGLONG low : 63;
ULONGLONG high : 1;
}BitField;
ULONGLONG Value;
}Temp = { 0 };
#else
union
{
struct
{
ULONG low : 31;
ULONG high : 1;
}BitField;
ULONG Value;
}Temp = { 0 };
#endif
//导入表
PIMAGE_IMPORT_DESCRIPTOR pIID = (PIMAGE_IMPORT_DESCRIPTOR)((ULONG_PTR)m_ImageBase+ m_MemNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);
while (pIID->Name)
{
PIMAGE_THUNK_DATA pITD = (PIMAGE_THUNK_DATA)((ULONG_PTR)m_ImageBase+ pIID->OriginalFirstThunk);
//函数地址
PULONG_PTR pFuncAddr = (PULONG_PTR)((ULONG_PTR)m_ImageBase + pIID->FirstThunk);
while (*pFuncAddr != 0)
{
HMODULE hModule = LoadLibraryA((LPSTR)((ULONG_PTR)m_ImageBase + pIID->Name));
if (!hModule)
return FALSE;
Temp.Value = pITD->u1.AddressOfData;
if (Temp.BitField.high == 1)
{
*pFuncAddr = (ULONG_PTR)GetProcAddress(hModule, (LPCSTR)Temp.BitField.low);
}
else
{
PIMAGE_IMPORT_BY_NAME pFuncName = (PIMAGE_IMPORT_BY_NAME)((ULONG_PTR)m_ImageBase+ pITD->u1.AddressOfData);
*pFuncAddr = (ULONG_PTR)GetProcAddress(hModule, pFuncName->Name);
}
pITD++;
pFuncAddr++;
}
pIID++;
}
return TRUE;
}
//修复重定位表
BOOL DealPEFile::RepairReloc()
{
if (m_MemNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].Size == 0)
{
return FALSE;
}
typedef struct BASE_RELOCATION_ENTRY
{
USHORT offset : 12;
USHORT type : 4;
}BASE_RELOCATION_ENTRY,*PBASE_RELOCATION_ENTRY;
//偏移
INT_PTR offset = (INT_PTR)((ULONG_PTR)m_ImageBase - m_NtHeaders->OptionalHeader.ImageBase);
//重定位表
PIMAGE_BASE_RELOCATION pIBR = (PIMAGE_BASE_RELOCATION)((ULONG_PTR)m_ImageBase+m_MemNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress);
while (pIBR->VirtualAddress != 0)
{
//重定位块数据
PBASE_RELOCATION_ENTRY pBlock = (PBASE_RELOCATION_ENTRY)((ULONG_PTR)pIBR+sizeof(IMAGE_BASE_RELOCATION));
//一块重定位数据的数量
DWORD NumberOfBlocks = (pIBR->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(BASE_RELOCATION_ENTRY);
for (DWORD i = 0; i < NumberOfBlocks; i++, pBlock++)
{
if (pBlock->type != IMAGE_REL_BASED_ABSOLUTE)
{
//修复地址
PINT_PTR RepairAddr = (PINT_PTR)((ULONG_PTR)m_ImageBase + pIBR->VirtualAddress + pBlock->offset);
if (*RepairAddr <= 0)
return FALSE;
*RepairAddr += offset;
}
}
pIBR = (PIMAGE_BASE_RELOCATION)((ULONG_PTR)pIBR + pIBR->SizeOfBlock);
}
return TRUE;
}
//获取导出函数
PVOID DealPEFile::MemGetProcAddress(LPCSTR funcName)
{
if (m_MemNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].Size == 0)
return NULL;
PIMAGE_EXPORT_DIRECTORY pIED = (PIMAGE_EXPORT_DIRECTORY)((ULONG_PTR)m_ImageBase + m_MemNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
//函数名地址
PDWORD pNameAddress = (PDWORD)((ULONG_PTR)m_ImageBase + pIED->AddressOfNames);
//函数地址
PDWORD pFuncAddress = (PDWORD)((ULONG_PTR)m_ImageBase + pIED->AddressOfFunctions);
//函数名序号地址
PWORD pNameOrdinalsAddress = (PWORD)((ULONG_PTR)m_ImageBase + pIED->AddressOfNameOrdinals);
for (DWORD i = 0; i < pIED->NumberOfNames; i++)
{
if (strcmp(funcName, (LPCSTR)(ULONG_PTR)m_ImageBase + *pNameAddress) == 0)
{
return (PVOID)((ULONG_PTR)m_ImageBase + pFuncAddress[pNameOrdinalsAddress[i]]);
}
pNameAddress++;
}
return PVOID();
}
//判断是否是Dll文件
BOOL DealPEFile::IsDllFile()
{
return m_MemNtHeaders->FileHeader.Characteristics & IMAGE_FILE_DLL;
}
//执行PE文件的入口函数
VOID DealPEFile::CallEntryPoint(DealPEFile* pDealPEFile)
{
if (IsDllFile())
{
typedef BOOL(APIENTRY *_DllMain)(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved);
((_DllMain)pDealPEFile->m_EntryPointer)((HMODULE)m_ImageBase, DLL_PROCESS_ATTACH,NULL);
typedef int(*_fntestdll)(void);
_fntestdll fntestdll = (_fntestdll)MemGetProcAddress("fntestdll");
fntestdll();
}
else
{
((void(*)())(pDealPEFile->m_EntryPointer))();
}
return VOID();
}
此代码加载的PE文件是我测试使用的,测试的代码也给大家附上
Test.exe
#include <Windows.h>
#pragma comment(linker,"/entry:Test")
int Test()
{
MessageBox(NULL,L"Test Exe!!!", L"提示", MB_OK);
return 0;
}
Test.dll
dllmain.cpp
// dllmain.cpp : 定义 DLL 应用程序的入口点。
#include <Windows.h>
BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
MessageBox(NULL,L"test dll Main",L"提示",MB_OK);
break;
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
testdll.h
#ifdef __cplusplus
extern "C" {
#endif
__declspec(dllexport) int fntestdll(void);
#ifdef __cplusplus
}
#endif
testdll.cpp
#include "testdll.h"
#include <Windows.h>
// 这是导出函数的一个示例。
int fntestdll(void)
{
MessageBox(NULL,L"fntestdll",L"提示",MB_OK);
return 42;
}